0.1.07 文件树拖拽与多思维导图

This commit is contained in:
liaibo
2026-01-08 06:28:14 +08:00
parent 9de2d69c55
commit 9af8b9e489
30 changed files with 817 additions and 410 deletions
@@ -59,13 +59,23 @@ async function ensureDocumentScaffold(id: string, title: string | null) {
}
async function copyMindmapIfExists(sourceId: string, targetId: string) {
const src = path.join(documentsBaseDir, sourceId, "mindmap.json");
const srcDir = path.join(documentsBaseDir, sourceId);
const destDir = path.join(documentsBaseDir, targetId);
const dest = path.join(destDir, "mindmap.json");
try {
const buf = await fs.readFile(src);
const entries = await fs.readdir(srcDir);
const mindmapFiles = entries.filter((name) => name === "mindmap.json" || /^mindmap-.+\\.json$/i.test(name));
if (mindmapFiles.length === 0) return;
await fs.mkdir(destDir, { recursive: true });
await fs.writeFile(dest, buf);
await Promise.all(
mindmapFiles.map(async (name) => {
try {
const buf = await fs.readFile(path.join(srcDir, name));
await fs.writeFile(path.join(destDir, name), buf);
} catch {
// ignore
}
}),
);
} catch {
// 源不存在则忽略
}
@@ -264,7 +274,8 @@ export async function POST(request: Request) {
} else {
siblingQuery.is("parent_id", null);
}
const { count: siblingCount = 0 } = await siblingQuery;
const { count: rawSiblingCount } = await siblingQuery;
const siblingCount = rawSiblingCount ?? 0;
const nextSortByParent = new Map<string | null, number>([[targetParentId, siblingCount]]);
const insertedDocs: Array<{ oldId: string; newId: string }> = [];
@@ -60,7 +60,8 @@ export async function POST(request: Request) {
siblingQuery.is("parent_id", null);
}
const { count: siblingCount = 0, error: countError } = await siblingQuery;
const { count: rawSiblingCount, error: countError } = await siblingQuery;
const siblingCount = rawSiblingCount ?? 0;
if (countError) {
return NextResponse.json({ error: countError.message }, { status: 500 });
@@ -86,7 +86,8 @@ async function handleCreateRequest(request: Request) {
siblingQuery.is("parent_id", null);
}
const { count: siblingCount = 0, error: countError } = await siblingQuery;
const { count: rawSiblingCount, error: countError } = await siblingQuery;
const siblingCount = rawSiblingCount ?? 0;
if (countError) {
return NextResponse.json({ error: countError.message }, { status: 500 });
@@ -19,13 +19,23 @@ async function ensureDocumentScaffold(id: string, title: string | null) {
}
async function copyMindmapIfExists(sourceId: string, targetId: string) {
const src = path.join(documentsBaseDir, sourceId, "mindmap.json");
const destDir = path.join(documentsBaseDir, targetId);
const dest = path.join(destDir, "mindmap.json");
try {
const buf = await fs.readFile(src);
const srcDir = path.join(documentsBaseDir, sourceId);
const destDir = path.join(documentsBaseDir, targetId);
const entries = await fs.readdir(srcDir);
const mindmapFiles = entries.filter((name) => name === "mindmap.json" || /^mindmap-.+\\.json$/i.test(name));
if (mindmapFiles.length === 0) return;
await fs.mkdir(destDir, { recursive: true });
await fs.writeFile(dest, buf);
await Promise.all(
mindmapFiles.map(async (name) => {
try {
const buf = await fs.readFile(path.join(srcDir, name));
await fs.writeFile(path.join(destDir, name), buf);
} catch {
// ignore
}
}),
);
} catch {
// 如果源不存在则忽略
}
@@ -68,7 +78,8 @@ export async function POST(request: Request) {
siblingQuery.is("parent_id", null);
}
const { count: siblingCount = 0, error: countError } = await siblingQuery;
const { count: rawSiblingCount, error: countError } = await siblingQuery;
const siblingCount = rawSiblingCount ?? 0;
if (countError) {
return NextResponse.json({ error: countError.message }, { status: 500 });
}
@@ -0,0 +1,158 @@
import { NextResponse } from "next/server";
import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { promises as fs } from "fs";
import path from "path";
const defaultMindmapData = {
data: { text: "中心主题" },
children: [],
};
const documentsBaseDir = path.join(process.cwd(), "public", "documents");
async function ensureDir(dir: string) {
await fs.mkdir(dir, { recursive: true });
}
async function removeFileSafe(file: string) {
try {
await fs.rm(file, { force: true });
} catch {
// ignore
}
}
async function ensureIndexFile(folder: string, title = "无标题") {
const indexFile = path.join(folder, "index.md");
try {
await fs.access(indexFile);
} catch {
await fs.writeFile(indexFile, `# ${title}\n`, "utf8");
}
}
function resolveMindmapFileName(mindmapId: string) {
if (mindmapId === "legacy" || mindmapId.startsWith("legacy-")) {
return "mindmap.json";
}
return `mindmap-${mindmapId}.json`;
}
async function tryReadJson(file: string) {
try {
const content = await fs.readFile(file, "utf8");
return JSON.parse(content);
} catch {
return null;
}
}
export async function GET(
_req: Request,
{ params }: { params: Promise<{ docId: string; mindmapId: string }> },
) {
const { docId, mindmapId } = await params;
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
const folder = path.join(documentsBaseDir, docId);
const file = path.join(folder, resolveMindmapFileName(mindmapId));
const legacyFile = path.join(folder, "mindmap.json");
const localData = (await tryReadJson(file)) ?? (await tryReadJson(legacyFile));
if (localData) {
return NextResponse.json({ data: localData, source: "local" });
}
// 兼容旧版:没有本地文件时,回退到 documents.mindmap_data(仅能表示单个旧导图)
const { data, error } = await supabase
.from("documents")
.select("mindmap_data")
.eq("id", docId)
.eq("user_id", session.user.id)
.maybeSingle();
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
const payload = data?.mindmap_data ?? defaultMindmapData;
return NextResponse.json({ data: payload, source: "supabase" });
}
export async function POST(
request: Request,
{ params }: { params: Promise<{ docId: string; mindmapId: string }> },
) {
const { docId, mindmapId } = await params;
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
// 校验页面归属(避免任意写入)
const { data: doc, error: docError } = await supabase
.from("documents")
.select("id,title")
.eq("id", docId)
.eq("user_id", session.user.id)
.maybeSingle();
if (docError) {
return NextResponse.json({ error: docError.message }, { status: 400 });
}
if (!doc) {
return NextResponse.json({ error: "页面不存在" }, { status: 404 });
}
const { data } = await request.json().catch(() => ({ data: null }));
const folder = path.join(documentsBaseDir, docId);
const file = path.join(folder, resolveMindmapFileName(mindmapId));
try {
await ensureDir(folder);
await ensureIndexFile(folder, doc.title ?? "无标题");
await fs.writeFile(file, JSON.stringify(data ?? defaultMindmapData, null, 2), "utf8");
} catch (error) {
return NextResponse.json({ error: String(error) }, { status: 500 });
}
return NextResponse.json({ ok: true });
}
export async function DELETE(
_req: Request,
{ params }: { params: Promise<{ docId: string; mindmapId: string }> },
) {
const { docId, mindmapId } = await params;
const supabase = await createSupabaseRouteClient();
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return NextResponse.json({ error: "未登录" }, { status: 401 });
}
// 校验页面归属
const { data: doc, error: docError } = await supabase
.from("documents")
.select("id")
.eq("id", docId)
.eq("user_id", session.user.id)
.maybeSingle();
if (docError) {
return NextResponse.json({ error: docError.message }, { status: 400 });
}
if (!doc) {
return NextResponse.json({ error: "页面不存在" }, { status: 404 });
}
const folder = path.join(documentsBaseDir, docId);
const file = path.join(folder, resolveMindmapFileName(mindmapId));
await removeFileSafe(file);
return NextResponse.json({ ok: true });
}
@@ -36,9 +36,9 @@ async function ensureIndexFile(folder: string, title = "无标题") {
export async function GET(
_req: Request,
{ params }: { params: Promise<{ id: string }> },
{ params }: { params: Promise<{ docId: string }> },
) {
const { id } = await params;
const { docId: id } = await params;
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -85,9 +85,9 @@ export async function GET(
export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> },
{ params }: { params: Promise<{ docId: string }> },
) {
const { id } = await params;
const { docId: id } = await params;
const supabase = await createSupabaseRouteClient();
const {
data: { session },
@@ -126,9 +126,9 @@ export async function POST(
export async function DELETE(
_req: Request,
{ params }: { params: Promise<{ id: string }> },
{ params }: { params: Promise<{ docId: string }> },
) {
const { id } = await params;
const { docId: id } = await params;
const supabase = await createSupabaseRouteClient();
const {
data: { session },
+33 -8
View File
@@ -3,7 +3,8 @@ import { createSupabaseRouteClient } from "@/lib/supabase/server";
import { ensureDefaultWorkspace, fetchWorkspaceSummaries } from "@/lib/workspaces";
import { fetchSidebarDataset } from "@/lib/sidebar-tree";
import type { SidebarInitialData } from "@/components/sidebar/types";
import { detectLocalMindmapDocs } from "@/lib/mindmap-files";
import { detectLocalMindmapFiles, detectLocalMindmapDocs } from "@/lib/mindmap-files";
import type { MediaAsset } from "@/types/media";
export const dynamic = "force-dynamic";
@@ -29,13 +30,36 @@ export async function GET(request: Request) {
}
try {
const dataset = await fetchSidebarDataset(supabase, targetWorkspaceId);
const localMindmaps = await detectLocalMindmapDocs(
dataset.documents.map((d) => d.id),
);
const mindmapDocs = Array.from(
new Set([...(dataset.mindmapDocs ?? []), ...localMindmaps]),
);
const dataset = await fetchSidebarDataset(supabase, targetWorkspaceId);
const docIds = dataset.documents.map((d) => d.id);
const localMindmapFiles = await detectLocalMindmapFiles(docIds);
const mindmapDocs = Array.from(new Set([...(dataset.mindmapDocs ?? []), ...(await detectLocalMindmapDocs(docIds))]));
const docById = new Map(dataset.documents.map((d) => [d.id, d]));
const mindmapAssets: MediaAsset[] = localMindmapFiles.map((item) => {
const doc = docById.get(item.documentId);
const workspaceId = doc?.workspace_id ?? targetWorkspaceId;
const fileUrlBase = item.source === "legacy" ? `/mindmaps/${item.documentId}` : `/documents/${item.documentId}`;
return {
id: item.mindmapId,
workspace_id: workspaceId,
document_id: item.documentId,
asset_type: "mindmap",
file_url: `${fileUrlBase}/${item.fileName}`,
thumbnail_url: null,
bucket: null,
storage_path: null,
file_name: item.fileName,
file_size: null,
mime_type: "application/json",
ocr_payload: undefined,
ocr_strategy: null,
ocr_text: null,
ocr_status: null,
signed_url: null,
created_at: "",
updated_at: "",
};
});
const payload: SidebarInitialData = {
activeWorkspaceId: targetWorkspaceId,
@@ -43,6 +67,7 @@ export async function GET(request: Request) {
documents: dataset.documents,
trashedDocuments: dataset.trashedDocuments,
mindmapDocs,
mindmapAssets,
mediaAssets: dataset.mediaAssets ?? [],
};