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.
577 lines
26 KiB
Vue
577 lines
26 KiB
Vue
<template>
|
|
<main class="exam-admin app-container">
|
|
<header class="page-head">
|
|
<div><h1>综合考核</h1><p>维护题库并管理考试全流程</p></div>
|
|
<div v-if="activeTab === 'questions'" class="page-actions">
|
|
<el-button type="primary" :icon="Plus" @click="openQuestion()">添加题目</el-button>
|
|
<el-dropdown trigger="click" @command="openImportPicker">
|
|
<el-button type="success" :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-dropdown trigger="click" @command="downloadTemplate">
|
|
<el-button type="success" :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-button type="primary" @click="openAi">AI出题</el-button>
|
|
</div>
|
|
<el-button v-else type="primary" :icon="Plus" @click="$router.push('/exam/manage/new')">新建考试</el-button>
|
|
</header>
|
|
|
|
<el-tabs v-model="activeTab" class="workspace-tabs">
|
|
<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>
|
|
<input ref="importInput" class="import-file-input" type="file" :accept="importAccept" @change="handleImportFile" />
|
|
</section>
|
|
<el-table v-loading="loading" :data="questions" class="data-table">
|
|
<el-table-column label="题型" 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="150"><template #default="{ row }">{{ displayAnswer(row.answer) }}</template></el-table-column>
|
|
<el-table-column prop="updatedAt" label="更新时间" width="180"><template #default="{ row }">{{ formatTime(row.updatedAt) }}</template></el-table-column>
|
|
<el-table-column label="操作" width="150" fixed="right"><template #default="{ row }">
|
|
<el-button link type="primary" @click="openQuestion(row)">{{ row.sourceType === 'PLATFORM' ? '查看' : '编辑' }}</el-button>
|
|
<el-button v-if="row.sourceType !== 'PLATFORM'" link type="danger" @click="removeQuestion(row)">删除</el-button>
|
|
</template></el-table-column>
|
|
<template #empty><el-empty class="table-empty" :image-size="92" description="暂无题目" /></template>
|
|
</el-table>
|
|
</el-tab-pane>
|
|
|
|
<el-tab-pane label="考评管理" name="exams">
|
|
<section class="assessment-toolbar" aria-label="考试筛选">
|
|
<div class="assessment-search"><el-input v-model="examKeyword" placeholder="请输入考试名称" clearable @keyup.enter="loadExams" /><el-button type="primary" :icon="Search" @click="loadExams">搜索</el-button></div>
|
|
<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">状态:<span :class="`status-${row.status.toLowerCase()}`">{{ statusMeta[row.status]?.label }}</span></div>
|
|
<div class="assessment-actions">
|
|
<el-button v-if="row.status === 'NOT_STARTED'" type="primary" @click="withdraw(row)">撤回</el-button>
|
|
<el-button v-if="['DRAFT','IN_PROGRESS'].includes(row.status)" type="primary" @click="$router.push(`/exam/manage/${row.id}`)">编辑</el-button>
|
|
<el-button v-if="row.status === 'IN_PROGRESS'" type="primary" @click="$router.push(`/exam/review/${row.id}`)">指定人回退</el-button>
|
|
<template v-if="row.status === 'ENDED'"><el-button type="primary" @click="$router.push(`/exam/review/${row.id}`)">评阅</el-button><el-button type="primary" @click="$router.push(`/exam/analysis/${row.id}`)">分析</el-button><el-dropdown @command="(command) => handleExport(command, row)"><el-button type="primary">导出<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 type="primary" @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-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-button><el-button v-if="!questionReadonly" type="primary" :loading="saving" @click="saveQuestion">保存</el-button></template>
|
|
</el-dialog>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onMounted, reactive, ref } from "vue";
|
|
import { useRouter } from "vue-router";
|
|
import { ElMessage, ElMessageBox } from "element-plus";
|
|
import { ArrowDown, Delete, Download, Paperclip, PictureFilled, Plus, Search, Upload } from "@element-plus/icons-vue";
|
|
import { saveAs } from "file-saver";
|
|
import * as examApi from "@/api/exam";
|
|
|
|
const router = useRouter();
|
|
const activeTab = ref("questions");
|
|
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()]));
|
|
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('/ai'); }
|
|
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.app-container {
|
|
--exam-accent: #39c9ff;
|
|
--exam-text: #eaf8ff;
|
|
--exam-muted: #9fc2d8;
|
|
--exam-border: rgba(69, 185, 236, 0.3);
|
|
position: relative;
|
|
min-height: 0 !important;
|
|
overflow: hidden;
|
|
color: var(--exam-text);
|
|
background:
|
|
radial-gradient(circle at 88% 0%, rgba(27, 126, 194, 0.16), transparent 31%),
|
|
linear-gradient(145deg, rgba(6, 25, 40, 0.92), rgba(3, 13, 24, 0.9));
|
|
}
|
|
|
|
.exam-admin::before {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 28px;
|
|
left: 28px;
|
|
height: 1px;
|
|
background: linear-gradient(90deg, transparent, rgba(79, 207, 255, 0.72), transparent);
|
|
content: "";
|
|
}
|
|
|
|
.page-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 24px;
|
|
margin-bottom: 10px;
|
|
padding: 4px 2px 8px;
|
|
|
|
h1 {
|
|
margin: 0;
|
|
color: #f4fbff;
|
|
font-size: 26px;
|
|
line-height: 1.3;
|
|
letter-spacing: 0.02em;
|
|
text-shadow: 0 0 18px rgba(58, 199, 255, 0.16);
|
|
}
|
|
|
|
p {
|
|
margin: 7px 0 0;
|
|
color: var(--exam-muted);
|
|
font-size: 14px;
|
|
}
|
|
}
|
|
|
|
.workspace-tabs {
|
|
--el-color-primary: var(--exam-accent);
|
|
|
|
:deep(.el-tabs__header) {
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
:deep(.el-tabs__nav-wrap::after) {
|
|
height: 1px;
|
|
background-color: rgba(99, 187, 226, 0.26);
|
|
}
|
|
|
|
:deep(.el-tabs__item) {
|
|
height: 44px;
|
|
padding: 0 24px;
|
|
color: #aac9dc;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
:deep(.el-tabs__item:hover),
|
|
:deep(.el-tabs__item.is-active) {
|
|
color: #73ddff;
|
|
}
|
|
|
|
:deep(.el-tabs__active-bar) {
|
|
height: 3px;
|
|
border-radius: 3px 3px 0 0;
|
|
background: linear-gradient(90deg, #27c9ff, #5be4ef);
|
|
box-shadow: 0 0 12px rgba(39, 201, 255, 0.45);
|
|
}
|
|
}
|
|
|
|
.toolbar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 16px;
|
|
padding: 14px;
|
|
border: 1px solid rgba(53, 162, 214, 0.25);
|
|
border-radius: 10px;
|
|
background: rgba(2, 18, 31, 0.6);
|
|
box-shadow: inset 0 1px 0 rgba(157, 224, 255, 0.04);
|
|
|
|
.el-select {
|
|
width: 160px;
|
|
}
|
|
|
|
.el-input {
|
|
width: 280px;
|
|
}
|
|
}
|
|
|
|
.toolbar-spacer {
|
|
flex: 1;
|
|
}
|
|
|
|
.toolbar-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.import-file-input {
|
|
display: none;
|
|
}
|
|
|
|
:deep(.toolbar .el-input__wrapper),
|
|
:deep(.toolbar .el-select__wrapper) {
|
|
min-height: 38px;
|
|
border-color: rgba(71, 181, 229, 0.42);
|
|
background: rgba(3, 28, 45, 0.92);
|
|
}
|
|
|
|
:deep(.toolbar .el-input__inner),
|
|
:deep(.toolbar .el-select__selected-item),
|
|
:deep(.toolbar .el-select__placeholder) {
|
|
color: #e6f7ff;
|
|
}
|
|
|
|
:deep(.toolbar .el-input__inner::placeholder) {
|
|
color: #7fa5bc;
|
|
}
|
|
|
|
:deep(.toolbar .el-button:not(.el-button--primary)) {
|
|
min-height: 38px;
|
|
color: #d8f2ff;
|
|
border-color: rgba(80, 181, 229, 0.46);
|
|
background: rgba(5, 32, 50, 0.88);
|
|
}
|
|
|
|
:deep(.toolbar .el-button:not(.el-button--primary):hover),
|
|
:deep(.toolbar .el-button:not(.el-button--primary):focus) {
|
|
color: #ffffff;
|
|
border-color: rgba(91, 217, 255, 0.82);
|
|
background: rgba(10, 63, 91, 0.92);
|
|
box-shadow: 0 0 16px rgba(28, 174, 226, 0.14);
|
|
}
|
|
|
|
.data-table {
|
|
--el-table-border-color: rgba(59, 164, 213, 0.24);
|
|
--el-table-header-text-color: #e5f7ff;
|
|
--el-table-text-color: #cce7f6;
|
|
--el-table-row-hover-bg-color: rgba(16, 100, 143, 0.22);
|
|
width: 100%;
|
|
margin-top: 0 !important;
|
|
border: 1px solid rgba(59, 164, 213, 0.24);
|
|
border-radius: 10px;
|
|
background: rgba(2, 16, 27, 0.62);
|
|
}
|
|
|
|
.assessment-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 34px;
|
|
margin: 0 auto 26px;
|
|
padding: 14px 0 18px;
|
|
}
|
|
|
|
.assessment-search {
|
|
display: flex;
|
|
gap: 24px;
|
|
width: min(560px, 70vw);
|
|
}
|
|
|
|
.assessment-search .el-input {
|
|
flex: 1;
|
|
}
|
|
|
|
.assessment-create {
|
|
min-width: 148px;
|
|
}
|
|
|
|
.assessment-list {
|
|
display: grid;
|
|
gap: 18px;
|
|
min-height: 220px;
|
|
}
|
|
|
|
.assessment-card {
|
|
display: grid;
|
|
grid-template-columns: 86px minmax(260px, 1.25fr) minmax(120px, .65fr) minmax(330px, 1.3fr);
|
|
align-items: center;
|
|
gap: 20px;
|
|
min-height: 126px;
|
|
padding: 18px 24px;
|
|
border: 1px solid rgba(55, 166, 216, .24);
|
|
border-radius: 10px;
|
|
background: linear-gradient(100deg, rgba(4, 31, 49, .9), rgba(3, 19, 32, .74));
|
|
box-shadow: 0 8px 26px rgba(0, 0, 0, .13), inset 0 1px 0 rgba(152, 229, 255, .04);
|
|
}
|
|
|
|
.assessment-cover {
|
|
display: grid;
|
|
width: 76px;
|
|
height: 76px;
|
|
place-items: center;
|
|
border: 5px solid rgba(196, 207, 214, .85);
|
|
color: #1faddd;
|
|
background: linear-gradient(150deg, #f4f8fb 0 34%, #1ba5d5 35% 100%);
|
|
}
|
|
|
|
.assessment-cover .el-icon {
|
|
font-size: 48px;
|
|
filter: drop-shadow(0 2px 0 rgba(255, 255, 255, .7));
|
|
}
|
|
|
|
.assessment-info h3 {
|
|
margin: 0 0 8px;
|
|
color: #f2fbff;
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.assessment-info p {
|
|
margin: 4px 0;
|
|
color: #b9d5e5;
|
|
font-size: 15px;
|
|
}
|
|
|
|
.assessment-status {
|
|
color: #a9cee0;
|
|
font-size: 15px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.assessment-status span { color: #32b9f0; }
|
|
.assessment-status .status-ended { color: #f5b04c; }
|
|
.assessment-status .status-in_progress { color: #49d79d; }
|
|
.assessment-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; }
|
|
.assessment-actions .el-button { min-width: 96px; }
|
|
|
|
@media (max-width: 1120px) {
|
|
.assessment-card { grid-template-columns: 76px minmax(240px, 1fr) minmax(100px, .6fr); }
|
|
.assessment-actions { grid-column: 2 / -1; justify-content: flex-start; }
|
|
}
|
|
|
|
@media (max-width: 760px) {
|
|
.assessment-toolbar { align-items: stretch; flex-direction: column; gap: 12px; }
|
|
.assessment-search { width: 100%; }
|
|
.assessment-card { grid-template-columns: 64px 1fr; padding: 14px; }
|
|
.assessment-cover { width: 62px; height: 62px; }
|
|
.assessment-cover .el-icon { font-size: 38px; }
|
|
.assessment-status, .assessment-actions { grid-column: 1 / -1; justify-content: flex-start; }
|
|
}
|
|
|
|
:deep(.data-table th.el-table__cell) {
|
|
height: 50px;
|
|
padding: 0;
|
|
color: #e9f8ff !important;
|
|
background: linear-gradient(180deg, rgba(14, 77, 111, 0.98), rgba(8, 49, 75, 0.98)) !important;
|
|
}
|
|
|
|
:deep(.data-table td.el-table__cell) {
|
|
padding: 13px 0;
|
|
color: #cce7f6;
|
|
border-bottom-color: rgba(51, 143, 187, 0.18);
|
|
background: rgba(2, 17, 29, 0.74);
|
|
}
|
|
|
|
:deep(.data-table .el-table__empty-block) {
|
|
min-height: 238px;
|
|
background:
|
|
radial-gradient(circle at 50% 42%, rgba(37, 147, 196, 0.1), transparent 28%),
|
|
rgba(2, 15, 25, 0.5);
|
|
}
|
|
|
|
:deep(.table-empty) {
|
|
padding: 26px 0 30px;
|
|
}
|
|
|
|
:deep(.table-empty .el-empty__description) {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
:deep(.table-empty .el-empty__description p) {
|
|
color: #9fc4d9;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.option-editor {
|
|
width: 100%;
|
|
}
|
|
|
|
.option-row {
|
|
display: grid;
|
|
grid-template-columns: 28px minmax(0, 1fr) 36px;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.option-label {
|
|
font-weight: 700;
|
|
}
|
|
|
|
.file-name {
|
|
margin-left: 12px;
|
|
color: var(--exam-accent);
|
|
}
|
|
|
|
:deep(.teacher-dialog .el-dialog__body) {
|
|
padding: 24px 28px 10px;
|
|
}
|
|
|
|
:deep(.teacher-dialog .el-form-item) {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
:deep(.teacher-dialog .el-radio-button__inner) {
|
|
min-width: 92px;
|
|
padding: 10px 18px;
|
|
color: #cbe6f5;
|
|
border-color: rgba(80, 181, 229, 0.42);
|
|
background: rgba(4, 29, 46, 0.9);
|
|
box-shadow: none;
|
|
}
|
|
|
|
:deep(.teacher-dialog .el-radio-button:first-child .el-radio-button__inner) {
|
|
border-left-color: rgba(80, 181, 229, 0.42);
|
|
}
|
|
|
|
:deep(.teacher-dialog .el-radio-button__original-radio:checked + .el-radio-button__inner) {
|
|
color: #ffffff;
|
|
border-color: #38c9ff;
|
|
background: linear-gradient(135deg, #078dce, #1557d6);
|
|
box-shadow: -1px 0 0 0 #38c9ff;
|
|
}
|
|
|
|
:deep(.teacher-dialog .el-radio__label),
|
|
:deep(.teacher-dialog .el-checkbox__label) {
|
|
color: #cbe6f5;
|
|
}
|
|
|
|
:deep(.teacher-dialog .el-radio__input.is-checked + .el-radio__label),
|
|
:deep(.teacher-dialog .el-checkbox__input.is-checked + .el-checkbox__label) {
|
|
color: #e9f8ff;
|
|
}
|
|
|
|
:deep(.teacher-dialog .option-row) {
|
|
min-height: 48px;
|
|
margin-bottom: 12px;
|
|
padding: 6px 10px;
|
|
border: 1px solid rgba(61, 171, 220, 0.2);
|
|
border-radius: 8px;
|
|
background: rgba(3, 24, 39, 0.68);
|
|
}
|
|
|
|
:deep(.teacher-dialog .option-label) {
|
|
color: #e7f7ff;
|
|
}
|
|
|
|
:deep(.teacher-dialog .option-row .el-button.is-text) {
|
|
color: #8edfff;
|
|
}
|
|
|
|
:deep(.teacher-dialog .option-row .el-button.is-text:hover) {
|
|
color: #ffffff;
|
|
background: rgba(19, 107, 145, 0.28);
|
|
}
|
|
|
|
:deep(.teacher-dialog .option-editor > .el-button.is-text) {
|
|
margin-top: 2px;
|
|
color: #77dcff;
|
|
}
|
|
|
|
:deep(.teacher-dialog .el-dialog__footer) {
|
|
padding: 14px 28px 22px;
|
|
border-top: 1px solid rgba(52, 177, 238, 0.16);
|
|
}
|
|
|
|
@media (max-width: 960px) {
|
|
.toolbar-spacer {
|
|
flex-basis: 100%;
|
|
height: 0;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 760px) {
|
|
.exam-admin.app-container {
|
|
padding: 16px 14px 20px;
|
|
}
|
|
|
|
.page-head,
|
|
.toolbar {
|
|
align-items: stretch;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.page-head {
|
|
gap: 14px;
|
|
}
|
|
|
|
.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%;
|
|
}
|
|
}
|
|
</style>
|