cadeditfronttest/components/DrawingViewer.js
puzzlesion 1bd8f25cdb feat: 图纸工作台核心功能重构与交互增强
本次提交对图纸工作台进行了全面的功能重构和交互体验升级,主要涵盖了数据结构优化、核心逻辑增强、以及全新的可视化交互功能。

### 主要更新内容:

#### 1. **核心应用逻辑 (app.js)**

-   **数据结构优化**:
    -   重构了初始化逻辑,现在每个图片被视为一个独立的 \drawing\ 对象,包含自身的坐标边界和关联图片,使数据模型更清晰。
-   **状态管理**:
    -   引入 \currentDrawingId\ 状态,用于精确追踪和控制当前显示的图纸。
-   **核心功能增强**:
    -   **自动图纸切换**: 实现了 \getDrawingForItem\ 映射逻辑,当用户在结构树中选择不同层级的件号时,系统能自动切换到其关联的图纸。
    -   **件号重命名 (handleItemRename)**: 彻底重写了重命名逻辑,现在支持递归更新所有子孙件号的ID和层级关系,保证了复杂结构下数据的一致性。
    -   **件号更新 (handleItemUpdate)**: 增强了件号创建和更新的鲁棒性,在添加深层级子件号时,会自动创建不存在的父级装配件,简化了操作流程。
    -   **图片拖拽**: 新增 \updateImagePosition\ 处理器,允许用户在画布上直接拖动图片并更新其世界坐标。

#### 2. **数据编辑器 (components/DataEditor.js)**

-   **UI/UX 改进**:
    -   大幅扩展了可编辑字段,增加了中文名、英文名、规格、材料、比例和备注等详细属性。
    -   界面现在能够智能区分零件和装配件,并对装配件的重量输入框进行只读处理,因为其重量由子件自动汇总计算。
    -   为只读字段添加了明确的提示信息,优化了用户体验。

#### 3. **图纸查看器 (components/DrawingViewer.js)**

-   **全新交互功能**:
    -   **画布平移与缩放**: 实现了完整的画布拖拽平移和鼠标滚轮缩放功能,操作更流畅。
    -   **表格与图片拖拽**: 用户现在可以直接在画布上拖动明细表 (\GraphicalTable\) 和图纸中的图片,实现自由布局。
    -   **点击创建件号**: 在画布空白处单击,会弹出输入框,允许用户在指定位置快速创建新的件号,并记录其在图纸上的标记位置 (\markerPosition\)。
-   **UI 浮层与控件**:
    -   在画布上增加了多个交互浮层,包括左上角的图纸-件号绑定工具、右上角的缩放按钮、以及两侧用于切换图纸的箭头按钮。
    -   新增了动态输入框 (\inputBox\),用于响应画布点击事件,提升了操作的直观性。
-   **渲染与状态**:
    -   \GraphicalTable\ 和 \<image>\ 元素现在具备了拖拽所需的所有属性和事件处理。
    -   内部状态管理重构,以支持新的视图变换 (\iewTransform\) 和交互状态。

#### 4. **主页面 (index.html)**

-   **项目依赖**:
    -   将 React 的 CDN 依赖切换为本地 \endor\ 目录下的文件,提高了应用的稳定性和加载速度。
    -   移除了 Babel 依赖,因为项目已统一使用 \React.createElement\ API,无需 JSX 编译。
-   **样式系统**:
    -   全面拥抱 Tailwind CSS 的 \@layer\ 规则,对样式进行了分层(theme, base, components, utilities),使 CSS 结构更清晰、更易于维护。
    -   定义了全局 CSS 变量,统一了颜色、尺寸等设计规范。
2025-09-13 13:52:07 +08:00

579 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

// Use React.* APIs without destructuring to avoid global redeclaration conflicts
// A helper function to create SVG text elements, handling the coordinate system flip.
function SvgText({ content, x, y, height, color = '#2563eb', alignment = 'BOTTOM_LEFT' }) {
const textAnchor = (alignment === 'BOTTOM_CENTER') ? 'middle' : (alignment === 'BOTTOM_RIGHT' ? 'end' : 'start');
// We apply a transform to flip the text right-side-up after the group's transform.
return React.createElement('text', {
x: x,
y: -y, // Y is flipped in the group, so we negate it here.
fontSize: height,
fill: color,
transform: 'scale(1, -1)', // Flip text back upright
textAnchor: textAnchor,
style: {
fontFamily: "'KaiTi', 'SimSun', sans-serif",
userSelect: 'none', // 禁用文字选择
pointerEvents: 'none' // 让文字不接收鼠标事件,事件将传递给父元素
}
}, content);
}
function GraphicalTable({ template, items, allItems, drawings }) {
if (!template) return null;
const { header_height, row_height, header_definition, column_definitions, column_boundaries } = template;
const strokeColor = "#334155", strokeWidth = 0.3;
const normalize = (name) => (name || '').replace(/\s+/g, '').replace('名称', '名称').replace('件号', '件号').replace('材料', '材料').replace('备注', '备注');
const tableWidth = Math.max(...column_boundaries);
const totalRows = items.length;
const tableHeight = header_height + (totalRows * row_height);
return React.createElement('g', null,
// Outer highlighted border (does not change coordinates)
React.createElement('rect', {
x: 0,
y: 0,
width: tableWidth,
height: tableHeight,
fill: 'none',
stroke: '#2563eb',
strokeWidth: 1.5,
opacity: 0.95
}),
// Header
React.createElement('g', { className: 'table-header' },
header_definition.lines.map((line, i) => React.createElement('line', {
key: `h-line-${i}`,
x1: line.start[0],
y1: line.start[1],
x2: line.end[0],
y2: line.end[1],
stroke: strokeColor,
strokeWidth: strokeWidth,
})),
header_definition.texts.map((text, i) => React.createElement(SvgText, {
key: `h-text-${i}`,
content: text.content,
x: text.relative_pos[0],
y: text.relative_pos[1],
height: text.height,
alignment: text.alignment,
}))
),
// Body Rows
React.createElement('g', { className: 'table-body' },
items.map((item, rowIndex) => {
const rowY = header_height + (rowIndex * row_height);
return React.createElement('g', { key: `row-${item.id}`, transform: `translate(0, ${rowY})`},
// Render horizontal line for the row
React.createElement('line', {
x1: 0, y1: row_height, x2: tableWidth, y2: row_height,
stroke: strokeColor, strokeWidth: strokeWidth
}),
// Render cell data
column_definitions.map(colDef => {
const normName = normalize(colDef.name);
const cell = {
'件号': item.id,
'图号或标准号': drawings[item.drawingId]?.drawingNumber || '',
'名称': {
chinese_name: item.chinese_name || item.name || '',
english_name: item.english_name || '',
specification: item.specification || ''
},
'数量': item.quantity,
'单': (item.weight ?? 0).toFixed(2),
'总': ((item.quantity ?? 0) * (item.weight ?? 0)).toFixed(2),
'材料': item.material?.main || item.material || item.chinese_material || 'Q235',
'备注': item.remark || '',
'质量': (item.weight ?? 0).toFixed(2),
'比例': item.scale || '1:1',
'所在图号': item.dwgNo || drawings[item.drawingId]?.drawingNumber || '',
'装配图号': item.assyDwgNo || (allItems[item.parentId] ? (drawings[allItems[item.parentId]?.drawingId]?.drawingNumber || '') : '')
};
const dataItem = cell[normName];
return colDef.text_definitions.map(textDef => {
let content = '';
if (normName === '名称' && typeof dataItem === 'object' && dataItem !== null) {
content = dataItem[textDef.data_key] || '';
} else {
content = dataItem;
}
if (content === null || content === undefined) return null;
return React.createElement(SvgText, {
key: `${item.id}-${normName}-${textDef.data_key || 'main'}`,
content,
x: colDef.relative_x_start + textDef.relative_pos[0],
y: textDef.relative_pos[1],
height: textDef.height,
alignment: textDef.alignment,
});
});
})
);
}),
// Vertical lines
column_boundaries.map((xPos, i) => React.createElement('line', {
key: `v-line-${i}`,
x1: xPos, y1: 0, x2: xPos, y2: tableHeight,
stroke: strokeColor, strokeWidth: strokeWidth
}))
)
);
}
function DrawingViewer({ drawing, item, items, drawings, template, tablePositions, setTablePositions, onCycleDrawing, onItemRename, onItemUpdate, onBindDrawing, onUpdateImagePosition }) {
const isInClass = (node, className) => {
let el = node;
while (el && el !== viewerRef.current) {
// 检查CSS class
if (el.classList && el.classList.contains(className)) return true;
// 检查SVG元素的特殊属性
if (el.getAttribute && el.getAttribute('data-draggable') === className) return true;
// 检查className属性对于SVG元素
if (el.className && el.className.baseVal && el.className.baseVal.includes(className)) return true;
el = el.parentNode;
}
return false;
};
const [viewTransform, setViewTransform] = React.useState({ x: 0, y: 0, scale: 1 });
const [editedItemId, setEditedItemId] = React.useState('');
const [inputBox, setInputBox] = React.useState(null); // { x, y, drawingId, show: true }
const viewerRef = React.useRef(null);
const inputRef = React.useRef(null);
const isPanning = React.useRef(false);
const isDraggingTable = React.useRef(false);
const isDraggingImage = React.useRef(false);
const draggedImageName = React.useRef(null);
const lastMousePos = React.useRef({ x: 0, y: 0 });
React.useEffect(() => {
if (item) {
setEditedItemId(item.id);
}
}, [item]);
React.useEffect(() => {
if (drawing && viewerRef.current) {
const container = viewerRef.current;
const containerWidth = container.offsetWidth;
const containerHeight = container.offsetHeight;
// Compute based on the actual rendered image bounding box
const img = drawing.images && drawing.images[0];
if (!img) return;
const contentX = img.coords[0].x;
const contentY = img.coords[0].y - img.height; // top-left y in SVG coords
const contentWidth = img.width;
const contentHeight = img.height;
if (contentWidth <= 0 || contentHeight <= 0) return;
const scaleX = containerWidth / contentWidth;
const scaleY = containerHeight / contentHeight;
const scale = Math.min(scaleX, scaleY) * 0.9;
const initialX = (containerWidth - contentWidth * scale) / 2 - contentX * scale;
const initialY = (containerHeight - contentHeight * scale) / 2 - contentY * scale;
setViewTransform({ x: initialX, y: initialY, scale });
}
// Reset input when switching drawing
setEditedItemId('');
}, [drawing]);
const handleCanvasClick = (e) => {
// Ignore clicks within UI overlays or existing input boxes
if (isInClass(e.target, 'ui-overlay') || isInClass(e.target, 'input-overlay')) return;
// 检查是否点击了表格或图片(这些不应该触发添加件号)
if (isInClass(e.target, 'graphical-table') || isInClass(e.target, 'draggable-image')) {
return;
}
// 点击空白区域,显示输入框
const rect = viewerRef.current.getBoundingClientRect();
const svgX = (e.clientX - rect.left - viewTransform.x) / viewTransform.scale;
const svgY = (e.clientY - rect.top - viewTransform.y) / viewTransform.scale;
setInputBox({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
svgX: svgX,
svgY: svgY,
drawingId: drawing?.id,
show: true
});
e.stopPropagation();
};
const handleMouseDown = (e) => {
// Ignore clicks within UI overlays
if (isInClass(e.target, 'ui-overlay') || isInClass(e.target, 'input-overlay')) return;
// 重置所有拖拽状态
isPanning.current = false;
isDraggingTable.current = false;
isDraggingImage.current = false;
draggedImageName.current = null;
// 检查是否点击了表格
if (isInClass(e.target, 'graphical-table')) {
isDraggingTable.current = true;
lastMousePos.current = { x: e.clientX, y: e.clientY };
viewerRef.current.style.cursor = 'move';
console.log('开始拖拽表格');
e.stopPropagation();
e.preventDefault();
return;
}
// 检查是否点击了图片
if (isInClass(e.target, 'draggable-image')) {
isDraggingImage.current = true;
draggedImageName.current = e.target.getAttribute('data-image-name');
lastMousePos.current = { x: e.clientX, y: e.clientY };
viewerRef.current.style.cursor = 'move';
console.log('开始拖拽图片:', draggedImageName.current);
e.stopPropagation();
e.preventDefault();
return;
}
// 否则开始画布平移
isPanning.current = true;
lastMousePos.current = { x: e.clientX, y: e.clientY };
viewerRef.current.style.cursor = 'grabbing';
e.preventDefault();
};
const handleMouseMove = (e) => {
if (!isPanning.current && !isDraggingTable.current && !isDraggingImage.current) return;
const dx = (e.clientX - lastMousePos.current.x);
const dy = (e.clientY - lastMousePos.current.y);
if (isDraggingTable.current) {
console.log('正在拖拽表格', dx, dy);
const tableId = drawing.id;
const defaultPos = (() => {
const img = drawing.images && drawing.images[0];
const width = template ? Math.max(...template.column_boundaries) : 0;
if (img) {
return { x: img.coords[0].x + img.width - width - 10, y: img.coords[0].y - 10 };
}
return { x: drawing.maxX - width, y: drawing.minY };
})();
const currentPos = tablePositions[tableId] || defaultPos;
const newPos = {
x: currentPos.x + (dx / viewTransform.scale),
y: currentPos.y - (dy / viewTransform.scale) // Y is inverted in SVG coords
};
console.log('表格新位置:', newPos);
setTablePositions(prev => ({
...prev,
[tableId]: newPos
}));
} else if (isDraggingImage.current && draggedImageName.current && onUpdateImagePosition) {
// 图片拖拽:直接修改世界坐标
const worldDx = dx / viewTransform.scale;
const worldDy = -dy / viewTransform.scale; // Y 在SVG中是反向的
onUpdateImagePosition(drawing.id, draggedImageName.current, worldDx, worldDy);
} else if(isPanning.current) {
setViewTransform(prev => ({ ...prev, x: prev.x + dx, y: prev.y + dy }));
}
lastMousePos.current = { x: e.clientX, y: e.clientY };
};
const handleMouseUp = (e) => {
if (isDraggingTable.current) {
console.log('停止拖拽表格');
}
isPanning.current = false;
isDraggingTable.current = false;
isDraggingImage.current = false;
draggedImageName.current = null;
if (viewerRef.current) {
viewerRef.current.style.cursor = 'grab';
}
};
const handleRenameItem = () => {
if (!editedItemId.trim() || editedItemId.trim() === item.id) return;
onItemRename(item.id, editedItemId.trim());
};
const handleCreateUnderRoot = () => {
const id = (editedItemId || '').trim();
if (!id) return;
onItemUpdate({
id,
name: `新建部件 ${id}`,
quantity: 1,
weight: 0,
drawingId: drawing ? drawing.id : null,
parentId: '__ROOT__',
children: []
});
setEditedItemId('');
};
const handleInputBoxSubmit = (newItemId) => {
if (!newItemId.trim() || !inputBox) return;
const trimmedId = newItemId.trim();
// 创建新件号
onItemUpdate({
id: trimmedId,
name: `新建部件 ${trimmedId}`,
quantity: 1,
weight: 0,
drawingId: inputBox.drawingId,
parentId: item?.id || '__ROOT__',
children: [],
// 保存件号标记的位置
markerPosition: {
svgX: inputBox.svgX,
svgY: inputBox.svgY
}
});
// 关闭输入框
setInputBox(null);
};
const handleInputBoxCancel = () => {
setInputBox(null);
};
// 自动聚焦输入框
React.useEffect(() => {
if (inputBox && inputRef.current) {
inputRef.current.focus();
}
}, [inputBox]);
const handleZoom = (factor, centerX, centerY) => {
setViewTransform(prev => {
const newScale = prev.scale * factor;
const dx = (centerX - prev.x) * (1 - factor);
const dy = (centerY - prev.y) * (1 - factor);
return { scale: newScale, x: prev.x + dx, y: prev.y + dy };
});
};
const handleWheel = (e) => {
e.preventDefault();
const rect = viewerRef.current.getBoundingClientRect();
const factor = e.deltaY > 0 ? 1 / 1.1 : 1.1;
handleZoom(factor, e.clientX - rect.left, e.clientY - rect.top);
};
React.useEffect(() => {
const viewer = viewerRef.current;
if (viewer) {
const opts = { passive: false };
viewer.addEventListener('pointerdown', handleMouseDown, opts);
viewer.addEventListener('click', handleCanvasClick, opts);
document.addEventListener('pointermove', handleMouseMove, opts);
document.addEventListener('pointerup', handleMouseUp, opts);
viewer.addEventListener('wheel', handleWheel, opts);
}
return () => {
if (viewer) {
viewer.removeEventListener('pointerdown', handleMouseDown);
viewer.removeEventListener('click', handleCanvasClick);
document.removeEventListener('pointermove', handleMouseMove);
document.removeEventListener('pointerup', handleMouseUp);
viewer.removeEventListener('wheel', handleWheel);
}
};
}, []);
if (!item) {
return React.createElement('div', { className: 'workspace-panel flex items-center justify-center h-full' },
React.createElement('p', { className: 'text-gray-500' }, '请在左侧选择一个件号')
);
}
if (!drawing) {
return React.createElement('div', { className: 'workspace-panel flex flex-col items-center justify-center h-full' },
React.createElement('p', { className: 'text-gray-500 mb-4' }, `件号 "${item.id}" 尚未绑定图纸`),
);
}
// 首先定义 isRoot 变量
const isRoot = item && item.id === '__ROOT__';
// 根据当前选中的item决定要显示的数据
// 如果是根节点,显示第一层子节点;否则显示当前节点的子节点
const displayItems = isRoot
? Object.values(items).filter(it => it.parentId === '__ROOT__')
: [item]; // 对于非根节点,显示自身信息
const tableWidth = template ? Math.max(...template.column_boundaries) : 0;
// Use position from state, or default to bottom-right
let tablePos = tablePositions[drawing.id];
if (!tablePos) {
const img = drawing.images && drawing.images[0];
if (img) {
const safeX = img.coords[0].x + img.width - tableWidth - 10;
const safeY = img.coords[0].y - 10;
tablePos = { x: safeX, y: safeY };
} else {
tablePos = { x: drawing.maxX - tableWidth, y: drawing.minY };
}
}
return React.createElement('div', { className: 'workspace-panel h-full relative bg-gray-50 overflow-hidden', ref: viewerRef, style: { cursor: 'grab', touchAction: 'none' } },
// 动态输入框 - 点击图片时出现
inputBox && React.createElement('div', {
className: 'absolute z-20 input-overlay',
style: {
left: inputBox.x - 50,
top: inputBox.y - 15,
transform: 'translate(-50%, -50%)'
}
},
React.createElement('div', { className: 'bg-white p-2 rounded shadow-lg border flex items-center gap-2' },
React.createElement('input', {
ref: inputRef,
type: 'text',
placeholder: '输入件号',
className: 'form-input w-24 text-sm py-1 px-2',
onKeyDown: (e) => {
if (e.key === 'Enter') {
handleInputBoxSubmit(e.target.value);
} else if (e.key === 'Escape') {
handleInputBoxCancel();
}
e.stopPropagation();
},
onClick: (e) => e.stopPropagation(),
onBlur: () => setTimeout(() => setInputBox(null), 200) // 延迟关闭,允许用户点击确认
}),
React.createElement('button', {
className: 'bg-blue-500 text-white px-2 py-1 text-xs rounded hover:bg-blue-600',
onClick: (e) => {
handleInputBoxSubmit(inputRef.current.value);
e.stopPropagation();
}
}, '确认')
)
),
// 左上:输入区(阻止事件冒泡,避免影响拖拽/缩放)
React.createElement('div', { className: 'absolute top-4 left-4 z-10 ui-overlay' },
React.createElement('div', { className: 'bg-white p-2 rounded-lg shadow-md flex items-center gap-2' },
React.createElement('span', { className: 'text-sm font-medium mr-2' }, '设置图片件号:'),
React.createElement('input', {
type: 'text',
placeholder: '输入件号,存在则绑定,不存在则创建',
className: 'form-input w-64',
value: editedItemId,
onChange: (e) => setEditedItemId(e.target.value),
onKeyDown: (e) => { if (e.key === 'Enter') (items[editedItemId] ? onBindDrawing(editedItemId, drawing.id) : handleCreateUnderRoot()); }
}),
React.createElement('button', { className: 'btn-primary', onClick: () => (items[editedItemId] ? onBindDrawing(editedItemId, drawing.id) : handleCreateUnderRoot()) }, '绑定')
)
),
// 右上:缩放按钮(内联 SVG
React.createElement('div', { className: 'absolute top-4 right-4 z-10 bg-white p-1 rounded-lg shadow-md flex flex-col items-center gap-1 ui-overlay' },
React.createElement('button', { className: 'p-2 hover:bg-gray-100 rounded', onClick: () => handleZoom(1.2, viewerRef.current.offsetWidth/2, viewerRef.current.offsetHeight/2) },
React.createElement('svg', { width: 20, height: 20, viewBox: '0 0 24 24', fill: 'none', stroke: '#1f2937', strokeWidth: 2 },
React.createElement('circle', { cx: 11, cy: 11, r: 7 }),
React.createElement('line', { x1: 21, y1: 21, x2: 16.65, y2: 16.65 }),
React.createElement('line', { x1: 11, y1: 8, x2: 11, y2: 14 }),
React.createElement('line', { x1: 8, y1: 11, x2: 14, y2: 11 })
)
),
React.createElement('button', { className: 'p-2 hover:bg-gray-100 rounded', onClick: () => handleZoom(1 / 1.2, viewerRef.current.offsetWidth/2, viewerRef.current.offsetHeight/2) },
React.createElement('svg', { width: 20, height: 20, viewBox: '0 0 24 24', fill: 'none', stroke: '#1f2937', strokeWidth: 2 },
React.createElement('circle', { cx: 11, cy: 11, r: 7 }),
React.createElement('line', { x1: 21, y1: 21, x2: 16.65, y2: 16.65 }),
React.createElement('line', { x1: 8, y1: 11, x2: 14, y2: 11 })
)
)
),
// 左右切换按钮(内联 SVG
React.createElement('button', {
className: 'absolute left-4 top-1/2 -translate-y-1/2 z-10 p-3 bg-white/60 rounded-full hover:bg-white transition-colors shadow-md',
onClick: () => onCycleDrawing(-1),
onMouseDown: (e) => e.stopPropagation()
},
React.createElement('svg', { width: 28, height: 28, viewBox: '0 0 24 24', fill: 'none', stroke: '#1f2937', strokeWidth: 2 },
React.createElement('polyline', { points: '15 18 9 12 15 6' })
)
),
React.createElement('button', {
className: 'absolute right-4 top-1/2 -translate-y-1/2 z-10 p-3 bg-white/60 rounded-full hover:bg-white transition-colors shadow-md',
onClick: () => onCycleDrawing(1),
onMouseDown: (e) => e.stopPropagation()
},
React.createElement('svg', { width: 28, height: 28, viewBox: '0 0 24 24', fill: 'none', stroke: '#1f2937', strokeWidth: 2 },
React.createElement('polyline', { points: '9 18 15 12 9 6' })
)
),
// SVG Canvas
React.createElement('svg', { width: '100%', height: '100%'},
React.createElement('g', { transform: `translate(${viewTransform.x}, ${viewTransform.y}) scale(${viewTransform.scale})` },
// Images
drawing.images.map(img => React.createElement('image', {
key: img.name,
className: 'draggable-image',
href: img.url,
x: img.coords[0].x,
y: img.coords[0].y - img.height, // SVG y starts from top, our coords from bottom
width: img.width,
height: img.height,
'data-image-name': img.name,
'data-draggable': 'draggable-image',
style: { cursor: 'move' },
onError: (e) => {
console.warn(`图片加载失败: ${img.url}`);
// 可以设置一个默认的占位符
e.target.style.opacity = '0.3';
e.target.style.fill = '#f3f4f6';
}
})),
// Graphical Table - 当件号绑定了图纸时渲染(包括根节点总装图)
template && item && item.drawingId && drawing && drawing.id === item.drawingId && React.createElement('g', {
className: 'graphical-table',
transform: `translate(${tablePos.x}, ${tablePos.y}) scale(1, -1)`,
style: { cursor: 'move' },
'data-draggable': 'graphical-table'
},
// 背景矩形 - 提供拖拽区域和视觉反馈
React.createElement('rect', {
x: -5,
y: -5,
width: Math.max(...template.column_boundaries) + 10,
height: template.header_height + (displayItems.length * template.row_height) + 10,
fill: 'rgba(37, 99, 235, 0.05)', // 轻微的蓝色背景
stroke: 'rgba(37, 99, 235, 0.2)',
strokeWidth: 1,
rx: 3, // 圆角
pointerEvents: 'all',
'data-draggable': 'graphical-table',
style: { cursor: 'move' }
}),
// 表格内容
React.createElement(GraphicalTable, { template, items: displayItems, allItems: items, drawings })
)
)
)
);
}