Commit 10cbf803 authored by fenghen777's avatar fenghen777

feat: 新增设计文档和7个AI开发skill模块

- docs/design.md: 项目设计文档(架构/已实现功能/待开发规划)
- .agent/skills/eplan-visualizer: 项目总览与通用规范
- .agent/skills/symbol-system: 符号定义与端口体系
- .agent/skills/component-editor: 符号编辑器约束
- .agent/skills/canvas-interaction: 画布交互与状态管理
- .agent/skills/modelica-export: Modelica导出与非因果建模
- .agent/skills/data-import: 数据导入导出与布局
- .agent/skills/ui-styling: 暗色主题与UI规范
parent e3663aab
---
name: canvas-interaction
description: React Flow 画布交互规则、useFlowStore 状态管理、节点旋转/连接/拖拽约束
---
# 画布交互与状态管理
## 涉及文件
| 文件 | 职责 |
|------|------|
| `src/components/Canvas/FlowCanvas.jsx` | React Flow 容器 + 事件处理 |
| `src/hooks/useFlowStore.js` | Zustand Store:nodes/edges/选中/增删改 |
| `src/components/Edges/GradientBezierEdge.jsx` | 渐变贝塞尔连线渲染 |
| `src/components/Edges/CustomConnectionLine.jsx` | 拖拽连线时的预览线 |
## useFlowStore 状态结构
```javascript
{
nodes: [], // React Flow 节点数组
edges: [], // React Flow 边数组
selectedNode: null, // 当前选中节点(单选)
selectedEdge: null, // 当前选中边(单选)
layoutDirection: 'TB', // 自动布局方向
connectionError: null, // 连接错误提示文本
}
```
### 核心方法
| 方法 | 说明 |
|------|------|
| `addNode(position, functionCode)` | 通过功能码从 DEVICE_CATEGORIES 查找定义,创建节点 |
| `addCustomNode(position, template)` | 通过自定义模板创建节点 |
| `onConnect(connection)` | 处理连线,自动设置 sourceColor/targetColor |
| `autoLayout(direction)` | 调用 Dagre 布局引擎重排节点 |
| `exportToJSON() / importFromJSON(json)` | 序列化/反序列化画布 |
| `removeSelectedNode() / removeSelectedEdge()` | 删除选中项 |
| `clearAll()` | 清空画布 |
## addNode 关键逻辑
```
addNode(position, functionCode):
1. 通过 functionCode 在 DEVICE_CATEGORIES 中查找 deviceDef
2. 递增全局 counter
3. 构建 ports 数组:
- 复制 deviceDef.ports
- id 拼接 counter 保证全局唯一:`${p.id}-${counter}`
- 保留 portId = 原始 p.id
- 传递 connector = p.connector || p.name
4. 构建 templateData(包含完整端口/形状/参数定义)
5. 使用 customDeviceNode 作为节点 type
```
### 节点类型
- **所有节点**统一使用 `type: 'customDeviceNode'` 注册
- `nodeTypes = { customDeviceNode: CustomDeviceNode }` 在 FlowCanvas 中注册
- DeviceNode 仅保留文件做旧数据兼容,不在 nodeTypes 中注册
## 事件处理规则
### 拖拽放置
```
onDrop:
1. 检查 dataTransfer 是否包含 'application/custom-template-id'
→ 自定义模板拖入,调用 addCustomNode
2. 否则检查 'application/eplan-device-type'
→ 内置符号拖入,调用 addNode
```
### 节点旋转
```
空格键 → 旋转选中节点 90 度
- 更新 node.data.rotation = (current + 90) % 360
- requestAnimationFrame → updateNodeInternals(nodeId)
(强制 React Flow 重新计算 Handle 位置)
- 仅在非输入框焦点时触发
```
### 连线重连
```
onReconnect:
- 拖拽已连接的端点到其他节点 → 重新连接
- 拖拽到空白处 → 删除该边(断开连接)
- 通过 edgeReconnectSuccessful ref 追踪
```
### 删除
```
键盘 Backspace / Delete → React Flow 内置删除
工具栏删除按钮 → 调用 removeSelectedNode / removeSelectedEdge
```
## 边渲染约束
### GradientBezierEdge
- 连线两端颜色取自源/目标节点的 `data.color`
- 使用 SVG `<linearGradient>` + `<path>` 渲染
- 选中时增加发光效果
### 连线创建时
- `onConnect` 回调自动读取源/目标节点颜色注入 `edge.data.sourceColor / targetColor`
## 画布配置
```javascript
snapToGrid: true
snapGrid: [16, 16]
deleteKeyCode: ['Backspace', 'Delete']
multiSelectionKeyCode: 'Shift'
proOptions: { hideAttribution: true }
fitView: true
```
## 禁止事项
1. 不要直接修改 `nodes` / `edges` 数组,必须通过 `useFlowStore.setState()` 或提供的方法
2. 不要在 `utils/` 中调用 `useFlowStore.getState()`(工具函数应保持纯粹)
3. 不要新增 nodeType,所有新符号都通过模板机制创建
---
name: component-editor
description: 自定义符号编辑器的交互约束、模板数据结构与 useComponentLibrary Store 规则
---
# 自定义符号编辑器
## 涉及文件
| 文件 | 职责 |
|------|------|
| `src/components/ComponentEditor/ComponentEditor.jsx` | 编辑器主组件:工具栏+画布+属性面板 |
| `src/components/ComponentEditor/ShapeCanvas.jsx` | SVG 形状绘制画布(矩形/圆形/椭圆) |
| `src/components/ComponentEditor/PortEditor.jsx` | 端口列表编辑面板 |
| `src/components/ComponentEditor/NodePreview.jsx` | 实时节点预览 |
| `src/components/ComponentEditor/ComponentEditor.module.css` | 编辑器样式 |
| `src/hooks/useComponentLibrary.js` | Zustand Store:模板 CRUD + 持久化 |
## 视图切换机制
```
App.jsx 根据 useComponentLibrary.currentView 切换:
'flow' → 显示 Sidebar + FlowCanvas + PropertiesPanel
'editor' → 显示 ComponentEditor(全屏独占)
```
通过 Toolbar 的"符号编辑器"按钮切换,调用 `switchView()`
## 模板数据结构 (ComponentTemplate)
```javascript
{
id: 'cpt-xxx-yyy', // 唯一 ID(自动生成)
name: '新符号', // 显示名称
category: '未分类', // 所属分类
color: '#6366f1', // 主题色
icon: '■', // 图标字符
width: 180, // 节点宽度 px
height: 100, // 节点 body 高度 px
shapes: [ // SVG 形状数组
{ type: 'rect', props: { x, y, width, height }, style: { fill, stroke, strokeWidth } },
],
params: [ // 模型参数定义
{ key: 'R', label: '阻值', unit: 'Ω', defaultValue: '10k' },
],
ports: [ // 端口数组
{ id: 'in1', portId: 1, name: '1', description: '', type: 'generic',
side: 'left', position: 0.5, connector: 'in1' },
],
}
```
## useComponentLibrary Store 规则
### 持久化
- 模板库 → localStorage key: `eplan-component-library`
- 自定义分类 → localStorage key: `eplan-custom-categories`
- 默认分类 `'未分类'` 不可删除
### 编辑生命周期
```
startNew() / startEditing(id)
→ editingTemplate 被填充
→ 用户操作调用 updateEditing / addPort / removePort / addShape 等
→ 所有操作自动 pushHistory()(最多 50 步)
→ undo() 回退
→ saveEditing() 保存到库
→ cancelEditing() 放弃
```
### addPort 默认值生成规则
```
新增端口 →
name: portN (N 从已有 connector 推导,如 port3)
connector: pN (N 与 name 同步,如 p3)
side: 默认 'left'
type: 默认 'generic'
position: 默认 0.5
```
- `generateDefaultPortInfo(existingPorts)` 函数负责生成不冲突的编号
- connector 编号根据已有 connector 列表去重递增
### 空白模板初始端口
```javascript
ports: [
{ id: 'in1', name: '1', side: 'left', connector: 'in1' },
{ id: 'out1', name: '2', side: 'right', connector: 'out1' },
]
```
## PortEditor 交互约束
### 每行端口显示
```
[色点] [portId输入] [名称输入] [描述输入] [connector输入] [类型下拉] [删除按钮]
```
- **无方向选择框**(side 由编辑器画布上的端口拖拽位置决定)
- connector 输入框 placeholder 为 "connector"
- 端口颜色取自 PORT_TYPES 定义
### 约束
1. connector 字段**必须为英文**,用于 Modelica 标识符
2. 同一模板内 connector 不应重复(代码不强制校验,导出时可能冲突)
3. description 字段仅用于编辑器提示,不影响导出
4. 端口的 side 属性**在 ShapeCanvas 中通过拖拽端口到对应边来设置**
## ShapeCanvas 约束
### 坐标系
- SVG viewBox 以 (0, 0) 为左上角
- 宽高对应模板的 width × height
- 网格 snap 到 10px
### 支持图形
| 类型 | SVG 元素 | 属性 |
|------|---------|------|
| rect | `<rect>` | x, y, width, height |
| circle | `<circle>` | cx, cy, r |
| ellipse | `<ellipse>` | cx, cy, rx, ry |
### 端口在画布上的表现
- 端口渲染为彩色圆点(半径 7px)
- 可拖拽到节点边缘任意位置
- 拖拽时自动吸附到最近的 side(left/right/top/bottom)
- hover 时显示发光效果
---
name: data-import
description: EPLAN xlsx 数据导入解析、JSON 导入导出格式与 Dagre 自动布局约束
---
# 数据导入导出与自动布局
## 涉及文件
| 文件 | 职责 |
|------|------|
| `src/utils/xlsxParser.js` | EPLAN xlsx 连接表解析 |
| `src/utils/layoutEngine.js` | Dagre 自动布局算法 |
| `src/hooks/useFlowStore.js` | importFromJSON / exportToJSON 方法 |
| `src/components/Sidebar/Sidebar.jsx` | 导入 UI(xlsx 文件选择) |
| `src/components/Toolbar/Toolbar.jsx` | JSON 导入导出 + .mo 导出按钮 |
## xlsx 导入流程
```
用户在 Sidebar 选择 xlsx 文件
→ FileReader 读取 ArrayBuffer
→ xlsxParser.parseEplanXlsx(data) 解析
→ 返回 { devices: [], connections: [] }
→ useFlowStore.importNodes(devices, connections) 构建节点和边
→ autoLayout() 自动排列
```
### 解析约束
- 支持的信号名称自动映射为端口类型:
- 电力:L1, L2, L3, N, PE → power
- 控制电源:24V, 0V → power
- 信号:DI, DO, AI, AO → digital/analog
- 设备功能码(functionCode)对应 `constants.js` 中的 code 值
- 未识别的功能码视为 `terminal`(通用端子)
## JSON 导入导出
### 导出格式
```javascript
{
nodes: [
{
id: 'node-xxx',
type: 'customDeviceNode',
position: { x: 100, y: 200 },
data: {
label: '电阻',
color: '#FF9800',
icon: 'R',
functionCode: 1001,
rotation: 0,
ports: [...], // 含 connector
params: [...],
paramValues: { R: '10k' },
templateData: {...}, // 完整模板快照
},
},
],
edges: [
{
id: 'edge-xxx',
source: 'node-1',
target: 'node-2',
sourceHandle: 'p1-1', // 端口 Handle ID
targetHandle: 'p-pos-2',
type: 'gradientBezier',
data: { sourceColor: '#FF9800', targetColor: '#2196F3' },
},
],
}
```
### 导入约束
- `importFromJSON(jsonString)` 解析 JSON 字符串
- 直接替换 nodes 和 edges(非合并)
- 导入后不会自动布局
- 文件扩展名过滤为 `.json`
### 导出约束
- `exportToJSON()` 返回 JSON 字符串
- 包含所有节点位置、参数值、模板快照
- 文件名格式:`eplan-flow-YYYY-MM-DD.json`
- 通过 Blob + createObjectURL 下载
## Dagre 自动布局
### 布局方向
```javascript
// constants.js
export const LAYOUT_DIRECTION = {
TB: 'TB', // 纵向(Top → Bottom)
LR: 'LR', // 横向(Left → Right)
};
```
### 布局参数
```javascript
ranksep: 80 // 层级间距
nodesep: 50 // 同层节点间距
edgesep: 30 // 边间距
```
### 调用方式
```javascript
useFlowStore.autoLayout(direction)
layoutEngine.getLayoutedElements(nodes, edges, direction)
返回重新定位的 { nodes, edges }
更新 Store + fitView
```
### 约束
- 节点尺寸从 `node.data.templateData.width / height` + HEADER_H(28) 读取
- 布局后保留所有节点数据不变,只修改 `position`
- 不影响边的连接关系
---
name: eplan-visualizer
description: EPLAN Visualizer 项目总览 - 技术栈、目录结构、编码通用规范
---
# EPLAN Visualizer 项目总览
> 面向工业仿真的可视化原理图编辑与 Modelica 模型导出前端系统
> 设计文档:`docs/design.md`
## 技术栈(勿擅自升级)
| 层级 | 技术 | 版本 |
|------|------|------|
| 框架 | React + Vite | React 19 / Vite 7 |
| 图引擎 | @xyflow/react | React Flow v12 |
| 状态管理 | Zustand | 最新 |
| 布局 | Dagre | - |
| 数据导入 | xlsx (SheetJS) | - |
| 样式 | CSS Modules(暗色主题)| - |
## 目录结构
```
src/
App.jsx # 主入口:视图切换 flow / editor
main.jsx # React 挂载点
components/ # UI 组件,按功能域分子目录
Canvas/FlowCanvas.jsx → 参考 skill: canvas-interaction
Nodes/ → 参考 skill: symbol-system
Edges/ → 参考 skill: canvas-interaction
Sidebar/Sidebar.jsx → 参考 skill: ui-styling
Toolbar/Toolbar.jsx → 参考 skill: ui-styling
PropertiesPanel/ → 参考 skill: ui-styling
ComponentEditor/ → 参考 skill: component-editor
hooks/ # Zustand Store
useFlowStore.js → 参考 skill: canvas-interaction
useComponentLibrary.js → 参考 skill: component-editor
utils/ # 纯工具函数,无 React 依赖
constants.js → 参考 skill: symbol-system
modelicaExporter.js → 参考 skill: modelica-export
modelMapping.js → 参考 skill: modelica-export
xlsxParser.js → 参考 skill: data-import
layoutEngine.js → 参考 skill: data-import
```
## 通用编码规范
1. 组件文件使用 `.jsx` 扩展名
2. 工具函数放 `utils/`,Hook 放 `hooks/`,组件放 `components/`
3. **禁止**`utils/` 中引入 React 依赖
4. 新增功能**优先创建新文件**,避免修改已稳定的核心文件
5. 大改动优先新增类/文件,而非修改现有文件
6. 禁止自动提交 Git
## 编译验证
```bash
npm run build
```
- 禁止使用 `-q` 静默模式
- 必须确认输出包含实际编译行数且无错误
## 关联 Skill 索引
| Skill | 覆盖模块 |
|-------|---------|
| `symbol-system` | 内置符号定义、节点渲染、端口类型 |
| `component-editor` | 自定义符号编辑器、useComponentLibrary |
| `canvas-interaction` | React Flow 画布、useFlowStore |
| `modelica-export` | .mo 导出、模型映射(非因果建模) |
| `data-import` | xlsx 导入、JSON 导入导出、自动布局 |
| `ui-styling` | 暗色主题、CSS Modules、工具栏/侧栏/属性面板 |
## 建模理念
本系统采用**非因果 (acausal) 建模**,所有连接均通过 Modelica `connect()` 描述,不区分信号流方向,由求解器自动推导因果关系。
---
name: modelica-export
description: Modelica .mo 文件导出逻辑、模型映射、connect() 生成规则与端口解析优先级
---
# Modelica 模型导出
> 本系统采用**非因果 (acausal) 建模**,所有连接均通过 `connect()` 描述,不区分信号流方向,由 Modelica 求解器自动推导因果关系。
## 涉及文件
| 文件 | 职责 |
|------|------|
| `src/utils/modelicaExporter.js` | 核心导出函数:节点→声明,边→connect() |
| `src/utils/modelMapping.js` | MODEL_MAP 映射表 + resolvePortName 端口解析 |
## 导出流程
```
exportToModelica({ nodes, edges }, modelName)
├── 1. model 声明
│ model Circuit
├── 2. 遍历 nodes → 组件声明
│ Resistor R_1(R=10000);
│ Capacitor C_2(C=0.0001);
├── 3. equation 段
│ 遍历 edges → connect() 语句
│ connect(R_1.n, C_2.p);
├── 4. annotation(坐标系)
└── 5. end Circuit;
```
## 组件声明生成规则
### 实例名生成
```javascript
toMoId(label) + '_' + (idx + 1)
```
- `toMoId()` 将非法字符替换为 `_`,数字开头加 `_` 前缀
- 索引从 1 开始
### 模型名解析优先级
```
1. mappingOverrides[type]?.modelName (用户自定义覆盖,localStorage)
2. MODEL_MAP[type]?.modelName (代码内置映射)
3. 自定义符号:mappingOverrides[`custom_${templateId}`]?.modelName
4. 无映射 → 报错,阻止导出
```
### 参数序列化
```
1. 从 MODEL_MAP[type].paramMap 获取 EPLAN key → Modelica name 映射
2. 从 node.data.paramValues 读取用户输入值
3. parseEngValue() 解析工程记号为数值
4. formatMoValue() 转 Modelica 数值格式
```
工程记号支持:
```
T=1e12 G=1e9 M=1e6 k/K=1e3 m=1e-3 u/μ=1e-6 n=1e-9 p=1e-12
```
## connect() 端口名解析(核心)
### resolvePortName 优先级
```
resolvePortName(handleId, type, ports):
1. 在 ports 中查找 port.id === handleId
→ 找到:优先返回 port.connector
→ connector 为空:查 MODEL_MAP[type].portMap[port.portId]
→ 仍无:返回 port.name
2. 未找到端口:从 handleId 中提取原始 portId(去掉 -counter 后缀)
→ 查 MODEL_MAP[type].portMap[rawId]
→ 仍无:返回 rawId
```
**关键约束**:connector 字段是最高优先级。只要端口有 connector,就直接使用,不再查 portMap。
### connect() 输出格式
```modelica
connect(srcInstance.srcConnector, tgtInstance.tgtConnector);
```
## MODEL_MAP 映射表结构
```javascript
export const MODEL_MAP = {
'resistor': {
modelName: 'Modelica.Electrical.Analog.Basic.Resistor',
paramMap: { R: 'R' }, // EPLAN key → Modelica param name
portMap: { 'p1': 'p', 'p2': 'n' }, // 回退用,connector 字段优先
},
// ...
};
```
### 新增映射必须遵守
1. key 与 `constants.js` 中符号的 `type` 值完全一致
2. `paramMap` 的 EPLAN key 与 `constants.js``params[].key` 一致
3. `portMap` 的 key 与 `constants.js``ports[].id` 一致
4. portMap 现在仅作为 connector 字段缺失时的回退
## 用户自定义映射覆盖
- localStorage key: `eplan_model_mapping_overrides`
- 格式: `{ [type_or_custom_id]: { modelName: 'xxx', portMap: {...} } }`
- 在 Sidebar "模型映射" Tab 中管理
- 覆盖优先级高于代码内 MODEL_MAP
## 导出下载机制
```javascript
downloadModelicaFile(data, modelName):
1. 调用 exportToModelica 生成代码
2. errors 返回错误,不下载
3. 创建 Blob URL.createObjectURL <a>.click() 下载
4. 回收 URL
```
## 禁止事项
1. 不要在 modelicaExporter.js 中引入 React 依赖
2. 不要硬编码端口名在导出逻辑中 - 必须通过 resolvePortName 解析
3. 不要跳过 toMoId() 生成标识符 - Modelica 对标识符有严格要求
---
name: symbol-system
description: 内置符号定义、节点渲染、端口类型体系与 connector 命名规范
---
# 符号与端口系统
## 涉及文件
| 文件 | 职责 |
|------|------|
| `src/utils/constants.js` | 分类符号定义(DEVICE_CATEGORIES)+ PORT_TYPES |
| `src/components/Nodes/CustomDeviceNode.jsx` | 统一节点渲染(内置+自定义共用) |
| `src/components/Nodes/DeviceNode.jsx` | 旧版内置节点渲染(仅兼容,新功能不应依赖) |
## 内置符号定义规范 (constants.js)
### DEVICE_CATEGORIES 结构
```javascript
export const DEVICE_CATEGORIES = [
{
category: '分类名称', // 如 '基本电子元件'
items: [
{
code: 1001, // 唯一功能码
type: 'resistor', // 英文标识(对应 modelMapping 的 key)
label: '电阻', // 中文显示名
color: '#FF9800', // 主题色
icon: 'R', // Header 图标字符
width: 120, // 节点宽度 px
height: 60, // 节点 body 高度 px(不含 Header 28px)
ports: [...], // 端口数组
params: [...], // 可选:模型参数
},
],
},
];
```
### 新增内置符号必须遵守
1. **code 必须全局唯一**(按分类区间分配:1000+ 基本电子, 100~999 电气控制, 2000+ PLC, 3000+ 水利, 4000+ 气动)
2. **type 必须为英文小写下划线**,与 `modelMapping.js` 的 MODEL_MAP key 一致
3. **每个端口必须有 connector 字段**
### 端口定义规范
```javascript
{
id: 'p1', // 节点内唯一标识(创建节点时会拼接 counter 生成全局唯一 Handle ID)
name: '1', // 显示在节点上的标签文本
side: 'left', // left | right | top | bottom
position: 0.5, // 0~1,沿边缘的比例位置
type: 'power', // 端口物理类型
connector: 'p', // Modelica connect() 中的端口标识符(英文小驼峰)
}
```
### connector 命名规则
| 场景 | 命名规则 | 举例 |
|------|---------|------|
| 二端元件正负极 | p, n | 电阻、电容、电感 |
| 输入输出 | pIn, pOut | 端子排、断路器、电缆 |
| 多引脚元件 | 对应端子名小驼峰 | l1, t1, a1, a2 |
| PLC 通道 | di1, do1, ai1, ao1 | PLC 各模块 |
| 流体端口 | inlet, outlet, waterIn | 阀门、水泵 |
| 电源引脚 | vcc, gnd, pe | 传感器、电机 |
| 自定义符号默认 | p1, p2, p3... | addPort 自动生成 |
**禁止使用中文作为 connector 值。**
## PORT_TYPES 端口类型(constants.js 导出)
```javascript
export const PORT_TYPES = {
power: { label: '电力', color: '#f44336' },
digital: { label: '数字量', color: '#2196F3' },
analog: { label: '模拟量', color: '#4CAF50' },
generic: { label: '通用', color: '#9E9E9E' },
water: { label: '水流', color: '#00BCD4' },
air: { label: '气流', color: '#8BC34A' },
};
```
- 新增端口类型需同时更新 `PORT_TYPES``PortEditor.jsx` 中的下拉选项
- 类型影响 Handle 渲染颜色和未来的连接兼容性校验
## 节点渲染 (CustomDeviceNode.jsx)
### 结构
```
总高度 = HEADER_H(28px) + tH(模板 height)
+---------------------------+
| [icon] [name] ← Header | ← 28px 高,背景=节点 color
+---------------------------+
| |
| [shape SVG] | ← tH px 高
| [参数文本] |
| |
+---------------------------+
← Handle 端口分布于四条边上
```
### Handle 定位核心规则
- **React Flow CSS 管贴边轴**(left:0 / right:0 / top:0 / bottom:0)
- **我们只设"沿边缘滑动"的轴**
- left/right → 只设 `top`(百分比)
- top/bottom → 只设 `left`(百分比)
- 百分比计算:`top = ((HEADER_H + tH * port.position) / totalH) * 100%`
### Handle 双层策略
每个端口同时渲染两个 Handle(同 id):
1. **target Handle** - 透明、无边框,接收连接
2. **source Handle** - 可见彩色圆点,发起连接
这保证同一端口既可以作为连接的源也可以作为目标。
### 旋转支持
- 通过 `data.rotation`(0/90/180/270)控制
- 使用 CSS `transform: rotate(Ndeg)` 实现
- 旋转后需调用 `updateNodeInternals()` 让 React Flow 重新计算 Handle 位置
## 当前分类统计
| 分类 | 数量 | 符号 |
|------|------|------|
| 基本电子元件 | 6 | resistor, capacitor, inductor, diode, voltage_source, ground |
| 电气控制 | 9 | terminal, contactor, relay, breaker, switch, motor, transformer, sensor, cable |
| PLC | 5 | plc_cpu, plc_di, plc_do, plc_ai, plc_ao |
| 水利/液压 | 4 | pump, valve, flow_meter, tank |
| 机械/气动 | 4 | cylinder, solenoid_valve, servo_motor, vfd |
---
name: ui-styling
description: 暗色主题体系、CSS Modules 规范、Toolbar/Sidebar/PropertiesPanel 布局与交互约束
---
# UI 与样式体系
## 涉及文件
| 文件 | 职责 |
|------|------|
| `src/index.css` | 全局基础样式(body/font/scrollbar) |
| `src/App.css` | 应用布局(app-container/app-main) |
| `src/components/Toolbar/Toolbar.module.css` | 顶部工具栏样式 |
| `src/components/Sidebar/Sidebar.module.css` | 侧边栏样式 |
| `src/components/PropertiesPanel/PropertiesPanel.module.css` | 右侧属性面板样式 |
| `src/components/Canvas/FlowCanvas.module.css` | 画布区域样式 |
| `src/components/ComponentEditor/ComponentEditor.module.css` | 符号编辑器样式 |
## 暗色主题色板
### 背景层级
```
Layer 0 (最深): #0d0d14 → 画布背景、编辑器画布
Layer 1: #14141e → 工具栏、侧边栏、属性面板背景
Layer 2: #1a1a28 → 输入框背景、卡片内部
Layer 3: #1e1e2e → 节点背景、hover 项
```
### 边框与分隔线
```
主分隔线: #2a2a3a → 面板边框、section 分割
次分隔线: #1e1e2e → section 内部分割
```
### 文字色阶
```
高亮: #fff → 选中状态、重要文本
主文本: #ccc → 输入框文本
副文本: #bbb → 标签文本
辅助: #888 → 描述、提示
弱化: #666 → section 标题
极弱: #555 → 画布提示、placeholder
禁用: #444 → 空状态文本
```
### 主题色
```
品牌色: #6366f1 → 选中高亮、按钮、焦点边框
品牌色暗: #5558e6 → 按钮 hover
危险色: #dc2626 → 删除按钮 hover
```
### 符号颜色
- 各符号分类使用各自 `color` 字段(如 #FF9800 电阻、#2196F3 电容)
- 节点 Header 背景色 = 符号 color
- 选中节点描边 = 白色
## CSS Modules 规范
1. 每个组件目录下配套 `组件名.module.css`
2. **禁止行内 style**(动态计算值除外,如节点宽高、旋转角度、端口位置百分比)
3. 类名使用 camelCase(CSS Modules 自动 scope)
4. 组件内通过 `styles.xxx` 引用
### 允许行内 style 的场景
- 节点渲染组件(CustomDeviceNode)中的动态尺寸和位置
- 端口 Handle 的百分比定位
- 编辑器画布中的拖拽位置
- 颜色动态值(取自 data.color)
## 全局布局结构
```
App (100vh × 100vw)
├── Toolbar → 顶部固定条,高度约 44px
│ ├── Brand(左)
│ ├── 布局按钮组
│ ├── 符号编辑器切换
│ └── 导入导出按钮组(右)
└── Main Area (flex: 1)
├── Sidebar → 左侧面板,固定宽度 220px
│ ├── Tab 切换(创建原理图 / 组件库 / 模型映射)
│ ├── 内置符号库(按分类折叠)
│ ├── 自定义符号库
│ └── xlsx 导入
├── FlowCanvas → 中间画布区(flex: 1)
│ ├── React Flow 容器
│ ├── Controls(右下角)
│ ├── MiniMap(左下角)
│ └── Background Dots
└── PropertiesPanel → 右侧属性面板,可拖拽调整宽度
├── 节点信息
├── 端口列表(显示 connector)
├── 模型参数编辑
└── 连接信息
```
## Toolbar 约束
- 按钮样式:`styles.btn`,激活态 `styles.active`,危险 `styles.danger`
- 分组用 `styles.group`,间隔用 `styles.divider`
- 右侧按钮组用 `styles.spacer` 推到右边
- 所有操作按钮必须有 `title` 提示
## Sidebar 约束
- 默认激活 Tab:"创建原理图"
- Tab 顺序:创建原理图 → 组件库 → 模型映射
- 内置符号拖拽:设置 `dataTransfer.setData('application/eplan-device-type', code)`
- 自定义符号拖拽:设置 `dataTransfer.setData('application/custom-template-id', id)`
- 分类折叠/展开使用 `details/summary` 原生元素
## PropertiesPanel 约束
- 无选中时显示空态提示
- 支持 Resizer 拖拽调整宽度(使用 mousedown/mousemove/mouseup)
- 参数编辑通过 `useFlowStore.updateNodeParam(nodeId, key, value)` 更新
- 端口列表显示 `port.connector || port.name`
## 字体
```css
font-family: 'Segoe UI', 'Inter', -apple-system, sans-serif;
```
- 代码/参数值使用 `'Consolas', monospace`
- 基础字号:12px(输入框)、11px(标签)、10px(辅助文本)、9px(极小信息)
# EPLAN Visualizer 设计文档
> 面向工业仿真的可视化原理图编辑与 Modelica 模型导出前端系统
## 1. 项目概述
EPLAN Visualizer 是一个基于 Web 的可视化原理图编辑器,面向工业仿真场景。用户可以通过拖拽内置/自定义电气符号、绘制连接关系,最终导出符合 Modelica 标准的 `.mo` 文件,用于数字孪生与控制系统仿真。
### 技术栈
| 层级 | 技术选型 |
|------|---------|
| 框架 | React 19 + Vite 7 |
| 图引擎 | @xyflow/react (React Flow v12) |
| 状态管理 | Zustand |
| 布局算法 | Dagre |
| 数据导入 | xlsx (SheetJS) |
| 模型导出 | 自研 Modelica 代码生成器 |
---
## 2. 系统架构
```
+-----------------------------------------------------------+
| App.jsx |
| +-------+ +------------------+ +-------------------+ |
| |Toolbar| | FlowCanvas | | PropertiesPanel | |
| +-------+ | (React Flow) | | (选中节点/边属性) | |
| | +------------+ | +-------------------+ |
| +-------+ | |DeviceNode | | |
| |Sidebar| | |CustomDevice| | +-------------------+ |
| | 符号库 | | |Node | | | ComponentEditor | |
| | 导入 | | +------------+ | | (符号编辑器) | |
| | 映射 | | |GradientEdge| | | ShapeCanvas | |
| +-------+ | +------------+ | | PortEditor | |
| +------------------+ | NodePreview | |
| +-------------------+ |
+-----------------------------------------------------------+
| 数据层 / 工具层 |
| useFlowStore useComponentLibrary |
| constants.js modelicaExporter.js modelMapping.js |
| xlsxParser.js layoutEngine.js |
+-----------------------------------------------------------+
```
---
## 3. 已实现功能模块
### 3.1 原理图画布 (FlowCanvas)
- 基于 React Flow 的节点-边图形编辑器
- 支持拖拽放置、框选、缩放、平移
- 节点旋转(空格键 90 度旋转,连线跟随)
- 边的重新连接(拖拽端点到其他节点)
- 拖拽到空白处自动断开连接
- MiniMap 小地图 + Controls 缩放控件
- 自动布局(Dagre 算法,支持纵向/横向)
### 3.2 内置符号库 (constants.js)
预定义 **28 种** 工业电气符号,按 5 大类组织:
| 分类 | 数量 | 包含符号 |
|------|------|---------|
| 基本电子元件 | 6 | 电阻、电容、电感、二极管、电压源、接地 |
| 电气控制 | 9 | 端子排、接触器、继电器、断路器、开关、电动机、变压器、传感器、电缆 |
| PLC | 5 | CPU、数字输入/输出、模拟输入/输出 |
| 水利/液压 | 4 | 水泵、阀门、流量计、水箱 |
| 机械/气动 | 4 | 气缸、电磁阀、伺服电机、变频器 |
每个符号定义包含:端口模板(位置/类型/connector 参数)、默认尺寸、模型参数。
### 3.3 自定义符号编辑器 (ComponentEditor)
- Blender 风格暗色 UI:左侧工具栏 + 中间 SVG 画布 + 右侧属性面板
- 支持绘制形状:矩形、圆形、椭圆
- 端口编辑:添加/删除端口,设置名称、类型、connector 参数
- 预设模板:双端口方块、圆形传感器、多端口模块
- 节点预览:实时预览最终渲染效果
- 自定义分类管理
- Ctrl+Z 撤销支持
- 持久化到 localStorage
### 3.4 连接与端口系统
- **端口类型**:电力(power)、数字量(digital)、模拟量(analog)、水流(water)、气流(air)、通用(generic)
- **connector 参数**:每个端口具有唯一的 connector 标识符(如 p、n、l1、vcc),用于 Modelica connect() 语句
- **类型着色**:不同端口类型使用不同颜色渲染
- **渐变连线**:连线两端颜色取自源/目标节点主色
### 3.5 Modelica 模型导出 (modelicaExporter.js)
- 将画布上的节点和连接关系转换为 `.mo` 模型代码
- 组件声明:基于模型映射表生成 `ModelName instanceName(params)` 声明
- 连接方程:基于端口 connector 参数生成 `connect(A.p, B.n)` 语句
- 工程记号解析:支持 10k、100u、4.7M 等工程记号转数值
- 用户可自定义模型映射覆盖(localStorage 持久化)
- 导出前校验,未映射符号给出明确错误提示
### 3.6 数据导入 (xlsxParser.js)
- 解析 EPLAN 导出的 xlsx 连接表
- 自动提取设备节点和连接关系
- 支持信号名称识别(L1/L2/L3/N/PE/24V/0V 等)
- 导入后自动布局
### 3.7 属性面板 (PropertiesPanel)
- 选中节点:显示设备标识、名称、颜色、端口列表(含 connector)、模型参数(可编辑)
- 选中连接:显示连接 ID、源/目标设备及端口
- 支持拖拽调整面板宽度
### 3.8 JSON 导入导出
- 导出当前原理图为 JSON 格式(含节点位置、参数、连接关系)
- 从 JSON 恢复原理图
---
## 4. 待开发功能规划
### 4.1 精细端口类型与物理量连接 [P1 - 核心]
**目标**:区分信号流连接与物理量连接,生成不同的 Modelica 建模语句。
**现状**
当前所有端口连接统一生成 `connect(A.port, B.port)`,未区分连接的物理语义。
**规划**
```
端口类型分类:
+-- 物理量端口 (physical)
| +-- 电气 (electrical) → Modelica.Electrical.Analog
| +-- 流体 (fluid) → Modelica.Fluid
| +-- 机械 (mechanical) → Modelica.Mechanics
| +-- 热 (thermal) → Modelica.Thermal
|
+-- 信号端口 (signal)
+-- RealInput / RealOutput
+-- BooleanInput / BooleanOutput
+-- IntegerInput / IntegerOutput
```
- 物理量端口之间使用 `connect()` 语句(双向能量流)
- 信号端口之间使用赋值语句或 `connect()` 取决于方向性
- 端口类型不兼容时在 UI 上给出可视化提示(颜色/禁止连接)
- 导出时根据端口物理类型自动引入对应的 Modelica 标准库 `import`
### 4.2 半实物仿真支持 [P1 - 核心]
**目标**:支持将部分节点标记为"实物设备",生成适配半实物仿真 (Hardware-in-the-Loop, HIL) 的配置。
**规划**
```
普通仿真模式:
[仿真模型A] --connect--> [仿真模型B] --connect--> [仿真模型C]
半实物仿真模式:
[仿真模型A] --connect--> [HIL接口] <==> [实物设备B] <==> [HIL接口] --connect--> [仿真模型C]
(数据采集/控制)
```
- 节点属性面板增加"标记为实物"开关
- 实物节点在画布上使用不同视觉样式(虚线边框 / 特殊底色)
- 导出时实物节点不生成 Modelica 模型声明
- 与实物节点相连的端口自动生成 HIL 通信接口代码(如 OPC-UA / Modbus 配置)
- 生成仿真配置文件,包含实物设备的通信参数(IP、端口、采样率等)
- 支持导出 FMU (Functional Mock-up Unit) 接口描述
### 4.3 前后端联合 [P2 - 基础设施]
**目标**:当前为纯前端应用,需要增加后端服务以支持多用户协作、仿真执行、数据持久化。
**规划**
```
前端 (React) 后端 (TBD) 仿真引擎
+-----------+ +------------------+ +----------------+
| 原理图编辑 | <-----> | REST API | --> | OpenModelica |
| 符号管理 | HTTP | 项目管理 | | FMU Runtime |
| 参数配置 | | 用户认证 | | HIL 通信网关 |
| 结果展示 | | 仿真任务调度 | +----------------+
+-----------+ | 文件存储(DB) |
+------------------+
```
- 项目持久化:从 localStorage 迁移到后端数据库存储
- 仿真执行:后端调用 OpenModelica 编译和运行模型,返回仿真结果
- 结果可视化:前端展示仿真曲线(时域波形、状态变量图表)
- 用户管理:登录/权限/项目归属
- HIL 网关:后端管理实物设备连接,转发实时数据
### 4.4 逻辑与数值节点扩展 [P2 - 功能扩展]
**目标**:扩展节点类型体系,新增逻辑运算节点和数值处理节点,参考 Blender 着色器节点的设计理念,实现可视化的信号处理与控制逻辑编排。
**规划**
```
当前节点体系:
设备节点(物理符号)─── 仅描述物理拓扑
扩展后:
设备节点(物理符号)─── 物理拓扑
逻辑节点(运算符号)─── 控制逻辑
数值节点(常量/变量)── 参数驱动
```
**逻辑节点**
| 节点 | 输入 | 输出 | 说明 |
|------|------|------|------|
| 大于 (GreaterThan) | A, B | out (Boolean) | A > B |
| 小于 (LessThan) | A, B | out (Boolean) | A < B |
| 等于 (Equal) | A, B | out (Boolean) | A == B |
| 与 (And) | A, B | out (Boolean) | A && B |
| 或 (Or) | A, B | out (Boolean) | A \|\| B |
| 非 (Not) | in | out (Boolean) | !in |
| 条件选择 (Switch) | cond, trueVal, falseVal | out | if cond then trueVal else falseVal |
**数值节点**(4 种基础类型):
| 节点 | 输出类型 | 说明 |
|------|---------|------|
| 整型 (Integer) | Integer | 输出整数值,参数可配 |
| 浮点型 (Real) | Real | 输出浮点数值,参数可配 |
| 字符串型 (String) | String | 输出字符串值,参数可配 |
| 布尔型 (Boolean) | Boolean | 输出 true/false,参数可配 |
- 逻辑/数值节点使用信号端口(signal 类型),与物理端口在视觉上区分
- 导出 .mo 时直接生成 Modelica 原生逻辑代码(equation 段内的表达式),不依赖 Blocks 库,例如:
```modelica
equation
greaterThan_1.out = a > b;
switch_1.out = if cond then trueVal else falseVal;
```
- 本系统采用**非因果 (acausal) 建模**,所有连接均通过 `connect()` 描述,不区分信号流方向,由 Modelica 求解器自动推导因果关系
---
## 5. 目录结构
```
eplan_gui/
src/
App.jsx # 主入口,视图切换
main.jsx # React 挂载点
index.css # 全局样式
components/
Canvas/
FlowCanvas.jsx # React Flow 画布容器
Nodes/
DeviceNode.jsx # 内置符号节点渲染
CustomDeviceNode.jsx # 统一符号节点渲染(四方向端口)
Edges/
GradientBezierEdge.jsx # 渐变贝塞尔连线
CustomConnectionLine.jsx # 自定义连线预览
Sidebar/
Sidebar.jsx # 侧边栏(符号库 / 映射 / 导入)
Toolbar/
Toolbar.jsx # 顶部工具栏
PropertiesPanel/
PropertiesPanel.jsx # 右侧属性面板
ComponentEditor/
ComponentEditor.jsx # 符号编辑器主组件
ShapeCanvas.jsx # SVG 形状绘制画布
PortEditor.jsx # 端口编辑面板
NodePreview.jsx # 节点预览
hooks/
useFlowStore.js # 画布状态管理 (Zustand)
useComponentLibrary.js # 符号库状态管理 (Zustand)
utils/
constants.js # 内置符号定义 + 端口类型 + 常量
modelicaExporter.js # .mo 模型代码生成
modelMapping.js # 符号 → Modelica 映射表
xlsxParser.js # EPLAN xlsx 解析
layoutEngine.js # Dagre 自动布局
docs/
design.md # 本文档
```
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment