feat: 答题页面
This commit is contained in:
parent
9d61f4ee8a
commit
a14d38b6f5
8
.components.d.ts
vendored
8
.components.d.ts
vendored
@ -9,10 +9,17 @@ declare module 'vue' {
|
|||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
ActivityProgress: typeof import('./src/components/tasks/ActivityProgress.vue')['default']
|
ActivityProgress: typeof import('./src/components/tasks/ActivityProgress.vue')['default']
|
||||||
AppHeader: typeof import('./src/components/layout/AppHeader.vue')['default']
|
AppHeader: typeof import('./src/components/layout/AppHeader.vue')['default']
|
||||||
|
ChoiceAnswerPanel: typeof import('./src/components/answerQuestions/ChoiceAnswerPanel.vue')['default']
|
||||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||||
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||||
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
|
LmSelect: typeof import('./src/components/lm/LmSelect.vue')['default']
|
||||||
LoadingState: typeof import('./src/components/loading/LoadingState.vue')['default']
|
LoadingState: typeof import('./src/components/loading/LoadingState.vue')['default']
|
||||||
PageFrame: typeof import('./src/components/layout/PageFrame.vue')['default']
|
PageFrame: typeof import('./src/components/layout/PageFrame.vue')['default']
|
||||||
|
QuestionNumberNav: typeof import('./src/components/answerQuestions/QuestionNumberNav.vue')['default']
|
||||||
|
QuestionStemPanel: typeof import('./src/components/answerQuestions/QuestionStemPanel.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
SubjectCard: typeof import('./src/components/cards/SubjectCard.vue')['default']
|
SubjectCard: typeof import('./src/components/cards/SubjectCard.vue')['default']
|
||||||
@ -20,5 +27,6 @@ declare module 'vue' {
|
|||||||
TabBar: typeof import('./src/components/layout/TabBar.vue')['default']
|
TabBar: typeof import('./src/components/layout/TabBar.vue')['default']
|
||||||
TaskCard: typeof import('./src/components/cards/TaskCard.vue')['default']
|
TaskCard: typeof import('./src/components/cards/TaskCard.vue')['default']
|
||||||
TaskSidebar: typeof import('./src/components/tasks/TaskSidebar.vue')['default']
|
TaskSidebar: typeof import('./src/components/tasks/TaskSidebar.vue')['default']
|
||||||
|
UploadAnswerPanel: typeof import('./src/components/answerQuestions/UploadAnswerPanel.vue')['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/assets/AnswerQuestions/time.png
Normal file
BIN
src/assets/AnswerQuestions/time.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
121
src/components/lm/LmSelect.vue
Normal file
121
src/components/lm/LmSelect.vue
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
<template>
|
||||||
|
<el-select
|
||||||
|
v-model="selectedValue"
|
||||||
|
class="lm-select"
|
||||||
|
:style="{ width }"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:popper-class="popperClass"
|
||||||
|
:teleported="teleported"
|
||||||
|
:fit-input-width="false"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
type SelectValue = string | number;
|
||||||
|
|
||||||
|
export interface LmSelectOption {
|
||||||
|
label: string;
|
||||||
|
value: SelectValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: SelectValue;
|
||||||
|
options: LmSelectOption[];
|
||||||
|
placeholder?: string;
|
||||||
|
width?: string;
|
||||||
|
popperClass?: string;
|
||||||
|
teleported?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
placeholder: "请选择",
|
||||||
|
width: "132px",
|
||||||
|
popperClass: "lm-select-popper",
|
||||||
|
teleported: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:modelValue": [value: SelectValue];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const selectedValue = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value: SelectValue) => emit("update:modelValue", value),
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.lm-select {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select__wrapper) {
|
||||||
|
min-height: 46px;
|
||||||
|
padding: 0 14px 0 20px;
|
||||||
|
border-radius: 24px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px #edf0f5 inset,
|
||||||
|
0 4px 12px rgba(20, 40, 80, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select__wrapper.is-focused),
|
||||||
|
:deep(.el-select__wrapper.is-hovering) {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px #edf0f5 inset,
|
||||||
|
0 4px 12px rgba(20, 40, 80, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select__selection) {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select__selected-item) {
|
||||||
|
color: #303133;
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select__placeholder) {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select__suffix) {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-select__caret) {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lm-select-popper.el-popper) {
|
||||||
|
border: 1px solid #edf0f5;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 24px rgba(20, 40, 80, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lm-select-popper .el-select-dropdown__item) {
|
||||||
|
height: 42px;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.lm-select-popper .el-select-dropdown__item.is-selected) {
|
||||||
|
color: #2f80ed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -21,12 +21,24 @@ const router = createRouter({
|
|||||||
component: () => import('@/views/Subjects/TextbookDetail.vue'),
|
component: () => import('@/views/Subjects/TextbookDetail.vue'),
|
||||||
meta: { hideLayout: true },
|
meta: { hideLayout: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/subjects/:subject/video/:bookId',
|
||||||
|
name: 'video-learning',
|
||||||
|
component: () => import('@/views/Subjects/VideoLearning.vue'),
|
||||||
|
meta: { hideLayout: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/subjects/:subject/wrong-questions',
|
path: '/subjects/:subject/wrong-questions',
|
||||||
name: 'wrong-questions',
|
name: 'wrong-questions',
|
||||||
component: () => import('@/views/Subjects/WrongQuestions.vue'),
|
component: () => import('@/views/Subjects/WrongQuestions.vue'),
|
||||||
meta: { hideLayout: true },
|
meta: { hideLayout: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/answer-questions/wrong-retrain',
|
||||||
|
name: 'wrong-retrain-answer',
|
||||||
|
component: () => import('@/views/AnswerQuestions/WrongRetrain.vue'),
|
||||||
|
meta: { hideLayout: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/tasks',
|
path: '/tasks',
|
||||||
name: 'tasks',
|
name: 'tasks',
|
||||||
|
|||||||
658
src/views/AnswerQuestions/WrongRetrain.vue
Normal file
658
src/views/AnswerQuestions/WrongRetrain.vue
Normal file
@ -0,0 +1,658 @@
|
|||||||
|
<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="light-action-btn" type="button">错题反馈</button>
|
||||||
|
<button class="light-action-btn" type="button">举一反三</button>
|
||||||
|
</template>
|
||||||
|
<button v-else class="next-btn" 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn {
|
||||||
|
border-radius: 90px;
|
||||||
|
border: none;
|
||||||
|
background: #2f80ed;
|
||||||
|
color: #fff;
|
||||||
|
padding: 12px 32px;
|
||||||
|
font-size: 24px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-action-btn {
|
||||||
|
min-width: 150px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 90px;
|
||||||
|
background: rgba(51, 126, 255, 0.10);
|
||||||
|
color: #2d6fe0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 12px 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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>
|
||||||
91
src/views/AnswerQuestions/components/ChoiceAnswerPanel.vue
Normal file
91
src/views/AnswerQuestions/components/ChoiceAnswerPanel.vue
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<div class="answer-card">
|
||||||
|
<h3 class="answer-title">我的答案</h3>
|
||||||
|
<div class="option-list">
|
||||||
|
<button
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option.value"
|
||||||
|
class="option-item"
|
||||||
|
:class="{ selected: modelValue === option.value }"
|
||||||
|
type="button"
|
||||||
|
@click="emit('update:modelValue', option.value)"
|
||||||
|
>
|
||||||
|
<span class="option-key">{{ option.value }}</span>
|
||||||
|
<span class="option-text">{{ option.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
modelValue?: string;
|
||||||
|
options: Array<{ value: string; label: string }>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "update:modelValue", value: string): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.answer-card {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-title {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #2f333a;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
// padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
text-align: left;
|
||||||
|
border: 2px solid #dbe7f5;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 92px;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-color: #2b80ea;
|
||||||
|
color: #2D6FE0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-key {
|
||||||
|
width: 88px;
|
||||||
|
min-height: 92px;
|
||||||
|
background: #edf4fd;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 44px;
|
||||||
|
// color: #1f2a3a;
|
||||||
|
border-right: 1px solid #dce8f6;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-text {
|
||||||
|
font-size: 32px;
|
||||||
|
// color: #2f333a;
|
||||||
|
padding: 0 22px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
75
src/views/AnswerQuestions/components/QuestionNumberNav.vue
Normal file
75
src/views/AnswerQuestions/components/QuestionNumberNav.vue
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<div class="question-nav-wrap">
|
||||||
|
<div ref="questionNavRef" class="question-nav">
|
||||||
|
<button
|
||||||
|
v-for="index in total"
|
||||||
|
:key="index"
|
||||||
|
class="question-nav-item"
|
||||||
|
:class="{ active: currentIndex === index - 1 }"
|
||||||
|
type="button"
|
||||||
|
@click="emit('update:currentIndex', index - 1)"
|
||||||
|
>
|
||||||
|
{{ index }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
total: number;
|
||||||
|
currentIndex: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "update:currentIndex", value: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const questionNavRef = ref<HTMLDivElement>();
|
||||||
|
|
||||||
|
void props;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.question-nav-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 32px;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
max-width: 780px;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-nav-item {
|
||||||
|
width: 56px;
|
||||||
|
min-width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
color: #9aa8bf;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #fff9ea;
|
||||||
|
color: #2D6FE0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
40
src/views/AnswerQuestions/components/QuestionStemPanel.vue
Normal file
40
src/views/AnswerQuestions/components/QuestionStemPanel.vue
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="left-panel">
|
||||||
|
<div class="question-scroll">
|
||||||
|
<p class="question-text">{{ question.stem }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AnswerQuestion } from "@/views/AnswerQuestions/components/types";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
question: AnswerQuestion;
|
||||||
|
currentIndex: number;
|
||||||
|
total: number;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.left-panel {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 32px;
|
||||||
|
border-right: 1px solid #e9edf5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-scroll {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 14px;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #d0dbec transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-text {
|
||||||
|
font-size: 32px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #2e3138;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
292
src/views/AnswerQuestions/components/UploadAnswerPanel.vue
Normal file
292
src/views/AnswerQuestions/components/UploadAnswerPanel.vue
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
<template>
|
||||||
|
<div class="answer-card">
|
||||||
|
<h3 class="answer-title">我的答案</h3>
|
||||||
|
<div class="upload-box">
|
||||||
|
<input
|
||||||
|
ref="fileInputRef"
|
||||||
|
class="upload-input"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
@change="handleFileChange"
|
||||||
|
/>
|
||||||
|
<div v-if="images.length === 0" class="upload-placeholder">
|
||||||
|
<button class="upload-trigger" type="button" @click="openFilePicker">
|
||||||
|
<div class="upload-btn">相机/上传</div>
|
||||||
|
<p>请上传答案,最多3张图片</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="main-preview">
|
||||||
|
<img :src="activeImage.url" :alt="activeImage.name" />
|
||||||
|
<button class="delete-btn" type="button" @click="removeImage(activeIndex)">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="thumb-list">
|
||||||
|
<button
|
||||||
|
v-for="(image, index) in images"
|
||||||
|
:key="image.url"
|
||||||
|
class="thumb-item"
|
||||||
|
:class="{ active: activeIndex === index }"
|
||||||
|
type="button"
|
||||||
|
@click="activeIndex = index"
|
||||||
|
>
|
||||||
|
<img :src="image.url" :alt="image.name" />
|
||||||
|
<span class="thumb-delete" @click.stop="removeImage(index)">×</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="images.length < maxImageCount"
|
||||||
|
class="add-thumb"
|
||||||
|
type="button"
|
||||||
|
@click="openFilePicker"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="upload-tip">
|
||||||
|
最多上传3张图片(已上传{{ images.length }}/3张)
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import type { UploadAnswerValue } from "@/views/AnswerQuestions/components/types";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue?: UploadAnswerValue[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "update:modelValue", value: UploadAnswerValue[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const maxImageCount = 3;
|
||||||
|
const activeIndex = ref(0);
|
||||||
|
const fileInputRef = ref<HTMLInputElement>();
|
||||||
|
|
||||||
|
const images = computed(() => props.modelValue ?? []);
|
||||||
|
const activeImage = computed(() => images.value[activeIndex.value] ?? images.value[0]);
|
||||||
|
|
||||||
|
function openFilePicker() {
|
||||||
|
fileInputRef.value?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
images,
|
||||||
|
(list: UploadAnswerValue[]) => {
|
||||||
|
// 删除图片后,保证当前预览下标始终落在已有图片范围内
|
||||||
|
if (activeIndex.value > list.length - 1) {
|
||||||
|
activeIndex.value = Math.max(0, list.length - 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleFileChange(event: Event) {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
const files = Array.from(target.files ?? []);
|
||||||
|
if (files.length === 0) return;
|
||||||
|
|
||||||
|
const restCount = maxImageCount - images.value.length;
|
||||||
|
const nextImages = files.slice(0, restCount).map((file) => ({
|
||||||
|
name: file.name,
|
||||||
|
// 这里先用本地预览 URL 模拟上传成功态,后续可替换接口返回地址
|
||||||
|
url: URL.createObjectURL(file),
|
||||||
|
}));
|
||||||
|
|
||||||
|
emit("update:modelValue", [...images.value, ...nextImages]);
|
||||||
|
activeIndex.value = images.value.length;
|
||||||
|
target.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeImage(index: number) {
|
||||||
|
const nextImages = images.value.filter(
|
||||||
|
(_image: UploadAnswerValue, currentIndex: number) => currentIndex !== index,
|
||||||
|
);
|
||||||
|
emit("update:modelValue", nextImages);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.answer-card {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-title {
|
||||||
|
font-size: 40px;
|
||||||
|
color: #2f333a;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
// padding-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-box {
|
||||||
|
margin-top: 2px;
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: transparent;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: #2d6fe0;
|
||||||
|
font-size: 24px;
|
||||||
|
gap: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 520px;
|
||||||
|
border: 2px solid #2b9be5;
|
||||||
|
border-radius: 14px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 26px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn {
|
||||||
|
height: 64px;
|
||||||
|
min-width: 190px;
|
||||||
|
border-radius: 32px;
|
||||||
|
padding: 0 34px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #e9f3ff;
|
||||||
|
color: #2f80ed;
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-trigger {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-preview {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 320px;
|
||||||
|
border: 2px solid #b9d6f4;
|
||||||
|
border-radius: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn,
|
||||||
|
.thumb-delete {
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid #6d7485;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
color: #4d5568;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-item,
|
||||||
|
.add-thumb {
|
||||||
|
position: relative;
|
||||||
|
width: 144px;
|
||||||
|
height: 96px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f6f8fb;
|
||||||
|
border: 1px solid #e1e8f2;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border: 2px solid #2b80ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-delete {
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-thumb {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px dashed #aac8ec;
|
||||||
|
color: #2d6fe0;
|
||||||
|
font-size: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-tip {
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff9e8;
|
||||||
|
color: #d99427;
|
||||||
|
font-size: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
22
src/views/AnswerQuestions/components/types.ts
Normal file
22
src/views/AnswerQuestions/components/types.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export type AnswerQuestion = {
|
||||||
|
id: number;
|
||||||
|
subject: string;
|
||||||
|
questionType: string;
|
||||||
|
difficulty: string;
|
||||||
|
score: number;
|
||||||
|
duration: number;
|
||||||
|
typeLabel: string;
|
||||||
|
answerType: "choice" | "upload";
|
||||||
|
stem: string;
|
||||||
|
options?: Array<{ value: string; label: string }>;
|
||||||
|
userAnswer?: string;
|
||||||
|
correctAnswer?: string;
|
||||||
|
isCorrect?: boolean;
|
||||||
|
answerText?: string;
|
||||||
|
explanation?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UploadAnswerValue = {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
@ -51,14 +51,11 @@ const tools = [
|
|||||||
"单词闯关",
|
"单词闯关",
|
||||||
"单词测评",
|
"单词测评",
|
||||||
"词典",
|
"词典",
|
||||||
"单词书",
|
"生词本",
|
||||||
"单词闯关",
|
"乐读",
|
||||||
"单词测评",
|
"乐训语感",
|
||||||
"词典",
|
"句子箱",
|
||||||
"单词书",
|
"零基础",
|
||||||
"单词闯关",
|
|
||||||
"单词测评",
|
|
||||||
"词典",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const pageSize = 9;
|
const pageSize = 9;
|
||||||
@ -108,9 +105,7 @@ function onMouseUp(e: MouseEvent) {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background: #d8e1f0;
|
gap: 16px;
|
||||||
// padding: 16px 16px 14px;
|
|
||||||
gap: 14px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,12 +151,12 @@ function onMouseUp(e: MouseEvent) {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 28px;
|
padding: 52px 64px;
|
||||||
box-shadow: 0 4px 12px rgba(51, 83, 145, 0.08);
|
box-shadow: 0 4px 12px rgba(51, 83, 145, 0.08);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: box-shadow 0.2s;
|
transition: box-shadow 0.2s;
|
||||||
width: 554px;
|
min-width: 0;
|
||||||
height: 238px;
|
min-height: 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: 0 6px 18px rgba(51, 83, 145, 0.15);
|
box-shadow: 0 6px 18px rgba(51, 83, 145, 0.15);
|
||||||
@ -180,23 +175,23 @@ function onMouseUp(e: MouseEvent) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -14px;
|
bottom: -22px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dot {
|
.dot {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #D6DEEB;
|
background: #d6deeb;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
background: #ADC9FF;
|
background: #adc9ff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -62,7 +62,7 @@
|
|||||||
v-for="book in activeBooks"
|
v-for="book in activeBooks"
|
||||||
:key="book.id"
|
:key="book.id"
|
||||||
:book="book"
|
:book="book"
|
||||||
@click="goToBook(book)"
|
@click="goToActiveBook(book)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -125,6 +125,15 @@ const englishBooks: Book[] = [
|
|||||||
{ id: 13, year: null, level: null, name: "Join in 外研剑桥", subtitle: "(三年级上册)" },
|
{ id: 13, year: null, level: null, name: "Join in 外研剑桥", subtitle: "(三年级上册)" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const englishVideoBooks: Book[] = [
|
||||||
|
{ id: 101, year: "2012", level: null, name: "北师大版", subtitle: "(三年级上册)" },
|
||||||
|
{ id: 102, year: null, level: null, name: "Join in 外研剑桥", subtitle: "(三年级上册)" },
|
||||||
|
{ id: 103, year: "2012", level: "入门", name: "北师大版", subtitle: "(三年级上册)" },
|
||||||
|
{ id: 104, year: null, level: null, name: "人教版 PEP", subtitle: "(三年级上册)" },
|
||||||
|
{ id: 105, year: null, level: null, name: "译林版", subtitle: "(三年级上册)" },
|
||||||
|
{ id: 106, year: "2024", level: null, name: "外研版", subtitle: "(三年级上册)" },
|
||||||
|
];
|
||||||
|
|
||||||
const subjectAllBooksMap: Record<string, Book[]> = {
|
const subjectAllBooksMap: Record<string, Book[]> = {
|
||||||
英语: englishBooks,
|
英语: englishBooks,
|
||||||
语文: [],
|
语文: [],
|
||||||
@ -132,8 +141,16 @@ const subjectAllBooksMap: Record<string, Book[]> = {
|
|||||||
科学: [],
|
科学: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const subjectVideoBooksMap: Record<string, Book[]> = {
|
||||||
|
英语: englishVideoBooks,
|
||||||
|
语文: [],
|
||||||
|
数学: [],
|
||||||
|
科学: [],
|
||||||
|
};
|
||||||
|
|
||||||
/** 当前科目下全部可选书本 */
|
/** 当前科目下全部可选书本 */
|
||||||
const allBooks = computed<Book[]>(() => subjectAllBooksMap[subjectName.value] ?? []);
|
const allBooks = computed<Book[]>(() => subjectAllBooksMap[subjectName.value] ?? []);
|
||||||
|
const videoBooks = computed<Book[]>(() => subjectVideoBooksMap[subjectName.value] ?? []);
|
||||||
|
|
||||||
/** 已添加的书本 id(默认全部添加,模拟初始状态) */
|
/** 已添加的书本 id(默认全部添加,模拟初始状态) */
|
||||||
const addedBookIds = ref<number[]>(englishBooks.map(b => b.id));
|
const addedBookIds = ref<number[]>(englishBooks.map(b => b.id));
|
||||||
@ -147,7 +164,7 @@ function handleConfirm(selectedIds: number[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const activeBooks = computed<Book[]>(() => {
|
const activeBooks = computed<Book[]>(() => {
|
||||||
if (activeTab.value === "video") return [];
|
if (activeTab.value === "video") return videoBooks.value;
|
||||||
return allBooks.value.filter((b: Book) => addedBookIds.value.includes(b.id));
|
return allBooks.value.filter((b: Book) => addedBookIds.value.includes(b.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -167,6 +184,28 @@ function goToBook(book: Book) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goToVideoBook(book: Book) {
|
||||||
|
router.push({
|
||||||
|
name: 'video-learning',
|
||||||
|
params: {
|
||||||
|
subject: subjectName.value,
|
||||||
|
bookId: book.id,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
name: book.name,
|
||||||
|
subtitle: book.subtitle,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToActiveBook(book: Book) {
|
||||||
|
if (activeTab.value === "video") {
|
||||||
|
goToVideoBook(book);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
goToBook(book);
|
||||||
|
}
|
||||||
|
|
||||||
function goToWrongQuestions() {
|
function goToWrongQuestions() {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'wrong-questions',
|
name: 'wrong-questions',
|
||||||
|
|||||||
601
src/views/Subjects/VideoLearning.vue
Normal file
601
src/views/Subjects/VideoLearning.vue
Normal file
@ -0,0 +1,601 @@
|
|||||||
|
<template>
|
||||||
|
<div class="video-learning">
|
||||||
|
<SubPageHeader>
|
||||||
|
<template #center>
|
||||||
|
<span class="header-title">{{ bookTitle }}</span>
|
||||||
|
</template>
|
||||||
|
</SubPageHeader>
|
||||||
|
|
||||||
|
<main class="video-body">
|
||||||
|
<section class="learning-panel">
|
||||||
|
<aside class="catalog-panel">
|
||||||
|
<div class="catalog-header">
|
||||||
|
<span class="catalog-title">目录</span>
|
||||||
|
<button class="catalog-action" aria-label="目录设置">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="catalog-list">
|
||||||
|
<div v-for="unit in units" :key="unit.id" class="unit-block">
|
||||||
|
<button class="unit-row" @click="toggleUnit(unit)">
|
||||||
|
<span>{{ unit.name }}</span>
|
||||||
|
<svg
|
||||||
|
class="unit-arrow"
|
||||||
|
:class="{ expanded: unit.expanded }"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M7 10l5 5 5-5" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="unit.expanded" class="lesson-list">
|
||||||
|
<button
|
||||||
|
v-for="lesson in unit.lessons"
|
||||||
|
:key="lesson.id"
|
||||||
|
class="lesson-row"
|
||||||
|
:class="{ active: activeLessonId === lesson.id }"
|
||||||
|
@click="selectLesson(lesson)"
|
||||||
|
>
|
||||||
|
{{ lesson.name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section class="content-panel">
|
||||||
|
<div class="content-header">
|
||||||
|
<h1>章节视频</h1>
|
||||||
|
<div class="segment-tabs">
|
||||||
|
<button
|
||||||
|
v-for="tab in videoTabs"
|
||||||
|
:key="tab.key"
|
||||||
|
class="segment-tab"
|
||||||
|
:class="{ active: activeVideoTab === tab.key }"
|
||||||
|
@click="activeVideoTab = tab.key"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="video-title">{{ selectedVideo.title }}</p>
|
||||||
|
|
||||||
|
<div class="hero-video">
|
||||||
|
<div class="board-pattern"></div>
|
||||||
|
<div class="teacher-card">
|
||||||
|
<div class="teacher-avatar"></div>
|
||||||
|
</div>
|
||||||
|
<button class="play-btn" aria-label="播放视频">
|
||||||
|
<span></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="episode-section">
|
||||||
|
<h2>选集</h2>
|
||||||
|
<div class="episode-list">
|
||||||
|
<button
|
||||||
|
v-for="episode in selectedVideo.episodes"
|
||||||
|
:key="episode.id"
|
||||||
|
class="episode-card"
|
||||||
|
:class="{ active: activeEpisodeId === episode.id }"
|
||||||
|
@click="activeEpisodeId = episode.id"
|
||||||
|
>
|
||||||
|
<div class="episode-thumb" :class="`episode-thumb--${episode.tone}`">
|
||||||
|
<span v-if="episode.playing" class="mini-pause"></span>
|
||||||
|
</div>
|
||||||
|
<span class="episode-name">{{ episode.title }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import SubPageHeader from '@/components/layout/SubPageHeader.vue'
|
||||||
|
|
||||||
|
interface Lesson {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Unit {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
expanded: boolean
|
||||||
|
lessons: Lesson[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Episode {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
tone: 'dark' | 'green' | 'blue'
|
||||||
|
playing?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VideoLesson {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
episodes: Episode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const bookName = computed(() => String(route.query.name || ''))
|
||||||
|
const bookSubtitle = computed(() => String(route.query.subtitle || ''))
|
||||||
|
const bookTitle = computed(() =>
|
||||||
|
bookName.value && bookSubtitle.value
|
||||||
|
? `${bookName.value}${bookSubtitle.value}`
|
||||||
|
: bookName.value || '视频学习',
|
||||||
|
)
|
||||||
|
|
||||||
|
const units = ref<Unit[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Unit 1 New school, new beginning!',
|
||||||
|
expanded: true,
|
||||||
|
lessons: [
|
||||||
|
{ id: 101, name: 'Lesson 1 Hello!' },
|
||||||
|
{ id: 102, name: 'Lesson 2 Let me show you around!' },
|
||||||
|
{ id: 103, name: 'Lesson 3 Look at the photo!' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Unit 2 Learning English is fun!',
|
||||||
|
expanded: false,
|
||||||
|
lessons: [
|
||||||
|
{ id: 201, name: 'Lesson 4 I love English!' },
|
||||||
|
{ id: 202, name: 'Lesson 5 My English book' },
|
||||||
|
{ id: 203, name: 'Lesson 6 Lets sing a song!' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Unit 3 All about me',
|
||||||
|
expanded: false,
|
||||||
|
lessons: [
|
||||||
|
{ id: 301, name: 'Lesson 7 My family' },
|
||||||
|
{ id: 302, name: 'Lesson 8 I have a pet' },
|
||||||
|
{ id: 303, name: 'Lesson 9 Review' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const videoTabs = [
|
||||||
|
{ key: 'chapter' as const, label: '章节' },
|
||||||
|
{ key: 'knowledge' as const, label: '知识点' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const activeLessonId = ref(101)
|
||||||
|
const activeVideoTab = ref<'chapter' | 'knowledge'>('chapter')
|
||||||
|
const activeEpisodeId = ref(1)
|
||||||
|
|
||||||
|
const videoLessons: VideoLesson[] = [
|
||||||
|
{
|
||||||
|
id: 101,
|
||||||
|
title: '视频标题xxxx',
|
||||||
|
episodes: [
|
||||||
|
{ id: 1, title: '视频标题xxx...', tone: 'dark', playing: true },
|
||||||
|
{ id: 2, title: '视频标题xxx...', tone: 'green' },
|
||||||
|
{ id: 3, title: '视频标题xxx...', tone: 'green' },
|
||||||
|
{ id: 4, title: '视频标题xxx...', tone: 'blue' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 102,
|
||||||
|
title: '校园介绍重点句型',
|
||||||
|
episodes: [
|
||||||
|
{ id: 5, title: 'Let me show...', tone: 'green', playing: true },
|
||||||
|
{ id: 6, title: 'Around school...', tone: 'blue' },
|
||||||
|
{ id: 7, title: 'Practice time...', tone: 'green' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 103,
|
||||||
|
title: '看图表达与单词复习',
|
||||||
|
episodes: [
|
||||||
|
{ id: 8, title: 'Look at photo...', tone: 'blue', playing: true },
|
||||||
|
{ id: 9, title: 'Words review...', tone: 'dark' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const selectedVideo = computed(() =>
|
||||||
|
videoLessons.find(video => video.id === activeLessonId.value) ?? videoLessons[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
function toggleUnit(unit: Unit) {
|
||||||
|
unit.expanded = !unit.expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectLesson(lesson: Lesson) {
|
||||||
|
activeLessonId.value = lesson.id
|
||||||
|
activeEpisodeId.value = selectedVideo.value.episodes[0]?.id ?? 0
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.video-learning {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
background: #292929;
|
||||||
|
color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 30px;
|
||||||
|
color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.back-icon path) {
|
||||||
|
stroke: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 64px 32px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.learning-panel {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1792px;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 690px minmax(0, 1fr);
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 22px;
|
||||||
|
background: #2e2e2f;
|
||||||
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-panel {
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #454545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-header {
|
||||||
|
height: 86px;
|
||||||
|
padding: 0 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-title {
|
||||||
|
font-size: 28px;
|
||||||
|
color: rgba(255, 255, 255, 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-action {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
place-content: center;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
span {
|
||||||
|
width: 18px;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgba(255, 255, 255, 0.55);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.catalog-list {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit-row,
|
||||||
|
.lesson-row {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
text-align: left;
|
||||||
|
color: rgba(255, 255, 255, 0.86);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit-row {
|
||||||
|
min-height: 78px;
|
||||||
|
padding: 0 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 25px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit-arrow {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
transform: rotate(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-list {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-row {
|
||||||
|
position: relative;
|
||||||
|
height: 78px;
|
||||||
|
padding: 0 34px 0 42px;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 24px;
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
transition: color 0.15s, background 0.15s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(0, 0, 0, 0.16);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 6px;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
background: #2f80ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 34px 34px 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #303030;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.segment-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 99px;
|
||||||
|
background: #616161;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segment-tab {
|
||||||
|
min-width: 78px;
|
||||||
|
height: 42px;
|
||||||
|
padding: 0 18px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 99px;
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(255, 255, 255, 0.54);
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #fff;
|
||||||
|
color: #252525;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-title {
|
||||||
|
margin-top: 36px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-video {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 28px;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
max-height: 572px;
|
||||||
|
min-height: 360px;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 70% 30%, rgba(255, 255, 255, 0.18), transparent 22%),
|
||||||
|
linear-gradient(155deg, #0d5a50 0%, #176f61 48%, #0c4a43 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-pattern {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0.58;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(25deg, transparent 0 45%, rgba(255, 255, 255, 0.45) 46%, transparent 48%),
|
||||||
|
linear-gradient(115deg, transparent 0 56%, rgba(255, 255, 255, 0.28) 57%, transparent 59%);
|
||||||
|
background-size: 210px 140px, 260px 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.teacher-card {
|
||||||
|
position: absolute;
|
||||||
|
right: 12%;
|
||||||
|
bottom: 0;
|
||||||
|
width: 230px;
|
||||||
|
height: 78%;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.teacher-avatar {
|
||||||
|
width: 165px;
|
||||||
|
height: 270px;
|
||||||
|
border-radius: 90px 90px 18px 18px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 50% 48px, #ffe2c5 0 42px, transparent 43px),
|
||||||
|
linear-gradient(#f4f1e9 0 92px, #6c6f75 93px 100%);
|
||||||
|
box-shadow: 0 0 0 12px rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-btn {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 82px;
|
||||||
|
height: 82px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0, 0, 0, 0.34);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
span {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
margin-left: 6px;
|
||||||
|
border-top: 17px solid transparent;
|
||||||
|
border-bottom: 17px solid transparent;
|
||||||
|
border-left: 25px solid #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-section {
|
||||||
|
margin-top: 36px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 22px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-card {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: 2px solid transparent;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
outline-color: rgba(255, 255, 255, 0.42);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-thumb {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 7;
|
||||||
|
background:
|
||||||
|
linear-gradient(155deg, rgba(5, 48, 43, 0.85), rgba(19, 108, 94, 0.88)),
|
||||||
|
repeating-linear-gradient(25deg, transparent 0 34px, rgba(255, 255, 255, 0.22) 35px 37px);
|
||||||
|
|
||||||
|
&--green {
|
||||||
|
background:
|
||||||
|
linear-gradient(155deg, rgba(18, 93, 80, 0.72), rgba(30, 120, 100, 0.88)),
|
||||||
|
repeating-linear-gradient(25deg, transparent 0 34px, rgba(255, 255, 255, 0.22) 35px 37px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--blue {
|
||||||
|
background:
|
||||||
|
linear-gradient(155deg, rgba(17, 75, 105, 0.72), rgba(45, 115, 145, 0.86)),
|
||||||
|
repeating-linear-gradient(25deg, transparent 0 34px, rgba(255, 255, 255, 0.2) 35px 37px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-pause {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
width: 6px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-name {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
padding: 22px 12px 8px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 17px;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.72));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -19,39 +19,24 @@
|
|||||||
<div class="filter-groups">
|
<div class="filter-groups">
|
||||||
<!-- 学科筛选 -->
|
<!-- 学科筛选 -->
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<button
|
<button v-for="item in subjectFilters" :key="item.key" class="filter-btn"
|
||||||
v-for="item in subjectFilters"
|
:class="{ active: activeSubject === item.key }" @click="activeSubject = item.key">
|
||||||
:key="item.key"
|
|
||||||
class="filter-btn"
|
|
||||||
:class="{ active: activeSubject === item.key }"
|
|
||||||
@click="activeSubject = item.key"
|
|
||||||
>
|
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 重练状态筛选 -->
|
<!-- 重练状态筛选 -->
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<button
|
<button v-for="item in practiceFilters" :key="item.key" class="filter-btn"
|
||||||
v-for="item in practiceFilters"
|
:class="{ active: activePractice === item.key }" @click="activePractice = item.key">
|
||||||
:key="item.key"
|
|
||||||
class="filter-btn"
|
|
||||||
:class="{ active: activePractice === item.key }"
|
|
||||||
@click="activePractice = item.key"
|
|
||||||
>
|
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 时间筛选 -->
|
<!-- 时间筛选 -->
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<button
|
<button v-for="item in timeFilters" :key="item.key" class="filter-btn"
|
||||||
v-for="item in timeFilters"
|
:class="{ active: activeTime === item.key }" @click="activeTime = item.key">
|
||||||
:key="item.key"
|
|
||||||
class="filter-btn"
|
|
||||||
:class="{ active: activeTime === item.key }"
|
|
||||||
@click="activeTime = item.key"
|
|
||||||
>
|
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -61,12 +46,8 @@
|
|||||||
<!-- 题目列表网格 -->
|
<!-- 题目列表网格 -->
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="question-grid">
|
<div class="question-grid">
|
||||||
<div
|
<div v-for="q in filteredQuestions" :key="q.id" class="question-card"
|
||||||
v-for="q in filteredQuestions"
|
:class="{ 'card-poor': q.accuracy === 0 }">
|
||||||
:key="q.id"
|
|
||||||
class="question-card"
|
|
||||||
:class="{ 'card-poor': q.accuracy === 0 }"
|
|
||||||
>
|
|
||||||
<!-- 题目文本 -->
|
<!-- 题目文本 -->
|
||||||
<p class="question-text">{{ q.content }}</p>
|
<p class="question-text">{{ q.content }}</p>
|
||||||
|
|
||||||
@ -80,10 +61,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="stat-item">
|
<span class="stat-item">
|
||||||
<span class="stat-label">正确率:</span>
|
<span class="stat-label">正确率:</span>
|
||||||
<span
|
<span class="stat-accuracy" :class="q.accuracy > 0 ? 'high' : 'zero'">
|
||||||
class="stat-accuracy"
|
|
||||||
:class="q.accuracy > 0 ? 'high' : 'zero'"
|
|
||||||
>
|
|
||||||
{{ q.accuracy }}%
|
{{ q.accuracy }}%
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@ -91,11 +69,7 @@
|
|||||||
|
|
||||||
<!-- 标签行 -->
|
<!-- 标签行 -->
|
||||||
<div class="tag-row">
|
<div class="tag-row">
|
||||||
<span
|
<span class="tag subject-tag" :class="'subject-' + q.subject">{{ q.subject }}</span>
|
||||||
class="tag subject-tag"
|
|
||||||
:class="'subject-' + q.subject"
|
|
||||||
>{{ q.subject }}</span
|
|
||||||
>
|
|
||||||
<span class="tag type-tag">{{ q.type }}</span>
|
<span class="tag type-tag">{{ q.type }}</span>
|
||||||
<span class="tag level-tag">{{ q.level }}</span>
|
<span class="tag level-tag">{{ q.level }}</span>
|
||||||
<span class="tag-time">{{ q.time }}</span>
|
<span class="tag-time">{{ q.time }}</span>
|
||||||
@ -106,10 +80,7 @@
|
|||||||
<button class="action-btn detail-btn" @click="viewDetail(q)">
|
<button class="action-btn detail-btn" @click="viewDetail(q)">
|
||||||
查看详情
|
查看详情
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="action-btn extend-btn" @click="extendPractice(q)">
|
||||||
class="action-btn extend-btn"
|
|
||||||
@click="extendPractice(q)"
|
|
||||||
>
|
|
||||||
举一反三
|
举一反三
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -119,12 +90,60 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 错题重练 -->
|
||||||
|
<el-dialog v-model="showRetrainDialog" class="retrain-dialog" width="880px" :show-close="false"
|
||||||
|
:close-on-click-modal="true" modal-class="retrain-dialog-overlay" align-center>
|
||||||
|
<div class="retrain-panel">
|
||||||
|
<h3 class="dialog-title">错题重练</h3>
|
||||||
|
|
||||||
|
<div class="dialog-section">
|
||||||
|
<div class="section-title">选择条件</div>
|
||||||
|
<div class="condition-row">
|
||||||
|
<LmSelect
|
||||||
|
v-for="item in retrainConditions"
|
||||||
|
:key="item.key"
|
||||||
|
v-model="retrainForm[item.key]"
|
||||||
|
:options="item.options"
|
||||||
|
width="180px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-section question-count-section">
|
||||||
|
<div class="section-title">
|
||||||
|
题目抽取数量 <span class="section-tip">(1-50题)</span>
|
||||||
|
</div>
|
||||||
|
<div class="counter-row">
|
||||||
|
<button class="counter-btn" type="button" :disabled="retrainCount <= minRetrainCount"
|
||||||
|
@click="decreaseRetrainCount">
|
||||||
|
-
|
||||||
|
</button>
|
||||||
|
<div class="counter-value">{{ retrainCount }}</div>
|
||||||
|
<button class="counter-btn" type="button" :disabled="retrainCount >= maxRetrainCount"
|
||||||
|
@click="increaseRetrainCount">
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-actions">
|
||||||
|
<button class="dialog-btn cancel-btn" type="button" @click="showRetrainDialog = false">
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button class="dialog-btn confirm-btn" type="button" @click="confirmRetrain">
|
||||||
|
确认
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
import SubPageHeader from "@/components/layout/SubPageHeader.vue";
|
import SubPageHeader from "@/components/layout/SubPageHeader.vue";
|
||||||
|
import LmSelect from "@/components/lm/LmSelect.vue";
|
||||||
import cuotichonglianIcon from "@/assets/subjects/cuotichonglian.svg";
|
import cuotichonglianIcon from "@/assets/subjects/cuotichonglian.svg";
|
||||||
|
|
||||||
/* ---- 筛选选项 ---- */
|
/* ---- 筛选选项 ---- */
|
||||||
@ -150,6 +169,59 @@ const timeFilters = [
|
|||||||
const activeSubject = ref("all");
|
const activeSubject = ref("all");
|
||||||
const activePractice = ref("all");
|
const activePractice = ref("all");
|
||||||
const activeTime = ref("all");
|
const activeTime = ref("all");
|
||||||
|
const showRetrainDialog = ref(false);
|
||||||
|
const minRetrainCount = 1;
|
||||||
|
const maxRetrainCount = 50;
|
||||||
|
const retrainCount = ref(10);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
type RetrainConditionKey = "subject" | "book" | "chapter" | "time";
|
||||||
|
|
||||||
|
const retrainForm = ref<Record<RetrainConditionKey, string>>({
|
||||||
|
subject: "all-subject",
|
||||||
|
book: "all-book",
|
||||||
|
chapter: "all-chapter",
|
||||||
|
time: "all-time",
|
||||||
|
});
|
||||||
|
|
||||||
|
const retrainConditions: Array<{
|
||||||
|
key: RetrainConditionKey;
|
||||||
|
options: Array<{ label: string; value: string }>;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
key: "subject",
|
||||||
|
options: [
|
||||||
|
{ label: "全部学科", value: "all-subject" },
|
||||||
|
{ label: "英语", value: "english" },
|
||||||
|
{ label: "数学", value: "math" },
|
||||||
|
{ label: "语文", value: "chinese" },
|
||||||
|
{ label: "科学", value: "science" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "book",
|
||||||
|
options: [
|
||||||
|
{ label: "全部教材", value: "all-book" },
|
||||||
|
{ label: "当前教材", value: "current-book" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "chapter",
|
||||||
|
options: [
|
||||||
|
{ label: "全部章节", value: "all-chapter" },
|
||||||
|
{ label: "当前章节", value: "current-chapter" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "time",
|
||||||
|
options: [
|
||||||
|
{ label: "全部时间", value: "all-time" },
|
||||||
|
{ label: "今天", value: "today" },
|
||||||
|
{ label: "近一周", value: "week" },
|
||||||
|
{ label: "近一月", value: "month" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
/* ---- 模拟数据 ---- */
|
/* ---- 模拟数据 ---- */
|
||||||
interface Question {
|
interface Question {
|
||||||
@ -277,14 +349,46 @@ const filteredQuestions = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleRetrain() {
|
function handleRetrain() {
|
||||||
// TODO: 跳转错题重练
|
showRetrainDialog.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function decreaseRetrainCount() {
|
||||||
|
retrainCount.value = Math.max(minRetrainCount, retrainCount.value - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function increaseRetrainCount() {
|
||||||
|
retrainCount.value = Math.min(maxRetrainCount, retrainCount.value + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmRetrain() {
|
||||||
|
showRetrainDialog.value = false;
|
||||||
|
// 关闭弹框后进入错题重练答题页,并把当前筛选条件传过去
|
||||||
|
router.push({
|
||||||
|
name: "wrong-retrain-answer",
|
||||||
|
query: {
|
||||||
|
mode: "answer",
|
||||||
|
count: String(retrainCount.value),
|
||||||
|
subject: retrainForm.value.subject,
|
||||||
|
book: retrainForm.value.book,
|
||||||
|
chapter: retrainForm.value.chapter,
|
||||||
|
time: retrainForm.value.time,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function viewDetail(q: Question) {
|
function viewDetail(q: Question) {
|
||||||
// TODO: 查看详情
|
router.push({
|
||||||
|
name: "wrong-retrain-answer",
|
||||||
|
query: {
|
||||||
|
mode: "detail",
|
||||||
|
questionId: String(q.id),
|
||||||
|
count: "10",
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function extendPractice(q: Question) {
|
function extendPractice(q: Question) {
|
||||||
|
void q;
|
||||||
// TODO: 举一反三
|
// TODO: 举一反三
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -426,11 +530,11 @@ function extendPractice(q: Question) {
|
|||||||
.question-card {
|
.question-card {
|
||||||
background: #f2fbff;
|
background: #f2fbff;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
// padding: 28px 28px 20px;
|
// padding: 28px 28px 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border: 1.5px solid #e1f2fa;
|
border: 1.5px solid #e1f2fa;
|
||||||
// gap: 16px;
|
// gap: 16px;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
&.card-poor {
|
&.card-poor {
|
||||||
@ -447,10 +551,11 @@ function extendPractice(q: Question) {
|
|||||||
padding: 22px 24px;
|
padding: 22px 24px;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.question-text {
|
.question-text {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
color: #121212;
|
color: #121212;
|
||||||
// line-height: 1.5;
|
// line-height: 1.5;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -524,14 +629,17 @@ function extendPractice(q: Question) {
|
|||||||
color: #2d6fe0;
|
color: #2d6fe0;
|
||||||
background: rgba(45, 111, 224, 0.1);
|
background: rgba(45, 111, 224, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.subject-语文 {
|
&.subject-语文 {
|
||||||
color: #27ae60;
|
color: #27ae60;
|
||||||
background: rgba(39, 174, 96, 0.1);
|
background: rgba(39, 174, 96, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.subject-数学 {
|
&.subject-数学 {
|
||||||
color: #2d6fe0;
|
color: #2d6fe0;
|
||||||
background: rgba(45, 111, 224, 0.1);
|
background: rgba(45, 111, 224, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.subject-科学 {
|
&.subject-科学 {
|
||||||
color: #9b59b6;
|
color: #9b59b6;
|
||||||
background: rgba(155, 89, 182, 0.1);
|
background: rgba(155, 89, 182, 0.1);
|
||||||
@ -589,4 +697,137 @@ function extendPractice(q: Question) {
|
|||||||
border: 1.5px solid #2d6fe0;
|
border: 1.5px solid #2d6fe0;
|
||||||
color: #2d6fe0;
|
color: #2d6fe0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(.retrain-dialog) {
|
||||||
|
--el-dialog-padding-primary: 0;
|
||||||
|
border-radius: 26px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.retrain-dialog .el-dialog__header) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.retrain-dialog .el-dialog__body) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.retrain-dialog-overlay) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
.retrain-panel {
|
||||||
|
width: 100%;
|
||||||
|
// min-height: 640px;
|
||||||
|
padding: 48px 56px;
|
||||||
|
border-radius: 26px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
margin-bottom: 52px;
|
||||||
|
color: #121212;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-section {
|
||||||
|
margin-bottom: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: #121212;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-tip {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.condition-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-count-section {
|
||||||
|
margin-bottom: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
border: 1px solid #edf0f5;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
color: #222;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 26px;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: #d9dee7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 168px;
|
||||||
|
height: 58px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f5f6f8;
|
||||||
|
color: #333;
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-btn {
|
||||||
|
width: 190px;
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.86;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
border: 2px solid #b7d4f2;
|
||||||
|
background: #fff;
|
||||||
|
color: #6ca8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn {
|
||||||
|
border: none;
|
||||||
|
background: #2f80ed;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user