152 lines
5.6 KiB
TypeScript
152 lines
5.6 KiB
TypeScript
"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>导入 Wolai(Markdown 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>
|
||
);
|
||
}
|