diff --git a/.trae/documents/空间拓扑与用电架构-实现计划.md b/.trae/documents/空间拓扑与用电架构-实现计划.md new file mode 100644 index 0000000..a81c867 --- /dev/null +++ b/.trae/documents/空间拓扑与用电架构-实现计划.md @@ -0,0 +1,121 @@ +# 空间拓扑与用电架构 · 实现计划 + +## Context(为什么做) + +现有「空间拓扑与用电架构」页面(`client/src/views/customer/topology/index.vue`)只实现了「客户 → 建筑 → 房间」三层树 + 建筑/房间增删改,**"用电架构"部分完全缺失**:数据库与后端没有任何承载"用电回路"的表或接口,页面上也没有配电拓扑可视化。 + +设计稿 `系统设计/UI/空间拓扑与用电架构/code.html` 定义了三栏工业级布局(左空间树 / 中配电单线拓扑流 / 右节点属性面板)。本次目标:在新表 + 新后端接口的基础上,把该页面补齐为三栏结构,让"空间"与"用电回路"打通。 + +## 已确认的边界(用户决策) + +| 项 | 结论 | +| ------ | ---------------------------------------- | +| 目标形态 | 按设计稿做完整三栏(左树 / 中单线图 / 右属性面板) | +| 左树层级 | 客户 → 建筑 → **楼层** → 房间 → **回路**(不要"配电箱"层) | +| 楼层 | 由房间/回路按 `floor_no` 分组生成的**虚拟层**,不建表 | +| 中间图上层 | 进线柜 / 变压器:**只读展示**,不落库、不做 CRUD | +| 中间图末端 | **回路卡片**(一个回路一张卡) | +| 实时电气量 | 电压/电流/功率/温度等**一律占位**(显示 `—`),不造假数据、不接采集 | +| ICC 映射 | **不做**(设计稿里的网关层、ICC 设备码、告警阈值联动均不实现) | +| 覆盖端 | 仅 Web 管理端 `client/` | +| 数据来源 | 新建表 + 后端 CRUD 接口 | + +## 数据模型(新增 1 张表) + +`customer_circuit`(客户用电回路)—— 参照 `customer_room` 的建表风格(`database/db_init.sql#L364-L387`): + +| 字段 | 类型 | 说明 | +| -------------- | --------------------- | ----------------------------------- | +| id | BIGSERIAL PK |
| +| customer\_id | BIGINT NOT NULL | → `customer(id)` | +| building\_id | BIGINT NOT NULL | → `customer_building(id)` | +| room\_id | BIGINT NULL | → `customer_room(id)`,允许挂建筑/楼层而不挂房间 | +| floor\_no | VARCHAR(16) | 楼层(冗余,便于按楼层分组) | +| circuit\_code | VARCHAR(64) NOT NULL | 回路编号,全局唯一 | +| circuit\_name | VARCHAR(128) NOT NULL | 回路名称 | +| loop\_spec | VARCHAR(128) | 回路规格(如 `三相380V / 铜芯4×25+1×16`) | +| load\_type | VARCHAR(32) | 负荷类型(照明/动力/空调/插座…) | +| rated\_voltage | VARCHAR(32) | 额定电压(配置值) | +| rated\_current | VARCHAR(32) | 额定电流(配置值) | +| rated\_power | NUMERIC(10,2) | 额定功率 kW(配置值) | +| status | SMALLINT DEFAULT 1 | 1 正常 / 0 停用 | +| remark | TEXT | 备注 | +| sort\_order | INT DEFAULT 0 | 排序 | +| created\_at | TIMESTAMP |
| + +索引:`customer_id`、`building_id`、`room_id`、唯一键 `circuit_code`。 + +> 命名跟随现有习惯:表 `customer_circuit`、entity `CustomerCircuit`、路由 `/api/customer-circuits`、权限码 `customer:circuit`(与 `customer:building` / `customer:room` 同级)。 + +## 后端改动(Midway.js + sequelize-typescript) + +参照样板:`customer_room` 全链路。 + +1. **迁移**:`database/migrations/20261011_customer_circuit.sql`(幂等 `CREATE TABLE IF NOT EXISTS`),并在 `database/db_init.sql` 同步该表定义。 +2. **Entity**:`server/src/entity/customer_circuit.entity.ts`(照抄 `customer_building.entity.ts` 的 `@Table/@Column/@BelongsTo/@CreatedAt` 写法)。 +3. **DTO**:`server/src/dto/customer_circuit.dto.ts`(Query/Create/Update,class-validator 风格照抄 `customer_room.dto.ts`)。 +4. **Service**:`server/src/service/customer_circuit.service.ts` + + * `list` / `detail` / `create` / `update` / `delete`(照抄 `customer_room.service.ts`,统一返回 `{ code, message, data }`)。 + + * `topology(customerId)`:**一次请求**返回整棵「建筑 → 楼层 → 房间 → 回路」结构,避免前端 N+1(现有页面是逐建筑拉房间)。返回形如: + + ```json + { "buildings": [ { "id":1, "buildingName":"…", "floors":[ { "floorNo":"2", "rooms":[ { "id":9, "roomName":"…", "circuits":[…] } ], "circuits":[…] } ] } ] } + ``` +5. **Controller**:`server/src/controller/customer_circuit.controller.ts`(`@Controller("/api/customer-circuits")`,含 `GET /topology`)。 +6. **权限注册**:`server/src/middleware/rbac.middleware.ts` 的 `PERMISSION_MAP` 新增 `"/api/customer-circuits": "customer:circuit"`。 +7. **菜单/权限种子**:在 `database/db_seed.sql` 复用现有「空间拓扑与用电架构」菜单(id=32, `/customer/topology`, 权限码 `customer:topology`),并**参照** **`customer:room`** **的登记方式**新增按钮级权限 `customer:circuit`。 + +## 前端改动(Vue3 + Ant Design Vue) + +1. **新增 API 封装** `client/src/api/customerCircuit.ts`:照抄 `customerRoom.ts` 风格(`getList/getDetail/create/update/delete/getTopology` + 类型定义 + 常量:负荷类型选项)。 +2. **重写** `client/src/views/customer/topology/index.vue` 为三栏: + + * **顶部**:面包屑 + 客户切换器 + 指标条 + 「新增回路」按钮。指标只展示**可由现有数据算出**的项(已接入建筑数、监测用电回路数、空间建模覆盖率 = 有回路的房间数/房间总数);无数据源的项(变电所/配电室、ICC 边缘网关)**不展示**。 + + * **左栏(≈280px)**:`a-tree` 渲染「客户 → 建筑 → 楼层 → 房间 → 回路」;节点徽章显示回路数;节点悬停操作:建筑/房间沿用现有增删改(`customerBuildingApi` / `customerRoomApi`),回路可新增/编辑/删除。 + + * **中栏**:单线图。上层为**只读示意**(10kV 进线柜 → 变压器 → 低压母线,按客户/建筑信息填充文案)、下层为**回路卡片网格**(编号/名称/所属房间/规格/额定参数,点击选中;实时电气量显示 `—`)。用 div 网格 + 内联 SVG 母线渲染(与设计稿一致),**不引入新依赖**(echarts 已在依赖中但不必要)。 + + * **右栏(≈320px)**:选中节点的属性面板(回路基本信息可编辑并保存;房间/建筑节点展示只读属性)。 + + * 样式沿用页面既有基线:背景 `#eef3f8`、主色 `#0054cd`、卡片圆角 `12px`、等宽字体用 `Consolas/Menlo`。 +3. **路由/权限**:沿用现有 `customer/topology` 路由与 `customer:topology` 菜单,**不新增页面路由**。 + +## 不做(明确排除) + +* ICC 网关节点、ICC 设备码映射、告警阈值联动、下发同步 + +* 实时电气量采集与展示(一律 `—`) + +* 「从 ICC 同步拓扑」「CAD/BIM 导入」「导出高清拓扑图」按钮 → 先不实现(可留占位,若留则点击提示"暂未开放") + +* 「空间三维映射」视图切换 + +* 配电箱/配电柜层 + +* Flutter 管理端 + +## 假设(如不认可请指出) + +1. 中间图的上层(进线柜/变压器)**纯前端静态示意**,文案按所选客户/建筑动态填充,不落库。 +2. `circuit_code` **全局唯一**(与设备台账 `equip_code` 一致);若你希望"客户内唯一",需改唯一约束。 +3. 回路可以**不挂房间**(只挂建筑+楼层),以适应公共区域回路。 +4. 顶部指标只做能算出来的 3 项,其余留白而非显示假数据。 + +## 验证方式 + +1. 执行迁移:`database/migrations/20261011_customer_circuit.sql`(以及 db\_seed 的权限登记),确认表与权限数据落库。 +2. 后端:启动 `server/`,用带 token 的请求验证 `GET /api/customer-circuits/topology?customerId=1`、以及 create/update/delete;无 token 应 401、无权限应 403。 +3. 前端:`client/` 执行 `npm run check`(vue-tsc)通过;`npm run dev` 后进入「客户管理 → 空间拓扑与用电架构」: + + * 选客户 → 左树按「建筑 → 楼层 → 房间 → 回路」正确展开; + + * 新增/编辑/删除回路后树与中间卡片同步刷新; + + * 点击回路卡片 → 右栏属性正确回填、可保存; + + * 中间图上层的进线柜/变压器为只读示意。 +4. 回归:确认原有建筑/房间增删改仍可用(房间功能未被破坏)。 + diff --git a/client/src/api/customerCircuit.ts b/client/src/api/customerCircuit.ts new file mode 100644 index 0000000..96851d7 --- /dev/null +++ b/client/src/api/customerCircuit.ts @@ -0,0 +1,132 @@ +import request from '@/utils/request'; + +export interface CustomerCircuitInfo { + id: number; + customerId: number; + buildingId: number; + roomId?: number | null; + floorNo?: string; + circuitCode: string; + circuitName: string; + loopSpec?: string; + loadType?: string; + ratedVoltage?: string; + ratedCurrent?: string; + ratedPower?: number | string | null; + status: number; + remark?: string; + sortOrder?: number; + createdAt?: string; +} + +export interface CustomerCircuitQuery { + page?: number; + pageSize?: number; + keyword?: string; + customerId?: number; + buildingId?: number; + roomId?: number; + floorNo?: string; +} + +export interface CustomerCircuitListResult { + list: CustomerCircuitInfo[]; + total: number; + page: number; + pageSize: number; +} + +export interface CustomerCircuitCreateParams { + circuitCode: string; + customerId: number; + buildingId: number; + circuitName: string; + roomId?: number; + floorNo?: string; + loopSpec?: string; + loadType?: string; + ratedVoltage?: string; + ratedCurrent?: string; + ratedPower?: number; + status?: number; + remark?: string; + sortOrder?: number; +} + +export interface CustomerCircuitUpdateParams { + circuitCode?: string; + customerId?: number; + buildingId?: number; + circuitName?: string; + roomId?: number; + floorNo?: string; + loopSpec?: string; + loadType?: string; + ratedVoltage?: string; + ratedCurrent?: string; + ratedPower?: number; + status?: number; + remark?: string; + sortOrder?: number; +} + +// ==================== 空间拓扑(一次返回整棵树) ==================== +export interface TopologyRoom { + id: number; + roomCode: string; + roomName: string; + buildingId: number; + floorNo?: string; + roomPurpose?: string; + deptId?: number; + circuits: CustomerCircuitInfo[]; +} + +export interface TopologyFloor { + floorNo: string; + rooms: TopologyRoom[]; + circuits: CustomerCircuitInfo[]; +} + +export interface TopologyBuilding { + id: number; + customerId: number; + buildingName: string; + purpose?: string; + basicInfo?: string; + floorCount?: number; + floorDownCount?: number; + floors: TopologyFloor[]; +} + +export interface CustomerTopologyResult { + buildings: TopologyBuilding[]; +} + +// ==================== 常量 ==================== +export const CIRCUIT_LOAD_TYPES = ['照明', '动力', '空调', '插座', '电梯', '消防', '其他']; + +export const CIRCUIT_STATUS_OPTIONS = [ + { label: '正常', value: 1 }, + { label: '停用', value: 0 }, +]; + +export const customerCircuitApi = { + getList: (params: CustomerCircuitQuery) => + request.get('/customer-circuits', { params }), + + getTopology: (customerId: number) => + request.get('/customer-circuits/topology', { params: { customerId } }), + + getDetail: (id: number) => + request.get(`/customer-circuits/${id}`), + + create: (data: CustomerCircuitCreateParams) => + request.post('/customer-circuits', data), + + update: (id: number, data: CustomerCircuitUpdateParams) => + request.put(`/customer-circuits/${id}`, data), + + delete: (id: number) => + request.delete(`/customer-circuits/${id}`), +}; \ No newline at end of file diff --git a/client/src/views/customer/topology/index.vue b/client/src/views/customer/topology/index.vue index 6d96d1d..4d7605a 100644 --- a/client/src/views/customer/topology/index.vue +++ b/client/src/views/customer/topology/index.vue @@ -1,6 +1,6 @@