0.1.11 ai修复与全屏

This commit is contained in:
liaibo
2026-01-10 10:35:21 +08:00
parent e74219c802
commit 0bcdc3e730
55 changed files with 6597 additions and 174 deletions
@@ -296,6 +296,7 @@ const MindmapBlockView = ({
block,
editor,
fullscreen = false,
onExitFullscreen,
}: {
block: SpecificBlock<
CustomBlockSchema,
@@ -305,6 +306,7 @@ const MindmapBlockView = ({
>;
editor: BlockNoteEditor<CustomBlockSchema>;
fullscreen?: boolean;
onExitFullscreen?: () => void;
}) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const fileInputRef = useRef<HTMLInputElement | null>(null);
@@ -327,9 +329,8 @@ const MindmapBlockView = ({
const hotkeyScopeRef = useRef(false);
const lastInteractionAtRef = useRef(0);
const skipNextPasteRef = useRef(false);
const persistDataRef = useRef<((data: unknown) => void) | null>(null);
const persistDataRef = useRef<((data: unknown) => void) | null>(null);
const recentNodeDblclickRef = useRef(false);
const [fullscreenApiActive, setFullscreenApiActive] = useState(false);
const deletingRef = useRef(false);
const getRootNode = useCallback((mm?: MindMapInstance | null) => {
@@ -441,6 +442,7 @@ const MindmapBlockView = ({
// 这里用 setTimeout(0) 而不是 microtaskBlockNote/ProseMirror 可能会在同一轮事件里重新抢回焦点,
// 导致“选中节点后 Ctrl+V 把思维导图替换成纯文本”。延后一拍把焦点拉回 wrapper,保证快捷键/粘贴作用域稳定。
if (effectiveFullscreen) return;
window.setTimeout(() => {
try {
wrapperRef.current?.focus?.({ preventScroll: true });
@@ -455,13 +457,14 @@ const MindmapBlockView = ({
document.removeEventListener("pointerdown", onPointerDownCapture, true);
document.removeEventListener("mousedown", onPointerDownCapture, true);
};
}, []);
}, [effectiveFullscreen]);
// 关键:阻止鼠标事件冒泡到 BlockNote/ProseMirror(它们会在 contenteditable 上处理 mousedown,从而产生 NodeSelection)。
// 不能用 React 的 onMouseDown(事件委托在 document,太晚了),必须用原生监听挂在 wrapper 上,确保在 bubble 链路中先于 editor DOM。
useEffect(() => {
const wrapper = wrapperRef.current;
if (!wrapper) return;
if (effectiveFullscreen) return;
const stopBubble = (e: Event) => {
const target = e.target as HTMLElement | null;
@@ -525,7 +528,10 @@ const MindmapBlockView = ({
const exitLocalFullscreen = useCallback(() => {
setActiveSidebar(null);
if (fullscreen) return;
if (fullscreen) {
onExitFullscreen?.();
return;
}
if (typeof document === "undefined") {
setLocalFullscreen(false);
return;
@@ -540,29 +546,12 @@ const MindmapBlockView = ({
return;
}
setLocalFullscreen(false);
}, [fullscreen]);
}, [fullscreen, onExitFullscreen]);
const enterLocalFullscreen = useCallback(() => {
if (fullscreen) return;
setLocalFullscreen(true);
setActiveSidebar(null);
if (typeof document === "undefined") return;
if (!document.fullscreenEnabled) return;
if (document.fullscreenElement) return;
// 必须在“用户手势回调”中同步调用,避免 Fullscreen API 被浏览器拒绝
try {
const target = document.documentElement as unknown as {
requestFullscreen?: () => Promise<void>;
};
const p = target.requestFullscreen?.();
if (p && typeof p.catch === "function") {
p.catch(() => {
// ignore:失败则保持 Portal 伪全屏
});
}
} catch {
// ignore:失败则保持 Portal 伪全屏
}
}, [fullscreen]);
// 沉浸式全屏:锁定页面滚动、支持 ESC 退出(仅对内嵌全屏生效)
@@ -596,24 +585,6 @@ const MindmapBlockView = ({
};
}, [localFullscreen, fullscreen, exitLocalFullscreen]);
// “真全屏”:使用 Fullscreen API 隐藏浏览器/桌面窗口的外层 UIElectron/Web 都可用)
useEffect(() => {
if (typeof document === "undefined") return;
const onFsChange = () => {
const active = Boolean(document.fullscreenElement);
setFullscreenApiActive(active);
// 注意:浏览器在打开原生对话框(例如 file picker / alert / confirm)时可能会自动退出
// Fullscreen API。此时不应退出“沉浸式全屏”UI,否则会导致用户在全屏编辑中执行导入/新建
// 等操作时被强制退出全屏。
};
document.addEventListener("fullscreenchange", onFsChange);
return () => {
document.removeEventListener("fullscreenchange", onFsChange);
};
}, [localFullscreen]);
// 让 Enter/Tab/复制/粘贴在“思维导图块”内生效(避免 BlockNote 抢走快捷键)
useLayoutEffect(() => {
const onKeyDownCapture = (e: KeyboardEvent) => {
@@ -706,6 +677,21 @@ const MindmapBlockView = ({
//(例如 Ctrl+C/Ctrl+V 作为文本复制粘贴)。
if (isNodeTextEditing) return;
if ((key === "Delete" || key === "Backspace") && !e.shiftKey && !e.altKey && !isMod) {
stop();
const inst = ensureActiveBefore(mm);
if (!inst) return;
inst.execCommand?.("REMOVE_NODE");
hasLocalEditsRef.current = true;
try {
const snapshot = inst.getData?.(true) ?? inst.getData?.() ?? null;
if (snapshot) persistDataRef.current?.(snapshot);
} catch {
// ignore
}
return;
}
if (isMod && lower === "c") {
stop();
const inst = ensureActiveBefore(mm);
@@ -1043,10 +1029,10 @@ const MindmapBlockView = ({
);
(async () => {
try {
const resp = await fetch(`/api/mindmap/${docId}/${mindmapId}`, {
const resp = await fetch(`/api/mindmap/${docId}/${mindmapId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data }),
body: JSON.stringify({ data, createOnly: true }),
});
if (!resp.ok) {
console.warn(
@@ -1426,9 +1412,14 @@ const MindmapBlockView = ({
window.__mindmapInstance = instance;
const w = window as unknown as {
__mindmapInstancesById?: Record<string, MindMapInstance>;
__mindmapPersistById?: Record<string, (data: unknown) => void>;
};
if (!w.__mindmapInstancesById) w.__mindmapInstancesById = {};
w.__mindmapInstancesById[mindmapId] = instance;
if (!w.__mindmapPersistById) w.__mindmapPersistById = {};
w.__mindmapPersistById[mindmapId] = (data: unknown) => {
persistDataRef.current?.(data);
};
}
setMindmap(instance);
@@ -1471,13 +1462,17 @@ const MindmapBlockView = ({
// 这里用内部事件标记“当前在思维导图作用域内”,确保 Ctrl+C/Ctrl+V 不会被 BlockNote 抢走。
hotkeyScopeRef.current = true;
lastInteractionAtRef.current = Date.now();
window.setTimeout(() => {
try {
wrapperRef.current?.focus?.({ preventScroll: true });
} catch {
// ignore
}
}, 0);
// 仅内嵌视图需要把焦点从编辑器拉回本块;全屏(Portal)下强制 focus
// 可能会抢走节点文本编辑的输入焦点,导致双击编辑不出光标。
if (!effectiveFullscreen) {
window.setTimeout(() => {
try {
wrapperRef.current?.focus?.({ preventScroll: true });
} catch {
// ignore
}
}, 0);
}
});
instance.on?.("node_click", (node: unknown) => {
// 确保点击即可激活(调用节点自身的 active 逻辑,保持和官方一致)
@@ -1499,13 +1494,17 @@ const MindmapBlockView = ({
);
hotkeyScopeRef.current = true;
lastInteractionAtRef.current = Date.now();
window.setTimeout(() => {
try {
wrapperRef.current?.focus?.({ preventScroll: true });
} catch {
// ignore
}
}, 0);
// 仅内嵌视图需要把焦点从编辑器拉回本块;全屏(Portal)下强制 focus
// 可能会抢走节点文本编辑的输入焦点,导致双击编辑不出光标。
if (!effectiveFullscreen) {
window.setTimeout(() => {
try {
wrapperRef.current?.focus?.({ preventScroll: true });
} catch {
// ignore
}
}, 0);
}
});
instance.on?.("painter_start", () => setPainterMode(true));
instance.on?.("painter_end", () => setPainterMode(false));
@@ -1601,10 +1600,16 @@ const MindmapBlockView = ({
window.__mindmapInstance = null;
}
try {
const w = window as unknown as { __mindmapInstancesById?: Record<string, MindMapInstance> };
const w = window as unknown as {
__mindmapInstancesById?: Record<string, MindMapInstance>;
__mindmapPersistById?: Record<string, (data: unknown) => void>;
};
if (w.__mindmapInstancesById && w.__mindmapInstancesById[mindmapId] === createdInstance) {
delete w.__mindmapInstancesById[mindmapId];
}
if (w.__mindmapPersistById && w.__mindmapPersistById[mindmapId]) {
delete w.__mindmapPersistById[mindmapId];
}
} catch {
// ignore
}
@@ -2392,7 +2397,7 @@ const MindmapBlockView = ({
>
<div className="fixed left-0 right-0 top-0 z-30 flex h-12 items-center justify-between border-b border-gray-200 bg-white/90 px-4 backdrop-blur">
<div className="text-sm font-medium text-gray-700">
{fullscreenApiActive ? "·真" : ""}
</div>
<button
type="button"
@@ -2421,6 +2426,8 @@ const MindmapBlockView = ({
onSelect={setActiveSidebar}
/>
<MindmapSidebar
documentId={docId}
mindmapId={mindmapId}
mindmap={mindmap}
activeNodes={activeNodes}
activeTab={activeSidebar}
@@ -2510,6 +2517,8 @@ const MindmapBlockView = ({
onSelect={setActiveSidebar}
/>
<MindmapSidebar
documentId={docId}
mindmapId={mindmapId}
mindmap={mindmap}
activeNodes={activeNodes}
activeTab={activeSidebar}