Initial commit: WordLoop 单词学习应用
Vue 前端 + FastAPI 后端,含部署脚本与词典数据。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import type { QuizQuestion } from '../api/request'
|
||||
|
||||
defineProps<{
|
||||
question: QuizQuestion
|
||||
index: number
|
||||
total: number
|
||||
selected?: string
|
||||
showResult?: boolean
|
||||
isCorrect?: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
select: [answer: string]
|
||||
}>()
|
||||
|
||||
const typeLabel: Record<string, string> = {
|
||||
en_to_zh: '看英文选中文',
|
||||
zh_to_en: '看中文选英文',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card quiz-card">
|
||||
<div class="quiz-progress">{{ index + 1 }} / {{ total }}</div>
|
||||
<div class="quiz-type">{{ typeLabel[question.question_type] || question.question_type }}</div>
|
||||
<div class="quiz-prompt">{{ question.prompt }}</div>
|
||||
<div class="options">
|
||||
<button
|
||||
v-for="opt in question.options"
|
||||
:key="opt.label"
|
||||
class="option-btn"
|
||||
:class="{
|
||||
selected: selected === opt.text,
|
||||
correct: showResult && opt.text === question.correct_answer,
|
||||
wrong: showResult && selected === opt.text && opt.text !== question.correct_answer,
|
||||
}"
|
||||
:disabled="showResult"
|
||||
@click="$emit('select', opt.text)"
|
||||
>
|
||||
<span class="opt-label">{{ opt.label }}.</span> {{ opt.text }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="showResult" class="result" :class="isCorrect ? 'ok' : 'fail'">
|
||||
{{ isCorrect ? '回答正确 ✅' : '回答错误 ❌' }}
|
||||
<template v-if="!isCorrect"> — 正确答案:{{ question.correct_answer }}</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.quiz-progress {
|
||||
font-size: 13px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.quiz-type {
|
||||
font-size: 12px;
|
||||
color: var(--primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.quiz-prompt {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.option-btn {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
font-size: 15px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.option-btn.selected {
|
||||
border-color: var(--primary);
|
||||
background: rgba(79, 110, 247, 0.08);
|
||||
}
|
||||
.option-btn.correct {
|
||||
border-color: var(--success);
|
||||
background: #d1fae5;
|
||||
}
|
||||
.option-btn.wrong {
|
||||
border-color: var(--danger);
|
||||
background: #fee2e2;
|
||||
}
|
||||
.opt-label {
|
||||
font-weight: 700;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.result {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
.result.ok { background: #d1fae5; color: #047857; }
|
||||
.result.fail { background: #fee2e2; color: #b91c1c; }
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import type { Word } from '../api/request'
|
||||
|
||||
defineProps<{
|
||||
word: Word
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
delete: [id: number]
|
||||
}>()
|
||||
|
||||
const statusMap: Record<string, string> = {
|
||||
new: '新词',
|
||||
learning: '学习中',
|
||||
mastered: '已掌握',
|
||||
weak: '易错词',
|
||||
}
|
||||
|
||||
function enText(word: Word) {
|
||||
return word.source_lang === 'en' ? word.source_text : word.target_text
|
||||
}
|
||||
|
||||
function zhText(word: Word) {
|
||||
return word.source_lang === 'zh' ? word.source_text : word.target_text
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card word-card">
|
||||
<div class="word-header">
|
||||
<div>
|
||||
<div class="word-pair">{{ enText(word) }} — {{ zhText(word) }}</div>
|
||||
<span :class="['badge', `badge-${word.status}`]">{{ statusMap[word.status] || word.status }}</span>
|
||||
</div>
|
||||
<button class="btn btn-danger" @click="$emit('delete', word.id)">删除</button>
|
||||
</div>
|
||||
<div class="word-meta">
|
||||
<span>答对 {{ word.correct_count }}</span>
|
||||
<span>答错 {{ word.wrong_count }}</span>
|
||||
<span>连续 {{ word.consecutive_correct_count }}</span>
|
||||
<span>掌握率 {{ word.mastery_score }}%</span>
|
||||
</div>
|
||||
<div v-if="word.review_due_date" class="word-due">下次复习:{{ word.review_due_date }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.word-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
.word-pair {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.word-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-top: 10px;
|
||||
}
|
||||
.word-due {
|
||||
font-size: 12px;
|
||||
color: var(--primary);
|
||||
margin-top: 6px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user