From 93c133349a5ccd7a328371fa113dce69d5611f21 Mon Sep 17 00:00:00 2001
From: liuyu <yu.liu@cqgbkj.com>
Date: 星期二, 22 九月 2026 09:25:39 +0800
Subject: [PATCH] feat(tms): 完成课程考试自测等页面与接口对接
---
apps/jnpf-web-apps-main/src/views/x/tms/selfTest/exam.vue | 825 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 798 insertions(+), 27 deletions(-)
diff --git a/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/exam.vue b/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/exam.vue
index 6911352..9541d22 100644
--- a/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/exam.vue
+++ b/apps/jnpf-web-apps-main/src/views/x/tms/selfTest/exam.vue
@@ -1,45 +1,138 @@
<script lang="ts" setup>
-import type { SelfTestPaper, SelfTestQuestionItem } from './types';
+import type { SelfTestPaper, SelfTestQuestionItem, SelfTestSubmitResult } from './types';
-import { computed, onMounted, reactive, ref } from 'vue';
-import { useRouter } from 'vue-router';
+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 { labelOfType } from '#/views/x/tms/question/constants';
+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锛沵ulti 瀛� 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);
-onMounted(() => {
+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('璇峰厛璁剧疆鎶介鏉′欢');
- router.replace('/tms/selfTest');
+ 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 {
- router.replace('/tms/selfTest');
+ 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() {
@@ -50,13 +143,111 @@
if (currentIndex.value < total.value - 1) currentIndex.value += 1;
}
-function goBack() {
- router.push('/tms/selfTest');
+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 = q.options.filter((o) => o.isCorrect === '1').map((o) => o.optionLabel);
+ const correctLabels = correctLabelsOf(q);
if (q.questionType === 'multi') {
const selected = Array.isArray(ans) ? [...ans].sort() : [];
return selected.join(',') === [...correctLabels].sort().join(',');
@@ -64,16 +255,41 @@
return String(ans || '') === String(correctLabels[0] || '');
}
-function handleSubmit() {
- if (!paper.value) return;
- const qs = paper.value.questions;
- let right = 0;
- qs.forEach((q) => {
- if (isCorrect(q)) right += 1;
- });
- submitted.value = true;
- scoreText.value = `${right} / ${qs.length}`;
- createMessage.success(`妫�娴嬪畬鎴愶紝姝g‘ ${right} 棰橈紝鍏� ${qs.length} 棰榒);
+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>
@@ -90,18 +306,36 @@
</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 type="primary" :disabled="submitted" @click="handleSubmit">浜ゅ嵎</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="mb-3 text-sm text-gray-500">
- {{ labelOfType(current.questionType) }}
+ <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]"
@@ -113,7 +347,6 @@
</a-radio>
</a-radio-group>
- <!-- 澶氶�� -->
<a-checkbox-group
v-else-if="current.questionType === 'multi'"
v-model:value="answers[current.id]"
@@ -127,14 +360,141 @@
<div v-if="submitted" class="mt-4 text-sm" :class="isCorrect(current) ? 'text-green-600' : 'text-red-500'">
{{ isCorrect(current) ? '鍥炵瓟姝g‘' : '鍥炵瓟閿欒' }}
- 路 姝g‘绛旀锛�
- {{ current.options.filter((o) => o.isCorrect === '1').map((o) => o.optionLabel).join('銆�') }}
+ 路 姝g‘绛旀锛歿{ 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">姝g‘鐜�</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">姝g‘绛旀</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) ? '鍥炵瓟姝g‘' : '鍥炵瓟閿欒' }}
+ </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">姝g‘绛旀</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>
@@ -167,6 +527,33 @@
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;
@@ -181,4 +568,388 @@
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>
--
Gitblit v1.8.0