jch-config-editor 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Canvas/index.d.ts +2 -0
- package/dist/components/Canvas/index.d.ts.map +1 -1
- package/dist/components/ConfigEditor/index.d.ts +1 -1
- package/dist/components/ConfigEditor/index.d.ts.map +1 -1
- package/dist/config-editor.cjs.js +7 -0
- package/dist/config-editor.es.js +6561 -12362
- package/dist/config-editor.umd.js +9 -25
- package/package.json +4 -1
- package/dist/components/Canvas/index.js +0 -759
- package/dist/components/ConfigEditor/index.js +0 -146
- package/dist/components/MaterialPanel/index.js +0 -102
- package/dist/components/NodeRenderer/index.js +0 -375
- package/dist/components/PropertyPanel/index.js +0 -542
- package/dist/config-editor.es.js.map +0 -1
- package/dist/config-editor.umd.js.map +0 -1
- package/dist/index.js +0 -12
- package/dist/jch-config-editor.css +0 -1
- package/dist/store/editorStore.js +0 -304
- package/dist/test/setup.d.ts +0 -2
- package/dist/test/setup.d.ts.map +0 -1
- package/dist/types/index.js +0 -1
- package/dist/utils/initData.js +0 -164
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import React, { useEffect, useCallback } from "react";
|
|
3
|
-
import { Layout, Button, Space, Tooltip, Modal, message, ConfigProvider, } from "antd";
|
|
4
|
-
import { DownloadOutlined, UploadOutlined } from "@ant-design/icons";
|
|
5
|
-
import zhCN from "antd/locale/zh_CN";
|
|
6
|
-
import { MaterialPanel } from "../MaterialPanel";
|
|
7
|
-
import { Canvas } from "../Canvas";
|
|
8
|
-
import { PropertyPanel } from "../PropertyPanel";
|
|
9
|
-
import { useEditorStore } from "../../store/editorStore";
|
|
10
|
-
import { initMaterials } from "../../utils/initData";
|
|
11
|
-
const { Header, Sider, Content } = Layout;
|
|
12
|
-
export const ConfigEditor = ({ initialScheme, onChange, readonly = false, headerExtra, className = "", style, showHeader = true, showMaterialPanel = true, showPropertyPanel = true, customMaterials, }) => {
|
|
13
|
-
const [importModalVisible, setImportModalVisible] = React.useState(false);
|
|
14
|
-
const [jsonInput, setJsonInput] = React.useState("");
|
|
15
|
-
const [defaultTestData, setDefaultTestData] = React.useState([
|
|
16
|
-
{
|
|
17
|
-
paramsCode: "001",
|
|
18
|
-
paramsName: "通频速度有效值",
|
|
19
|
-
value: 25,
|
|
20
|
-
paramsUnit: "mm/s",
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
paramsCode: "002",
|
|
24
|
-
paramsName: "通频加速度有效值",
|
|
25
|
-
value: 1.5,
|
|
26
|
-
paramsUnit: "m/s²",
|
|
27
|
-
},
|
|
28
|
-
]);
|
|
29
|
-
const { exportScheme, importScheme, nodes, materials, addMaterial } = useEditorStore();
|
|
30
|
-
const internalRef = React.useRef(null);
|
|
31
|
-
const initializedRef = React.useRef(false);
|
|
32
|
-
// useEffect(() => {
|
|
33
|
-
// if (internalRef.current) {
|
|
34
|
-
// clearInterval(internalRef.current);
|
|
35
|
-
// }
|
|
36
|
-
// internalRef.current = setInterval(() => {
|
|
37
|
-
// setDefaultTestData([
|
|
38
|
-
// {
|
|
39
|
-
// paramsCode: "001",
|
|
40
|
-
// paramsName: "通频速度有效值",
|
|
41
|
-
// value: 25 + Math.random() * 5 * (Math.random() > 0.5 ? 1 : -1),
|
|
42
|
-
// paramsUnit: "mm/s",
|
|
43
|
-
// },
|
|
44
|
-
// {
|
|
45
|
-
// paramsCode: "002",
|
|
46
|
-
// paramsName: "通频加速度有效值",
|
|
47
|
-
// value: 1.5 + Math.random() * 0.5 * (Math.random() > 0.5 ? 1 : -1),
|
|
48
|
-
// paramsUnit: "m/s²",
|
|
49
|
-
// },
|
|
50
|
-
// ]);
|
|
51
|
-
// }, 1000);
|
|
52
|
-
// return () => {
|
|
53
|
-
// if (internalRef.current) {
|
|
54
|
-
// clearInterval(internalRef.current);
|
|
55
|
-
// }
|
|
56
|
-
// };
|
|
57
|
-
// }, []);
|
|
58
|
-
// 初始化
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
// 防止 React 18 严格模式下重复执行
|
|
61
|
-
if (initializedRef.current)
|
|
62
|
-
return;
|
|
63
|
-
initializedRef.current = true;
|
|
64
|
-
// 加载默认物料
|
|
65
|
-
if (materials.length === 0) {
|
|
66
|
-
initMaterials.forEach((m) => addMaterial(m));
|
|
67
|
-
}
|
|
68
|
-
// 加载自定义物料
|
|
69
|
-
if (customMaterials && customMaterials.length > 0) {
|
|
70
|
-
customMaterials.forEach((m) => addMaterial(m));
|
|
71
|
-
}
|
|
72
|
-
// 加载初始 scheme
|
|
73
|
-
if (initialScheme) {
|
|
74
|
-
importScheme(initialScheme);
|
|
75
|
-
}
|
|
76
|
-
}, []);
|
|
77
|
-
// 监听变化并回调
|
|
78
|
-
useEffect(() => {
|
|
79
|
-
if (onChange) {
|
|
80
|
-
const scheme = exportScheme();
|
|
81
|
-
onChange(scheme);
|
|
82
|
-
}
|
|
83
|
-
}, [nodes, onChange]);
|
|
84
|
-
// 导出 JSON
|
|
85
|
-
const handleExport = useCallback(() => {
|
|
86
|
-
const scheme = exportScheme();
|
|
87
|
-
const dataStr = JSON.stringify(scheme, null, 2);
|
|
88
|
-
const dataUri = "data:application/json;charset=utf-8," + encodeURIComponent(dataStr);
|
|
89
|
-
const exportFileDefaultName = `scheme_${Date.now()}.json`;
|
|
90
|
-
const linkElement = document.createElement("a");
|
|
91
|
-
linkElement.setAttribute("href", dataUri);
|
|
92
|
-
linkElement.setAttribute("download", exportFileDefaultName);
|
|
93
|
-
linkElement.click();
|
|
94
|
-
message.success("导出成功!");
|
|
95
|
-
}, [exportScheme]);
|
|
96
|
-
// 导入 JSON
|
|
97
|
-
const handleImport = useCallback(() => {
|
|
98
|
-
try {
|
|
99
|
-
const scheme = JSON.parse(jsonInput);
|
|
100
|
-
importScheme(scheme);
|
|
101
|
-
setImportModalVisible(false);
|
|
102
|
-
setJsonInput("");
|
|
103
|
-
message.success("导入成功!");
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
message.error("JSON 格式错误,请检查输入");
|
|
107
|
-
}
|
|
108
|
-
}, [jsonInput, importScheme]);
|
|
109
|
-
// 处理文件导入
|
|
110
|
-
const handleFileImport = useCallback((file) => {
|
|
111
|
-
const reader = new FileReader();
|
|
112
|
-
reader.onload = (e) => {
|
|
113
|
-
try {
|
|
114
|
-
const content = e.target?.result;
|
|
115
|
-
const scheme = JSON.parse(content);
|
|
116
|
-
importScheme(scheme);
|
|
117
|
-
message.success("导入成功!");
|
|
118
|
-
}
|
|
119
|
-
catch (error) {
|
|
120
|
-
message.error("文件格式错误");
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
reader.readAsText(file);
|
|
124
|
-
return false;
|
|
125
|
-
}, [importScheme]);
|
|
126
|
-
// 导出 scheme 方法(供外部 ref 调用)
|
|
127
|
-
const getScheme = useCallback(() => {
|
|
128
|
-
return exportScheme();
|
|
129
|
-
}, [exportScheme]);
|
|
130
|
-
// 暴露给父组件
|
|
131
|
-
React.useImperativeHandle(React.useRef?.(), () => ({
|
|
132
|
-
getScheme,
|
|
133
|
-
exportScheme,
|
|
134
|
-
importScheme,
|
|
135
|
-
}), [getScheme, exportScheme, importScheme]);
|
|
136
|
-
console.log("defaultTestData", defaultTestData);
|
|
137
|
-
return (_jsx(ConfigProvider, { locale: zhCN, children: _jsxs(Layout, { className: `h-full w-full overflow-hidden ${className}`, style: style, children: [showHeader && (_jsxs(Header, { className: "bg-white border-b border-gray-200 px-4 flex items-center justify-between flex-shrink-0", children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx("div", { className: "text-xl font-bold text-blue-600", children: "\u7EC4\u6001\u7F16\u8F91\u5668" }), _jsxs("div", { className: "text-sm text-gray-500", children: [nodes.length, " \u4E2A\u8282\u70B9"] })] }), _jsxs(Space, { children: [headerExtra, !readonly && (_jsxs(_Fragment, { children: [_jsx(Tooltip, { title: "\u5BFC\u51FA JSON", children: _jsx(Button, { icon: _jsx(DownloadOutlined, {}), onClick: handleExport, children: "\u5BFC\u51FA" }) }), _jsx(Tooltip, { title: "\u5BFC\u5165 JSON", children: _jsx(Button, { icon: _jsx(UploadOutlined, {}), onClick: () => setImportModalVisible(true), children: "\u5BFC\u5165" }) })] }))] })] })), _jsxs(Layout, { children: [showMaterialPanel && !readonly && (_jsx(Sider, { width: 280, className: "bg-white", theme: "light", children: _jsx(MaterialPanel, {}) })), _jsx(Content, { className: "bg-gray-100 relative", children: _jsx(Canvas, { defaultTestData: defaultTestData }) }), showPropertyPanel && !readonly && (_jsx(Sider, { width: 360, className: "bg-white", theme: "light", children: _jsx(PropertyPanel, { defaultTestData: defaultTestData }) }))] }), _jsx(Modal, { title: "\u5BFC\u5165\u7EC4\u6001\u65B9\u6848", open: importModalVisible, onOk: handleImport, onCancel: () => {
|
|
138
|
-
setImportModalVisible(false);
|
|
139
|
-
setJsonInput("");
|
|
140
|
-
}, width: 600, children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsx("p", { className: "mb-2", children: "\u9009\u62E9 JSON \u6587\u4EF6\u5BFC\u5165\uFF1A" }), _jsx("input", { type: "file", accept: ".json", onChange: (e) => {
|
|
141
|
-
const file = e.target.files?.[0];
|
|
142
|
-
if (file)
|
|
143
|
-
handleFileImport(file);
|
|
144
|
-
}, className: "block w-full text-sm text-gray-500\n file:mr-4 file:py-2 file:px-4\n file:rounded-full file:border-0\n file:text-sm file:font-semibold\n file:bg-blue-50 file:text-blue-700\n hover:file:bg-blue-100" })] }), _jsx("div", { className: "text-center text-gray-400", children: "\u6216" }), _jsxs("div", { children: [_jsx("p", { className: "mb-2", children: "\u7C98\u8D34 JSON \u5185\u5BB9\uFF1A" }), _jsx("textarea", { value: jsonInput, onChange: (e) => setJsonInput(e.target.value), placeholder: "\u7C98\u8D34 JSON \u5185\u5BB9...", className: "w-full h-48 p-3 border border-gray-300 rounded-lg font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-blue-500" })] })] }) })] }) }));
|
|
145
|
-
};
|
|
146
|
-
export default ConfigEditor;
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from "react";
|
|
3
|
-
import { Collapse, Upload, Button, List, Empty, Typography, Image, Tag } from "antd";
|
|
4
|
-
import { UploadOutlined, DeleteOutlined, EditOutlined } from "@ant-design/icons";
|
|
5
|
-
import { useEditorStore } from "../../store/editorStore";
|
|
6
|
-
const { Panel } = Collapse;
|
|
7
|
-
const { Text } = Typography;
|
|
8
|
-
export const MaterialPanel = () => {
|
|
9
|
-
const { materials, addMaterial, removeMaterial, selectedStatusId, selectStatus, mode, lineDrawing, startLineDrawing, cancelLineDrawing, } = useEditorStore();
|
|
10
|
-
const [activeKeys, setActiveKeys] = useState([
|
|
11
|
-
"basic",
|
|
12
|
-
"device",
|
|
13
|
-
"text",
|
|
14
|
-
"line",
|
|
15
|
-
]);
|
|
16
|
-
// 初始化默认物料
|
|
17
|
-
// useEffect(() => {
|
|
18
|
-
// if (materials.length === 0) {
|
|
19
|
-
// initMaterials.forEach((m) => addMaterial(m));
|
|
20
|
-
// }
|
|
21
|
-
// }, []);
|
|
22
|
-
// 处理 SVG 上传
|
|
23
|
-
const handleUpload = (file) => {
|
|
24
|
-
const reader = new FileReader();
|
|
25
|
-
reader.onload = (e) => {
|
|
26
|
-
const src = e.target?.result;
|
|
27
|
-
addMaterial({
|
|
28
|
-
name: file.name.replace(".svg", ""),
|
|
29
|
-
type: "IMAGE",
|
|
30
|
-
src,
|
|
31
|
-
});
|
|
32
|
-
};
|
|
33
|
-
reader.readAsDataURL(file);
|
|
34
|
-
return false;
|
|
35
|
-
};
|
|
36
|
-
// 按类型分组物料
|
|
37
|
-
const groupedMaterials = {
|
|
38
|
-
basic: materials.filter((m) => ["矩形", "圆形", "圆角矩形"].includes(m.name)),
|
|
39
|
-
device: materials.filter((m) => ["阀门", "泵", "罐体"].includes(m.name) ||
|
|
40
|
-
(m.type === "IMAGE" &&
|
|
41
|
-
!["矩形", "圆形", "圆角矩形"].includes(m.name) &&
|
|
42
|
-
m.name !== "自定义")),
|
|
43
|
-
text: materials.filter((m) => m.type === "TEXT"),
|
|
44
|
-
line: materials.filter((m) => m.type === "LINE"),
|
|
45
|
-
group: materials.filter((m) => m.type === "CUSTOM" && m.config?.nodes),
|
|
46
|
-
custom: materials.filter((m) => (m.type === "CUSTOM" && !m.config?.nodes) ||
|
|
47
|
-
(m.type === "IMAGE" &&
|
|
48
|
-
!["矩形", "圆形", "圆角矩形", "阀门", "泵", "罐体"].includes(m.name))),
|
|
49
|
-
};
|
|
50
|
-
// 处理拖拽开始
|
|
51
|
-
const handleDragStart = (e, material) => {
|
|
52
|
-
e.dataTransfer.effectAllowed = "copy";
|
|
53
|
-
e.dataTransfer.setData("application/json", JSON.stringify(material));
|
|
54
|
-
const dragImage = e.currentTarget.querySelector(".material-preview");
|
|
55
|
-
if (dragImage) {
|
|
56
|
-
e.dataTransfer.setDragImage(dragImage, 20, 20);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
// 处理线条物料点击(进入绘制模式)
|
|
60
|
-
const handleLineMaterialClick = (material) => {
|
|
61
|
-
if (material.type === "LINE") {
|
|
62
|
-
startLineDrawing(material);
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
// 渲染物料项
|
|
66
|
-
const renderMaterialItem = (material, isLineMode = false) => {
|
|
67
|
-
// 物料库中的物料不再被"选中",而是用于拖拽创建节点或在属性面板中选择绑定到状态
|
|
68
|
-
const isSelected = false;
|
|
69
|
-
// 确保 isLineMode 是布尔值
|
|
70
|
-
const isLineMaterial = isLineMode === true;
|
|
71
|
-
// 只有当前物料是正在绘制的那条线时才显示"绘制中"
|
|
72
|
-
const isDrawingLine = mode === "line-draw" && isLineMaterial && lineDrawing.material?.id === material.id;
|
|
73
|
-
return (_jsx(List.Item, { draggable: !isLineMaterial, onDragStart: (e) => handleDragStart(e, material), onClick: () => isLineMaterial && handleLineMaterialClick(material), className: `
|
|
74
|
-
rounded-lg transition-all duration-200 select-none
|
|
75
|
-
${isLineMaterial ? "cursor-pointer" : "cursor-move"}
|
|
76
|
-
${isSelected ? "bg-blue-100 ring-2 ring-blue-400" : "hover:bg-gray-100"}
|
|
77
|
-
${isDrawingLine ? "bg-blue-50 ring-2 ring-blue-400" : ""}
|
|
78
|
-
`, actions: [
|
|
79
|
-
isLineMaterial ? (_jsx(Button, { type: "text", size: "small", icon: _jsx(EditOutlined, {}), onClick: (e) => {
|
|
80
|
-
e.stopPropagation();
|
|
81
|
-
handleLineMaterialClick(material);
|
|
82
|
-
}, children: "\u7ED8\u5236" }, "draw")) : null,
|
|
83
|
-
_jsx(Button, { type: "text", size: "small", danger: true, icon: _jsx(DeleteOutlined, {}), onClick: (e) => {
|
|
84
|
-
e.stopPropagation();
|
|
85
|
-
removeMaterial(material.id);
|
|
86
|
-
} }, "delete"),
|
|
87
|
-
].filter(Boolean), children: _jsxs("div", { className: `flex items-center gap-3 w-full py-2 ${isLineMaterial ? "" : "pointer-events-none"}`, children: [_jsx("div", { className: "material-preview w-12 h-12 flex items-center justify-center bg-white rounded border border-gray-200", children: material.type === "IMAGE" && material.src ? (_jsx(Image, { src: material.src, alt: material.name, width: 40, height: 40, preview: false })) : material.type === "TEXT" ? (_jsx("span", { className: "text-xs text-gray-500", children: "T" })) : material.type === "LINE" ? (_jsx("div", { className: "w-8 h-0.5 bg-gray-400" })) : material.type === "CUSTOM" && material.config?.nodes ? (_jsx("span", { className: "text-xs text-blue-500 font-bold", children: "G" })) : (_jsx("span", { className: "text-xs text-gray-500", children: "?" })) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx(Text, { strong: true, className: "block truncate", children: material.name }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Text, { type: "secondary", className: "text-xs", children: material.type }), isDrawingLine && (_jsx(Tag, { color: "blue", className: "text-xs", children: "\u7ED8\u5236\u4E2D" }))] })] })] }) }, material.id));
|
|
88
|
-
};
|
|
89
|
-
return (_jsxs("div", { className: "h-full flex flex-col bg-white border-r border-gray-200", children: [_jsxs("div", { className: "p-4 border-b border-gray-200", children: [_jsx("h3", { className: "text-lg font-semibold text-gray-800", children: "\u7269\u6599\u5E93" }), _jsx(Text, { type: "secondary", className: "text-sm", children: "\u62D6\u62FD\u7269\u6599\u5230\u753B\u5E03\u521B\u5EFA\u8282\u70B9" })] }), _jsx("div", { className: "p-3 border-b border-gray-200", children: _jsx(Upload, { accept: ".svg", beforeUpload: handleUpload, showUploadList: false, children: _jsx(Button, { icon: _jsx(UploadOutlined, {}), block: true, type: "dashed", children: "\u4E0A\u4F20 SVG" }) }) }), _jsx("div", { className: "flex-1 overflow-y-auto p-2", children: _jsxs(Collapse, { activeKey: activeKeys, onChange: (keys) => setActiveKeys(keys), ghost: true, expandIconPosition: "end", children: [_jsx(Panel, { header: "\u57FA\u7840\u5F62\u72B6", children: _jsx(List, { dataSource: groupedMaterials.basic, renderItem: (item) => renderMaterialItem(item, false), locale: {
|
|
90
|
-
emptyText: (_jsx(Empty, { description: "\u6682\u65E0\u7269\u6599", image: Empty.PRESENTED_IMAGE_SIMPLE })),
|
|
91
|
-
} }) }, "basic"), _jsx(Panel, { header: "\u8BBE\u5907\u56FE\u6807", children: _jsx(List, { dataSource: groupedMaterials.device, renderItem: (item) => renderMaterialItem(item, false), locale: {
|
|
92
|
-
emptyText: (_jsx(Empty, { description: "\u6682\u65E0\u7269\u6599", image: Empty.PRESENTED_IMAGE_SIMPLE })),
|
|
93
|
-
} }) }, "device"), _jsx(Panel, { header: "\u6587\u672C", children: _jsx(List, { dataSource: groupedMaterials.text, renderItem: (item) => renderMaterialItem(item, false), locale: {
|
|
94
|
-
emptyText: (_jsx(Empty, { description: "\u6682\u65E0\u7269\u6599", image: Empty.PRESENTED_IMAGE_SIMPLE })),
|
|
95
|
-
} }) }, "text"), _jsxs(Panel, { header: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { children: "\u7EBF\u6761" }), mode === "line-draw" && (_jsx(Tag, { color: "blue", className: "text-xs", children: "\u7ED8\u5236\u6A21\u5F0F" }))] }), children: [_jsx(List, { dataSource: groupedMaterials.line, renderItem: (item) => renderMaterialItem(item, true), locale: {
|
|
96
|
-
emptyText: (_jsx(Empty, { description: "\u6682\u65E0\u7269\u6599", image: Empty.PRESENTED_IMAGE_SIMPLE })),
|
|
97
|
-
} }), mode === "line-draw" && (_jsxs("div", { className: "mt-2 p-2 bg-blue-50 rounded text-xs text-blue-600", children: [_jsx("div", { children: "\u70B9\u51FB\u7EBF\u6761\u7269\u6599\u5F00\u59CB\u7ED8\u5236" }), _jsx("div", { children: "\u70B9\u51FB\u753B\u5E03\u786E\u5B9A\u8D77\u70B9\u548C\u7EC8\u70B9" })] }))] }, "line"), groupedMaterials.group.length > 0 && (_jsx(Panel, { header: "\u7FA4\u7EC4", children: _jsx(List, { dataSource: groupedMaterials.group, renderItem: (item) => renderMaterialItem(item, false), locale: {
|
|
98
|
-
emptyText: (_jsx(Empty, { description: "\u6682\u65E0\u7269\u6599", image: Empty.PRESENTED_IMAGE_SIMPLE })),
|
|
99
|
-
} }) }, "group")), groupedMaterials.custom.length > 0 && (_jsx(Panel, { header: "\u81EA\u5B9A\u4E49", children: _jsx(List, { dataSource: groupedMaterials.custom, renderItem: (item) => renderMaterialItem(item, false), locale: {
|
|
100
|
-
emptyText: (_jsx(Empty, { description: "\u6682\u65E0\u7269\u6599", image: Empty.PRESENTED_IMAGE_SIMPLE })),
|
|
101
|
-
} }) }, "custom"))] }) }), _jsx("div", { className: "p-3 border-t border-gray-200 bg-gray-50", children: _jsx(Text, { type: "secondary", className: "text-xs", children: mode === "line-draw" ? (_jsx(_Fragment, { children: "\uD83D\uDCA1 \u63D0\u793A\uFF1A\u70B9\u51FB\u7ED8\u5236\u7EBF\u6BB5\uFF0C\u53EF\u8FDE\u7EED\u7ED8\u5236\u591A\u6BB5\u7EBF\uFF0C\u53CC\u51FB\u7ED3\u675F\u7ED8\u5236\uFF0CESC \u53D6\u6D88" })) : (_jsx(_Fragment, { children: "\uD83D\uDCA1 \u63D0\u793A\uFF1A\u62D6\u62FD\u7269\u6599\u5230\u753B\u5E03\u521B\u5EFA\u8282\u70B9\uFF0C\u70B9\u51FB\u7EBF\u6761\u7269\u6599\u8FDB\u5165\u7ED8\u5236\u6A21\u5F0F" })) }) })] }));
|
|
102
|
-
};
|
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import React, { useMemo, useState, useCallback, useRef, useEffect } from "react";
|
|
3
|
-
import { Typography } from "antd";
|
|
4
|
-
const { Text } = Typography;
|
|
5
|
-
// 执行状态表达式
|
|
6
|
-
const evaluateStatusExpression = (status, data) => {
|
|
7
|
-
try {
|
|
8
|
-
//生成函数所需要的参数列表
|
|
9
|
-
const params = data && Array.isArray(data)
|
|
10
|
-
? data.map((d, index) => d.paramsName)
|
|
11
|
-
: ["A"]; //大写英文字符
|
|
12
|
-
const fn = new Function(...params, status.expression);
|
|
13
|
-
const result = fn(...data.map((d) => d.value));
|
|
14
|
-
console.log(`Evaluating status [${status.name}] with data:`, data, "Result:", result);
|
|
15
|
-
return result === true;
|
|
16
|
-
}
|
|
17
|
-
catch (error) {
|
|
18
|
-
console.warn(`状态表达式执行失败 [${status.name}]:`, error);
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
// 找到第一个满足条件的状态
|
|
23
|
-
const findActiveStatus = (statusList, data) => {
|
|
24
|
-
for (const status of statusList) {
|
|
25
|
-
console.log(status, "status");
|
|
26
|
-
let bindData = [];
|
|
27
|
-
if (Array.isArray(data)) {
|
|
28
|
-
console.log(status.bindCodes, "bindCodes");
|
|
29
|
-
bindData = data.filter((d) => status.bindCodes?.includes(d.paramsCode));
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
bindData = data;
|
|
33
|
-
}
|
|
34
|
-
if (evaluateStatusExpression(status, bindData)) {
|
|
35
|
-
return status;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return undefined;
|
|
39
|
-
};
|
|
40
|
-
// 从数据中获取字段值
|
|
41
|
-
const getDataValue = (data, valueSourceCode) => {
|
|
42
|
-
if (!valueSourceCode || !data) {
|
|
43
|
-
return { value: undefined };
|
|
44
|
-
}
|
|
45
|
-
// 数据是数组格式:查找 paramsCode 匹配的项
|
|
46
|
-
if (Array.isArray(data)) {
|
|
47
|
-
const dataItem = data.find((d) => d.paramsCode === valueSourceCode);
|
|
48
|
-
if (dataItem) {
|
|
49
|
-
return {
|
|
50
|
-
value: dataItem.value,
|
|
51
|
-
unit: dataItem.unit,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
// 数据是对象格式:直接取字段
|
|
56
|
-
else if (typeof data === "object" && valueSourceCode in data) {
|
|
57
|
-
return {
|
|
58
|
-
value: data[valueSourceCode],
|
|
59
|
-
unit: data.unit,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
return { value: undefined };
|
|
63
|
-
};
|
|
64
|
-
// 格式化数值,根据小数位配置
|
|
65
|
-
const formatValue = (value, decimals) => {
|
|
66
|
-
if (value === undefined || value === null)
|
|
67
|
-
return "";
|
|
68
|
-
// -1 表示不格式化
|
|
69
|
-
if (decimals === -1)
|
|
70
|
-
return String(value);
|
|
71
|
-
// 检查是否为有效数值
|
|
72
|
-
const num = Number(value);
|
|
73
|
-
if (isNaN(num))
|
|
74
|
-
return String(value);
|
|
75
|
-
// 格式化小数位
|
|
76
|
-
return num.toFixed(decimals ?? 2);
|
|
77
|
-
};
|
|
78
|
-
// 渲染物料
|
|
79
|
-
const renderMaterial = (material, data, isSelected, node) => {
|
|
80
|
-
switch (material.type) {
|
|
81
|
-
case "IMAGE":
|
|
82
|
-
const imageMaterial = material;
|
|
83
|
-
return (_jsx("div", { className: "w-full h-full flex items-center justify-center overflow-hidden", children: imageMaterial.src ? (_jsx("img", { src: imageMaterial.src, alt: material.name, style: {
|
|
84
|
-
width: "100%",
|
|
85
|
-
height: "100%",
|
|
86
|
-
objectFit: "contain",
|
|
87
|
-
} })) : (_jsx("div", { className: "w-full h-full bg-gray-200 flex items-center justify-center", children: _jsx("span", { className: "text-gray-400 text-xs", children: "\u65E0\u56FE\u7247" }) })) }));
|
|
88
|
-
case "TEXT":
|
|
89
|
-
const textMaterial = material;
|
|
90
|
-
const { label, value: configValue, valueSourceCode, unit: configUnit, decimals, labelStyle, valueStyle, customStyle, } = textMaterial.content || {};
|
|
91
|
-
// 如果有 valueSourceCode,从数据中获取值
|
|
92
|
-
const { value: dataValue, unit: dataUnit } = valueSourceCode
|
|
93
|
-
? getDataValue(data, valueSourceCode)
|
|
94
|
-
: { value: undefined, unit: undefined };
|
|
95
|
-
// 优先使用数据中的值,否则使用配置的默认值
|
|
96
|
-
const rawValue = dataValue !== undefined ? dataValue : configValue;
|
|
97
|
-
// 格式化数值
|
|
98
|
-
const displayValue = formatValue(rawValue, decimals);
|
|
99
|
-
// 优先使用配置的单位,否则使用数据中的单位
|
|
100
|
-
const displayUnit = configUnit || dataUnit || "";
|
|
101
|
-
return (_jsxs("div", { className: "w-full h-full flex flex-col justify-center gap-1", style: { ...customStyle }, children: [label && (_jsx(Text, { style: {
|
|
102
|
-
fontSize: 14,
|
|
103
|
-
fontWeight: "bold",
|
|
104
|
-
color: "#262626",
|
|
105
|
-
textAlign: "left",
|
|
106
|
-
...labelStyle,
|
|
107
|
-
}, children: label })), displayValue !== undefined && displayValue !== null && (_jsxs(Text, { style: {
|
|
108
|
-
fontSize: 14,
|
|
109
|
-
color: "#1890ff",
|
|
110
|
-
textAlign: "left",
|
|
111
|
-
...valueStyle,
|
|
112
|
-
}, children: [displayValue, displayUnit ? ` ${displayUnit}` : ""] }))] }));
|
|
113
|
-
case "LINE":
|
|
114
|
-
const lineMaterial = material;
|
|
115
|
-
const { thickness = 2, color = "#d9d9d9", dashed = false, lineType = dashed ? "dashed" : "solid", lineWeight = thickness,
|
|
116
|
-
// 起点和终点的相对坐标
|
|
117
|
-
startX = 0, startY = 0, endX = 100, endY = 0, } = lineMaterial.config || {};
|
|
118
|
-
// 根据线型计算 dash array
|
|
119
|
-
const getDashArray = (type) => {
|
|
120
|
-
const baseWeight = Math.max(lineWeight, 1);
|
|
121
|
-
switch (type) {
|
|
122
|
-
case "solid":
|
|
123
|
-
return "";
|
|
124
|
-
case "dashed":
|
|
125
|
-
return `${baseWeight * 8},${baseWeight * 4}`;
|
|
126
|
-
case "center":
|
|
127
|
-
return `${baseWeight * 12},${baseWeight * 3},${baseWeight * 2},${baseWeight * 3}`;
|
|
128
|
-
case "phantom":
|
|
129
|
-
return `${baseWeight * 12},${baseWeight * 3},${baseWeight * 2},${baseWeight * 3},${baseWeight * 2},${baseWeight * 3}`;
|
|
130
|
-
case "dot":
|
|
131
|
-
return `${baseWeight},${baseWeight * 3}`;
|
|
132
|
-
case "dash-dot":
|
|
133
|
-
return `${baseWeight * 8},${baseWeight * 3},${baseWeight},${baseWeight * 3}`;
|
|
134
|
-
default:
|
|
135
|
-
return "";
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
const dashArray = getDashArray(lineType);
|
|
139
|
-
// 选中时高亮颜色
|
|
140
|
-
const strokeColor = isSelected ? "#1890ff" : color;
|
|
141
|
-
// 直接使用保存的相对坐标画线
|
|
142
|
-
// 这和预览线的逻辑一模一样:从起点画到终点
|
|
143
|
-
return (_jsx("div", { className: "w-full h-full relative", style: { overflow: "visible" }, children: _jsxs("svg", { className: "absolute top-0 left-0", width: "100%", height: "100%", style: { overflow: "visible", pointerEvents: "none" }, children: [_jsx("line", { x1: startX, y1: startY, x2: endX, y2: endY, stroke: "transparent", strokeWidth: Math.max(lineWeight, 10), strokeLinecap: "round", style: { pointerEvents: "stroke", cursor: "pointer" }, onClick: (e) => {
|
|
144
|
-
e.stopPropagation();
|
|
145
|
-
// 触发父容器的点击
|
|
146
|
-
const parent = e.target.closest('[data-node-id]');
|
|
147
|
-
if (parent) {
|
|
148
|
-
parent.click();
|
|
149
|
-
}
|
|
150
|
-
} }), _jsx("line", { x1: startX, y1: startY, x2: endX, y2: endY, stroke: strokeColor, strokeWidth: lineWeight, strokeDasharray: dashArray, strokeLinecap: "butt", style: { pointerEvents: "none" } })] }) }));
|
|
151
|
-
case "CUSTOM":
|
|
152
|
-
const customMaterial = material;
|
|
153
|
-
if (customMaterial.render) {
|
|
154
|
-
return customMaterial.render({});
|
|
155
|
-
}
|
|
156
|
-
return (_jsx("div", { className: "w-full h-full flex items-center justify-center bg-gray-100", children: _jsx("span", { className: "text-gray-400 text-xs", children: "\u81EA\u5B9A\u4E49\u7EC4\u4EF6" }) }));
|
|
157
|
-
default:
|
|
158
|
-
return (_jsx("div", { className: "w-full h-full flex items-center justify-center bg-gray-100", children: _jsx("span", { className: "text-gray-400 text-xs", children: "\u672A\u77E5\u7C7B\u578B" }) }));
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
const NodeRendererComponent = ({ node, isSelected = false, onClick, onMouseDown, data, onUpdateNode, scale: canvasScale = 1, }) => {
|
|
162
|
-
const { normalStyle, contentInfo, controlInfo } = node;
|
|
163
|
-
const { statusList } = contentInfo;
|
|
164
|
-
const { isClickable, isDraggable } = controlInfo;
|
|
165
|
-
// 线条端点拖拽状态
|
|
166
|
-
const [draggingPoint, setDraggingPoint] = useState(null);
|
|
167
|
-
const [dragStartPos, setDragStartPos] = useState({ x: 0, y: 0 });
|
|
168
|
-
const nodeRef = useRef(null);
|
|
169
|
-
// 计算当前应该显示的状态(完全由表达式决定)
|
|
170
|
-
const currentStatus = useMemo(() => {
|
|
171
|
-
if (statusList.length === 0)
|
|
172
|
-
return undefined;
|
|
173
|
-
// 执行状态表达式,找到第一个返回 true 的状态
|
|
174
|
-
const activeStatus = findActiveStatus(statusList, data);
|
|
175
|
-
if (activeStatus)
|
|
176
|
-
return activeStatus;
|
|
177
|
-
// 如果没有状态满足条件,返回第一个状态作为默认
|
|
178
|
-
return statusList[0];
|
|
179
|
-
}, [statusList, data]);
|
|
180
|
-
// 获取当前状态绑定的物料
|
|
181
|
-
const currentMaterial = currentStatus?.material;
|
|
182
|
-
// 判断当前物料是否是线条
|
|
183
|
-
const isLine = currentMaterial?.type === 'LINE';
|
|
184
|
-
// 获取线条配置
|
|
185
|
-
const lineConfig = isLine ? currentMaterial.config : null;
|
|
186
|
-
// 计算缩放比例
|
|
187
|
-
const nodeScale = normalStyle.scale ?? 1;
|
|
188
|
-
const scaledWidth = (normalStyle.width || 100) * nodeScale;
|
|
189
|
-
const scaledHeight = (normalStyle.height || 100) * nodeScale;
|
|
190
|
-
// 处理端点拖拽开始
|
|
191
|
-
const handlePointMouseDown = useCallback((e, point) => {
|
|
192
|
-
e.stopPropagation();
|
|
193
|
-
e.preventDefault();
|
|
194
|
-
setDraggingPoint(point);
|
|
195
|
-
setDragStartPos({ x: e.clientX, y: e.clientY });
|
|
196
|
-
}, []);
|
|
197
|
-
// 处理端点拖拽
|
|
198
|
-
useEffect(() => {
|
|
199
|
-
if (!draggingPoint || !isLine || !lineConfig || !onUpdateNode)
|
|
200
|
-
return;
|
|
201
|
-
const handleMouseMove = (e) => {
|
|
202
|
-
// 计算鼠标移动的偏移量(考虑画布缩放)
|
|
203
|
-
const dx = (e.clientX - dragStartPos.x) / canvasScale;
|
|
204
|
-
const dy = (e.clientY - dragStartPos.y) / canvasScale;
|
|
205
|
-
// 获取当前的起止坐标
|
|
206
|
-
const currentStartX = lineConfig.startX || 0;
|
|
207
|
-
const currentStartY = lineConfig.startY || 0;
|
|
208
|
-
const currentEndX = lineConfig.endX || 0;
|
|
209
|
-
const currentEndY = lineConfig.endY || 0;
|
|
210
|
-
let newStartX = currentStartX;
|
|
211
|
-
let newStartY = currentStartY;
|
|
212
|
-
let newEndX = currentEndX;
|
|
213
|
-
let newEndY = currentEndY;
|
|
214
|
-
let newNodeX = normalStyle.x || 0;
|
|
215
|
-
let newNodeY = normalStyle.y || 0;
|
|
216
|
-
if (draggingPoint === 'start') {
|
|
217
|
-
// 拖拽起点
|
|
218
|
-
newStartX = currentStartX + dx;
|
|
219
|
-
newStartY = currentStartY + dy;
|
|
220
|
-
// 如果起点移出了节点左上边界,调整节点位置和坐标
|
|
221
|
-
if (newStartX < 0) {
|
|
222
|
-
newNodeX += newStartX;
|
|
223
|
-
newEndX -= newStartX;
|
|
224
|
-
newStartX = 0;
|
|
225
|
-
}
|
|
226
|
-
if (newStartY < 0) {
|
|
227
|
-
newNodeY += newStartY;
|
|
228
|
-
newEndY -= newStartY;
|
|
229
|
-
newStartY = 0;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
// 拖拽终点
|
|
234
|
-
newEndX = currentEndX + dx;
|
|
235
|
-
newEndY = currentEndY + dy;
|
|
236
|
-
// 如果终点移出了节点左上边界,调整节点位置和坐标
|
|
237
|
-
if (newEndX < 0) {
|
|
238
|
-
newNodeX += newEndX;
|
|
239
|
-
newStartX -= newEndX;
|
|
240
|
-
newEndX = 0;
|
|
241
|
-
}
|
|
242
|
-
if (newEndY < 0) {
|
|
243
|
-
newNodeY += newEndY;
|
|
244
|
-
newStartY -= newEndY;
|
|
245
|
-
newEndY = 0;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
// 重新计算节点大小
|
|
249
|
-
const minNodeSize = Math.max((lineConfig.lineWeight || 2) * 2, 4);
|
|
250
|
-
const maxX = Math.max(newStartX, newEndX);
|
|
251
|
-
const maxY = Math.max(newStartY, newEndY);
|
|
252
|
-
const newWidth = Math.max(maxX, minNodeSize);
|
|
253
|
-
const newHeight = Math.max(maxY, minNodeSize);
|
|
254
|
-
// 更新节点
|
|
255
|
-
onUpdateNode(node.id, {
|
|
256
|
-
normalStyle: {
|
|
257
|
-
...normalStyle,
|
|
258
|
-
x: newNodeX,
|
|
259
|
-
y: newNodeY,
|
|
260
|
-
width: newWidth,
|
|
261
|
-
height: newHeight,
|
|
262
|
-
},
|
|
263
|
-
contentInfo: {
|
|
264
|
-
...contentInfo,
|
|
265
|
-
statusList: statusList.map(s => ({
|
|
266
|
-
...s,
|
|
267
|
-
material: s.material.type === 'LINE' ? {
|
|
268
|
-
...s.material,
|
|
269
|
-
config: {
|
|
270
|
-
...lineConfig,
|
|
271
|
-
startX: newStartX,
|
|
272
|
-
startY: newStartY,
|
|
273
|
-
endX: newEndX,
|
|
274
|
-
endY: newEndY,
|
|
275
|
-
},
|
|
276
|
-
} : s.material,
|
|
277
|
-
})),
|
|
278
|
-
},
|
|
279
|
-
});
|
|
280
|
-
// 更新拖拽起始位置
|
|
281
|
-
setDragStartPos({ x: e.clientX, y: e.clientY });
|
|
282
|
-
};
|
|
283
|
-
const handleMouseUp = () => {
|
|
284
|
-
setDraggingPoint(null);
|
|
285
|
-
};
|
|
286
|
-
window.addEventListener('mousemove', handleMouseMove);
|
|
287
|
-
window.addEventListener('mouseup', handleMouseUp);
|
|
288
|
-
return () => {
|
|
289
|
-
window.removeEventListener('mousemove', handleMouseMove);
|
|
290
|
-
window.removeEventListener('mouseup', handleMouseUp);
|
|
291
|
-
};
|
|
292
|
-
}, [draggingPoint, dragStartPos, isLine, lineConfig, node.id, normalStyle, contentInfo, statusList, onUpdateNode, canvasScale]);
|
|
293
|
-
// 判断是否为群组节点
|
|
294
|
-
const isGroup = node.type === 'group';
|
|
295
|
-
// 样式计算
|
|
296
|
-
const containerStyle = {
|
|
297
|
-
position: "absolute",
|
|
298
|
-
left: normalStyle.x || 0,
|
|
299
|
-
top: normalStyle.y || 0,
|
|
300
|
-
width: scaledWidth,
|
|
301
|
-
height: scaledHeight,
|
|
302
|
-
// 群组节点不显示背景和边框,只作为容器
|
|
303
|
-
background: isGroup ? 'transparent' : (normalStyle.background || "transparent"),
|
|
304
|
-
backgroundImage: isGroup ? undefined : (normalStyle.backgroundImage
|
|
305
|
-
? `url(${normalStyle.backgroundImage})`
|
|
306
|
-
: undefined),
|
|
307
|
-
backgroundSize: "cover",
|
|
308
|
-
backgroundPosition: "center",
|
|
309
|
-
padding: Array.isArray(normalStyle.padding)
|
|
310
|
-
? normalStyle.padding.join("px ") + "px"
|
|
311
|
-
: normalStyle.padding,
|
|
312
|
-
margin: Array.isArray(normalStyle.margin)
|
|
313
|
-
? normalStyle.margin.join("px ") + "px"
|
|
314
|
-
: normalStyle.margin,
|
|
315
|
-
borderRadius: isGroup ? undefined : normalStyle.borderRadius,
|
|
316
|
-
// 线条物料选中时不显示边框,而是通过高亮线条颜色来表示
|
|
317
|
-
// 群组节点选中时显示边框,平时不显示
|
|
318
|
-
border: isSelected && !isLine
|
|
319
|
-
? "2px solid #1890ff"
|
|
320
|
-
: isLine
|
|
321
|
-
? "none"
|
|
322
|
-
: isGroup
|
|
323
|
-
? "none"
|
|
324
|
-
: normalStyle.border || "1px dashed transparent",
|
|
325
|
-
// 线条节点本身不捕获点击,让内部 SVG 处理
|
|
326
|
-
pointerEvents: isLine ? "none" : "auto",
|
|
327
|
-
opacity: normalStyle.opacity ?? 1,
|
|
328
|
-
transform: normalStyle.transform,
|
|
329
|
-
zIndex: isSelected ? 1000 : (normalStyle.zIndex ?? 1),
|
|
330
|
-
cursor: isDraggable && !draggingPoint ? "move" : isClickable ? "pointer" : "default",
|
|
331
|
-
boxSizing: "border-box",
|
|
332
|
-
// 线条和群组节点使用 visible overflow,避免裁剪子节点
|
|
333
|
-
overflow: isLine || isGroup ? "visible" : "hidden",
|
|
334
|
-
};
|
|
335
|
-
// 获取线条端点位置用于显示拖拽手柄
|
|
336
|
-
const startPointPos = lineConfig ? { x: lineConfig.startX || 0, y: lineConfig.startY || 0 } : { x: 0, y: 0 };
|
|
337
|
-
const endPointPos = lineConfig ? { x: lineConfig.endX || 0, y: lineConfig.endY || 0 } : { x: 0, y: 0 };
|
|
338
|
-
return (_jsxs("div", { ref: nodeRef, "data-node-id": node.id, style: containerStyle, onClick: (e) => {
|
|
339
|
-
e.stopPropagation();
|
|
340
|
-
if (isClickable && onClick && !draggingPoint) {
|
|
341
|
-
onClick(e);
|
|
342
|
-
}
|
|
343
|
-
}, onMouseDown: (e) => {
|
|
344
|
-
if (isDraggable && onMouseDown && !draggingPoint) {
|
|
345
|
-
onMouseDown(e);
|
|
346
|
-
}
|
|
347
|
-
}, children: [isSelected && !isLine && (_jsxs(_Fragment, { children: [!isGroup && (_jsxs(_Fragment, { children: [_jsx("div", { className: "absolute -top-1 -left-1 w-2 h-2 bg-white border border-blue-500 rounded-full" }), _jsx("div", { className: "absolute -top-1 -right-1 w-2 h-2 bg-white border border-blue-500 rounded-full" }), _jsx("div", { className: "absolute -bottom-1 -left-1 w-2 h-2 bg-white border border-blue-500 rounded-full" }), _jsx("div", { className: "absolute -bottom-1 -right-1 w-2 h-2 bg-white border border-blue-500 rounded-full" })] })), _jsxs("div", { className: "absolute -top-6 left-0 bg-blue-500 text-white text-xs px-2 py-0.5 rounded whitespace-nowrap", children: [node.name, " ", currentStatus ? `(${currentStatus.name})` : ""] })] })), isSelected && isLine && lineConfig && onUpdateNode && (_jsxs(_Fragment, { children: [_jsx("div", { className: "absolute w-3 h-3 bg-blue-500 border-2 border-white rounded-full cursor-move z-50", style: {
|
|
348
|
-
left: startPointPos.x - 6,
|
|
349
|
-
top: startPointPos.y - 6,
|
|
350
|
-
boxShadow: '0 0 4px rgba(0,0,0,0.3)',
|
|
351
|
-
}, onMouseDown: (e) => handlePointMouseDown(e, 'start'), title: "\u62D6\u62FD\u8C03\u6574\u8D77\u70B9" }), _jsx("div", { className: "absolute w-3 h-3 bg-blue-500 border-2 border-white rounded-full cursor-move z-50", style: {
|
|
352
|
-
left: endPointPos.x - 6,
|
|
353
|
-
top: endPointPos.y - 6,
|
|
354
|
-
boxShadow: '0 0 4px rgba(0,0,0,0.3)',
|
|
355
|
-
}, onMouseDown: (e) => handlePointMouseDown(e, 'end'), title: "\u62D6\u62FD\u8C03\u6574\u7EC8\u70B9" }), _jsx("div", { className: "absolute -top-6 left-0 bg-blue-500 text-white text-xs px-2 py-0.5 rounded whitespace-nowrap", children: node.name })] })), _jsx("div", { className: "w-full h-full relative", children: currentMaterial ? (_jsx("div", { className: "w-full h-full", children: renderMaterial(currentMaterial, data, isSelected, node) })) : !isGroup ? (
|
|
356
|
-
// 只有非群组节点才显示"无状态"
|
|
357
|
-
_jsx("div", { className: "w-full h-full flex items-center justify-center text-gray-300 text-xs", children: "\u65E0\u72B6\u6001" })) : null }), node.type === 'group' && node.children && (_jsx("div", { className: "absolute inset-0 pointer-events-none", children: node.children.map(childNode => (_jsx("div", { className: "pointer-events-auto", children: _jsx(NodeRenderer, { node: childNode, isSelected: false, data: data, onUpdateNode: onUpdateNode, scale: canvasScale }) }, childNode.id))) }))] }));
|
|
358
|
-
};
|
|
359
|
-
// 使用 React.memo 避免不必要的重渲染,提高拖拽性能
|
|
360
|
-
// 注意:只在节点位置、选中状态、缩放比例变化时才重渲染
|
|
361
|
-
// 忽略 data、onClick、onMouseDown、onUpdateNode 等频繁变化的 props
|
|
362
|
-
export const NodeRenderer = React.memo(NodeRendererComponent, (prevProps, nextProps) => {
|
|
363
|
-
const nodeA = prevProps.node;
|
|
364
|
-
const nodeB = nextProps.node;
|
|
365
|
-
// 比较节点位置和尺寸
|
|
366
|
-
const nodeStyleEqual = nodeA.id === nodeB.id &&
|
|
367
|
-
nodeA.normalStyle.x === nodeB.normalStyle.x &&
|
|
368
|
-
nodeA.normalStyle.y === nodeB.normalStyle.y &&
|
|
369
|
-
nodeA.normalStyle.width === nodeB.normalStyle.width &&
|
|
370
|
-
nodeA.normalStyle.height === nodeB.normalStyle.height;
|
|
371
|
-
// 比较其他关键 props
|
|
372
|
-
const otherPropsEqual = prevProps.isSelected === nextProps.isSelected &&
|
|
373
|
-
prevProps.scale === nextProps.scale;
|
|
374
|
-
return nodeStyleEqual && otherPropsEqual;
|
|
375
|
-
});
|