Initial commit: WordLoop 单词学习应用

Vue 前端 + FastAPI 后端,含部署脚本与词典数据。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
John
2026-06-04 14:30:53 -07:00
commit bd7635986a
66 changed files with 5495 additions and 0 deletions
+107
View File
@@ -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>
+72
View File
@@ -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>