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
+9 -2
View File
@@ -9,11 +9,18 @@ type AssetsChangedPayload = {
asset?: unknown;
assetIds?: string[];
mindmapDeleted?: boolean;
mindmapAssetIds?: string[];
};
export function emitAssetsChanged(docId?: string, asset?: unknown, assetIds?: string[], mindmapDeleted?: boolean) {
export function emitAssetsChanged(
docId?: string,
asset?: unknown,
assetIds?: string[],
mindmapDeleted?: boolean,
mindmapAssetIds?: string[],
) {
if (typeof window === "undefined") return;
const detail: AssetsChangedPayload = { docId, asset, assetIds, mindmapDeleted };
const detail: AssetsChangedPayload = { docId, asset, assetIds, mindmapDeleted, mindmapAssetIds };
window.dispatchEvent(new CustomEvent(ASSETS_CHANGED_EVENT, { detail }));
}
+61 -2
View File
@@ -16,9 +16,21 @@ const tryAccess = async (file: string) => {
export async function detectLocalMindmapDocs(docIds: string[]): Promise<string[]> {
const results: string[] = [];
for (const id of docIds) {
const preferred = path.join(preferredBaseDir, id, "mindmap.json");
const folder = path.join(preferredBaseDir, id);
const preferredLegacy = path.join(folder, "mindmap.json");
const legacy = path.join(legacyBaseDir, id, "mindmap.json");
if ((await tryAccess(preferred)) || (await tryAccess(legacy))) {
let has = false;
if ((await tryAccess(preferredLegacy)) || (await tryAccess(legacy))) {
has = true;
} else {
try {
const entries = await fs.readdir(folder);
has = entries.some((name) => /^mindmap-.+\.json$/i.test(name));
} catch {
has = false;
}
}
if (has) {
results.push(id);
}
}
@@ -26,3 +38,50 @@ export async function detectLocalMindmapDocs(docIds: string[]): Promise<string[]
}
export { preferredBaseDir, legacyBaseDir };
export type LocalMindmapFile = {
documentId: string;
mindmapId: string;
fileName: string;
source: "preferred" | "legacy";
};
export async function detectLocalMindmapFiles(docIds: string[]): Promise<LocalMindmapFile[]> {
const results: LocalMindmapFile[] = [];
for (const id of docIds) {
const folder = path.join(preferredBaseDir, id);
try {
const entries = await fs.readdir(folder);
for (const name of entries) {
if (name === "mindmap.json") {
// mindmap.json 属于旧版单文件导图:mindmapId 必须包含 docId,避免不同页面间 ID 冲突
results.push({
documentId: id,
mindmapId: `legacy-${id}`,
fileName: name,
source: "preferred",
});
continue;
}
const m = /^mindmap-(.+)\.json$/i.exec(name);
if (m && m[1]) {
results.push({ documentId: id, mindmapId: m[1], fileName: name, source: "preferred" });
}
}
} catch {
// ignore
}
// 旧目录(仅支持 mindmap.json
const legacy = path.join(legacyBaseDir, id, "mindmap.json");
if (await tryAccess(legacy)) {
results.push({
documentId: id,
mindmapId: `legacy-${id}`,
fileName: "mindmap.json",
source: "legacy",
});
}
}
return results;
}
+9 -5
View File
@@ -28,12 +28,16 @@ const wrapCookies = (store: RequestCookies) => {
getAll: (...args: Parameters<RequestCookies["getAll"]>) =>
store.getAll(...args).map((cookie) => ({ ...cookie, value: decodeValue(cookie.value) })),
set: (...args: Parameters<RequestCookies["set"]>) => {
const [name, value, options] = args;
if (typeof value === "string") {
store.set(name, encodeValue(value), options);
} else {
store.set(name, value);
const [name, value, options] = args as unknown as [
unknown,
unknown,
unknown,
];
if (typeof name === "string" && typeof value === "string") {
(store as any).set(name, encodeValue(value), options as any);
return;
}
(store as any).set(...(args as any));
},
delete: (...args: Parameters<RequestCookies["delete"]>) => store.delete(...args),
has: (...args: Parameters<RequestCookies["has"]>) => store.has(...args),
+5 -5
View File
@@ -1,5 +1,4 @@
import type { SupabaseClient } from "@supabase/supabase-js";
import type { SupabaseClient } from "@supabase/supabase-js";
import type { DocumentRecord, DocumentNode } from "@/lib/documents";
import type { SidebarSectionId, TrashRecord } from "@/components/sidebar/types";
import type { Database } from "@/types/supabase";
@@ -36,7 +35,8 @@ export async function fetchSidebarDataset(
throw new Error(`获取工作空间文档列表失败:${error.message}`);
}
const documents: DocumentRecord[] = (documentRows ?? []).map((row) => ({
const documentRowsAny = (documentRows ?? []) as any[];
const documents: DocumentRecord[] = documentRowsAny.map((row) => ({
access_scope: (row.access_scope ?? "private") as DocumentRecord["access_scope"],
id: row.id,
workspace_id: row.workspace_id,
@@ -61,7 +61,8 @@ export async function fetchSidebarDataset(
throw new Error(`获取垃圾桶内容失败:${trashError.message}`);
}
const trashedDocuments: TrashRecord[] = (trashRows ?? []).map((row) => ({
const trashRowsAny = (trashRows ?? []) as any[];
const trashedDocuments: TrashRecord[] = trashRowsAny.map((row) => ({
id: row.id,
title: row.title,
parent_id: row.parent_id,
@@ -69,8 +70,7 @@ export async function fetchSidebarDataset(
access_scope: (row.access_scope ?? "private") as DocumentRecord["access_scope"],
}));
const mindmapDocs =
documentRows?.filter((row) => row.mindmap_data != null).map((row) => row.id) ?? [];
const mindmapDocs = documentRowsAny.filter((row) => row.mindmap_data != null).map((row) => row.id);
const { data: assetRows, error: assetError } = await client
.from("media_assets")
+4 -4
View File
@@ -4,13 +4,13 @@ import { getDecodedCookies } from "@/lib/server-cookies";
export const createSupabaseServerClient = async () => {
const cookieStore = await getDecodedCookies();
return createServerComponentClient({
cookies: () => cookieStore,
});
cookies: () => cookieStore as any,
} as any);
};
export const createSupabaseRouteClient = async () => {
const cookieStore = await getDecodedCookies();
return createRouteHandlerClient({
cookies: () => cookieStore,
});
cookies: () => cookieStore as any,
} as any);
};
+23 -15
View File
@@ -1,17 +1,24 @@
import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "@/types/supabase";
type TypedClient = SupabaseClient<Database>;
type TypedClient = SupabaseClient<any>;
interface WorkspaceMembershipRow {
workspace_id: string;
is_default: boolean;
workspaces: {
id: string;
name: string;
type: "personal" | "team";
icon_url: string | null;
} | null;
workspaces:
| {
id: string;
name: string;
type: "personal" | "team";
icon_url: string | null;
}
| Array<{
id: string;
name: string;
type: "personal" | "team";
icon_url: string | null;
}>
| null;
}
export interface WorkspaceSummary {
@@ -80,7 +87,7 @@ export async function fetchWorkspaceSummaries(
throw new Error(`拉取工作空间列表失败:${error.message}`);
}
const rows: WorkspaceMembershipRow[] = memberRows ?? [];
const rows: WorkspaceMembershipRow[] = (memberRows ?? []) as any;
const workspaceIds = rows.map((row) => row.workspace_id);
const memberCountMap: Record<string, number> = {};
@@ -102,15 +109,16 @@ export async function fetchWorkspaceSummaries(
const summaries: WorkspaceSummary[] = rows
.map((row) => {
if (!row.workspaces) {
const workspace = Array.isArray(row.workspaces) ? row.workspaces[0] : row.workspaces;
if (!workspace) {
return null;
}
return {
id: row.workspaces.id,
name: row.workspaces.name,
type: row.workspaces.type,
iconUrl: row.workspaces.icon_url,
memberCount: memberCountMap[row.workspaces.id] ?? 1,
id: workspace.id,
name: workspace.name,
type: workspace.type,
iconUrl: workspace.icon_url,
memberCount: memberCountMap[workspace.id] ?? 1,
isDefault: row.is_default,
} satisfies WorkspaceSummary;
})