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.
@@ -0,0 +1,375 @@
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
+ });
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ export declare const PropertyPanel: React.FC<{
3
+ defaultTestData?: any;
4
+ }>;