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.
fix/exam-question-access-material-excel-delete-draft
qinzhenpen 2 weeks ago
parent b37cb241ee
commit 6d9fe83418

@ -28,7 +28,8 @@
<el-select v-model="questionQuery.type" placeholder="全部题型" clearable @change="loadQuestions">
<el-option v-for="item in typeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="questionQuery.taskKey" placeholder="全部项目" clearable @change="loadQuestions" style="width: 190px">
<!-- 全部项目筛选暂时隐藏 -->
<el-select v-if="false" v-model="questionQuery.taskKey" placeholder="全部项目" clearable @change="loadQuestions" style="width: 190px">
<el-option v-for="item in projectOptions" :key="item.taskKey" :label="`${item.projectName} / ${item.taskName}`" :value="item.taskKey" />
</el-select>
<el-input v-model="questionQuery.keyword" placeholder="搜索题干" clearable :prefix-icon="Search" @keyup.enter="loadQuestions" />
@ -58,7 +59,7 @@
<input ref="importInput" class="import-file-input" type="file" :accept="importAccept" @change="handleImportFile" />
</div>
</section>
<el-table v-loading="loading" :data="questions" class="data-table">
<el-table v-loading="loading" :data="pagedQuestions" class="data-table">
<el-table-column label="题型" min-width="110"><template #default="{ row }"><el-tag :type="typeTone[row.type]">{{ typeName[row.type] }}</el-tag></template></el-table-column>
<el-table-column prop="stem" label="题干" min-width="420" show-overflow-tooltip />
<el-table-column label="题目来源" width="110"><template #default="{ row }"><el-tag :type="row.sourceType === 'PLATFORM' ? 'success' : 'info'">{{ row.sourceType === 'PLATFORM' ? '平台题库' : '教师自建' }}</el-tag></template></el-table-column>
@ -78,6 +79,16 @@
</div>
</template>
</el-table>
<div v-if="questions.length" class="question-pager">
<el-pagination
layout="total, prev, pager, next, sizes, jumper"
:total="questions.length"
v-model:current-page="questionPager.page"
v-model:page-size="questionPager.pageSize"
:page-sizes="[10, 20, 50, 100]"
background
/>
</div>
</el-tab-pane>
</el-tabs>
@ -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%;

@ -308,7 +308,7 @@
<span class="section-number">03</span>
<div class="section-info">
<h4>资料与步骤</h4>
<p>上传案例资料、案例视频、实训指导手册与AI排错指导手册;步骤数量固定,仅可修改步骤名称</p>
<p>上传案例数据、实训指导手册与AI辅助指导手册;步骤数量固定,仅可修改步骤名称</p>
</div>
</div>
<div v-for="slot in materialSlots" :key="slot.key" class="material-row">
@ -327,6 +327,7 @@
</div>
<div class="material-ops">
<el-upload v-if="!isReadonly" action="#" :auto-upload="false" :show-file-list="false"
:accept="slot.accept || undefined"
:on-change="(file) => handleMaterialChange(slot.key, file)">
<el-button type="primary" :loading="uploadingField === slot.key"
class="upload-btn">
@ -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

Loading…
Cancel
Save