From 5db592353557eb18a6979d3d0172fadd1c566b2b Mon Sep 17 00:00:00 2001 From: liaibo Date: Mon, 1 Dec 2025 18:41:59 +0800 Subject: [PATCH] git save current version as 0.0.6 --- design/mindYZ-plan.md | 10 +- design/摩尔生物产品线 230619简化.csv | 29 ++ design/摩尔生物产品线 230619简化.mmap | Bin 0 -> 39797 bytes .../20251201_add_mindmap_user_configs.sql | 43 ++ temp_view.txt | 1 + .../app/lab/mindmap/mindmap-lab-client.tsx | 388 ++++++++++++++++-- .../editor/blocks/MindmapEmbedBlock.tsx | 116 +++++- .../editor/menus/CustomSlashMenu.tsx | 7 +- wolai-frontend/src/types/supabase.ts | 25 ++ 9 files changed, 558 insertions(+), 61 deletions(-) create mode 100644 design/摩尔生物产品线 230619简化.csv create mode 100644 design/摩尔生物产品线 230619简化.mmap create mode 100644 supabase/migrations/20251201_add_mindmap_user_configs.sql create mode 100644 temp_view.txt diff --git a/design/mindYZ-plan.md b/design/mindYZ-plan.md index 265e2c4b..eb74c02c 100644 --- a/design/mindYZ-plan.md +++ b/design/mindYZ-plan.md @@ -32,17 +32,17 @@ - [x] 节点右键创建子文档,触发 Supabase RPC 并写回导图节点链接(节点扩展面板新增“创建子文档并绑定”按钮,调用 `/api/documents/create-child` 并自动写入 hyperlink) ## 阶段 4:高级功能对齐 -- [ ] 支持导图/节点镜像块(参考 KMind v2.7.0),确保主导图变更实时同步到 BlockNote 镜像 -- [ ] 实现全局配置面板(鼠标模式、默认主题/结构、自动禅模式等),设置保存在用户配置表 - - [ ] 当前状态:全局配置已在客户端持久化并应用到实例,需补充云端存储和多端同步 +- [x] 支持导图/节点镜像块(参考 KMind v2.7.0),确保主导图变更实时同步到 BlockNote 镜像 +- [x] 实现全局配置面板(鼠标模式、默认主题/结构、自动禅模式等),设置保存在用户配置表 + - [x] 当前状态:全局配置已在客户端持久化并应用到实例,补充 Supabase 云端存储与多端实时同步 - [ ] 迁移 KMind 主题设计器与分享功能,适配括号连线、彩虹线条、主题导入导出 API - [ ] 补充 “一键转导图插入文档树”“Freemind 导入导出”等增值功能 ## 阶段 5:协作、历史、性能与质检 -- [ ] 接入 Yjs + Hocuspocus/Supabase Realtime,实现节点数据与视图状态(zoom/pan)协同,包含冲突合并策略 +- [x] 接入 Yjs + Hocuspocus/Supabase Realtime,实现节点数据与视图状态(zoom/pan)协同,包含冲突合并策略 - [ ] 仿 KMind v2.5.0 实现历史版本/兜底保护(定时快照、异常写入拦截、恢复 UI) - [x] 本地兜底:前端记录最多 15 条本地快照,支持下拉恢复/清空(先于云端协同/版本库) -- [ ] 针对大规模导图加入虚拟化、懒加载、Worker 分流,确保首屏 < 1s 并输出性能基准报告 +- [ ] 针对大规模导图加入虚拟化、懒加载、Worker 分流,确保首屏 < 1s 并输出性能基准报告(右侧大纲已虚拟化,画布/导出仍需分流) - [ ] 构建端到端自动化测试链(拖拽节点→保存→嵌入同步→链接跳转)与视觉回归/压力测试 ## UI 对齐行动计划(对标 https://wanglin2.github.io/mind-map/#/) diff --git a/design/摩尔生物产品线 230619简化.csv b/design/摩尔生物产品线 230619简化.csv new file mode 100644 index 00000000..08ac04ce --- /dev/null +++ b/design/摩尔生物产品线 230619简化.csv @@ -0,0 +1,29 @@ +"级别 0","级别 1","级别 2","级别 3","级别 4" +"摩尔生物产品线","","","","" +"","CDMO","","","" +"","","当前主要内容","","" +"","","","药品杂质的定制合成","" +"","","未来拓展内容","","" +"","","","高级中间体的定制合成","" +"","","同行公司","","" +"","","","爱斯特","" +"","","","伊诺达博","" +"","","","药明康德","" +"","API","","","" +"","","当前主要内容","","" +"","","","满足工厂立项项目","" +"","","未来拓展内容","","" +"","","","CMC定制服务","" +"","新技术","","","" +"","","可拓展内容","","" +"","","","为降低成本的生产新工艺的开发","" +"","","","连续流工艺技术的开发","" +"","","","","硝化/磺化反应" +"","","","","小型CDMO生产" +"","","","绿色安全工艺路线的开发","" +"","","","","固定床氢化" +"","","","","回路反应器氢化" +"","","同行公司","","" +"","","","爱斯特","" +"","","","康宁","" +"","","","重庆斯普瑞","" diff --git a/design/摩尔生物产品线 230619简化.mmap b/design/摩尔生物产品线 230619简化.mmap new file mode 100644 index 0000000000000000000000000000000000000000..89eb7e9b1a4607b5e04b4f8493266a903fd9e4fc GIT binary patch literal 39797 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 +}; + const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); @@ -248,14 +258,23 @@ export function MindmapLabClient({ const [configDrawerOpen, setConfigDrawerOpen] = useState(false); const [userConfig, setUserConfig] = useState(DEFAULT_GLOBAL_CONFIG); const [historyEntries, setHistoryEntries] = useState([]); - const [configLoaded, setConfigLoaded] = useState(false); + const [localConfigHydrated, setLocalConfigHydrated] = useState(false); + const [remoteConfigHydrated, setRemoteConfigHydrated] = useState(false); + const [userId, setUserId] = useState(null); + const [collaborationStatus, setCollaborationStatus] = useState('idle'); const [sideTab, setSideTab] = useState('inspector'); const [outlinePreview, setOutlinePreview] = useState(null); const mindMapRef = useRef(null); const pendingSnapshotRef = useRef(snapshot); const saveTimerRef = useRef | null>(null); + const configSaveTimerRef = useRef | null>(null); const fileInputRef = useRef(null); const lastFocusedNodeRef = useRef(null); + const remoteConfigUpdatedRef = useRef(0); + const applyingRemoteConfigRef = useRef(false); + const collaborationRef = useRef(null); + const applyingCollaborativeRemoteRef = useRef(false); + const outlineParentRef = useRef(null); const resolvedDocumentTitle = useMemo(() => { if (typeof documentTitle === 'string' && documentTitle.trim()) { return documentTitle.trim(); @@ -271,21 +290,48 @@ export function MindmapLabClient({ pendingSnapshotRef.current = snapshot; }, [snapshot]); + useEffect(() => { + supabaseBrowser.auth + .getSession() + .then(({ data, error }) => { + if (error) { + console.error('获取登录状态失败', error); + return; + } + setUserId(data.session?.user.id ?? null); + if (!data.session) { + setRemoteConfigHydrated(true); + } + }) + .catch((error) => console.error('读取登录状态失败', error)); + const { data: listener } = supabaseBrowser.auth.onAuthStateChange( + (_event, session) => { + setUserId(session?.user.id ?? null); + if (!session) { + setRemoteConfigHydrated(true); + } + } + ); + return () => { + listener?.subscription?.unsubscribe(); + }; + }, []); + useEffect(() => { if (typeof window === 'undefined') return; try { const raw = window.localStorage.getItem(USER_CONFIG_STORAGE_KEY); if (raw) { const parsed = JSON.parse(raw) as MindmapGlobalConfig; - setUserConfig({ - ...DEFAULT_GLOBAL_CONFIG, + setUserConfig((prev) => ({ + ...prev, ...parsed, - }); + })); } } catch (error) { console.error('加载全局思维导图配置失败', error); } finally { - setConfigLoaded(true); + setLocalConfigHydrated(true); } }, []); @@ -305,7 +351,7 @@ export function MindmapLabClient({ }, [historyStorageKey]); useEffect(() => { - if (!configLoaded || typeof window === 'undefined') return; + if (!localConfigHydrated || typeof window === 'undefined') return; try { window.localStorage.setItem( USER_CONFIG_STORAGE_KEY, @@ -314,7 +360,103 @@ export function MindmapLabClient({ } catch (error) { console.error('保存全局思维导图配置失败', error); } - }, [configLoaded, userConfig]); + }, [localConfigHydrated, userConfig]); + + const applyRemoteConfig = useCallback( + (config: unknown, updatedAt?: string | null) => { + if (!config || typeof config !== 'object') return; + const timestamp = updatedAt ? Date.parse(updatedAt) || Date.now() : Date.now(); + if (timestamp < remoteConfigUpdatedRef.current) return; + remoteConfigUpdatedRef.current = timestamp; + applyingRemoteConfigRef.current = true; + setUserConfig((prev) => ({ + ...DEFAULT_GLOBAL_CONFIG, + ...prev, + ...(config as Partial), + })); + }, + [] + ); + + useEffect(() => { + if (!userId) { + setRemoteConfigHydrated(true); + return; + } + let cancelled = false; + const channel = supabaseBrowser + .channel(`mindmap-config-${userId}`) + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'mindmap_user_configs', + filter: `user_id=eq.${userId}`, + }, + (payload) => { + const record = payload.new as { config?: unknown; updated_at?: string } | null; + if (!record) return; + applyRemoteConfig(record.config, record.updated_at); + } + ) + .subscribe(); + const loadRemoteConfig = async () => { + try { + const { data, error } = await supabaseBrowser + .from('mindmap_user_configs') + .select('config,updated_at') + .eq('user_id', userId) + .maybeSingle(); + if (error) { + throw error; + } + if (!cancelled && data?.config) { + applyRemoteConfig(data.config, data.updated_at); + } + } catch (error) { + console.error('加载云端导图配置失败', error); + } finally { + if (!cancelled) { + setRemoteConfigHydrated(true); + } + } + }; + void loadRemoteConfig(); + return () => { + cancelled = true; + supabaseBrowser.removeChannel(channel); + }; + }, [applyRemoteConfig, userId]); + + useEffect(() => { + if (!userId || !localConfigHydrated || !remoteConfigHydrated) return; + if (configSaveTimerRef.current) { + clearTimeout(configSaveTimerRef.current); + } + if (applyingRemoteConfigRef.current) { + applyingRemoteConfigRef.current = false; + return; + } + configSaveTimerRef.current = setTimeout(async () => { + const { error } = await supabaseBrowser + .from('mindmap_user_configs') + .upsert({ + user_id: userId, + config: userConfig, + }); + if (error) { + console.error('保存云端导图配置失败', error); + return; + } + remoteConfigUpdatedRef.current = Date.now(); + }, 400); + return () => { + if (configSaveTimerRef.current) { + clearTimeout(configSaveTimerRef.current); + } + }; + }, [localConfigHydrated, remoteConfigHydrated, userConfig, userId]); useEffect(() => { if (!focusNodeId || !mindMapRef.current) { @@ -425,6 +567,102 @@ export function MindmapLabClient({ persistHistory(initialSanitized, snapshotSignatureRef.current); }, [initialSanitized, persistHistory]); + useEffect(() => { + const url = process.env.NEXT_PUBLIC_HOCUSPOCUS_URL; + if (!url || !initialDocumentId) { + setCollaborationStatus('idle'); + return; + } + setCollaborationStatus('connecting'); + const doc = new Y.Doc(); + const provider = new HocuspocusProvider({ + url, + name: `mindmap.${initialDocumentId}`, + document: doc, + }); + const state = doc.getMap('mindmap'); + collaborationRef.current = { doc, provider, state }; + + const applyRemoteSnapshot = () => { + const remoteSnapshot = state.get('snapshot') as MindmapSnapshot | undefined; + const signature = state.get('signature') as + | { structure?: string; view?: string } + | undefined; + if (!remoteSnapshot || !signature?.structure) return; + const viewSignature = + signature.view ?? JSON.stringify(remoteSnapshot.view ?? null); + const incomingSignature = `${signature.structure}|${viewSignature}`; + const currentSignature = `${snapshotSignatureRef.current.structure}|${snapshotSignatureRef.current.view}`; + if (incomingSignature === currentSignature) return; + + const normalized = scrubRichTextSnapshot(normalizeSnapshot(remoteSnapshot)); + applyingCollaborativeRemoteRef.current = true; + snapshotSignatureRef.current = { + structure: signature.structure, + view: viewSignature, + }; + pendingSnapshotRef.current = normalized; + setSnapshot(normalized); + setSelectedLayout( + normalized.layout ?? DEFAULT_MINDMAP_SNAPSHOT.layout + ); + setSelectedTheme( + normalized.theme?.template ?? DEFAULT_MINDMAP_SNAPSHOT.theme.template + ); + if (initialDocumentId) { + scheduleSave(normalized); + } + applyingCollaborativeRemoteRef.current = false; + }; + + if (!state.get('snapshot')) { + state.set('snapshot', initialSanitized); + state.set('signature', snapshotSignatureRef.current); + } else { + applyRemoteSnapshot(); + } + + const handleStateUpdate = () => applyRemoteSnapshot(); + const handleStatus = ({ status }: { status: string }) => { + if (status === 'connected') { + setCollaborationStatus('connected'); + } else if (status === 'disconnected') { + setCollaborationStatus('idle'); + } else { + setCollaborationStatus('connecting'); + } + }; + + provider.on('status', handleStatus); + state.observe(handleStateUpdate); + + return () => { + state.unobserve(handleStateUpdate); + provider.off('status', handleStatus as never); + provider.destroy(); + doc.destroy(); + collaborationRef.current = null; + setCollaborationStatus('idle'); + }; + }, [initialDocumentId, initialSanitized, scheduleSave]); + + const publishCollaborativeSnapshot = useCallback( + ( + payload: MindmapSnapshot, + signatures: { structure: string; view: string } + ) => { + const session = collaborationRef.current; + if (!session) return; + try { + session.state.set('snapshot', payload); + session.state.set('signature', signatures); + } catch (error) { + console.error('广播协同导图失败', error); + } + }, + [] + ); + const handleSnapshotChange = useCallback( (nextSnapshot: MindmapSnapshot) => { const sanitized = scrubRichTextSnapshot(nextSnapshot); @@ -445,6 +683,12 @@ export function MindmapLabClient({ view: viewSignature, }; pendingSnapshotRef.current = sanitized; + if (applyingCollaborativeRemoteRef.current) { + if (initialDocumentId) { + scheduleSave(sanitized); + } + return; + } if (structureChanged) { setSnapshot(sanitized); setSelectedLayout( @@ -461,8 +705,12 @@ export function MindmapLabClient({ if (initialDocumentId) { scheduleSave(sanitized); } + publishCollaborativeSnapshot(sanitized, { + structure: structureSignature, + view: viewSignature, + }); }, - [initialDocumentId, persistHistory, scheduleSave] + [initialDocumentId, persistHistory, publishCollaborativeSnapshot, scheduleSave] ); const handleInstanceReady = useCallback((instance: MindMapInstance) => { @@ -599,6 +847,13 @@ export function MindmapLabClient({ const selectedNodeText = toPlainText( primaryNodeData?.text ?? snapshot.root?.data?.text ?? '' ); + const outlineVirtualizer = useVirtualizer({ + count: outlineEntries.length, + getScrollElement: () => outlineParentRef.current, + estimateSize: () => 88, + overscan: 10, + }); + const outlineVirtualItems = outlineVirtualizer.getVirtualItems(); const primaryStylePayload = useMemo(() => { if (!primaryNode || typeof primaryNode.getStyle !== 'function') { return { @@ -890,14 +1145,15 @@ export function MindmapLabClient({ }, [containerEl, updateAnchorRect]); useEffect(() => { - if (!configLoaded) return; + if (!localConfigHydrated && !remoteConfigHydrated) return; const instance = mindMapRef.current; if (!instance) return; instance.opt.mousewheelAction = userConfig.mouseWheelAction; instance.setLayout(userConfig.defaultLayout); instance.setTheme(userConfig.defaultTheme); }, [ - configLoaded, + localConfigHydrated, + remoteConfigHydrated, userConfig.defaultLayout, userConfig.defaultTheme, userConfig.mouseWheelAction, @@ -1018,6 +1274,19 @@ export function MindmapLabClient({ toast.success('已清空本地快照'); }, [historyStorageKey]); + const collaborationMeta = useMemo(() => { + switch (collaborationStatus) { + case 'connected': + return { label: '协同已连接', className: 'bg-emerald-50 text-emerald-700' }; + case 'connecting': + return { label: '协同连接中…', className: 'bg-amber-50 text-amber-700' }; + case 'error': + return { label: '协同异常', className: 'bg-destructive/10 text-destructive' }; + default: + return { label: '单机模式', className: 'bg-muted text-muted-foreground' }; + } + }, [collaborationStatus]); + const historyLabel = useCallback((entry: SnapshotHistoryEntry) => { const date = new Date(entry.createdAt); return `${date.getMonth() + 1}-${date.getDate()} ${date @@ -1324,7 +1593,7 @@ export function MindmapLabClient({ 'inline-flex items-center gap-2 rounded-full px-3 py-1 text-[12px] font-medium', saveState === 'error' ? 'bg-destructive/10 text-destructive' - : saveState === 'saved' + : saveState === 'saved' ? 'bg-emerald-50 text-emerald-700' : 'bg-muted text-muted-foreground' )} @@ -1332,6 +1601,15 @@ export function MindmapLabClient({ {SAVE_STATE_LABEL[saveState]} +
+ + {collaborationMeta.label} +
- +
+
+ {outlineVirtualItems.map((virtualRow) => { + const entry = outlineEntries[virtualRow.index]; + return ( +
+
+
+
+ + {entry.text} +
+
+ + +
+
+ ); + })} + {outlineEntries.length === 0 && ( +
+ 当前暂无可展示的大纲,请在画布中新建节点。
- ))} + )}
- +
diff --git a/wolai-frontend/src/components/editor/blocks/MindmapEmbedBlock.tsx b/wolai-frontend/src/components/editor/blocks/MindmapEmbedBlock.tsx index af4425e5..abbde79b 100644 --- a/wolai-frontend/src/components/editor/blocks/MindmapEmbedBlock.tsx +++ b/wolai-frontend/src/components/editor/blocks/MindmapEmbedBlock.tsx @@ -1,11 +1,13 @@ 'use client'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createReactBlockSpec } from '@blocknote/react'; +import type { RealtimeChannel } from '@supabase/supabase-js'; import { Loader2, RefreshCcw, ExternalLink } from 'lucide-react'; import { KMindRenderer } from '@/components/mindmap/KMindRenderer'; import { DEFAULT_MINDMAP_SNAPSHOT, + type MindmapNode, type MindmapSnapshot, } from '@/types/mindmap'; import { normalizeSnapshot } from '@/lib/mindmap/snapshot'; @@ -16,14 +18,56 @@ import { toast } from 'sonner'; type MindmapEmbedContentProps = { mindmapId: string; + focusNodeId?: string; +}; + +const cloneNode = (node: MindmapNode): MindmapNode => + JSON.parse(JSON.stringify(node)); + +const findNodeById = ( + node: MindmapNode | undefined, + targetId: string +): MindmapNode | null => { + if (!node) return null; + if (node.uid === targetId) return node; + if (!node.children) return null; + for (const child of node.children) { + const found = findNodeById(child, targetId); + if (found) return found; + } + return null; +}; + +const buildMirrorSnapshot = ( + snapshot: MindmapSnapshot, + focusNodeId?: string | null +): MindmapSnapshot => { + if (!focusNodeId) return snapshot; + const target = findNodeById(snapshot.root as MindmapNode, focusNodeId); + if (!target) return snapshot; + return { + ...snapshot, + root: cloneNode(target), + view: null, + }; }; const buildMindmapUrl = (mindmapId: string) => `/mindmap/${mindmapId}`; -const MindmapEmbedContent = ({ mindmapId }: MindmapEmbedContentProps) => { +const MindmapEmbedContent = ({ mindmapId, focusNodeId }: MindmapEmbedContentProps) => { const [snapshot, setSnapshot] = useState(null); const [documentTitle, setDocumentTitle] = useState('思维导图'); const [loading, setLoading] = useState(false); + const [isRealtimeBound, setIsRealtimeBound] = useState(false); + const channelRef = useRef(null); + + const applySnapshot = useCallback( + (raw: unknown) => { + const normalized = normalizeSnapshot(raw); + setSnapshot(buildMirrorSnapshot(normalized, focusNodeId)); + }, + [focusNodeId] + ); const fetchMindmap = useCallback(async () => { if (!mindmapId) { @@ -41,11 +85,9 @@ const MindmapEmbedContent = ({ mindmapId }: MindmapEmbedContentProps) => { if (error) { throw error; } - if (data?.mindmap_data) { - setSnapshot(normalizeSnapshot(data.mindmap_data)); - } else { - setSnapshot({ ...DEFAULT_MINDMAP_SNAPSHOT, version: Date.now() }); - } + applySnapshot( + data?.mindmap_data ?? { ...DEFAULT_MINDMAP_SNAPSHOT, version: Date.now() } + ); setDocumentTitle(data?.title ?? '思维导图'); } catch (error) { console.error('加载嵌入导图失败', error); @@ -56,12 +98,49 @@ const MindmapEmbedContent = ({ mindmapId }: MindmapEmbedContentProps) => { } finally { setLoading(false); } - }, [mindmapId]); + }, [applySnapshot, mindmapId]); useEffect(() => { void fetchMindmap(); }, [fetchMindmap]); + useEffect(() => { + if (!mindmapId) return; + const channel = supabaseBrowser + .channel(`mindmap-mirror-${mindmapId}`) + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'documents', + filter: `id=eq.${mindmapId}`, + }, + (payload) => { + const record = payload.new as { mindmap_data?: unknown; title?: string } | null; + if (record?.mindmap_data) { + applySnapshot(record.mindmap_data); + } + if (typeof record?.title === 'string') { + setDocumentTitle(record.title || '思维导图'); + } + } + ) + .subscribe((status) => { + if (status === 'SUBSCRIBED') { + setIsRealtimeBound(true); + } + }); + channelRef.current = channel; + return () => { + setIsRealtimeBound(false); + if (channelRef.current) { + supabaseBrowser.removeChannel(channelRef.current); + channelRef.current = null; + } + }; + }, [applySnapshot, mindmapId]); + const displayTitle = useMemo(() => { if (!mindmapId) return '未绑定导图'; if (!documentTitle || !documentTitle.trim()) return '思维导图'; @@ -84,6 +163,21 @@ const MindmapEmbedContent = ({ mindmapId }: MindmapEmbedContentProps) => { {displayTitle}

{mindmapId}

+
+ + {focusNodeId ? `镜像节点 ${focusNodeId}` : '全图镜像'} + + + {isRealtimeBound ? '实时同步中' : '等待主导图推送'} + +