新增图标
This commit is contained in:
@@ -33,6 +33,15 @@ import {
|
||||
layoutList,
|
||||
outlineDepthOptions,
|
||||
} from "./mindmapOptions";
|
||||
import iconConfig from "./mindmapIconConfig";
|
||||
import imageConfig from "./mindmapImageConfig";
|
||||
// 避免 SSR 阶段触发浏览器依赖,改为运行时动态引入
|
||||
// @ts-expect-error 第三方库缺少类型定义
|
||||
const loadIconModules = async () => {
|
||||
const { nodeIconList } = await import("simple-mind-map/src/svg/icons.js");
|
||||
const { mergerIconList } = await import("simple-mind-map/src/utils/index.js");
|
||||
return { nodeIconList, mergerIconList };
|
||||
};
|
||||
|
||||
type MindMapNode = {
|
||||
getStyle: (prop: string, checkRoot?: boolean) => any;
|
||||
@@ -407,55 +416,150 @@ const StructurePanel = ({ mindmap }: { mindmap: any }) => {
|
||||
}
|
||||
|
||||
const IconPanel = ({ activeNodes }: { activeNodes: MindMapNode[] }) => {
|
||||
const addIcon = (type: string, name: string) => {
|
||||
// simple-mind-map icon key format usually: type_name (e.g. priority_1)
|
||||
const key = `${type}_${name}`;
|
||||
activeNodes.forEach(node => {
|
||||
const icons = node.getData('icon') || [];
|
||||
// Remove existing icon of same type
|
||||
const newIcons = icons.filter((i: string) => !i.startsWith(type + '_'));
|
||||
newIcons.push(key);
|
||||
node.setIcon(newIcons);
|
||||
});
|
||||
const [activeSubTab, setActiveSubTab] = useState<"icon" | "sticker">("icon");
|
||||
const [{ iconGroups, loading }, setIconGroups] = useState<{
|
||||
iconGroups: any[];
|
||||
loading: boolean;
|
||||
}>({ iconGroups: [], loading: true });
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
const { nodeIconList, mergerIconList } = await loadIconModules();
|
||||
if (!mounted) return;
|
||||
setIconGroups({
|
||||
iconGroups: mergerIconList([
|
||||
...nodeIconList,
|
||||
...(iconConfig as unknown as any[]),
|
||||
]),
|
||||
loading: false,
|
||||
});
|
||||
})();
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
const [stickerGroups] = useState(imageConfig);
|
||||
|
||||
const removeIcon = (type: string) => {
|
||||
activeNodes.forEach(node => {
|
||||
const icons = node.getData('icon') || [];
|
||||
const newIcons = icons.filter((i: string) => !i.startsWith(type + '_'));
|
||||
node.setIcon(newIcons);
|
||||
});
|
||||
}
|
||||
const addIcon = (type: string, name: string) => {
|
||||
const key = `${type}_${name}`;
|
||||
activeNodes.forEach((node) => {
|
||||
const icons = node.getData("icon") || [];
|
||||
const newIcons = icons.filter((i: string) => !i.startsWith(`${type}_`));
|
||||
newIcons.push(key);
|
||||
node.setIcon(newIcons);
|
||||
});
|
||||
};
|
||||
|
||||
if (activeNodes.length === 0) return <div className="p-4 text-center text-gray-400">请选择节点</div>;
|
||||
const removeIcon = (type: string) => {
|
||||
activeNodes.forEach((node) => {
|
||||
const icons = node.getData("icon") || [];
|
||||
const newIcons = icons.filter((i: string) => !i.startsWith(`${type}_`));
|
||||
node.setIcon(newIcons);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 py-2">
|
||||
{predefinedIcons.map(group => (
|
||||
<div key={group.type}>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Label className="text-xs font-bold text-gray-600">{group.name}</Label>
|
||||
<button onClick={() => removeIcon(group.type)} className="text-[10px] text-red-500 hover:underline">清除</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{group.list.map(item => (
|
||||
<button
|
||||
key={item}
|
||||
onClick={() => addIcon(group.type, item)}
|
||||
className="flex h-8 w-8 items-center justify-center rounded border border-gray-200 bg-gray-50 text-xs hover:bg-blue-50 hover:border-blue-300"
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
const setSticker = (img: { url: string; width?: number; height?: number }) => {
|
||||
activeNodes.forEach((node) => {
|
||||
// simple-mind-map 支持 setImage 接收对象,包含 url/width/height
|
||||
// @ts-expect-error 第三方库无类型
|
||||
node.setImage({
|
||||
url: img.url,
|
||||
width: img.width || 100,
|
||||
height: img.height || 100,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const clearSticker = () => {
|
||||
activeNodes.forEach((node) => {
|
||||
// 传入 null 以清除贴纸
|
||||
// @ts-expect-error 第三方库无类型
|
||||
node.setImage(null);
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) return <div className="p-4 text-center text-gray-400">图标加载中…</div>;
|
||||
if (iconGroups.length === 0) return <div className="p-4 text-center text-gray-400">暂无图标</div>;
|
||||
if (activeNodes.length === 0) return <div className="p-4 text-center text-gray-400">请选择节点</div>;
|
||||
|
||||
return (
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="flex gap-2 px-1">
|
||||
<button
|
||||
className={`flex-1 rounded border px-2 py-1 text-xs ${activeSubTab === "icon" ? "border-blue-500 bg-blue-50 text-blue-600" : "border-gray-200"}`}
|
||||
onClick={() => setActiveSubTab("icon")}
|
||||
>
|
||||
图标
|
||||
</button>
|
||||
<button
|
||||
className={`flex-1 rounded border px-2 py-1 text-xs ${activeSubTab === "sticker" ? "border-blue-500 bg-blue-50 text-blue-600" : "border-gray-200"}`}
|
||||
onClick={() => setActiveSubTab("sticker")}
|
||||
>
|
||||
贴纸
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeSubTab === "icon" && iconGroups.map((group) => (
|
||||
<div key={group.type}>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<Label className="text-xs font-bold text-gray-600">{group.name}</Label>
|
||||
<button onClick={() => removeIcon(group.type)} className="text-[10px] text-blue-500 hover:underline">
|
||||
清除
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{group.list.map((item) => (
|
||||
<button
|
||||
key={`${group.type}-${item.name}`}
|
||||
onClick={() => addIcon(group.type, item.name)}
|
||||
className="flex h-9 w-9 items-center justify-center rounded border border-gray-200 bg-white text-xs hover:border-blue-400 hover:shadow-sm"
|
||||
>
|
||||
{item.icon ? (
|
||||
typeof item.icon === "string" && item.icon.trim().startsWith("<svg") ? (
|
||||
<span
|
||||
className="inline-flex h-6 w-6 items-center justify-center"
|
||||
// @ts-expect-error: dangerouslySetInnerHTML 用于复用官方 SVG 片段
|
||||
dangerouslySetInnerHTML={{ __html: item.icon }}
|
||||
/>
|
||||
) : (
|
||||
<img src={item.icon} alt={item.name} className="h-6 w-6 object-contain" />
|
||||
)
|
||||
) : (
|
||||
<span className="text-xs text-gray-700">{item.name}</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
<div className="text-xs text-gray-400 mt-4">
|
||||
* 更多图标需接入 SVG 资源库
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
))}
|
||||
|
||||
{activeSubTab === "sticker" && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<Label className="text-xs font-bold text-gray-600">贴纸</Label>
|
||||
<button onClick={clearSticker} className="text-[10px] text-blue-500 hover:underline">清除</button>
|
||||
</div>
|
||||
{stickerGroups.map((group: any) => (
|
||||
<div key={group.name}>
|
||||
<div className="mb-2 text-xs font-semibold text-gray-600">{group.name}</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{group.list.map((item: any, idx: number) => (
|
||||
<button
|
||||
key={`${group.name}-${idx}`}
|
||||
onClick={() => setSticker(item)}
|
||||
className="flex h-16 w-16 items-center justify-center rounded border border-gray-200 bg-white hover:border-blue-400 hover:shadow-sm"
|
||||
>
|
||||
<img src={item.url} alt={group.name} className="h-14 w-14 object-contain" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const OutlinePanel = ({ mindmap }: { mindmap: any }) => {
|
||||
const [depth, setDepth] = useState<number>(-1);
|
||||
|
||||
Reference in New Issue
Block a user