0.1.02思维导图全屏/lightrag修复/luckysheet修复
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import "simple-mind-map/dist/simpleMindMap.esm.css";
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { createReactBlockSpec } from "@blocknote/react";
|
||||
import type { Block, BlockNoteEditor } from "@blocknote/core";
|
||||
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
||||
@@ -230,6 +238,9 @@ const MindmapBlockView = ({
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [mindmap, setMindmap] = useState<MindMapInstance | null>(null);
|
||||
const mindmapReadyRef = useRef(false);
|
||||
const mindmapRef = useRef<MindMapInstance | null>(null);
|
||||
const hasLocalEditsRef = useRef(false);
|
||||
const applyingRemoteRef = useRef(false);
|
||||
const [canBack, setCanBack] = useState(false);
|
||||
const [canForward, setCanForward] = useState(false);
|
||||
const [activeNodes, setActiveNodes] = useState<unknown[]>([]);
|
||||
@@ -238,6 +249,12 @@ const MindmapBlockView = ({
|
||||
const [activeSidebar, setActiveSidebar] = useState<SidebarPanel | null>(null);
|
||||
const [showNoteModal, setShowNoteModal] = useState(false);
|
||||
const [noteContent, setNoteContent] = useState("");
|
||||
const [localFullscreen, setLocalFullscreen] = useState(false);
|
||||
const effectiveFullscreen = fullscreen || localFullscreen;
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const hotkeyScopeRef = useRef(false);
|
||||
const recentNodeDblclickRef = useRef(false);
|
||||
const [fullscreenApiActive, setFullscreenApiActive] = useState(false);
|
||||
|
||||
const getRootNode = useCallback((mm?: MindMapInstance | null) => {
|
||||
const instance = mm ?? mindmap;
|
||||
@@ -254,6 +271,11 @@ const MindmapBlockView = ({
|
||||
[block.props.docId, currentDocumentId],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
hasLocalEditsRef.current = false;
|
||||
applyingRemoteRef.current = false;
|
||||
}, [docId]);
|
||||
|
||||
const autosaveKey = useMemo(
|
||||
() => `${STORAGE_PREFIX}${docId || block.id}`,
|
||||
[block.id, docId],
|
||||
@@ -283,10 +305,19 @@ const MindmapBlockView = ({
|
||||
const payload = await resp.json().catch(() => null);
|
||||
const data = payload?.data;
|
||||
if (!data || cancelled) return;
|
||||
// 若用户已在本地做过编辑,则不再用远端数据覆盖,避免“插入节点后又被重置”
|
||||
if (hasLocalEditsRef.current) return;
|
||||
initialDataRef.current = data;
|
||||
if (mindmap) {
|
||||
mindmap.setData(data);
|
||||
mindmap.command.clearHistory();
|
||||
applyingRemoteRef.current = true;
|
||||
try {
|
||||
mindmap.setData(data);
|
||||
mindmap.command.clearHistory();
|
||||
} finally {
|
||||
window.setTimeout(() => {
|
||||
applyingRemoteRef.current = false;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("加载本地/远端思维导图失败", error);
|
||||
@@ -297,6 +328,220 @@ const MindmapBlockView = ({
|
||||
};
|
||||
}, [docId, mindmap]);
|
||||
|
||||
// 记录“最近一次指针交互是否发生在思维导图块内”,用于键盘快捷键作用域
|
||||
useEffect(() => {
|
||||
const onPointerDownCapture = (e: Event) => {
|
||||
const wrapper = wrapperRef.current;
|
||||
if (!wrapper) return;
|
||||
const target = e.target as Node | null;
|
||||
hotkeyScopeRef.current = !!(target && wrapper.contains(target));
|
||||
};
|
||||
document.addEventListener("pointerdown", onPointerDownCapture, true);
|
||||
document.addEventListener("mousedown", onPointerDownCapture, true);
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", onPointerDownCapture, true);
|
||||
document.removeEventListener("mousedown", onPointerDownCapture, true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 标记“节点双击”事件,避免和“画布双击进入全屏”产生冲突
|
||||
useEffect(() => {
|
||||
if (!mindmap) return;
|
||||
const mark = () => {
|
||||
recentNodeDblclickRef.current = true;
|
||||
window.setTimeout(() => {
|
||||
recentNodeDblclickRef.current = false;
|
||||
}, 0);
|
||||
};
|
||||
mindmap.on?.("node_dblclick", mark);
|
||||
return () => {
|
||||
mindmap.off?.("node_dblclick", mark);
|
||||
};
|
||||
}, [mindmap]);
|
||||
|
||||
const exitLocalFullscreen = useCallback(() => {
|
||||
setActiveSidebar(null);
|
||||
if (fullscreen) return;
|
||||
if (typeof document === "undefined") {
|
||||
setLocalFullscreen(false);
|
||||
return;
|
||||
}
|
||||
if (document.fullscreenElement) {
|
||||
document
|
||||
.exitFullscreen()
|
||||
.catch(() => {
|
||||
// ignore
|
||||
})
|
||||
.finally(() => setLocalFullscreen(false));
|
||||
return;
|
||||
}
|
||||
setLocalFullscreen(false);
|
||||
}, [fullscreen]);
|
||||
|
||||
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 any;
|
||||
const p = target?.requestFullscreen?.();
|
||||
if (p && typeof p.catch === "function") {
|
||||
p.catch(() => {
|
||||
// ignore:失败则保持 Portal 伪全屏
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// ignore:失败则保持 Portal 伪全屏
|
||||
}
|
||||
}, [fullscreen]);
|
||||
|
||||
// 沉浸式全屏:锁定页面滚动、支持 ESC 退出(仅对内嵌全屏生效)
|
||||
useEffect(() => {
|
||||
if (!localFullscreen) return;
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
const prevOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
// 进入全屏后尽量把焦点放到思维导图容器上,保证快捷键立即生效
|
||||
queueMicrotask(() => {
|
||||
try {
|
||||
wrapperRef.current?.focus?.({ preventScroll: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key !== "Escape") return;
|
||||
if (fullscreen) return; // 外部传入的 fullscreen 不在这里处理
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
exitLocalFullscreen();
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", onKeyDown, true);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDown, true);
|
||||
document.body.style.overflow = prevOverflow;
|
||||
};
|
||||
}, [localFullscreen, fullscreen, exitLocalFullscreen]);
|
||||
|
||||
// “真全屏”:使用 Fullscreen API 隐藏浏览器/桌面窗口的外层 UI(Electron/Web 都可用)
|
||||
useEffect(() => {
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
const onFsChange = () => {
|
||||
const active = Boolean(document.fullscreenElement);
|
||||
setFullscreenApiActive(active);
|
||||
// 用户按 ESC 退出浏览器全屏时,同步退出沉浸式全屏界面
|
||||
if (!active && localFullscreen) {
|
||||
setLocalFullscreen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("fullscreenchange", onFsChange);
|
||||
return () => {
|
||||
document.removeEventListener("fullscreenchange", onFsChange);
|
||||
};
|
||||
}, [localFullscreen]);
|
||||
|
||||
// 让 Enter/Tab/复制/粘贴在“思维导图块”内生效(避免 BlockNote 抢走快捷键)
|
||||
useLayoutEffect(() => {
|
||||
const onKeyDownCapture = (e: KeyboardEvent) => {
|
||||
if (!hotkeyScopeRef.current) return;
|
||||
const mm = mindmapRef.current;
|
||||
if (!mm || !mindmapReadyRef.current) return;
|
||||
|
||||
const target = e.target as HTMLElement | null;
|
||||
const editClasses = (mm as any)?.editNodeClassList as string[] | undefined;
|
||||
const wrapper = wrapperRef.current;
|
||||
const isInWrapper = Boolean(wrapper && target && wrapper.contains(target));
|
||||
if (target && isInWrapper) {
|
||||
const tag = target.tagName;
|
||||
if (tag === "INPUT" || tag === "TEXTAREA") return;
|
||||
// contenteditable 输入中不拦截:避免破坏节点文本编辑/粘贴
|
||||
if ((target as any).isContentEditable) return;
|
||||
const editableAncestor = target.closest?.('[contenteditable="true"]');
|
||||
if (editableAncestor) return;
|
||||
if (editClasses && target.classList) {
|
||||
for (const cls of editClasses) {
|
||||
if (cls && target.classList.contains(cls)) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isMod = e.ctrlKey || e.metaKey;
|
||||
const key = e.key;
|
||||
const lower = key.toLowerCase();
|
||||
const stop = () => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
const pickTargetNode = (inst: MindMapInstance) => {
|
||||
const renderer = inst.renderer as any;
|
||||
const active: any[] = renderer?.activeNodeList ?? [];
|
||||
const last: any[] = renderer?.lastActiveNodeList ?? [];
|
||||
const root = renderer?.root ?? renderer?.renderTree?._node ?? null;
|
||||
return (active && active[0]) || (last && last[0]) || root || null;
|
||||
};
|
||||
|
||||
if (key === "Enter" && !e.shiftKey && !e.altKey && !isMod) {
|
||||
stop();
|
||||
const inst = ensureActiveBefore(mm);
|
||||
if (!inst) return;
|
||||
const node = pickTargetNode(inst);
|
||||
if (!node) return;
|
||||
inst.execCommand?.("SET_NODE_ACTIVE", node, true);
|
||||
inst.execCommand?.("INSERT_NODE", false, [node]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "Tab" && !e.shiftKey && !e.altKey && !isMod) {
|
||||
stop();
|
||||
const inst = ensureActiveBefore(mm);
|
||||
if (!inst) return;
|
||||
const node = pickTargetNode(inst);
|
||||
if (!node) return;
|
||||
inst.execCommand?.("SET_NODE_ACTIVE", node, true);
|
||||
inst.execCommand?.("INSERT_CHILD_NODE", false, [node]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMod && lower === "c") {
|
||||
stop();
|
||||
const inst = ensureActiveBefore(mm);
|
||||
if (!inst) return;
|
||||
const renderer = inst.renderer as any;
|
||||
if (typeof renderer?.copy === "function") renderer.copy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMod && lower === "v") {
|
||||
stop();
|
||||
const inst = ensureActiveBefore(mm);
|
||||
if (!inst) return;
|
||||
const renderer = inst.renderer as any;
|
||||
const copyData = renderer?.beingCopyData ?? null;
|
||||
if (copyData) {
|
||||
inst.execCommand?.("PASTE_NODE", copyData);
|
||||
return;
|
||||
}
|
||||
if (typeof renderer?.paste === "function") renderer.paste();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", onKeyDownCapture, true);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKeyDownCapture, true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const persistData = useCallback(
|
||||
(data: unknown) => {
|
||||
if (!editor) return;
|
||||
@@ -328,7 +573,7 @@ const MindmapBlockView = ({
|
||||
|
||||
const debouncedPersist = useDebouncedCallback((data: unknown) => {
|
||||
persistData(data);
|
||||
}, 500);
|
||||
}, 800);
|
||||
|
||||
const initialSyncDone = useRef(false);
|
||||
useEffect(() => {
|
||||
@@ -559,6 +804,7 @@ const MindmapBlockView = ({
|
||||
]),
|
||||
}) as MindMapInstance;
|
||||
createdInstance = instance;
|
||||
mindmapRef.current = instance;
|
||||
// 尽早标记为可用:主页面通过 `/mind` 插入后,若依赖 render_end/尺寸探测,可能导致按钮永远不可用
|
||||
//(render_end 可能早于监听注册触发,或容器尺寸长时间为 0)。命令本身不需要等待 render_end。
|
||||
mindmapReadyRef.current = true;
|
||||
@@ -629,7 +875,6 @@ const MindmapBlockView = ({
|
||||
if (renderer?.emitNodeActiveEvent) renderer.emitNodeActiveEvent(root);
|
||||
}
|
||||
mindmapReadyRef.current = true;
|
||||
console.log("[mindmap] render_end activeLen", renderer?.activeNodeList?.length ?? 0);
|
||||
centerAndFit();
|
||||
};
|
||||
|
||||
@@ -673,6 +918,9 @@ const MindmapBlockView = ({
|
||||
instance.on?.("painter_start", () => setPainterMode(true));
|
||||
instance.on?.("painter_end", () => setPainterMode(false));
|
||||
instance.on?.("data_change", (data: unknown) => {
|
||||
if (!applyingRemoteRef.current) {
|
||||
hasLocalEditsRef.current = true;
|
||||
}
|
||||
debouncedPersist(data);
|
||||
});
|
||||
const renderer = instance.renderer;
|
||||
@@ -682,7 +930,6 @@ const MindmapBlockView = ({
|
||||
renderer?.clearActiveNodeList && renderer.clearActiveNodeList();
|
||||
renderer?.addNodeToActiveList && renderer.addNodeToActiveList(rootNode, true);
|
||||
renderer?.emitNodeActiveEvent && renderer.emitNodeActiveEvent(rootNode);
|
||||
console.log("[mindmap] init select root", instance.renderer?.activeNodeList?.length ?? 0);
|
||||
setActiveNodes([rootNode]);
|
||||
}
|
||||
// 若首次渲染时 root 仍未就绪,设置兜底延迟激活
|
||||
@@ -692,7 +939,6 @@ const MindmapBlockView = ({
|
||||
renderer?.clearActiveNodeList && renderer.clearActiveNodeList();
|
||||
renderer?.addNodeToActiveList && renderer.addNodeToActiveList(root, true);
|
||||
renderer?.emitNodeActiveEvent && renderer.emitNodeActiveEvent(root);
|
||||
console.log("[mindmap] delayed select root", instance.renderer?.activeNodeList?.length ?? 0);
|
||||
setActiveNodes([root]);
|
||||
}, 120);
|
||||
|
||||
@@ -704,6 +950,32 @@ const MindmapBlockView = ({
|
||||
return () => {
|
||||
destroyed = true;
|
||||
try {
|
||||
// 切换“内嵌/全屏”会导致实例重建:这里尽量在销毁前同步一次数据,避免丢失最后一次编辑
|
||||
if (createdInstance && typeof window !== "undefined") {
|
||||
const data =
|
||||
createdInstance.getData?.(true) ?? createdInstance.getData?.();
|
||||
if (data) {
|
||||
try {
|
||||
window.localStorage.setItem(autosaveKey, JSON.stringify(data));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
editor?.updateBlock(block, { props: { ...block.props, data } });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (docId) {
|
||||
fetch(`/api/mindmap/${docId}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ data }),
|
||||
}).catch(() => {
|
||||
// ignore
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
createdInstance?.destroy();
|
||||
} catch {
|
||||
// ignore
|
||||
@@ -716,10 +988,11 @@ const MindmapBlockView = ({
|
||||
}
|
||||
}
|
||||
setMindmap(null);
|
||||
mindmapRef.current = null;
|
||||
mindmapReadyRef.current = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [block.id]);
|
||||
}, [block.id, effectiveFullscreen]);
|
||||
|
||||
const getInstanceCandidate = () =>
|
||||
mindmap ??
|
||||
@@ -1463,33 +1736,61 @@ const MindmapBlockView = ({
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
if (fullscreen) {
|
||||
return (
|
||||
<div className="relative h-screen w-screen overflow-hidden bg-white">
|
||||
<div className="fixed left-1/2 top-4 z-20 flex w-fit -translate-x-1/2 transform items-center justify-center gap-4">
|
||||
if (effectiveFullscreen) {
|
||||
const fullscreenView = (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
data-testid="mindmap-fullscreen"
|
||||
tabIndex={0}
|
||||
className="fixed inset-0 z-[9999] overflow-hidden bg-white"
|
||||
>
|
||||
<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"
|
||||
title="退出全屏"
|
||||
className="rounded p-2 text-gray-500 hover:bg-gray-100 hover:text-gray-700"
|
||||
onClick={exitLocalFullscreen}
|
||||
>
|
||||
<span className="text-lg leading-none">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="fixed left-1/2 top-14 z-20 flex w-fit -translate-x-1/2 transform items-center justify-center gap-4">
|
||||
<MindmapToolbar {...toolbarProps} />
|
||||
</div>
|
||||
<div className="relative h-full w-full">
|
||||
<div className="relative h-full w-full pt-12">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="h-full w-full"
|
||||
data-testid="mindmap-canvas"
|
||||
contentEditable={false}
|
||||
/>
|
||||
|
||||
<MindmapSidebarTrigger
|
||||
activeSidebar={activeSidebar}
|
||||
onSelect={setActiveSidebar}
|
||||
|
||||
<MindmapSidebarTrigger
|
||||
activeSidebar={activeSidebar}
|
||||
onSelect={setActiveSidebar}
|
||||
/>
|
||||
{/* @ts-expect-error mindmap type */}
|
||||
<MindmapSidebar
|
||||
mindmap={mindmap}
|
||||
<MindmapSidebar
|
||||
mindmap={mindmap}
|
||||
activeNodes={activeNodes} // @ts-expect-error type
|
||||
activeTab={activeSidebar}
|
||||
onClose={() => setActiveSidebar(null)}
|
||||
/>
|
||||
<MindmapNavigator
|
||||
mindmap={mindmap}
|
||||
fullscreen={fullscreen}
|
||||
fullscreen={effectiveFullscreen}
|
||||
toggleFullscreen={
|
||||
fullscreen
|
||||
? undefined
|
||||
: () => {
|
||||
if (localFullscreen) exitLocalFullscreen();
|
||||
else enterLocalFullscreen();
|
||||
}
|
||||
}
|
||||
onToggleMiniMap={() => setShowMiniMap(!showMiniMap)}
|
||||
miniMapOpen={showMiniMap}
|
||||
/>
|
||||
@@ -1503,19 +1804,53 @@ const MindmapBlockView = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (typeof document === "undefined") return null;
|
||||
return createPortal(fullscreenView, document.body);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="not-prose my-4 w-full overflow-hidden rounded-xl border border-gray-200 bg-[#f8fafc] shadow-sm">
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
data-testid="mindmap-embed"
|
||||
tabIndex={0}
|
||||
className="not-prose my-4 w-full overflow-hidden rounded-xl border border-gray-200 bg-[#f8fafc] shadow-sm"
|
||||
>
|
||||
<div className="flex flex-col gap-3 border-b border-gray-100 bg-white px-4 py-3">
|
||||
<div className="w-full">
|
||||
<MindmapToolbar {...toolbarProps} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative h-[520px] w-full overflow-hidden bg-white">
|
||||
<div
|
||||
data-testid="mindmap-stage"
|
||||
className="relative h-[520px] w-full overflow-hidden bg-white"
|
||||
onPointerDown={() => {
|
||||
hotkeyScopeRef.current = true;
|
||||
// 点击后把焦点落到容器上,避免快捷键被编辑器抢走
|
||||
try {
|
||||
wrapperRef.current?.focus?.({ preventScroll: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}}
|
||||
onMouseDown={() => {
|
||||
hotkeyScopeRef.current = true;
|
||||
// 兼容:某些环境下 pointer 事件不触发
|
||||
try {
|
||||
wrapperRef.current?.focus?.({ preventScroll: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
if (recentNodeDblclickRef.current) return;
|
||||
enterLocalFullscreen();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="h-full w-full"
|
||||
data-testid="mindmap-canvas"
|
||||
contentEditable={false}
|
||||
/>
|
||||
|
||||
@@ -1532,7 +1867,15 @@ const MindmapBlockView = ({
|
||||
/>
|
||||
<MindmapNavigator
|
||||
mindmap={mindmap}
|
||||
fullscreen={fullscreen}
|
||||
fullscreen={effectiveFullscreen}
|
||||
toggleFullscreen={
|
||||
fullscreen
|
||||
? undefined
|
||||
: () => {
|
||||
if (localFullscreen) exitLocalFullscreen();
|
||||
else enterLocalFullscreen();
|
||||
}
|
||||
}
|
||||
onToggleMiniMap={() => setShowMiniMap(!showMiniMap)}
|
||||
miniMapOpen={showMiniMap}
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,7 @@ type Props = {
|
||||
onSelect: (value: SidebarPanel | null) => void;
|
||||
};
|
||||
|
||||
export const MindmapSidebarTrigger = ({ activeSidebar, onSelect }: Props) => {
|
||||
export const MindmapSidebarTrigger = ({ activeSidebar, onSelect }: Props) => {
|
||||
const [show, setShow] = React.useState(true);
|
||||
|
||||
return (
|
||||
@@ -15,35 +15,64 @@ export const MindmapSidebarTrigger = ({ activeSidebar, onSelect }: Props) => {
|
||||
className={`absolute top-1/2 z-30 flex -translate-y-1/2 transition-all duration-300 ${
|
||||
activeSidebar ? "right-[300px]" : "right-0"
|
||||
}`}
|
||||
contentEditable={false}
|
||||
onPointerDownCapture={(e) => e.stopPropagation()}
|
||||
onMouseDownCapture={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
className="absolute -left-4 top-1/2 flex h-12 w-4 -translate-y-1/2 cursor-pointer items-center justify-center rounded-l-md bg-blue-500 text-white shadow-md hover:w-6 hover:-left-6 transition-all"
|
||||
onClick={() => setShow(!show)}
|
||||
style={{ display: show ? "flex" : "none" }}
|
||||
data-testid="mindmap-sidebar-collapse-handle"
|
||||
className="absolute -left-4 top-1/2 flex h-12 w-4 -translate-y-1/2 cursor-pointer items-center justify-center rounded-l-md bg-blue-500 text-white shadow-md transition-all hover:-left-6 hover:w-6"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShow((prev) => {
|
||||
const next = !prev;
|
||||
if (!next) {
|
||||
// 避免在 setShow 的 updater 内触发父组件 setState,导致 React 警告
|
||||
queueMicrotask(() => onSelect(null));
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
<ChevronRight
|
||||
className={`h-3 w-3 transition-transform ${show ? "" : "rotate-180"}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col overflow-hidden rounded-l-lg border border-gray-200 bg-white shadow-lg">
|
||||
{sidebarTriggers
|
||||
.filter((item) => item.visible !== false)
|
||||
.map((item) => {
|
||||
const target = item.target;
|
||||
const isActive = activeSidebar === target;
|
||||
return (
|
||||
<button
|
||||
key={item.value}
|
||||
onClick={() => onSelect(isActive ? null : target)}
|
||||
className={`flex h-16 w-16 flex-col items-center justify-center gap-1 border-b border-gray-100 p-2 text-gray-600 transition-colors hover:bg-gray-50 last:border-0 ${
|
||||
isActive ? "bg-blue-50 text-blue-600 font-medium" : ""
|
||||
}`}
|
||||
>
|
||||
<i className={`iconfont ${item.iconClass} text-[18px] leading-none`} />
|
||||
<span className="text-xs whitespace-nowrap">{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{show ? (
|
||||
<div
|
||||
data-testid="mindmap-sidebar-trigger-panel"
|
||||
className="flex flex-col overflow-hidden rounded-l-lg border border-gray-200 bg-white shadow-lg"
|
||||
>
|
||||
{sidebarTriggers
|
||||
.filter((item) => item.visible !== false)
|
||||
.map((item) => {
|
||||
const target = item.target;
|
||||
const isActive = activeSidebar === target;
|
||||
return (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
contentEditable={false}
|
||||
onPointerDown={(ev) => ev.stopPropagation()}
|
||||
onMouseDown={(ev) => ev.stopPropagation()}
|
||||
onClick={(ev) => {
|
||||
ev.stopPropagation();
|
||||
onSelect(isActive ? null : target);
|
||||
}}
|
||||
className={`flex h-16 w-16 flex-col items-center justify-center gap-1 border-b border-gray-100 p-2 text-gray-600 transition-colors hover:bg-gray-50 last:border-0 ${
|
||||
isActive ? "bg-blue-50 font-medium text-blue-600" : ""
|
||||
}`}
|
||||
>
|
||||
<i
|
||||
className={`iconfont ${item.iconClass} text-[18px] leading-none`}
|
||||
/>
|
||||
<span className="whitespace-nowrap text-xs">{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user