196 lines
6.4 KiB
TypeScript
Executable File
196 lines
6.4 KiB
TypeScript
Executable File
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;
|