feat(ui): 提升界面响应式支持和移动端适配体验
- 新增移动端全屏对话框支持及标签宽度和位置动态调整,优化新增班级、年级和学生弹窗布局 - 所有对话框增加屏幕宽度监听,实现自动切换移动端和桌面端样式 - 表格组件增加移动端列表视图,隐藏侧边栏并改进分页和按钮自适应,提升小屏幕浏览体验 - Dialog及详情弹窗添加最大高度限制并启用滚动,防止移动端显示区域拥挤 - 登录页增加安全区域内边距,保证iOS等设备显示完整性 - 新增移动端菜单抽屉组件,支持手机端侧边栏交互显示 - 学生详情页调整词汇热力图列数,实现移动端更合理布局 - 表格和按钮统一增设触控友好大尺寸区域,提升移动端操作便利性 - 修正后端空词汇ID查询问题,避免空列表导致查询异常 - 统一隐藏小屏幕时的固定侧边栏,避免界面混乱和重复显示 - 搜索页和上传页表格添加移动端适配样式和展开收起逻辑,提升列表浏览灵活性
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>enlish-vue</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -3,12 +3,17 @@
|
||||
<template>
|
||||
<el-config-provider :locale="locale">
|
||||
<router-view />
|
||||
<el-drawer v-model="mobileSidebarOpen" class="md:hidden" title="菜单" size="260px">
|
||||
<Sidebar />
|
||||
</el-drawer>
|
||||
</el-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
const locale = zhCn
|
||||
import Sidebar from '@/layouts/components/Sidebar.vue'
|
||||
import { mobileSidebarOpen } from '@/composables/ui.js'
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -43,3 +43,34 @@ html, body, #app {
|
||||
radial-gradient(1000px at 90% 10%, rgba(2,132,199,0.3) 0%, transparent 40%),
|
||||
linear-gradient(180deg, #0f172a 0%, #111827 100%);
|
||||
}
|
||||
|
||||
.safe-area {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
}
|
||||
|
||||
.media-fluid img,
|
||||
.media-fluid video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.touch-target {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar-fixed {
|
||||
display: none;
|
||||
width: 0;
|
||||
min-width: 0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.panel-shell {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
12
enlish-vue/src/composables/ui.js
Normal file
12
enlish-vue/src/composables/ui.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const mobileSidebarOpen = ref(false)
|
||||
|
||||
export function openMobileSidebar() {
|
||||
mobileSidebarOpen.value = true
|
||||
}
|
||||
|
||||
export function closeMobileSidebar() {
|
||||
mobileSidebarOpen.value = false
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="新增班级" width="480px" :close-on-click-modal="false">
|
||||
<el-dialog v-model="visible" title="新增班级" width="480px" :fullscreen="isMobile" :close-on-click-modal="false">
|
||||
<div class="space-y-4" v-loading="loading">
|
||||
<el-form label-width="80px">
|
||||
<el-form :label-width="isMobile ? 0 : 80" :label-position="isMobile ? 'top' : 'right'">
|
||||
<el-form-item label="班级名称">
|
||||
<el-input v-model="name" placeholder="请输入班级名称,如:二班" clearable />
|
||||
<el-input v-model="name" placeholder="请输入班级名称,如:二班" clearable class="w-full" />
|
||||
</el-form-item>
|
||||
<el-form-item label="年级">
|
||||
<el-select v-model="gradeId" placeholder="请选择年级" style="width: 260px">
|
||||
<el-select v-model="gradeId" placeholder="请选择年级" class="w-full sm:w-[260px]">
|
||||
<el-option v-for="g in gradeOptions" :key="g.id" :label="g.title" :value="g.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||
<div class="footer-actions flex sm:justify-end gap-2 sm:flex-row flex-col">
|
||||
<el-button class="w-full sm:w-auto touch-target" @click="visible = false">取消</el-button>
|
||||
<el-button class="w-full sm:w-auto touch-target" type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { getGradeList } from '@/api/grade'
|
||||
import { addClass } from '@/api/class'
|
||||
|
||||
@@ -41,6 +41,7 @@ const loading = ref(false)
|
||||
const name = ref('')
|
||||
const gradeId = ref(null)
|
||||
const gradeOptions = ref([])
|
||||
const isMobile = ref(false)
|
||||
|
||||
const canSubmit = computed(() => name.value.trim().length > 0 && !!gradeId.value)
|
||||
|
||||
@@ -71,6 +72,10 @@ async function handleSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
function updateIsMobile() {
|
||||
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(v) => {
|
||||
@@ -78,9 +83,22 @@ watch(
|
||||
name.value = ''
|
||||
gradeId.value = props.defaultGradeId ? Number(props.defaultGradeId) : null
|
||||
fetchGrades()
|
||||
updateIsMobile()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
updateIsMobile()
|
||||
window.addEventListener('resize', updateIsMobile)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateIsMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.footer-actions :deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="新增年级" width="420px" :close-on-click-modal="false">
|
||||
<el-dialog v-model="visible" title="新增年级" width="420px" :fullscreen="isMobile" :close-on-click-modal="false">
|
||||
<div class="space-y-4" v-loading="loading">
|
||||
<el-input v-model="name" placeholder="请输入年级名称,如:一年级" clearable />
|
||||
<el-input v-model="name" placeholder="请输入年级名称,如:一年级" clearable class="w-full" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||
<div class="footer-actions flex sm:justify-end gap-2 sm:flex-row flex-col">
|
||||
<el-button class="w-full sm:w-auto touch-target" @click="visible = false">取消</el-button>
|
||||
<el-button class="w-full sm:w-auto touch-target" type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { addGrade } from '@/api/grade'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -29,6 +29,7 @@ const visible = computed({
|
||||
const loading = ref(false)
|
||||
const name = ref('')
|
||||
const canSubmit = computed(() => name.value.trim().length > 0)
|
||||
const isMobile = ref(false)
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!canSubmit.value) return
|
||||
@@ -43,12 +44,29 @@ async function handleSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
function updateIsMobile() {
|
||||
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(v) => {
|
||||
if (v) name.value = ''
|
||||
updateIsMobile()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
updateIsMobile()
|
||||
window.addEventListener('resize', updateIsMobile)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateIsMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.footer-actions :deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="新增学生" width="560px" :close-on-click-modal="false">
|
||||
<el-dialog v-model="visible" title="新增学生" width="560px" :fullscreen="isMobile" :close-on-click-modal="false" class="responsive-dialog">
|
||||
<div class="space-y-4" v-loading="loading">
|
||||
<el-form label-width="90px">
|
||||
<el-form :label-width="isMobile ? 0 : 90" :label-position="isMobile ? 'top' : 'right'">
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="name" placeholder="请输入学生姓名" clearable />
|
||||
<el-input v-model="name" placeholder="请输入学生姓名" clearable class="w-full" />
|
||||
</el-form-item>
|
||||
<el-form-item label="年级">
|
||||
<el-select v-model="gradeId" placeholder="请选择年级" style="width: 260px" @change="handleGradeChange">
|
||||
<el-select v-model="gradeId" placeholder="请选择年级" class="w-full sm:w-[260px]" @change="handleGradeChange">
|
||||
<el-option v-for="g in gradeOptions" :key="g.id" :label="g.title" :value="g.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="班级">
|
||||
<el-select v-model="classId" placeholder="请选择班级" style="width: 260px">
|
||||
<el-select v-model="classId" placeholder="请选择班级" class="w-full sm:w-[260px]">
|
||||
<el-option v-for="c in filteredClassOptions" :key="c.id" :label="c.title" :value="c.id" />
|
||||
</el-select>
|
||||
<div v-if="gradeId && filteredClassOptions.length === 0" class="mt-2 flex items-center gap-2">
|
||||
@@ -20,15 +20,15 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="入学时间">
|
||||
<el-date-picker v-model="startDate" type="datetime" placeholder="选择日期时间"
|
||||
<el-date-picker v-model="startDate" type="datetime" placeholder="选择日期时间" class="w-full"
|
||||
format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||
<div class="footer-actions flex sm:justify-end gap-2 sm:flex-row flex-col">
|
||||
<el-button class="w-full sm:w-auto touch-target" @click="visible = false">取消</el-button>
|
||||
<el-button class="w-full sm:w-auto touch-target" type="primary" :disabled="!canSubmit" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -36,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { getGradeList } from '@/api/grade'
|
||||
import { getClassList } from '@/api/class'
|
||||
import { addStudent } from '@/api/student'
|
||||
@@ -61,6 +61,7 @@ const classId = ref(null)
|
||||
const startDate = ref('')
|
||||
const gradeOptions = ref([])
|
||||
const classOptions = ref([])
|
||||
const isMobile = ref(false)
|
||||
|
||||
const filteredClassOptions = computed(() => {
|
||||
if (!gradeId.value) return []
|
||||
@@ -116,6 +117,10 @@ async function handleSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
function updateIsMobile() {
|
||||
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (v) => {
|
||||
@@ -126,9 +131,26 @@ watch(
|
||||
startDate.value = ''
|
||||
await fetchBaseOptions()
|
||||
if (gradeId.value) handleGradeChange()
|
||||
updateIsMobile()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
updateIsMobile()
|
||||
window.addEventListener('resize', updateIsMobile)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateIsMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.footer-actions :deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
.responsive-dialog :deep(.el-dialog__body) {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="词条记录详情" width="820px" :close-on-click-modal="false">
|
||||
<el-dialog v-model="visible" title="词条记录详情" width="820px" :fullscreen="isMobile" :close-on-click-modal="false" class="responsive-dialog">
|
||||
<div class="space-y-4" v-loading="loading">
|
||||
<el-card shadow="hover">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
@@ -79,7 +79,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { getExamWordsDetailResult } from '@/api/exam'
|
||||
import { getStudentDetail } from '@/api/student'
|
||||
import { getWordsListByIds } from '@/api/words'
|
||||
@@ -101,6 +101,7 @@ const activeNames = ref(['correct', 'wrong'])
|
||||
const correctTitles = ref([])
|
||||
const wrongTitles = ref([])
|
||||
const studentDetail = ref(null)
|
||||
const isMobile = ref(false)
|
||||
|
||||
async function fetchDetail() {
|
||||
if (!props.id && props.id !== 0) return
|
||||
@@ -149,10 +150,17 @@ async function fetchStudent() {
|
||||
studentDetail.value = res?.data?.data ?? null
|
||||
}
|
||||
|
||||
function updateIsMobile() {
|
||||
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(v) => {
|
||||
if (v) fetchDetail()
|
||||
if (v) {
|
||||
updateIsMobile()
|
||||
fetchDetail()
|
||||
}
|
||||
}
|
||||
)
|
||||
watch(
|
||||
@@ -161,6 +169,19 @@ watch(
|
||||
if (visible.value && v !== undefined && v !== null) fetchDetail()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
updateIsMobile()
|
||||
window.addEventListener('resize', updateIsMobile)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', updateIsMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.responsive-dialog :deep(.el-dialog__body) {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<button data-collapse-toggle="mobile-menu-2" type="button"
|
||||
<button data-collapse-toggle="mobile-menu-2" type="button" @click="openMobileSidebar()"
|
||||
class="inline-flex items-center p-2 ml-1 text-sm rounded-lg lg:hidden fluent-btn"
|
||||
aria-controls="mobile-menu-2" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
@@ -74,6 +74,7 @@ import { getUserInfo, logout } from '@/api/user'
|
||||
import { removeToken } from '@/composables/auth'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showMessage } from '@/composables/util.js'
|
||||
import { openMobileSidebar } from '@/composables/ui.js'
|
||||
const showLogin = ref(false)
|
||||
const userName = ref('')
|
||||
const menuOpen = ref(false)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<el-container class="pt-4">
|
||||
|
||||
<el-aside width="200px" class="">
|
||||
<el-aside width="200px" class="hidden md:block sidebar-fixed">
|
||||
<Sidebar />
|
||||
</el-aside>
|
||||
|
||||
@@ -19,40 +19,77 @@
|
||||
<el-button type="primary" @click="onSearch">查询</el-button>
|
||||
<el-button @click="onReset">重置</el-button>
|
||||
</div>
|
||||
<el-table ref="tableRef" :data="rows" border class="w-full" v-loading="loading" row-key="id">
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row }">
|
||||
<div class="p-3">
|
||||
<div class="text-sm font-semibold mb-2">学案</div>
|
||||
<el-table :data="row.plans || []" size="small" border>
|
||||
<el-table-column prop="title" label="标题" min-width="280" />
|
||||
<el-table-column label="状态" width="120">
|
||||
<template #default="{ row: plan }">
|
||||
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'"
|
||||
effect="plain">
|
||||
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row: plan }">
|
||||
<el-button type="primary" size="small"
|
||||
:loading="downloadingIds.includes(plan.id)"
|
||||
@click="onDownload(plan)">下载</el-button>
|
||||
<el-button class="ml-2" type="primary" size="small"
|
||||
:disabled="plan.isFinished === 1"
|
||||
:loading="finishingIds.includes(plan.id)"
|
||||
@click="onFinish(row.id, plan.id, plan)">完成</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="hidden sm:block overflow-x-auto">
|
||||
<el-table ref="tableRef" :data="rows" border class="min-w-[720px]" v-loading="loading" row-key="id">
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row }">
|
||||
<div class="p-3">
|
||||
<div class="text-sm font-semibold mb-2">学案</div>
|
||||
<div class="overflow-x-auto">
|
||||
<el-table :data="row.plans || []" size="small" border class="min-w-[600px]">
|
||||
<el-table-column prop="title" label="标题" min-width="280" />
|
||||
<el-table-column label="状态" width="120">
|
||||
<template #default="{ row: plan }">
|
||||
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'"
|
||||
effect="plain">
|
||||
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row: plan }">
|
||||
<el-button type="primary" size="small"
|
||||
:loading="downloadingIds.includes(plan.id)"
|
||||
@click="onDownload(plan)">下载</el-button>
|
||||
<el-button class="ml-2" type="primary" size="small"
|
||||
:disabled="plan.isFinished === 1"
|
||||
:loading="finishingIds.includes(plan.id)"
|
||||
@click="onFinish(row.id, plan.id, plan)">完成</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="姓名" min-width="140" />
|
||||
<el-table-column prop="className" label="班级" min-width="140" />
|
||||
<el-table-column prop="gradeName" label="年级" min-width="140" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="sm:hidden space-y-3">
|
||||
<div v-for="row in rows" :key="row.id" class="panel-shell p-4">
|
||||
<div class="text-base font-semibold mb-2">{{ row.name }}</div>
|
||||
<div class="text-sm text-gray-700 mb-1">班级:{{ row.className }}</div>
|
||||
<div class="text-sm text-gray-700 mb-3">年级:{{ row.gradeName }}</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="text-sm font-medium">学案</div>
|
||||
<el-button size="small" @click="toggleMobileExpand(row.id)">
|
||||
{{ mobileExpanded[row.id] ? '收起' : '展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="mobileExpanded[row.id]" class="mt-3 space-y-2">
|
||||
<div v-for="plan in (row.plans || [])" :key="plan.id"
|
||||
class="rounded-lg border border-white/30 bg-white/50 p-3">
|
||||
<div class="text-sm font-medium mb-2">{{ plan.title }}</div>
|
||||
<div class="mb-2">
|
||||
<el-tag :type="plan.isFinished === 1 ? 'success' : 'info'" effect="plain">
|
||||
{{ plan.isFinished === 1 ? '已完成' : '未完成' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<el-button type="primary" size="small"
|
||||
:loading="downloadingIds.includes(plan.id)"
|
||||
@click="onDownload(plan)">下载</el-button>
|
||||
<el-button type="primary" size="small"
|
||||
:disabled="plan.isFinished === 1"
|
||||
:loading="finishingIds.includes(plan.id)"
|
||||
@click="onFinish(row.id, plan.id, plan)">完成</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="姓名" min-width="120" />
|
||||
<el-table-column prop="className" label="班级" min-width="120" />
|
||||
<el-table-column prop="gradeName" label="年级" min-width="120" />
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<el-pagination background layout="prev, pager, next, sizes, total" :total="totalCount"
|
||||
:page-size="pageSize" :current-page="pageNo" @current-change="handlePageChange"
|
||||
@@ -84,6 +121,12 @@ const searchName = ref('')
|
||||
const tableRef = ref(null)
|
||||
const downloadingIds = ref([])
|
||||
const finishingIds = ref([])
|
||||
const mobileExpanded = ref({})
|
||||
|
||||
function toggleMobileExpand(id) {
|
||||
const v = mobileExpanded.value[id]
|
||||
mobileExpanded.value[id] = !v
|
||||
}
|
||||
|
||||
async function fetchLessonPlans() {
|
||||
loading.value = true
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<div
|
||||
class="min-h-screen relative flex items-center justify-center bg-fixed bg-cover bg-center bg-[url('https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg')]">
|
||||
class="min-h-screen relative flex items-center justify-center bg-scroll md:bg-fixed bg-cover bg-center bg-[url('https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg')] safe-area">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-[rgba(30,20,50,0.4)] to-[rgba(10,10,20,0.6)]"></div>
|
||||
|
||||
<div
|
||||
class="relative z-10 w-[400px] max-w-[92%] bg-white/10 backdrop-blur-2xl border border-white/20 rounded-2xl p-8 shadow-2xl text-white">
|
||||
class="relative z-10 w-full sm:w-[400px] max-w-[420px] sm:max-w-[92%] bg-white/10 backdrop-blur-2xl border border-white/20 rounded-2xl p-6 sm:p-8 shadow-2xl text-white">
|
||||
<div class="text-center mb-6">
|
||||
<h2 class="text-2xl font-semibold tracking-wide mb-1">Welcome Back</h2>
|
||||
<h2 class="text-xl sm:text-2xl font-semibold tracking-wide mb-1">Welcome Back</h2>
|
||||
<p class="text-sm text-white/80">智慧英语 · 让学习更简单</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mb-6 border-b border-white/10">
|
||||
<button class="px-5 py-2 text-white/70 hover:text-white transition"
|
||||
<button class="px-4 sm:px-5 py-2 text-white/70 hover:text-white transition"
|
||||
:class="mode === 'login' ? 'font-bold text-white border-b-2 border-white' : ''"
|
||||
@click="switchMode('login')">登录</button>
|
||||
<button class="px-5 py-2 text-white/70 hover:text-white transition"
|
||||
<button class="px-4 sm:px-5 py-2 text-white/70 hover:text-white transition"
|
||||
:class="mode === 'register' ? 'font-bold text-white border-b-2 border-white' : ''"
|
||||
@click="switchMode('register')">注册</button>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</el-header>
|
||||
|
||||
<el-container class="pt-4">
|
||||
<el-aside width="200px">
|
||||
<el-aside width="200px" class="hidden md:block sidebar-fixed">
|
||||
<Sidebar></Sidebar>
|
||||
</el-aside>
|
||||
|
||||
@@ -15,17 +15,28 @@
|
||||
<div class="lg:col-span-1 flex flex-col gap-6">
|
||||
<div class="panel-shell p-6">
|
||||
<div class="text-lg font-semibold mb-4">班级列表</div>
|
||||
<el-table ref="classTableRef" :data="classes" border class="w-full" v-loading="loading" highlight-current-row
|
||||
row-key="id" :current-row-key="selectedClassId" @row-click="onClassRowClick">
|
||||
<el-table-column prop="title" label="班级名称" min-width="120" />
|
||||
<el-table-column prop="gradeName" label="年级" min-width="120" />
|
||||
<div class="hidden sm:block overflow-x-auto">
|
||||
<el-table ref="classTableRef" :data="classes" border class="min-w-[520px]" v-loading="loading" highlight-current-row
|
||||
row-key="id" :current-row-key="selectedClassId" @row-click="onClassRowClick">
|
||||
<el-table-column prop="title" label="班级名称" min-width="160" />
|
||||
<el-table-column prop="gradeName" label="年级" min-width="120" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="danger" size="small"
|
||||
@click.stop="onDeleteClass(row)">删除</el-button>
|
||||
<el-button type="danger" size="small" @click.stop="onDeleteClass(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="sm:hidden space-y-3">
|
||||
<div v-for="row in classes" :key="row.id" class="panel-shell p-4">
|
||||
<div class="text-base font-semibold mb-1">{{ row.title }}</div>
|
||||
<div class="text-sm mb-2">年级:{{ row.gradeName }}</div>
|
||||
<div class="flex gap-2">
|
||||
<el-button size="small" type="primary" @click="onClassRowClick(row)">选择</el-button>
|
||||
<el-button size="small" type="danger" @click="onDeleteClass(row)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<el-pagination background layout="prev, pager, next, sizes, total"
|
||||
:total="totalCount" :page-size="pageSize" :current-page="pageNo"
|
||||
@@ -56,40 +67,61 @@
|
||||
生成学案
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table ref="studentTableRef" :data="students" border class="w-full"
|
||||
v-loading="studentLoading" @selection-change="onStudentSelectionChange">
|
||||
<el-table-column type="selection" width="48" />
|
||||
<el-table-column prop="name" label="姓名" min-width="120" />
|
||||
<el-table-column prop="className" label="班级" min-width="120" />
|
||||
<el-table-column prop="gradeName" label="年级" min-width="120" />
|
||||
<el-table-column prop="phone" label="学案" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<template v-if="generatingPercents[row.id] !== undefined">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-progress :percentage="generatingPercents[row.id]"
|
||||
:stroke-width="8" />
|
||||
</div>
|
||||
<div class="hidden sm:block overflow-x-auto">
|
||||
<el-table ref="studentTableRef" :data="students" border class="min-w-[760px]"
|
||||
v-loading="studentLoading" @selection-change="onStudentSelectionChange">
|
||||
<el-table-column type="selection" width="48" />
|
||||
<el-table-column prop="name" label="姓名" min-width="120" />
|
||||
<el-table-column prop="className" label="班级" min-width="120" />
|
||||
<el-table-column prop="gradeName" label="年级" min-width="120" />
|
||||
<el-table-column prop="phone" label="学案" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<template v-if="generatingPercents[row.id] !== undefined">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-progress :percentage="generatingPercents[row.id]" :stroke-width="8" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-button type="primary" size="small" @click="planStudentId = row.id; showPlanListDialog = true">查看学案</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-button type="primary" size="small"
|
||||
@click="planStudentId = row.id; showPlanListDialog = true">查看学案</el-button>
|
||||
</div>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="info" size="small" @click.stop="onShowAnalysis(row)">学情分析</el-button>
|
||||
<el-button type="primary" size="small" @click.stop="onViewStudent(row)">详情</el-button>
|
||||
<el-button type="danger" size="small" @click.stop="onDeleteStudent(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="sm:hidden space-y-3">
|
||||
<div v-for="row in students" :key="row.id" class="panel-shell p-4">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<div class="text-base font-semibold">{{ row.name }}</div>
|
||||
<el-checkbox
|
||||
:model-value="isStudentSelected(row.id)"
|
||||
@change="(val) => setStudentSelection(row.id, val)">
|
||||
选中
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<div class="text-sm mb-1">班级:{{ row.className }}</div>
|
||||
<div class="text-sm mb-2">年级:{{ row.gradeName }}</div>
|
||||
<template v-if="generatingPercents[row.id] !== undefined">
|
||||
<div class="mb-2">
|
||||
<el-progress :percentage="generatingPercents[row.id]" :stroke-width="8" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<!-- 学情分析 -->
|
||||
<el-button type="info" size="small"
|
||||
@click.stop="onShowAnalysis(row)">学情分析</el-button>
|
||||
<el-button type="primary" size="small"
|
||||
@click.stop="onViewStudent(row)">详情</el-button>
|
||||
<el-button type="danger" size="small"
|
||||
@click.stop="onDeleteStudent(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<el-button size="small" @click="planStudentId = row.id; showPlanListDialog = true">查看学案</el-button>
|
||||
<el-button size="small" type="info" @click="onShowAnalysis(row)">学情分析</el-button>
|
||||
<el-button size="small" type="primary" @click="onViewStudent(row)">详情</el-button>
|
||||
<el-button size="small" type="danger" @click="onDeleteStudent(row)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<el-pagination background layout="prev, pager, next, sizes, total"
|
||||
:total="studentTotalCount" :page-size="studentPageSize"
|
||||
@@ -113,10 +145,21 @@
|
||||
|
||||
<div class="panel-shell p-6" v-loading="gradeLoading">
|
||||
<div class="text-lg font-semibold mb-4">年级列表</div>
|
||||
<el-table ref="gradeTableRef" :data="grades" border class="w-full" highlight-current-row
|
||||
row-key="id" :current-row-key="selectedGradeId" @row-click="onGradeRowClick">
|
||||
<el-table-column prop="title" label="年级名称" min-width="160" />
|
||||
</el-table>
|
||||
<div class="hidden sm:block overflow-x-auto">
|
||||
<el-table ref="gradeTableRef" :data="grades" border class="min-w-[360px]" highlight-current-row
|
||||
row-key="id" :current-row-key="selectedGradeId" @row-click="onGradeRowClick">
|
||||
<el-table-column prop="title" label="年级名称" min-width="160" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="sm:hidden space-y-3">
|
||||
<div v-for="row in grades" :key="row.id" class="panel-shell p-4">
|
||||
<div class="text-base font-semibold mb-1">{{ row.title }}</div>
|
||||
<div class="flex gap-2">
|
||||
<el-button size="small" type="primary" @click="onGradeRowClick(row)">选择</el-button>
|
||||
<el-button size="small" type="danger" @click="onDeleteGrade(row)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<el-pagination background layout="prev, pager, next, sizes, total"
|
||||
:total="gradeTotalCount" :page-size="gradePageSize" :current-page="gradePageNo"
|
||||
@@ -190,6 +233,18 @@ const planStudentId = ref(null)
|
||||
const showAnalysisDialog = ref(false)
|
||||
const analysisStudentId = ref(null)
|
||||
|
||||
function isStudentSelected(id) {
|
||||
return selectedStudentIds.value.includes(id)
|
||||
}
|
||||
function setStudentSelection(id, selected) {
|
||||
const exists = selectedStudentIds.value.includes(id)
|
||||
if (selected && !exists) {
|
||||
selectedStudentIds.value = [...selectedStudentIds.value, id]
|
||||
} else if (!selected && exists) {
|
||||
selectedStudentIds.value = selectedStudentIds.value.filter(x => x !== id)
|
||||
}
|
||||
}
|
||||
|
||||
const units = ref([])
|
||||
const unitPageNo = ref(1)
|
||||
const unitPageSize = ref(10)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<el-container class="pt-4">
|
||||
<el-main class="h-full">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6" v-loading="loading">
|
||||
<div class="panel-shell p-6">
|
||||
<div class="panel-shell p-4 sm:p-6">
|
||||
<div class="text-lg font-semibold mb-4">学生详情</div>
|
||||
<template v-if="detail">
|
||||
<el-descriptions :column="1" border>
|
||||
@@ -23,7 +23,7 @@
|
||||
<el-empty description="请从班级页跳转" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="panel-shell p-6">
|
||||
<div class="panel-shell p-4 sm:p-6">
|
||||
<div class="text-lg font-semibold mb-4">学生词汇统计</div>
|
||||
<template v-if="wordStat">
|
||||
<el-descriptions :column="1" border>
|
||||
@@ -36,19 +36,19 @@
|
||||
<el-empty description="暂无统计" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="panel-shell p-6">
|
||||
<div class="panel-shell p-4 sm:p-6">
|
||||
<div class="text-md font-semibold mb-3">学生考试记录</div>
|
||||
<ExamHistoryChart :data="history" />
|
||||
</div>
|
||||
<div class="panel-shell p-6">
|
||||
<div class="panel-shell p-4 sm:p-6">
|
||||
<div class="text-md font-semibold mb-3">学生学案记录</div>
|
||||
<PlanHistoryChart :student-id="route.params.id" />
|
||||
</div>
|
||||
<div class="panel-shell p-6 lg:col-span-2">
|
||||
<div class="panel-shell p-4 sm:p-6 lg:col-span-2">
|
||||
<div class="text-md font-semibold mb-3">词汇掌握热力图</div>
|
||||
<WordMasteryHeatmap :student-id="route.params.id" :columns="50" />
|
||||
<WordMasteryHeatmap :student-id="route.params.id" :columns="heatmapColumns" />
|
||||
</div>
|
||||
<div class="panel-shell p-6 lg:col-span-2">
|
||||
<div class="panel-shell p-4 sm:p-6 lg:col-span-2">
|
||||
<div class="text-md font-semibold mb-3">学情分析</div>
|
||||
<StudyAnalysis :student-id="route.params.id" />
|
||||
</div>
|
||||
@@ -77,6 +77,12 @@ const detail = ref(null)
|
||||
const route = useRoute()
|
||||
const history = ref([])
|
||||
const wordStat = ref(null)
|
||||
const isMobile = ref(false)
|
||||
const heatmapColumns = computed(() => isMobile.value ? 20 : 50)
|
||||
|
||||
function updateIsMobile() {
|
||||
isMobile.value = window.matchMedia('(max-width: 768px)').matches
|
||||
}
|
||||
|
||||
async function fetchDetail() {
|
||||
const id = route.params.id
|
||||
@@ -117,5 +123,7 @@ onMounted(() => {
|
||||
fetchDetail()
|
||||
fetchExamHistory()
|
||||
fetchWordStat()
|
||||
updateIsMobile()
|
||||
window.addEventListener('resize', updateIsMobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</el-header>
|
||||
|
||||
<el-container class="pt-4">
|
||||
<el-aside width="200px" class="">
|
||||
<el-aside width="200px" class="hidden md:block sidebar-fixed">
|
||||
<Sidebar />
|
||||
</el-aside>
|
||||
<el-main class="">
|
||||
@@ -14,11 +14,12 @@
|
||||
<div class="panel-shell p-6">
|
||||
<div class="text-lg font-semibold mb-4">上传图片</div>
|
||||
<el-upload :show-file-list="false" :http-request="doUpload" accept="image/*">
|
||||
<el-button type="primary">选择图片并上传</el-button>
|
||||
<el-button type="primary" class="w-full sm:w-auto touch-target">选择图片并上传</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div class="panel-shell p-6">
|
||||
<div class="text-lg font-semibold mb-4">结果集</div>
|
||||
<div class="hidden sm:block">
|
||||
<el-form :inline="true" class="mb-4">
|
||||
<el-form-item label="班级">
|
||||
<el-select v-model="classId" placeholder="选择班级" clearable filterable
|
||||
@@ -43,26 +44,53 @@
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table :data="list" border class="w-full" v-loading="loading"
|
||||
@row-click="handleRowClick">
|
||||
<el-table-column prop="studentName" label="学生姓名" min-width="70" />
|
||||
<el-table-column prop="examWordsTitle" label="试题名称" min-width="100" />
|
||||
<el-table-column prop="correctWordCount" label="正确词数" width="110" />
|
||||
<el-table-column prop="wrongWordCount" label="错误词数" width="110" />
|
||||
<el-table-column label="完成状态" width="110">
|
||||
<template #default="{ row }">
|
||||
</div>
|
||||
<div class="hidden sm:block overflow-x-auto">
|
||||
<el-table :data="list" border class="min-w-[800px]" v-loading="loading"
|
||||
@row-click="handleRowClick">
|
||||
<el-table-column prop="studentName" label="学生姓名" min-width="120" />
|
||||
<el-table-column prop="examWordsTitle" label="试题名称" min-width="160" />
|
||||
<el-table-column prop="correctWordCount" label="正确词数" width="120" />
|
||||
<el-table-column prop="wrongWordCount" label="错误词数" width="120" />
|
||||
<el-table-column label="完成状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isFinished === 1 ? 'success' : 'info'">
|
||||
{{ row.isFinished === 1 ? '已完成' : '未完成' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="startDate" label="开始时间" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ row.startDate.replace('T', ' ') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="msg" label="判卷结算" min-width="180" />
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="sm:hidden space-y-3">
|
||||
<div class="mb-3 grid grid-cols-1 gap-3">
|
||||
<el-select v-model="classId" placeholder="选择班级" clearable filterable @change="onClassChange" />
|
||||
<el-select v-model="gradeId" placeholder="选择年级" clearable filterable />
|
||||
<el-input v-model="studentName" placeholder="学生姓名" clearable />
|
||||
<div class="flex gap-2">
|
||||
<el-button type="primary" class="flex-1" @click="handleSearch">查询</el-button>
|
||||
<el-button class="flex-1" @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="row in list" :key="row.id" class="panel-shell p-4">
|
||||
<div class="text-base font-semibold mb-1">{{ row.studentName }}</div>
|
||||
<div class="text-sm mb-1">试题:{{ row.examWordsTitle }}</div>
|
||||
<div class="text-sm mb-1">正确:{{ row.correctWordCount }},错误:{{ row.wrongWordCount }}</div>
|
||||
<div class="text-sm mb-1">开始:{{ row.startDate.replace('T', ' ') }}</div>
|
||||
<div class="text-sm mb-2">结算:{{ row.msg }}</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<el-tag :type="row.isFinished === 1 ? 'success' : 'info'">
|
||||
{{ row.isFinished === 1 ? '已完成' : '未完成' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="startDate" label="开始时间" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ row.startDate.replace('T', ' ') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="msg" label="判卷结算" min-width="160" />
|
||||
</el-table>
|
||||
<el-button size="small" type="primary" @click="handleRowClick(row)">查看详情</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<el-pagination background layout="prev, pager, next, sizes, total" :total="totalCount"
|
||||
:page-size="pageSize" :current-page="pageNo" @current-change="handlePageChange"
|
||||
|
||||
Reference in New Issue
Block a user