"use client"; import { useRef, useState } from "react"; import { AlertTriangle, CheckCircle2, ClipboardCheck, Database, FileUp, ListChecks, ListX, Loader2, MessageSquare, ScanLine, Upload, Users, XCircle, } from "lucide-react"; import { AdminSettingsGroup, AdminSwitch, } from "@/components/admin"; import { apiAdminImportLegacy, type LegacyCommentReport, type LegacyImportReport, type LegacyPostReport, } from "@/lib/api"; /** 数据源清单:enabled=false 的来源展示为「计划中」,为后续 WordPress/Typecho 适配预留 */ const SOURCES = [ { key: "jiang13", label: "姜十三论坛(SQLite)", enabled: true }, { key: "wordpress", label: "WordPress", enabled: false }, { key: "typecho", label: "Typecho", enabled: false }, ] as const; /** * 站点设置 → 数据导入: * 上传旧站数据库(+可选头像包)→ 预检(dry-run,不写库)→ 确认正式导入。 * 全程幂等:已导入对象自动跳过,可安全重复执行。 */ export default function ImportPanel() { const [source, setSource] = useState("jiang13"); const [dbFile, setDbFile] = useState(null); const [zipFile, setZipFile] = useState(null); const [withContent, setWithContent] = useState(true); const [busy, setBusy] = useState<"check" | "import" | null>(null); const [error, setError] = useState(""); const [report, setReport] = useState(null); const dbInputRef = useRef(null); const zipInputRef = useRef(null); // 「确认导入」只有在当前配置下跑过预检后才可用;文件/选项变化即重置 const [preflighted, setPreflighted] = useState(false); const resetResult = () => { setPreflighted(false); setReport(null); setError(""); }; const pickDb = (f: File | null) => { setDbFile(f); resetResult(); }; const pickZip = (f: File | null) => { setZipFile(f); resetResult(); }; const toggleContent = () => { setWithContent((v) => !v); resetResult(); }; const run = async (dryRun: boolean) => { if (!dbFile || busy) return; setBusy(dryRun ? "check" : "import"); setError(""); try { const rep = await apiAdminImportLegacy(source, dbFile, zipFile, { withContent, dryRun, }); setReport(rep); if (dryRun) setPreflighted(true); } catch (e) { setError(e instanceof Error ? e.message : "导入失败"); } finally { setBusy(null); } }; return (
{SOURCES.map((s) => ( ))}
同时导入帖子与评论
{busy ? null : !dbFile ? "请先选择站点数据库" : !preflighted ? "请先运行预检" : "预检通过,可以正式导入"}
{error && (

{error}

)} {report && }
); } function FilePickRow({ label, hint, fileName, inputRef, accept, onPick, busy, }: { label: string; hint: string; fileName?: string; inputRef: React.RefObject; accept: string; onPick: (f: File | null) => void; busy: boolean; }) { return (

{label}

{fileName || hint}

{ onPick(e.target.files?.[0] ?? null); e.target.value = ""; }} />
); } function StatChip({ icon, label, value, ok }: { icon: React.ReactNode; label: string; value: number; ok?: boolean }) { return (
{icon} {label} {value}
); } function ReportView({ report }: { report: LegacyImportReport }) { const isDry = report.dry_run; return (

{isDry ? ( <> 预检结果(未写入任何数据)——确认无误后点击「确认正式导入」 ) : ( <> 导入完成。重复执行时已导入的内容会自动跳过 )}

} title="用户"> } label="旧库用户" value={report.users.total} /> } label={isDry ? "将导入" : "已导入"} value={report.users.imported} /> } label="已存在跳过" value={report.users.skipped} ok={false} /> {report.users.avatar_written > 0 && ( } label="头像写入" value={report.users.avatar_written} /> )} {report.boards && ( } title="板块"> } label={isDry ? "将新建" : "已新建"} value={report.boards.created} /> } label="复用同名" value={report.boards.reused} ok={false} /> )} {report.posts && } {report.comments && } {report.notes && report.notes.length > 0 && (
{report.notes.map((n, i) => (

{n}

))}
)}
); } function PostsView({ report, isDry }: { report: LegacyPostReport; isDry: boolean }) { return ( } title="帖子"> } label="旧帖" value={report.total} /> } label={isDry ? "将导入" : "已导入"} value={report.imported} /> } label="已导入跳过" value={report.skipped} ok={false} /> {report.polls_skipped > 0 && ( } label="投票帖未迁移" value={report.polls_skipped} ok={false} /> )} ); } function CommentsView({ report, isDry }: { report: LegacyCommentReport; isDry: boolean }) { return ( } title="评论"> } label="旧评论" value={report.total} /> } label={isDry ? "将导入" : "已导入"} value={report.imported} /> } label="已导入跳过" value={report.skipped} ok={false} /> ); } function ReportCard({ icon, title, children, }: { icon: React.ReactNode; title: string; children: React.ReactNode; }) { return (

{icon} {title}

{children}
); } function DetailList({ title, items, color, }: { title: string; items?: string[]; color?: string; }) { if (!items || items.length === 0) return null; return (
{title}:{items.length}
    {items.map((it, i) => (
  • {it}
  • ))}
); }