feat: 新增学生端浅色模式样式

新增学生端教育蓝浅色主题样式,覆盖Element Plus组件及自定义样式,适配学生端界面风格,添加KeepAlive支持使考试列表进入页面自动刷新
fix/exam-question-access-material-excel-delete-draft
qinzhenpen 3 weeks ago
parent 63f717ece4
commit fbba3b28d6

@ -2685,3 +2685,273 @@ body.teacher-light-mode {
background: #eff6ff !important; background: #eff6ff !important;
} }
} }
/* =============================================================
* 12. 学生端教育蓝(roleId === 4,更活泼版)
* 锚点:body.student-light-mode(layout/index.vue role===4 时自动加)
* 边界:ink-training-theme 的 `.student-training-shell` 规则特异性更长
* + `!important`,不会被这里覆盖;实训页保持墨黑不动。
* ============================================================= */
body.student-light-mode {
// —— EP CSS 变量覆盖(最紧急:干掉 html.dark 的 #00aaff 青蓝污染)——
--el-color-primary: #3b82f6;
--el-color-primary-light-3: #60a5fa;
--el-color-primary-light-5: #93c5fd;
--el-color-primary-light-7: #bfdbfe;
--el-color-primary-light-8: #dbeafe;
--el-color-primary-light-9: #eff6ff;
--el-color-primary-dark-2: #2563eb;
// —— 令牌(略活泼版:更大圆角、略大字、略宽间距)——
--edu-primary: #3b82f6;
--edu-primary-deep: #2563eb;
--edu-primary-soft: #eff6ff;
--text-strong: #1f2937;
--text-body: #4b5563;
--text-muted: #9ca3af;
--border-color: #e5e7eb;
--r-card: 14px;
--r-control: 10px;
--fs-body: 13px;
--sp-2: 10px;
--sp-3: 14px;
// —— 基础画布(和教师端共享,浅色渐变)——
background: #ffffff !important;
color: var(--text-strong);
// —— 输入框(13px 更清晰 + 10px 圆角)——
.el-input__wrapper,
.el-select__wrapper {
border-radius: var(--r-control) !important;
background-color: #ffffff !important;
border: 1px solid var(--border-color) !important;
box-shadow: none !important;
&:hover { border-color: var(--edu-primary) !important; }
&.is-focus, &.is-focused {
border-color: var(--edu-primary) !important;
box-shadow: 0 0 0 3px rgba(59,130,246,0.12) !important;
}
}
.el-input__inner,
.el-textarea__inner {
color: var(--text-strong) !important;
font-size: var(--fs-body) !important;
&::placeholder { color: var(--text-muted) !important; }
}
.el-textarea__inner {
border-radius: var(--r-control) !important;
border: 1px solid var(--border-color) !important;
background-color: #ffffff !important;
box-shadow: none !important;
&:hover { border-color: var(--edu-primary) !important; }
&:focus {
border-color: var(--edu-primary) !important;
box-shadow: 0 0 0 3px rgba(59,130,246,0.12) !important;
}
}
// —— 按钮(outline 为主 + 更圆润 10px + 悬停微抬升)——
// 主按钮:教育蓝实心 + 阴影(和教师端同深,阴影略轻)
.el-button--primary:not(.is-text):not(.is-link) {
background-color: var(--edu-primary) !important;
background-image: none !important;
border-color: var(--edu-primary) !important;
color: #ffffff !important;
border-radius: var(--r-control) !important;
box-shadow: 0 2px 6px rgba(59,130,246,0.25) !important;
font-weight: 500;
font-size: var(--fs-body);
&:hover, &:focus {
background-color: var(--edu-primary-deep) !important;
border-color: var(--edu-primary-deep) !important;
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(59,130,246,0.32) !important;
}
}
// 默认按钮:白底 + 灰边 + 深字(ghost 次级)
.el-button:not(.is-text):not(.is-link):not(.el-button--primary):not(.el-button--success):not(.el-button--danger):not(.el-button--warning) {
background: #ffffff !important;
border: 1px solid var(--border-color) !important;
border-radius: var(--r-control) !important;
color: var(--text-strong) !important;
box-shadow: none !important;
font-size: var(--fs-body);
&:hover {
background: var(--edu-primary-soft) !important;
border-color: var(--edu-primary) !important;
color: var(--edu-primary) !important;
}
}
// text 按钮(列表行内操作 / 添加)
.el-button.is-text,
.el-button.is-link {
background: transparent !important;
border-color: transparent !important;
color: var(--edu-primary) !important;
font-weight: 500;
&:hover { color: var(--edu-primary-deep) !important; }
.el-icon { color: inherit !important; }
}
// —— 弹窗(flex 滚动 + EP 变量重设,从教师端陷阱 6/7 继承)——
.el-dialog {
display: flex !important;
flex-direction: column !important;
max-height: calc(100vh - 120px) !important;
border-radius: 14px !important;
box-shadow: 0 20px 56px rgba(30, 60, 100, 0.12) !important;
// dialog 内部 EP CSS 变量二次覆盖(html.dark 污染兜底)
--el-color-primary: #3b82f6;
--el-color-primary-light-3: #60a5fa;
--el-color-primary-light-9: #eff6ff;
}
.el-dialog__header {
background: #ffffff !important;
border-bottom: 1px solid var(--border-color);
padding: 16px 22px 12px;
.el-dialog__title {
color: var(--text-strong) !important;
font-size: 16px;
font-weight: 700;
}
}
.el-dialog__body {
background: #ffffff !important;
flex: 1 1 auto;
overflow-y: auto !important;
padding: 22px;
color: var(--text-strong);
}
.el-dialog__footer {
background: #ffffff !important;
border-top: 1px solid var(--border-color);
flex: 0 0 auto;
padding: 12px 22px;
text-align: right;
}
// 遮罩层
.el-overlay {
background-color: rgba(15, 23, 42, 0.45) !important;
backdrop-filter: none !important;
}
// —— 表格:白卡 + 灰线 + 名称首字徽标(10px 圆角)——
.el-table {
--el-table-bg-color: #ffffff;
--el-table-tr-bg-color: #ffffff;
--el-table-header-bg-color: #ffffff;
--el-table-border-color: var(--border-color);
--el-table-row-hover-bg-color: #f9fafb;
background: #ffffff !important;
border: 1px solid var(--border-color) !important;
border-radius: 14px;
overflow: hidden;
.el-table__header th {
background: #ffffff !important;
color: var(--text-strong) !important;
font-size: var(--fs-body);
font-weight: 600;
border-bottom: 1px solid var(--border-color) !important;
}
.el-table__body tr:hover > td {
background: #f9fafb !important;
}
.el-table__cell {
font-size: var(--fs-body);
color: var(--text-strong);
border-color: var(--border-color) !important;
}
}
// —— 分页(教育蓝当前页)——
.el-pagination {
.btn-prev, .btn-next, .el-pager li {
background: #ffffff !important;
border: 1px solid var(--border-color) !important;
color: var(--text-body) !important;
border-radius: var(--r-control) !important;
font-size: var(--fs-body);
&.is-active {
background: var(--edu-primary) !important;
border-color: var(--edu-primary) !important;
color: #ffffff !important;
}
&:hover:not(.is-disabled):not(.is-active) {
color: var(--edu-primary) !important;
border-color: var(--edu-primary) !important;
}
}
.el-pagination__total, .el-pagination__jump {
color: var(--text-body) !important;
font-size: var(--fs-body);
}
}
// —— 标签页 active(教育蓝下划线)——
.el-tabs__active-bar {
background-color: var(--edu-primary) !important;
}
.el-tabs__item {
color: var(--text-body) !important;
font-size: var(--fs-body);
&.is-active { color: var(--edu-primary) !important; font-weight: 600; }
&:hover { color: var(--edu-primary) !important; }
}
// —— 徽章/tag(学生端更圆润 10px)——
.el-tag {
border-radius: var(--r-control) !important;
font-size: var(--fs-body);
}
// —— radio / checkbox(覆盖 html.dark 的深色圆点)——
.el-radio__inner { border-color: var(--border-color) !important; }
.el-radio__input.is-checked .el-radio__inner {
border-color: var(--edu-primary) !important;
background: var(--edu-primary) !important;
}
.el-checkbox__inner { border-color: var(--border-color) !important; }
.el-checkbox__input.is-checked .el-checkbox__inner {
border-color: var(--edu-primary) !important;
background: var(--edu-primary) !important;
}
.el-radio__label, .el-checkbox__label { color: var(--text-strong) !important; }
// —— 滚动条(和教师端同风格,但 10px 更圆润)——
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb {
background: #d1d5db;
border-radius: 10px;
&:hover { background: #9ca3af; }
}
::-webkit-scrollbar-track { background: transparent; }
// —— Form label ——
.el-form-item__label {
color: var(--text-strong) !important;
font-size: var(--fs-body);
font-weight: 600;
}
}

@ -1,6 +1,28 @@
<template> <template>
<main class="exam-admin"> <main class="exam-admin">
<el-tabs v-model="activeTab" class="workspace-tabs"> <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"> <el-tab-pane label="题库管理" name="questions">
<section class="toolbar" aria-label="题库筛选"> <section class="toolbar" aria-label="题库筛选">
<el-select v-model="questionQuery.type" placeholder="全部题型" clearable @change="loadQuestions"> <el-select v-model="questionQuery.type" placeholder="全部题型" clearable @change="loadQuestions">
@ -58,27 +80,6 @@
</el-table> </el-table>
</el-tab-pane> </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 :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-tabs> </el-tabs>
<el-dialog v-model="questionDialog" :title="questionReadonly ? '查看题目' : (questionForm.id ? '编辑题目' : '新增题目')" width="760px" class="teacher-dialog" destroy-on-close> <el-dialog v-model="questionDialog" :title="questionReadonly ? '查看题目' : (questionForm.id ? '编辑题目' : '新增题目')" width="760px" class="teacher-dialog" destroy-on-close>
@ -111,7 +112,7 @@
</template> </template>
<script setup> <script setup>
import { computed, onMounted, reactive, ref } from "vue"; import { computed, onActivated, onMounted, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { useRouter } from "vue-router"; 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 { ArrowDown, Check, Close, DataAnalysis, Delete, Download, Edit, EditPen, FolderOpened, MagicStick, Paperclip, PictureFilled, Plus, RefreshLeft, Search, Upload } from "@element-plus/icons-vue";
@ -119,7 +120,7 @@ import { saveAs } from "file-saver";
import * as examApi from "@/api/exam"; import * as examApi from "@/api/exam";
const router = useRouter(); const router = useRouter();
const activeTab = ref("questions"); 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 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 questionDialog = ref(false); const questionFormRef = ref(); const questionReadonly = ref(false);
const importInput = ref(); const importFormat = ref("word"); const importInput = ref(); const importFormat = ref("word");
@ -136,6 +137,8 @@ const isChoice = computed(() => ["SINGLE", "MULTIPLE"].includes(questionForm.typ
const singleAnswer = computed({ get: () => questionForm.answer?.[0] || "", set: (value) => { questionForm.answer = value ? [value] : []; } }); const singleAnswer = computed({ get: () => questionForm.answer?.[0] || "", set: (value) => { questionForm.answer = value ? [value] : []; } });
onMounted(() => Promise.all([loadQuestions(), loadExams(), loadProjects()])); 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 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 { 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; } } async function loadExams() { loading.value = true; try { exams.value = (await examApi.getTeacherExams({ keyword: examKeyword.value })).data || []; } finally { loading.value = false; } }

Loading…
Cancel
Save