Files
mnote/wolai-frontend/src/app/(app)/wolai-import/page.tsx
T

152 lines
5.6 KiB
TypeScript
Raw Normal View History

2026-01-21 18:21:10 +08:00
"use client";
import { useMemo, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
export default function WolaiImportPage() {
const router = useRouter();
const searchParams = useSearchParams();
const parentId = useMemo(() => searchParams.get("parentId") ?? "", [searchParams]);
const [file, setFile] = useState<File | null>(null);
const [zipPath, setZipPath] = useState("");
const [rootMdPath, setRootMdPath] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string>("");
const [warnings, setWarnings] = useState<string[]>([]);
const handleSubmit = async () => {
if (!zipPath.trim() && !file) {
setError("请填写本地 ZIP 路径,或选择 ZIP 文件");
return;
}
setBusy(true);
setError("");
setWarnings([]);
try {
const useLocalPath = Boolean(zipPath.trim());
let res: Response;
if (useLocalPath) {
res = await fetch("/api/wolai-import", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
zipPath: zipPath.trim(),
parentId: parentId || undefined,
rootMdPath: rootMdPath.trim() || undefined,
}),
});
} else {
if (!file) {
setError("请选择 ZIP 文件");
return;
}
const formData = new FormData();
formData.set("file", file);
if (parentId) formData.set("parentId", parentId);
if (rootMdPath.trim()) formData.set("rootMdPath", rootMdPath.trim());
res = await fetch("/api/wolai-import", { method: "POST", body: formData });
}
const payload = (await res.json().catch(() => null)) as any;
if (!res.ok) {
const msg = payload?.error ? String(payload.error) : `导入失败:${res.status}`;
setError(msg);
if (res.status === 409 && Array.isArray(payload?.candidates)) {
setWarnings([`无法自动判断入口页面,请在“入口 rootMdPath”填入其中一个:`, ...payload.candidates.map(String)]);
}
return;
}
const ws = Array.isArray(payload?.warnings) ? payload.warnings.map(String) : [];
setWarnings(ws);
const rootDocumentId = String(payload?.rootDocumentId ?? "");
if (rootDocumentId) {
router.push(`/documents/${encodeURIComponent(rootDocumentId)}`);
return;
}
setError("导入完成但缺少 rootDocumentId");
} catch (err) {
const msg = err instanceof Error ? err.message : "导入失败";
setError(msg);
} finally {
setBusy(false);
}
};
return (
<div className="mx-auto w-full max-w-2xl p-6">
<Card>
<CardHeader>
<CardTitle>导入 WolaiMarkdown ZIP</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="text-sm font-medium">本地 ZIP 路径(大文件推荐)</div>
<Input
value={zipPath}
onChange={(e) => setZipPath(e.target.value)}
placeholder="例如:C:\\Users\\liaib\\Downloads\\个人.zip"
disabled={busy}
/>
<div className="text-xs text-muted-foreground">
说明:你的 <span className="font-mono">个人.zip</span> 1.26GB,浏览器上传很容易 500
建议填本地路径并清空 ZIP 文件选择(或不选择文件)。
</div>
</div>
<div className="space-y-2">
<div className="text-sm font-medium">ZIP 文件</div>
<Input
type="file"
accept=".zip,application/zip"
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
disabled={busy}
/>
<div className="text-xs text-muted-foreground">说明:请使用 Wolai 导出的“Markdown(含附件/图片)”ZIP</div>
</div>
<div className="space-y-2">
<div className="text-sm font-medium">入口 rootMdPath(可选)</div>
<Input
value={rootMdPath}
onChange={(e) => setRootMdPath(e.target.value)}
placeholder="例如:dQeAax/个人.md(留空则自动判断)"
disabled={busy}
/>
<div className="text-xs text-muted-foreground">
自动判断失败时会返回候选列表,你可以把其中一个填进来重试。
</div>
</div>
{parentId ? (
<div className="text-xs text-muted-foreground">
将作为子页面导入到:<span className="font-mono">{parentId}</span>
</div>
) : null}
{error ? <div className="text-sm text-red-600">{error}</div> : null}
{warnings.length > 0 ? (
<div className={cn("rounded-md border bg-muted/30 p-3 text-xs whitespace-pre-wrap")}>
{warnings.join("\n")}
</div>
) : null}
<div className="flex gap-2">
<Button onClick={() => void handleSubmit()} disabled={busy || (!zipPath.trim() && !file)}>
{busy ? "导入中..." : "开始导入"}
</Button>
<Button variant="secondary" onClick={() => router.back()} disabled={busy}>
返回
</Button>
</div>
</CardContent>
</Card>
</div>
);
}