refactor: 完全重构为 Anki 兼容数据结构

- 用 Deck 替换 Folder
- 用 Note + Card 替换 Pair (双向复习)
- 添加 NoteType (卡片模板)
- 添加 Revlog (复习历史)
- 实现 SM-2 间隔重复算法
- 更新所有前端页面
- 添加数据库迁移
This commit is contained in:
2026-03-10 19:20:46 +08:00
parent 9b78fd5215
commit 57ad1b8699
72 changed files with 7107 additions and 2430 deletions

View File

@@ -0,0 +1,84 @@
import { Edit, Trash2 } from "lucide-react";
import { useState } from "react";
import { CircleButton } from "@/design-system/base/button";
import { UpdateCardModal } from "./UpdateCardModal";
import { useTranslations } from "next-intl";
import type { ActionOutputCardWithNote } from "@/modules/card/card-action-dto";
import { toast } from "sonner";
interface CardItemProps {
card: ActionOutputCardWithNote;
isReadOnly: boolean;
onDel: () => void;
refreshCards: () => void;
}
export function CardItem({
card,
isReadOnly,
onDel,
refreshCards,
}: CardItemProps) {
const [openUpdateModal, setOpenUpdateModal] = useState(false);
const t = useTranslations("deck_id");
const fields = card.note.flds.split('\x1f');
const field1 = fields[0] || "";
const field2 = fields[1] || "";
return (
<div className="group border-b border-gray-100 hover:bg-gray-50 transition-colors">
<div className="p-4">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2 text-xs text-gray-500">
<span className="px-2 py-1 bg-gray-100 rounded-md">
{t("card")}
</span>
</div>
<div className="flex items-center gap-1 opacity-50 group-hover:opacity-100 transition-opacity">
{!isReadOnly && (
<>
<CircleButton
onClick={() => setOpenUpdateModal(true)}
title={t("edit")}
className="text-gray-400 hover:text-gray-600"
>
<Edit size={14} />
</CircleButton>
<CircleButton
onClick={onDel}
title={t("delete")}
className="text-gray-400 hover:text-red-500 hover:bg-red-50"
>
<Trash2 size={14} />
</CircleButton>
</>
)}
</div>
</div>
<div className="text-gray-900 grid grid-cols-2 gap-4 w-3/4">
<div>
{field1.length > 30
? field1.substring(0, 30) + "..."
: field1}
</div>
<div>
{field2.length > 30
? field2.substring(0, 30) + "..."
: field2}
</div>
</div>
</div>
<UpdateCardModal
isOpen={openUpdateModal}
onClose={() => setOpenUpdateModal(false)}
card={card}
onUpdated={() => {
setOpenUpdateModal(false);
refreshCards();
}}
/>
</div>
);
}