From 6d9fe834182dd9cc1e8f40c27c0c1bc76833eebb Mon Sep 17 00:00:00 2001 From: qinzhenpen Date: Tue, 29 Sep 2026 17:39:05 +0800 Subject: [PATCH] feat: Implement pagination for question list and update UI components Added front-end pagination to the question list, allowing users to navigate through questions more efficiently. Updated the question loading function to reset the page on new queries. Additionally, modified the UI to hide the project selection temporarily and improved the layout of the question table and pagination controls for better user experience. --- src/views/teacherEnd/exam/index.vue | 30 ++++++++++++++++++--- src/views/teacherEnd/trainingTask/index.vue | 23 +++++++++++++--- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/views/teacherEnd/exam/index.vue b/src/views/teacherEnd/exam/index.vue index 4518a7a..adc0c63 100644 --- a/src/views/teacherEnd/exam/index.vue +++ b/src/views/teacherEnd/exam/index.vue @@ -28,7 +28,8 @@ - + + @@ -58,7 +59,7 @@ - + @@ -78,6 +79,16 @@ +
+ +
@@ -130,6 +141,12 @@ const typeName = Object.fromEntries(typeOptions.map((item) => [item.value, item. const typeTone = { SINGLE: "", MULTIPLE: "success", TRUE_FALSE: "warning", SHORT_ANSWER: "info" }; const statusMeta = { DRAFT: { label: "未发布", tone: "info" }, NOT_STARTED: { label: "未开始", tone: "" }, IN_PROGRESS: { label: "进行中", tone: "success" }, ENDED: { label: "已结束", tone: "warning" } }; const questionQuery = reactive({ type: "", taskKey: "", keyword: "" }); +// 题库列表使用前端分页:后端一次返回全部,前端按页切片 +const questionPager = reactive({ page: 1, pageSize: 10 }); +const pagedQuestions = computed(() => { + const start = (questionPager.page - 1) * questionPager.pageSize; + return questions.value.slice(start, start + questionPager.pageSize); +}); const emptyQuestion = () => ({ id: null, taskKey: "", type: "SINGLE", stem: "", options: [{ label: "A", content: "" }, { label: "B", content: "" }, { label: "C", content: "" }, { label: "D", content: "" }], answer: [], analysis: "", attachmentName: "", attachmentUrl: "" }); const questionForm = reactive(emptyQuestion()); const questionRules = { type: [{ required: true, message: "请选择题型" }], stem: [{ required: true, message: "请输入题干" }], answer: [{ required: true, message: "请输入参考答案" }] }; @@ -140,7 +157,7 @@ onMounted(() => Promise.all([loadQuestions(), loadExams(), loadProjects()])); let examsMounted = false; // KeepAlive 缓存页面:再次进入时刷新考试列表(新建/发布后无需手动刷新),首次激活跳过避免与 onMounted 重复请求 onActivated(() => { if (examsMounted) loadExams(); examsMounted = true; }); async function loadProjects() { try { projectOptions.value = (await examApi.getQuestionProjects()).data || []; } catch (e) { projectOptions.value = []; } } -async function loadQuestions() { loading.value = true; try { questions.value = (await examApi.getQuestions(questionQuery)).data || []; } finally { loading.value = false; } } +async function loadQuestions() { loading.value = true; try { questionPager.page = 1; questions.value = (await examApi.getQuestions(questionQuery)).data || []; } finally { loading.value = false; } } async function loadExams() { loading.value = true; try { exams.value = (await examApi.getTeacherExams({ keyword: examKeyword.value })).data || []; } finally { loading.value = false; } } function openQuestion(row) { questionReadonly.value = row?.sourceType === 'PLATFORM'; Object.assign(questionForm, row ? JSON.parse(JSON.stringify(row)) : emptyQuestion()); if (!Array.isArray(questionForm.answer) && isChoice.value) questionForm.answer = []; questionDialog.value = true; } function openAi() { router.push('/exam/ai-generate'); } @@ -234,6 +251,13 @@ function durationBetween(row) { if (!row.startTime || !row.endTime) return 0; re display: none; } + /* ===== 题库分页(外观继承自 teacher-light.scss 全局样式,仅控制布局)===== */ + .question-pager { + display: flex; + justify-content: flex-end; + margin-top: var(--sp-4); + } + /* ===== 数据表格(纯白 + 灰线)===== */ .data-table { width: 100%; diff --git a/src/views/teacherEnd/trainingTask/index.vue b/src/views/teacherEnd/trainingTask/index.vue index 2f8ed7a..511f727 100644 --- a/src/views/teacherEnd/trainingTask/index.vue +++ b/src/views/teacherEnd/trainingTask/index.vue @@ -308,7 +308,7 @@ 03
@@ -327,6 +327,7 @@
@@ -426,11 +427,12 @@ const uploadingField = ref("") const choiceEditorVisible = ref(false) const bankPickerVisible = ref(false) // 四类资料槽位:key 对应 taskForm 的 `${key}Name` / `${key}Url` 字段 +// accept 为空表示不限制文件类型,否则按逗号分隔的扩展名校验 const materialSlots = [ - { key: "material", label: "案例资料" }, - { key: "trainingData", label: "案例视频" }, + { key: "material", label: "案例数据", accept: ".xls,.xlsx" }, + { key: "trainingData", label: "案例数据", accept: ".xls,.xlsx" }, { key: "guideManual", label: "实训指导手册" }, - { key: "aiManual", label: "AI排错指导手册" }, + { key: "aiManual", label: "AI辅助指导手册" }, ] const tasks = ref([]) const classList = ref([]) @@ -741,6 +743,19 @@ function removeObjective(index) { async function handleMaterialChange(slotKey, file) { const rawFile = file?.raw if (!rawFile) return + const slot = materialSlots.find((item) => item.key === slotKey) + if (slot?.accept) { + const allowedExts = slot.accept + .split(",") + .map((item) => item.trim().toLowerCase()) + .filter(Boolean) + const fileName = String(rawFile.name || file?.name || "").toLowerCase() + const isAllowed = allowedExts.some((ext) => fileName.endsWith(ext)) + if (!isAllowed) { + proxy?.$modal?.msgError?.(`${slot.label}仅支持 ${allowedExts.join(" / ")} 格式`) + return + } + } const data = new FormData() data.append("file", rawFile) uploadingField.value = slotKey