298 lines
12 KiB
JavaScript
298 lines
12 KiB
JavaScript
const { useState, useEffect } = React;
|
||
|
||
// 模拟的初始数据
|
||
const initialData = {
|
||
drawings: {
|
||
'drawing-1': { id: 'drawing-1', name: '总装图', drawingNumber: 'Abcd', url: 'assets/images/drawing_main.svg' },
|
||
'drawing-2': { id: 'drawing-2', name: '组件A', drawingNumber: 'Bcde', url: 'assets/images/drawing_part_A.svg' },
|
||
'drawing-3': { id: 'drawing-3', name: '组件B', drawingNumber: 'Cdef', url: null }, // 无图纸
|
||
'drawing-4': { id: 'drawing-4', name: '零件C', drawingNumber: 'Defg', url: null },
|
||
},
|
||
items: {
|
||
'__ROOT__': { id: '__ROOT__', name: '总装图', quantity: 1, weight: 0, drawingId: 'drawing-1', parentId: null, children: ['1', '2'] },
|
||
'1': { id: '1', name: '产品', quantity: 1, weight: 0, drawingId: 'drawing-1', parentId: '__ROOT__', children: ['1-1', '1-2'] },
|
||
'1-1': { id: '1-1', name: '部件A', quantity: 1, weight: 10, drawingId: 'drawing-2', parentId: '1', children: ['1-1-1'] },
|
||
'1-2': { id: '1-2', name: '部件B', quantity: 2, weight: 5, drawingId: 'drawing-3', parentId: '1', children: [] },
|
||
'1-1-1': { id: '1-1-1', name: '零件C', quantity: 3, weight: 2, drawingId: 'drawing-4', parentId: '1-1', children: [] },
|
||
'2': { id: '2', name: '螺丝', quantity: 10, weight: 0.1, drawingId: null, parentId: '__ROOT__', children: [] },
|
||
},
|
||
// 存储表格在图纸上的位置
|
||
tablePositions: {
|
||
'drawing-1': { x: 50, y: 50 },
|
||
'drawing-2': { x: 30, y: 40 },
|
||
'drawing-3': { x: 60, y: 70 },
|
||
}
|
||
};
|
||
|
||
function App() {
|
||
const [drawings, setDrawings] = useState(initialData.drawings);
|
||
const [items, setItems] = useState(initialData.items);
|
||
const [tablePositions, setTablePositions] = useState(initialData.tablePositions);
|
||
const [selectedItemId, setSelectedItemId] = useState('__ROOT__');
|
||
|
||
// 自动计算重量的副作用钩子
|
||
useEffect(() => {
|
||
const calculateWeight = (itemId) => {
|
||
const item = items[itemId];
|
||
if (!item) return 0;
|
||
|
||
if (item.children.length === 0) {
|
||
return item.quantity * (item.weight || 0);
|
||
}
|
||
|
||
const childrenWeight = item.children.reduce((sum, childId) => {
|
||
return sum + calculateWeight(childId);
|
||
}, 0);
|
||
|
||
// 更新当前项的重量(如果是父项)
|
||
if (items[itemId].weight !== childrenWeight) {
|
||
setItems(prevItems => ({
|
||
...prevItems,
|
||
[itemId]: { ...prevItems[itemId], weight: childrenWeight }
|
||
}));
|
||
}
|
||
|
||
return childrenWeight;
|
||
};
|
||
|
||
// 从根节点开始计算
|
||
const rootItems = Object.keys(items).filter(id => items[id].parentId === null);
|
||
rootItems.forEach(calculateWeight);
|
||
|
||
}, [items]); // 依赖于 items 状态
|
||
|
||
const handleItemRename = (oldId, newId) => {
|
||
if (oldId === '__ROOT__') {
|
||
alert('错误:根节点“总装图”的ID不可修改。');
|
||
return;
|
||
}
|
||
if (!newId || oldId === newId) return;
|
||
|
||
if (items[newId]) {
|
||
alert(`错误:件号 "${newId}" 已存在,请使用唯一的件号。`);
|
||
return;
|
||
}
|
||
|
||
setItems(prevItems => {
|
||
const newItems = { ...prevItems };
|
||
|
||
const itemToMove = { ...newItems[oldId] };
|
||
const oldParentId = itemToMove.parentId;
|
||
|
||
// 1. 从旧的父节点移除
|
||
if (oldParentId && newItems[oldParentId]) {
|
||
newItems[oldParentId].children = newItems[oldParentId].children.filter(id => id !== oldId);
|
||
}
|
||
|
||
// 2. 递归更新所有子孙
|
||
const descendantsToUpdate = {};
|
||
const updateDescendantsRecursively = (currentOldId, currentNewId) => {
|
||
const item = newItems[currentOldId];
|
||
if (!item || !item.children) return;
|
||
|
||
item.children.forEach(childOldId => {
|
||
// e.g., child '1-1-1' of '1-1' becomes child '3-1' of '3'
|
||
const newChildId = childOldId.replace(currentOldId, currentNewId);
|
||
|
||
const childItem = { ...newItems[childOldId] };
|
||
childItem.id = newChildId;
|
||
childItem.parentId = currentNewId;
|
||
|
||
descendantsToUpdate[childOldId] = { newId: newChildId, newItem: childItem };
|
||
|
||
updateDescendantsRecursively(childOldId, newChildId);
|
||
});
|
||
};
|
||
updateDescendantsRecursively(oldId, newId);
|
||
|
||
// 3. 应用子孙更新
|
||
Object.values(descendantsToUpdate).forEach(({ newId, newItem }) => {
|
||
newItem.children = newItem.children.map(cid => descendantsToUpdate[cid] ? descendantsToUpdate[cid].newId : cid);
|
||
newItems[newId] = newItem;
|
||
});
|
||
Object.keys(descendantsToUpdate).forEach(oldDescendantId => delete newItems[oldDescendantId]);
|
||
|
||
|
||
// 4. 更新当前项本身
|
||
itemToMove.id = newId;
|
||
itemToMove.parentId = newId.includes('-') ? newId.substring(0, newId.lastIndexOf('-')) : '__ROOT__';
|
||
itemToMove.children = itemToMove.children.map(cid => descendantsToUpdate[cid] ? descendantsToUpdate[cid].newId : cid);
|
||
|
||
delete newItems[oldId];
|
||
newItems[newId] = itemToMove;
|
||
|
||
// 5. 添加到新的父节点
|
||
const newParentId = itemToMove.parentId;
|
||
if (newParentId) {
|
||
if (!newItems[newParentId]) {
|
||
// 如果新父项不存在,则创建它 (e.g. renaming 1-1 to 3-1-1, and 3-1 does not exist)
|
||
newItems[newParentId] = {
|
||
id: newParentId,
|
||
name: `新建部件 ${newParentId}`,
|
||
quantity: 1,
|
||
weight: 0,
|
||
drawingId: null,
|
||
parentId: newParentId.includes('-') ? newParentId.substring(0, newParentId.lastIndexOf('-')) : '__ROOT__',
|
||
children: []
|
||
};
|
||
}
|
||
if (!newItems[newParentId].children.includes(newId)) {
|
||
newItems[newParentId].children.push(newId);
|
||
}
|
||
}
|
||
|
||
return newItems;
|
||
});
|
||
|
||
setSelectedItemId(newId);
|
||
};
|
||
|
||
const handleCycleDrawing = (direction) => {
|
||
const drawingsInUse = [...new Set(Object.values(items).map(i => i.drawingId).filter(Boolean))];
|
||
if (drawingsInUse.length <= 1) return;
|
||
|
||
const currentDrawingId = items[selectedItemId]?.drawingId;
|
||
|
||
let currentIndex = drawingsInUse.indexOf(currentDrawingId);
|
||
|
||
if (currentIndex === -1) currentIndex = 0;
|
||
|
||
let nextIndex = currentIndex + direction;
|
||
if (nextIndex >= drawingsInUse.length) nextIndex = 0;
|
||
if (nextIndex < 0) nextIndex = drawingsInUse.length - 1;
|
||
|
||
const nextDrawingId = drawingsInUse[nextIndex];
|
||
|
||
// 找到第一个使用该图纸的 item 并选中它
|
||
const itemToSelect = Object.values(items).find(i => i.drawingId === nextDrawingId);
|
||
if (itemToSelect) {
|
||
setSelectedItemId(itemToSelect.id);
|
||
}
|
||
};
|
||
|
||
const handleItemUpdate = (newItemData) => {
|
||
const { id } = newItemData;
|
||
|
||
// 检查父节点是否存在
|
||
const parentId = id.includes('-') ? id.substring(0, id.lastIndexOf('-')) : null;
|
||
|
||
setItems(prevItems => {
|
||
const newItems = { ...prevItems };
|
||
|
||
// 如果父节点不存在,则创建
|
||
if (parentId && !newItems[parentId]) {
|
||
newItems[parentId] = {
|
||
id: parentId,
|
||
name: `新建部件 ${parentId}`,
|
||
quantity: 1,
|
||
weight: 0,
|
||
drawingId: null, // 父项通常关联更高级别的图纸
|
||
parentId: parentId.includes('-') ? parentId.substring(0, parentId.lastIndexOf('-')) : null,
|
||
children: []
|
||
};
|
||
}
|
||
|
||
// 更新或创建当前项
|
||
newItems[id] = { ...newItems[id], ...newItemData };
|
||
|
||
// 更新父节点的 children 列表
|
||
if (parentId && newItems[parentId] && !newItems[parentId].children.includes(id)) {
|
||
newItems[parentId] = {
|
||
...newItems[parentId],
|
||
children: [...newItems[parentId].children, id]
|
||
};
|
||
}
|
||
|
||
return newItems;
|
||
});
|
||
|
||
// 选中新创建的项
|
||
setSelectedItemId(id);
|
||
};
|
||
|
||
const bindDrawingToItem = (itemId, drawingId) => {
|
||
setItems(prev => ({
|
||
...prev,
|
||
[itemId]: { ...prev[itemId], drawingId }
|
||
}));
|
||
};
|
||
|
||
const handleSave = () => {
|
||
const emptyItems = Object.values(items).filter(item => {
|
||
return !item.id || (item.weight === 0 && item.children.length === 0);
|
||
});
|
||
|
||
if (emptyItems.length > 0) {
|
||
const itemIds = emptyItems.map(item => item.id || '[空ID]').join(', ');
|
||
alert(`校验失败!\n以下件号存在问题(ID为空或重量为0):\n${itemIds}\n\n请处理后再保存。`);
|
||
return;
|
||
}
|
||
|
||
alert('校验通过,保存成功!\n(原型功能,数据未实际持久化)');
|
||
};
|
||
|
||
const handleExport = () => {
|
||
// 模拟DXF文件内容
|
||
let dxfContent = "0\nSECTION\n2\nENTITIES\n";
|
||
|
||
Object.values(items).forEach(item => {
|
||
dxfContent += "0\nTEXT\n";
|
||
dxfContent += `10\n${Math.random() * 100}\n`; // X coordinate
|
||
dxfContent += `20\n${Math.random() * 100}\n`; // Y coordinate
|
||
dxfContent += `40\n8.0\n`; // Text height
|
||
dxfContent += `1\nID: ${item.id}, Name: ${item.name}, Qty: ${item.quantity}, W: ${item.weight}\n`;
|
||
});
|
||
|
||
dxfContent += "0\nENDSEC\n0\nEOF\n";
|
||
|
||
// 创建并触发下载
|
||
const blob = new Blob([dxfContent], { type: 'application/dxf' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `export_${new Date().toISOString()}.dxf`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(url);
|
||
|
||
alert('DXF文件已开始下载。');
|
||
};
|
||
|
||
const selectedItem = items[selectedItemId];
|
||
|
||
const currentDrawing = drawings[selectedItem?.drawingId];
|
||
|
||
return React.createElement('div', { className: 'flex h-screen bg-gray-100' },
|
||
React.createElement('aside', { className: 'w-[var(--sidebar-width)] bg-white p-4 border-r border-gray-200' },
|
||
React.createElement(StructureTree, { items, selectedItemId, setSelectedItemId })
|
||
),
|
||
React.createElement('main', { className: 'flex-1 flex flex-col' },
|
||
React.createElement(Header, { drawings, items, onSave: handleSave, onExport: handleExport }),
|
||
React.createElement('div', { className: 'flex-1 p-4 grid grid-cols-3 gap-4' },
|
||
React.createElement('div', { className: 'col-span-2' },
|
||
React.createElement(DrawingViewer, {
|
||
drawing: currentDrawing,
|
||
item: selectedItem,
|
||
items,
|
||
tablePositions,
|
||
setTablePositions,
|
||
drawings,
|
||
onItemUpdate: handleItemUpdate, // This is now for creating children, which is not used by the UI but kept for future.
|
||
onItemRename: handleItemRename,
|
||
onBindDrawing: bindDrawingToItem,
|
||
onCycleDrawing: handleCycleDrawing
|
||
})
|
||
),
|
||
React.createElement('div', { className: 'col-span-1' },
|
||
React.createElement(DataEditor, { item: selectedItem, items, setItems })
|
||
)
|
||
)
|
||
)
|
||
);
|
||
}
|
||
|
||
const container = document.getElementById('root');
|
||
const root = ReactDOM.createRoot(container);
|
||
root.render(React.createElement(App));
|