training_data/apps/web/src/components/common/editor/NodeMenu.tsx

196 lines
6.4 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;