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 00000000..89eb7e9b Binary files /dev/null and b/design/摩尔生物产品线 230619简化.mmap differ diff --git a/supabase/migrations/20251201_add_mindmap_user_configs.sql b/supabase/migrations/20251201_add_mindmap_user_configs.sql new file mode 100644 index 00000000..d681eb77 --- /dev/null +++ b/supabase/migrations/20251201_add_mindmap_user_configs.sql @@ -0,0 +1,43 @@ +create table if not exists public.mindmap_user_configs ( + user_id uuid primary key references public.profiles(id) on delete cascade, + config jsonb not null default '{}'::jsonb, + updated_at timestamptz not null default timezone('utc', now()) +); + +create or replace function public.set_mindmap_user_configs_updated_at() +returns trigger +language plpgsql +security invoker +as $$ +begin + new.updated_at := timezone('utc', now()); + return new; +end; +$$; + +drop trigger if exists set_mindmap_user_configs_updated_at on public.mindmap_user_configs; +create trigger set_mindmap_user_configs_updated_at +before update on public.mindmap_user_configs +for each row +execute procedure public.set_mindmap_user_configs_updated_at(); + +alter table public.mindmap_user_configs enable row level security; + +drop policy if exists "Select own mindmap config" on public.mindmap_user_configs; +create policy "Select own mindmap config" + on public.mindmap_user_configs + for select + using (auth.uid() = user_id); + +drop policy if exists "Insert own mindmap config" on public.mindmap_user_configs; +create policy "Insert own mindmap config" + on public.mindmap_user_configs + for insert + with check (auth.uid() = user_id); + +drop policy if exists "Update own mindmap config" on public.mindmap_user_configs; +create policy "Update own mindmap config" + on public.mindmap_user_configs + for update + using (auth.uid() = user_id) + with check (auth.uid() = user_id); diff --git a/temp_view.txt b/temp_view.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/temp_view.txt @@ -0,0 +1 @@ + diff --git a/wolai-frontend/src/app/lab/mindmap/mindmap-lab-client.tsx b/wolai-frontend/src/app/lab/mindmap/mindmap-lab-client.tsx index 5d6666ee..69e4af58 100644 --- a/wolai-frontend/src/app/lab/mindmap/mindmap-lab-client.tsx +++ b/wolai-frontend/src/app/lab/mindmap/mindmap-lab-client.tsx @@ -8,6 +8,9 @@ import { useState, type ChangeEvent, } from 'react'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { HocuspocusProvider } from '@hocuspocus/provider'; +import * as Y from 'yjs'; import { createPortal } from 'react-dom'; import { useRouter } from 'next/navigation'; import { toast } from 'sonner'; @@ -47,7 +50,6 @@ import { DrawerTitle, } from '@/components/ui/drawer'; import { Separator } from '@/components/ui/separator'; -import { ScrollArea } from '@/components/ui/scroll-area'; import { DEFAULT_MINDMAP_SNAPSHOT, type MindmapNode, @@ -170,6 +172,14 @@ type SnapshotHistoryEntry = { viewSignature: string; }; +type CollaborationStatus = 'idle' | 'connecting' | 'connected' | 'error'; + +type MindmapCollaborationRefs = { + doc: Y.Doc; + provider: HocuspocusProvider; + state: Y.Map; +}; + 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 ? '实时同步中' : '等待主导图推送'} + +