style: align student exam pages with dev-QQq

fix/exam-question-access-material-excel-delete-draft
chenyuan 3 weeks ago
parent 1779bbe9a7
commit 99f7646388

@ -1,124 +1,15 @@
<template> <template>
<main class="student-exams exam-page" v-loading="loading"> <main class="student-exams app-container" v-loading="loading">
<header class="search-head"> <header class="page-head"><div><span class="page-eyebrow">EXAMINATION</span><h1>考试考核</h1><p>按考试时间完成作答并查看已结束考试成绩</p></div><el-button class="refresh-btn" :icon="Refresh" circle title="刷新" @click="load" /></header>
<label for="exam-keyword">考试名称:</label> <section class="status-filter"><button v-for="item in filters" :key="item.value" :class="{ active: filter === item.value }" @click="filter=item.value">{{ item.label }}<b>{{ count(item.value) }}</b></button></section>
<el-input id="exam-keyword" v-model="keywordInput" class="keyword-input" clearable placeholder="请输入考试名称关键字" @keyup.enter="search" /> <section class="exam-list"><article v-for="exam in visibleExams" :key="exam.id" class="exam-row"><div class="date-block"><strong>{{ day(exam.startTime) }}</strong><span>{{ month(exam.startTime) }}</span></div><div class="exam-info"><div><el-tag :type="statusMeta[exam.status]?.tone">{{ statusMeta[exam.status]?.label || exam.status }}</el-tag><span v-if="exam.attemptStatus === 'SUBMITTED'" class="submitted">已交卷</span></div><h2>{{ exam.name }}</h2><p><el-icon><Clock /></el-icon>{{ formatTime(exam.startTime) }} 至 {{ formatTime(exam.endTime) }} · {{ exam.durationMinutes || durationBetween(exam) }} 分钟</p><p v-if="exam.teacherName || exam.classNames">发布人:{{ exam.teacherName || '—' }} · 参与班级:{{ exam.classNames || '—' }}</p></div><div class="exam-action"><el-button v-if="exam.status === 'IN_PROGRESS'" type="primary" :disabled="exam.attemptStatus === 'SUBMITTED'" @click="enter(exam)">{{ exam.attemptStatus === 'IN_PROGRESS' ? '继续考试' : exam.attemptStatus === 'SUBMITTED' ? '已交卷' : '进入考试' }}</el-button><el-button v-else-if="exam.status === 'ENDED' && exam.attemptStatus === 'SUBMITTED'" type="primary" plain @click="$router.push(`/examination/result/${exam.id}`)">查看成绩</el-button><span v-else-if="exam.status === 'NOT_STARTED'">{{ countdown(exam.startTime) }}</span><span v-else>未参加</span></div></article><el-empty v-if="!visibleExams.length" description="当前没有考试" /></section>
<el-button type="primary" class="search-button" @click="search">搜索</el-button>
<el-button class="refresh-button" :icon="Refresh" circle title="刷新" @click="load" />
</header>
<section v-if="statusSections.length" class="status-columns" aria-live="polite">
<section v-for="section in statusSections" :key="section.value" class="status-column">
<h2>{{ section.label }}</h2>
<div class="status-cards">
<article v-for="exam in section.items" :key="exam.id" class="exam-card">
<h3>{{ exam.name }}</h3>
<dl>
<div><dt>考试时间:</dt><dd>{{ formatTime(exam.startTime) }}~{{ formatTime(exam.endTime) }}</dd></div>
<div><dt>发布人:</dt><dd>{{ exam.teacherName || '—' }}</dd></div>
<div><dt>参与班级:</dt><dd>{{ exam.classNames || '—' }}</dd></div>
</dl>
<div class="card-action">
<el-button v-if="exam.status === 'IN_PROGRESS' && exam.attemptStatus !== 'SUBMITTED'" type="primary" @click="enter(exam)">{{ exam.attemptStatus === 'IN_PROGRESS' ? '继续考试' : '进入考试' }}</el-button>
<el-button v-else-if="exam.status === 'IN_PROGRESS'" type="primary" disabled>已交卷</el-button>
<el-button v-else-if="exam.status === 'ENDED' && exam.attemptStatus === 'SUBMITTED'" class="result-button" @click="$router.push(`/examination/result/${exam.id}`)">查看成绩</el-button>
<el-button v-else-if="exam.status === 'NOT_STARTED'" class="disabled-action" disabled>进入考试</el-button>
<el-button v-else class="disabled-action" disabled>{{ actionLabel(exam) }}</el-button>
</div>
</article>
</div>
</section>
</section>
<el-empty v-else description="暂无符合条件的考试" />
</main> </main>
</template> </template>
<script setup> <script setup>
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue"; import { useRouter } from "vue-router"; import { ElMessage } from "element-plus"; import { Clock, Refresh } from "@element-plus/icons-vue"; import * as examApi from "@/api/exam";
import { useRouter } from "vue-router"; const router=useRouter(),loading=ref(false),exams=ref([]),filter=ref("ALL"); const filters=[{label:"全部",value:"ALL"},{label:"未开始",value:"NOT_STARTED"},{label:"进行中",value:"IN_PROGRESS"},{label:"已结束",value:"ENDED"}]; const statusMeta={NOT_STARTED:{label:"未开始",tone:""},IN_PROGRESS:{label:"进行中",tone:"success"},ENDED:{label:"已结束",tone:"info"}}; const visibleExams=computed(()=>filter.value==="ALL"?exams.value:exams.value.filter((item)=>item.status===filter.value)); onMounted(load);
import { ElMessage } from "element-plus"; async function load(){loading.value=true;try{exams.value=(await examApi.getStudentExams()).data||[]}finally{loading.value=false}} async function enter(exam){try{const token=deviceToken(exam.id);await examApi.enterExam(exam.id,token);router.push(`/examination/take/${exam.id}`)}catch(error){ElMessage.error(error?.message||"进入考试失败")}} function deviceToken(id){const key=`exam-device-${id}`;let value=localStorage.getItem(key);if(!value){value=crypto?.randomUUID?.()||`${Date.now()}-${Math.random().toString(36).slice(2)}`;localStorage.setItem(key,value)}return value} function count(value){return value==="ALL"?exams.value.length:exams.value.filter((item)=>item.status===value).length} function day(value){return value?String(new Date(value).getDate()).padStart(2,"0"):"--"} function month(value){return value?`${new Date(value).getMonth()+1}月`:"—"} function formatTime(value){return value?new Date(value).toLocaleString("zh-CN",{hour12:false}).replaceAll("/","-"):"—"} function durationBetween(row){return row.startTime&&row.endTime?Math.max(0,Math.round((new Date(row.endTime)-new Date(row.startTime))/60000)):0} function countdown(value){const minutes=Math.max(0,Math.ceil((new Date(value)-Date.now())/60000));return minutes<60?`${minutes} 分钟后开始`:`${Math.ceil(minutes/60)} 小时后开始`}
import { Refresh } from "@element-plus/icons-vue";
import * as examApi from "@/api/exam";
const router = useRouter();
const loading = ref(false);
const exams = ref([]);
const keywordInput = ref("");
const keyword = ref("");
const knownStatuses = [
{ label: "进行中", value: "IN_PROGRESS" },
{ label: "未开始", value: "NOT_STARTED" },
{ label: "已结束", value: "ENDED" }
];
const statusLabels = { DRAFT: "未发布", IN_PROGRESS: "进行中", NOT_STARTED: "未开始", ENDED: "已结束" };
const statusSections = computed(() => {
const result = knownStatuses.map((status) => ({ ...status, items: filteredExams.value.filter((exam) => exam.status === status.value) }));
const known = new Set(knownStatuses.map((status) => status.value));
const extraStatuses = [...new Set(filteredExams.value.map((exam) => exam.status).filter((status) => !known.has(status)))];
extraStatuses.forEach((status) => result.push({ value: status, label: statusLabels[status] || status || "其他状态", items: filteredExams.value.filter((exam) => exam.status === status) }));
return result.filter((section) => section.items.length);
});
const filteredExams = computed(() => {
const value = keyword.value.trim().toLowerCase();
return value ? exams.value.filter((exam) => String(exam.name || "").toLowerCase().includes(value)) : exams.value;
});
onMounted(load);
async function load() {
loading.value = true;
try {
exams.value = (await examApi.getStudentExams()).data || [];
} finally {
loading.value = false;
}
}
function search() { keyword.value = keywordInput.value; }
async function enter(exam) {
try {
const token = deviceToken(exam.id);
await examApi.enterExam(exam.id, token);
router.push(`/examination/take/${exam.id}`);
} catch (error) {
ElMessage.error(error?.message || "进入考试失败");
}
}
function deviceToken(id) {
const key = `exam-device-${id}`;
let value = localStorage.getItem(key);
if (!value) { value = crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`; localStorage.setItem(key, value); }
return value;
}
function actionLabel(exam) {
if (exam.status === "NOT_STARTED") return "进入考试";
if (exam.status === "ENDED") return "未参加";
return statusLabels[exam.status] || "暂不可用";
}
function formatTime(value) {
return value ? new Date(value).toLocaleString("zh-CN", { hour12: false }).replaceAll("/", "-") : "—";
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.student-exams { width: 100%; max-width: none; min-height: calc(100vh - 104px); margin: 10px auto 32px; padding: 24px 32px 40px; } .student-exams{--student-accent:#1f2937;--student-accent-soft:#f3f4f6;--student-success:#4a9d6e;--student-border:#e5e7eb;--student-text:#1f2937;--student-muted:#6b7280;width:100%;margin:0;color:var(--student-text);line-height:1.55}.page-head{display:flex;justify-content:space-between;align-items:flex-start}.page-head h1{margin:6px 0 0;font-size:17px}.page-head p{margin:4px 0 0;color:var(--student-muted);font-size:12px}.page-eyebrow{color:var(--student-muted);font-size:11px;font-weight:700;letter-spacing:.12em}.refresh-btn{width:36px;height:36px;border:1px solid #d1d5db;border-radius:8px;background:#fff;color:#374151}.status-filter{display:flex;gap:28px;margin-top:20px;border-bottom:1px solid var(--student-border)}.status-filter button{position:relative;padding:10px 2px;border:0;background:none;color:var(--student-muted);font-size:13px;cursor:pointer}.status-filter button b{margin-left:7px;font-weight:500}.status-filter button.active{color:var(--student-text);font-weight:700}.status-filter button.active:after{position:absolute;right:0;bottom:-1px;left:0;height:3px;background:var(--student-accent);content:""}.exam-list{margin-top:14px;overflow:hidden;border:1px solid var(--student-border);border-radius:16px;background:#fff}.exam-row{display:grid;grid-template-columns:78px 1fr 150px;gap:22px;align-items:center;padding:20px;border-bottom:1px solid var(--student-border)}.exam-row:last-child{border-bottom:0}.date-block{display:grid;border-right:1px solid var(--student-border);text-align:center}.date-block strong{font-size:24px;line-height:1.2}.date-block span{color:var(--student-muted);font-size:12px}.exam-info h2{margin:8px 0;color:var(--student-text);font-size:15px}.exam-info p{display:flex;align-items:center;gap:5px;margin:4px 0 0;color:var(--student-muted);font-size:12px}.submitted{margin-left:8px;color:var(--student-success);font-size:12px}.exam-action{color:var(--student-muted);font-size:12px;text-align:right}.student-exams:deep(.el-tag){height:22px;padding:0 9px;border:1px solid var(--student-border);border-radius:6px;background:var(--student-accent-soft);color:#4b5563;font-size:11px}.student-exams:deep(.el-button){height:36px;padding:0 16px;border-radius:8px;font-size:13px;font-weight:600}.student-exams:deep(.el-button--primary){border-color:var(--student-accent);background:var(--student-accent);color:#fff}.student-exams:deep(.el-button--primary.is-plain){border-color:rgba(31,41,55,.4);background:#fff;color:var(--student-accent)}@media(max-width:650px){.exam-row{grid-template-columns:58px 1fr}.exam-action{grid-column:2;text-align:left}.status-filter{gap:14px;overflow-x:auto}}
.search-head { display: flex; align-items: center; gap: 12px; padding: 0 0 18px; color: var(--exam-text); font-size: 16px; font-weight: 700; border-bottom: 1px solid var(--exam-line); }
.keyword-input { width: min(360px, 42vw); :deep(.el-input__wrapper) { min-height: 38px; background: rgba(5, 29, 47, .78); border: 1px solid rgba(102, 183, 218, .55); box-shadow: none; } :deep(.el-input__inner) { color: var(--exam-text); } }
.search-button { min-width: 80px; height: 38px; border-radius: 7px; font-weight: 700; }
.refresh-button { margin-left: auto; }
.status-columns { display: grid; grid-template-columns: repeat(auto-fit, minmax(310px, 1fr)); gap: 26px; margin-top: 30px; align-items: start; }
.status-column { min-width: 0; }
.status-column > h2 { margin: 0 0 12px; padding: 10px 0 10px 14px; border-left: 4px solid var(--exam-accent); color: var(--exam-text-strong); font-size: 18px; font-weight: 800; }
.status-cards { display: grid; gap: 16px; }
.exam-card { min-height: 286px; padding: 26px 22px 20px; border: 1px solid var(--exam-card-border); border-radius: 2px; background: linear-gradient(150deg, rgba(15, 56, 79, .86), rgba(3, 22, 36, .96)); box-shadow: 0 12px 24px rgba(0, 0, 0, .16); transition: transform .2s ease, border-color .2s ease; &:hover { transform: translateY(-3px); border-color: var(--exam-primary-border); } }
.exam-card h3 { margin: 18px 0 28px; color: var(--exam-text-strong); font-size: 22px; line-height: 1.3; text-align: center; }
.exam-card dl { display: grid; gap: 9px; margin: 0; color: var(--exam-secondary); font-size: 14px; line-height: 1.4; }
.exam-card dl > div { display: grid; grid-template-columns: 76px minmax(0, 1fr); gap: 4px; }
.exam-card dt { color: var(--exam-muted); }
.exam-card dd { min-width: 0; margin: 0; overflow-wrap: anywhere; }
.card-action { display: flex; justify-content: center; margin-top: 24px; }
.card-action :deep(.el-button) { min-width: 150px; height: 42px; border-radius: 7px; font-size: 16px; font-weight: 700; }
.result-button { color: #fff; border-color: #c48727; background: #b87416; &:hover { color: #fff; border-color: #d59636; background: #c9851e; } }
.disabled-action { color: #dce8ef; border-color: rgba(149, 166, 176, .45); background: rgba(126, 136, 144, .72); }
@media (max-width: 760px) { .student-exams { padding: 20px 16px 30px; } .search-head { flex-wrap: wrap; } .keyword-input { width: calc(100% - 100px); } .refresh-button { margin-left: 0; } .status-columns { grid-template-columns: 1fr; margin-top: 22px; } }
</style> </style>

@ -1,83 +1,10 @@
<template> <template>
<main class="result-page exam-page" v-loading="loading"> <main class="result-page app-container" v-loading="loading"><header><el-button text @click="$router.push('/examination/index')">返回考试列表</el-button><h1>{{ result.examName }}</h1><p>试卷成绩与作答详情</p></header><section class="score-band"><div class="total"><span>总成绩</span><strong>{{ result.totalScore || 0 }}</strong><small>分</small></div><div><span>客观题正确率</span><b>{{ result.objectiveAccuracy || 0 }}%</b></div><div><span>答对</span><b>{{ result.objectiveCorrect || 0 }} 题</b></div><div><span>答错</span><b>{{ result.objectiveWrong || 0 }} 题</b></div><div><span>主观题得分</span><b>{{ result.subjectiveScore || 0 }} 分</b></div></section><div class="result-layout"><aside><h2>答题卡</h2><div><button v-for="q in result.questions || []" :key="q.id" :class="answerClass(q)" @click="currentId=q.id">{{ q.order }}</button></div></aside><article v-if="current" class="result-question"><header><el-tag :type="current.correct===false?'danger':'success'">{{ current.type==='SHORT_ANSWER'?'教师评分':current.correct?'回答正确':'回答错误' }}</el-tag><b>{{ current.awardedScore || 0 }} / {{ current.score }} 分</b></header><div class="question-meta"><span>题干({{ current.type }})</span><a v-if="current.attachmentUrl" :href="current.attachmentUrl" target="_blank">下载题目资料</a></div><h2>{{ current.order }}. {{ current.stem }}</h2><p v-for="o in current.options" :key="o.label">{{ o.label }}. {{ o.content }}</p><dl><dt>你的答案</dt><dd>{{ display(current.studentAnswer) }}</dd><dt>正确答案</dt><dd>{{ display(current.answer) }}</dd><template v-if="current.analysis"><dt>答案解析</dt><dd>{{ current.analysis }}</dd></template></dl><a v-if="current.answerFileUrl" :href="current.answerFileUrl" target="_blank">作答附件:{{ current.answerFileName }}</a></article></div><footer v-if="result.questions?.length" class="result-navigation"><el-button :disabled="currentIndex===0" @click="move(-1)">上一题</el-button><el-button type="primary" :disabled="currentIndex===result.questions.length-1" @click="move(1)">下一题</el-button><el-button @click="$router.push('/examination/index')">返回考试列表</el-button></footer></main>
<header v-if="current" class="result-toolbar">
<div class="toolbar-type">A卷</div>
<div class="score-item"><span>客观题答对题数:</span><b>{{ result.objectiveCorrect || 0 }}</b></div>
<div class="score-item"><span>客观题错题数:</span><b>{{ result.objectiveWrong || 0 }}</b></div>
<div class="score-item"><span>客观题正确率:</span><b>{{ result.objectiveAccuracy || 0 }}%</b></div>
<div class="score-item"><span>主观题得分:</span><b>{{ result.subjectiveScore || 0 }}分</b></div>
<div class="score-item total"><span>总成绩:</span><b>{{ result.totalScore || 0 }}分</b></div>
</header>
<div class="result-layout">
<aside class="answer-sheet">
<section v-for="group in grouped" :key="group.type" class="sheet-group">
<h2>{{ typeName[group.type] }}</h2>
<div class="sheet-grid"><button v-for="question in group.items" :key="question.id" :class="answerClass(question)" @click="select(question.id)">{{ question.order }}</button></div>
</section>
</aside>
<section v-if="current" class="result-question">
<div class="result-question-head"><span>题干({{ typeName[current.type] }}):</span><a v-if="current.attachmentUrl" :href="current.attachmentUrl" target="_blank" class="material-button"><el-icon><Download /></el-icon>下载题目资料</a><span v-if="current.type === 'SHORT_ANSWER'" class="teacher-score">老师评分:{{ current.awardedScore || 0 }}分</span></div>
<p class="question-stem">{{ current.order }}. {{ current.stem }}</p>
<p v-for="option in current.options" :key="option.label" class="option-line">{{ option.label }}. {{ option.content }}</p>
<div class="answer-content"><label>你的答案:</label><div>{{ display(current.studentAnswer) }}</div></div>
<div v-if="current.answerFileUrl" class="answer-content"><label>作答文件:</label><a :href="current.answerFileUrl" target="_blank">{{ current.answerFileName }}</a></div>
<div class="answer-content"><label>参考答案:</label><div>{{ display(current.answer) }}</div></div>
<div v-if="current.analysis" class="answer-content"><label>答案解析:</label><div>{{ current.analysis }}</div></div>
</section>
</div>
<footer v-if="result.questions?.length" class="result-navigation"><button class="previous" :disabled="currentIndex === 0" @click="move(-1)">上一题</button><button class="next" :disabled="currentIndex === result.questions.length - 1" @click="move(1)">下一题</button><button class="back" @click="$router.push('/examination/index')">返回考试列表</button></footer>
</main>
</template> </template>
<script setup> <script setup>
import { computed, onMounted, ref } from "vue"; import { computed,onMounted,ref } from "vue"; import { useRoute } from "vue-router"; import { getExamResult } from "@/api/exam";
import { useRoute } from "vue-router"; const route=useRoute(),loading=ref(false),result=ref({questions:[]}),currentId=ref(null); const current=computed(()=>result.value.questions?.find(q=>q.id===currentId.value)); const currentIndex=computed(()=>result.value.questions?.findIndex(q=>q.id===currentId.value)??-1); onMounted(async()=>{loading.value=true;try{result.value=(await getExamResult(route.params.id)).data;currentId.value=result.value.questions?.[0]?.id}finally{loading.value=false}}); function select(id){currentId.value=id} function move(step){currentId.value=result.value.questions[currentIndex.value+step]?.id} function display(v){if(Array.isArray(v))return v.join("、");if(v===true)return"正确";if(v===false)return"错误";return v||"未作答"} function answerClass(q){return{current:q.id===currentId.value,wrong:q.correct===false,right:q.correct===true,subjective:q.type==="SHORT_ANSWER"}}
import { Download } from "@element-plus/icons-vue";
import { getExamResult } from "@/api/exam";
const route = useRoute(); const loading = ref(false); const result = ref({ questions: [] }); const currentId = ref(null);
const typeName = { SINGLE: "单选题", MULTIPLE: "多选题", TRUE_FALSE: "判断题", SHORT_ANSWER: "简答题" };
const current = computed(() => result.value.questions?.find((q) => q.id === currentId.value));
const currentIndex = computed(() => result.value.questions?.findIndex((q) => q.id === currentId.value) ?? -1);
const grouped = computed(() => Object.keys(typeName).map((type) => ({ type, items: (result.value.questions || []).filter((q) => q.type === type) })).filter((group) => group.items.length));
onMounted(async () => { loading.value = true; try { result.value = (await getExamResult(route.params.id)).data; currentId.value = result.value.questions?.[0]?.id; } finally { loading.value = false; } });
function select(id) { currentId.value = id; }
function move(step) { currentId.value = result.value.questions[currentIndex.value + step]?.id; }
function display(value) { if (Array.isArray(value)) return value.join("、"); if (value === true) return "正确"; if (value === false) return "错误"; return value || "未作答"; }
function answerClass(question) { return { current: question.id === currentId.value, wrong: question.correct === false, right: question.correct === true, subjective: question.type === "SHORT_ANSWER" }; }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.result-page { min-height: calc(100vh - 104px); margin: 10px auto 32px; padding: 18px 30px 34px; } .result-page{--accent:#1f2937;--danger:#d9554c;--success:#4a9d6e;--border:#e5e7eb;--muted:#6b7280;width:100%;margin:0;color:var(--accent);line-height:1.55}.result-page>header h1{margin:10px 0 4px;font-size:17px}.result-page>header p{margin:0;color:var(--muted);font-size:12px}.result-page:deep(.el-button--text){padding:0;color:#4b5563;font-size:12px}.score-band{display:grid;grid-template-columns:1.25fr repeat(4,1fr);margin:18px 0;overflow:hidden;border:1px solid var(--border);border-radius:14px;background:#fff}.score-band>div{display:grid;gap:6px;padding:16px 18px;border-right:1px solid var(--border)}.score-band>div:last-child{border-right:0}.score-band span{color:var(--muted);font-size:12px}.score-band b{font-size:18px}.score-band .total{grid-template-columns:auto auto 1fr;align-items:end}.score-band .total span{grid-column:1/4}.score-band strong{font-size:34px;line-height:1}.score-band small{padding-bottom:4px;color:var(--muted)}.result-layout{display:grid;grid-template-columns:230px 1fr;gap:20px;align-items:start}.result-layout aside,.result-question{border:1px solid var(--border);border-radius:14px;background:#fff}.result-layout aside{padding:16px}.result-layout aside h2{margin:0 0 14px;font-size:14px}.result-layout aside>div{display:grid;grid-template-columns:repeat(5,1fr);gap:7px}.result-layout aside button{aspect-ratio:1;border:1px solid var(--border);border-radius:6px;background:#fff;color:var(--muted);cursor:pointer}.result-layout aside button.right{border-color:var(--success);color:var(--success)}.result-layout aside button.wrong{border-color:var(--danger);color:var(--danger)}.result-layout aside button.subjective{border-color:#9ca3af}.result-layout aside button.current{outline:2px solid var(--accent);outline-offset:2px}.result-question{padding:18px 20px}.result-question>header{display:flex;justify-content:space-between;align-items:center}.result-question>header b{font-size:13px}.question-meta{display:flex;justify-content:space-between;align-items:center;color:var(--muted);font-size:12px}.question-meta a{margin:0}.result-question h2{margin:16px 0;color:var(--accent);font-size:15px;line-height:1.65}.result-question p{margin:6px 0;color:#4b5563;font-size:13px}.result-question dl{display:grid;grid-template-columns:90px 1fr;gap:10px 14px;margin-top:18px;padding:14px 16px;border:1px solid var(--border);border-radius:10px;font-size:13px}.result-question dt{color:var(--muted);font-size:12px}.result-question dd{margin:0;white-space:pre-wrap;line-height:1.6}.result-question a{display:inline-block;margin-top:12px;color:var(--accent);font-size:12px;font-weight:600}.result-navigation{display:flex;justify-content:flex-end;gap:10px;margin-top:18px}.result-page:deep(.el-tag){height:22px;padding:0 9px;border-radius:6px;font-size:11px;font-weight:700}.result-page:deep(.el-tag--success){border-color:var(--success);background:#fff;color:var(--success)}.result-page:deep(.el-tag--danger){border-color:var(--danger);background:#fff;color:var(--danger)}@media(max-width:760px){.score-band{grid-template-columns:1fr 1fr}.result-layout{grid-template-columns:1fr}.result-navigation{justify-content:stretch}.result-navigation .el-button{flex:1}}
.result-toolbar { display: grid; grid-template-columns: 174px repeat(5, 1fr); align-items: center; min-height: 72px; border-bottom: 1px solid var(--exam-line); }
.toolbar-type { align-self: stretch; display: flex; align-items: center; padding-left: 18px; color: var(--exam-text-strong); font-size: 24px; font-weight: 900; }
.score-item { display: grid; gap: 6px; padding: 10px 16px; color: var(--exam-muted); font-size: 14px; border-left: 1px solid var(--exam-line); text-align: center; }
.score-item b { color: var(--exam-text); font-size: 19px; }
.score-item.total b { color: #7fe3ff; font-size: 22px; }
.result-layout { display: grid; grid-template-columns: 34% minmax(0, 1fr); gap: 34px; margin-top: 18px; }
.answer-sheet { padding: 0 16px 18px 0; border-right: 1px solid var(--exam-line); }
.sheet-group { margin-bottom: 28px; }
.sheet-group h2 { width: 174px; margin: 0 0 28px; padding: 14px 10px; border-radius: 8px; color: #fff; background: #159bd0; font-size: 18px; text-align: center; }
.sheet-grid { display: grid; grid-template-columns: repeat(8, minmax(38px, 1fr)); gap: 18px 20px; }
.sheet-grid button { min-height: 56px; border: 2px solid rgba(130, 137, 143, .34); border-radius: 3px; color: var(--exam-secondary); background: transparent; font-size: 18px; cursor: pointer; &.right { border-color: #3498f4; color: #fff; background: #3b9df4; } &.wrong { border-color: #f06d63; color: #fff; background: #b94f48; } &.subjective { border-color: #8a9aa7; } &.current { outline: 2px solid var(--exam-current); outline-offset: 2px; } }
.result-question { min-width: 0; padding: 6px 0; }
.result-question-head { display: flex; align-items: center; gap: 24px; min-height: 44px; color: var(--exam-text); font-size: 17px; font-weight: 700; }
.material-button { display: inline-flex; align-items: center; gap: 6px; min-height: 42px; padding: 0 20px; border-radius: 7px; color: #fff; background: #159bd0; font-weight: 700; text-decoration: none; }
.teacher-score { margin-left: auto; font-weight: 500; }
.question-stem { margin: 24px 0 22px; color: var(--exam-text); font-size: 19px; line-height: 1.65; white-space: pre-wrap; }
.option-line { margin: 0 0 8px; color: var(--exam-secondary); line-height: 1.55; }
.answer-content { display: grid; grid-template-columns: 100px minmax(0, 1fr); gap: 8px 18px; margin-top: 24px; color: var(--exam-text); font-size: 17px; line-height: 1.65; }
.answer-content label { white-space: nowrap; }
.answer-content > div { white-space: pre-wrap; overflow-wrap: anywhere; }
.answer-content a { color: var(--exam-accent); }
.result-navigation { display: flex; justify-content: center; gap: 110px; margin: 38px 0 0 34%; }
.result-navigation button { min-width: 190px; height: 56px; border: 0; border-radius: 8px; color: #fff; font-size: 18px; font-weight: 700; cursor: pointer; &:disabled { opacity: .45; cursor: not-allowed; } }
.result-navigation .previous { background: #bd7416; }
.result-navigation .next, .result-navigation .back { background: #159bd0; }
@media (max-width: 1100px) { .result-toolbar { grid-template-columns: 120px repeat(5, 1fr); } .result-layout { grid-template-columns: 1fr; } .answer-sheet { border-right: 0; border-bottom: 1px solid var(--exam-line); padding-right: 0; } .result-navigation { margin-left: 0; gap: 20px; } }
@media (max-width: 700px) { .result-page { padding: 14px 16px 24px; } .result-toolbar { grid-template-columns: 1fr 1fr; gap: 4px; padding-bottom: 10px; } .toolbar-type { grid-row: span 2; } .score-item { border-left: 0; } .sheet-grid { grid-template-columns: repeat(5, 1fr); gap: 10px; } .result-navigation { gap: 10px; } .result-navigation button { min-width: 0; flex: 1; font-size: 14px; } }
</style> </style>

@ -1,94 +1,14 @@
<template> <template>
<main class="take-page exam-page" v-loading="loading"> <main class="take-page app-container" v-loading="loading">
<header v-if="current" class="exam-toolbar"> <header v-if="current" class="take-head"><div><h1>{{ paper.name || '在线考试' }}</h1><p>考试结束时间 {{ formatTime(paper.endTime) }}</p></div><div class="timer" :class="{ urgent: remaining < 300 }"><span>剩余时间</span><strong>{{ timerText }}</strong></div><el-button type="danger" plain @click="confirmSubmit">交卷</el-button></header>
<div class="toolbar-type">{{ typeName[current.type] }}</div> <div class="exam-body"><aside class="answer-sheet"><h2>答题卡</h2><div v-for="group in grouped" :key="group.type" class="sheet-group"><h3>{{ typeName[group.type] }}</h3><div><button v-for="question in group.items" :key="question.id" :class="{ current:current?.id===question.id,answered:isAnswered(question) }" @click="select(question.id)">{{ question.order }}</button></div></div><footer><span><i class="answered-dot" />已答</span><span><i />未答</span></footer></aside><section v-if="current" class="question-panel" @copy="guardCopy" @contextmenu="guardContextMenu"><header><span>{{ typeName[current.type] }}</span><b>{{ current.score }} 分</b></header><h2>{{ current.order }}. {{ current.stem }}</h2><a v-if="current.attachmentUrl" :href="current.attachmentUrl" target="_blank" class="material"><el-icon><Download /></el-icon>下载题目资料 {{ current.attachmentName }}</a><el-radio-group v-if="current.type==='SINGLE'" v-model="answers[current.id]" class="choice-list" @change="persist(current)"><el-radio v-for="option in current.options" :key="option.label" :label="option.label"><b>{{ option.label }}</b>{{ option.content }}</el-radio></el-radio-group><el-checkbox-group v-else-if="current.type==='MULTIPLE'" v-model="answers[current.id]" class="choice-list" @change="persist(current)"><el-checkbox v-for="option in current.options" :key="option.label" :label="option.label"><b>{{ option.label }}</b>{{ option.content }}</el-checkbox></el-checkbox-group><el-radio-group v-else-if="current.type==='TRUE_FALSE'" v-model="answers[current.id]" class="choice-list judgement" @change="persist(current)"><el-radio :label="true"><b>✓</b>正确</el-radio><el-radio :label="false"><b>×</b>错误</el-radio></el-radio-group><template v-else><el-input v-model="answers[current.id]" type="textarea" :rows="10" placeholder="请输入答案" @blur="persist(current)" @paste="guardPaste" /><div class="file-answer"><span>作答文件:</span><el-upload :show-file-list="false" :http-request="uploadAnswer" accept=".pdf,.doc,.docx,.png,.jpg,.jpeg"><el-button type="primary" :icon="Upload">上传</el-button></el-upload><a v-if="files[current.id]" :href="files[current.id].url" target="_blank">{{ files[current.id].name }}</a></div></template><footer class="question-actions"><el-button :disabled="currentIndex===0" @click="move(-1)">上一题</el-button><el-button type="primary" :disabled="currentIndex===paper.questions.length-1" @click="move(1)">下一题</el-button></footer></section></div>
<div class="toolbar-title">题干({{ typeName[current.type] }}):</div>
<a v-if="current.attachmentUrl" :href="current.attachmentUrl" target="_blank" class="material-button"><el-icon><Download /></el-icon>下载题目资料</a>
<div class="timer" :class="{ urgent: remaining < 300 }">倒计时:{{ timerText }}</div>
<button class="submit-button" @click="confirmSubmit"><el-icon><Promotion /></el-icon>交卷</button>
</header>
<div class="exam-layout">
<aside class="answer-sheet">
<section v-for="group in grouped" :key="group.type" class="sheet-group">
<h2>{{ typeName[group.type] }}</h2>
<div class="sheet-grid"><button v-for="question in group.items" :key="question.id" :class="{ answered: isAnswered(question), current: current?.id === question.id }" @click="select(question.id)">{{ question.order }}</button></div>
</section>
</aside>
<section v-if="current" class="question-panel" @copy="guardCopy" @contextmenu="guardContextMenu">
<p class="question-stem">{{ current.order }}. {{ current.stem }}</p>
<el-radio-group v-if="current.type === 'SINGLE'" v-model="answers[current.id]" class="choice-list" @change="persist(current)"><el-radio v-for="option in current.options" :key="option.label" :label="option.label"><b>{{ option.label }}</b>{{ option.content }}</el-radio></el-radio-group>
<el-checkbox-group v-else-if="current.type === 'MULTIPLE'" v-model="answers[current.id]" class="choice-list" @change="persist(current)"><el-checkbox v-for="option in current.options" :key="option.label" :label="option.label"><b>{{ option.label }}</b>{{ option.content }}</el-checkbox></el-checkbox-group>
<el-radio-group v-else-if="current.type === 'TRUE_FALSE'" v-model="answers[current.id]" class="choice-list judgement" @change="persist(current)"><el-radio :label="true"><b>✓</b>正确</el-radio><el-radio :label="false"><b>×</b>错误</el-radio></el-radio-group>
<template v-else><el-input v-model="answers[current.id]" type="textarea" :rows="10" placeholder="请输入答案" @blur="persist(current)" @paste="guardPaste" /><div class="file-answer"><span>作答文件:</span><el-upload :show-file-list="false" :http-request="uploadAnswer" accept=".pdf,.doc,.docx,.png,.jpg,.jpeg"><el-button type="primary" :icon="Upload">上传</el-button></el-upload><a v-if="files[current.id]" :href="files[current.id].url" target="_blank">{{ files[current.id].name }}</a></div></template>
</section>
</div>
<footer v-if="paper.questions?.length" class="exam-navigation"><button class="previous" :disabled="currentIndex === 0" @click="move(-1)">上一题</button><button class="next" :disabled="currentIndex === paper.questions.length - 1" @click="move(1)">下一题</button></footer>
</main> </main>
</template> </template>
<script setup> <script setup>
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue"; import { computed,onBeforeUnmount,onMounted,reactive,ref } from "vue"; import { useRoute,useRouter } from "vue-router"; import { ElMessage,ElMessageBox } from "element-plus"; import { Download,Upload } from "@element-plus/icons-vue"; import * as examApi from "@/api/exam";
import { useRoute, useRouter } from "vue-router"; const route=useRoute(),router=useRouter(),examId=route.params.id,loading=ref(false),paper=ref({questions:[]}),currentId=ref(null),remaining=ref(0),answers=reactive({}),files=reactive({});let ticker; const typeName={SINGLE:"单选题",MULTIPLE:"多选题",TRUE_FALSE:"判断题",SHORT_ANSWER:"简答题"}; const current=computed(()=>paper.value.questions.find(q=>q.id===currentId.value)); const currentIndex=computed(()=>paper.value.questions.findIndex(q=>q.id===currentId.value)); const grouped=computed(()=>Object.keys(typeName).map(type=>({type,items:paper.value.questions.filter(q=>q.type===type)})).filter(g=>g.items.length)); const timerText=computed(()=>`${String(Math.floor(remaining.value/60)).padStart(2,"0")} 分钟 ${String(remaining.value%60).padStart(2,"0")} 秒`); onMounted(load); onBeforeUnmount(()=>clearInterval(ticker));
import { ElMessage, ElMessageBox } from "element-plus"; async function load(){loading.value=true;try{const token=deviceToken();paper.value=(await examApi.getActivePaper(examId,token)).data;paper.value.questions.forEach(q=>{answers[q.id]=q.studentAnswer??(q.type==="MULTIPLE"?[]:"");if(q.answerFileUrl)files[q.id]={name:q.answerFileName,url:q.answerFileUrl}});currentId.value=paper.value.questions[0]?.id;remaining.value=Math.max(0,Math.floor((new Date(paper.value.endTime)-Date.now())/1000));ticker=setInterval(()=>{remaining.value=Math.max(0,remaining.value-1);if(remaining.value===0){clearInterval(ticker);autoSubmit()}},1000)}finally{loading.value=false}} function deviceToken(){return localStorage.getItem(`exam-device-${examId}`)||""} function select(id){currentId.value=id} function move(step){currentId.value=paper.value.questions[currentIndex.value+step]?.id} function isAnswered(q){const answer=answers[q.id];return(Array.isArray(answer)?answer.length:Boolean(String(answer??"").trim()))||Boolean(files[q.id])} function guardCopy(event){if(paper.value.allowCopyQuestion||event.target?.closest("textarea,input"))return;event.preventDefault();ElMessage.warning("该考试不允许复制试题")} function guardContextMenu(event){if(paper.value.allowCopyQuestion||event.target?.closest("textarea,input"))return;event.preventDefault()} function guardPaste(event){if(!paper.value.allowPasteAnswer){event.preventDefault();ElMessage.warning("该考试不允许粘贴答案")}} async function persist(q){await examApi.saveExamAnswer(examId,q.id,{deviceToken:deviceToken(),answer:answers[q.id]})} async function uploadAnswer({file}){const form=new FormData();form.append("file",file);const data=(await examApi.uploadExamAnswerFile(examId,current.value.id,deviceToken(),form)).data;files[current.value.id]={name:data.name,url:data.url};ElMessage.success("作答文件已上传")} async function confirmSubmit(){const unanswered=paper.value.questions.filter(q=>!isAnswered(q)).length;await ElMessageBox.confirm(unanswered?`还有 ${unanswered} 道题未作答,确认交卷?`:"确认提交试卷?","确认交卷",{type:"warning",confirmButtonText:"确认交卷"});await doSubmit()} async function doSubmit(){await examApi.submitExam(examId,deviceToken());ElMessage.success("交卷成功");router.replace(`/examination/result/${examId}`)} async function autoSubmit(){try{await doSubmit()}catch{router.replace("/examination/index")}} function formatTime(v){return v?new Date(v).toLocaleString("zh-CN",{hour12:false}).replaceAll("/","-"):"—"}
import { Download, Promotion, Upload } from "@element-plus/icons-vue";
import * as examApi from "@/api/exam";
const route = useRoute(); const router = useRouter(); const examId = route.params.id;
const loading = ref(false); const paper = ref({ questions: [] }); const currentId = ref(null); const remaining = ref(0); const answers = reactive({}); const files = reactive({}); let ticker;
const typeName = { SINGLE: "单选题", MULTIPLE: "多选题", TRUE_FALSE: "判断题", SHORT_ANSWER: "简答题" };
const current = computed(() => paper.value.questions.find((q) => q.id === currentId.value));
const currentIndex = computed(() => paper.value.questions.findIndex((q) => q.id === currentId.value));
const grouped = computed(() => Object.keys(typeName).map((type) => ({ type, items: paper.value.questions.filter((q) => q.type === type) })).filter((group) => group.items.length));
const timerText = computed(() => `${String(Math.floor(remaining.value / 60)).padStart(2, "0")} 分钟 ${String(remaining.value % 60).padStart(2, "0")} 秒`);
onMounted(load); onBeforeUnmount(() => clearInterval(ticker));
async function load() { loading.value = true; try { const token = deviceToken(); paper.value = (await examApi.getActivePaper(examId, token)).data; paper.value.questions.forEach((q) => { answers[q.id] = q.studentAnswer ?? (q.type === "MULTIPLE" ? [] : ""); if (q.answerFileUrl) files[q.id] = { name: q.answerFileName, url: q.answerFileUrl }; }); currentId.value = paper.value.questions[0]?.id; remaining.value = Math.max(0, Math.floor((new Date(paper.value.endTime) - Date.now()) / 1000)); ticker = setInterval(() => { remaining.value = Math.max(0, remaining.value - 1); if (remaining.value === 0) { clearInterval(ticker); autoSubmit(); } }, 1000); } finally { loading.value = false; } }
function deviceToken() { return localStorage.getItem(`exam-device-${examId}`) || ""; }
function select(id) { currentId.value = id; }
function move(step) { currentId.value = paper.value.questions[currentIndex.value + step]?.id; }
function isAnswered(q) { const answer = answers[q.id]; return (Array.isArray(answer) ? answer.length : Boolean(String(answer ?? "").trim())) || Boolean(files[q.id]); }
function guardCopy(event) { if (paper.value.allowCopyQuestion || event.target?.closest("textarea,input")) return; event.preventDefault(); ElMessage.warning("该考试不允许复制试题"); }
function guardContextMenu(event) { if (paper.value.allowCopyQuestion || event.target?.closest("textarea,input")) return; event.preventDefault(); }
function guardPaste(event) { if (!paper.value.allowPasteAnswer) { event.preventDefault(); ElMessage.warning("该考试不允许粘贴答案"); } }
async function persist(q) { await examApi.saveExamAnswer(examId, q.id, { deviceToken: deviceToken(), answer: answers[q.id] }); }
async function uploadAnswer({ file }) { const form = new FormData(); form.append("file", file); const data = (await examApi.uploadExamAnswerFile(examId, current.value.id, deviceToken(), form)).data; files[current.value.id] = { name: data.name, url: data.url }; ElMessage.success("作答文件已上传"); }
async function confirmSubmit() { const unanswered = paper.value.questions.filter((q) => !isAnswered(q)).length; await ElMessageBox.confirm(unanswered ? `还有 ${unanswered} 道题未作答,确认交卷?` : "确认提交试卷?", "确认交卷", { type: "warning", confirmButtonText: "确认交卷" }); await doSubmit(); }
async function doSubmit() { await examApi.submitExam(examId, deviceToken()); ElMessage.success("交卷成功"); router.replace(`/examination/result/${examId}`); }
async function autoSubmit() { try { await doSubmit(); } catch { router.replace("/examination/index"); } }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.take-page { min-height: calc(100vh - 104px); margin: 10px auto 32px; padding: 18px 30px 34px; } .take-page{--accent:#1f2937;--border:#e5e7eb;--muted:#6b7280;min-height:calc(100vh - 84px);background:#f8fafc;color:#1f2937}.take-head{height:72px;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;padding:0 28px;border-bottom:1px solid var(--border);background:#fff}.take-head h1{margin:0;font-size:17px}.take-head p{margin:4px 0 0;color:var(--muted);font-size:12px}.take-head>button{justify-self:end}.timer{display:grid;gap:2px;text-align:center;color:var(--muted);font-size:12px}.timer strong{color:var(--accent);font-size:18px;font-variant-numeric:tabular-nums}.timer.urgent strong{color:#c2413b}.exam-body{display:grid;grid-template-columns:250px minmax(0,1fr);gap:20px;padding:20px 22px}.answer-sheet,.question-panel{border:1px solid var(--border);border-radius:14px;background:#fff}.answer-sheet{align-self:start;padding:16px;position:sticky;top:16px}.answer-sheet h2{margin:0 0 14px;font-size:14px}.sheet-group{margin-bottom:18px}.sheet-group h3{margin:0 0 10px;color:var(--muted);font-size:12px}.sheet-group>div{display:grid;grid-template-columns:repeat(5,1fr);gap:7px}.sheet-group button{aspect-ratio:1;border:1px solid var(--border);border-radius:6px;background:#fff;color:var(--muted);cursor:pointer}.sheet-group button.answered{border-color:#4a9d6e;color:#4a9d6e;background:#f0fdf4}.sheet-group button.current{outline:2px solid var(--accent);outline-offset:2px}.answer-sheet footer{display:flex;gap:14px;color:var(--muted);font-size:11px}.answer-sheet i{display:inline-block;width:8px;height:8px;margin-right:4px;border:1px solid #d1d5db;border-radius:50%}.answer-sheet i.answered-dot{border-color:#4a9d6e;background:#4a9d6e}.question-panel{min-height:610px;padding:22px 28px 80px;position:relative}.question-panel>header{display:flex;justify-content:space-between;color:var(--muted);font-size:13px}.question-panel>header b{color:var(--accent)}.question-panel h2{margin:18px 0;color:#1f2937;font-size:17px;line-height:1.65}.material{display:inline-flex;gap:6px;margin-bottom:16px;color:var(--accent);font-size:12px;text-decoration:none}.choice-list{display:grid;gap:10px;width:100%}.question-panel:deep(.el-radio),.question-panel:deep(.el-checkbox){height:auto;margin:0;padding:13px 16px;border:1px solid var(--border);border-radius:8px;background:#fff}.question-panel:deep(.el-radio:hover),.question-panel:deep(.el-checkbox:hover),.question-panel:deep(.is-checked){border-color:var(--accent);background:#f9fafb}.question-panel:deep(.el-radio__label),.question-panel:deep(.el-checkbox__label){display:flex;gap:12px;color:#1f2937;font-size:13px;line-height:1.55}.judgement{grid-template-columns:1fr 1fr}.question-panel:deep(.el-textarea__inner){border-color:var(--border);border-radius:8px;box-shadow:none}.file-answer{display:flex;align-items:center;gap:12px;margin-top:12px;color:var(--muted);font-size:12px}.file-answer a{color:var(--accent)}.question-actions{position:absolute;right:28px;bottom:24px;left:28px;display:flex;justify-content:space-between}.take-page:deep(.el-button){height:40px;border-radius:10px;font-weight:700}.take-page:deep(.el-button--primary){border-color:var(--accent);background:var(--accent)}.take-page:deep(.el-button--danger.is-plain){border-color:rgba(31,41,55,.4);background:#fff;color:var(--accent)}@media(max-width:760px){.take-head{grid-template-columns:1fr auto;height:auto;padding:10px 14px}.timer{grid-column:1/3;margin-top:8px}.exam-body{grid-template-columns:1fr;padding:12px}.answer-sheet{position:static}.question-panel{padding:20px 18px 78px}.question-actions{right:18px;left:18px}}
.exam-toolbar { display: grid; grid-template-columns: 174px minmax(180px, 1fr) auto auto auto; align-items: center; gap: 18px; min-height: 70px; padding: 0 10px 18px; border-bottom: 1px solid var(--exam-line); }
.toolbar-type, .material-button, .submit-button { display: inline-flex; align-items: center; justify-content: center; min-height: 50px; border-radius: 8px; color: #fff; font-size: 17px; font-weight: 700; }
.toolbar-type { background: #159bd0; }
.toolbar-title { color: var(--exam-text); font-size: 17px; font-weight: 700; text-align: center; }
.material-button { gap: 6px; min-width: 176px; padding: 0 18px; text-decoration: none; background: #159bd0; }
.timer { color: #ff4646; font-size: 20px; font-weight: 800; white-space: nowrap; &.urgent { color: #ff3131; } }
.submit-button { gap: 6px; min-width: 128px; border: 0; cursor: pointer; background: #60bf32; &:hover { background: #4fae24; } }
.exam-layout { display: grid; grid-template-columns: 34% minmax(0, 1fr); gap: 34px; margin-top: 18px; }
.answer-sheet { padding: 0 16px 18px 0; border-right: 1px solid var(--exam-line); }
.sheet-group { margin-bottom: 28px; }
.sheet-group h2 { width: 174px; margin: 0 0 28px; padding: 14px 10px; border-radius: 8px; color: #fff; background: #159bd0; font-size: 18px; text-align: center; }
.sheet-grid { display: grid; grid-template-columns: repeat(8, minmax(38px, 1fr)); gap: 18px 20px; }
.sheet-grid button { min-height: 56px; border: 2px solid rgba(130, 137, 143, .34); border-radius: 3px; color: var(--exam-secondary); background: transparent; font-size: 18px; cursor: pointer; &:hover { border-color: var(--exam-accent); } &.answered { border-color: #3498f4; color: #fff; background: #3b9df4; } &.current { outline: 2px solid var(--exam-current); outline-offset: 2px; } }
.question-panel { min-width: 0; padding: 4px 0 0; }
.question-stem { margin: 0 0 22px; color: var(--exam-text); font-size: 19px; line-height: 1.65; white-space: pre-wrap; }
.choice-list { display: grid; gap: 12px; width: 100%; }
.judgement { grid-template-columns: 1fr 1fr; }
.question-panel :deep(.el-radio), .question-panel :deep(.el-checkbox) { min-height: 42px; margin-right: 0; padding: 10px 14px; border: 1px solid var(--exam-line); border-radius: 4px; color: var(--exam-secondary); background: rgba(4, 24, 39, .45); }
.question-panel :deep(.el-radio.is-checked), .question-panel :deep(.el-checkbox.is-checked) { border-color: #3b9df4; background: rgba(59, 157, 244, .18); }
.question-panel :deep(.el-radio__label), .question-panel :deep(.el-checkbox__label) { color: inherit; }
.question-panel :deep(.el-input__wrapper), .question-panel :deep(.el-textarea__inner) { color: var(--exam-text); background: rgba(3, 20, 34, .58); border-color: var(--exam-line); box-shadow: none; }
.file-answer { display: flex; align-items: center; gap: 14px; margin-top: 26px; color: var(--exam-text); font-size: 17px; }
.file-answer a { color: var(--exam-accent); }
.exam-navigation { display: flex; justify-content: center; gap: 140px; margin: 38px 0 0 34%; }
.exam-navigation button { min-width: 190px; height: 56px; border: 0; border-radius: 8px; color: #fff; font-size: 18px; font-weight: 700; cursor: pointer; &:disabled { opacity: .45; cursor: not-allowed; } }
.exam-navigation .previous { background: #bd7416; }
.exam-navigation .next { background: #159bd0; }
@media (max-width: 1000px) { .exam-toolbar { grid-template-columns: 140px 1fr auto; .toolbar-title { display: none; } .material-button { display: none; } } .exam-layout { grid-template-columns: 1fr; } .answer-sheet { border-right: 0; border-bottom: 1px solid var(--exam-line); padding-right: 0; } .exam-navigation { margin-left: 0; gap: 24px; } }
@media (max-width: 620px) { .take-page { padding: 14px 16px 24px; } .exam-toolbar { grid-template-columns: 1fr 1fr; gap: 10px; } .toolbar-type, .timer, .submit-button { min-height: 42px; font-size: 14px; } .timer { order: 3; } .submit-button { order: 4; } .sheet-grid { grid-template-columns: repeat(5, 1fr); gap: 10px; } .exam-navigation { gap: 12px; } .exam-navigation button { min-width: 0; flex: 1; } }
</style> </style>

Loading…
Cancel
Save