本次提交对图纸工作台进行了全面的功能重构和交互体验升级,主要涵盖了数据结构优化、核心逻辑增强、以及全新的可视化交互功能。
### 主要更新内容:
#### 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 变量,统一了颜色、尺寸等设计规范。
515 lines
30 KiB
JavaScript
515 lines
30 KiB
JavaScript
// Use React.* APIs without destructuring to avoid global redeclaration conflicts
|
||
|
||
// 模拟的初始数据
|
||
const pictureInfo = [
|
||
{
|
||
"图片名": "图1",
|
||
"图片世界id": "A$C2EB80DB8",
|
||
"图片图号": "NCY24-S008-00",
|
||
"世界坐标": [
|
||
{ "x": 559.314, "y": -2484.602 },
|
||
{ "x": 1153.314, "y": -2484.602 },
|
||
{ "x": 1153.314, "y": -1643.602 },
|
||
{ "x": 559.314, "y": -1643.602 }
|
||
]
|
||
},
|
||
{
|
||
"图片名": "图2-1",
|
||
"图片世界id": "A$C1CC9093B_1",
|
||
"图片图号": "NCY24-S008-01",
|
||
"世界坐标": [
|
||
{ "x": 1288.8, "y": -2458.3 },
|
||
{ "x": 1691.8, "y": -2458.3 },
|
||
{ "x": 1691.8, "y": -1884.3 },
|
||
{ "x": 1288.8, "y": -1884.3 }
|
||
]
|
||
},
|
||
{
|
||
"图片名": "图2-2",
|
||
"图片世界id": "A$C1CC9093B_1",
|
||
"图片图号": "NCY24-S008-01",
|
||
"世界坐标": [
|
||
{ "x": 1691.8, "y": -2171.3 },
|
||
{ "x": 1893.3, "y": -2171.3 },
|
||
{ "x": 1893.3, "y": -1884.3 },
|
||
{ "x": 1691.8, "y": -1884.3 }
|
||
]
|
||
},
|
||
{
|
||
"图片名": "图2-3",
|
||
"图片世界id": "A$C1CC9093B_1",
|
||
"图片图号": "NCY24-S008-01",
|
||
"世界坐标": [
|
||
{ "x": 1893.3, "y": -2171.3 },
|
||
{ "x": 2094.8, "y": -2171.3 },
|
||
{ "x": 2094.8, "y": -1884.3 },
|
||
{ "x": 1893.3, "y": -1884.3 }
|
||
]
|
||
},
|
||
{
|
||
"图片名": "图2-4",
|
||
"图片世界id": "A$C1CC9093B_1",
|
||
"图片图号": "NCY24-S008-01",
|
||
"世界坐标": [
|
||
{ "x": 1691.8, "y": -2458.3 },
|
||
{ "x": 2094.8, "y": -2458.3 },
|
||
{ "x": 2094.8, "y": -2171.3 },
|
||
{ "x": 1691.8, "y": -2171.3 }
|
||
]
|
||
},
|
||
{
|
||
"图片名": "图3-1",
|
||
"图片世界id": "A$C1CC9093B_2",
|
||
"图片图号": "NCY24-S008-02",
|
||
"世界坐标": [
|
||
{ "x": 2237.5, "y": -2458.3 },
|
||
{ "x": 2640.5, "y": -2458.3 },
|
||
{ "x": 2640.5, "y": -1884.3 },
|
||
{ "x": 2237.5, "y": -1884.3 }
|
||
]
|
||
},
|
||
{
|
||
"图片名": "图3-2",
|
||
"图片世界id": "A$C1CC9093B_2",
|
||
"图片图号": "NCY24-S008-02",
|
||
"世界坐标": [
|
||
{ "x": 2640.5, "y": -2458.3 },
|
||
{ "x": 3043.5, "y": -2458.3 },
|
||
{ "x": 3043.5, "y": -1884.3 },
|
||
{ "x": 2640.5, "y": -1884.3 }
|
||
]
|
||
}
|
||
];
|
||
|
||
// New logic: Treat each picture as a separate drawing
|
||
const processedDrawings = {};
|
||
pictureInfo.forEach(info => {
|
||
const imageName = info["图片名"];
|
||
const coords = info["世界坐标"];
|
||
|
||
const minX = Math.min(coords[0].x, coords[1].x, coords[2].x, coords[3].x);
|
||
const minY = Math.min(coords[0].y, coords[1].y, coords[2].y, coords[3].y);
|
||
const maxX = Math.max(coords[0].x, coords[1].x, coords[2].x, coords[3].x);
|
||
const maxY = Math.max(coords[0].y, coords[1].y, coords[2].y, coords[3].y);
|
||
|
||
processedDrawings[imageName] = {
|
||
id: imageName,
|
||
name: `图纸 ${imageName}`,
|
||
drawingNumber: info["图片图号"],
|
||
// Each drawing now has only one image
|
||
images: [{
|
||
url: `图片/${imageName}_01.png`,
|
||
name: imageName,
|
||
coords: coords,
|
||
width: Math.abs(coords[1].x - coords[0].x),
|
||
height: Math.abs(coords[2].y - coords[1].y)
|
||
}],
|
||
minX,
|
||
minY,
|
||
maxX,
|
||
maxY,
|
||
};
|
||
});
|
||
|
||
|
||
const initialData = {
|
||
drawings: processedDrawings,
|
||
items: {
|
||
'__ROOT__': { id: '__ROOT__', name: '总装图', quantity: 1, weight: 0, drawingId: '图1', parentId: null, children: [] },
|
||
},
|
||
// Positions are now per-drawing (which is per-image)
|
||
tablePositions: {
|
||
'图1': { x: 50, y: 50 }, // Example default
|
||
},
|
||
templates: {
|
||
'zongzhuang': { "template_name": "标准物料清单-底部表头", "row_height": 8.0, "header_height": 14.0, "column_boundaries": [0.0, 15.0, 45.0, 100.0, 110.0, 140.0, 150.0, 160.0, 185.0], "header_definition": { "lines": [{ "start": [0.0, 14.0], "end": [185.0, 14.0] }, { "start": [160.0, 6.5], "end": [140.0, 6.5] }, { "start": [110.0, 0.0], "end": [110.0, 14.0] }, { "start": [15.0, 14.0], "end": [15.0, 0.0] }, { "start": [45.0, 0.0], "end": [45.0, 14.0] }, { "start": [100.0, 0.0], "end": [100.0, 14.0] }, { "start": [140.0, 0.0], "end": [140.0, 14.0] }, { "start": [160.0, 0.0], "end": [160.0, 14.0] }, { "start": [150.0, 6.5], "end": [150.0, 14.0] }, { "start": [0.0, 0.0], "end": [185.0, 0.0] }, { "start": [185.0, 0.0], "end": [185.0, 14.0] }], "texts": [{ "content": "总", "relative_pos": [153.62, 10.23], "height": 3.5 }, { "content": "单", "relative_pos": [143.79, 10.02], "height": 3.5 }, { "content": "TOTAL", "relative_pos": [152.64, 7.45], "height": 2.0 }, { "content": "SINGLE", "relative_pos": [141.89, 7.45], "height": 2.0 }, { "content": "备 注", "relative_pos": [169.33, 6.8], "alignment": "BOTTOM_CENTER", "height": 3.5 }, { "content": "材 料", "relative_pos": [121.56, 6.56], "height": 3.5 }, { "content": "数量", "relative_pos": [102.5, 6.56], "height": 3.5 }, { "content": "名 称", "relative_pos": [67.3, 6.56], "height": 3.5 }, { "content": "件 号", "relative_pos": [4.43, 6.56], "height": 3.5 }, { "content": "图号或标准号", "relative_pos": [22.43, 6.53], "height": 3.5 }, { "content": "MAT'L", "relative_pos": [122.94, 2.33], "height": 2.0 }, { "content": "QTY.", "relative_pos": [102.89, 2.33], "height": 2.0 }, { "content": "REMARKS", "relative_pos": [165.48, 2.3], "height": 2.0 }, { "content": "PARTS. NAME.", "relative_pos": [65.06, 1.76], "height": 2.0 }, { "content": "DWG NO. OR STD. NO.", "relative_pos": [19.43, 1.76], "height": 2.0 }, { "content": "PARTS .NO.", "relative_pos": [7.77, 1.76], "alignment": "BOTTOM_CENTER", "height": 2.0 }, { "content": "MASS(kg)", "relative_pos": [148.32, 1.7], "height": 2.0 }, { "content": "质量", "relative_pos": [142.49, 0.87], "height": 3.5 }] }, "column_definitions": [{ "name": "件号", "relative_x_start": 0.0, "text_definitions": [{ "data_key": "main", "relative_pos": [5.82, 1.46], "alignment": "BOTTOM_LEFT", "height": 3.5 }] }, { "name": "图号或标准号", "relative_x_start": 15.0, "text_definitions": [{ "data_key": "main", "relative_pos": [14.77, 1.15], "alignment": "BOTTOM_CENTER", "height": 3.0 }] }, { "name": "名称", "relative_x_start": 45.0, "text_definitions": [{ "data_key": "chinese_name", "relative_pos": [1.76, 3.88], "height": 3.5 }, { "data_key": "english_name", "relative_pos": [1.79, 1.0], "height": 2.0 }, { "data_key": "specification", "relative_pos": [18.68, 3.9], "height": 3.0 }] }, { "name": "数量", "relative_x_start": 100.0, "text_definitions": [{ "data_key": "main", "relative_pos": [4.97, 1.37], "alignment": "BOTTOM_CENTER", "height": 3.0 }] }, { "name": "材料", "relative_x_start": 110.0, "text_definitions": [{ "data_key": "main", "relative_pos": [15.16, 1.12], "alignment": "BOTTOM_CENTER", "height": 3.5 }] }, { "name": "单", "relative_x_start": 140.0, "text_definitions": [{ "data_key": "main", "relative_pos": [5.06, 1.42], "alignment": "BOTTOM_CENTER", "height": 3.0 }] }, { "name": "总", "relative_x_start": 150.0, "text_definitions": [{ "data_key": "main", "relative_pos": [5.06, 1.42], "alignment": "BOTTOM_CENTER", "height": 3.0 }] }, { "name": "备注", "relative_x_start": 160.0, "text_definitions": [{ "data_key": "main", "relative_pos": [12.06, 1.42], "alignment": "BOTTOM_CENTER", "height": 3.0 }] }] },
|
||
'lingjian': { "template_name": "标准物料清单-底部表头", "row_height": 12.0, "header_height": 12.0, "column_boundaries": [0.0, 20.0, 65.0, 95.0, 115.0, 130.0, 155.0, 180.0], "header_definition": { "lines": [{ "start": [0.0, 0.0], "end": [180.0, 0.0] }, { "start": [0.0, 0.0], "end": [0.0, 12.0] }, { "start": [20.0, 0.0], "end": [20.0, 12.0] }, { "start": [65.0, 0.0], "end": [65.0, 12.0] }, { "start": [95.0, 0.0], "end": [95.0, 12.0] }, { "start": [115.0, 0.0], "end": [115.0, 12.0] }, { "start": [130.0, 0.0], "end": [130.0, 12.0] }, { "start": [155.0, 0.0], "end": [155.0, 12.0] }, { "start": [180.0, 0.0], "end": [180.0, 12.0] }], "texts": [{ "content": "件 号", "relative_pos": [6.16, 6.25], "height": 3.5 }, { "content": "PART NO.", "relative_pos": [5.29, 1.89], "height": 2.0 }, { "content": "名 称", "relative_pos": [35.60, 6.25], "height": 3.5 }, { "content": "PARTS. NAME.", "relative_pos": [35.45, 1.89], "height": 2.0 }, { "content": "材 料", "relative_pos": [75.65, 6.25], "height": 3.5 }, { "content": "MAT'L", "relative_pos": [76.19, 1.89], "height": 2.0 }, { "content": "质量", "relative_pos": [98.70, 6.25], "height": 3.5 }, { "content": "(kg)", "relative_pos": [104.32, 6.25], "height": 3 }, { "content": "MASS", "relative_pos": [102.15, 1.89], "height": 2.0 }, { "content": "比 例", "relative_pos": [119.09, 6.25], "height": 3.5 }, { "content": "SCALE", "relative_pos": [119.04, 1.89], "height": 2.0 }, { "content": "所在图号", "relative_pos": [137.23, 6.25], "height": 3.5 }, { "content": "DWG. NO.", "relative_pos": [138.16, 1.89], "height": 2.0 }, { "content": "装配图号", "relative_pos": [162.59, 6.25], "height": 3.5 }, { "content": "ASSY. DWG. NO.", "relative_pos": [160.50, 1.89], "height": 2.0 }] }, "column_definitions": [{ "name": "件号", "relative_x_start": 0.0, "text_definitions": [{ "data_key": "main", "relative_pos": [7.5, 2.17], "alignment": "BOTTOM_CENTER", "height": 3.5 }] }, { "name": "名称", "relative_x_start": 20.0, "text_definitions": [{ "data_key": "chinese_name", "relative_pos": [2.0, 3.58], "alignment": "BOTTOM_LEFT", "height": 3.5 }, { "data_key": "english_name", "relative_pos": [2.0, 1.0], "alignment": "BOTTOM_LEFT", "height": 2.0 }, { "data_key": "specification", "relative_pos": [20.0, 4.1], "alignment": "BOTTOM_LEFT", "height": 3.0 }] }, { "name": "材料", "relative_x_start": 65.0, "text_definitions": [{ "data_key": "main", "relative_pos": [15.0, 2.17], "alignment": "BOTTOM_CENTER", "height": 3.5 }, { "data_key": "chinese_name", "relative_pos": [15.0, 3.58], "alignment": "BOTTOM_CENTER", "height": 3.5 }, { "data_key": "english_name", "relative_pos": [15.0, 1.0], "alignment": "BOTTOM_CENTER", "height": 2.0 }] }, { "name": "质量", "relative_x_start": 95.0, "text_definitions": [{ "data_key": "main", "relative_pos": [10.0, 2.17], "alignment": "BOTTOM_CENTER", "height": 3.5 }] }, { "name": "比例", "relative_x_start": 115.0, "text_definitions": [{ "data_key": "main", "relative_pos": [7.5, 2.17], "alignment": "BOTTOM_CENTER", "height": 3.5 }] }, { "name": "所在图号", "relative_x_start": 130.0, "text_definitions": [{ "data_key": "main", "relative_pos": [12.5, 2.17], "alignment": "BOTTOM_CENTER", "height": 3.5 }] }, { "name": "装配图号", "relative_x_start": 155.0, "text_definitions": [{ "data_key": "main", "relative_pos": [12.5, 2.17], "alignment": "BOTTOM_CENTER", "height": 3.5 }] }] },
|
||
'lingjian_sub': { "template_name": "标准物料清单-底部表头", "row_height": 8.0, "header_height": 14.0, "column_boundaries": [0.0, 15.0, 45.0, 100.0, 110.0, 140.0, 150.0, 160.0, 180.0], "header_definition": { "lines": [{ "start": [0.0, 14.0], "end": [185.0, 14.0] }, { "start": [160.0, 6.5], "end": [140.0, 6.5] }, { "start": [110.0, 0.0], "end": [110.0, 14.0] }, { "start": [15.0, 14.0], "end": [15.0, 0.0] }, { "start": [45.0, 0.0], "end": [45.0, 14.0] }, { "start": [100.0, 0.0], "end": [100.0, 14.0] }, { "start": [140.0, 0.0], "end": [140.0, 14.0] }, { "start": [160.0, 0.0], "end": [160.0, 14.0] }, { "start": [150.0, 6.5], "end": [150.0, 14.0] }, { "start": [0.0, 0.0], "end": [185.0, 0.0] }, { "start": [180.0, 0.0], "end": [180.0, 14.0] }], "texts": [{ "content": "总", "relative_pos": [153.62, 10.23], "alignment": "BOTTOM_LEFT", "height": 3.5 }, { "content": "单", "relative_pos": [143.79, 10.02], "alignment": "BOTTOM_LEFT", "height": 3.5 }, { "content": "TOTAL", "relative_pos": [152.64, 7.45], "alignment": "BOTTOM_LEFT", "height": 2.0 }, { "content": "SINGLE", "relative_pos": [141.89, 7.45], "alignment": "BOTTOM_LEFT", "height": 2.0 }, { "content": "备 注", "relative_pos": [169.33, 6.8], "alignment": "BOTTOM_CENTER", "height": 3.5 }, { "content": "材 料", "relative_pos": [121.56, 6.56], "alignment": "BOTTOM_LEFT", "height": 3.5 }, { "content": "数量", "relative_pos": [102.5, 6.56], "alignment": "BOTTOM_LEFT", "height": 3.5 }, { "content": "名 称", "relative_pos": [67.3, 6.56], "alignment": "BOTTOM_LEFT", "height": 3.5 }, { "content": "件 号", "relative_pos": [4.43, 6.56], "alignment": "BOTTOM_LEFT", "height": 3.5 }, { "content": "图号或标准号", "relative_pos": [22.43, 6.53], "alignment": "BOTTOM_LEFT", "height": 3.5 }, { "content": "MAT'L", "relative_pos": [122.94, 2.33], "alignment": "BOTTOM_LEFT", "height": 2.0 }, { "content": "QTY.", "relative_pos": [102.89, 2.33], "alignment": "BOTTOM_LEFT", "height": 2.0 }, { "content": "REMARKS", "relative_pos": [165.48, 2.3], "alignment": "BOTTOM_LEFT", "height": 2.0 }, { "content": "PARTS. NAME.", "relative_pos": [65.06, 1.76], "alignment": "BOTTOM_LEFT", "height": 2.0 }, { "content": "DWG NO. OR STD. NO.", "relative_pos": [19.43, 1.76], "alignment": "BOTTOM_LEFT", "height": 2.0 }, { "content": "PARTS .NO.", "relative_pos": [7.77, 1.76], "alignment": "BOTTOM_CENTER", "height": 2.0 }, { "content": "MASS(kg)", "relative_pos": [148.32, 1.7], "alignment": "BOTTOM_LEFT", "height": 2.0 }, { "content": "质量", "relative_pos": [142.49, 0.87], "alignment": "BOTTOM_LEFT", "height": 3.5 }] }, "column_definitions": [{ "name": "件 号", "relative_x_start": 0.0, "text_definitions": [{ "data_key": "main", "relative_pos": [5.82, 1.46], "alignment": "BOTTOM_LEFT", "height": 3.5 }] }, { "name": "图号或标准号", "relative_x_start": 15.0, "text_definitions": [{ "data_key": "main", "relative_pos": [14.77, 1.15], "alignment": "BOTTOM_CENTER", "height": 3.0 }] }, { "name": "名 称", "relative_x_start": 45.0, "text_definitions": [{ "data_key": "chinese_name", "relative_pos": [1.76, 3.88], "height": 3.5 }, { "data_key": "english_name", "relative_pos": [1.79, 1.0], "height": 2.0 }, { "data_key": "specification", "relative_pos": [18.68, 3.9], "height": 3.0 }] }, { "name": "数量", "relative_x_start": 100.0, "text_definitions": [{ "data_key": "main", "relative_pos": [4.97, 1.37], "alignment": "BOTTOM_CENTER", "height": 3.0 }] }, { "name": "材 料", "relative_x_start": 110.0, "text_definitions": [{ "data_key": "main", "relative_pos": [15.16, 1.12], "alignment": "BOTTOM_CENTER", "height": 3.5 }] }, { "name": "单", "relative_x_start": 140.0, "text_definitions": [{ "data_key": "main", "relative_pos": [5.06, 1.42], "alignment": "BOTTOM_CENTER", "height": 3.0 }] }, { "name": "总", "relative_x_start": 150.0, "text_definitions": [{ "data_key": "main", "relative_pos": [5.06, 1.42], "alignment": "BOTTOM_CENTER", "height": 3.0 }] }, { "name": "备 注", "relative_x_start": 160.0, "text_definitions": [{ "data_key": "main", "relative_pos": [10.06, 1.42], "alignment": "BOTTOM_CENTER", "height": 3.0 }] }] }
|
||
}
|
||
};
|
||
|
||
function App() {
|
||
const [drawings, setDrawings] = React.useState(initialData.drawings);
|
||
const [items, setItems] = React.useState(initialData.items);
|
||
const [tablePositions, setTablePositions] = React.useState(initialData.tablePositions);
|
||
const [templates, setTemplates] = React.useState(initialData.templates);
|
||
const [selectedItemId, setSelectedItemId] = React.useState('__ROOT__');
|
||
const [currentDrawingId, setCurrentDrawingId] = React.useState(null);
|
||
|
||
// 根据item层级决定使用哪个模板
|
||
const getTemplateForItem = (itemId) => {
|
||
if (!itemId) return null;
|
||
if (itemId === '__ROOT__') {
|
||
return templates.zongzhuang;
|
||
}
|
||
const depth = (itemId.match(/-/g) || []).length;
|
||
if (depth === 0) { // e.g. '1', '2' -> children of root are parts.
|
||
return templates.lingjian;
|
||
}
|
||
// e.g. '1-1', '1-2'
|
||
return templates.lingjian_sub;
|
||
};
|
||
|
||
// 自动计算重量的副作用钩子
|
||
React.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 (Math.abs(items[itemId].weight - childrenWeight) > 0.001) {
|
||
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 getDrawingForItem = (itemId) => {
|
||
const mapping = {
|
||
'__ROOT__': '图1', // 总装图 -> 图1
|
||
'1': '图2-1', // 件号1 -> 图2-1
|
||
'1-1': '图2-1', // 件号1-1 -> 图2-1
|
||
'1-2': '图2-2', // 件号1-2 -> 图2-2
|
||
'2': '图2-3', // 件号2 -> 图2-3
|
||
'2-1': '图2-4', // 件号2-1 -> 图2-4
|
||
'3': '图3-1', // 件号3 -> 图3-1
|
||
'3-1': '图3-2' // 件号3-1 -> 图3-2
|
||
};
|
||
|
||
// 如果有直接映射,使用映射;否则尝试解析件号寻找对应图纸
|
||
if (mapping[itemId]) {
|
||
return mapping[itemId];
|
||
}
|
||
|
||
// 对于新创建的件号,尝试根据层级推导对应图纸
|
||
if (itemId.includes('-')) {
|
||
const parts = itemId.split('-');
|
||
if (parts.length === 2) {
|
||
// 二级件号,尝试找到对应的图2-x或图3-x系列
|
||
const base = parts[0];
|
||
if (base === '1') return '图2-1';
|
||
if (base === '2') return '图2-3';
|
||
if (base === '3') return '图3-1';
|
||
}
|
||
}
|
||
|
||
// 默认返回图1
|
||
return '图1';
|
||
};
|
||
|
||
// 根据选中的件号自动切换图纸显示
|
||
React.useEffect(() => {
|
||
const targetDrawingId = getDrawingForItem(selectedItemId);
|
||
if (currentDrawingId !== targetDrawingId) {
|
||
setCurrentDrawingId(targetDrawingId);
|
||
}
|
||
}, [selectedItemId, currentDrawingId]);
|
||
|
||
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 ids = Object.keys(drawings);
|
||
if (ids.length === 0) return;
|
||
let idx = ids.indexOf(currentDrawingId || '');
|
||
if (idx === -1) idx = 0;
|
||
let next = idx + direction;
|
||
if (next >= ids.length) next = 0;
|
||
if (next < 0) next = ids.length - 1;
|
||
setCurrentDrawingId(ids[next]);
|
||
};
|
||
|
||
const handleItemUpdate = (newItemData) => {
|
||
const { id } = newItemData;
|
||
|
||
const ensureAncestors = (itemsMap, targetId) => {
|
||
const parentId = targetId.includes('-') ? targetId.substring(0, targetId.lastIndexOf('-')) : '__ROOT__';
|
||
if (parentId === '__ROOT__') {
|
||
// 根一定存在
|
||
if (!itemsMap['__ROOT__']) {
|
||
itemsMap['__ROOT__'] = { id: '__ROOT__', name: '总装图', quantity: 1, weight: 0, drawingId: Object.keys(drawings)[0] || null, parentId: null, children: [] };
|
||
}
|
||
return '__ROOT__';
|
||
}
|
||
// 确保父存在
|
||
if (!itemsMap[parentId]) {
|
||
const grandParentId = parentId.includes('-') ? parentId.substring(0, parentId.lastIndexOf('-')) : '__ROOT__';
|
||
// 递归确保祖先
|
||
ensureAncestors(itemsMap, parentId);
|
||
itemsMap[parentId] = {
|
||
id: parentId,
|
||
name: `新建部件 ${parentId}`,
|
||
quantity: 1,
|
||
weight: 0,
|
||
drawingId: null,
|
||
parentId: grandParentId,
|
||
children: []
|
||
};
|
||
// 将父加入祖父的 children
|
||
if (itemsMap[grandParentId] && !itemsMap[grandParentId].children.includes(parentId)) {
|
||
itemsMap[grandParentId].children = [...itemsMap[grandParentId].children, parentId];
|
||
}
|
||
}
|
||
return parentId;
|
||
};
|
||
|
||
setItems(prevItems => {
|
||
const newItems = { ...prevItems };
|
||
|
||
// 确保祖先链存在
|
||
const parentId = ensureAncestors(newItems, id);
|
||
|
||
// 创建/更新当前项(parentId 由我们统一设置)
|
||
const merged = { ...newItems[id], ...newItemData, parentId, children: newItems[id]?.children || [] };
|
||
newItems[id] = merged;
|
||
|
||
// 将当前项加入父的 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 = currentDrawingId ? drawings[currentDrawingId] : undefined;
|
||
|
||
// 找到与当前图纸绑定的item,用于右侧编辑区域显示
|
||
const currentDrawingItem = React.useMemo(() => {
|
||
if (!currentDrawingId) return selectedItem;
|
||
|
||
// 查找绑定了当前图纸的item
|
||
const boundItem = Object.values(items).find(item => item.drawingId === currentDrawingId);
|
||
return boundItem || selectedItem;
|
||
}, [currentDrawingId, items, selectedItem]);
|
||
|
||
// 使用与当前图纸绑定的item来选择模板
|
||
const currentTemplate = getTemplateForItem(currentDrawingItem?.id || selectedItemId);
|
||
|
||
const updateImagePosition = (drawingId, imageName, deltaX, deltaY) => {
|
||
setDrawings(prevDrawings => {
|
||
const newDrawings = { ...prevDrawings };
|
||
const drawing = newDrawings[drawingId];
|
||
if (!drawing) return prevDrawings;
|
||
|
||
// 更新图片的世界坐标
|
||
const updatedImages = drawing.images.map(img => {
|
||
if (img.name === imageName) {
|
||
const newCoords = img.coords.map(coord => ({
|
||
x: coord.x + deltaX,
|
||
y: coord.y + deltaY
|
||
}));
|
||
return { ...img, coords: newCoords };
|
||
}
|
||
return img;
|
||
});
|
||
|
||
// 重新计算drawing的边界
|
||
const allCoords = updatedImages.flatMap(img => img.coords);
|
||
const minX = Math.min(...allCoords.map(c => c.x));
|
||
const minY = Math.min(...allCoords.map(c => c.y));
|
||
const maxX = Math.max(...allCoords.map(c => c.x));
|
||
const maxY = Math.max(...allCoords.map(c => c.y));
|
||
|
||
newDrawings[drawingId] = {
|
||
...drawing,
|
||
images: updatedImages,
|
||
minX,
|
||
minY,
|
||
maxX,
|
||
maxY
|
||
};
|
||
|
||
return newDrawings;
|
||
});
|
||
};
|
||
|
||
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: currentDrawingItem,
|
||
items,
|
||
tablePositions,
|
||
setTablePositions,
|
||
drawings,
|
||
template: currentTemplate,
|
||
onItemUpdate: handleItemUpdate,
|
||
onItemRename: handleItemRename,
|
||
onBindDrawing: bindDrawingToItem,
|
||
onCycleDrawing: handleCycleDrawing,
|
||
onUpdateImagePosition: updateImagePosition
|
||
})
|
||
),
|
||
React.createElement('div', { className: 'col-span-1' },
|
||
React.createElement(DataEditor, { item: currentDrawingItem, items, setItems })
|
||
)
|
||
)
|
||
)
|
||
);
|
||
}
|
||
|
||
const container = document.getElementById('root');
|
||
const root = ReactDOM.createRoot(container);
|
||
root.render(React.createElement(App));
|