jch-config-editor 0.1.0
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/README.md +260 -0
- package/dist/components/Canvas/index.d.ts +4 -0
- package/dist/components/Canvas/index.js +759 -0
- package/dist/components/ConfigEditor/index.d.ts +33 -0
- package/dist/components/ConfigEditor/index.js +146 -0
- package/dist/components/MaterialPanel/index.d.ts +2 -0
- package/dist/components/MaterialPanel/index.js +102 -0
- package/dist/components/NodeRenderer/index.d.ts +13 -0
- package/dist/components/NodeRenderer/index.js +375 -0
- package/dist/components/PropertyPanel/index.d.ts +4 -0
- package/dist/components/PropertyPanel/index.js +542 -0
- package/dist/config-editor.css +1 -0
- package/dist/config-editor.es.js +3421 -0
- package/dist/config-editor.es.js.map +1 -0
- package/dist/config-editor.umd.js +20 -0
- package/dist/config-editor.umd.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +12 -0
- package/dist/jch-config-editor.css +1 -0
- package/dist/store/editorStore.d.ts +35 -0
- package/dist/store/editorStore.js +304 -0
- package/dist/types/index.d.ts +164 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/initData.d.ts +4 -0
- package/dist/utils/initData.js +164 -0
- package/dist/vite.svg +1 -0
- package/package.json +91 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useCallback, useState, useEffect } from "react";
|
|
3
|
+
import { TransformWrapper, TransformComponent, } from "react-zoom-pan-pinch";
|
|
4
|
+
import { Button, Space, Tooltip, Empty, Modal, Form, Input, Collapse, } from "antd";
|
|
5
|
+
import { ZoomInOutlined, ZoomOutOutlined, UndoOutlined, RedoOutlined, SelectOutlined, DragOutlined, EyeOutlined, DeleteOutlined, ExperimentOutlined, GroupOutlined, AppstoreAddOutlined, SaveOutlined, VerticalAlignTopOutlined, VerticalAlignBottomOutlined, AlignLeftOutlined, AlignRightOutlined, } from "@ant-design/icons";
|
|
6
|
+
import { useEditorStore } from "../../store/editorStore";
|
|
7
|
+
import { NodeRenderer } from "../NodeRenderer";
|
|
8
|
+
import { createDefaultNode, createDefaultStatus } from "../../utils/initData";
|
|
9
|
+
import { nanoid } from "nanoid";
|
|
10
|
+
const { Panel } = Collapse;
|
|
11
|
+
const { TextArea } = Input;
|
|
12
|
+
export const Canvas = ({ defaultTestData, }) => {
|
|
13
|
+
const transformRef = useRef(null);
|
|
14
|
+
const canvasRef = useRef(null);
|
|
15
|
+
const wrapperRef = useRef(null);
|
|
16
|
+
const { nodes, selectedNodeId, selectedStatusId, mode, lineDrawing, viewport, setViewport, addNode, selectNode, updateNode, removeNode, undo, redo, setMode, startLineDrawing, updateLineDrawing, endLineDrawing, cancelLineDrawing, addMaterial, } = useEditorStore();
|
|
17
|
+
// 使用 ref 存储拖拽状态,避免频繁触发 useEffect 重新绑定事件
|
|
18
|
+
const isDraggingRef = useRef(false);
|
|
19
|
+
const dragStartRef = useRef({ x: 0, y: 0 });
|
|
20
|
+
const dragOffsetRef = useRef({ x: 0, y: 0, newX: 0, newY: 0 });
|
|
21
|
+
const draggingNodeRef = useRef(null);
|
|
22
|
+
// 用于强制刷新(实际拖拽逻辑使用 ref)
|
|
23
|
+
const [, forceUpdate] = useState({});
|
|
24
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
25
|
+
// 线条绘制状态
|
|
26
|
+
const [linePreview, setLinePreview] = useState({ start: null, end: null, isDrawing: false });
|
|
27
|
+
// 框选状态
|
|
28
|
+
const [isBoxSelecting, setIsBoxSelecting] = useState(false);
|
|
29
|
+
const [selectionBox, setSelectionBox] = useState({ start: null, end: null });
|
|
30
|
+
const [selectedNodeIds, setSelectedNodeIds] = useState([]);
|
|
31
|
+
// 使用 ref 存储状态,避免 useEffect 依赖它们导致频繁重新绑定事件
|
|
32
|
+
const nodesRef = useRef(nodes);
|
|
33
|
+
const selectionBoxRef = useRef({ start: null, end: null });
|
|
34
|
+
const isBoxSelectingRef = useRef(isBoxSelecting);
|
|
35
|
+
// 同步 nodes 到 ref
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
nodesRef.current = nodes;
|
|
38
|
+
}, [nodes]);
|
|
39
|
+
// 同步 selectionBox 到 ref
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
selectionBoxRef.current = selectionBox;
|
|
42
|
+
}, [selectionBox]);
|
|
43
|
+
// 同步 isBoxSelecting 到 ref
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
isBoxSelectingRef.current = isBoxSelecting;
|
|
46
|
+
}, [isBoxSelecting]);
|
|
47
|
+
// 数据模拟面板
|
|
48
|
+
const [dataModalVisible, setDataModalVisible] = useState(false);
|
|
49
|
+
// 编辑模式:数据只渲染一次,使用 ref 避免重渲染
|
|
50
|
+
// 预览模式:数据实时更新
|
|
51
|
+
const testDataRef = useRef(defaultTestData);
|
|
52
|
+
const [previewData, setPreviewData] = useState(defaultTestData);
|
|
53
|
+
const [dataInput, setDataInput] = useState(JSON.stringify(defaultTestData, null, 2));
|
|
54
|
+
const [dataError, setDataError] = useState(null);
|
|
55
|
+
// 根据模式处理数据更新
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (mode === 'preview') {
|
|
58
|
+
// 预览模式:实时更新数据
|
|
59
|
+
setPreviewData(defaultTestData);
|
|
60
|
+
}
|
|
61
|
+
// 编辑模式:只更新 ref,不触发重渲染
|
|
62
|
+
testDataRef.current = defaultTestData;
|
|
63
|
+
// 只在弹窗关闭时更新输入框
|
|
64
|
+
if (!dataModalVisible) {
|
|
65
|
+
setDataInput(JSON.stringify(defaultTestData, null, 2));
|
|
66
|
+
}
|
|
67
|
+
}, [defaultTestData, dataModalVisible, mode]);
|
|
68
|
+
// 同步视口状态
|
|
69
|
+
const handleTransform = useCallback(() => {
|
|
70
|
+
const state = transformRef.current?.state;
|
|
71
|
+
if (state) {
|
|
72
|
+
setViewport({
|
|
73
|
+
scale: state.scale,
|
|
74
|
+
positionX: state.positionX,
|
|
75
|
+
positionY: state.positionY
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}, [setViewport]);
|
|
79
|
+
// 处理画布点击
|
|
80
|
+
const handleCanvasClick = useCallback((e) => {
|
|
81
|
+
if (isDraggingRef.current)
|
|
82
|
+
return;
|
|
83
|
+
// 框选拖拽过程中不处理点击(避免清除选择)
|
|
84
|
+
if (isBoxSelecting && selectionBox.start)
|
|
85
|
+
return;
|
|
86
|
+
if (e.target === canvasRef.current ||
|
|
87
|
+
e.target.dataset?.canvas === "true") {
|
|
88
|
+
selectNode(null);
|
|
89
|
+
}
|
|
90
|
+
}, [selectNode, isBoxSelecting, selectionBox.start]);
|
|
91
|
+
// 处理拖拽悬停
|
|
92
|
+
const handleDragOver = useCallback((e) => {
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
e.dataTransfer.dropEffect = "copy";
|
|
95
|
+
if (!isDragOver) {
|
|
96
|
+
setIsDragOver(true);
|
|
97
|
+
}
|
|
98
|
+
}, [isDragOver]);
|
|
99
|
+
// 处理拖拽离开
|
|
100
|
+
const handleDragLeave = useCallback((e) => {
|
|
101
|
+
e.preventDefault();
|
|
102
|
+
setIsDragOver(false);
|
|
103
|
+
}, []);
|
|
104
|
+
// 处理放置
|
|
105
|
+
const handleDrop = useCallback((e) => {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
e.stopPropagation();
|
|
108
|
+
setIsDragOver(false);
|
|
109
|
+
if (!canvasRef.current || mode !== "select")
|
|
110
|
+
return;
|
|
111
|
+
try {
|
|
112
|
+
const data = e.dataTransfer.getData("application/json");
|
|
113
|
+
if (!data)
|
|
114
|
+
return;
|
|
115
|
+
const material = JSON.parse(data);
|
|
116
|
+
const rect = canvasRef.current.getBoundingClientRect();
|
|
117
|
+
// 优先使用 transformRef 中的实时状态
|
|
118
|
+
const currentTransform = transformRef.current?.state;
|
|
119
|
+
const scale = currentTransform?.scale ?? viewport.scale;
|
|
120
|
+
const positionX = currentTransform?.positionX ?? viewport.positionX;
|
|
121
|
+
const positionY = currentTransform?.positionY ?? viewport.positionY;
|
|
122
|
+
const x = (e.clientX - rect.left - positionX) / scale - 50;
|
|
123
|
+
const y = (e.clientY - rect.top - positionY) / scale - 50;
|
|
124
|
+
const newNode = createDefaultNode(x, y, material);
|
|
125
|
+
addNode(newNode);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.error("Failed to parse dropped material:", error);
|
|
129
|
+
}
|
|
130
|
+
}, [mode, viewport, addNode]);
|
|
131
|
+
// 处理节点拖拽开始
|
|
132
|
+
const handleNodeMouseDown = useCallback((e, node) => {
|
|
133
|
+
if (mode !== "select" || !node.controlInfo.isDraggable)
|
|
134
|
+
return;
|
|
135
|
+
e.stopPropagation();
|
|
136
|
+
selectNode(node.id);
|
|
137
|
+
// 使用 ref 存储拖拽状态,避免触发重渲染
|
|
138
|
+
isDraggingRef.current = true;
|
|
139
|
+
dragStartRef.current = { x: e.clientX, y: e.clientY };
|
|
140
|
+
dragOffsetRef.current = {
|
|
141
|
+
x: node.normalStyle.x || 0,
|
|
142
|
+
y: node.normalStyle.y || 0,
|
|
143
|
+
newX: node.normalStyle.x || 0,
|
|
144
|
+
newY: node.normalStyle.y || 0,
|
|
145
|
+
};
|
|
146
|
+
draggingNodeRef.current = node;
|
|
147
|
+
// 阻止文本选中
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
}, [mode, selectNode]);
|
|
150
|
+
// 获取鼠标在画布上的坐标(考虑缩放和平移)
|
|
151
|
+
const getCanvasPosition = useCallback((clientX, clientY) => {
|
|
152
|
+
if (!canvasRef.current) {
|
|
153
|
+
console.log('[getCanvasPosition] canvas ref not ready');
|
|
154
|
+
return { x: 0, y: 0 };
|
|
155
|
+
}
|
|
156
|
+
// 获取 canvas 元素在屏幕上的位置(已经包含变换)
|
|
157
|
+
const canvasRect = canvasRef.current.getBoundingClientRect();
|
|
158
|
+
// 从 transformRef 获取 scale
|
|
159
|
+
const scale = transformRef.current?.state?.scale ?? 1;
|
|
160
|
+
// 计算鼠标相对于 canvas 左上角的屏幕坐标
|
|
161
|
+
const mouseXInCanvas = clientX - canvasRect.left;
|
|
162
|
+
const mouseYInCanvas = clientY - canvasRect.top;
|
|
163
|
+
// canvas 的逻辑尺寸是 1920x1080
|
|
164
|
+
// canvasRect 是视觉尺寸 = 1920 * scale
|
|
165
|
+
// 所以转换比例 = 逻辑尺寸 / 视觉尺寸 = 1920 / (1920 * scale) = 1 / scale
|
|
166
|
+
const x = mouseXInCanvas * (1920 / canvasRect.width);
|
|
167
|
+
const y = mouseYInCanvas * (1080 / canvasRect.height);
|
|
168
|
+
const result = { x: Math.round(x), y: Math.round(y) };
|
|
169
|
+
console.log('[getCanvasPosition]', { clientX, clientY, canvasRect, scale, result });
|
|
170
|
+
return result;
|
|
171
|
+
}, []);
|
|
172
|
+
// 创建线条节点
|
|
173
|
+
const createLineNode = useCallback((start, end, continueDrawing = true) => {
|
|
174
|
+
if (!lineDrawing.material)
|
|
175
|
+
return;
|
|
176
|
+
const material = lineDrawing.material;
|
|
177
|
+
const config = material.config || {};
|
|
178
|
+
const lineWeight = config.lineWeight || config.thickness || 2;
|
|
179
|
+
// 节点位置:覆盖起点和终点的最小矩形左上角
|
|
180
|
+
const x = Math.min(start.x, end.x);
|
|
181
|
+
const y = Math.min(start.y, end.y);
|
|
182
|
+
// 起点和终点相对于节点左上角的坐标
|
|
183
|
+
const relativeStartX = Math.round(start.x - x);
|
|
184
|
+
const relativeStartY = Math.round(start.y - y);
|
|
185
|
+
const relativeEndX = Math.round(end.x - x);
|
|
186
|
+
const relativeEndY = Math.round(end.y - y);
|
|
187
|
+
// 节点大小:刚好包含整条线
|
|
188
|
+
const minNodeSize = Math.max(lineWeight * 2, 4);
|
|
189
|
+
const nodeWidth = Math.max(Math.abs(start.x - end.x), minNodeSize);
|
|
190
|
+
const nodeHeight = Math.max(Math.abs(start.y - end.y), minNodeSize);
|
|
191
|
+
// 创建线条物料的复制,只保留关键参数
|
|
192
|
+
const lineMaterial = {
|
|
193
|
+
...material,
|
|
194
|
+
id: nanoid(),
|
|
195
|
+
config: {
|
|
196
|
+
// 只保留关键配置:颜色、线宽、线型
|
|
197
|
+
color: config.color,
|
|
198
|
+
lineWeight,
|
|
199
|
+
thickness: lineWeight,
|
|
200
|
+
lineType: config.lineType,
|
|
201
|
+
// 起止位置(相对于节点)
|
|
202
|
+
startX: relativeStartX,
|
|
203
|
+
startY: relativeStartY,
|
|
204
|
+
endX: relativeEndX,
|
|
205
|
+
endY: relativeEndY,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
// 创建节点
|
|
209
|
+
const newNode = {
|
|
210
|
+
id: nanoid(),
|
|
211
|
+
name: `${material.name}_${Date.now()}`,
|
|
212
|
+
type: 'normal',
|
|
213
|
+
normalStyle: {
|
|
214
|
+
width: nodeWidth,
|
|
215
|
+
height: nodeHeight,
|
|
216
|
+
x,
|
|
217
|
+
y,
|
|
218
|
+
background: 'transparent',
|
|
219
|
+
},
|
|
220
|
+
contentInfo: {
|
|
221
|
+
statusList: [createDefaultStatus(lineMaterial)],
|
|
222
|
+
currentStatusId: undefined,
|
|
223
|
+
},
|
|
224
|
+
controlInfo: {
|
|
225
|
+
isDraggable: true,
|
|
226
|
+
isClickable: true,
|
|
227
|
+
isResizable: true,
|
|
228
|
+
isSelectable: true,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
// 绘制线条时不自动选中节点,避免属性面板弹出影响绘制
|
|
232
|
+
addNode(newNode, false);
|
|
233
|
+
if (continueDrawing) {
|
|
234
|
+
// 继续绘制下一条线段,以当前终点作为新起点
|
|
235
|
+
const nextStart = { x: end.x, y: end.y };
|
|
236
|
+
setLinePreview({
|
|
237
|
+
start: nextStart,
|
|
238
|
+
end: { ...nextStart },
|
|
239
|
+
isDrawing: true,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
// 结束绘制
|
|
244
|
+
setLinePreview({ start: null, end: null, isDrawing: false });
|
|
245
|
+
endLineDrawing();
|
|
246
|
+
}
|
|
247
|
+
}, [lineDrawing.material, addNode, endLineDrawing]);
|
|
248
|
+
// 处理画布鼠标按下
|
|
249
|
+
const handleCanvasMouseDown = useCallback((e) => {
|
|
250
|
+
// 使用 ref 获取最新状态,避免闭包问题
|
|
251
|
+
const currentIsBoxSelecting = isBoxSelectingRef.current;
|
|
252
|
+
console.log('[handleCanvasMouseDown]', { button: e.button, mode, isBoxSelecting: currentIsBoxSelecting, isBoxSelectingState: isBoxSelecting, lineDrawingMaterial: !!lineDrawing.material });
|
|
253
|
+
console.log('[handleCanvasMouseDown] ref values:', { isBoxSelectingRef: isBoxSelectingRef.current, selectionBoxRef: selectionBoxRef.current });
|
|
254
|
+
// 只处理左键
|
|
255
|
+
if (e.button !== 0)
|
|
256
|
+
return;
|
|
257
|
+
// 框选模式
|
|
258
|
+
if (currentIsBoxSelecting) {
|
|
259
|
+
const pos = getCanvasPosition(e.clientX, e.clientY);
|
|
260
|
+
setSelectionBox({ start: pos, end: pos });
|
|
261
|
+
setSelectedNodeIds([]);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// 普通选择模式:让 TransformWrapper 处理画布平移,不阻止事件
|
|
265
|
+
if (mode === "select" && !lineDrawing.material) {
|
|
266
|
+
// 不处理,让事件冒泡给 TransformWrapper 进行平移
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// 线条绘制模式
|
|
270
|
+
if (mode !== "line-draw" || !lineDrawing.material) {
|
|
271
|
+
console.log('[handleCanvasMouseDown] not in line-draw mode or no material');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const pos = getCanvasPosition(e.clientX, e.clientY);
|
|
275
|
+
console.log('[handleCanvasMouseDown] drawing at', pos, 'isDrawing:', linePreview.isDrawing);
|
|
276
|
+
if (!linePreview.isDrawing) {
|
|
277
|
+
// 开始绘制
|
|
278
|
+
setLinePreview({
|
|
279
|
+
start: pos,
|
|
280
|
+
end: pos,
|
|
281
|
+
isDrawing: true,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
// 绘制线段,继续绘制下一条(连续绘制模式)
|
|
286
|
+
if (linePreview.start) {
|
|
287
|
+
createLineNode(linePreview.start, pos, true);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}, [mode, lineDrawing.material, linePreview.isDrawing, linePreview.start, getCanvasPosition, createLineNode]);
|
|
291
|
+
// 处理双击结束绘制
|
|
292
|
+
const handleCanvasDoubleClick = useCallback((e) => {
|
|
293
|
+
if (mode !== "line-draw" || !lineDrawing.material)
|
|
294
|
+
return;
|
|
295
|
+
if (linePreview.isDrawing && linePreview.start) {
|
|
296
|
+
// 绘制最后一条线段并结束绘制
|
|
297
|
+
const pos = getCanvasPosition(e.clientX, e.clientY);
|
|
298
|
+
createLineNode(linePreview.start, pos, false);
|
|
299
|
+
}
|
|
300
|
+
}, [mode, lineDrawing.material, linePreview.isDrawing, linePreview.start, getCanvasPosition, createLineNode]);
|
|
301
|
+
// 处理框选 - 判断节点是否在选择框内
|
|
302
|
+
const getNodesInSelectionBox = useCallback(() => {
|
|
303
|
+
if (!selectionBox.start || !selectionBox.end)
|
|
304
|
+
return [];
|
|
305
|
+
const minX = Math.min(selectionBox.start.x, selectionBox.end.x);
|
|
306
|
+
const maxX = Math.max(selectionBox.start.x, selectionBox.end.x);
|
|
307
|
+
const minY = Math.min(selectionBox.start.y, selectionBox.end.y);
|
|
308
|
+
const maxY = Math.max(selectionBox.start.y, selectionBox.end.y);
|
|
309
|
+
return nodes.filter(node => {
|
|
310
|
+
const nodeX = node.normalStyle.x || 0;
|
|
311
|
+
const nodeY = node.normalStyle.y || 0;
|
|
312
|
+
const nodeWidth = node.normalStyle.width || 0;
|
|
313
|
+
const nodeHeight = node.normalStyle.height || 0;
|
|
314
|
+
// 检查节点中心点是否在选择框内
|
|
315
|
+
const centerX = nodeX + nodeWidth / 2;
|
|
316
|
+
const centerY = nodeY + nodeHeight / 2;
|
|
317
|
+
return centerX >= minX && centerX <= maxX && centerY >= minY && centerY <= maxY;
|
|
318
|
+
}).map(node => node.id);
|
|
319
|
+
}, [selectionBox, nodes]);
|
|
320
|
+
// 组合选中节点
|
|
321
|
+
const handleGroupNodes = useCallback(() => {
|
|
322
|
+
if (selectedNodeIds.length < 2)
|
|
323
|
+
return;
|
|
324
|
+
const selectedNodes = nodes.filter(n => selectedNodeIds.includes(n.id));
|
|
325
|
+
if (selectedNodes.length < 2)
|
|
326
|
+
return;
|
|
327
|
+
// 计算边界框
|
|
328
|
+
let minX = Infinity, minY = Infinity;
|
|
329
|
+
let maxX = -Infinity, maxY = -Infinity;
|
|
330
|
+
selectedNodes.forEach(node => {
|
|
331
|
+
const x = node.normalStyle.x || 0;
|
|
332
|
+
const y = node.normalStyle.y || 0;
|
|
333
|
+
const w = node.normalStyle.width || 0;
|
|
334
|
+
const h = node.normalStyle.height || 0;
|
|
335
|
+
minX = Math.min(minX, x);
|
|
336
|
+
minY = Math.min(minY, y);
|
|
337
|
+
maxX = Math.max(maxX, x + w);
|
|
338
|
+
maxY = Math.max(maxY, y + h);
|
|
339
|
+
});
|
|
340
|
+
// 调整子节点位置为相对坐标,并移除 scale 避免双重缩放
|
|
341
|
+
const children = selectedNodes.map(node => ({
|
|
342
|
+
...node,
|
|
343
|
+
normalStyle: {
|
|
344
|
+
...node.normalStyle,
|
|
345
|
+
x: (node.normalStyle.x || 0) - minX,
|
|
346
|
+
y: (node.normalStyle.y || 0) - minY,
|
|
347
|
+
scale: undefined, // 移除 scale,由父级容器统一控制
|
|
348
|
+
},
|
|
349
|
+
}));
|
|
350
|
+
// 创建群组节点
|
|
351
|
+
const groupNode = {
|
|
352
|
+
id: nanoid(),
|
|
353
|
+
name: `群组_${Date.now()}`,
|
|
354
|
+
type: 'group',
|
|
355
|
+
normalStyle: {
|
|
356
|
+
x: minX,
|
|
357
|
+
y: minY,
|
|
358
|
+
width: maxX - minX,
|
|
359
|
+
height: maxY - minY,
|
|
360
|
+
// 不设置背景和边框,由 NodeRenderer 控制
|
|
361
|
+
},
|
|
362
|
+
contentInfo: {
|
|
363
|
+
statusList: [],
|
|
364
|
+
currentStatusId: undefined,
|
|
365
|
+
},
|
|
366
|
+
controlInfo: {
|
|
367
|
+
isDraggable: true,
|
|
368
|
+
isClickable: true,
|
|
369
|
+
isResizable: true,
|
|
370
|
+
isSelectable: true,
|
|
371
|
+
},
|
|
372
|
+
children,
|
|
373
|
+
};
|
|
374
|
+
// 删除原子节点并添加群组节点
|
|
375
|
+
selectedNodeIds.forEach(id => removeNode(id));
|
|
376
|
+
addNode(groupNode);
|
|
377
|
+
setSelectedNodeIds([]);
|
|
378
|
+
selectNode(groupNode.id);
|
|
379
|
+
}, [selectedNodeIds, nodes, addNode, removeNode, selectNode]);
|
|
380
|
+
// 保存群组到物料库
|
|
381
|
+
const handleSaveGroupToMaterial = useCallback(() => {
|
|
382
|
+
if (!selectedNodeId)
|
|
383
|
+
return;
|
|
384
|
+
const selectedNode = nodes.find(n => n.id === selectedNodeId);
|
|
385
|
+
if (!selectedNode || selectedNode.type !== 'group')
|
|
386
|
+
return;
|
|
387
|
+
// 创建群组物料
|
|
388
|
+
const groupMaterial = {
|
|
389
|
+
id: nanoid(),
|
|
390
|
+
name: `群组_${selectedNode.name || Date.now()}`,
|
|
391
|
+
type: 'CUSTOM',
|
|
392
|
+
config: {
|
|
393
|
+
nodes: selectedNode.children,
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
addMaterial(groupMaterial);
|
|
397
|
+
// 显示成功提示
|
|
398
|
+
console.log('[Canvas] 群组已保存到物料库:', groupMaterial.name);
|
|
399
|
+
}, [selectedNodeId, nodes, addMaterial]);
|
|
400
|
+
// 对齐选中的节点
|
|
401
|
+
const handleAlignNodes = useCallback((direction) => {
|
|
402
|
+
if (selectedNodeIds.length < 2)
|
|
403
|
+
return;
|
|
404
|
+
// 获取选中的非群组节点
|
|
405
|
+
const selectedNodes = selectedNodeIds
|
|
406
|
+
.map(id => nodes.find(n => n.id === id))
|
|
407
|
+
.filter((n) => !!n && n.type !== 'group');
|
|
408
|
+
if (selectedNodes.length < 2)
|
|
409
|
+
return;
|
|
410
|
+
// 计算对齐基准值
|
|
411
|
+
let alignValue;
|
|
412
|
+
switch (direction) {
|
|
413
|
+
case 'left':
|
|
414
|
+
alignValue = Math.min(...selectedNodes.map(n => n.normalStyle.x || 0));
|
|
415
|
+
break;
|
|
416
|
+
case 'right':
|
|
417
|
+
alignValue = Math.max(...selectedNodes.map(n => (n.normalStyle.x || 0) + (n.normalStyle.width || 0)));
|
|
418
|
+
break;
|
|
419
|
+
case 'top':
|
|
420
|
+
alignValue = Math.min(...selectedNodes.map(n => n.normalStyle.y || 0));
|
|
421
|
+
break;
|
|
422
|
+
case 'bottom':
|
|
423
|
+
alignValue = Math.max(...selectedNodes.map(n => (n.normalStyle.y || 0) + (n.normalStyle.height || 0)));
|
|
424
|
+
break;
|
|
425
|
+
default:
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
// 更新每个节点的位置
|
|
429
|
+
selectedNodes.forEach(node => {
|
|
430
|
+
let newX = node.normalStyle.x || 0;
|
|
431
|
+
let newY = node.normalStyle.y || 0;
|
|
432
|
+
switch (direction) {
|
|
433
|
+
case 'left':
|
|
434
|
+
newX = alignValue;
|
|
435
|
+
break;
|
|
436
|
+
case 'right':
|
|
437
|
+
newX = alignValue - (node.normalStyle.width || 0);
|
|
438
|
+
break;
|
|
439
|
+
case 'top':
|
|
440
|
+
newY = alignValue;
|
|
441
|
+
break;
|
|
442
|
+
case 'bottom':
|
|
443
|
+
newY = alignValue - (node.normalStyle.height || 0);
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
updateNode(node.id, {
|
|
447
|
+
normalStyle: {
|
|
448
|
+
...node.normalStyle,
|
|
449
|
+
x: newX,
|
|
450
|
+
y: newY,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
}, [selectedNodeIds, nodes, updateNode]);
|
|
455
|
+
// 处理鼠标移动(线条绘制预览和框选)
|
|
456
|
+
useEffect(() => {
|
|
457
|
+
const handleMouseMove = (e) => {
|
|
458
|
+
// 使用 ref 获取最新状态,避免闭包问题
|
|
459
|
+
const currentIsBoxSelecting = isBoxSelectingRef.current;
|
|
460
|
+
const currentSelectionBox = selectionBoxRef.current;
|
|
461
|
+
// 框选处理
|
|
462
|
+
if (currentIsBoxSelecting && currentSelectionBox.start) {
|
|
463
|
+
const pos = getCanvasPosition(e.clientX, e.clientY);
|
|
464
|
+
setSelectionBox(prev => ({ ...prev, end: pos }));
|
|
465
|
+
// 实时更新选中的节点
|
|
466
|
+
const box = {
|
|
467
|
+
start: currentSelectionBox.start,
|
|
468
|
+
end: pos,
|
|
469
|
+
};
|
|
470
|
+
const minX = Math.min(box.start.x, box.end.x);
|
|
471
|
+
const maxX = Math.max(box.start.x, box.end.x);
|
|
472
|
+
const minY = Math.min(box.start.y, box.end.y);
|
|
473
|
+
const maxY = Math.max(box.start.y, box.end.y);
|
|
474
|
+
// 使用 ref 获取最新 nodes,避免依赖导致的重新绑定
|
|
475
|
+
const selectedIds = nodesRef.current.filter(node => {
|
|
476
|
+
const nodeX = node.normalStyle.x || 0;
|
|
477
|
+
const nodeY = node.normalStyle.y || 0;
|
|
478
|
+
const nodeWidth = node.normalStyle.width || 0;
|
|
479
|
+
const nodeHeight = node.normalStyle.height || 0;
|
|
480
|
+
const centerX = nodeX + nodeWidth / 2;
|
|
481
|
+
const centerY = nodeY + nodeHeight / 2;
|
|
482
|
+
return centerX >= minX && centerX <= maxX && centerY >= minY && centerY <= maxY;
|
|
483
|
+
}).map(node => node.id);
|
|
484
|
+
setSelectedNodeIds(selectedIds);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
// 节点拖拽 - 直接操作 DOM,不触发 React 重渲染
|
|
488
|
+
if (isDraggingRef.current && selectedNodeId && draggingNodeRef.current) {
|
|
489
|
+
if (!canvasRef.current)
|
|
490
|
+
return;
|
|
491
|
+
// 获取 canvas 的视觉尺寸
|
|
492
|
+
const canvasRect = canvasRef.current.getBoundingClientRect();
|
|
493
|
+
// 计算转换比例(与 getCanvasPosition 保持一致)
|
|
494
|
+
const scaleX = 1920 / canvasRect.width;
|
|
495
|
+
const scaleY = 1080 / canvasRect.height;
|
|
496
|
+
// 计算鼠标位移(屏幕像素)并转换为画布坐标位移
|
|
497
|
+
const screenDx = e.clientX - dragStartRef.current.x;
|
|
498
|
+
const screenDy = e.clientY - dragStartRef.current.y;
|
|
499
|
+
const canvasDx = screenDx * scaleX;
|
|
500
|
+
const canvasDy = screenDy * scaleY;
|
|
501
|
+
const newX = dragOffsetRef.current.x + canvasDx;
|
|
502
|
+
const newY = dragOffsetRef.current.y + canvasDy;
|
|
503
|
+
// 直接操作 DOM,不触发 React 重渲染
|
|
504
|
+
const nodeEl = document.querySelector(`[data-node-id="${selectedNodeId}"]`);
|
|
505
|
+
if (nodeEl) {
|
|
506
|
+
nodeEl.style.left = `${newX}px`;
|
|
507
|
+
nodeEl.style.top = `${newY}px`;
|
|
508
|
+
}
|
|
509
|
+
// 同时更新 ref 中的位置,用于拖拽结束时保存
|
|
510
|
+
dragOffsetRef.current.newX = newX;
|
|
511
|
+
dragOffsetRef.current.newY = newY;
|
|
512
|
+
}
|
|
513
|
+
// 线条绘制预览
|
|
514
|
+
if (mode === 'line-draw' && linePreview.isDrawing) {
|
|
515
|
+
const pos = getCanvasPosition(e.clientX, e.clientY);
|
|
516
|
+
setLinePreview((prev) => ({
|
|
517
|
+
...prev,
|
|
518
|
+
end: pos,
|
|
519
|
+
}));
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
const handleMouseUp = () => {
|
|
523
|
+
// 拖拽结束,将最终位置同步到 store
|
|
524
|
+
if (isDraggingRef.current && selectedNodeId && draggingNodeRef.current) {
|
|
525
|
+
const newX = dragOffsetRef.current.newX ?? dragOffsetRef.current.x;
|
|
526
|
+
const newY = dragOffsetRef.current.newY ?? dragOffsetRef.current.y;
|
|
527
|
+
updateNode(selectedNodeId, {
|
|
528
|
+
normalStyle: {
|
|
529
|
+
...draggingNodeRef.current.normalStyle,
|
|
530
|
+
x: newX,
|
|
531
|
+
y: newY,
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
// 使用 ref 更新拖拽状态
|
|
536
|
+
isDraggingRef.current = false;
|
|
537
|
+
draggingNodeRef.current = null;
|
|
538
|
+
forceUpdate({}); // 触发一次渲染更新鼠标样式
|
|
539
|
+
// 结束框选(使用 ref 获取最新状态)
|
|
540
|
+
if (isBoxSelectingRef.current) {
|
|
541
|
+
setSelectionBox({ start: null, end: null });
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
const handleKeyDown = (e) => {
|
|
545
|
+
// ESC 取消线条绘制
|
|
546
|
+
if (e.key === 'Escape' && mode === 'line-draw') {
|
|
547
|
+
setLinePreview({ start: null, end: null, isDrawing: false });
|
|
548
|
+
cancelLineDrawing();
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
// 始终监听事件,在 handler 内部根据 ref 判断状态
|
|
552
|
+
// 避免 ref 变化无法触发 useEffect 重新执行的问题
|
|
553
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
554
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
555
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
556
|
+
return () => {
|
|
557
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
558
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
559
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
560
|
+
};
|
|
561
|
+
// 移除了 isDragging, dragStart, dragOffset, nodes, selectionBox, isBoxSelecting
|
|
562
|
+
// 使用 ref 替代这些状态,避免拖拽时频繁重新绑定事件导致卡顿
|
|
563
|
+
}, [
|
|
564
|
+
selectedNodeId,
|
|
565
|
+
updateNode,
|
|
566
|
+
mode,
|
|
567
|
+
linePreview.isDrawing,
|
|
568
|
+
getCanvasPosition,
|
|
569
|
+
cancelLineDrawing,
|
|
570
|
+
]);
|
|
571
|
+
// 键盘快捷键
|
|
572
|
+
useEffect(() => {
|
|
573
|
+
const handleKeyDown = (e) => {
|
|
574
|
+
if (e.key === "Delete" && selectedNodeId) {
|
|
575
|
+
removeNode(selectedNodeId);
|
|
576
|
+
}
|
|
577
|
+
if (e.ctrlKey && e.key === "z") {
|
|
578
|
+
e.preventDefault();
|
|
579
|
+
undo();
|
|
580
|
+
}
|
|
581
|
+
if (e.ctrlKey && e.key === "y") {
|
|
582
|
+
e.preventDefault();
|
|
583
|
+
redo();
|
|
584
|
+
}
|
|
585
|
+
// ESC 取消线条绘制
|
|
586
|
+
if (e.key === "Escape" && mode === "line-draw") {
|
|
587
|
+
setLinePreview({ start: null, end: null, isDrawing: false });
|
|
588
|
+
cancelLineDrawing();
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
592
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
593
|
+
}, [selectedNodeId, removeNode, undo, redo, mode, cancelLineDrawing]);
|
|
594
|
+
// 应用测试数据
|
|
595
|
+
const handleApplyData = () => {
|
|
596
|
+
try {
|
|
597
|
+
const parsed = JSON.parse(dataInput);
|
|
598
|
+
testDataRef.current = parsed;
|
|
599
|
+
// 同时更新预览数据
|
|
600
|
+
setPreviewData(parsed);
|
|
601
|
+
setDataError(null);
|
|
602
|
+
setDataModalVisible(false);
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
setDataError("JSON 格式错误,请检查输入");
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
// 工具栏按钮
|
|
609
|
+
const toolbarButtons = [
|
|
610
|
+
{
|
|
611
|
+
icon: _jsx(SelectOutlined, {}),
|
|
612
|
+
title: "选择模式",
|
|
613
|
+
active: mode === "select",
|
|
614
|
+
onClick: () => {
|
|
615
|
+
setMode("select");
|
|
616
|
+
setIsBoxSelecting(false);
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
icon: _jsx(DragOutlined, {}),
|
|
621
|
+
title: "拖拽模式",
|
|
622
|
+
active: mode === "drag",
|
|
623
|
+
onClick: () => {
|
|
624
|
+
setMode("drag");
|
|
625
|
+
setIsBoxSelecting(false);
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
icon: _jsx(EyeOutlined, {}),
|
|
630
|
+
title: "预览模式",
|
|
631
|
+
active: mode === "preview",
|
|
632
|
+
onClick: () => {
|
|
633
|
+
setMode("preview");
|
|
634
|
+
setIsBoxSelecting(false);
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
];
|
|
638
|
+
const actionButtons = [
|
|
639
|
+
{
|
|
640
|
+
icon: _jsx(UndoOutlined, {}),
|
|
641
|
+
title: "撤销 (Ctrl+Z)",
|
|
642
|
+
onClick: undo,
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
icon: _jsx(RedoOutlined, {}),
|
|
646
|
+
title: "重做 (Ctrl+Y)",
|
|
647
|
+
onClick: redo,
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
icon: _jsx(GroupOutlined, {}),
|
|
651
|
+
title: "框选模式",
|
|
652
|
+
active: isBoxSelecting,
|
|
653
|
+
onClick: () => {
|
|
654
|
+
setIsBoxSelecting(!isBoxSelecting);
|
|
655
|
+
setMode("select");
|
|
656
|
+
setSelectedNodeIds([]);
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
icon: _jsx(AppstoreAddOutlined, {}),
|
|
661
|
+
title: "组合选中节点",
|
|
662
|
+
disabled: selectedNodeIds.length < 2,
|
|
663
|
+
onClick: handleGroupNodes,
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
icon: _jsx(AlignLeftOutlined, {}),
|
|
667
|
+
title: "左对齐",
|
|
668
|
+
disabled: selectedNodeIds.filter(id => nodes.find(n => n.id === id)?.type !== 'group').length < 2,
|
|
669
|
+
onClick: () => handleAlignNodes('left'),
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
icon: _jsx(AlignRightOutlined, {}),
|
|
673
|
+
title: "右对齐",
|
|
674
|
+
disabled: selectedNodeIds.filter(id => nodes.find(n => n.id === id)?.type !== 'group').length < 2,
|
|
675
|
+
onClick: () => handleAlignNodes('right'),
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
icon: _jsx(VerticalAlignTopOutlined, {}),
|
|
679
|
+
title: "上对齐",
|
|
680
|
+
disabled: selectedNodeIds.filter(id => nodes.find(n => n.id === id)?.type !== 'group').length < 2,
|
|
681
|
+
onClick: () => handleAlignNodes('top'),
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
icon: _jsx(VerticalAlignBottomOutlined, {}),
|
|
685
|
+
title: "下对齐",
|
|
686
|
+
disabled: selectedNodeIds.filter(id => nodes.find(n => n.id === id)?.type !== 'group').length < 2,
|
|
687
|
+
onClick: () => handleAlignNodes('bottom'),
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
icon: _jsx(SaveOutlined, {}),
|
|
691
|
+
title: "保存群组到物料库",
|
|
692
|
+
disabled: !selectedNodeId || nodes.find(n => n.id === selectedNodeId)?.type !== 'group',
|
|
693
|
+
onClick: handleSaveGroupToMaterial,
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
icon: _jsx(ExperimentOutlined, {}),
|
|
697
|
+
title: "数据模拟",
|
|
698
|
+
type: "primary",
|
|
699
|
+
onClick: () => setDataModalVisible(true),
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
icon: _jsx(DeleteOutlined, {}),
|
|
703
|
+
title: "删除选中 (Delete)",
|
|
704
|
+
disabled: !selectedNodeId && selectedNodeIds.length === 0,
|
|
705
|
+
onClick: () => {
|
|
706
|
+
if (selectedNodeId) {
|
|
707
|
+
removeNode(selectedNodeId);
|
|
708
|
+
}
|
|
709
|
+
else if (selectedNodeIds.length > 0) {
|
|
710
|
+
selectedNodeIds.forEach(id => removeNode(id));
|
|
711
|
+
setSelectedNodeIds([]);
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
];
|
|
716
|
+
return (_jsxs("div", { className: "flex-1 flex flex-col h-full bg-gray-100", children: [_jsxs("div", { className: "h-12 bg-white border-b border-gray-200 flex items-center justify-between px-4", children: [_jsx(Space, { children: toolbarButtons.map((btn) => (_jsx(Tooltip, { title: btn.title, children: _jsx(Button, { type: btn.active ? "primary" : "default", icon: btn.icon, onClick: btn.onClick }) }, btn.title))) }), _jsx(Space, { children: actionButtons.map((btn) => (_jsx(Tooltip, { title: btn.title, children: _jsx(Button, { type: btn.type || "default", icon: btn.icon, onClick: btn.onClick, disabled: btn.disabled }) }, btn.title))) })] }), _jsx("div", { ref: wrapperRef, className: "flex-1 relative", children: _jsx(TransformWrapper, { ref: transformRef, initialScale: 1, initialPositionX: 0, initialPositionY: 0, minScale: 0.5, maxScale: 5, onTransformed: handleTransform, limitToBounds: false, centerZoomedOut: false, panning: {
|
|
717
|
+
disabled: false,
|
|
718
|
+
velocityDisabled: true,
|
|
719
|
+
}, wheel: {
|
|
720
|
+
disabled: false,
|
|
721
|
+
step: 0.1,
|
|
722
|
+
}, pinch: {
|
|
723
|
+
disabled: false,
|
|
724
|
+
}, doubleClick: {
|
|
725
|
+
disabled: true,
|
|
726
|
+
}, children: ({ zoomIn, zoomOut, resetTransform }) => (_jsxs(_Fragment, { children: [_jsx("div", { className: "absolute bottom-4 right-4 z-10 bg-white rounded-lg shadow-lg p-2", children: _jsxs(Space, { direction: "vertical", children: [_jsx(Tooltip, { title: "\u653E\u5927", children: _jsx(Button, { icon: _jsx(ZoomInOutlined, {}), onClick: () => zoomIn() }) }), _jsx(Tooltip, { title: "\u7F29\u5C0F", children: _jsx(Button, { icon: _jsx(ZoomOutOutlined, {}), onClick: () => zoomOut() }) }), _jsx(Tooltip, { title: "\u91CD\u7F6E\u89C6\u56FE", children: _jsx(Button, { onClick: () => resetTransform(), children: "100%" }) }), _jsxs("div", { className: "text-center text-xs text-gray-500", children: [Math.round(viewport.scale * 100), "%"] })] }) }), _jsx(TransformComponent, { wrapperStyle: { width: "100%", height: "100%", position: "relative" }, contentStyle: { width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center" }, children: _jsxs("div", { ref: canvasRef, "data-canvas": "true", className: `
|
|
727
|
+
relative bg-white
|
|
728
|
+
${isDragOver ? "ring-4 ring-blue-400 ring-opacity-50" : ""}
|
|
729
|
+
${mode === "line-draw" ? "cursor-crosshair" : ""}
|
|
730
|
+
`, style: {
|
|
731
|
+
width: 1920,
|
|
732
|
+
height: 1080,
|
|
733
|
+
flexShrink: 0,
|
|
734
|
+
}, onClick: handleCanvasClick, onMouseDown: handleCanvasMouseDown, onDoubleClick: handleCanvasDoubleClick, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop, children: [_jsxs("svg", { className: "absolute top-0 left-0 pointer-events-none", width: "1920", height: "1080", children: [_jsx("defs", { children: _jsx("pattern", { id: "grid", width: "20", height: "20", patternUnits: "userSpaceOnUse", children: _jsx("path", { d: "M 20 0 L 0 0 0 20", fill: "none", stroke: "#e8e8e8", strokeWidth: "1" }) }) }), _jsx("rect", { width: "1920", height: "1080", fill: "url(#grid)" })] }), _jsx("div", { className: "absolute left-1/2 top-1/2 w-4 h-4 -ml-2 -mt-2 border border-blue-300 rounded-full opacity-50" }), isDragOver && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none", children: _jsx("div", { className: "bg-blue-500 text-white px-6 py-3 rounded-lg shadow-lg text-lg font-medium", children: "\u91CA\u653E\u4EE5\u6DFB\u52A0\u8282\u70B9" }) })), mode === "line-draw" && (_jsx("div", { className: "absolute top-4 left-1/2 -translate-x-1/2 z-50 pointer-events-none", children: _jsxs("div", { className: "bg-blue-500 text-white px-4 py-2 rounded-lg shadow-lg text-sm font-medium flex items-center gap-2", children: [_jsx("span", { children: "\u7EBF\u6761\u7ED8\u5236\u6A21\u5F0F" }), _jsx("span", { className: "text-blue-200", children: "|" }), _jsx("span", { className: "text-blue-100", children: linePreview.isDrawing
|
|
735
|
+
? "点击绘制下一条线段,双击结束绘制"
|
|
736
|
+
: "点击确定起点" }), _jsx("span", { className: "text-blue-200", children: "|" }), _jsx("span", { className: "text-blue-100 text-xs", children: "ESC \u53D6\u6D88" })] }) })), mode === "line-draw" && linePreview.isDrawing && linePreview.start && linePreview.end && (_jsxs("svg", { className: "absolute top-0 left-0 pointer-events-none", style: {
|
|
737
|
+
zIndex: 1000,
|
|
738
|
+
width: 1920,
|
|
739
|
+
height: 1080,
|
|
740
|
+
overflow: 'visible'
|
|
741
|
+
}, children: [_jsx("line", { x1: linePreview.start.x, y1: linePreview.start.y, x2: linePreview.end.x, y2: linePreview.end.y, stroke: "#1890ff", strokeWidth: 2, strokeDasharray: "5,5" }), _jsx("circle", { cx: linePreview.start.x, cy: linePreview.start.y, r: 4, fill: "#1890ff" }), _jsx("circle", { cx: linePreview.end.x, cy: linePreview.end.y, r: 4, fill: "#1890ff", stroke: "#fff", strokeWidth: 2 })] })), isBoxSelecting && selectionBox.start && selectionBox.end && (_jsx("div", { className: "absolute border-2 border-blue-500 bg-blue-500/10 pointer-events-none", style: {
|
|
742
|
+
left: Math.min(selectionBox.start.x, selectionBox.end.x),
|
|
743
|
+
top: Math.min(selectionBox.start.y, selectionBox.end.y),
|
|
744
|
+
width: Math.abs(selectionBox.end.x - selectionBox.start.x),
|
|
745
|
+
height: Math.abs(selectionBox.end.y - selectionBox.start.y),
|
|
746
|
+
zIndex: 999,
|
|
747
|
+
} })), nodes.map((node) => (_jsx(NodeRenderer, { node: node, isSelected: (selectedNodeId === node.id || selectedNodeIds.includes(node.id)) && mode !== "preview", onClick: () => {
|
|
748
|
+
if (isBoxSelecting)
|
|
749
|
+
return;
|
|
750
|
+
selectNode(node.id);
|
|
751
|
+
}, onMouseDown: (e) => handleNodeMouseDown(e, node), data: mode === 'preview' ? previewData : testDataRef.current, onUpdateNode: updateNode, scale: transformRef.current?.state?.scale ?? 1 }, node.id))), nodes.length === 0 && !isDragOver && mode !== "line-draw" && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: _jsx(Empty, { description: _jsx("span", { children: "\u62D6\u62FD\u5DE6\u4FA7\u7269\u6599\u5E93\u4E2D\u7684\u7269\u6599\u5230\u753B\u5E03\u4EE5\u521B\u5EFA\u8282\u70B9" }) }) }))] }) })] })) }) }), _jsxs("div", { className: "h-8 bg-white border-t border-gray-200 flex items-center justify-between px-4 text-xs text-gray-500", children: [_jsxs("span", { children: ["\u8282\u70B9\u6570: ", nodes.length, " | \u9009\u4E2D\u8282\u70B9: ", selectedNodeId ? "是" : "否", " | \u9009\u4E2D\u72B6\u6001: ", selectedStatusId || "无"] }), _jsxs("span", { children: ["\u4F4D\u7F6E: (", Math.round(viewport.positionX), ",", " ", Math.round(viewport.positionY), ") | \u7F29\u653E:", " ", Math.round(viewport.scale * 100), "%"] })] }), _jsx(Modal, { title: "\u6570\u636E\u6A21\u62DF", open: dataModalVisible, onOk: handleApplyData, onCancel: () => setDataModalVisible(false), width: 600, okText: "\u5E94\u7528", cancelText: "\u53D6\u6D88", children: _jsxs("div", { className: "space-y-4", children: [_jsxs("p", { className: "text-sm text-gray-500", children: ["\u8F93\u5165\u6D4B\u8BD5\u6570\u636E\uFF08JSON\u683C\u5F0F\uFF09\uFF0C\u7528\u4E8E\u72B6\u6001\u8868\u8FBE\u5F0F\u8BA1\u7B97\u3002\u72B6\u6001\u8868\u8FBE\u5F0F\u53EF\u4EE5\u4F7F\u7528", " ", _jsx("code", { children: "data" }), " \u53D8\u91CF\u8BBF\u95EE\u8FD9\u4E9B\u6570\u636E\u3002"] }), _jsx(Form, { layout: "vertical", children: _jsx(Form.Item, { label: "\u6D4B\u8BD5\u6570\u636E", validateStatus: dataError ? "error" : "", help: dataError, children: _jsx(TextArea, { value: dataInput, onChange: (e) => {
|
|
752
|
+
setDataInput(e.target.value);
|
|
753
|
+
setDataError(null);
|
|
754
|
+
}, rows: 10, placeholder: `{
|
|
755
|
+
"running": true,
|
|
756
|
+
"temperature": 25,
|
|
757
|
+
"pressure": 1.5
|
|
758
|
+
}` }) }) }), _jsx(Collapse, { ghost: true, children: _jsx(Panel, { header: "\u4F7F\u7528\u793A\u4F8B", children: _jsxs("div", { className: "space-y-2 text-sm", children: [_jsx("p", { children: _jsx("strong", { children: "\u72B6\u6001A\uFF08\u8FD0\u884C\u4E2D\uFF09:" }) }), _jsx("code", { className: "bg-gray-100 px-2 py-1 rounded", children: "return data.running === true;" }), _jsx("p", { className: "mt-2", children: _jsx("strong", { children: "\u72B6\u6001B\uFF08\u9AD8\u6E29\u62A5\u8B66\uFF09:" }) }), _jsx("code", { className: "bg-gray-100 px-2 py-1 rounded", children: "return data.temperature > 80;" }), _jsx("p", { className: "mt-2", children: _jsx("strong", { children: "\u72B6\u6001C\uFF08\u9ED8\u8BA4\uFF09:" }) }), _jsx("code", { className: "bg-gray-100 px-2 py-1 rounded", children: "return true;" })] }) }, "examples") })] }) })] }));
|
|
759
|
+
};
|