Files
lexgo/learner/src/components/ReviewCard.vue
T
ila 0328505b77 feat: 完成到期单词复习与幂等答题 (#8)
- schema v6 新增 lexgo_term_reviews 与 lexgo_review_answers:语句都是可重试的加法迁移,
  既有已保存词汇按 created_at 立即进入队列;旧二进制回到 v5 仍可写入个人词条
- 固定间隔表 1/2/4/7/15/30/60 天:答对升级封顶 7、答错降级最低 1、再学一次不改等级,
  答错与再学立即回到本轮;已知与忽略不入队
- GET /api/v1/reviews/queue 与 POST /api/v1/reviews/:termId/answers:按 answerId 去重、
  按 expectedDueAt 判定过期标签页,重复提交与并发都不重复更新次数和间隔
- 学习端新增 /review 路由与到期复习入口,正面挖空例句、答案面评分、完成页与空队列页
- 同步 Architecture-and-Code-Map、Business-Rules-and-Glossary、
  Local-Development-and-Verification、Product-Requirements-Overview 与 Home
2026-09-11 20:54:17 +08:00

50 lines
2.8 KiB
Vue

<script setup lang="ts">
import { nextTick, ref, watch } from 'vue'
import { ElButton } from 'element-plus'
import { clozeSentence, type ReviewItem } from '../stores/review'
const props = defineProps<{ item: ReviewItem; position: number; total: number; revealed: boolean; busy: boolean; error: string }>()
const emit = defineEmits<{ reveal: []; grade: [grade: 'correct' | 'wrong' | 'again']; end: []; retry: [] }>()
// The word and its masked example are enough to answer; the definition stays hidden.
const prompt = () => clozeSentence(props.item)
const revealButton = ref<{ $el?: HTMLElement } | null>(null)
const correctButton = ref<{ $el?: HTMLElement } | null>(null)
// Keyboard users should reach the next action without tabbing through the page again.
watch(() => props.item.id, async () => {
await nextTick()
if (!props.revealed) revealButton.value?.$el?.focus()
}, { immediate: true })
watch(() => props.revealed, async value => {
if (!value) return
await nextTick()
correctButton.value?.$el?.focus()
})
</script>
<template>
<section class="review-card" aria-labelledby="review-word" data-testid="review-card">
<p class="subtle" data-testid="review-position">到期复习 · {{ position }} / {{ total }}</p>
<h2 id="review-word" tabindex="-1" class="review-word">{{ item.originalForm }}</h2>
<p v-if="prompt()" class="review-example" lang="en">{{ prompt() }}</p>
<p class="subtle">{{ item.status === 'learning' ? `学习中 · 等级 ${item.level}` : '新词 · 尚未复习' }}</p>
<p v-if="error" role="alert" class="notice">{{ error }}</p>
<template v-if="!revealed">
<ElButton ref="revealButton" type="primary" data-testid="review-reveal" :disabled="busy || !!error" @click="emit('reveal')">显示答案</ElButton>
</template>
<template v-else>
<div class="review-answer">
<p class="review-definition" data-testid="review-definition">{{ item.definition || '(未填写个人释义)' }}</p>
<blockquote v-for="(example, index) in item.examples" :key="index" lang="en">{{ example }}</blockquote>
</div>
<div class="review-grades">
<ElButton ref="correctButton" type="primary" data-testid="review-correct" :loading="busy" @click="emit('grade', 'correct')">认识 / 答对</ElButton>
<ElButton data-testid="review-wrong" :disabled="busy" @click="emit('grade', 'wrong')">不认识 / 答错</ElButton>
<ElButton data-testid="review-again" :disabled="busy" @click="emit('grade', 'again')">再学一次</ElButton>
</div>
</template>
<p v-if="error" class="review-actions"><ElButton data-testid="review-retry" @click="emit('retry')">重试提交</ElButton></p>
<p class="review-actions"><ElButton text data-testid="review-end" @click="emit('end')">结束本次复习</ElButton></p>
</section>
</template>