双向删除同步

This commit is contained in:
liaibo
2026-01-02 07:25:50 +08:00
parent 1db64c1c55
commit b1288487af
48 changed files with 2406 additions and 8841 deletions
+23
View File
@@ -0,0 +1,23 @@
/**
* 侧边栏与编辑区之间的轻量事件总线
*/
export const ASSETS_CHANGED_EVENT = "wolai:assets-changed";
export const DOCUMENTS_CHANGED_EVENT = "wolai:documents-changed";
type AssetsChangedPayload = {
docId?: string;
asset?: unknown;
assetIds?: string[];
mindmapDeleted?: boolean;
};
export function emitAssetsChanged(docId?: string, asset?: unknown, assetIds?: string[], mindmapDeleted?: boolean) {
if (typeof window === "undefined") return;
const detail: AssetsChangedPayload = { docId, asset, assetIds, mindmapDeleted };
window.dispatchEvent(new CustomEvent(ASSETS_CHANGED_EVENT, { detail }));
}
export function emitDocumentsChanged(docId?: string) {
if (typeof window === "undefined") return;
window.dispatchEvent(new CustomEvent(DOCUMENTS_CHANGED_EVENT, { detail: { docId } }));
}
+28
View File
@@ -0,0 +1,28 @@
import path from "path";
import { promises as fs } from "fs";
const preferredBaseDir = path.join(process.cwd(), "public", "documents");
const legacyBaseDir = path.join(process.cwd(), "public", "mindmaps");
const tryAccess = async (file: string) => {
try {
await fs.access(file);
return true;
} catch {
return false;
}
};
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 legacy = path.join(legacyBaseDir, id, "mindmap.json");
if ((await tryAccess(preferred)) || (await tryAccess(legacy))) {
results.push(id);
}
}
return results;
}
export { preferredBaseDir, legacyBaseDir };
+49 -24
View File
@@ -1,40 +1,31 @@
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";
import type { MediaAsset } from "@/types/media";
import { buildDocumentTree } from "@/lib/documents";
type TypedClient = SupabaseClient<Database>;
export interface SidebarSectionSnapshot {
id: SidebarSectionId;
title: string;
icon?: string;
nodes: DocumentNode[];
export interface SidebarDataset {
documents: DocumentRecord[];
trashedDocuments: TrashRecord[];
/**
* 仅依据远端 mindmap_data 判定的页面 id 列表;本地文件检测由服务器侧辅助完成
*/
mindmapDocs: string[];
mediaAssets?: MediaAsset[];
}
export interface FlattenedTreeNode {
node: DocumentNode;
depth: number;
parentId: string | null;
}
const SECTION_META: Record<SidebarSectionId, { title: string; icon: string }> = {
starred: { title: "星标置顶", icon: "star" },
public: { title: "公共页面", icon: "globe" },
shared: { title: "共享页面", icon: "users" },
private: { title: "私有 / 我的页面", icon: "lock" },
templates: { title: "模板中心", icon: "grid" },
};
export async function fetchSidebarDataset(
client: TypedClient,
workspaceId: string,
): Promise<{ documents: DocumentRecord[]; trashedDocuments: TrashRecord[] }> {
): Promise<SidebarDataset> {
const { data: documentRows, error } = await client
.from("documents")
.select(
"id,title,parent_id,sort_order,is_starred,access_scope,is_template,created_at,updated_at,workspace_id",
"id,title,parent_id,sort_order,is_starred,access_scope,is_template,created_at,updated_at,workspace_id,mindmap_data",
)
.eq("workspace_id", workspaceId)
.is("deleted_at", null)
@@ -78,9 +69,26 @@ 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 { data: assetRows, error: assetError } = await client
.from("media_assets")
.select("*")
.eq("workspace_id", workspaceId)
.order("created_at", { ascending: false });
if (assetError) {
throw new Error(`获取附件列表失败:${assetError.message}`);
}
const mediaAssets: MediaAsset[] = (assetRows ?? []) as MediaAsset[];
return {
documents,
trashedDocuments,
mindmapDocs,
mediaAssets,
};
}
@@ -107,8 +115,8 @@ export function flattenDocumentTree(
expanded: Set<string>,
depth = 0,
parentId: string | null = null,
): FlattenedTreeNode[] {
const list: FlattenedTreeNode[] = [];
): Array<{ node: DocumentNode; depth: number; parentId: string | null }> {
const list: Array<{ node: DocumentNode; depth: number; parentId: string | null }> = [];
nodes.forEach((node) => {
list.push({ node, depth, parentId });
if (node.children.length > 0 && expanded.has(node.id)) {
@@ -118,12 +126,29 @@ export function flattenDocumentTree(
return list;
}
const SECTION_META: Record<SidebarSectionId, { title: string; icon: string }> = {
starred: { title: "星标置顶", icon: "star" },
public: { title: "公共页面", icon: "globe" },
shared: { title: "共享页面", icon: "users" },
private: { title: "私有 / 我的页面", icon: "lock" },
templates: { title: "模板中心", icon: "grid" },
};
type SidebarSectionSnapshot = {
id: SidebarSectionId;
title: string;
icon?: string;
nodes: DocumentNode[];
};
export function buildSidebarSections(records: DocumentRecord[]): SidebarSectionSnapshot[] {
const tree = buildDocumentTree(records);
return buildSidebarSectionsFromTree(tree);
}
export function buildSidebarSectionsFromTree(tree: DocumentNode[]): SidebarSectionSnapshot[] {
export function buildSidebarSectionsFromTree(
tree: DocumentNode[],
): SidebarSectionSnapshot[] {
const sections: Array<{ id: SidebarSectionId; predicate: NodePredicate }> = [
{ id: "starred", predicate: (node) => Boolean(node.is_starred) },
{ id: "public", predicate: (node) => node.access_scope === "public" },