You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/views/teacherEnd/exam/index.vue

677 lines
30 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<main class="exam-admin">
<el-tabs v-model="activeTab" class="workspace-tabs">
<el-tab-pane label="考评管理" name="exams">
<section class="assessment-toolbar" aria-label="考试筛选">
<div class="assessment-search"><el-input v-model="examKeyword" placeholder="请输入考试名称" clearable :prefix-icon="Search" @keyup.enter="loadExams" /><el-button :icon="Search" @click="loadExams">搜索</el-button></div>
<div class="toolbar-spacer" />
<el-button class="assessment-create" type="primary" :icon="Plus" @click="$router.push('/exam/manage/new')">新建考试</el-button>
</section>
<section v-loading="loading" class="assessment-list">
<article v-for="row in exams" :key="row.id" class="assessment-card">
<div class="assessment-cover"><el-icon><PictureFilled /></el-icon></div>
<div class="assessment-info"><h3>{{ row.name }}</h3><p class="time-line"><el-icon><Clock /></el-icon><span class="time-label">起止时间</span><span class="time-value">{{ formatTime(row.startTime) }} ~ {{ formatTime(row.endTime) }}</span></p><p>考试时长:{{ row.durationMinutes || durationBetween(row) }}分钟</p></div>
<div class="assessment-status"><el-tag :type="statusMeta[row.status]?.tone || 'info'" effect="light">{{ statusMeta[row.status]?.label || row.status }}</el-tag></div>
<div class="assessment-actions">
<el-button v-if="row.status === 'NOT_STARTED'" text type="primary" :icon="RefreshLeft" @click="withdraw(row)">撤回</el-button>
<el-button v-if="['DRAFT','IN_PROGRESS'].includes(row.status)" text type="primary" :icon="Edit" @click="$router.push(`/exam/manage/${row.id}`)">编辑</el-button>
<el-button v-if="row.status === 'DRAFT'" text type="danger" :icon="Delete" @click="removeExam(row)">删除</el-button>
<el-button v-if="row.status === 'IN_PROGRESS'" text type="primary" :icon="RefreshLeft" @click="$router.push(`/exam/review/${row.id}`)">指定人回退</el-button>
<template v-if="row.status === 'ENDED'"><el-button text type="primary" :icon="EditPen" @click="$router.push(`/exam/review/${row.id}`)">评阅</el-button><el-button text type="primary" :icon="DataAnalysis" @click="$router.push(`/exam/analysis/${row.id}`)">分析</el-button><el-dropdown @command="(command) => handleExport(command, row)"><el-button text type="primary" :icon="Download">导出<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button><template #dropdown><el-dropdown-menu><el-dropdown-item command="paper">空白试卷</el-dropdown-item><el-dropdown-item command="scores">班级成绩</el-dropdown-item><el-dropdown-item command="submissions">全部答卷</el-dropdown-item></el-dropdown-menu></template></el-dropdown><el-button text type="danger" :icon="Delete" @click="removeExam(row)">删除</el-button></template>
</div>
</article>
<el-empty v-if="!exams.length" class="table-empty" :image-size="92" description="暂无考试" />
</section>
</el-tab-pane>
<el-tab-pane label="题库管理" name="questions">
<section class="toolbar" aria-label="题库筛选">
<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="sourceFilter" placeholder="全部来源" clearable @change="resetQuestionPager" style="width: 140px">
<el-option label="平台题库" value="PLATFORM" />
<el-option label="教师自建" value="CUSTOM" />
</el-select>
<!-- 全部项目筛选暂时隐藏 -->
<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" />
<el-button :icon="Search" @click="loadQuestions">查询</el-button>
<div class="toolbar-spacer" />
<div class="toolbar-actions">
<el-button type="primary" :icon="MagicStick" @click="openAi">AI出题</el-button>
<el-dropdown trigger="click" @command="downloadTemplate">
<el-button :icon="Download">下载模板<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="word">Word 导入模板</el-dropdown-item>
<el-dropdown-item command="excel">Excel 导入模板</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown trigger="click" @command="openImportPicker">
<el-button :icon="Upload" :loading="importing">批量导入<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="word">导入 Word 文件</el-dropdown-item>
<el-dropdown-item command="excel">导入 Excel 文件</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button type="primary" :icon="Plus" @click="openQuestion()">新增题目</el-button>
<input ref="importInput" class="import-file-input" type="file" :accept="importAccept" @change="handleImportFile" />
</div>
</section>
<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>
<el-table-column label="正确答案" min-width="200"><template #default="{ row }"><el-tooltip :content="displayAnswer(row.answer)" placement="top" :show-after="300" popper-class="answer-tooltip" :disabled="!displayAnswer(row.answer)"><span class="answer-clip">{{ displayAnswer(row.answer) }}</span></el-tooltip></template></el-table-column>
<el-table-column prop="updatedAt" label="更新时间" min-width="180"><template #default="{ row }">{{ formatTime(row.updatedAt) }}</template></el-table-column>
<el-table-column label="操作" min-width="160" fixed="right"><template #default="{ row }">
<el-button text type="primary" :icon="Edit" @click="openQuestion(row)">{{ row.sourceType === 'PLATFORM' ? '查看' : '编辑' }}</el-button>
<el-button v-if="row.sourceType !== 'PLATFORM'" text type="danger" :icon="Delete" @click="removeQuestion(row)">删除</el-button>
</template></el-table-column>
<template #empty>
<div class="table-empty">
<el-icon class="empty-icon">
<FolderOpened />
</el-icon>
<p class="empty-title">暂无题目</p>
<p class="empty-tip">可通过“新增题目”按钮添加题目</p>
</div>
</template>
</el-table>
<div v-if="filteredQuestions.length" class="question-pager">
<el-pagination
layout="total, prev, pager, next, sizes, jumper"
:total="filteredQuestions.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>
<el-dialog v-model="questionDialog" :title="questionReadonly ? '查看题目' : (questionForm.id ? '编辑题目' : '新增题目')" width="760px" class="teacher-dialog" destroy-on-close>
<el-form ref="questionFormRef" :model="questionForm" :rules="questionRules" label-width="92px" :disabled="questionReadonly">
<el-form-item label="题型" prop="type"><el-radio-group v-model="questionForm.type" @change="resetAnswer"><el-radio-button v-for="item in typeOptions" :key="item.value" :label="item.value">{{ item.label }}</el-radio-button></el-radio-group></el-form-item>
<el-form-item label="归属项目"><el-select v-model="questionForm.taskKey" placeholder="可选项目" clearable style="width: 100%"><el-option v-for="item in projectOptions" :key="item.taskKey" :label="`${item.projectName} / ${item.taskName}`" :value="item.taskKey" /></el-select></el-form-item>
<el-form-item label="题干" prop="stem"><el-input v-model="questionForm.stem" type="textarea" :rows="4" maxlength="2000" show-word-limit /></el-form-item>
<el-form-item v-if="isChoice" label="选项">
<div class="option-editor">
<div v-for="(option, index) in questionForm.options" :key="option.label" class="option-row">
<el-checkbox v-if="questionForm.type === 'MULTIPLE'" :model-value="questionForm.answer.includes(option.label)" @change="toggleMultiple(option.label, $event)" />
<el-radio v-else v-model="singleAnswer" :label="option.label"><span class="option-label">{{ option.label }}</span></el-radio>
<el-input v-model="option.content" :placeholder="`选项 ${option.label}`" />
<el-button v-if="questionForm.options.length > 2" text :icon="Delete" aria-label="删除选项" @click="deleteOption(index)" />
</div>
<el-button v-if="questionForm.options.length < 8" text type="primary" :icon="Plus" @click="addOption">添加选项</el-button>
</div>
</el-form-item>
<el-form-item v-else-if="questionForm.type === 'TRUE_FALSE'" label="正确答案"><el-radio-group v-model="questionForm.answer"><el-radio :label="true">正确</el-radio><el-radio :label="false">错误</el-radio></el-radio-group></el-form-item>
<el-form-item v-else label="参考答案" prop="answer"><el-input v-model="questionForm.answer" type="textarea" :rows="4" /></el-form-item>
<el-form-item v-if="questionForm.type === 'SHORT_ANSWER'" label="题目资料">
<el-upload :show-file-list="false" :http-request="uploadMaterial" accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg"><el-button :icon="Paperclip">上传资料</el-button></el-upload>
<span v-if="questionForm.attachmentName" class="file-name">{{ questionForm.attachmentName }}</span>
</el-form-item>
<el-form-item label="答案解析"><el-input v-model="questionForm.analysis" type="textarea" :rows="3" /></el-form-item>
</el-form>
<template #footer><el-button @click="questionDialog=false"><el-icon><Close /></el-icon>取消</el-button><el-button v-if="!questionReadonly" type="primary" :loading="saving" @click="saveQuestion"><el-icon><Check /></el-icon>保存</el-button></template>
</el-dialog>
</main>
</template>
<script setup>
import { computed, onActivated, onMounted, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { useRouter } from "vue-router";
import { ArrowDown, Check, Clock, Close, DataAnalysis, Delete, Download, Edit, EditPen, FolderOpened, MagicStick, Paperclip, PictureFilled, Plus, RefreshLeft, Search, Upload } from "@element-plus/icons-vue";
import { saveAs } from "file-saver";
import * as examApi from "@/api/exam";
const router = useRouter();
const activeTab = ref("exams");
const loading = ref(false); const saving = ref(false); const importing = ref(false); const questions = ref([]); const exams = ref([]); const examKeyword = ref(""); const projectOptions = ref([]);
const questionDialog = ref(false); const questionFormRef = ref(); const questionReadonly = ref(false);
const importInput = ref(); const importFormat = ref("word");
const importAccept = computed(() => importFormat.value === "word" ? ".docx" : ".xls,.xlsx");
const typeOptions = [{ label: "单选题", value: "SINGLE" }, { label: "多选题", value: "MULTIPLE" }, { label: "判断题", value: "TRUE_FALSE" }, { label: "简答题", value: "SHORT_ANSWER" }];
const typeName = Object.fromEntries(typeOptions.map((item) => [item.value, item.label]));
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 sourceFilter = ref(""); // 题目来源前端筛选:PLATFORM=平台题库 / CUSTOM=教师自建(非 PLATFORM)
// 题库列表使用前端分页:后端一次返回全部,前端按页切片
const questionPager = reactive({ page: 1, pageSize: 10 });
const filteredQuestions = computed(() => {
const st = sourceFilter.value;
if (!st) return questions.value;
return questions.value.filter(q => st === "PLATFORM" ? q.sourceType === "PLATFORM" : q.sourceType !== "PLATFORM");
});
const pagedQuestions = computed(() => {
const start = (questionPager.page - 1) * questionPager.pageSize;
return filteredQuestions.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: "请输入参考答案" }] };
const isChoice = computed(() => ["SINGLE", "MULTIPLE"].includes(questionForm.type));
const singleAnswer = computed({ get: () => questionForm.answer?.[0] || "", set: (value) => { questionForm.answer = value ? [value] : []; } });
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 { questionPager.page = 1; questions.value = (await examApi.getQuestions(questionQuery)).data || []; } finally { loading.value = false; } }
function resetQuestionPager() { questionPager.page = 1; }
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'); }
function resetAnswer() { questionForm.answer = questionForm.type === "TRUE_FALSE" ? true : questionForm.type === "SHORT_ANSWER" ? "" : []; }
function toggleMultiple(label, checked) { questionForm.answer = checked ? [...new Set([...questionForm.answer, label])] : questionForm.answer.filter((item) => item !== label); }
function addOption() { questionForm.options.push({ label: String.fromCharCode(65 + questionForm.options.length), content: "" }); }
function deleteOption(index) { const [removed] = questionForm.options.splice(index, 1); questionForm.answer = questionForm.answer.filter((item) => item !== removed.label); }
async function saveQuestion() { await questionFormRef.value.validate(); if (isChoice.value && (!questionForm.answer.length || questionForm.options.some((item) => !item.content.trim()))) return ElMessage.warning("请补全选项并设置正确答案"); saving.value = true; try { questionForm.id ? await examApi.updateQuestion(questionForm.id, questionForm) : await examApi.createQuestion(questionForm); ElMessage.success("题目已保存"); questionDialog.value = false; loadQuestions(); } finally { saving.value = false; } }
async function removeQuestion(row) { await ElMessageBox.confirm("删除后无法恢复,确认删除该题目?", "删除题目", { type: "warning" }); await examApi.deleteQuestion(row.id); ElMessage.success("已删除"); loadQuestions(); }
async function uploadMaterial({ file }) { const form = new FormData(); form.append("file", file); const { data } = await examApi.uploadQuestionAttachment(form); questionForm.attachmentName = data.name; questionForm.attachmentUrl = data.url; ElMessage.success("资料已上传"); }
function openImportPicker(format) { importFormat.value = format; importInput.value.value = ""; importInput.value.click(); }
async function handleImportFile(event) { const [file] = event.target.files; if (!file) return; await doImport({ file }); }
async function doImport({ file }) { importing.value = true; try { const form = new FormData(); form.append("file", file); const { data } = await examApi.importQuestions(form); ElMessage.success(`成功导入 ${data.length} 道题`); loadQuestions(); } finally { importing.value = false; if (importInput.value) importInput.value.value = ""; } }
async function downloadTemplate(format) { const extension = format === "excel" ? "xlsx" : "docx"; saveAs(await examApi.downloadQuestionTemplate(format), `考试题库${format === "excel" ? "Excel" : "Word"}导入模板.${extension}`); }
async function publish(row) { await ElMessageBox.confirm(`确认发布“${row.name}”?发布后将在设定时间自动开始。`, "发布考试", { type: "warning" }); await examApi.publishExam(row.id); ElMessage.success("已发布"); loadExams(); }
async function withdraw(row) { await examApi.withdrawExam(row.id); ElMessage.success("已撤回"); loadExams(); }
async function removeExam(row) { const message = row.status === "DRAFT" ? `确认删除未发布的考试“${row.name}”?删除后无法恢复。` : `确认永久删除“${row.name}”?仅在已导出全部答卷后允许删除。`; await ElMessageBox.confirm(message, "删除考试", { type: "warning", confirmButtonText: "确认删除" }); await examApi.deleteExam(row.id); ElMessage.success("已删除"); loadExams(); }
async function handleExport(command, row) { const actions = { paper: [examApi.exportExamPaper, `${row.name}试卷.docx`], scores: [examApi.exportExamScores, `${row.name}班级成绩.xlsx`], submissions: [examApi.exportExamSubmissions, `${row.name}全部试卷.zip`] }; const [action, filename] = actions[command]; saveAs(await action(row.id), filename); if (command === "submissions") loadExams(); }
function displayAnswer(value) { if (Array.isArray(value)) return value.join("、"); if (value === true) return "正确"; if (value === false) return "错误"; return value || "-"; }
function formatTime(value) { return value ? new Date(value).toLocaleString("zh-CN", { hour12: false }).replaceAll("/", "-") : "-"; }
function durationBetween(row) { if (!row.startTime || !row.endTime) return 0; return Math.max(0, Math.round((new Date(row.endTime).getTime() - new Date(row.startTime).getTime()) / 60000)); }
</script>
<style scoped lang="scss">
.exam-admin {
color: var(--text-strong);
/* ===== 标签页(极简文字式,tab 文字不换行)===== */
.workspace-tabs {
:deep(.el-tabs__header) {
margin-bottom: var(--sp-4);
}
:deep(.el-tabs__nav-wrap::after) {
height: 1px;
background-color: var(--border-color);
}
:deep(.el-tabs__item) {
flex: 0 0 auto;
height: 36px;
max-width: none;
padding: 0 var(--sp-4);
color: var(--text-body);
font-size: var(--fs-body);
font-weight: 400;
white-space: nowrap;
&:hover {
color: var(--text-strong);
}
&.is-active {
color: var(--edu-primary);
font-weight: 600;
}
}
:deep(.el-tabs__active-bar) {
height: 2px;
background-color: var(--edu-primary);
}
}
/* ===== 筛选 / 操作条(扁平,直接落在画布上;外观归 teacher-light.scss 全局接管)===== */
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--sp-3);
margin-bottom: var(--sp-4);
.el-select,
.el-input {
width: var(--field-w);
}
}
.toolbar-spacer {
flex: 1;
}
.toolbar-actions {
display: flex;
align-items: center;
gap: var(--sp-2);
}
.import-file-input {
display: none;
}
/* ===== 题库分页(外观继承自 teacher-light.scss 全局样式,仅控制布局)===== */
.question-pager {
display: flex;
justify-content: flex-end;
margin-top: var(--sp-4);
}
/* ===== 数据表格(纯白 + 灰线)===== */
.data-table {
width: 100%;
margin-top: 0 !important;
background: #ffffff;
border: 1px solid var(--border-color);
border-radius: var(--r-card);
overflow: hidden;
:deep(.el-table__header-wrapper th) {
background: #ffffff;
color: var(--text-strong);
font-weight: 600;
font-size: var(--fs-body);
}
:deep(.el-table__body tr:hover > td) {
background: #f9fafb !important;
}
:deep(.el-table__cell) {
font-size: var(--fs-body);
color: var(--text-strong);
border-color: var(--border-color);
}
}
/* ===== 考试名称首字徽标(对齐班级/学生/任务分配基准)===== */
.name-cell {
display: inline-flex;
align-items: center;
gap: var(--sp-2);
vertical-align: middle;
.exam-badge {
flex: 0 0 auto;
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
background: var(--edu-primary-soft);
color: var(--edu-primary);
border-radius: var(--r-control);
font-size: var(--fs-body);
font-weight: 600;
}
.name-text {
font-weight: 600;
color: var(--text-strong);
}
}
/* ===== 自定义空状态 ===== */
.table-empty {
padding: var(--sp-5) 0;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--sp-1);
.empty-icon {
font-size: 28px;
color: var(--text-muted);
}
.empty-title {
margin: var(--sp-1) 0 0;
font-size: var(--fs-body);
font-weight: 600;
color: var(--text-body);
}
.empty-tip {
margin: 0;
font-size: var(--fs-tag);
color: var(--text-muted);
}
}
/* ===== 弹窗内选项编辑器(外观由 teacher-light.scss 全局接管 + 本地布局)===== */
.option-editor {
width: 100%;
display: flex;
flex-direction: column;
gap: var(--sp-2);
.option-row {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: var(--r-control);
background: #ffffff;
transition: border-color 0.15s;
&:hover {
border-color: var(--edu-primary);
}
.el-radio,
.el-checkbox {
flex: 0 0 auto;
margin-right: 0;
}
.option-label {
flex: 0 0 auto;
width: 22px;
height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--edu-primary-soft);
color: var(--edu-primary);
font-size: 12px;
font-weight: 600;
margin-left: 4px;
}
.el-input {
flex: 1 1 auto;
min-width: 0;
}
.el-button {
flex: 0 0 auto;
}
}
> .el-button.el-button--text {
align-self: flex-start;
margin-top: var(--sp-1);
}
}
/* ===== 弹窗内表单微调 ===== */
:deep(.teacher-dialog) &,
:deep(.admin-edit-dialog) & {
// dialog 内部的 exam-admin 表单间距
}
// dialog 表单整体紧凑化
:deep(.el-form) {
.el-form-item {
margin-bottom: 18px;
}
.el-form-item:last-child {
margin-bottom: 0;
}
}
/* ===== 考评管理卡片列表(布局来自 master,配色按教育风令牌重制)===== */
.assessment-toolbar {
display: flex;
align-items: center;
gap: var(--sp-3);
margin-bottom: var(--sp-4);
.el-input {
width: 240px;
}
}
.assessment-search {
display: flex;
gap: var(--sp-2);
.el-input {
width: 260px;
}
}
.assessment-create {
min-width: 110px;
}
.assessment-list {
display: grid;
gap: var(--sp-4);
min-height: 220px;
}
.assessment-card {
display: grid;
grid-template-columns: 56px minmax(220px, 1.4fr) auto auto;
align-items: center;
gap: var(--sp-4);
padding: 14px 20px;
border: 1px solid var(--border-color);
border-radius: 12px;
background: #ffffff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
transition: border-color 0.18s ease;
&:hover {
border-color: #d1d5db;
}
}
.assessment-cover {
display: grid;
width: 56px;
height: 56px;
place-items: center;
border-radius: 10px;
color: var(--edu-primary);
background: var(--edu-primary-soft);
.el-icon {
font-size: 26px;
}
}
.assessment-info {
min-width: 0;
h3 {
margin: 0 0 6px;
color: var(--text-strong);
font-size: var(--fs-card);
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
p {
margin: 2px 0;
color: var(--text-body);
font-size: var(--fs-body);
}
.time-line {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 4px 0;
padding: 4px 10px;
border-radius: 6px;
background: var(--edu-primary-soft);
.el-icon {
font-size: 15px;
color: var(--edu-primary);
}
.time-label {
color: var(--edu-primary-deep);
font-size: var(--fs-body);
}
.time-value {
color: var(--text-strong);
font-size: var(--fs-body);
font-weight: 600;
font-variant-numeric: tabular-nums;
}
}
}
.assessment-status {
white-space: nowrap;
}
.assessment-actions {
display: flex;
flex-wrap: nowrap;
justify-content: flex-end;
align-items: center;
gap: var(--sp-2);
.el-button + .el-button {
margin-left: 0;
}
}
@media (max-width: 1120px) {
.assessment-card {
grid-template-columns: 56px minmax(220px, 1fr) auto;
}
.assessment-actions {
grid-column: 2 / -1;
justify-content: flex-start;
}
}
@media (max-width: 760px) {
.assessment-toolbar {
align-items: stretch;
flex-direction: column;
gap: var(--sp-3);
}
.assessment-search {
width: 100%;
.el-input {
flex: 1;
width: auto;
}
}
.assessment-card {
grid-template-columns: 48px 1fr;
gap: var(--sp-3);
padding: var(--sp-3);
}
.assessment-cover {
width: 48px;
height: 48px;
.el-icon {
font-size: 22px;
}
}
.assessment-status,
.assessment-actions {
grid-column: 1 / -1;
justify-content: flex-start;
}
}
.option-row {
display: grid;
grid-template-columns: 28px minmax(0, 1fr) 36px;
align-items: center;
gap: var(--sp-2);
margin-bottom: var(--sp-2);
}
.option-label {
font-weight: 700;
}
.file-name {
margin-left: var(--sp-3);
color: var(--edu-primary);
}
@media (max-width: 960px) {
.toolbar-spacer {
flex-basis: 100%;
height: 0;
}
}
@media (max-width: 760px) {
.toolbar {
align-items: stretch;
flex-direction: column;
}
.toolbar-spacer {
display: none;
}
.toolbar-actions {
align-items: stretch;
flex-direction: column;
width: 100%;
}
.toolbar-actions :deep(.el-dropdown) {
width: 100%;
}
.toolbar .el-input,
.toolbar .el-select {
width: 100%;
}
:deep(.toolbar .el-button) {
width: 100%;
}
}
}
/* 正确答案列:两行截断省略,全文悬浮 tooltip 查看 */
.answer-clip {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
line-height: 1.45;
word-break: break-all;
}
</style>
<style lang="scss">
/* tooltip 弹层 teleport 到 body,需 non-scoped;限宽避免长答案撑满屏幕 */
.answer-tooltip.el-popper {
max-width: 460px;
line-height: 1.6;
word-break: break-all;
}
</style>