<script lang="ts" setup>
|
import type { SelfTestPaper, SelfTestQuestionItem, SelfTestSubmitResult } from './types';
|
|
import {
|
computed,
|
nextTick,
|
onDeactivated,
|
onMounted,
|
onUnmounted,
|
reactive,
|
ref,
|
watch,
|
} from 'vue';
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useMessage } from '@jnpf/hooks';
|
import { useTabbarStore } from '@vben/stores';
|
import { AppstoreOutlined } from '@ant-design/icons-vue';
|
|
import { saveSelfTest, submitSelfTest } from '#/api/x/tms/selfTest';
|
import { labelOfType, loadQuestionDics } from '#/views/x/tms/question/constants';
|
|
defineOptions({ name: 'TmsSelfTestExam' });
|
|
const route = useRoute();
|
const router = useRouter();
|
const { createMessage } = useMessage();
|
const tabbarStore = useTabbarStore();
|
|
const paper = ref<SelfTestPaper | null>(null);
|
const currentIndex = ref(0);
|
/** questionId -> answer: single/judge 存 optionLabel;multi 存 label[] */
|
const answers = reactive<Record<string, string | string[]>>({});
|
const submitted = ref(false);
|
const submitting = ref(false);
|
const saving = ref(false);
|
const answersReady = ref(false);
|
const scoreText = ref('');
|
const resultModalOpen = ref(false);
|
const submitResult = ref<SelfTestSubmitResult | null>(null);
|
const resultFilter = ref<'all' | 'wrong'>('all');
|
const sheetOpen = ref(false);
|
let saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
const current = computed(() => paper.value?.questions?.[currentIndex.value]);
|
const total = computed(() => paper.value?.questions?.length || 0);
|
|
const wrongCount = computed(
|
() => paper.value?.questions?.filter((q) => !isCorrect(q)).length || 0,
|
);
|
|
const rightCount = computed(() => submitResult.value?.correctCount ?? 0);
|
|
const resultQuestions = computed(() => {
|
const list = paper.value?.questions || [];
|
if (resultFilter.value === 'wrong') {
|
return list.filter((q) => !isCorrect(q));
|
}
|
return list;
|
});
|
const answeredCount = computed(
|
() => (paper.value?.questions || []).filter((q) => isAnswered(q)).length,
|
);
|
|
onMounted(async () => {
|
// 若此前 refreshTab 中断导致全局内容区不渲染,这里强制恢复
|
tabbarStore.renderRouteView = true;
|
await loadQuestionDics();
|
|
const raw = sessionStorage.getItem('tms_self_test_paper');
|
if (!raw) {
|
createMessage.warning('请先设置抽题条件');
|
goBack();
|
return;
|
}
|
try {
|
paper.value = JSON.parse(raw);
|
// 继续考试:恢复已保存的作答
|
const ansRaw = sessionStorage.getItem('tms_self_test_answers');
|
if (ansRaw) {
|
try {
|
const saved = JSON.parse(ansRaw) as Record<string, string | string[]>;
|
Object.keys(saved || {}).forEach((qid) => {
|
answers[qid] = saved[qid];
|
});
|
} catch {
|
// ignore
|
}
|
sessionStorage.removeItem('tms_self_test_answers');
|
}
|
} catch {
|
goBack();
|
return;
|
}
|
answersReady.value = true;
|
});
|
|
onDeactivated(() => {
|
resultModalOpen.value = false;
|
sheetOpen.value = false;
|
});
|
|
onUnmounted(() => {
|
if (saveTimer) {
|
clearTimeout(saveTimer);
|
saveTimer = null;
|
}
|
resultModalOpen.value = false;
|
sheetOpen.value = false;
|
// 再次确保离开本页后内容区可渲染
|
tabbarStore.renderRouteView = true;
|
});
|
|
/** 作答变更后自动暂存,方便中途离开再继续 */
|
watch(
|
answers,
|
() => {
|
if (!answersReady.value || submitted.value || !paper.value) return;
|
if (saveTimer) clearTimeout(saveTimer);
|
saveTimer = setTimeout(() => {
|
void persistAnswers(false);
|
}, 1500);
|
},
|
{ deep: true },
|
);
|
|
function stripHtml(html?: string) {
|
if (!html) return '';
|
return html.replace(/<[^>]+>/g, '').replace(/ /g, ' ').trim();
|
}
|
|
function isAnswered(q: SelfTestQuestionItem) {
|
const val = answers[q.id];
|
if (Array.isArray(val)) return val.length > 0;
|
return String(val ?? '').trim().length > 0;
|
}
|
|
function goPrev() {
|
if (currentIndex.value > 0) currentIndex.value -= 1;
|
}
|
|
function goNext() {
|
if (currentIndex.value < total.value - 1) currentIndex.value += 1;
|
}
|
|
function goToQuestion(index: number) {
|
if (index < 0 || index >= total.value) {
|
sheetOpen.value = false;
|
return;
|
}
|
currentIndex.value = index;
|
sheetOpen.value = false;
|
}
|
|
function openSheet() {
|
sheetOpen.value = true;
|
}
|
|
function getSettingsPath() {
|
return String(route.meta.currentActiveMenu || '/tms/selfTest');
|
}
|
|
/**
|
* 与在线考试一致:按 path 跳转,不调用 closeCurrentTab。
|
* closeCurrentTab 内部按 route.name 跳转,动态菜单场景容易跳到空页并影响全局内容区。
|
*/
|
async function goBack() {
|
if (!submitted.value && paper.value) {
|
if (saveTimer) {
|
clearTimeout(saveTimer);
|
saveTimer = null;
|
}
|
await persistAnswers(false);
|
}
|
resultModalOpen.value = false;
|
tabbarStore.renderRouteView = true;
|
router.push(getSettingsPath());
|
}
|
|
async function persistAnswers(showTip: boolean) {
|
if (!paper.value || submitted.value || saving.value) return;
|
const payload = { ...answers };
|
if (!Object.keys(payload).length) {
|
if (showTip) createMessage.info('暂无作答可保存');
|
return;
|
}
|
saving.value = true;
|
try {
|
await saveSelfTest(paper.value.paperId, { answers: payload });
|
if (showTip) createMessage.success('已暂存,可稍后继续考试');
|
} catch (e: any) {
|
if (showTip) createMessage.error(e?.message || '暂存失败');
|
} finally {
|
saving.value = false;
|
}
|
}
|
|
async function handleSave() {
|
if (saveTimer) {
|
clearTimeout(saveTimer);
|
saveTimer = null;
|
}
|
await persistAnswers(true);
|
}
|
|
function closeResultModal() {
|
resultModalOpen.value = false;
|
}
|
|
async function openResultModal() {
|
if (!submitResult.value && paper.value) {
|
const qs = paper.value.questions || [];
|
let correct = 0;
|
qs.forEach((q) => {
|
if (isCorrect(q)) correct += 1;
|
});
|
submitResult.value = {
|
paperId: paper.value.paperId,
|
totalCount: qs.length,
|
correctCount: correct,
|
scoreRate: qs.length ? Math.round((correct * 10000) / qs.length) / 100 : 0,
|
};
|
}
|
resultFilter.value = 'all';
|
await nextTick();
|
resultModalOpen.value = true;
|
}
|
|
function goRecords() {
|
resultModalOpen.value = false;
|
tabbarStore.renderRouteView = true;
|
router.push('/tms/selfTest/records');
|
}
|
|
function correctLabelsOf(q: SelfTestQuestionItem) {
|
return q.options.filter((o) => o.isCorrect === '1').map((o) => o.optionLabel);
|
}
|
|
function userAnswerText(q: SelfTestQuestionItem) {
|
const ans = answers[q.id];
|
if (q.questionType === 'multi') {
|
const selected = Array.isArray(ans) ? [...ans].sort() : [];
|
return selected.length ? selected.join('、') : '未作答';
|
}
|
return String(ans || '') || '未作答';
|
}
|
|
function isCorrect(q: SelfTestQuestionItem): boolean {
|
const ans = answers[q.id];
|
const correctLabels = correctLabelsOf(q);
|
if (q.questionType === 'multi') {
|
const selected = Array.isArray(ans) ? [...ans].sort() : [];
|
return selected.join(',') === [...correctLabels].sort().join(',');
|
}
|
return String(ans || '') === String(correctLabels[0] || '');
|
}
|
|
function optionClass(q: SelfTestQuestionItem, label: string) {
|
const correctSet = new Set(correctLabelsOf(q));
|
const ans = answers[q.id];
|
const userSet = new Set(
|
q.questionType === 'multi'
|
? Array.isArray(ans)
|
? ans
|
: []
|
: ans
|
? [String(ans)]
|
: [],
|
);
|
if (correctSet.has(label)) return 'opt-correct';
|
if (userSet.has(label) && !correctSet.has(label)) return 'opt-wrong';
|
return '';
|
}
|
|
async function handleSubmit() {
|
if (!paper.value || submitting.value) return;
|
if (saveTimer) {
|
clearTimeout(saveTimer);
|
saveTimer = null;
|
}
|
submitting.value = true;
|
try {
|
const result = await submitSelfTest(paper.value.paperId, { answers: { ...answers } });
|
submitted.value = true;
|
submitResult.value = result;
|
scoreText.value = `${result.correctCount} / ${result.totalCount}`;
|
await openResultModal();
|
} catch (e: any) {
|
createMessage.error(e?.message || '交卷失败');
|
} finally {
|
submitting.value = false;
|
}
|
}
|
</script>
|
|
<template>
|
<div class="jnpf-content-wrapper">
|
<div class="jnpf-content-wrapper-center">
|
<div class="jnpf-content-wrapper-content tms-exam-page" v-if="paper">
|
<div class="tms-exam-header">
|
<div>
|
<div class="text-base font-medium">自我检测 · {{ paper.bankName }}</div>
|
<div class="mt-1 text-gray-400 text-sm">
|
第 {{ currentIndex + 1 }} / {{ total }} 题
|
<span v-if="submitted" class="ml-3 text-primary">得分:{{ scoreText }}</span>
|
</div>
|
</div>
|
<a-space>
|
<a-button v-if="submitted" type="link" @click="openResultModal">查看本次结果</a-button>
|
<a-button v-if="submitted" type="link" @click="goRecords">检测记录</a-button>
|
<a-button @click="goBack">返回设置</a-button>
|
<a-button
|
v-if="!submitted"
|
:loading="saving"
|
:disabled="submitting"
|
@click="handleSave"
|
>
|
暂存
|
</a-button>
|
<a-button type="primary" :disabled="submitted" :loading="submitting" @click="handleSubmit">
|
交卷
|
</a-button>
|
</a-space>
|
</div>
|
|
<div v-if="current" class="tms-exam-body">
|
<div class="q-meta mb-3">
|
<div class="text-sm text-gray-500">
|
{{ labelOfType(current.questionType) }}
|
</div>
|
<a-tooltip title="答题卡">
|
<button type="button" class="sheet-icon-btn" aria-label="答题卡" @click="openSheet">
|
<AppstoreOutlined />
|
</button>
|
</a-tooltip>
|
</div>
|
<div class="stem mb-4">{{ stripHtml(current.stem) }}</div>
|
|
<a-radio-group
|
v-if="current.questionType === 'single' || current.questionType === 'judge'"
|
v-model:value="answers[current.id]"
|
class="!flex !flex-col gap-3"
|
:disabled="submitted"
|
>
|
<a-radio v-for="opt in current.options" :key="opt.optionLabel" :value="opt.optionLabel">
|
{{ opt.optionLabel }}. {{ opt.optionContent }}
|
</a-radio>
|
</a-radio-group>
|
|
<a-checkbox-group
|
v-else-if="current.questionType === 'multi'"
|
v-model:value="answers[current.id]"
|
class="!flex !flex-col gap-3"
|
:disabled="submitted"
|
>
|
<a-checkbox v-for="opt in current.options" :key="opt.optionLabel" :value="opt.optionLabel">
|
{{ opt.optionLabel }}. {{ opt.optionContent }}
|
</a-checkbox>
|
</a-checkbox-group>
|
|
<div v-if="submitted" class="mt-4 text-sm" :class="isCorrect(current) ? 'text-green-600' : 'text-red-500'">
|
{{ isCorrect(current) ? '回答正确' : '回答错误' }}
|
· 正确答案:{{ correctLabelsOf(current).join('、') }}
|
</div>
|
</div>
|
|
<div class="tms-exam-footer">
|
<a-button :disabled="currentIndex <= 0" @click="goPrev">上一题</a-button>
|
<a-button :disabled="currentIndex >= total - 1" @click="goNext">下一题</a-button>
|
</div>
|
</div>
|
</div>
|
|
<div v-if="sheetOpen && paper" class="tms-sheet-overlay" @click.self="sheetOpen = false">
|
<div class="tms-sheet-dialog" role="dialog" aria-modal="true">
|
<div class="sheet-head">
|
<div>
|
<div class="sheet-title">答题卡</div>
|
<div class="sheet-sub">已答 {{ answeredCount }} / {{ total }},点击题号可切换</div>
|
</div>
|
<button type="button" class="tms-result-close" aria-label="关闭" @click="sheetOpen = false">×</button>
|
</div>
|
<div class="sheet-legend">
|
<span class="legend-chip current">当前</span>
|
<span class="legend-chip answered">已答</span>
|
<span class="legend-chip unanswered">未答</span>
|
</div>
|
<div class="sheet-grid">
|
<button
|
v-for="(q, idx) in paper.questions"
|
:key="q.id"
|
type="button"
|
class="sheet-item"
|
:class="{
|
current: idx === currentIndex,
|
answered: isAnswered(q),
|
unanswered: !isAnswered(q),
|
}"
|
@click="goToQuestion(idx)"
|
>
|
{{ idx + 1 }}
|
</button>
|
</div>
|
</div>
|
</div>
|
|
<!-- 不用 Teleport 到 body,避免 KeepAlive 场景下遮罩残留导致全局无法点击/白屏 -->
|
<div
|
v-if="resultModalOpen && submitResult && paper"
|
class="tms-result-overlay"
|
@click.self="closeResultModal"
|
>
|
<div class="tms-result-dialog" role="dialog" aria-modal="true">
|
<button type="button" class="tms-result-close" aria-label="关闭" @click="closeResultModal">×</button>
|
|
<div class="result-hero">
|
<div class="hero-title">本次检测结果</div>
|
<div class="hero-bank">{{ paper.bankName }}</div>
|
<div class="hero-stats">
|
<div class="stat-item">
|
<div class="stat-value">{{ rightCount }}</div>
|
<div class="stat-label">答对</div>
|
</div>
|
<div class="stat-divider" />
|
<div class="stat-item">
|
<div class="stat-value">{{ wrongCount }}</div>
|
<div class="stat-label">答错</div>
|
</div>
|
<div class="stat-divider" />
|
<div class="stat-item">
|
<div class="stat-value">{{ submitResult.scoreRate }}%</div>
|
<div class="stat-label">正确率</div>
|
</div>
|
<div class="stat-divider" />
|
<div class="stat-item">
|
<div class="stat-value">{{ submitResult.correctCount }}/{{ submitResult.totalCount }}</div>
|
<div class="stat-label">得分</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="result-toolbar">
|
<a-radio-group v-model:value="resultFilter" button-style="solid" size="small">
|
<a-radio-button value="all">全部题目({{ total }})</a-radio-button>
|
<a-radio-button value="wrong">仅错题({{ wrongCount }})</a-radio-button>
|
</a-radio-group>
|
<div class="legend">
|
<span class="legend-item correct">正确答案</span>
|
<span class="legend-item wrong">你的错误选项</span>
|
</div>
|
</div>
|
|
<div class="result-list">
|
<a-empty v-if="!resultQuestions.length" description="暂无题目" />
|
<div
|
v-for="(q, idx) in resultQuestions"
|
:key="q.id"
|
class="question-card"
|
:class="isCorrect(q) ? 'is-right' : 'is-wrong'"
|
>
|
<div class="q-head">
|
<div class="q-head-left">
|
<span class="q-index">第 {{ idx + 1 }} 题</span>
|
<span class="q-type">{{ labelOfType(q.questionType) }}</span>
|
</div>
|
<a-tag :color="isCorrect(q) ? 'success' : 'error'">
|
{{ isCorrect(q) ? '回答正确' : '回答错误' }}
|
</a-tag>
|
</div>
|
<div class="q-stem">{{ stripHtml(q.stem) }}</div>
|
<div class="q-options">
|
<div
|
v-for="opt in q.options"
|
:key="opt.optionLabel"
|
class="q-option"
|
:class="optionClass(q, opt.optionLabel)"
|
>
|
<span class="opt-label">{{ opt.optionLabel }}</span>
|
<span class="opt-content">{{ opt.optionContent }}</span>
|
</div>
|
</div>
|
<div class="q-answer-bar">
|
<div>
|
<span class="ans-label">你的答案</span>
|
<span :class="isCorrect(q) ? 'ans-ok' : 'ans-bad'">{{ userAnswerText(q) }}</span>
|
</div>
|
<div>
|
<span class="ans-label">正确答案</span>
|
<span class="ans-ok">{{ correctLabelsOf(q).join('、') || '-' }}</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="result-footer">
|
<a-button @click="closeResultModal">关闭</a-button>
|
<a-button type="primary" @click="goRecords">我的检测记录</a-button>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<style scoped>
|
.tms-exam-page {
|
background: #fff;
|
padding: 20px 24px;
|
height: 100%;
|
display: flex;
|
flex-direction: column;
|
min-height: 0;
|
}
|
|
.tms-exam-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding-bottom: 12px;
|
margin-bottom: 16px;
|
border-bottom: 1px solid #f0f0f0;
|
flex-shrink: 0;
|
}
|
|
.tms-exam-body {
|
flex: 1;
|
min-height: 0;
|
overflow: auto;
|
}
|
|
.q-meta {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
}
|
|
.sheet-icon-btn {
|
display: inline-flex;
|
align-items: center;
|
justify-content: center;
|
width: 32px;
|
height: 32px;
|
border: 1px solid #d9d9d9;
|
border-radius: 6px;
|
background: #fff;
|
color: #1677ff;
|
font-size: 16px;
|
cursor: pointer;
|
flex-shrink: 0;
|
}
|
|
.sheet-icon-btn:hover {
|
border-color: #1677ff;
|
background: #e6f4ff;
|
}
|
|
.stem {
|
font-size: 15px;
|
line-height: 1.7;
|
color: rgba(0, 0, 0, 0.88);
|
}
|
|
.tms-exam-footer {
|
flex-shrink: 0;
|
display: flex;
|
gap: 12px;
|
justify-content: center;
|
padding-top: 16px;
|
border-top: 1px solid #f0f0f0;
|
}
|
|
.tms-sheet-overlay {
|
position: fixed;
|
inset: 0;
|
z-index: 1900;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
padding: 24px;
|
background: rgba(0, 0, 0, 0.45);
|
}
|
|
.tms-sheet-dialog {
|
width: min(520px, 100%);
|
max-height: min(70vh, 640px);
|
display: flex;
|
flex-direction: column;
|
background: #fff;
|
border-radius: 12px;
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
|
overflow: hidden;
|
}
|
|
.sheet-head {
|
position: relative;
|
flex-shrink: 0;
|
padding: 18px 48px 12px 20px;
|
border-bottom: 1px solid #f0f0f0;
|
}
|
|
.sheet-title {
|
font-size: 16px;
|
font-weight: 600;
|
color: rgba(0, 0, 0, 0.88);
|
}
|
|
.sheet-sub {
|
margin-top: 4px;
|
font-size: 13px;
|
color: rgba(0, 0, 0, 0.45);
|
}
|
|
.sheet-legend {
|
flex-shrink: 0;
|
display: flex;
|
gap: 16px;
|
padding: 12px 20px 0;
|
font-size: 12px;
|
color: rgba(0, 0, 0, 0.55);
|
}
|
|
.legend-chip::before {
|
content: '';
|
display: inline-block;
|
width: 12px;
|
height: 12px;
|
margin-right: 6px;
|
border-radius: 3px;
|
vertical-align: -2px;
|
}
|
|
.legend-chip.current::before {
|
background: #fff;
|
border: 2px solid #1677ff;
|
box-sizing: border-box;
|
}
|
|
.legend-chip.answered::before {
|
background: #1677ff;
|
}
|
|
.legend-chip.unanswered::before {
|
background: #f5f5f5;
|
border: 1px solid #d9d9d9;
|
box-sizing: border-box;
|
}
|
|
.sheet-grid {
|
flex: 1;
|
min-height: 0;
|
overflow: auto;
|
display: grid;
|
grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
|
gap: 10px;
|
padding: 16px 20px 20px;
|
}
|
|
.sheet-item {
|
height: 40px;
|
border-radius: 8px;
|
border: 1px solid #d9d9d9;
|
background: #fafafa;
|
color: rgba(0, 0, 0, 0.65);
|
font-size: 14px;
|
font-weight: 600;
|
cursor: pointer;
|
transition: all 0.15s ease;
|
}
|
|
.sheet-item.answered {
|
background: #1677ff;
|
border-color: #1677ff;
|
color: #fff;
|
}
|
|
.sheet-item.unanswered {
|
background: #f5f5f5;
|
border-color: #d9d9d9;
|
color: rgba(0, 0, 0, 0.45);
|
}
|
|
.sheet-item.current {
|
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.35);
|
}
|
|
.sheet-item.current.unanswered {
|
border-color: #1677ff;
|
color: #1677ff;
|
background: #e6f4ff;
|
}
|
|
.sheet-item:hover {
|
filter: brightness(0.97);
|
}
|
|
.tms-result-overlay {
|
position: fixed;
|
inset: 0;
|
z-index: 2000;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
padding: 24px;
|
background: rgba(0, 0, 0, 0.45);
|
}
|
|
.tms-result-dialog {
|
position: relative;
|
width: min(720px, 100%);
|
max-height: min(80vh, 780px);
|
display: flex;
|
flex-direction: column;
|
background: #fff;
|
border-radius: 12px;
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
|
overflow: hidden;
|
}
|
|
.tms-result-close {
|
position: absolute;
|
top: 10px;
|
right: 12px;
|
z-index: 2;
|
width: 32px;
|
height: 32px;
|
border: none;
|
border-radius: 6px;
|
background: transparent;
|
color: rgba(0, 0, 0, 0.45);
|
cursor: pointer;
|
display: inline-flex;
|
align-items: center;
|
justify-content: center;
|
font-size: 22px;
|
line-height: 1;
|
}
|
|
.tms-result-close:hover {
|
background: rgba(0, 0, 0, 0.06);
|
color: rgba(0, 0, 0, 0.75);
|
}
|
|
.result-hero {
|
flex-shrink: 0;
|
padding: 24px 28px 20px;
|
background: linear-gradient(135deg, #f0f7ff 0%, #f8fbff 55%, #ffffff 100%);
|
border-bottom: 1px solid #eef2f7;
|
}
|
|
.hero-title {
|
font-size: 18px;
|
font-weight: 600;
|
color: rgba(0, 0, 0, 0.88);
|
padding-right: 36px;
|
}
|
|
.hero-bank {
|
margin-top: 4px;
|
font-size: 13px;
|
color: rgba(0, 0, 0, 0.45);
|
}
|
|
.hero-stats {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
margin-top: 18px;
|
padding: 14px 8px;
|
background: #fff;
|
border: 1px solid #e8eef5;
|
border-radius: 10px;
|
}
|
|
.stat-item {
|
flex: 1;
|
text-align: center;
|
}
|
|
.stat-value {
|
font-size: 22px;
|
font-weight: 700;
|
line-height: 1.2;
|
color: #1677ff;
|
}
|
|
.stat-label {
|
margin-top: 4px;
|
font-size: 12px;
|
color: rgba(0, 0, 0, 0.45);
|
}
|
|
.stat-divider {
|
width: 1px;
|
height: 28px;
|
background: #eef2f7;
|
}
|
|
.result-toolbar {
|
flex-shrink: 0;
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
gap: 12px;
|
flex-wrap: wrap;
|
padding: 12px 28px;
|
border-bottom: 1px solid #f0f0f0;
|
}
|
|
.legend {
|
display: flex;
|
gap: 12px;
|
font-size: 12px;
|
color: rgba(0, 0, 0, 0.45);
|
}
|
|
.legend-item::before {
|
content: '';
|
display: inline-block;
|
width: 10px;
|
height: 10px;
|
margin-right: 6px;
|
border-radius: 2px;
|
vertical-align: -1px;
|
}
|
|
.legend-item.correct::before {
|
background: #b7eb8f;
|
}
|
|
.legend-item.wrong::before {
|
background: #ffa39e;
|
}
|
|
.result-list {
|
flex: 1;
|
min-height: 160px;
|
overflow: auto;
|
padding: 8px 28px 4px;
|
}
|
|
.question-card {
|
margin-bottom: 12px;
|
padding: 14px 16px;
|
border: 1px solid #f0f0f0;
|
border-radius: 10px;
|
background: #fff;
|
}
|
|
.question-card.is-wrong {
|
border-color: #ffccc7;
|
background: #fffafa;
|
}
|
|
.question-card.is-right {
|
border-color: #d9f7be;
|
background: #fcfffb;
|
}
|
|
.q-head {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 8px;
|
}
|
|
.q-head-left {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
}
|
|
.q-index {
|
font-weight: 600;
|
color: rgba(0, 0, 0, 0.88);
|
}
|
|
.q-type {
|
font-size: 12px;
|
color: rgba(0, 0, 0, 0.45);
|
}
|
|
.q-stem {
|
margin-bottom: 10px;
|
font-size: 14px;
|
line-height: 1.7;
|
color: rgba(0, 0, 0, 0.88);
|
}
|
|
.q-options {
|
display: grid;
|
gap: 6px;
|
margin-bottom: 10px;
|
}
|
|
.q-option {
|
display: flex;
|
gap: 8px;
|
padding: 8px 10px;
|
border-radius: 6px;
|
background: #fafafa;
|
border: 1px solid transparent;
|
color: rgba(0, 0, 0, 0.75);
|
}
|
|
.q-option .opt-label {
|
min-width: 18px;
|
font-weight: 600;
|
}
|
|
.q-option.opt-correct {
|
background: #f6ffed;
|
color: #389e0d;
|
border-color: #b7eb8f;
|
}
|
|
.q-option.opt-wrong {
|
background: #fff2f0;
|
color: #cf1322;
|
border-color: #ffccc7;
|
}
|
|
.q-answer-bar {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 16px 28px;
|
padding-top: 8px;
|
border-top: 1px dashed #f0f0f0;
|
font-size: 13px;
|
}
|
|
.ans-label {
|
margin-right: 8px;
|
color: rgba(0, 0, 0, 0.45);
|
}
|
|
.ans-ok {
|
color: #389e0d;
|
font-weight: 600;
|
}
|
|
.ans-bad {
|
color: #cf1322;
|
font-weight: 600;
|
}
|
|
.result-footer {
|
flex-shrink: 0;
|
display: flex;
|
justify-content: center;
|
gap: 16px;
|
padding: 14px 28px 18px;
|
border-top: 1px solid #f0f0f0;
|
background: #fafafa;
|
}
|
</style>
|