586 lines
26 KiB
JavaScript
586 lines
26 KiB
JavaScript
// 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;
|
||
|
||
const target = e.target;
|
||
const hitTable = isInClass(e.target, 'graphical-table');
|
||
const hitImage = isInClass(e.target, 'draggable-image');
|
||
|
||
// 检查是否点击了表格
|
||
if (hitTable) {
|
||
isDraggingTable.current = true;
|
||
lastMousePos.current = { x: e.clientX, y: e.clientY };
|
||
if (viewerRef.current) viewerRef.current.style.cursor = 'move';
|
||
try {
|
||
if (e.target.setPointerCapture && typeof e.pointerId === 'number') {
|
||
e.target.setPointerCapture(e.pointerId);
|
||
}
|
||
} catch (err) {}
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
return;
|
||
}
|
||
|
||
// 检查是否点击了图片
|
||
if (hitImage) {
|
||
isDraggingImage.current = true;
|
||
draggedImageName.current = e.target.getAttribute('data-image-name');
|
||
lastMousePos.current = { x: e.clientX, y: e.clientY };
|
||
if (viewerRef.current) viewerRef.current.style.cursor = 'move';
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
return;
|
||
}
|
||
|
||
// 否则开始画布平移
|
||
isPanning.current = true;
|
||
lastMousePos.current = { x: e.clientX, y: e.clientY };
|
||
if (viewerRef.current) 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) {
|
||
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 };
|
||
})();
|
||
setTablePositions(prev => {
|
||
const base = prev[tableId] || defaultPos;
|
||
const updated = {
|
||
x: base.x + (dx / viewTransform.scale),
|
||
y: base.y + (dy / viewTransform.scale)
|
||
};
|
||
return { ...prev, [tableId]: updated };
|
||
});
|
||
|
||
} 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) => {
|
||
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',
|
||
onPointerDown: (e) => { handleMouseDown(e); },
|
||
onPointerMove: (e) => { if (isDraggingTable.current) { handleMouseMove(e); } },
|
||
onPointerUp: (e) => { handleMouseUp(e); }
|
||
},
|
||
// 背景矩形 - 提供拖拽区域和视觉反馈
|
||
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' },
|
||
onPointerDown: (e) => { handleMouseDown(e); },
|
||
onPointerMove: (e) => { if (isDraggingTable.current) { handleMouseMove(e); } },
|
||
onPointerUp: (e) => { handleMouseUp(e); }
|
||
}),
|
||
// 表格内容
|
||
React.createElement(GraphicalTable, { template, items: displayItems, allItems: items, drawings })
|
||
)
|
||
)
|
||
)
|
||
);
|
||
}
|