2026-05-06 15:56:56 +08:00

644 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="wrong-retrain-page">
<SubPageHeader class="retrain-header titled-header">
<template #left>
<button class="detail-back" type="button" @click="handleBack">
<svg class="detail-back-icon" viewBox="0 0 32 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M24 4L6 26L24 48"
stroke="#111827"
stroke-width="4.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<span>{{ pageTitle }}</span>
</button>
</template>
<template #center>
<QuestionNumberNav
:total="questionList.length"
:current-index="currentQuestionIndex"
@update:current-index="currentQuestionIndex = $event"
/>
</template>
</SubPageHeader>
<div class="workspace-card">
<div class="workspace-header">
<div class="question-base-info">
<span class="question-progress">
{{ currentQuestionIndex + 1 }}/{{ questionList.length }}
</span>
<span class="question-score">[{{ currentQuestion.score }}]</span>
<span class="small-tag blue">{{ currentQuestion.subject }}</span>
<span class="small-tag blue-light">{{ currentQuestion.questionType }}</span>
<span class="small-tag orange">{{ currentQuestion.difficulty }}</span>
</div>
<div class="timer-chip">用时: <span class="time">{{ currentQuestion.duration }}s</span></div>
<div class="header-actions">
<template v-if="isDetailMode">
<button class="lx-btn lx-btn--sm lx-btn--secondary" type="button">错题反馈</button>
<button class="lx-btn lx-btn--sm lx-btn--secondary" type="button">举一反三</button>
</template>
<button
v-else
class="lx-btn lx-btn--md lx-btn--short lx-btn--primary"
type="button"
@click="goNextQuestion"
>
下一题
</button>
</div>
</div>
<div class="page-content" :class="{ 'answer-collapsed': isAnswerCollapsed }">
<QuestionStemPanel
:question="currentQuestion"
:current-index="currentQuestionIndex"
:total="questionList.length"
/>
<button
class="collapse-answer-btn"
:class="{ collapsed: isAnswerCollapsed }"
type="button"
:aria-label="isAnswerCollapsed ? '展开答题区域' : '折叠答题区域'"
@click="isAnswerCollapsed = !isAnswerCollapsed"
>
<span class="collapse-arrow">{{ isAnswerCollapsed ? "" : "" }}</span>
</button>
<div v-if="!isAnswerCollapsed" class="right-panel">
<template v-if="!isDetailMode">
<ChoiceAnswerPanel
v-if="currentQuestion.answerType === 'choice'"
:model-value="choiceAnswers[currentQuestion.id]"
:options="currentQuestion.options ?? []"
@update:model-value="choiceAnswers[currentQuestion.id] = $event"
/>
<UploadAnswerPanel
v-else
:model-value="uploadAnswers[currentQuestion.id]"
@update:model-value="uploadAnswers[currentQuestion.id] = $event"
/>
</template>
<div v-else class="detail-answer-card">
<div class="answer-title-row">
<h3 class="answer-title">我的答案</h3>
<div
class="score-result"
:class="currentQuestion.isCorrect ? 'correct' : 'wrong'"
>
{{ currentQuestion.isCorrect ? "对啦" : "错啦" }}{{ currentQuestion.isCorrect ? currentQuestion.score : 0 }}
</div>
</div>
<div v-if="currentQuestion.answerType === 'choice'" class="review-option-list">
<div
v-for="option in currentQuestion.options ?? []"
:key="option.value"
class="review-option-item"
:class="getReviewOptionClass(option.value)"
>
<span class="review-option-key">{{ option.value }}</span>
<span class="review-option-text">{{ option.label }}</span>
</div>
</div>
<div v-else class="image-answer-preview">
<div class="paper-answer">
<div class="paper-text">
There is no <span>point</span> trying<br />
to persuade him.
</div>
</div>
</div>
<div class="answer-section">
<h4>答案</h4>
<p>{{ currentQuestion.answerText }}</p>
</div>
<div class="answer-section">
<h4>解析</h4>
<p>{{ currentQuestion.explanation }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import SubPageHeader from "@/components/layout/SubPageHeader.vue";
import QuestionNumberNav from "@/views/AnswerQuestions/components/QuestionNumberNav.vue";
import QuestionStemPanel from "@/views/AnswerQuestions/components/QuestionStemPanel.vue";
import ChoiceAnswerPanel from "@/views/AnswerQuestions/components/ChoiceAnswerPanel.vue";
import UploadAnswerPanel from "@/views/AnswerQuestions/components/UploadAnswerPanel.vue";
import type {
AnswerQuestion,
UploadAnswerValue,
} from "@/views/AnswerQuestions/components/types";
const route = useRoute();
const router = useRouter();
const isDetailMode = computed(() => route.query.mode === "detail");
const pageTitle = computed(() => (isDetailMode.value ? "查看详情" : "错题重练"));
const requestedCount = Number(route.query.count ?? 10);
const answerQuestionCount = Number.isNaN(requestedCount)
? 10
: Math.min(50, Math.max(1, requestedCount));
const questionCount = computed(() => answerQuestionCount);
const detailQuestionId = Number(route.query.questionId ?? 1);
// 页面题目列表,后续可替换成接口返回数据
const questionList = computed<AnswerQuestion[]>(() =>
Array.from({ length: questionCount.value }, (_, index) => {
const detailIsUpload = isDetailMode.value && detailQuestionId % 2 === 0;
const isChoice = index % 2 === 0;
const answerType: AnswerQuestion["answerType"] =
detailIsUpload || !isChoice ? "upload" : "choice";
const isCorrect = detailIsUpload;
return {
id: index + 1,
subject: "英语",
questionType: answerType === "choice" ? "单选题" : "填空题",
difficulty: "中等",
score: 10,
duration: 30,
typeLabel: answerType === "choice" ? "单选题" : "填空题",
answerType,
stem: answerType === "choice"
? "下列全是元音字母的一组是 ______ 。"
: "There is no ______ trying to persuade him.",
options: answerType === "choice"
? [
{ value: "A", label: "b;f;k" },
{ value: "B", label: "a;e;o" },
{ value: "C", label: "h;0;m" },
{ value: "D", label: "a;u;f" },
]
: undefined,
userAnswer: answerType === "choice" ? "C" : undefined,
correctAnswer: answerType === "choice" ? "B" : "There is no point trying to persuade him.",
isCorrect,
answerText:
answerType === "choice"
? "正确答案 B。"
: "正确答案There is no point trying to persuade him.",
explanation:
answerType === "choice"
? "元音字母为 a、e、i、o、u本题中只有 B 选项全部为元音字母。"
: "There is no point doing sth. 表示“做某事没有意义”,后面接动名词 trying。",
};
}),
);
const currentQuestionIndex = ref(0);
// 以题目 id 为 key 存储答案,便于题号切换后保留作答状态
const choiceAnswers = ref<Record<number, string>>({});
const uploadAnswers = ref<Record<number, UploadAnswerValue[]>>({});
const isAnswerCollapsed = ref(false);
const currentQuestion = computed(() => questionList.value[currentQuestionIndex.value]);
function getReviewOptionClass(value: string) {
return {
correct: value === currentQuestion.value.correctAnswer,
wrong:
value === currentQuestion.value.userAnswer &&
value !== currentQuestion.value.correctAnswer,
};
}
function goNextQuestion() {
currentQuestionIndex.value =
(currentQuestionIndex.value + 1) % questionList.value.length;
}
function handleBack() {
router.back();
}
</script>
<style scoped lang="scss">
.wrong-retrain-page {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
background: #dce6f7;
padding-top: 12px;
}
:deep(.retrain-header.sub-page-header) {
grid-template-columns: 120px 1fr 0;
padding: 14px 36px;
min-height: 74px;
}
:deep(.retrain-header.titled-header.sub-page-header) {
grid-template-columns: max-content minmax(0, 1fr) 0;
}
:deep(.retrain-header.titled-header .question-nav) {
max-width: 100%;
}
:deep(.retrain-header.titled-header .sph-center) {
min-width: 0;
}
:deep(.retrain-header .sph-center) {
justify-content: flex-end;
}
:deep(.retrain-header .sph-right) {
display: none;
}
.detail-back {
display: inline-flex;
align-items: center;
gap: 18px;
border: none;
background: transparent;
color: #151821;
cursor: pointer;
font-size: 28px;
line-height: 1;
padding: 8px 12px;
white-space: nowrap;
}
.detail-back-icon {
width: 24px;
height: 38px;
}
.workspace-card {
flex: 1;
min-height: 0;
margin: 0 34px 22px;
background: rgba(255, 255, 255, 0.30);
border-radius: 20px;
display: flex;
flex-direction: column;
overflow: hidden;
margin-top: 12px;
border: 2px solid rgba(255, 255, 255);
}
.workspace-header {
// min-height: 76px;
display: grid;
grid-template-columns: 1fr auto 1fr;
// align-items: start;
gap: 14px;
padding: 26px 32px;
position: relative;
// background: #eef3fc;
}
.question-base-info {
display: flex;
align-items: center;
gap: 16px;
grid-column: 1;
justify-self: start;
}
.question-progress,
.question-score {
font-size: 32px;
color: #151821;
font-weight: 600;
}
.small-tag {
font-size: 24px;
line-height: 1;
padding: 8px 12px;
border-radius: 8px;
font-weight: 500;
&.blue {
color: #3193F5;
background: rgba(49, 147, 245, 0.10);
}
&.blue-light {
color: #4e97d1;
background: #edf6ff;
}
&.orange {
color: #EB5E00;
background: rgba(255, 117, 26, 0.10);
}
}
.timer-chip {
position: absolute;
height: 72px;
min-width: 300px;
padding: 18px 0;
background: url("@/assets/AnswerQuestions/time.png") center / 100% 100% no-repeat;
display: flex;
align-items: center;
justify-content: center;
color: #2e3442;
font-size: 24px;
// font-weight: 600;
// line-height: 1;
top: 0;
left: 50%;
transform: translateX(-50%);
.time{
font-weight: 600;
margin-left: 12px;
font-size: 32px;
}
}
.header-actions {
grid-column: 3;
display: flex;
justify-content: flex-end;
align-items: flex-start;
gap: 28px;
}
.page-content {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: minmax(0, 1.05fr) 0 minmax(0, 1fr);
background: #fff;
border-radius: 32px;
padding: 32px 0px;
position: relative;
transition: grid-template-columns 0.24s ease;
&.answer-collapsed {
grid-template-columns: minmax(0, 1fr) 0 0;
:deep(.left-panel) {
border-right: none;
}
}
}
.right-panel {
grid-column: 3;
min-height: 0;
overflow: hidden;
// padding: 12px 14px 16px;
}
.collapse-answer-btn {
grid-column: 2;
position: relative;
z-index: 2;
align-self: center;
justify-self: center;
width: 34px;
height: 64px;
border: 1px solid rgba(170, 202, 238, 0.72);
border-radius: 999px;
background: linear-gradient(180deg, #f8fbff 0%, #ddecff 100%);
color: #2d6fe0;
cursor: pointer;
box-shadow:
0 8px 20px rgba(45, 111, 224, 0.16),
0 1px 4px rgba(45, 111, 224, 0.12);
transform: translateX(0);
transition:
background 0.2s,
border-color 0.2s,
box-shadow 0.2s,
transform 0.24s ease;
&::before,
&::after {
content: "";
position: absolute;
left: 50%;
width: 4px;
height: 4px;
border-radius: 50%;
background: rgba(45, 111, 224, 0.22);
transform: translateX(-50%);
}
&::before {
top: 12px;
}
&::after {
bottom: 12px;
}
&.collapsed {
justify-self: end;
transform: translateX(-12px);
}
&:hover {
border-color: rgba(45, 111, 224, 0.42);
background: linear-gradient(180deg, #fff 0%, #d7e9ff 100%);
box-shadow:
0 10px 24px rgba(45, 111, 224, 0.22),
0 2px 6px rgba(45, 111, 224, 0.16);
}
}
.collapse-arrow {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
font-size: 34px;
font-weight: 700;
line-height: 1;
transform: translateY(-1px);
}
.page-content.answer-collapsed {
.collapse-arrow {
transform: translateY(-1px);
}
}
.detail-answer-card {
height: 100%;
overflow-y: auto;
padding: 0 32px;
background: #fff;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.answer-title-row {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 42px;
margin-bottom: 18px;
}
.answer-title {
color: #151821;
font-size: 28px;
font-weight: 700;
line-height: 1;
}
.score-result {
min-width: 180px;
border-radius: 6px;
font-size: 22px;
font-weight: 600;
line-height: 1;
padding: 8px 14px;
text-align: right;
&.correct {
background: linear-gradient(90deg, rgba(44, 180, 132, 0), rgba(44, 180, 132, 0.14));
color: #16936b;
}
&.wrong {
background: linear-gradient(90deg, rgba(226, 68, 68, 0), rgba(226, 68, 68, 0.12));
color: #c72535;
}
}
.review-option-list {
display: flex;
flex-direction: column;
gap: 24px;
}
.review-option-item {
display: flex;
align-items: center;
overflow: hidden;
min-height: 92px;
border: 2px solid #dbe7f5;
border-radius: 10px;
background: #fff;
color: #171b24;
&.correct {
border-color: #31aa85;
background: rgba(49, 170, 133, 0.07);
color: #18936d;
.review-option-key {
background: rgba(49, 170, 133, 0.08);
border-right-color: rgba(49, 170, 133, 0.45);
}
}
&.wrong {
border-color: #d92535;
background: rgba(217, 37, 53, 0.06);
color: #c72535;
.review-option-key {
background: rgba(217, 37, 53, 0.06);
border-right-color: rgba(217, 37, 53, 0.45);
}
}
}
.review-option-key {
width: 88px;
min-height: 92px;
display: flex;
align-items: center;
justify-content: center;
border-right: 1px solid #dce8f6;
background: #edf4fd;
font-size: 40px;
font-weight: 500;
}
.review-option-text {
padding: 0 24px;
font-size: 28px;
}
.image-answer-preview {
overflow: hidden;
border-radius: 10px;
border: 1px solid #dbe7f5;
background: #f8fafc;
}
.paper-answer {
position: relative;
height: 220px;
background:
repeating-linear-gradient(
to bottom,
#f7f2e8 0,
#f7f2e8 42px,
#9bb8d2 44px,
#f7f2e8 46px
);
display: flex;
align-items: center;
justify-content: center;
}
.paper-text {
color: #111;
font-family: "Comic Sans MS", "Segoe Print", cursive;
font-size: 46px;
font-style: italic;
line-height: 1.25;
text-align: center;
transform: rotate(-1deg);
span {
color: #2d6fe0;
}
}
.answer-section {
margin-top: 34px;
color: #151821;
h4 {
margin-bottom: 18px;
font-size: 26px;
font-weight: 700;
line-height: 1;
}
p {
color: #2e3138;
font-size: 22px;
line-height: 1.7;
}
}
</style>