// 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 }) ) ) ) ); }