130 lines
3.8 KiB
TypeScript
130 lines
3.8 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
import React, { useState, useEffect } from "react";
|
|
import { Minus, Plus, Maximize, Map, MapPin, Minimize, Eye, EyeOff } from "lucide-react";
|
|
|
|
type NavigatorProps = {
|
|
mindmap: any;
|
|
fullscreen: boolean;
|
|
toggleFullscreen?: () => void; // Optional for now
|
|
onToggleMiniMap?: () => void;
|
|
miniMapOpen?: boolean;
|
|
};
|
|
|
|
export const MindmapNavigator = ({
|
|
mindmap,
|
|
fullscreen,
|
|
toggleFullscreen,
|
|
onToggleMiniMap,
|
|
miniMapOpen,
|
|
}: NavigatorProps) => {
|
|
const [scale, setScale] = useState(100);
|
|
const [isReadonly, setIsReadonly] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!mindmap) return;
|
|
|
|
const handleScale = (s: number) => {
|
|
setScale(Math.round(s * 100));
|
|
};
|
|
|
|
mindmap.on("scale", handleScale);
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
setScale(Math.round(mindmap.view.scale * 100));
|
|
|
|
// Handle readonly state sync if needed
|
|
// mindmap.on("mode_change", (mode) => setIsReadonly(mode === 'readonly'));
|
|
|
|
return () => {
|
|
mindmap.off("scale", handleScale);
|
|
};
|
|
}, [mindmap]);
|
|
|
|
const handleZoomIn = () => mindmap?.view.enlarge();
|
|
const handleZoomOut = () => mindmap?.view.narrow();
|
|
const handleCenter = () => mindmap?.renderer.setRootNodeCenter();
|
|
|
|
const handleReadonly = () => {
|
|
const newMode = !isReadonly;
|
|
setIsReadonly(newMode);
|
|
mindmap?.setMode(newMode ? 'readonly' : 'edit');
|
|
};
|
|
|
|
const handleScaleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const val = parseInt(e.target.value);
|
|
if (!isNaN(val) && val > 0) {
|
|
setScale(val);
|
|
const cx = mindmap.width / 2;
|
|
const cy = mindmap.height / 2;
|
|
mindmap.view.setScale(val / 100, cx, cy);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="absolute bottom-4 right-4 flex items-center gap-2 rounded-lg border border-gray-200 bg-white p-2 shadow-md text-gray-600">
|
|
<button
|
|
onClick={handleCenter}
|
|
className="rounded p-1 hover:bg-gray-100"
|
|
title="回到中心"
|
|
>
|
|
<MapPin className="h-4 w-4" />
|
|
</button>
|
|
|
|
<div className="mx-1 h-4 w-px bg-gray-200" />
|
|
|
|
<button
|
|
onClick={onToggleMiniMap}
|
|
className={`rounded p-1 hover:bg-gray-100 ${miniMapOpen ? "bg-blue-50 text-blue-600" : ""}`}
|
|
title={miniMapOpen ? "关闭小地图" : "打开小地图"}
|
|
>
|
|
<Map className="h-4 w-4" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleReadonly}
|
|
className={`rounded p-1 hover:bg-gray-100 ${isReadonly ? "bg-blue-50 text-blue-600" : ""}`}
|
|
title={isReadonly ? "切换编辑模式" : "切换只读模式"}
|
|
>
|
|
{isReadonly ? <Eye className="h-4 w-4" /> : <EyeOff className="h-4 w-4" />}
|
|
</button>
|
|
|
|
{toggleFullscreen && (
|
|
<button
|
|
onClick={toggleFullscreen}
|
|
className="rounded p-1 hover:bg-gray-100"
|
|
title={fullscreen ? "退出全屏" : "全屏"}
|
|
>
|
|
{fullscreen ? <Minimize className="h-4 w-4" /> : <Maximize className="h-4 w-4" />}
|
|
</button>
|
|
)}
|
|
|
|
<div className="mx-1 h-4 w-px bg-gray-200" />
|
|
|
|
<button
|
|
onClick={handleZoomOut}
|
|
className="rounded p-1 hover:bg-gray-100"
|
|
title="缩小"
|
|
>
|
|
<Minus className="h-4 w-4" />
|
|
</button>
|
|
|
|
<div className="flex items-center text-xs">
|
|
<input
|
|
type="number"
|
|
value={scale}
|
|
onChange={handleScaleInput}
|
|
className="w-8 text-center bg-transparent outline-none appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
|
/>
|
|
<span>%</span>
|
|
</div>
|
|
|
|
<button
|
|
onClick={handleZoomIn}
|
|
className="rounded p-1 hover:bg-gray-100"
|
|
title="放大"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
);
|
|
};
|