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>
|
|||
|
|
);
|
|||
|
|
}
|