196 lines
6.4 KiB
TypeScript
196 lines
6.4 KiB
TypeScript
![]() |
import React, { useState, useEffect, useRef } from 'react';
|
|||
|
import { Input, Button, ColorPicker, Select } from 'antd';
|
|||
|
import {
|
|||
|
FontSizeOutlined,
|
|||
|
BoldOutlined,
|
|||
|
LinkOutlined,
|
|||
|
} from '@ant-design/icons';
|
|||
|
import type { MindElixirInstance, NodeObj } from 'mind-elixir';
|
|||
|
|
|||
|
const xmindColorPresets = [
|
|||
|
// 经典16色
|
|||
|
'#FFFFFF', '#F5F5F5', // 白色系
|
|||
|
'#2196F3', '#1976D2', // 蓝色系
|
|||
|
'#4CAF50', '#388E3C', // 绿色系
|
|||
|
'#FF9800', '#F57C00', // 橙色系
|
|||
|
'#F44336', '#D32F2F', // 红色系
|
|||
|
'#9C27B0', '#7B1FA2', // 紫色系
|
|||
|
'#424242', '#757575', // 灰色系
|
|||
|
'#FFEB3B', '#FBC02D' // 黄色系
|
|||
|
];
|
|||
|
|
|||
|
interface NodeMenuProps {
|
|||
|
mind: MindElixirInstance;
|
|||
|
}
|
|||
|
|
|||
|
const NodeMenu: React.FC<NodeMenuProps> = ({ mind }) => {
|
|||
|
const [isOpen, setIsOpen] = useState(false);
|
|||
|
const [selectedFontColor, setSelectedFontColor] = useState<string>('');
|
|||
|
const [selectedBgColor, setSelectedBgColor] = useState<string>('');
|
|||
|
const [selectedSize, setSelectedSize] = useState<string>('');
|
|||
|
const [isBold, setIsBold] = useState(false);
|
|||
|
const [url, setUrl] = useState<string>('');
|
|||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|||
|
|
|||
|
useEffect(() => {
|
|||
|
const handleSelectNode = (nodeObj: NodeObj) => {
|
|||
|
setIsOpen(true);
|
|||
|
|
|||
|
const style = nodeObj.style || {};
|
|||
|
setSelectedFontColor(style.color || '');
|
|||
|
setSelectedBgColor(style.background || '');
|
|||
|
|
|||
|
setSelectedSize(style.fontSize || '24');
|
|||
|
setIsBold(style.fontWeight === 'bold');
|
|||
|
setUrl(nodeObj.hyperLink || '');
|
|||
|
};
|
|||
|
|
|||
|
const handleUnselectNode = () => {
|
|||
|
setIsOpen(false);
|
|||
|
};
|
|||
|
|
|||
|
mind.bus.addListener('selectNode', handleSelectNode);
|
|||
|
mind.bus.addListener('unselectNode', handleUnselectNode);
|
|||
|
|
|||
|
}, [mind]);
|
|||
|
|
|||
|
useEffect(() => {
|
|||
|
if (containerRef.current && mind.container) {
|
|||
|
mind.container.appendChild(containerRef.current);
|
|||
|
}
|
|||
|
|
|||
|
}, [mind.container]);
|
|||
|
|
|||
|
const handleColorChange = (type: "font" | "background", color: string) => {
|
|||
|
if (type === 'font') {
|
|||
|
setSelectedFontColor(color);
|
|||
|
} else {
|
|||
|
setSelectedBgColor(color);
|
|||
|
}
|
|||
|
const patch = { style: {} as any };
|
|||
|
if (type === 'font') {
|
|||
|
patch.style.color = color;
|
|||
|
} else {
|
|||
|
patch.style.background = color;
|
|||
|
}
|
|||
|
mind.reshapeNode(mind.currentNode, patch);
|
|||
|
};
|
|||
|
|
|||
|
const handleSizeChange = (size: string) => {
|
|||
|
setSelectedSize(size);
|
|||
|
mind.reshapeNode(mind.currentNode, { style: { fontSize: size } });
|
|||
|
};
|
|||
|
|
|||
|
const handleBoldToggle = () => {
|
|||
|
const fontWeight = isBold ? '' : 'bold';
|
|||
|
setIsBold(!isBold);
|
|||
|
mind.reshapeNode(mind.currentNode, { style: { fontWeight } });
|
|||
|
};
|
|||
|
|
|||
|
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|||
|
const value = e.target.value;
|
|||
|
setUrl(value);
|
|||
|
mind.reshapeNode(mind.currentNode, { hyperLink: value });
|
|||
|
};
|
|||
|
|
|||
|
return (
|
|||
|
<div
|
|||
|
className={`node-menu-container absolute right-2 top-2 rounded-lg bg-slate-200 shadow-xl ring-2 ring-white transition-all duration-300 ${isOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4 pointer-events-none'
|
|||
|
}`}
|
|||
|
ref={containerRef}
|
|||
|
>
|
|||
|
<div className="p-5 space-y-6">
|
|||
|
{/* Font Size Selector */}
|
|||
|
<div className="space-y-2">
|
|||
|
<h3 className="text-sm font-medium text-gray-600">文字样式</h3>
|
|||
|
<div className="flex gap-3 items-center justify-between">
|
|||
|
<Select
|
|||
|
value={selectedSize}
|
|||
|
onChange={handleSizeChange}
|
|||
|
prefix={<FontSizeOutlined className='mr-2' />}
|
|||
|
className="w-1/2"
|
|||
|
options={[
|
|||
|
{ value: '12', label: '12' },
|
|||
|
{ value: '14', label: '14' },
|
|||
|
{ value: '16', label: '16' },
|
|||
|
{ value: '18', label: '18' },
|
|||
|
{ value: '20', label: '20' },
|
|||
|
{ value: '24', label: '24' },
|
|||
|
{ value: '28', label: '28' },
|
|||
|
{ value: '32', label: '32' }
|
|||
|
]}
|
|||
|
/>
|
|||
|
<Button
|
|||
|
type={isBold ? "primary" : "default"}
|
|||
|
onClick={handleBoldToggle}
|
|||
|
className='w-1/2'
|
|||
|
icon={<BoldOutlined />}
|
|||
|
>
|
|||
|
|
|||
|
加粗
|
|||
|
</Button>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
{/* Color Picker */}
|
|||
|
<div className="space-y-3">
|
|||
|
<h3 className="text-sm font-medium text-gray-600">颜色设置</h3>
|
|||
|
|
|||
|
{/* Font Color Picker */}
|
|||
|
<div className="space-y-2">
|
|||
|
<h4 className="text-xs font-medium text-gray-500">文字颜色</h4>
|
|||
|
<div className="grid grid-cols-8 gap-2 p-2 bg-gray-50 rounded-lg">
|
|||
|
{xmindColorPresets.map((color) => (
|
|||
|
<div
|
|||
|
key={`font-${color}`}
|
|||
|
className={`w-6 h-6 rounded-full cursor-pointer hover:scale-105 transition-transform outline outline-2 outline-offset-1 ${selectedFontColor === color ? 'outline-blue-500' : 'outline-transparent'
|
|||
|
}`}
|
|||
|
style={{ backgroundColor: color }}
|
|||
|
onClick={() => {
|
|||
|
|
|||
|
handleColorChange('font', color);
|
|||
|
}}
|
|||
|
/>
|
|||
|
))}
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
{/* Background Color Picker */}
|
|||
|
<div className="space-y-2">
|
|||
|
<h4 className="text-xs font-medium text-gray-500">背景颜色</h4>
|
|||
|
<div className="grid grid-cols-8 gap-2 p-2 bg-gray-50 rounded-lg">
|
|||
|
{xmindColorPresets.map((color) => (
|
|||
|
<div
|
|||
|
key={`bg-${color}`}
|
|||
|
className={`w-6 h-6 rounded-full cursor-pointer hover:scale-105 transition-transform outline outline-2 outline-offset-1 ${selectedBgColor === color ? 'outline-blue-500' : 'outline-transparent'
|
|||
|
}`}
|
|||
|
style={{ backgroundColor: color }}
|
|||
|
onClick={() => {
|
|||
|
handleColorChange('background', color);
|
|||
|
}}
|
|||
|
/>
|
|||
|
))}
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<h3 className="text-sm font-medium text-gray-600">关联链接</h3>
|
|||
|
{/* URL Input */}
|
|||
|
<div className="space-y-1">
|
|||
|
<Input
|
|||
|
placeholder="例如:https://example.com"
|
|||
|
value={url}
|
|||
|
onChange={handleUrlChange}
|
|||
|
addonBefore={<LinkOutlined />}
|
|||
|
/>
|
|||
|
{url && !/^https?:\/\/\S+$/.test(url) && (
|
|||
|
<p className="text-xs text-red-500">请输入有效的URL地址</p>
|
|||
|
)}
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
);
|
|||
|
};
|
|||
|
|
|||
|
export default NodeMenu;
|