feat: 单词书页面+优化
This commit is contained in:
parent
1c2d429b11
commit
d34cb7b5a4
4
.components.d.ts
vendored
4
.components.d.ts
vendored
@ -13,7 +13,6 @@ declare module 'vue' {
|
||||
AnalysisLineChart: typeof import('./src/components/analysis/AnalysisLineChart.vue')['default']
|
||||
AnalysisStatCard: typeof import('./src/components/analysis/AnalysisStatCard.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']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
@ -25,8 +24,6 @@ declare module 'vue' {
|
||||
LmStudentTable: typeof import('./src/components/lm/LmStudentTable.vue')['default']
|
||||
LoadingState: typeof import('./src/components/loading/LoadingState.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']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SubjectAnalysisContent: typeof import('./src/components/analysis/SubjectAnalysisContent.vue')['default']
|
||||
@ -36,6 +33,5 @@ declare module 'vue' {
|
||||
TabBar: typeof import('./src/components/layout/TabBar.vue')['default']
|
||||
TaskCard: typeof import('./src/components/cards/TaskCard.vue')['default']
|
||||
TaskSidebar: typeof import('./src/components/tasks/TaskSidebar.vue')['default']
|
||||
UploadAnswerPanel: typeof import('./src/components/answerQuestions/UploadAnswerPanel.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,13 @@
|
||||
<span class="reward-item"><i class="reward-icon"></i>x20</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="task-btn" :class="{ 'is-disabled': disabled }">前往</button>
|
||||
<button
|
||||
class="task-btn lx-btn lx-btn--lg lx-btn--short lx-btn--primary"
|
||||
type="button"
|
||||
:disabled="disabled"
|
||||
>
|
||||
前往
|
||||
</button>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
@ -78,33 +84,6 @@ defineProps<{
|
||||
}
|
||||
|
||||
.task-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
min-height: 4.125rem;
|
||||
min-width: 10.5rem;
|
||||
padding: 0 24px;
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
background: #3d78f2;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #356de0;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
background: #aec4f2;
|
||||
cursor: not-allowed;
|
||||
|
||||
&:hover {
|
||||
background: #aec4f2;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -13,7 +13,12 @@
|
||||
<img :src="emptySvg" class="loading-state__img" alt="error" />
|
||||
<p class="loading-state__text loading-state__text--error">{{ errorText }}</p>
|
||||
<slot name="error-action">
|
||||
<button v-if="showRetry" class="loading-state__retry-btn" @click="$emit('retry')">
|
||||
<button
|
||||
v-if="showRetry"
|
||||
class="loading-state__retry-btn lx-btn lx-btn--sm lx-btn--primary"
|
||||
type="button"
|
||||
@click="$emit('retry')"
|
||||
>
|
||||
重新加载
|
||||
</button>
|
||||
</slot>
|
||||
@ -98,24 +103,6 @@ defineEmits<{
|
||||
|
||||
&__retry-btn {
|
||||
margin-top: 4px;
|
||||
padding: 8px 24px;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
background: $theme-color;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: $hover-theme-fill;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载中旋转动画
|
||||
|
||||
@ -79,6 +79,12 @@ const router = createRouter({
|
||||
name: 'english-tools',
|
||||
component: () => import('@/views/EnglishTools/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/english-tools/word-book',
|
||||
name: 'english-word-book',
|
||||
component: () => import('@/views/EnglishTools/WordBook.vue'),
|
||||
meta: { hideLayout: true },
|
||||
},
|
||||
{
|
||||
path: '/insights',
|
||||
name: 'insights',
|
||||
|
||||
@ -38,10 +38,17 @@
|
||||
<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>
|
||||
<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="next-btn" type="button" @click="goNextQuestion">下一题</button>
|
||||
<button
|
||||
v-else
|
||||
class="lx-btn lx-btn--md lx-btn--short lx-btn--primary"
|
||||
type="button"
|
||||
@click="goNextQuestion"
|
||||
>
|
||||
下一题
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -370,28 +377,6 @@ function handleBack() {
|
||||
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;
|
||||
|
||||
256
src/views/EnglishTools/WordBook.vue
Normal file
256
src/views/EnglishTools/WordBook.vue
Normal file
@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<div class="word-book-page">
|
||||
<SubPageHeader>
|
||||
<template #center>
|
||||
<div class="header-tabs">
|
||||
<button
|
||||
v-for="tab in pageTabs"
|
||||
:key="tab.key"
|
||||
class="header-tab"
|
||||
:class="{ active: activeMode === tab.key }"
|
||||
type="button"
|
||||
@click="activeMode = tab.key"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</SubPageHeader>
|
||||
|
||||
<main class="word-book-body">
|
||||
<section class="word-book-panel">
|
||||
<WordBookFilterBar
|
||||
v-model:grade="activeGrade"
|
||||
v-model:status="activeStatus"
|
||||
v-model:edition="activeEdition"
|
||||
v-model:book="activeBook"
|
||||
v-model:unit="activeUnit"
|
||||
:mode="activeMode"
|
||||
:count="filteredCount"
|
||||
:grade-options="gradeOptions"
|
||||
:status-options="statusOptions"
|
||||
:edition-options="editionOptions"
|
||||
:book-options="bookOptions"
|
||||
:unit-options="unitOptions"
|
||||
/>
|
||||
|
||||
<div class="word-book-content">
|
||||
<LoadingState
|
||||
v-if="displayWords.length === 0"
|
||||
status="empty"
|
||||
empty-text="暂无内容"
|
||||
/>
|
||||
|
||||
<div v-else class="word-grid">
|
||||
<WordCard
|
||||
v-for="word in displayWords"
|
||||
:key="word.id"
|
||||
:word="word"
|
||||
@click="openWordDetail"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="displayWords.length > 0"
|
||||
class="start-btn lx-btn lx-btn--primary lx-btn--short"
|
||||
type="button"
|
||||
>
|
||||
开始练习
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<WordDetailDialog v-model="showDetail" :word="currentWord" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import SubPageHeader from "@/components/layout/SubPageHeader.vue";
|
||||
import LoadingState from "@/components/loading/LoadingState.vue";
|
||||
import WordBookFilterBar from "./components/WordBookFilterBar.vue";
|
||||
import WordCard, { type WordCardItem } from "./components/WordCard.vue";
|
||||
import WordDetailDialog from "./components/WordDetailDialog.vue";
|
||||
|
||||
type PageMode = "stage" | "textbook";
|
||||
|
||||
const pageTabs = [
|
||||
{ key: "stage" as const, label: "阶段" },
|
||||
{ key: "textbook" as const, label: "教材" },
|
||||
];
|
||||
|
||||
const gradeOptions = [
|
||||
{ key: "all", label: "全部" },
|
||||
{ key: "primary", label: "小学" },
|
||||
{ key: "junior", label: "初中" },
|
||||
{ key: "senior", label: "高中" },
|
||||
{ key: "cet4", label: "四级" },
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{ key: "all", label: "全部" },
|
||||
{ key: "unlocked", label: "未解锁" },
|
||||
{ key: "new", label: "陌生" },
|
||||
{ key: "known", label: "认识" },
|
||||
{ key: "mastered", label: "掌握" },
|
||||
];
|
||||
|
||||
const editionOptions = [
|
||||
{ label: "译林版", value: "yilin" },
|
||||
{ label: "人教版", value: "renjiao" },
|
||||
{ label: "外研版", value: "waiyan" },
|
||||
];
|
||||
|
||||
const bookOptions = [
|
||||
{ label: "必修第一册", value: "book-1" },
|
||||
{ label: "必修第二册", value: "book-2" },
|
||||
{ label: "选择性必修", value: "book-3" },
|
||||
];
|
||||
|
||||
const unitOptions = [
|
||||
{ label: "Unit 1", value: "unit-1" },
|
||||
{ label: "Unit 2", value: "unit-2" },
|
||||
{ label: "Unit 3", value: "unit-3" },
|
||||
];
|
||||
|
||||
const tones: WordCardItem["tone"][] = ["pink", "cream", "yellow", "green", "gray"];
|
||||
|
||||
const words: WordCardItem[] = Array.from({ length: 700 }, (_, index) => ({
|
||||
id: index + 1,
|
||||
word: "no",
|
||||
meaning: "不;没有;不是",
|
||||
partOfSpeech: "int.",
|
||||
tone: tones[index % tones.length],
|
||||
muted: index % tones.length === 4,
|
||||
grade: index % 4 === 0 ? "primary" : index % 4 === 1 ? "junior" : index % 4 === 2 ? "senior" : "cet4",
|
||||
status: statusOptions[(index % (statusOptions.length - 1)) + 1].key,
|
||||
}));
|
||||
|
||||
const activeMode = ref<PageMode>("stage");
|
||||
const activeGrade = ref("all");
|
||||
const activeStatus = ref("all");
|
||||
const activeEdition = ref("yilin");
|
||||
const activeBook = ref("book-1");
|
||||
const activeUnit = ref("unit-1");
|
||||
const showDetail = ref(false);
|
||||
const currentWord = ref<WordCardItem | null>(null);
|
||||
|
||||
const filteredWords = computed(() =>
|
||||
words.filter((word) => {
|
||||
if (activeGrade.value !== "all" && word.grade !== activeGrade.value) return false;
|
||||
if (activeStatus.value !== "all" && word.status !== activeStatus.value) return false;
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
|
||||
const filteredCount = computed(() => {
|
||||
if (activeGrade.value === "all" && activeStatus.value === "all") return words.length;
|
||||
return filteredWords.value.length;
|
||||
});
|
||||
|
||||
const displayWords = computed(() => filteredWords.value.slice(0, 80));
|
||||
|
||||
function openWordDetail(word: WordCardItem) {
|
||||
currentWord.value = word;
|
||||
showDetail.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.word-book-page {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 70px;
|
||||
}
|
||||
|
||||
.header-tab {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #121212;
|
||||
cursor: pointer;
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
padding: 8px 0 16px;
|
||||
|
||||
&.active {
|
||||
color: #2d6fe0;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 32px;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: #3d78f2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.word-book-body {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding: 0 64px 32px;
|
||||
}
|
||||
|
||||
.word-book-panel {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 1792px;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
border: 2px solid rgba(255, 255, 255, 0.72);
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.word-book-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
border-radius: 16px 16px 0 0;
|
||||
background: #fff;
|
||||
padding: 24px 32px 108px;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.word-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
position: fixed;
|
||||
bottom: 64px;
|
||||
align-self: center;
|
||||
margin-top: 20px;
|
||||
padding: 12px 64px;
|
||||
font-size: 28px;
|
||||
height: 72px;
|
||||
}
|
||||
</style>
|
||||
96
src/views/EnglishTools/components/WordBookFilterBar.vue
Normal file
96
src/views/EnglishTools/components/WordBookFilterBar.vue
Normal file
@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="word-book-filter">
|
||||
<div class="filter-left">
|
||||
<LmFilterTabs v-model="gradeValue" :options="gradeOptions" mode="compact" />
|
||||
<LmFilterTabs v-model="statusValue" :options="statusOptions" mode="compact" />
|
||||
|
||||
<template v-if="mode === 'textbook'">
|
||||
<LmSelect v-model="editionValue" :options="editionOptions" width="150px" />
|
||||
<LmSelect v-model="bookValue" :options="bookOptions" width="180px" />
|
||||
<LmSelect v-model="unitValue" :options="unitOptions" width="130px" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="filter-count">数量:{{ count }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import LmFilterTabs, { type FilterTabOption } from "@/components/lm/LmFilterTabs.vue";
|
||||
import LmSelect, { type LmSelectOption } from "@/components/lm/LmSelect.vue";
|
||||
|
||||
type Mode = "stage" | "textbook";
|
||||
|
||||
const props = defineProps<{
|
||||
mode: Mode;
|
||||
grade: string;
|
||||
status: string;
|
||||
edition: string | number;
|
||||
book: string | number;
|
||||
unit: string | number;
|
||||
count: number;
|
||||
gradeOptions: FilterTabOption[];
|
||||
statusOptions: FilterTabOption[];
|
||||
editionOptions: LmSelectOption[];
|
||||
bookOptions: LmSelectOption[];
|
||||
unitOptions: LmSelectOption[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:grade": [value: string];
|
||||
"update:status": [value: string];
|
||||
"update:edition": [value: string | number];
|
||||
"update:book": [value: string | number];
|
||||
"update:unit": [value: string | number];
|
||||
}>();
|
||||
|
||||
const gradeValue = computed({
|
||||
get: () => props.grade,
|
||||
set: (value: string) => emit("update:grade", value),
|
||||
});
|
||||
|
||||
const statusValue = computed({
|
||||
get: () => props.status,
|
||||
set: (value: string) => emit("update:status", value),
|
||||
});
|
||||
|
||||
const editionValue = computed({
|
||||
get: () => props.edition,
|
||||
set: (value: string | number) => emit("update:edition", value),
|
||||
});
|
||||
|
||||
const bookValue = computed({
|
||||
get: () => props.book,
|
||||
set: (value: string | number) => emit("update:book", value),
|
||||
});
|
||||
|
||||
const unitValue = computed({
|
||||
get: () => props.unit,
|
||||
set: (value: string | number) => emit("update:unit", value),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.word-book-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
min-height: 92px;
|
||||
padding: 16px 28px;
|
||||
}
|
||||
|
||||
.filter-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 28px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-count {
|
||||
color: #333;
|
||||
font-size: 22px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
109
src/views/EnglishTools/components/WordCard.vue
Normal file
109
src/views/EnglishTools/components/WordCard.vue
Normal file
@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<button
|
||||
class="word-card"
|
||||
:class="[`tone-${word.tone}`, { 'is-muted': word.muted }]"
|
||||
type="button"
|
||||
@click="$emit('click', word)"
|
||||
>
|
||||
<strong class="word-text">{{ word.word }}</strong>
|
||||
<span class="word-meaning">{{ word.meaning }}</span>
|
||||
<span class="word-pos">{{ word.partOfSpeech }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
export interface WordCardItem {
|
||||
id: number;
|
||||
word: string;
|
||||
meaning: string;
|
||||
partOfSpeech: string;
|
||||
tone: "pink" | "cream" | "yellow" | "green" | "gray";
|
||||
muted?: boolean;
|
||||
grade: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
word: WordCardItem;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
click: [word: WordCardItem];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.word-card {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
height: 248px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(212, 222, 238, 0.8);
|
||||
border-radius: 16px;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 12px rgba(54, 84, 132, 0.06);
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
transition:
|
||||
transform 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(54, 84, 132, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.tone-pink {
|
||||
background: #fff5f6;
|
||||
}
|
||||
|
||||
.tone-cream {
|
||||
background: #fff9f4;
|
||||
}
|
||||
|
||||
.tone-yellow {
|
||||
background: #fffdf0;
|
||||
}
|
||||
|
||||
.tone-green {
|
||||
background: #effff7;
|
||||
}
|
||||
|
||||
.tone-gray {
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
.is-muted {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.word-text {
|
||||
color: #121212;
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.word-meaning {
|
||||
margin-top: 32px;
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.word-pos {
|
||||
margin-top: 20px;
|
||||
color: #333;
|
||||
font-size: 26px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.is-muted .word-text,
|
||||
.is-muted .word-meaning,
|
||||
.is-muted .word-pos {
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
305
src/views/EnglishTools/components/WordDetailDialog.vue
Normal file
305
src/views/EnglishTools/components/WordDetailDialog.vue
Normal file
@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
class="word-detail-dialog"
|
||||
width="1000px"
|
||||
:show-close="false"
|
||||
:close-on-click-modal="true"
|
||||
modal-class="word-detail-overlay"
|
||||
align-center
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<div v-if="word" class="detail-panel">
|
||||
<div class="detail-header">
|
||||
<div class="word-main">
|
||||
<h2 class="detail-word">{{ word.word }}</h2>
|
||||
<button class="sound-row" type="button" aria-label="播放发音">
|
||||
<svg class="sound-icon" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M4 9v6h4l5 4V5L8 9H4Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" />
|
||||
<path d="M16 9c1.1 1.6 1.1 4.4 0 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||||
</svg>
|
||||
<span>/ˈklevə(r)/</span>
|
||||
</button>
|
||||
<p class="definition">int. (用于否定的回答或陈述)不;没有;不是</p>
|
||||
</div>
|
||||
|
||||
<button class="book-link" type="button">
|
||||
<svg viewBox="0 0 28 28" fill="none">
|
||||
<path d="M7 4h11a3 3 0 0 1 3 3v17H9a2 2 0 0 1-2-2V4Z" stroke="currentColor" stroke-width="2" />
|
||||
<path d="M7 20h12" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
<path d="M13 9h5M13 14h5" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
<span>生词本</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-row">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="detail-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
type="button"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div v-if="activeTab === 'example'" class="example-block">
|
||||
<button class="sentence-audio" type="button" aria-label="播放例句">
|
||||
<svg class="sound-icon" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M4 9v6h4l5 4V5L8 9H4Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" />
|
||||
<path d="M16 9c1.1 1.6 1.1 4.4 0 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||||
</svg>
|
||||
<span>Just say yes or no.</span>
|
||||
</button>
|
||||
<p class="translation">只要说“是”或“不是”。</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTab === 'mnemonic'" class="mnemonic-block">
|
||||
<h3>形象联想</h3>
|
||||
<p>
|
||||
将字母 “n” 想象成一个摇头的动作,“o” 想象成张大的嘴巴,合起来就像在说“不”时摇头和张嘴的样子。
|
||||
</p>
|
||||
<h3>谐音记忆</h3>
|
||||
<p>
|
||||
“no” 的发音类似于中文的“闹”,可以联想成“不要再闹了”来表示拒绝。短小有力的发音,也适合在回答问题时快速表达否定。
|
||||
</p>
|
||||
<h3>用法提示</h3>
|
||||
<p>作为否定回答时可单独使用,也可以放在名词前表达“没有”。</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="level-block">
|
||||
<h3>1阶-第2关</h3>
|
||||
<div class="record-grid">
|
||||
<div class="record-card">
|
||||
<span>选择题</span>
|
||||
<p>正确 <strong class="success">5</strong> 次</p>
|
||||
<p>错误 <strong class="danger">5</strong> 次</p>
|
||||
</div>
|
||||
<div class="record-card">
|
||||
<span>拼写题</span>
|
||||
<p>正确 <strong class="success">50</strong> 次</p>
|
||||
<p>错误 <strong class="danger">999</strong> 次</p>
|
||||
</div>
|
||||
<div class="record-card">
|
||||
<span>复习</span>
|
||||
<p>成功 <strong class="success">555</strong> 次</p>
|
||||
<p>失败 <strong class="danger">5</strong> 次</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import type { WordCardItem } from "./WordCard.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
word: WordCardItem | null;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
"update:modelValue": [value: boolean];
|
||||
}>();
|
||||
|
||||
const tabs = [
|
||||
{ key: "example", label: "例句" },
|
||||
{ key: "mnemonic", label: "助记" },
|
||||
{ key: "level", label: "关卡" },
|
||||
] as const;
|
||||
|
||||
type DetailTab = (typeof tabs)[number]["key"];
|
||||
|
||||
const activeTab = ref<DetailTab>("example");
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(visible: boolean) => {
|
||||
if (visible) activeTab.value = "example";
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:global(.word-detail-dialog) {
|
||||
--el-dialog-padding-primary: 0;
|
||||
border-radius: 24px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:global(.word-detail-dialog .el-dialog__header) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:global(.word-detail-dialog .el-dialog__body) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.word-detail-overlay) {
|
||||
background-color: rgba(0, 0, 0, 0.58);
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
width: 1000px;
|
||||
height: 680px;
|
||||
padding: 64px 56px 58px;
|
||||
overflow-y: auto;
|
||||
border-radius: 24px;
|
||||
background: #fff;
|
||||
scrollbar-color: #d6dced transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 32px;
|
||||
padding-bottom: 34px;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
.detail-word {
|
||||
color: #121212;
|
||||
font-size: 38px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sound-row,
|
||||
.sentence-audio,
|
||||
.book-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.sound-row {
|
||||
gap: 10px;
|
||||
margin-top: 28px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.sound-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.definition {
|
||||
margin-top: 28px;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.book-link {
|
||||
gap: 8px;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
white-space: nowrap;
|
||||
|
||||
svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 22px;
|
||||
margin-top: 34px;
|
||||
}
|
||||
|
||||
.detail-tab {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
font-size: 24px;
|
||||
padding: 8px 18px;
|
||||
|
||||
&.active {
|
||||
background: #eaf4ff;
|
||||
color: #2d6fe0;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
margin-top: 32px;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.sentence-audio {
|
||||
gap: 10px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.translation {
|
||||
margin-top: 12px;
|
||||
padding-left: 34px;
|
||||
color: #555;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.mnemonic-block {
|
||||
line-height: 1.7;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 16px;
|
||||
color: #121212;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p + h3 {
|
||||
margin-top: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.level-block h3 {
|
||||
margin-bottom: 22px;
|
||||
color: #121212;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.record-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.record-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
min-height: 70px;
|
||||
border: 1px solid #edf0f5;
|
||||
border-radius: 10px;
|
||||
padding: 0 22px;
|
||||
color: #333;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #12b886;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: #e84040;
|
||||
font-size: 28px;
|
||||
}
|
||||
</style>
|
||||
@ -20,6 +20,7 @@
|
||||
v-for="tool in pageTools(pageIndex - 1)"
|
||||
:key="tool + pageIndex"
|
||||
class="tool-card"
|
||||
@click="handleToolClick(tool)"
|
||||
>
|
||||
<h3 class="tool-title">{{ tool }}</h3>
|
||||
</article>
|
||||
@ -44,6 +45,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import PageFrame from "@/components/layout/PageFrame.vue";
|
||||
|
||||
const tools = [
|
||||
@ -60,6 +62,7 @@ const tools = [
|
||||
|
||||
const pageSize = 9;
|
||||
const currentPage = ref(0);
|
||||
const router = useRouter();
|
||||
const totalPages = computed(() => Math.ceil(tools.length / pageSize));
|
||||
|
||||
const pageTools = (pageIndex: number) =>
|
||||
@ -69,6 +72,11 @@ const trackStyle = computed(() => ({
|
||||
transform: `translateX(-${currentPage.value * 100}%)`,
|
||||
}));
|
||||
|
||||
function handleToolClick(tool: string) {
|
||||
if (tool !== "单词书") return;
|
||||
router.push({ name: "english-word-book" });
|
||||
}
|
||||
|
||||
// ---- 触摸滑动 ----
|
||||
let touchStartX = 0;
|
||||
|
||||
|
||||
@ -11,7 +11,9 @@ function goHome() {
|
||||
<template>
|
||||
<div class="login px-w">
|
||||
<p>登录页占位</p>
|
||||
<el-button type="primary" @click="goHome">返回首页</el-button>
|
||||
<button class="lx-btn lx-btn--md lx-btn--primary" type="button" @click="goHome">
|
||||
返回首页
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@ -48,7 +48,12 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="edit-btn" :class="{ saveBtn: isEditing }" type="button" @click="toggleEditing">
|
||||
<button
|
||||
class="edit-btn lx-btn lx-btn--md"
|
||||
:class="isEditing ? 'lx-btn--short saveBtn' : 'lx-btn--primary'"
|
||||
type="button"
|
||||
@click="toggleEditing"
|
||||
>
|
||||
<img v-if="!isEditing" class="edit-icon" :src="editIcon" alt="" />
|
||||
{{ isEditing ? '保存' : '修改资料' }}
|
||||
</button>
|
||||
@ -379,30 +384,21 @@ const toggleEditing = async () => {
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
min-width: 172px;
|
||||
// height: 38px;
|
||||
margin-top: auto;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: #2d76ee;
|
||||
// background: ;
|
||||
color:#fff;
|
||||
cursor: pointer;
|
||||
font-size: 24px;
|
||||
padding: 12px 24px;
|
||||
border: 1px solid #fff;
|
||||
gap: 5.5px;
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.saveBtn {
|
||||
background: #fff;
|
||||
color: #2d76ee;
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
margin-right: 5.5px;
|
||||
object-fit: contain;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
|
||||
.profile-main {
|
||||
|
||||
@ -43,7 +43,13 @@
|
||||
<img class="btn-icon" :src="cuotibenIcon" alt="错题本" />
|
||||
错题本
|
||||
</button>
|
||||
<button class="add-book-btn" @click="showDrawer = true">+ 添加书本</button>
|
||||
<button
|
||||
class="lx-btn lx-btn--md lx-btn--primary"
|
||||
type="button"
|
||||
@click="showDrawer = true"
|
||||
>
|
||||
+ 添加书本
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -362,22 +368,6 @@ function goToSubjectAnalysis() {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.add-book-btn {
|
||||
background: #3d78f2;
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 26px;
|
||||
// font-weight: 600;
|
||||
padding: 14px 32px;
|
||||
border-radius: 99px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #2d64d8;
|
||||
}
|
||||
}
|
||||
|
||||
/* 卡片内容 */
|
||||
.card-content {
|
||||
display: flex;
|
||||
|
||||
@ -94,7 +94,13 @@
|
||||
</div>
|
||||
|
||||
<div class="record-footer">
|
||||
<button class="start-btn" @click="handleStartPractice">开始练习</button>
|
||||
<button
|
||||
class="lx-btn lx-btn--lg lx-btn--primary"
|
||||
type="button"
|
||||
@click="handleStartPractice"
|
||||
>
|
||||
开始练习
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -497,23 +503,4 @@ function handleStartPractice() {
|
||||
// border-top: 1.5px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
padding: 18px 80px;
|
||||
border-radius: 99px;
|
||||
border: none;
|
||||
background: #3d78f2;
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #2d64d8;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -62,10 +62,18 @@
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-row">
|
||||
<button class="action-btn detail-btn" @click="viewDetail(q)">
|
||||
<button
|
||||
class="action-btn lx-btn lx-btn--sm lx-btn--outline-primary"
|
||||
type="button"
|
||||
@click="viewDetail(q)"
|
||||
>
|
||||
查看详情
|
||||
</button>
|
||||
<button class="action-btn extend-btn" @click="extendPractice(q)">
|
||||
<button
|
||||
class="action-btn lx-btn lx-btn--sm lx-btn--outline-primary"
|
||||
type="button"
|
||||
@click="extendPractice(q)"
|
||||
>
|
||||
举一反三
|
||||
</button>
|
||||
</div>
|
||||
@ -112,10 +120,18 @@
|
||||
</div>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button class="dialog-btn cancel-btn" type="button" @click="showRetrainDialog = false">
|
||||
<button
|
||||
class="dialog-btn lx-btn lx-btn--xl lx-btn--short lx-btn--default"
|
||||
type="button"
|
||||
@click="showRetrainDialog = false"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button class="dialog-btn confirm-btn" type="button" @click="confirmRetrain">
|
||||
<button
|
||||
class="dialog-btn lx-btn lx-btn--xl lx-btn--short lx-btn--primary"
|
||||
type="button"
|
||||
@click="confirmRetrain"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
</div>
|
||||
@ -628,28 +644,6 @@ function extendPractice(q: Question) {
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 12px 0;
|
||||
border-radius: 99px;
|
||||
text-align: center;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
background: #fff;
|
||||
border: 1.5px solid #2d6fe0;
|
||||
color: #2d6fe0;
|
||||
}
|
||||
|
||||
.extend-btn {
|
||||
background: #fff;
|
||||
border: 1.5px solid #2d6fe0;
|
||||
color: #2d6fe0;
|
||||
}
|
||||
|
||||
:global(.retrain-dialog) {
|
||||
@ -759,29 +753,4 @@ function extendPractice(q: Question) {
|
||||
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>
|
||||
|
||||
@ -10,9 +10,21 @@
|
||||
<div class="drawer-inner">
|
||||
<!-- ===== 顶部操作栏 ===== -->
|
||||
<div class="drawer-header">
|
||||
<button class="cancel-btn" @click="handleCancel">取消</button>
|
||||
<button
|
||||
class="lx-btn lx-btn--sm lx-btn--short lx-btn--default"
|
||||
type="button"
|
||||
@click="handleCancel"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<span class="drawer-title">选择教材</span>
|
||||
<button class="confirm-btn" @click="handleConfirm">确认</button>
|
||||
<button
|
||||
class="lx-btn lx-btn--sm lx-btn--short lx-btn--primary"
|
||||
type="button"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ===== 筛选区域 ===== -->
|
||||
@ -200,37 +212,6 @@ function handleCancel() {
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
font-size: 26px;
|
||||
color: #555;
|
||||
background: transparent;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 99px;
|
||||
padding: 12px 36px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: #999;
|
||||
color: #222;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
font-size: 26px;
|
||||
color: #fff;
|
||||
background: #3d78f2;
|
||||
border: none;
|
||||
border-radius: 99px;
|
||||
padding: 12px 36px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #2d64d8;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 筛选区域 ---- */
|
||||
.filter-section {
|
||||
flex-shrink: 0;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user