From f2fabed2eef4428524a0c7eaf7c78f309d02c32e Mon Sep 17 00:00:00 2001 From: chen guanjun Date: Mon, 22 Jun 2026 10:24:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=8D=E4=B9=A0=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=A7=91=E7=9B=AE=E7=AD=9B=E9=80=89=E3=80=81?= =?UTF-8?q?=E9=A2=98=E7=9B=AE=E5=88=97=E8=A1=A8=E9=80=89=E6=8B=A9=E3=80=81?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E9=A2=98=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/review/page.tsx | 345 +++++++++++++++++-------------- 1 file changed, 187 insertions(+), 158 deletions(-) diff --git a/frontend/src/app/review/page.tsx b/frontend/src/app/review/page.tsx index e00009d..353f7c5 100644 --- a/frontend/src/app/review/page.tsx +++ b/frontend/src/app/review/page.tsx @@ -1,13 +1,13 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import { useRouter } from "next/navigation"; import toast from "react-hot-toast"; import api from "@/lib/api"; import { useAuthStore } from "@/lib/store"; import { useAuth } from "@/hooks/useAuth"; import type { ReviewCardResponse, ReviewTodayResponse } from "@/types"; -import { REVIEW_QUALITY_LABELS } from "@/types"; +import { REVIEW_QUALITY_LABELS, SUBJECTS, SUBJECT_COLORS } from "@/types"; export default function ReviewPage() { const router = useRouter(); @@ -20,12 +20,29 @@ export default function ReviewPage() { const [submitting, setSubmitting] = useState(false); const [completed, setCompleted] = useState(false); const [analyzing, setAnalyzing] = useState(false); + const [subjectFilter, setSubjectFilter] = useState(""); + const [view, setView] = useState<"list" | "review">("list"); useEffect(() => { if (!ready) return; loadToday(); }, [ready]); + const filteredCards = useMemo(() => { + if (!data) return []; + if (!subjectFilter) return data.cards; + return data.cards.filter((c) => c.mistake.subject === subjectFilter); + }, [data, subjectFilter]); + + const subjectCounts = useMemo(() => { + if (!data) return {}; + const counts: Record = {}; + data.cards.forEach((c) => { + counts[c.mistake.subject] = (counts[c.mistake.subject] || 0) + 1; + }); + return counts; + }, [data]); + const loadToday = async () => { try { const res = await api.get("/review/today"); @@ -40,9 +57,15 @@ export default function ReviewPage() { } }; + const startReview = (index: number) => { + setCurrentIndex(index); + setIsFlipped(false); + setView("review"); + }; + const handleRate = async (quality: number) => { if (!data || submitting) return; - const card = data.cards[currentIndex]; + const card = filteredCards[currentIndex]; setSubmitting(true); try { @@ -51,11 +74,24 @@ export default function ReviewPage() { duration_seconds: null, }); - if (currentIndex + 1 >= data.cards.length) { - setCompleted(true); - toast.success("今天的复习全部完成!"); + // Remove from data + setData((prev) => { + if (!prev) return prev; + return { + ...prev, + cards: prev.cards.filter((c) => c.review_card_id !== card.review_card_id), + due_count: prev.due_count - 1, + }; + }); + + toast.success(quality >= 3 ? "答对了!" : "没关系,下次会更好!"); + + if (currentIndex + 1 >= filteredCards.length) { + setView("list"); + if (filteredCards.length <= 1) { + setCompleted(true); + } } else { - setCurrentIndex(currentIndex + 1); setIsFlipped(false); } } catch (err: any) { @@ -67,15 +103,16 @@ export default function ReviewPage() { const handleAnalyze = async () => { if (!data || analyzing) return; - const card = data.cards[currentIndex]; + const card = filteredCards[currentIndex]; setAnalyzing(true); try { const res = await api.post(`/mistakes/${card.mistake.id}/analyze`); const updatedMistake = { ...card.mistake, ai_analysis: res.data.ai_analysis, ai_analysis_at: new Date().toISOString() }; setData((prev) => { if (!prev) return prev; - const newCards = [...prev.cards]; - newCards[currentIndex] = { ...newCards[currentIndex], mistake: updatedMistake }; + const newCards = prev.cards.map((c) => + c.review_card_id === card.review_card_id ? { ...c, mistake: updatedMistake } : c + ); return { ...prev, cards: newCards }; }); toast.success("AI解析完成!"); @@ -121,163 +158,155 @@ export default function ReviewPage() { ); } - const card = data.cards[currentIndex]; - const mistake = card.mistake; - const progress = ((currentIndex) / data.cards.length) * 100; + // === Review Mode === + if (view === "review") { + const card = filteredCards[currentIndex]; + if (!card) { + setView("list"); + return null; + } + const mistake = card.mistake; + const progress = ((currentIndex) / filteredCards.length) * 100; + return ( +
+ {/* Header */} +
+
+ + + {currentIndex + 1} / {filteredCards.length} + +
+
+
+
+
+ + {/* Flip Card */} +
+
+ {/* Front */} +
+
+ + {mistake.subject} + + {mistake.question_type && {mistake.question_type}} + {card.is_new && 新题} +
+
+

{mistake.question_text}

+
+ {mistake.image_url && ( + + )} + +
+ + {/* Back */} +
+

题目解析

+
+
+ 题目 +

{mistake.question_text}

+
+ {mistake.student_answer && ( +
你的答案

{mistake.student_answer}

+ )} + {mistake.correct_answer && ( +
正确答案

{mistake.correct_answer}

+ )} + {mistake.error_analysis && ( +
错误原因

{mistake.error_analysis}

+ )} + {mistake.ai_analysis ? ( +
+ AI深度解析 + {mistake.ai_analysis.solution_steps &&

{mistake.ai_analysis.solution_steps}

} + {mistake.ai_analysis.knowledge_explanation &&
知识点

{mistake.ai_analysis.knowledge_explanation}

} +
+ ) : ( + + )} +
+ + {/* Rating */} +
+

掌握得怎么样?

+
+ {REVIEW_QUALITY_LABELS.map((q) => ( + + ))} +
+
+
+
+
+ + {/* Quick Nav */} +
+ + +
+
+ ); + } + + // === List Mode === return (
- {/* Progress */} -
-
-

今日复习

- - {currentIndex + 1} / {data.cards.length} - -
-
-
-
+

今日复习

+ + {/* Subject Filter */} +
+ + {SUBJECTS.filter((s) => subjectCounts[s]).map((s) => ( + + ))}
- {/* Flip Card */} -
-
- {/* Front - Question */} -
-
- - {mistake.subject} - - {mistake.question_type && ( - - {mistake.question_type} - - )} - {card.is_new && ( - - 新题 - - )} -
- -
-

- {mistake.question_text} -

-
- - {mistake.image_url && ( - 原题图片 - )} - - -
- - {/* Back - Answer */} -
-

题目解析

- -
-
- 题目 -

{mistake.question_text}

-
- {mistake.student_answer && ( -
- 你的答案 -

{mistake.student_answer}

+ {/* Question List */} +
+ {filteredCards.map((card, idx) => { + const m = card.mistake; + const colors = SUBJECT_COLORS[m.subject] || { bg: "bg-slate-50", text: "text-slate-700", border: "border-slate-300" }; + return ( +
startReview(idx)} className={`bg-white rounded-xl shadow-sm border ${colors.border} p-4 cursor-pointer active:scale-[0.98] transition-transform`}> +
+
+
+ {m.subject} + {m.question_type && {m.question_type}} + {card.is_new && 新题} +
+

{m.question_text}

- )} - {mistake.correct_answer && ( -
- 正确答案 -

{mistake.correct_answer}

-
- )} - {mistake.error_analysis && ( -
- 错误原因 -

{mistake.error_analysis}

-
- )} - - {/* AI Analysis */} - {mistake.ai_analysis ? ( -
- AI深度解析 - {mistake.ai_analysis.solution_steps && ( -

{mistake.ai_analysis.solution_steps}

- )} - {mistake.ai_analysis.knowledge_explanation && ( -
- 知识点 -

{mistake.ai_analysis.knowledge_explanation}

-
- )} -
- ) : ( - - )} -
- - {/* Rating Buttons */} -
-

你觉得掌握得怎么样?

-
- {REVIEW_QUALITY_LABELS.map((q) => ( - - ))} +
-
-
+ ); + })}
+ + {/* Start All Button */} +
); }