Files
en-edu/enlish-vue/src/pages/class.vue
lbw 065da854ee feat(exam): 支持按单个学生和考试类型生成考试试题
- 修改生成试题按钮仅在选中特定一个学生时可用,避免多选时误操作
- 在考试生成对话框新增“类型”选择项,支持“摸底”和“期中|期末”类型
- 调整后台接口,使用单个学生ID和考试类型替代学生ID列表参数
- 优化考试生成服务,新增摸底考试生成逻辑,按年级分区随机抽词汇
- 考试相关数据对象新增类型字段,保持数据完整性和一致性
- 修改考试判卷服务,将错误信息字段统一为msg,避免字段混淆
- 调整数据库操作,支持单个学生考试与词汇随机获取
- 同步更新测试用例和词汇库数据插入逻辑,确保环境一致性
- 修复界面生成按钮状态和对话框提交按钮的校验逻辑,提升用户体验
2025-12-18 17:21:37 +08:00

411 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="common-layout">
<el-container>
<el-header>
<Header></Header>
</el-header>
<el-main class="p-4">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="lg:col-span-1 flex flex-col gap-6">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow 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="id" label="ID" width="80" />
<el-table-column prop="title" label="班级名称" min-width="120" />
<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>
</template>
</el-table-column>
</el-table>
<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"
@size-change="handleSizeChange" />
</div>
<div class="mt-3 flex justify-end">
<el-button type="primary" @click="showAddClassDialog = true">新增班级</el-button>
</div>
<AddClassDialog v-model="showAddClassDialog" :default-grade-id="selectedGradeId"
@success="fetchClasses" />
</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 lg:col-span-1 lg:row-span-1">
<div class="text-lg font-semibold mb-4">学生查询</div>
<div class="flex flex-wrap items-center gap-3 mb-4">
<el-input v-model="studentName" placeholder="按姓名查询" clearable style="max-width: 220px" />
<el-tag v-if="selectedClassId" effect="plain">班级{{ selectedClassTitle }} (ID: {{
selectedClassId }})</el-tag>
<el-tag v-if="selectedGradeId" effect="plain">年级{{ selectedGradeTitle }} (ID: {{
selectedGradeId }})</el-tag>
<el-button type="primary" @click="fetchStudents">查询</el-button>
<el-button @click="resetStudentFilters">重置</el-button>
<el-button type="success" :disabled="selectedStudentIds.length !== 1"
@click="showGenerateDialog = true">
生成试题
</el-button>
<el-button type="warning" :disabled="selectedStudentIds.length !== 1"
@click="showLessonPlanDialog = true">
生成学案
</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="id" label="ID" width="80" />
<el-table-column prop="name" label="姓名" min-width="120" />
<el-table-column prop="classId" label="班级ID" width="100" />
<el-table-column prop="gradeId" label="年级ID" width="100" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<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="mt-4 flex justify-end">
<el-pagination background layout="prev, pager, next, sizes, total"
:total="studentTotalCount" :page-size="studentPageSize" :current-page="studentPageNo"
@current-change="handleStudentPageChange" @size-change="handleStudentSizeChange" />
</div>
<ExamGenerateDialog v-model="showGenerateDialog" :student-ids="selectedStudentIds"
:default-grade-id="selectedGradeId" />
<div class="mt-3 flex justify-end">
<el-button type="primary" @click="showAddStudentDialog = true">新增学生</el-button>
</div>
<AddStudentDialog
v-model="showAddStudentDialog"
:default-class-id="selectedClassId"
:default-grade-id="selectedGradeId"
@success="fetchStudents"
/>
<LessonPlanDialog
v-model="showLessonPlanDialog"
:student-id="selectedStudentIds[0]"
/>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow 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="id" label="ID" width="80" />
<el-table-column prop="title" label="年级名称" min-width="160" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="danger" size="small"
@click.stop="onDeleteGrade(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="mt-4 flex justify-end">
<el-pagination background layout="prev, pager, next, sizes, total" :total="gradeTotalCount"
:page-size="gradePageSize" :current-page="gradePageNo"
@current-change="handleGradePageChange" @size-change="handleGradeSizeChange" />
</div>
<div class="mt-3 flex justify-end">
<el-button type="primary" @click="showAddGradeDialog = true">新增年级</el-button>
</div>
<AddGradeDialog v-model="showAddGradeDialog" @success="fetchGrades" />
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6" v-loading="unitLoading">
<div class="text-lg font-semibold mb-4">单元列表</div>
<el-table ref="unitTableRef" :data="units" border class="w-full">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="单元名称" min-width="200" />
<el-table-column prop="version" label="版本" min-width="120" />
<el-table-column prop="createAt" label="创建时间" min-width="160" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="danger" size="small" @click.stop="onDeleteUnit(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="mt-4 flex justify-end">
<el-pagination
background
layout="prev, pager, next, sizes, total"
:total="unitTotalCount"
:page-size="unitPageSize"
:current-page="unitPageNo"
@current-change="handleUnitPageChange"
@size-change="handleUnitSizeChange"
/>
</div>
<div class="mt-3 flex justify-end">
<el-button type="primary" :disabled="!selectedGradeId" @click="showAddUnitDialog = true">新增单元</el-button>
</div>
<AddUnitDialog
v-model="showAddUnitDialog"
:default-grade-id="selectedGradeId"
@success="fetchUnits"
/>
</div>
</div>
</el-main>
</el-container>
</div>
</template>
<script setup>
import Header from '@/layouts/components/Header.vue'
import { ref, onMounted } from 'vue'
import { getClassList, deleteClass } from '@/api/class'
import { getGradeList, deleteGrade } from '@/api/grade'
import { getStudentList, deleteStudent } from '@/api/student'
import ExamGenerateDialog from '@/layouts/components/ExamGenerateDialog.vue'
import AddClassDialog from '@/layouts/components/AddClassDialog.vue'
import AddGradeDialog from '@/layouts/components/AddGradeDialog.vue'
import AddStudentDialog from '@/layouts/components/AddStudentDialog.vue'
import LessonPlanDialog from '@/layouts/components/LessonPlanDialog.vue'
import { getUnitList, deleteUnit } from '@/api/unit'
import AddUnitDialog from '@/layouts/components/AddUnitDialog.vue'
import { useRouter } from 'vue-router'
const classes = ref([])
const pageNo = ref(1)
const pageSize = ref(10)
const totalCount = ref(0)
const loading = ref(false)
const classTableRef = ref(null)
const selectedClassId = ref(null)
const selectedClassTitle = ref('')
const showAddClassDialog = ref(false)
const grades = ref([])
const gradePageNo = ref(1)
const gradePageSize = ref(10)
const gradeTotalCount = ref(0)
const gradeLoading = ref(false)
const gradeTableRef = ref(null)
const selectedGradeId = ref(null)
const selectedGradeTitle = ref('')
const showAddGradeDialog = ref(false)
const students = ref([])
const studentPageNo = ref(1)
const studentPageSize = ref(10)
const studentTotalCount = ref(0)
const studentLoading = ref(false)
const studentName = ref('')
const studentTableRef = ref(null)
const selectedStudentIds = ref([])
const showGenerateDialog = ref(false)
const showAddStudentDialog = ref(false)
const showLessonPlanDialog = ref(false)
const units = ref([])
const unitPageNo = ref(1)
const unitPageSize = ref(10)
const unitTotalCount = ref(0)
const unitLoading = ref(false)
const unitTableRef = ref(null)
const showAddUnitDialog = ref(false)
const router = useRouter()
async function fetchClasses() {
loading.value = true
try {
const res = await getClassList(pageNo.value, pageSize.value)
const d = res.data
classes.value = Array.isArray(d.data) ? d.data : []
totalCount.value = d.totalCount || 0
pageNo.value = d.pageNo || pageNo.value
pageSize.value = d.pageSize || pageSize.value
} finally {
loading.value = false
}
}
async function fetchGrades() {
gradeLoading.value = true
try {
const res = await getGradeList(gradePageNo.value, gradePageSize.value)
const d = res.data
grades.value = Array.isArray(d.data) ? d.data : []
gradeTotalCount.value = d.totalCount || 0
gradePageNo.value = d.pageNo || gradePageNo.value
gradePageSize.value = d.pageSize || gradePageSize.value
} finally {
gradeLoading.value = false
}
}
async function fetchStudents() {
studentLoading.value = true
try {
const payload = {
name: studentName.value || null,
classId: selectedClassId.value ?? null,
gradeId: selectedGradeId.value ?? null,
pageNo: studentPageNo.value,
pageSize: studentPageSize.value
}
const res = await getStudentList(payload)
const d = res.data
students.value = Array.isArray(d.data) ? d.data : []
studentTotalCount.value = d.totalCount || 0
studentPageNo.value = d.pageNo || studentPageNo.value
studentPageSize.value = d.pageSize || studentPageSize.value
} finally {
studentLoading.value = false
}
}
function handlePageChange(p) {
pageNo.value = p
fetchClasses()
}
function handleSizeChange(s) {
pageSize.value = s
pageNo.value = 1
fetchClasses()
}
function handleGradePageChange(p) {
gradePageNo.value = p
fetchGrades()
}
function handleGradeSizeChange(s) {
gradePageSize.value = s
gradePageNo.value = 1
fetchGrades()
}
function handleStudentPageChange(p) {
studentPageNo.value = p
fetchStudents()
}
function handleStudentSizeChange(s) {
studentPageSize.value = s
studentPageNo.value = 1
fetchStudents()
}
function onStudentSelectionChange(rows) {
selectedStudentIds.value = rows.map(r => r.id)
}
function onViewStudent(row) {
router.push(`/student/${row.id}`)
}
function onClassRowClick(row) {
selectedClassId.value = row.id
selectedClassTitle.value = row.title
classTableRef.value?.setCurrentRow(row)
studentPageNo.value = 1
fetchStudents()
}
function onGradeRowClick(row) {
selectedGradeId.value = row.id
selectedGradeTitle.value = row.title
gradeTableRef.value?.setCurrentRow(row)
studentPageNo.value = 1
fetchStudents()
}
async function onDeleteStudent(row) {
try {
await deleteStudent(row.id)
ElMessage.success('删除成功')
if (selectedStudentIds.value?.length) {
selectedStudentIds.value = selectedStudentIds.value.filter(id => id !== row.id)
studentTableRef.value?.toggleRowSelection(row, false)
}
await fetchStudents()
} catch (e) {
ElMessage.error('删除失败')
}
}
async function onDeleteClass(row) {
try {
await deleteClass(row.id)
ElMessage.success('删除成功')
if (selectedClassId.value === row.id) {
selectedClassId.value = null
selectedClassTitle.value = ''
classTableRef.value?.setCurrentRow()
}
await fetchClasses()
} catch (e) {
ElMessage.error('删除失败')
}
}
function resetStudentFilters() {
studentName.value = ''
selectedClassId.value = null
selectedClassTitle.value = ''
selectedGradeId.value = null
selectedGradeTitle.value = ''
classTableRef.value?.setCurrentRow()
gradeTableRef.value?.setCurrentRow()
studentPageNo.value = 1
fetchStudents()
}
async function onDeleteGrade(row) {
try {
await deleteGrade(row.id)
ElMessage.success('删除成功')
if (selectedGradeId.value === row.id) {
selectedGradeId.value = null
selectedGradeTitle.value = ''
gradeTableRef.value?.setCurrentRow()
}
await fetchGrades()
} catch (e) {
ElMessage.error('删除失败')
}
}
async function fetchUnits() {
unitLoading.value = true
try {
const res = await getUnitList(unitPageNo.value, unitPageSize.value)
const d = res.data
units.value = Array.isArray(d.data) ? d.data : []
unitTotalCount.value = d.totalCount || 0
unitPageNo.value = d.pageNo || unitPageNo.value
unitPageSize.value = d.pageSize || unitPageSize.value
} finally {
unitLoading.value = false
}
}
function handleUnitPageChange(p) {
unitPageNo.value = p
fetchUnits()
}
function handleUnitSizeChange(s) {
unitPageSize.value = s
unitPageNo.value = 1
fetchUnits()
}
async function onDeleteUnit(row) {
try {
const res = await deleteUnit(row.id)
const data = res.data
console.log(data)
if (data.success) {
ElMessage.success('删除成功')
} else {
ElMessage.error(data.message || '删除失败')
}
await fetchUnits()
} catch (e) {
ElMessage.error('删除失败')
}
}
onMounted(() => {
fetchClasses()
fetchGrades()
fetchStudents()
fetchUnits()
})
</script>