Initial commit

This commit is contained in:
Sionwu 2025-09-12 18:44:30 +08:00
commit 7fbf56ab1a
15 changed files with 4228 additions and 0 deletions

3
.cursorindexingignore Normal file
View File

@ -0,0 +1,3 @@
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
.specstory/**

2
.specstory/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# SpecStory explanation file
/.what-is-this.md

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,50 @@
<!-- Generated by SpecStory -->
# 使用密钥上传代码到git (2025-09-12 10:36Z)
_**User (2025-09-12 09:02Z)**_
使用密钥 id_rsa 将代码上传到git
---
_**Assistant**_
<think><details><summary>Thought Process</summary>**Confirming SSH Configuration**
I'm starting with verifying the SSH key's existence and configuration. Need to ensure `id_rsa` is present and properly set up before attempting any remote operations. This is the foundational step, and I will detail out the checks needed here.
**Mapping the Workflow**
\n\n
Okay, I've formalized the plan to guide the user through the Git push. First, SSH key verification, then remote and branch checks. After that, I'll tackle staging and committing the files before pushing. I'm focusing on providing explicit instructions, especially regarding branch management. Listing files will help contextualize the project.
</details></think>
---
好的,我将帮助您使用 `id_rsa` 密钥将代码上传到 Git 仓库。
根据您提供的信息,上一次推送失败是因为本地没有 `main` 分支,而您试图推送 `main` 分支。当前您在 `master` 分支上。另外,您的项目中还有很多文件没有被 Git 跟踪。
我将按照以下步骤操作:
1. **添加文件到暂存区**: 将所有项目文件添加到 Git 暂存区。
2. **提交文件**: 创建一个初始提交。
3. **推送到远程仓库**: 将本地的 `master` 分支推送到远程仓库 `origin`
我将先列出当前目录中的文件,以确保所有需要的文件都将被添加。
---
---
---

50
README.md Normal file
View File

@ -0,0 +1,50 @@
# 图纸工作台 - 切分识别与编辑系统 (产品原型)
这是一个用于产品功能展示的交互式前端原型,旨在模拟一个专业的图纸切分、识别与编辑平台的核心功能。
该原型完全使用HTML、CSS和JavaScript通过React库构建不包含任何后端服务或数据库所有数据都存储在浏览器内存中刷新页面会重置所有状态。
## 核心功能
- **层级结构管理**:在左侧树状视图中展示和操作件号的层级关系。
- **图纸与表格预览**:在中间区域展示图纸背景、可拖拽的数据表格,并支持通过输入件号动态创建层级。
- **数据编辑**:在右侧区域编辑选中件号的详细信息,父项的重量等数据会根据子项自动汇总计算。
- **数据校验与导出**:实现了“保存”时的数据校验逻辑,以及模拟生成和下载 `.dxf` 文件的“导出”功能。
## 如何预览
因为这是一个纯静态项目您不需要安装任何依赖如Node.js, npm等。您只需要一个现代浏览器如Chrome, Firefox, Edge, Safari即可。
### 方法一:直接在浏览器中打开 (最简单)
1. 在文件管理器中找到项目所在的文件夹。
2. 直接双击 `index.html` 文件,或者将该文件拖拽到浏览器窗口中。
### 方法二通过本地Web服务器运行 (推荐)
直接从文件系统打开HTML文件有时会因为浏览器的安全策略CORS等导致某些功能如加载外部资源出现问题。虽然本项目目前没有这类问题但通过本地服务器预览是更专业、更可靠的方式。
如果您安装了 Python (macOS 和大多数 Linux 发行版都已预装),可以轻松启动一个本地服务器:
1. **打开终端 (Terminal)**
2. **进入项目目录**
```bash
cd /path/to/your/project/cad编辑页面
```
(请将 `/path/to/your/project/` 替换为实际的项目路径)
3. **启动服务器**
- 如果您使用 Python 3:
```bash
python3 -m http.server 8000
```
- 如果您使用 Python 2:
```bash
python -m SimpleHTTPServer 8000
```
4. **在浏览器中访问**
打开您的浏览器,并访问地址:[http://localhost:8000](http://localhost:8000)
现在您应该能看到并与这个产品原型进行交互了。

297
app.js Normal file
View File

@ -0,0 +1,297 @@
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));

View File

@ -0,0 +1,8 @@
<svg width="800" height="600" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f0f0f0"/>
<text x="50%" y="50%" font-family="Arial" font-size="30" fill="#bbb" text-anchor="middle" dominant-baseline="middle">总装图 (Abcd)</text>
<rect x="100" y="150" width="200" height="150" fill="none" stroke="#999" stroke-width="2"/>
<text x="200" y="225" font-family="Arial" font-size="16" fill="#999" text-anchor="middle">部件A区域</text>
<rect x="500" y="300" width="180" height="200" fill="none" stroke="#999" stroke-width="2"/>
<text x="590" y="400" font-family="Arial" font-size="16" fill="#999" text-anchor="middle">部件B区域</text>
</svg>

After

Width:  |  Height:  |  Size: 694 B

View File

@ -0,0 +1,6 @@
<svg width="800" height="600" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="#f9f9f9"/>
<text x="50%" y="50%" font-family="Arial" font-size="30" fill="#bbb" text-anchor="middle" dominant-baseline="middle">部件A图 (Bcde)</text>
<rect x="250" y="200" width="300" height="200" fill="none" stroke="#aaa" stroke-width="2" stroke-dasharray="5,5"/>
<text x="400" y="300" font-family="Arial" font-size="16" fill="#aaa" text-anchor="middle">零件C位置</text>
</svg>

After

Width:  |  Height:  |  Size: 507 B

75
components/DataEditor.js Normal file
View File

@ -0,0 +1,75 @@
function DataEditor({ item, items, setItems, setRootNodeName }) {
if (!item) {
return React.createElement('div', { className: 'workspace-panel flex items-center justify-center h-full' },
React.createElement('p', { className: 'text-gray-500' }, '未选择任何件号')
);
}
const isRoot = item.isRoot;
if (isRoot) {
return React.createElement('div', { className: 'workspace-panel p-4 h-full' },
React.createElement('h2', { className: 'text-lg font-semibold mb-4' }, `编辑总装图`),
React.createElement('div', { className: 'space-y-4' },
React.createElement('div', {},
React.createElement('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, '总装图名称'),
React.createElement('input', {
type: 'text',
name: 'name',
value: item.name,
onChange: (e) => setRootNodeName(e.target.value),
className: 'form-input'
})
),
React.createElement('p', {className: 'text-xs text-gray-500 mt-1'}, '这是项目的根节点,只可修改名称。')
)
);
}
const handleInputChange = (e) => {
const { name, value } = e.target;
const isNumber = ['quantity', 'weight'].includes(name);
setItems(prevItems => ({
...prevItems,
[item.id]: {
...prevItems[item.id],
[name]: isNumber ? Number(value) : value
}
}));
};
const isParent = item.children && item.children.length > 0;
return React.createElement('div', { className: 'workspace-panel p-4 h-full' },
React.createElement('h2', { className: 'text-lg font-semibold mb-4' }, `编辑: ${item.id}`),
React.createElement('div', { className: 'space-y-4' },
React.createElement('div', {},
React.createElement('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, '件号 (不可修改)'),
React.createElement('input', { type: 'text', readOnly: true, value: item.id, className: 'form-input bg-gray-100' })
),
React.createElement('div', {},
React.createElement('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, '名称'),
React.createElement('input', { type: 'text', name: 'name', value: item.name, onChange: handleInputChange, className: 'form-input' })
),
React.createElement('div', {},
React.createElement('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, '数量'),
React.createElement('input', { type: 'number', name: 'quantity', value: item.quantity, onChange: handleInputChange, className: 'form-input' })
),
React.createElement('div', {},
React.createElement('label', { className: 'block text-sm font-medium text-gray-700 mb-1' }, '重量 (kg)'),
React.createElement('input', {
type: 'number',
name: 'weight',
value: item.weight,
onChange: handleInputChange,
className: 'form-input',
readOnly: isParent, // 如果是父项,重量由子项计算,不可直接编辑
title: isParent ? '父项重量由子项自动汇总' : ''
}),
isParent && React.createElement('p', {className: 'text-xs text-gray-500 mt-1'}, '父项重量由子项自动汇总,此处不可编辑。')
)
)
);
}

157
components/DrawingViewer.js Normal file
View File

@ -0,0 +1,157 @@
const { useState, useEffect, useRef } = React;
function DraggableTable({ drawingId, position, onPositionChange, children }) {
const tableRef = useRef(null);
const [isDragging, setIsDragging] = useState(false);
const [offset, setOffset] = useState({ x: 0, y: 0 });
const handleMouseDown = (e) => {
if (!tableRef.current) return;
setIsDragging(true);
const rect = tableRef.current.getBoundingClientRect();
setOffset({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
e.preventDefault();
};
const handleMouseMove = (e) => {
if (!isDragging || !tableRef.current) return;
const parentRect = tableRef.current.parentElement.getBoundingClientRect();
// 计算新的 left 和 bottom
let newX = e.clientX - parentRect.left - offset.x;
let newY = parentRect.height - (e.clientY - parentRect.top) - tableRef.current.offsetHeight + offset.y;
// 边界检测
newX = Math.max(0, Math.min(newX, parentRect.width - tableRef.current.offsetWidth));
newY = Math.max(0, Math.min(newY, parentRect.height - tableRef.current.offsetHeight));
onPositionChange(drawingId, { x: newX, y: newY });
e.preventDefault();
};
const handleMouseUp = () => {
setIsDragging(false);
};
useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
} else {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
}
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isDragging, handleMouseMove, handleMouseUp]);
return React.createElement('div', {
ref: tableRef,
className: `absolute bg-white border border-gray-300 shadow-lg p-2 rounded-lg ${isDragging ? 'cursor-grabbing' : 'cursor-grab'}`,
style: { left: `${position.x}px`, bottom: `${position.y}px` },
onMouseDown: handleMouseDown
}, children);
}
function DrawingViewer({ drawing, item, items, tablePositions, setTablePositions, drawings, onItemUpdate, onBindDrawing, onItemRename, onCycleDrawing }) {
const [editedItemId, setEditedItemId] = React.useState('');
React.useEffect(() => {
if (item) {
setEditedItemId(item.id);
}
}, [item]);
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}" 尚未绑定图纸`),
// 假设我们总是可以绑定到一张默认图纸上,或者需要一个图纸选择器
// 这里为了简化,我们假设绑定到 'drawing-1'
React.createElement('button', {
className: 'btn-primary',
onClick: () => onBindDrawing(item.id, 'drawing-1')
}, '为此件号绑定图纸')
);
}
const tablePos = tablePositions[drawing.id] || { x: 50, y: 50 };
// 获取子项
const childItems = item.children.map(childId => items[childId]);
const handleRenameItem = () => {
if (!editedItemId.trim() || editedItemId.trim() === item.id) return;
onItemRename(item.id, editedItemId.trim());
};
const isRoot = item && item.isRoot;
return React.createElement('div', { className: 'workspace-panel h-full p-4 relative bg-gray-50 overflow-hidden' },
!isRoot && React.createElement('div', { className: 'absolute top-4 left-4 z-10 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-48',
value: editedItemId,
onChange: (e) => setEditedItemId(e.target.value),
onBlur: handleRenameItem, // 失去焦点时也触发
onKeyPress: (e) => { if (e.key === 'Enter') handleRenameItem() } // 回车触发
}),
React.createElement('button', { className: 'btn-primary', onClick: handleRenameItem }, '确认')
),
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)
}, React.createElement('i', { className: 'lucide lucide-chevron-left w-8 h-8' })),
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)
}, React.createElement('i', { className: 'lucide lucide-chevron-right w-8 h-8' })),
drawing.url ?
React.createElement('img', { src: drawing.url, className: 'w-full h-full object-contain' }) :
React.createElement('div', { className: 'absolute inset-0 flex items-center justify-center' },
React.createElement('h2', {className: 'text-9xl font-bold text-gray-200 select-none'}, drawing.drawingNumber)
),
React.createElement(DraggableTable, {
drawingId: drawing.id,
position: tablePos,
onPositionChange: (drawingId, newPos) => {
setTablePositions(prev => ({...prev, [drawingId]: newPos}))
}
},
React.createElement('table', { className: 'w-full text-sm text-left' },
React.createElement('thead', {},
React.createElement('tr', { className: 'bg-gray-100' },
React.createElement('th', {className: 'p-2'}, '件号'),
React.createElement('th', {className: 'p-2'}, '名称'),
React.createElement('th', {className: 'p-2'}, '数量'),
React.createElement('th', {className: 'p-2'}, '重量'),
React.createElement('th', {className: 'p-2'}, '图号')
)
),
React.createElement('tbody', {},
childItems.map(child => React.createElement('tr', { key: child.id, className: 'border-b' },
React.createElement('td', {className: 'p-2'}, child.id),
React.createElement('td', {className: 'p-2'}, child.name),
React.createElement('td', {className: 'p-2'}, child.quantity),
React.createElement('td', {className: 'p-2'}, child.weight.toFixed(2)),
React.createElement('td', {className: 'p-2'}, drawings[child.drawingId]?.drawingNumber || 'N/A')
))
)
)
)
);
}

15
components/Header.js Normal file
View File

@ -0,0 +1,15 @@
function Header({ drawings, items, onSave, onExport }) {
const itemCount = Object.keys(items).length;
const drawingCount = Object.keys(drawings).length;
return React.createElement('header', { className: 'h-[var(--header-height)] bg-white border-b border-gray-200 flex items-center px-6 justify-between' },
React.createElement('div', {},
React.createElement('h1', { className: 'text-xl font-semibold text-gray-800' }, '图纸工作台'),
React.createElement('p', { className: 'text-sm text-gray-500' }, `共管理 ${itemCount} 个件号和 ${drawingCount} 张图纸`)
),
React.createElement('div', { className: 'flex items-center gap-2' },
React.createElement('button', { className: 'btn-secondary', onClick: onSave }, '保存'),
React.createElement('button', { className: 'btn-primary', onClick: onExport }, '导出图纸')
)
);
}

View File

@ -0,0 +1,43 @@
function StructureTree({ items, selectedItemId, setSelectedItemId }) {
const buildTree = (parentId) => {
const children = Object.values(items)
.filter(item => item.parentId === parentId)
.sort((a,b) => a.id.localeCompare(b.id, undefined, { numeric: true }));
return children.map(item => ({
...item,
children: buildTree(item.id)
}));
};
const treeData = buildTree(null);
const renderTree = (nodes) => {
return React.createElement('ul', { className: 'space-y-1' },
nodes.map(node => {
const isRoot = node.id === '__ROOT__';
const isSelected = selectedItemId === node.id;
const nodeClasses = `tree-item ${isSelected ? 'selected' : ''} ${isRoot ? 'font-semibold' : ''}`;
const iconClasses = `lucide mr-2 w-4 h-4 ${isSelected ? 'text-blue-600' : 'text-gray-500'} ${isRoot ? 'lucide-folder-root' : 'lucide-file-text'}`;
return React.createElement('li', { key: node.id },
React.createElement('div', {
className: nodeClasses,
onClick: () => setSelectedItemId(node.id)
},
React.createElement('i', { className: iconClasses }),
React.createElement('span', {}, isRoot ? node.name : `${node.id} ${node.name}`)
),
node.children.length > 0 && React.createElement('div', { className: 'pl-6' }, renderTree(node.children))
)
})
);
};
return React.createElement('div', { className: 'workspace-panel p-4 h-full overflow-y-auto' },
React.createElement('h2', { className: 'text-lg font-semibold mb-3' }, '层级结构'),
renderTree(treeData)
);
}

Binary file not shown.

95
index.html Normal file
View File

@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图纸工作台 - 切分识别与编辑系统</title>
<meta name="description" content="专业的图纸切分识别与编辑工作台,支持层级结构管理和数据编辑">
<meta name="keywords" content="图纸工作台,图纸切分,零件识别,数据编辑,料号管理">
<meta property="og:title" content="图纸工作台 - 切分识别与编辑系统">
<meta property="og:description" content="专业的图纸切分识别与编辑工作台,支持层级结构管理和数据编辑">
<meta name="twitter:title" content="图纸工作台 - 切分识别与编辑系统">
<meta name="twitter:description" content="专业的图纸切分识别与编辑工作台,支持层级结构管理和数据编辑">
<script src="https://resource.trickle.so/vendor_lib/unpkg/react@18/umd/react.production.min.js"></script>
<script src="https://resource.trickle.so/vendor_lib/unpkg/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://resource.trickle.so/vendor_lib/unpkg/@babel/standalone/babel.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://resource.trickle.so/vendor_lib/unpkg/lucide-static@0.516.0/font/lucide.css" rel="stylesheet">
<style type="text/tailwindcss">
@layer theme {
:root {
--primary-color: #2563eb;
--secondary-color: #f1f5f9;
--accent-color: #10b981;
--text-primary: #1e293b;
--text-secondary: #64748b;
--border-color: #e2e8f0;
--sidebar-width: 280px;
--header-height: 60px;
}
}
@layer base {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background-color: #f8fafc;
color: var(--text-primary);
}
}
@layer components {
.workspace-panel {
@apply bg-white border border-[var(--border-color)] rounded-lg shadow-sm;
}
.tree-item {
@apply flex items-center px-3 py-2 hover:bg-gray-50 cursor-pointer rounded-md transition-colors;
}
.tree-item.selected {
@apply bg-blue-50 border-l-4 border-l-[var(--primary-color)];
}
.btn-primary {
@apply bg-[var(--primary-color)] text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors;
}
.btn-secondary {
@apply bg-gray-100 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-200 transition-colors;
}
.form-input {
@apply w-full px-3 py-2 border border-[var(--border-color)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--primary-color)] focus:border-transparent;
}
}
@layer utilities {
.drag-handle {
cursor: grab;
}
.drag-handle:active {
cursor: grabbing;
}
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" src="components/Header.js"></script>
<script type="text/babel" src="components/StructureTree.js"></script>
<script type="text/babel" src="components/DrawingViewer.js"></script>
<script type="text/babel" src="components/DataEditor.js"></script>
<script type="text/babel" src="utils/dragUtils.js"></script>
<script type="text/babel" src="app.js"></script>
</body>
</html>

1
utils/dragUtils.js Normal file
View File

@ -0,0 +1 @@
// Drag and drop utility functions will go here.