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

614 lines
28 KiB
Vue

<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>起止时间:{{ formatTime(row.startTime) }} - {{ formatTime(row.endTime) }}</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 === '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="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="questions" 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>
</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,.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, 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 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 { 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'); }
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) { await ElMessageBox.confirm(`确认永久删除“${row.name}”?仅在已导出全部答卷后允许删除。`, "删除考试", { 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;
}
/* ===== 数据表格(纯白 + 灰线)===== */
.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);
}
}
.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>