- 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
77 lines
3.4 KiB
Vue
77 lines
3.4 KiB
Vue
<script setup lang="ts">
|
||
import { onMounted } from 'vue'
|
||
import { RouterLink, useRouter } from 'vue-router'
|
||
import { ElButton } from 'element-plus'
|
||
import ReviewCard from '../components/ReviewCard.vue'
|
||
import { useReviewStore, type ReviewItem } from '../stores/review'
|
||
import { useSessionStore } from '../stores/session'
|
||
|
||
const session = useSessionStore()
|
||
const review = useReviewStore()
|
||
const router = useRouter()
|
||
|
||
function endReview() { void router.push('/') }
|
||
// Retrying reuses the grade of the failed attempt; the store keeps the answer id.
|
||
let lastGrade: 'correct' | 'wrong' | 'again' = 'correct'
|
||
function grade(value: 'correct' | 'wrong' | 'again') { lastGrade = value; void review.answer(value) }
|
||
function retry() { void review.answer(lastGrade) }
|
||
|
||
onMounted(() => { void review.load() })
|
||
async function logout() {
|
||
try { await session.logout() }
|
||
catch { session.notice = '已退出此设备。服务器暂时无法连接,请稍后重试。' }
|
||
finally { await router.replace('/login') }
|
||
}
|
||
// The template guards for a card before rendering it; this keeps the prop type strict.
|
||
const requireItem = (value: ReviewItem | null): ReviewItem => value as ReviewItem
|
||
</script>
|
||
|
||
<template>
|
||
<div v-if="session.user">
|
||
<header class="site-header">
|
||
<RouterLink to="/" class="brand">LexGo<span class="brand-dot">.</span></RouterLink>
|
||
<nav aria-label="学习导航"><RouterLink to="/">我的书库</RouterLink></nav>
|
||
<div class="account">
|
||
<span class="account-name">{{ session.user.username }}</span>
|
||
<ElButton text @click="logout">退出登录</ElButton>
|
||
</div>
|
||
</header>
|
||
<main class="page review-page">
|
||
<h1>到期复习</h1>
|
||
<p v-if="review.loading" role="status" class="loading">正在加载…</p>
|
||
<div v-else-if="review.error && !review.current" class="notice">
|
||
<p role="alert">{{ review.error }}</p>
|
||
<ElButton data-testid="review-reload" @click="review.load()">重试</ElButton>
|
||
</div>
|
||
<section v-else-if="review.finished" class="review-summary" data-testid="review-summary">
|
||
<h2>本次复习完成</h2>
|
||
<p>复习了 {{ review.wordsReviewed }} 个词条 · 共 {{ review.answered }} 次作答</p>
|
||
<p class="subtle">答对 {{ review.correctCount }} · 答错或再学 {{ review.wrongCount }} · 已更新复习计划</p>
|
||
<p v-if="review.pending" role="status" class="notice">还有 {{ review.pending }} 个词条到期。</p>
|
||
<div class="review-grades">
|
||
<ElButton v-if="review.pending" type="primary" data-testid="review-more" @click="review.continueRound()">继续复习</ElButton>
|
||
<ElButton data-testid="review-finish" @click="endReview">继续阅读</ElButton>
|
||
</div>
|
||
</section>
|
||
<section v-else-if="review.empty" class="review-summary" data-testid="review-empty">
|
||
<h2>今天没有到期词条</h2>
|
||
<p class="subtle">读一篇文章,积累下一次的词汇。</p>
|
||
<ElButton data-testid="review-finish" @click="endReview">返回书库</ElButton>
|
||
</section>
|
||
<ReviewCard
|
||
v-else-if="review.current"
|
||
:item="requireItem(review.current)"
|
||
:position="review.answered + 1"
|
||
:total="review.answered + review.queue.length"
|
||
:revealed="review.revealed"
|
||
:busy="review.busy"
|
||
:error="review.error"
|
||
@reveal="review.reveal()"
|
||
@grade="grade"
|
||
@end="endReview"
|
||
@retry="retry"
|
||
/>
|
||
</main>
|
||
</div>
|
||
</template>
|