refactor: 完全重构为 Anki 兼容数据结构
- 用 Deck 替换 Folder - 用 Note + Card 替换 Pair (双向复习) - 添加 NoteType (卡片模板) - 添加 Revlog (复习历史) - 实现 SM-2 间隔重复算法 - 更新所有前端页面 - 添加数据库迁移
This commit is contained in:
142
src/app/decks/[deck_id]/InDeck.tsx
Normal file
142
src/app/decks/[deck_id]/InDeck.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowLeft, Plus } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { redirect, useRouter } from "next/navigation";
|
||||
import { AddCardModal } from "./AddCardModal";
|
||||
import { CardItem } from "./CardItem";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { PageLayout } from "@/components/ui/PageLayout";
|
||||
import { PrimaryButton, CircleButton, LinkButton } from "@/design-system/base/button";
|
||||
import { CardList } from "@/components/ui/CardList";
|
||||
import { actionGetCardsByDeckIdWithNotes, actionDeleteCard } from "@/modules/card/card-action";
|
||||
import type { ActionOutputCardWithNote } from "@/modules/card/card-action-dto";
|
||||
import { toast } from "sonner";
|
||||
|
||||
|
||||
export function InDeck({ deckId, isReadOnly }: { deckId: number; isReadOnly: boolean; }) {
|
||||
const [cards, setCards] = useState<ActionOutputCardWithNote[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [openAddModal, setAddModal] = useState(false);
|
||||
const router = useRouter();
|
||||
const t = useTranslations("deck_id");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCards = async () => {
|
||||
setLoading(true);
|
||||
await actionGetCardsByDeckIdWithNotes({ deckId })
|
||||
.then(result => {
|
||||
if (!result.success || !result.data) {
|
||||
throw new Error(result.message || "Failed to load cards");
|
||||
}
|
||||
return result.data;
|
||||
}).then(setCards)
|
||||
.catch((error) => {
|
||||
toast.error(error instanceof Error ? error.message : "Unknown error");
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
fetchCards();
|
||||
}, [deckId]);
|
||||
|
||||
const refreshCards = async () => {
|
||||
await actionGetCardsByDeckIdWithNotes({ deckId })
|
||||
.then(result => {
|
||||
if (!result.success || !result.data) {
|
||||
throw new Error(result.message || "Failed to refresh cards");
|
||||
}
|
||||
return result.data;
|
||||
}).then(setCards)
|
||||
.catch((error) => {
|
||||
toast.error(error instanceof Error ? error.message : "Unknown error");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="mb-6">
|
||||
<LinkButton
|
||||
onClick={router.back}
|
||||
className="flex items-center gap-2 mb-4"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
<span className="text-sm">{t("back")}</span>
|
||||
</LinkButton>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-1">
|
||||
{t("cards")}
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
{t("itemsCount", { count: cards.length })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
redirect(`/memorize?deck_id=${deckId}`);
|
||||
}}
|
||||
>
|
||||
{t("memorize")}
|
||||
</PrimaryButton>
|
||||
{!isReadOnly && (
|
||||
<CircleButton
|
||||
onClick={() => {
|
||||
setAddModal(true);
|
||||
}}
|
||||
>
|
||||
<Plus size={18} className="text-gray-700" />
|
||||
</CircleButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardList>
|
||||
{loading ? (
|
||||
<div className="p-8 text-center">
|
||||
<div className="w-8 h-8 border-2 border-gray-200 border-t-gray-400 rounded-full animate-spin mx-auto mb-3"></div>
|
||||
<p className="text-sm text-gray-500">{t("loadingCards")}</p>
|
||||
</div>
|
||||
) : cards.length === 0 ? (
|
||||
<div className="p-12 text-center">
|
||||
<p className="text-sm text-gray-500 mb-2">{t("noCards")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-100">
|
||||
{cards
|
||||
.toSorted((a, b) => Number(BigInt(a.id) - BigInt(b.id)))
|
||||
.map((card) => (
|
||||
<CardItem
|
||||
key={card.id}
|
||||
card={card}
|
||||
isReadOnly={isReadOnly}
|
||||
onDel={() => {
|
||||
actionDeleteCard({ cardId: BigInt(card.id) })
|
||||
.then(result => {
|
||||
if (!result.success) throw new Error(result.message || "Delete failed");
|
||||
}).then(refreshCards)
|
||||
.catch((error) => {
|
||||
toast.error(error instanceof Error ? error.message : "Unknown error");
|
||||
});
|
||||
}}
|
||||
refreshCards={refreshCards}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardList>
|
||||
|
||||
<AddCardModal
|
||||
isOpen={openAddModal}
|
||||
onClose={() => setAddModal(false)}
|
||||
deckId={deckId}
|
||||
onAdded={refreshCards}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user