feat: 添加 Anki APKG 导入/导出功能

- 添加 APKG 解析器 (src/lib/anki/apkg-parser.ts)
- 添加 APKG 导出器 (src/lib/anki/apkg-exporter.ts)
- 添加导入/导出 Server Actions
- 添加导入/导出 UI 组件
- 集成到牌组页面
- 添加 i18n 翻译

同时修复断链:
- /folders → /decks (Navbar, signup, profile)
This commit is contained in:
2026-03-11 10:37:23 +08:00
parent 4d4062985d
commit 7ba31a37bd
13 changed files with 1654 additions and 32 deletions

View File

@@ -25,6 +25,7 @@ import {
actionGetDeckById,
} from "@/modules/deck/deck-action";
import type { ActionOutputDeck } from "@/modules/deck/deck-action-dto";
import { ImportButton, ExportButton } from "@/components/deck/ImportExport";
interface DeckCardProps {
deck: ActionOutputDeck;
@@ -149,25 +150,17 @@ export function DecksClient({ userId }: DecksClientProps) {
const [decks, setDecks] = useState<ActionOutputDeck[]>([]);
const [loading, setLoading] = useState(true);
const loadDecks = async () => {
setLoading(true);
const result = await actionGetDecksByUserId(userId);
if (result.success && result.data) {
setDecks(result.data);
}
setLoading(false);
};
useEffect(() => {
let ignore = false;
const loadDecks = async () => {
setLoading(true);
const result = await actionGetDecksByUserId(userId);
if (!ignore) {
if (result.success && result.data) {
setDecks(result.data);
}
setLoading(false);
}
};
loadDecks();
return () => {
ignore = true;
};
}, [userId]);
const handleUpdateDeck = (deckId: number, updates: Partial<ActionOutputDeck>) => {
@@ -199,11 +192,12 @@ export function DecksClient({ userId }: DecksClientProps) {
<PageLayout>
<PageHeader title={t("title")} subtitle={t("subtitle")} />
<div className="mb-4">
<div className="mb-4 flex gap-2">
<LightButton onClick={handleCreateDeck}>
<Plus size={18} />
{t("newDeck")}
</LightButton>
<ImportButton onImportComplete={loadDecks} />
</div>
<CardList>