From 88c688c0dfc566598a8f3ec7d69376360a5bb424 Mon Sep 17 00:00:00 2001 From: 18086616529 <344820985@qq.com> Date: Tue, 23 Jun 2026 17:22:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=88=86=E6=AE=B5=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Doc/MIGU-API.md | 496 +++++++++++++ MiGu.Server/Auth/PageCatalog.cs | 1 + MiGu.Server/Auth/RbacStore.cs | 6 + .../Controllers/ProjectionController.cs | 45 ++ .../Controllers/SimpleFieldController.cs | 85 +++ MiGu.Server/Persistence/PlatformDbContext.cs | 27 + .../Persistence/PlatformPersistence.cs | 94 +++ MiGu.Server/Program.cs | 1 + .../SimpleFields/SimpleFieldDateTime.cs | 21 + MiGu.Server/SimpleFields/SimpleFieldModels.cs | 84 +++ .../SimpleFields/SimpleFieldService.cs | 132 ++++ .../simple-platform-vue/src/api/reflection.ts | 30 + .../src/api/simpleField.ts | 31 + .../src/components/DataTablePro.vue | 54 ++ .../src/layouts/AppShell.vue | 3 +- .../apps/simple-platform-vue/src/mock/rbac.ts | 1 + .../simple-platform-vue/src/router/index.ts | 1 + .../src/types/simpleField.ts | 89 +++ .../views/admin/SimpleFieldManagementView.vue | 682 ++++++++++++++++++ 19 files changed, 1882 insertions(+), 1 deletion(-) create mode 100644 Doc/MIGU-API.md create mode 100644 MiGu.Server/Controllers/ProjectionController.cs create mode 100644 MiGu.Server/Controllers/SimpleFieldController.cs create mode 100644 MiGu.Server/SimpleFields/SimpleFieldDateTime.cs create mode 100644 MiGu.Server/SimpleFields/SimpleFieldModels.cs create mode 100644 MiGu.Server/SimpleFields/SimpleFieldService.cs create mode 100644 frontends/apps/simple-platform-vue/src/api/simpleField.ts create mode 100644 frontends/apps/simple-platform-vue/src/components/DataTablePro.vue create mode 100644 frontends/apps/simple-platform-vue/src/types/simpleField.ts create mode 100644 frontends/apps/simple-platform-vue/src/views/admin/SimpleFieldManagementView.vue diff --git a/Doc/MIGU-API.md b/Doc/MIGU-API.md new file mode 100644 index 0000000..85faa00 --- /dev/null +++ b/Doc/MIGU-API.md @@ -0,0 +1,496 @@ +# SimpleLite 面向咪咕平台数据 API 文档 + +> **版本**:与 SimpleLite 源码同步 +> **更新日期**:2026-06-18 +> **服务栈**:EmbedIO WebApi(非 ASP.NET Core) +> **OpenAPI**:`Docs/openapi/simplelite-projection.json`(已在 MiGu.Server Swagger UI 中展示) + +--- + +## 1. 架构与访问路径 + +SimpleLite 在进程内启动 HTTP 投影服务,默认监听 `http://127.0.0.1:8222`。咪咕平台后端 **MiGu.Server** 通过 YARP 反向代理将请求转发到 SimpleLite。 + +| 层级 | 前缀 | 说明 | +|------|------|------| +| 直连 SimpleLite | `http://127.0.0.1:8222/projection/...` | 开发调试、本机回环 | +| 经 MiGu.Server | `http://{host}:8080/api/sl/projection/...` | 平台前端/运维正式入口 | + +``` +浏览器 / Vue SPA + │ Authorization: Bearer {JWT} + ▼ +MiGu.Server :8080 (/api/sl/*) + │ 追加 X-Platform-Internal-Token + ▼ +SimpleLite :8222 (/projection/*) +``` + +### 1.1 鉴权 + +| 场景 | 要求 | +|------|------| +| 经 MiGu.Server 访问 | 需登录 JWT(`Authorization: Bearer` 或 Cookie `simple.auth.token`);YARP 自动注入 `X-Platform-Internal-Token` | +| 直连 SimpleLite | 本机回环(127.0.0.1 / ::1)默认放行;远程需携带与 MiGu.Server 共享的 `X-Platform-Internal-Token` | + +Token 解析优先级(SimpleLite 侧): + +1. `simple.json` → `platform.internalToken` +2. 环境变量 `SIMPLELITE__PLATFORM__INTERNALTOKEN` +3. `Platform.Server/data/.internal-token` 或 `MiGu.Server/data/.internal-token` + +### 1.2 通用响应格式 + +**投影快照类**(`ProjectionWebApiController`)直接返回 JSON 数组或对象,无统一信封。 + +**反射 / 地图编辑 / 工具栏 / 诊断 / AI 配置类** 使用统一信封: + +```json +{ + "success": true, + "code": 200, + "data": { }, + "message": "Success" +} +``` + +失败时 `success=false`,`code` 为 HTTP 语义码(400/500 等),`message` 为错误说明。 + +### 1.3 Swagger 查看 + +开发环境下启动 MiGu.Server 后访问: + +- **统一文档**:`http://localhost:8080/swagger`(含 MiGu.Server 自有 API + SimpleLite 全部 WebApi) +- SimpleLite 相关接口标签前缀为 `SimpleLite/` + +--- + +## 2. 实时数据流(SSE) + +| 方法 | 路径(MiGu.Server) | 说明 | +|------|---------------------|------| +| GET | `/api/sl/projection/stream` | Server-Sent Events 实时推送 | + +**Content-Type**:`text/event-stream` + +**事件类型**: + +| event | 说明 | +|-------|------| +| `snapshot-tick` | 连接建立时立即推送当前快照 | +| `car-state` | 车辆状态增量 | +| `mission-status` | 任务状态增量 | +| `selection-detail` | 选中对象详情变更 | +| `heartbeat` | 保活(约 15s) | + +地图编辑写操作完成后也会通过此通道广播增量,供多 Tab / 3D 视口同步。 + +--- + +## 3. 投影快照 API + +**前缀**:`/api/sl/projection` +**控制器**:`ProjectionWebApiController.cs` +**用途**:运营监控、工作台列表、选中详情、配送单管理 + +### 3.1 只读快照 + +| 方法 | 路径 | 说明 | 查询参数 | +|------|------|------|----------| +| GET | `/cars` | 全部车辆快照 | — | +| GET | `/missions` | 全部任务快照 | — | +| GET | `/sites` | 全部站点 | — | +| GET | `/tracks` | 全部路径 | — | +| GET | `/deliveries` | 配送单列表 | `includeFinished`(默认 true)、`includeAborted`(默认 true) | +| GET | `/fleet/health` | 车队健康探测 | — | +| GET | `/workbench/{nav}` | 工作台分页列表 | `nav` ∈ map / car / process / scene / script | +| GET | `/selection/detail` | 选中对象详情面板 | `objectKind`/`kind`、`objectId`/`id`、`tab`(默认 properties) | + +**车辆字段示例**(`GET /cars`): + +```json +{ + "id": "C01", + "name": "AGV-1", + "typeName": "SimpleLite.RCS.CarTypes.Car", + "rawId": 1, + "x": 1200.5, + "y": 800.0, + "theta": 1.57, + "batterySoc": 0.85, + "state": "running", + "lastUpdate": "2026-06-09T08:00:00.0000000Z", + "group": "main", + "address": "tcp://192.168.1.10:5000", + "ip": "192.168.1.10", + "onboardUrl": "http://192.168.1.10:8080", + "lstatus": "运行中" +} +``` + +**车辆 state 枚举**:`idle` / `running` / `paused` / `charging` / `fault` / `offline`(由 `lstatus` 中文/英文子串映射) + +**任务 status 枚举**:`queued` / `running` / `paused` / `completed` / `cancelled` / `failed` + +### 3.2 配送单操作 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/deliveries/{deliveryId}/cancel` | 取消配送单 | +| POST | `/deliveries/{deliveryId}/resend` | 重发配送单 | +| POST | `/deliveries/{deliveryId}/force-complete` | 强制完成 | + +成功返回 `{ "success": true, "deliveryId": N }`,失败 HTTP 400。 + +--- + +## 4. 反射式通用 API + +**前缀**:`/api/sl/projection/reflection` +**控制器**:`ReflectionApiController.cs` +**用途**:工作台动态渲染、字段读写、方法执行、插件管理、项目/应用配置 + +### 4.1 元数据与列表 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/kinds` | 顶级分类 KPI(map/car/process/scene/script)及 subKinds | +| GET | `/assemblies` | 已加载用户程序集列表 | +| GET | `/objects/{kind}` | 对象列表;`scene` 为 site+track+special 合成 | +| GET | `/types/{kind}` | 可实例化的 .NET 类型列表 | +| GET | `/methods/{kind}/{id}` | 单对象可执行方法表 | +| GET | `/methods-by-type/{kind}` | 按类型聚合的方法表 | +| GET | `/status/{kind}/{id}` | 对象运行状态键值 | +| GET | `/fields/{kind}/{id}` | 用户字段 + 成员字段 | +| GET | `/bundle/{kind}/{id}` | 对象 + 字段 + 方法 + 状态聚合包 | + +**kind 取值**:`map` / `car` / `mission` / `site` / `track` / `special` / `scene`(合成)/ `script` / `process` + +### 4.2 字段读写 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/fields/{kind}/{id}/{field}` | 写入字段(body: `{ "value": "..." }`) | +| DELETE | `/fields/{kind}/{id}/{field}` | 删除用户自定义字段 | + +### 4.3 选中态 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/selection` | 当前选中对象 | +| POST | `/selection` | 设置选中(body: `{ "kind", "id" }`) | +| POST | `/selection/clear` | 清除选中 | + +### 4.4 对象 CRUD + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/objects/{kind}` | 创建对象(body 依 kind 类型) | +| DELETE | `/objects/{kind}/{id}` | 删除对象 | + +### 4.5 方法执行 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET / POST | `/execute/{kind}/{id}/{method}` | 调用 `[MethodMember]` 装饰的方法;POST body 为参数 JSON 数组 | +| POST | `/car/{id}/goto-site` | 车辆前往站点(body: `{ "siteId": N }`) | +| POST | `/car/{id}/gotosite` | 同上(别名路由) | + +### 4.6 脚本 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/scripts/{id}/source` | 脚本源码 | +| GET | `/scripts/{id}/exception-status` | 脚本异常状态 | + +### 4.7 插件管理 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/plugins` | 已加载插件列表 | +| POST | `/plugins/reload` | 重新扫描并增量加载 | +| POST | `/plugins/{name}/unload` | 卸载指定插件(需重启才彻底移除) | + +### 4.8 项目 / 应用 / 监控配置 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/project/fields` | 项目级字段 | +| POST | `/project/fields/{field}` | 写入项目字段 | +| POST | `/project/save` | 保存项目到磁盘 | +| GET | `/app-config/fields` | 应用配置字段 | +| POST | `/app-config/fields/{field}` | 写入应用配置字段 | +| POST | `/app-config/save` | 保存应用配置 | +| GET / POST | `/monitor-config` | 监控端配置读写 | + +### 4.9 视口与车辆样式 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET / PATCH | `/viewport-style` | 3D 视口样式 | +| GET | `/car-style/types` | 车辆类型样式列表 | +| GET | `/car-types/coder-fields` | 所有插件车型的 Coder Fields 字段袋定义(Site/Track/Plan/Car) | +| GET / POST / DELETE | `/car-style/{typeFullName}` | 单类型样式 CRUD | +| GET / POST | `/car-style/alarm-colors` | 告警颜色配置 | +| POST | `/car-style/save` | 持久化样式到磁盘 | + +### 4.10 车型 Coder Fields + +**路径**:`GET /api/sl/projection/reflection/car-types/coder-fields` + +返回所有已加载插件中带 `[CarType]` 的车型,及其脚本 Coder 使用的 **SiteFields / TrackFields / PlanFields / CarFields** 字段袋定义。字段来源: + +- 车型类上的 `[TemplateTrackCoderSettings]` / `[TemplateSiteCoderSettings]` +- `[ProgramTrackCoderSettings]` 关联的 `ITrackCoder`(如 `CommonTemplateTrackCoder`、`MagneticTrackCoder`) + +**响应示例**(`ReflectionEnvelope` 信封): + +```json +{ + "success": true, + "code": 200, + "message": "Success", + "data": [ + { + "typeName": "StandardScene.CarTypes.Kiva", + "shortName": "Kiva", + "label": "Kiva", + "assemblyName": "StandardScene.QrLidar", + "siteFields": { + "typeName": "StandardScene.CarTypes.KivaSiteFields", + "shortName": "KivaSiteFields", + "assemblyName": "StandardScene.Core", + "baseTypeName": "StandardScene.CarTypes.BasicSiteFields", + "fields": [ + { "name": "Shelf", "typeName": "System.String", "defaultValue": "" }, + { "name": "FetchSpeed", "typeName": "System.Single", "defaultValue": 0 } + ] + }, + "trackFields": { "typeName": "StandardScene.CarTypes.KivaTrackFields", "fields": [] }, + "planFields": { "typeName": "StandardScene.CarTypes.KivaPlanFields", "fields": [] }, + "carFields": null + } + ] +} +``` + +**说明**: + +| 字段 | 含义 | +|------|------| +| `siteFields` / `trackFields` / `planFields` / `carFields` | 该车型对应类别的字段袋;未注册时为 `null` | +| `fields[].name` | 字段名(对应 `site.fields` / `track.fields` 字典键) | +| `fields[].typeName` | .NET 字段类型全名 | +| `fields[].defaultValue` | 字段袋类声明时的默认值 | + +同一车型若多个 Coder 引用不同 Fields 类型,接口会合并后取**最派生、字段最全**的类型(如 Kiva 取 `KivaTrackFields` 而非 `BasicTrackFields`)。返回的 `fields` 数组**包含继承链上全部 public 字段**(例如 `KivaTrackFields` 会同时包含 `BasicTrackFields` 的 `IOArea`、`Speed` 等基类字段)。 + +> **实现说明**:Fields 类在插件程序集中多为 `internal`;插件已通过 `InternalsVisibleTo("SimpleLite")` 向 SimpleLite 开放。**插件在 collectible ALC 中加载时**,SimpleCore 特性类型与宿主不一致,须通过 `CustomAttributeData` 读取 Coder 上的 `siteFields`/`trackFields` 等 metadata(不能依赖 `GetCustomAttributes(typeof(TemplateTrackCoderSettings))`)。`StandardScene.dll` 内 Fields 由 `CoderFieldsMetadata.Describe` 在 Core 程序集内反射导出。 + +--- + +## 5. 地图编辑 API + +**前缀**:`/api/sl/projection/map-edit` +**控制器**:`MapEditApiController.cs` +**用途**:平台地图设计器(创建/删除图元、拾取、工程/地图管理、AI 生图、录制回放) + +### 5.1 对象操作 + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/objects/{kind}` | 创建图元;kind ∈ site/track/bezier/arc/nurbs/image/text/model | +| DELETE | `/objects/{kind}/{id}` | 删除图元 | +| POST | `/objects/batch` | 批量创建/更新/删除 | +| POST | `/objects/{kind}/{id}/fields/copy-to` | 字段复制到另一对象 | +| POST | `/objects/track/{id}/sample-sites` | 路径自动采样站点 | +| POST | `/pick` | 拾取会话(canvas 点击返回坐标与命中对象) | + +### 5.2 仪表盘与资产 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/dashboard/summary` | 地图编辑 KPI 聚合 | +| POST | `/assets/upload` | 上传图片/模型资产 | + +### 5.3 工程管理 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/project/current` | 当前打开的工程信息 | +| POST | `/project/save` | 保存工程 | +| POST | `/project/load` | 加载工程 | +| GET | `/project/browse` | 浏览工程目录 | +| POST | `/project/native-pick-open` | 原生文件对话框打开 | +| POST | `/project/native-pick-save` | 原生文件对话框保存 | + +### 5.4 地图管理 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/maps` | 地图列表 | +| GET | `/maps/scene-task-status` | 场景任务状态 | +| POST | `/maps/save` | 保存当前地图 | +| POST | `/maps/open` | 打开地图 | +| POST | `/maps/use` | 切换使用地图 | +| POST | `/maps/rename` | 重命名地图 | +| DELETE | `/maps/{rawName}` | 删除地图 | +| POST | `/maps/merge` | 合并地图 | + +### 5.5 视图 / 图层 / AI / 录制 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET / POST | `/view-filter` | 视图过滤器 | +| GET | `/layers` | 图层列表 | +| POST | `/layers/visible` | 设置图层可见性 | +| POST | `/ai/map-generate` | AI 自然语言生成地图元素 | +| GET | `/recording/status` | 录制状态 | +| POST | `/recording/start` | 开始录制 | +| POST | `/recording/stop` | 停止录制 | +| GET | `/recordings` | 录像文件列表 | +| POST | `/recordings/delete` | 删除录像 | +| POST | `/playback/start` | 开始回放 | +| POST | `/playback/stop` | 停止回放 | + +--- + +## 6. 工作区工具栏 API + +**前缀**:`/api/sl/projection/toolbar` +**控制器**:`WorkspaceToolbarApiController.cs` +**用途**:Embed/CanvasOnly iframe 底栏(对齐、选择、显示、图层、录制、相机) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/state` | 聚合读取全部工具栏状态 | +| POST | `/align` | 对齐吸附开关(sites/cars/tracks) | +| POST | `/select` | 选择过滤(tracks/cars/decor/sites) | +| POST | `/display` | 显示内容(labels/primitives/cars) | +| POST | `/layer` | 图层可见性 | +| POST | `/recording/start` | 开始录制 | +| POST | `/recording/stop` | 停止录制 | +| POST | `/recording/rename` | 重命名录像 | +| DELETE | `/recording/{fileName}` | 删除录像 | +| POST | `/playback/start` | 开始回放 | +| POST | `/playback/stop` | 停止回放 | +| POST | `/view/toggle` | 切换视图模式 | +| POST | `/camera/follow` | 相机跟随车辆 | +| POST | `/camera/locate` | 相机定位到坐标/对象 | + +--- + +## 7. 场景插件 API(配置向导) + +**前缀**:`/api/sl/projection/scenes` +**控制器**:`SceneApiController.cs` +**用途**:咪咕平台「配置向导」选择性加载导航场景插件 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/available` | plugins 目录下全部场景清单 | +| GET | `/active` | 当前激活集合及已加载画像 | +| POST | `/apply` | 写入 `active-scenes.json` 并增量 reload | + +**POST /apply 请求体**: + +```json +{ + "activeScenes": ["scene-id-1", "scene-id-2"], + "alwaysLoad": ["common-plugin"], + "source": "wizard" +} +``` + +与 MiGu.Server `WizardController` 写配置后调用本接口联动。 + +--- + +## 8. 实时诊断 API + +**前缀**:`/api/sl/projection/diagnosis` +**控制器**:`DiagnosisApiController.cs` +**用途**:平台「日志管理 → 实时诊断」只读查看内核 Diagnosis 内存态 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/all` | 全部诊断条目(合订标签 + 滚动记录) | + +**响应 data 结构**: + +```json +{ + "serverTime": "2026-06-09T16:00:00.000", + "total": 42, + "taggedCount": 5, + "untaggedCount": 37, + "items": [ + { "index": 0, "time": "...", "tag": "Traffic", "tagged": true, "content": "..." } + ] +} +``` + +--- + +## 9. AI 服务配置 API + +**前缀**:`/api/sl/projection/ai-config` +**控制器**:`AiConfigController.cs` +**用途**:平台「系统配置 → AI 服务」表单读写 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/` | 读取配置(apiKey 脱敏为 `***last4`) | +| POST | `/` | 保存配置(脱敏占位符不覆盖原密钥) | + +**配置字段**:`endpoint`、`apiKey`、`model`、`systemPrompt`、`temperature`、`maxTokens`、`timeoutSec` + +--- + +## 10. 已下线接口 + +**前缀**:`/api/sl/projection/persistence/*` +**状态**:已在 `ProjectionWebHost` 中取消挂载,调用返回 **404** + +| 原路径 | 说明 | +|--------|------| +| POST `/export-json` | 导出 JSON | +| POST `/import-json` | 导入 JSON | +| POST `/reload-from-db` | 从 DB 重载 | +| POST `/resume-missions` | 恢复任务 | +| GET `/status` | 持久化状态 | +| GET / PATCH `/scene-meta` | 场景元数据 | + +--- + +## 11. 前端 API 胶水对照 + +咪咕 Vue 前端封装位于 `Migu2.0/frontends/apps/simple-platform-vue/src/api/`: + +| 文件 | 对应 SimpleLite 前缀 | +|------|----------------------| +| `reflection.ts` | `/api/sl/projection/reflection` | +| `mapEdit.ts` | `/api/sl/projection/map-edit` | +| `workspaceToolbar.ts` | `/api/sl/projection/toolbar` | +| `logs.ts` | `/api/sl/projection/diagnosis` | + +--- + +## 12. 端口与部署速查 + +| 服务 | 默认端口 | 路径 | +|------|----------|------| +| MiGu.Server | 8080 | `/api/sl/*` → SimpleLite | +| SimpleLite 投影 API | 8222 | `/projection/*` | +| SimpleLite webVRender | 8223 | `/vr/*`(3D 嵌入 UI,非数据 API) | + +**健康检查**:`GET http://localhost:8080/api/health/simplelite` + +**本地联调**: + +```powershell +# 1. 启动 SimpleLite(或登录 MiGu.Server 后自动拉起) +# 2. 启动 MiGu.Server +# 3. 浏览器打开 http://localhost:8080/swagger +# 4. 授权 Bearer token 后测试 /api/sl/projection/cars +``` diff --git a/MiGu.Server/Auth/PageCatalog.cs b/MiGu.Server/Auth/PageCatalog.cs index 01daf5d..ee1fb1f 100644 --- a/MiGu.Server/Auth/PageCatalog.cs +++ b/MiGu.Server/Auth/PageCatalog.cs @@ -40,6 +40,7 @@ public static class PageCatalog new("admin-processes", "进程管理", "设计与编排", ScopePlatform), new("admin-scripts", "脚本管理", "设计与编排", ScopePlatform), new("admin-task-templates", "任务编排", "设计与编排", ScopePlatform), + new("admin-simple-fields", "字段管理", "设计与编排", ScopePlatform), // ── 管理端 / Platform:平台配置中心(聚合页,每个 Key 对齐前端聚合路由 route.name) ── new("admin-config-strategy", "调度策略", "平台配置中心", ScopePlatform), diff --git a/MiGu.Server/Auth/RbacStore.cs b/MiGu.Server/Auth/RbacStore.cs index 11efef2..4cbc269 100644 --- a/MiGu.Server/Auth/RbacStore.cs +++ b/MiGu.Server/Auth/RbacStore.cs @@ -120,6 +120,12 @@ public sealed class RbacStore && !r.Pages.Contains("admin-task-templates", StringComparer.OrdinalIgnoreCase) && hasProcessAndScript) r.Pages.Add("admin-task-templates"); + + // Simple 字段管理:与任务编排同属设计与编排,有任务编排权限时自动补齐。 + if (!r.Pages.Contains(PageCatalog.Wildcard) + && !r.Pages.Contains("admin-simple-fields", StringComparer.OrdinalIgnoreCase) + && r.Pages.Contains("admin-task-templates", StringComparer.OrdinalIgnoreCase)) + r.Pages.Add("admin-simple-fields"); } private RbacSnapshot SeedDefault(IConfiguration config) diff --git a/MiGu.Server/Controllers/ProjectionController.cs b/MiGu.Server/Controllers/ProjectionController.cs new file mode 100644 index 0000000..d3bdb0b --- /dev/null +++ b/MiGu.Server/Controllers/ProjectionController.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace MiGu.Server.Controllers; + +/// +/// 投影 API 占位:真实落地时由 YARP 反代到 SimpleLite WebAPI 的 /api/projection/* 路径。 +/// 本地 Mock 数据仅用于无 SimpleLite 运行时的开发联调。 +/// +/// AR-4: 全 class 加 [Authorize] —— 任何登录用户都能读 mock 投影数据;未登录直接 401。 +/// +[ApiController] +[Authorize] +[Route("api/projection")] +public class ProjectionController : ControllerBase +{ + [HttpGet("sites")] + public IActionResult Sites() => Ok(new[] + { + new { id = "S001", name = "A 区-入库点", x = 1000, y = 2000 }, + new { id = "S002", name = "A 区-出库点", x = 3000, y = 2000 }, + new { id = "S003", name = "B 区-缓存区", x = 5000, y = 2000 } + }); + + [HttpGet("tracks")] + public IActionResult Tracks() => Ok(new[] + { + new { id = "T001", kind = "line", fromSiteId = "S001", toSiteId = "S002" }, + new { id = "T002", kind = "line", fromSiteId = "S002", toSiteId = "S003" } + }); + + [HttpGet("cars")] + public IActionResult Cars() => Ok(new[] + { + new { id = "C01", name = "AGV-001", state = "running", batterySoc = 0.86 }, + new { id = "C02", name = "AGV-002", state = "idle", batterySoc = 0.42 } + }); + + [HttpGet("missions")] + public IActionResult Missions() => Ok(new[] + { + new { id = "M01", name = "A 区送料 #1", status = "running", priority = 50 }, + new { id = "M02", name = "A→B 缓存搬运", status = "queued", priority = 60 } + }); +} diff --git a/MiGu.Server/Controllers/SimpleFieldController.cs b/MiGu.Server/Controllers/SimpleFieldController.cs new file mode 100644 index 0000000..9f08616 --- /dev/null +++ b/MiGu.Server/Controllers/SimpleFieldController.cs @@ -0,0 +1,85 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using MiGu.Server.SimpleFields; + +namespace MiGu.Server.Controllers; + +/// +/// Simple 字段管理 API:按车型维护 site / track / plan / car 四类反射字段的默认值与多语言名称。 +/// 数据持久化于 platform.db 的 simple_fields 表。 +/// +/// 对应前端「字段管理」页(/admin/simple-fields,页面 key admin-simple-fields)。 +/// 前端主流程为「刷新」读库、「默认」从 SimpleLite 拉取模板、「保存」走 全量替换。 +/// +[ApiController] +[Authorize] +[TypeFilter(typeof(SimpleFieldExceptionFilter))] +[Route("api/simple-fields")] +public sealed class SimpleFieldController : ControllerBase +{ + private readonly SimpleFieldService _service; + + public SimpleFieldController(SimpleFieldService service) => _service = service; + + /// + /// 查询字段列表,支持按字段类型、车型与关键字过滤。 + /// 结果按 car_type → field_type → key 排序。 + /// + /// 字段类型,如 siteFields、carFields。 + /// 车型唯一标识:assemblyName.shortName。 + /// 关键字,匹配 key / car_type / 中英文名 / 其他语言 / 默认值。 + [HttpGet] + public Task> List([FromQuery] string? fieldType, [FromQuery] string? carType, [FromQuery] string? q) => _service.ListAsync(fieldType, carType, q); + + /// + /// 新增单条字段;同车型 + 字段类型下 key 不可重复 + /// + [HttpPost] + public Task Create([FromBody] SimpleFieldRequest req) => _service.SaveAsync(req); + + /// + /// 按 id 更新单条字段 + /// + [HttpPut("{id:guid}")] + public Task Update(Guid id, [FromBody] SimpleFieldRequest req) => _service.SaveAsync(req with { Id = id }); + + /// + /// 按 id 删除单条字段 + /// + [HttpDelete("{id:guid}")] + public async Task Delete(Guid id) + { + await _service.DeleteAsync(id); + + return NoContent(); + } + + /// + /// 批量保存字段 + /// . 为 true 时先清空表再写入(前端「保存」使用此模式)。 + /// 返回实际写入条数 { count }。 + /// + [HttpPost("batch")] + public async Task SaveBatch([FromBody] SimpleFieldBatchRequest req) + { + var count = await _service.SaveBatchAsync(req); + + return Ok(new { count }); + } +} + +/// +/// 将 转为 HTTP 400,响应体 { message } +/// +public sealed class SimpleFieldExceptionFilter : IExceptionFilter +{ + public void OnException(ExceptionContext context) + { + if (context.Exception is not SimpleFieldException ex) { return; } + + context.Result = new BadRequestObjectResult(new { message = ex.Message }); + + context.ExceptionHandled = true; + } +} diff --git a/MiGu.Server/Persistence/PlatformDbContext.cs b/MiGu.Server/Persistence/PlatformDbContext.cs index 488326e..96ccc08 100644 --- a/MiGu.Server/Persistence/PlatformDbContext.cs +++ b/MiGu.Server/Persistence/PlatformDbContext.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using MiGu.Server.Wms; +using MiGu.Server.SimpleFields; namespace MiGu.Server.Persistence; @@ -16,6 +17,7 @@ public sealed class PlatformDbContext : DbContext public DbSet ContainerMaterials => Set(); public DbSet ContainerLocationHistories => Set(); public DbSet ContainerMaterialHistories => Set(); + public DbSet SimpleFields => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -55,6 +57,31 @@ public sealed class PlatformDbContext : DbContext modelBuilder.Entity().Property(x => x.Quantity).HasPrecision(18, 4); modelBuilder.Entity().Property(x => x.QuantityDelta).HasPrecision(18, 4); + + ConfigureSimpleField(modelBuilder); + } + + private static void ConfigureSimpleField(ModelBuilder modelBuilder) + { + var e = modelBuilder.Entity(); + e.ToTable("simple_fields"); + e.HasKey(x => x.Id); + e.Property(x => x.Id).HasColumnName("id"); + e.Property(x => x.CarType).HasColumnName("car_type").HasMaxLength(64); + e.Property(x => x.FieldType).HasColumnName("field_type").HasMaxLength(64); + e.Property(x => x.Key).HasColumnName("key").HasMaxLength(128); + e.Property(x => x.Value).HasColumnName("value"); + e.Property(x => x.DataType).HasColumnName("data_type").HasMaxLength(128); + e.Property(x => x.Chinese).HasColumnName("chinese").HasMaxLength(256).IsRequired(false); + e.Property(x => x.English).HasColumnName("english").HasMaxLength(256).IsRequired(false); + e.Property(x => x.Other).HasColumnName("other").HasMaxLength(512); + e.Property(x => x.IsDefault).HasColumnName("is_default"); + var dateTime = new ValueConverter( + v => SimpleFieldDateTime.ToStorage(v), + v => SimpleFieldDateTime.FromStorage(v)); + e.Property(x => x.CreateTime).HasColumnName("create_time").HasConversion(dateTime).HasMaxLength(19); + e.Property(x => x.UpdateTime).HasColumnName("update_time").HasConversion(dateTime).HasMaxLength(19); + e.HasIndex(x => new { x.CarType, x.FieldType, x.Key }).IsUnique(); } public override int SaveChanges(bool acceptAllChangesOnSuccess) diff --git a/MiGu.Server/Persistence/PlatformPersistence.cs b/MiGu.Server/Persistence/PlatformPersistence.cs index 4b09c7d..9b6cab8 100644 --- a/MiGu.Server/Persistence/PlatformPersistence.cs +++ b/MiGu.Server/Persistence/PlatformPersistence.cs @@ -1,6 +1,8 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Data.Sqlite; using MiGu.Server.Wms; +using MiGu.Server.SimpleFields; namespace MiGu.Server.Persistence; @@ -38,6 +40,7 @@ public static class PlatformPersistence services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } @@ -46,6 +49,97 @@ public static class PlatformPersistence using var scope = services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); await db.Database.EnsureCreatedAsync(); + // EnsureCreated 只在「库文件不存在」时建表;已有 platform.db 时新增实体不会自动补表。 + await EnsureSimpleFieldsTableAsync(db); + } + + /// 为已存在的数据库补建 simple_fields 表(幂等)。 + private static async Task EnsureSimpleFieldsTableAsync(PlatformDbContext db) + { + if (db.Database.IsSqlite()) + { + await db.Database.ExecuteSqlRawAsync(""" + CREATE TABLE IF NOT EXISTS simple_fields ( + id TEXT NOT NULL CONSTRAINT PK_simple_fields PRIMARY KEY, + car_type TEXT NOT NULL DEFAULT '', + field_type TEXT NOT NULL, + "key" TEXT NOT NULL, + value TEXT NOT NULL DEFAULT '', + data_type TEXT NOT NULL DEFAULT '', + chinese TEXT, + english TEXT, + other TEXT NOT NULL DEFAULT '', + is_default INTEGER NOT NULL, + create_time TEXT NOT NULL, + update_time TEXT NOT NULL + ); + """); + // 须先删旧索引 (field_type, other, key):把 other 清空为「其他语言」后会与旧唯一约束冲突。 + await db.Database.ExecuteSqlRawAsync("DROP INDEX IF EXISTS IX_simple_fields_field_type_other_key;"); + await db.Database.ExecuteSqlRawAsync(""" + UPDATE simple_fields SET car_type = other + WHERE (car_type IS NULL OR car_type = '') AND other <> ''; + """); + await db.Database.ExecuteSqlRawAsync(""" + UPDATE simple_fields SET other = '' + WHERE other <> '' AND other = car_type; + """); + await db.Database.ExecuteSqlRawAsync(""" + CREATE UNIQUE INDEX IF NOT EXISTS IX_simple_fields_car_type_field_type_key + ON simple_fields (car_type, field_type, "key"); + """); + return; + } + + // 非 SQLite:表不存在时尝试按当前模型创建(已有库不会走 EnsureCreated)。 + if (!await TableExistsAsync(db, "simple_fields")) + { + var creator = db.GetService(); + await creator.CreateTablesAsync(); + } + } + + /// + /// 检查表是否存在 + /// + /// 数据库上下文 + /// 表名 + /// 表是否存在 + private static async Task TableExistsAsync(PlatformDbContext db, string table) + { + var conn = db.Database.GetDbConnection(); + if (conn.State != System.Data.ConnectionState.Open) + await conn.OpenAsync(); + try + { + await using var cmd = conn.CreateCommand(); + if (db.Database.IsSqlServer()) + { + cmd.CommandText = "SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @t"; + var p = cmd.CreateParameter(); p.ParameterName = "@t"; p.Value = table; cmd.Parameters.Add(p); + } + else if (db.Database.IsNpgsql()) + { + cmd.CommandText = "SELECT 1 FROM information_schema.tables WHERE table_name = @t"; + var p = cmd.CreateParameter(); p.ParameterName = "@t"; p.Value = table; cmd.Parameters.Add(p); + } + else if (db.Database.IsMySql()) + { + cmd.CommandText = "SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = @t"; + var p = cmd.CreateParameter(); p.ParameterName = "@t"; p.Value = table; cmd.Parameters.Add(p); + } + else + { + return false; + } + var result = await cmd.ExecuteScalarAsync(); + return result != null; + } + finally + { + if (conn.State == System.Data.ConnectionState.Open) + await conn.CloseAsync(); + } } private static string ResolveConnectionString(IConfiguration configuration, IWebHostEnvironment env, string provider) diff --git a/MiGu.Server/Program.cs b/MiGu.Server/Program.cs index a01362b..b26ffdb 100644 --- a/MiGu.Server/Program.cs +++ b/MiGu.Server/Program.cs @@ -6,6 +6,7 @@ using Microsoft.OpenApi.Models; using MiGu.Server.Auth; using MiGu.Server.Configs; using MiGu.Server.Launcher; +using MiGu.Server.OpenApi; using MiGu.Server.Persistence; using Yarp.ReverseProxy.Transforms; diff --git a/MiGu.Server/SimpleFields/SimpleFieldDateTime.cs b/MiGu.Server/SimpleFields/SimpleFieldDateTime.cs new file mode 100644 index 0000000..1ab0bd1 --- /dev/null +++ b/MiGu.Server/SimpleFields/SimpleFieldDateTime.cs @@ -0,0 +1,21 @@ +using System.Globalization; + +namespace MiGu.Server.SimpleFields; + +public static class SimpleFieldDateTime +{ + public const string StorageFormat = "yyyy-MM-dd HH:mm:ss"; + + public static DateTimeOffset Now => DateTimeOffset.Now; + + public static string ToStorage(DateTimeOffset value) => + value.LocalDateTime.ToString(StorageFormat, CultureInfo.InvariantCulture); + + public static DateTimeOffset FromStorage(string value) + { + if (DateTime.TryParseExact(value, StorageFormat, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var local)) + return new DateTimeOffset(local); + + return DateTimeOffset.Parse(value, CultureInfo.InvariantCulture); + } +} diff --git a/MiGu.Server/SimpleFields/SimpleFieldModels.cs b/MiGu.Server/SimpleFields/SimpleFieldModels.cs new file mode 100644 index 0000000..1f2f2a5 --- /dev/null +++ b/MiGu.Server/SimpleFields/SimpleFieldModels.cs @@ -0,0 +1,84 @@ +using System.ComponentModel.DataAnnotations; + +namespace MiGu.Server.SimpleFields; + +public sealed class SimpleField +{ + public Guid Id { get; set; } = Guid.NewGuid(); + + /// 车型唯一标识:assemblyName.shortName,如 StandardScene.QrLidar.Forklift。 + [MaxLength(64)] + public string CarType { get; set; } = ""; + + /// + /// 字段类型唯一标识:assemblyName.shortName,如 StandardScene.QrLidar.Forklift。 + /// + [MaxLength(64)] + public string FieldType { get; set; } = ""; + + /// + /// 字段唯一标识:key,如 Forklift.PositionX。 + /// + [MaxLength(128)] + public string Key { get; set; } = ""; + + /// + /// 字段值,如 10.0。 + /// + public string Value { get; set; } = ""; + + /// + /// 数据类型,如 System.String。 + /// + [MaxLength(128)] + public string DataType { get; set; } = ""; + + /// + /// 中文名称,如 位置 X。 + /// + [MaxLength(256)] + public string? Chinese { get; set; } + + /// + /// 英文名称,如 Position X。 + /// + [MaxLength(256)] + public string? English { get; set; } + + /// + /// 其他语言名称,如 位置 X。 + /// + [MaxLength(512)] + public string Other { get; set; } = ""; + + /// + /// 是否内置默认字段,如 true。 + /// + public bool IsDefault { get; set; } + + /// + /// 创建时间,如 2021-01-01 12:00:00。 + /// + public DateTimeOffset CreateTime { get; set; } + + /// + /// 更新时间,如 2021-01-01 12:00:00。 + /// + public DateTimeOffset UpdateTime { get; set; } +} + +public sealed record SimpleFieldRequest( + Guid? Id, + string CarType, + string FieldType, + string Key, + string? Value, + string? DataType, + string? Chinese, + string? English, + string? Other, + bool IsDefault); + +public sealed record SimpleFieldBatchRequest( + bool ReplaceAll, + List Items); diff --git a/MiGu.Server/SimpleFields/SimpleFieldService.cs b/MiGu.Server/SimpleFields/SimpleFieldService.cs new file mode 100644 index 0000000..b9a23ea --- /dev/null +++ b/MiGu.Server/SimpleFields/SimpleFieldService.cs @@ -0,0 +1,132 @@ +using Microsoft.EntityFrameworkCore; +using MiGu.Server.Persistence; + +namespace MiGu.Server.SimpleFields; + +public sealed class SimpleFieldService +{ + private readonly PlatformDbContext _db; + + public SimpleFieldService(PlatformDbContext db) => _db = db; + + public async Task> ListAsync(string? fieldType = null, string? carType = null, string? q = null) + { + var query = _db.SimpleFields.AsNoTracking() + .OrderBy(x => x.CarType).ThenBy(x => x.FieldType).ThenBy(x => x.Key) + .AsQueryable(); + if (!string.IsNullOrWhiteSpace(fieldType)) + query = query.Where(x => x.FieldType == fieldType); + if (!string.IsNullOrWhiteSpace(carType)) + query = query.Where(x => x.CarType == carType); + if (!string.IsNullOrWhiteSpace(q)) + { + var kw = q.Trim(); + query = query.Where(x => + x.Key.Contains(kw) || + x.CarType.Contains(kw) || + (x.Chinese != null && x.Chinese.Contains(kw)) || + (x.English != null && x.English.Contains(kw)) || + x.Other.Contains(kw) || + x.Value.Contains(kw)); + } + return await query.ToListAsync(); + } + + public async Task SaveAsync(SimpleFieldRequest req) + { + if (string.IsNullOrWhiteSpace(req.CarType)) throw new SimpleFieldException("car_type 不能为空"); + if (string.IsNullOrWhiteSpace(req.FieldType)) throw new SimpleFieldException("field_type 不能为空"); + if (string.IsNullOrWhiteSpace(req.Key)) throw new SimpleFieldException("key 不能为空"); + + var carType = req.CarType.Trim(); + var now = SimpleFieldDateTime.Now; + SimpleField entity; + if (req.Id is { } id && id != Guid.Empty) + { + entity = await _db.SimpleFields.FirstOrDefaultAsync(x => x.Id == id) + ?? throw new SimpleFieldException("记录不存在"); + } + else + { + var dup = await _db.SimpleFields.AnyAsync(x => + x.CarType == carType && + x.FieldType == req.FieldType.Trim() && + x.Key == req.Key.Trim()); + if (dup) throw new SimpleFieldException("同车型与字段类型下 key 已存在"); + + entity = new SimpleField { Id = Guid.NewGuid(), CreateTime = now }; + _db.SimpleFields.Add(entity); + } + + entity.CarType = carType; + entity.FieldType = req.FieldType.Trim(); + entity.Key = req.Key.Trim(); + entity.Value = req.Value?.Trim() ?? ""; + entity.DataType = req.DataType?.Trim() ?? ""; + entity.Chinese = req.Chinese is null ? null : req.Chinese.Trim(); + entity.English = req.English is null ? null : req.English.Trim(); + entity.Other = req.Other?.Trim() ?? ""; + entity.IsDefault = req.IsDefault; + entity.UpdateTime = now; + if (entity.CreateTime == default) entity.CreateTime = now; + + await _db.SaveChangesAsync(); + return entity; + } + + public async Task DeleteAsync(Guid id) + { + var entity = await _db.SimpleFields.FirstOrDefaultAsync(x => x.Id == id) + ?? throw new SimpleFieldException("记录不存在"); + _db.SimpleFields.Remove(entity); + await _db.SaveChangesAsync(); + } + + /// 批量保存全部字段;ReplaceAll=true 时清空表后写入。 + public async Task SaveBatchAsync(SimpleFieldBatchRequest req) + { + var items = req.Items ?? new List(); + if (items.Count == 0) throw new SimpleFieldException("没有可保存的字段"); + + if (req.ReplaceAll) + { + var all = await _db.SimpleFields.ToListAsync(); + _db.SimpleFields.RemoveRange(all); + } + + var now = SimpleFieldDateTime.Now; + var added = 0; + foreach (var item in items) + { + if (string.IsNullOrWhiteSpace(item.CarType) || + string.IsNullOrWhiteSpace(item.FieldType) || + string.IsNullOrWhiteSpace(item.Key)) + continue; + + _db.SimpleFields.Add(new SimpleField + { + Id = Guid.NewGuid(), + CarType = item.CarType.Trim(), + FieldType = item.FieldType.Trim(), + Key = item.Key.Trim(), + Value = item.Value?.Trim() ?? "", + DataType = item.DataType?.Trim() ?? "", + Chinese = item.Chinese is null ? null : item.Chinese.Trim(), + English = item.English is null ? null : item.English.Trim(), + Other = item.Other?.Trim() ?? "", + IsDefault = item.IsDefault, + CreateTime = now, + UpdateTime = now + }); + added++; + } + + await _db.SaveChangesAsync(); + return added; + } +} + +public sealed class SimpleFieldException : Exception +{ + public SimpleFieldException(string message) : base(message) { } +} diff --git a/frontends/apps/simple-platform-vue/src/api/reflection.ts b/frontends/apps/simple-platform-vue/src/api/reflection.ts index 5dee0e2..e5da923 100644 --- a/frontends/apps/simple-platform-vue/src/api/reflection.ts +++ b/frontends/apps/simple-platform-vue/src/api/reflection.ts @@ -568,6 +568,11 @@ export const reflectionApi = { ? Promise.resolve({ globalDefault: defaultCarStyleDto(), types: [] }) : get('/car-style/types'), + /** 车型编码字段元数据(site/track/plan/car 四类字段及默认值)。 */ + getCarTypeCoderFields: () => MOCK + ? Promise.resolve([]) + : get('/car-types/coder-fields'), + getCarStyle: (typeFullName: string) => MOCK ? Promise.resolve(defaultCarStyleDto()) : get(`/car-style/${encodeURIComponent(typeFullName)}`), @@ -741,6 +746,31 @@ export interface CarStyleTypesPayload { types: CarStyleTypeRow[] } +export interface CoderFieldDef { + name: string + typeName: string + defaultValue: unknown +} + +export interface CoderFieldGroup { + typeName: string + shortName: string + assemblyName: string + baseTypeName?: string + fields: CoderFieldDef[] +} + +export interface CarTypeCoderFieldsRow { + typeName: string + shortName: string + label: string + assemblyName: string + siteFields: CoderFieldGroup + trackFields: CoderFieldGroup + planFields: CoderFieldGroup + carFields: CoderFieldGroup +} + export interface AlarmColorEntry { key: string colorArgb: number diff --git a/frontends/apps/simple-platform-vue/src/api/simpleField.ts b/frontends/apps/simple-platform-vue/src/api/simpleField.ts new file mode 100644 index 0000000..350e4ea --- /dev/null +++ b/frontends/apps/simple-platform-vue/src/api/simpleField.ts @@ -0,0 +1,31 @@ +import http from './http' +import type { SimpleFieldBatchPayload, SimpleFieldPayload, SimpleFieldRecord } from '@/types/simpleField' + +function q(params?: Record) { + return { params } +} + +export async function listSimpleFields(fieldType?: string, carType?: string, keyword?: string) { + const { data } = await http.get('/simple-fields', q({ + fieldType, + carType, + q: keyword + })) + return data +} + +export async function saveSimpleField(payload: SimpleFieldPayload) { + const { data } = payload.id + ? await http.put(`/simple-fields/${payload.id}`, payload) + : await http.post('/simple-fields', payload) + return data +} + +export async function deleteSimpleField(id: string) { + await http.delete(`/simple-fields/${id}`) +} + +export async function saveSimpleFieldsBatch(payload: SimpleFieldBatchPayload) { + const { data } = await http.post<{ count: number }>('/simple-fields/batch', payload) + return data +} diff --git a/frontends/apps/simple-platform-vue/src/components/DataTablePro.vue b/frontends/apps/simple-platform-vue/src/components/DataTablePro.vue new file mode 100644 index 0000000..19a238e --- /dev/null +++ b/frontends/apps/simple-platform-vue/src/components/DataTablePro.vue @@ -0,0 +1,54 @@ + + + + + {{ title }} + + + + + + + + + + + {{ scope.row[c.prop] }} + + + + + + + + + + + diff --git a/frontends/apps/simple-platform-vue/src/layouts/AppShell.vue b/frontends/apps/simple-platform-vue/src/layouts/AppShell.vue index a5cebcc..76e8828 100644 --- a/frontends/apps/simple-platform-vue/src/layouts/AppShell.vue +++ b/frontends/apps/simple-platform-vue/src/layouts/AppShell.vue @@ -154,7 +154,8 @@ const ADMIN_MENU: MenuItem[] = [ { path: '/admin/cars', label: '车辆管理', key: 'admin-cars' }, { path: '/admin/processes', label: '进程管理', key: 'admin-processes' }, { path: '/admin/scripts', label: '脚本管理', key: 'admin-scripts' }, - { path: '/admin/task-templates', label: '任务编排', key: 'admin-task-templates' } + { path: '/admin/task-templates', label: '任务编排', key: 'admin-task-templates' }, + { path: '/admin/simple-fields', label: '字段管理', key: 'admin-simple-fields' } ] }, { diff --git a/frontends/apps/simple-platform-vue/src/mock/rbac.ts b/frontends/apps/simple-platform-vue/src/mock/rbac.ts index 4357498..3d664bb 100644 --- a/frontends/apps/simple-platform-vue/src/mock/rbac.ts +++ b/frontends/apps/simple-platform-vue/src/mock/rbac.ts @@ -20,6 +20,7 @@ const PAGES: PageDef[] = [ { key: 'admin-processes', label: '进程管理', group: '设计与编排', scope: 'Platform' }, { key: 'admin-scripts', label: '脚本管理', group: '设计与编排', scope: 'Platform' }, { key: 'admin-task-templates', label: '任务编排', group: '设计与编排', scope: 'Platform' }, + { key: 'admin-simple-fields', label: '字段管理', group: '设计与编排', scope: 'Platform' }, { key: 'admin-config-strategy', label: '调度策略', group: '平台配置中心', scope: 'Platform' }, { key: 'admin-vehicle-hub', label: '车辆运维', group: '平台配置中心', scope: 'Platform' }, { key: 'admin-config-facility', label: '设备与库位', group: '平台配置中心', scope: 'Platform' }, diff --git a/frontends/apps/simple-platform-vue/src/router/index.ts b/frontends/apps/simple-platform-vue/src/router/index.ts index cdd0d89..1f0779c 100644 --- a/frontends/apps/simple-platform-vue/src/router/index.ts +++ b/frontends/apps/simple-platform-vue/src/router/index.ts @@ -37,6 +37,7 @@ const routes: RouteRecordRaw[] = [ { path: 'processes', name: 'admin-processes', component: () => import('@/views/admin/ProcessPanelView.vue'), meta: { title: '进程管理' } }, { path: 'scripts', name: 'admin-scripts', component: () => import('@/views/admin/ScriptPanelView.vue'), meta: { title: '脚本管理' } }, { path: 'task-templates', name: 'admin-task-templates', component: () => import('@/views/admin/TaskTemplateView.vue'), meta: { title: '任务编排' } }, + { path: 'simple-fields', name: 'admin-simple-fields', component: () => import('@/views/admin/SimpleFieldManagementView.vue'), meta: { title: '字段管理' } }, { path: 'project-properties', name: 'admin-project-properties', component: () => import('@/views/admin/ProjectPropertiesView.vue'), meta: { title: '项目属性' } }, // ── 平台配置中心:聚合页(每个聚合页一个 page key = route.name,对齐后端 PageCatalog)。 // 原十余个独立配置页按业务收敛为下列 6 个入口,子页改为聚合页内的 tab。 ── diff --git a/frontends/apps/simple-platform-vue/src/types/simpleField.ts b/frontends/apps/simple-platform-vue/src/types/simpleField.ts new file mode 100644 index 0000000..394bd90 --- /dev/null +++ b/frontends/apps/simple-platform-vue/src/types/simpleField.ts @@ -0,0 +1,89 @@ +export type SimpleFieldCategory = 'siteFields' | 'trackFields' | 'planFields' | 'carFields' + +export const SIMPLE_FIELD_CATEGORIES: { key: SimpleFieldCategory; label: string }[] = [ + { key: 'siteFields', label: '站点字段 (siteFields)' }, + { key: 'trackFields', label: '路径字段 (trackFields)' }, + { key: 'planFields', label: '计划字段 (planFields)' }, + { key: 'carFields', label: '车辆字段 (carFields)' } +] + +export interface SimpleFieldRecord { + id: string + carType: string + fieldType: string + key: string + value: string + dataType: string + chinese: string | null + english: string | null + other: string + isDefault: boolean + createTime: string + updateTime: string +} + +export interface SimpleFieldPayload { + id?: string + carType: string + fieldType: string + key: string + value?: string + dataType?: string + chinese?: string | null + english?: string | null + other?: string + isDefault: boolean +} + +export interface CoderFieldDef { + name: string + typeName: string + defaultValue: unknown +} + +export interface CoderFieldGroup { + typeName: string + shortName: string + assemblyName: string + baseTypeName?: string + fields: CoderFieldDef[] +} + +export interface CarTypeCoderFields { + typeName: string + shortName: string + label: string + assemblyName: string + siteFields: CoderFieldGroup + trackFields: CoderFieldGroup + planFields: CoderFieldGroup + carFields: CoderFieldGroup +} + +export interface SimpleFieldBatchPayload { + replaceAll: boolean + items: SimpleFieldPayload[] +} + +export interface FieldRow { + rowKey: string + id?: string + carType: string + fieldType: SimpleFieldCategory + key: string + dataType: string + value: string + chinese: string | null + english: string | null + other: string + isDefault: boolean +} + +export const ALL_CAR_TYPES = '__all__' + +/** car_type 唯一标识:assemblyName.shortName */ +export function buildCarType(assemblyName: string, shortName: string): string { + const asm = assemblyName?.trim() ?? '' + const sn = shortName?.trim() ?? '' + return asm && sn ? `${asm}.${sn}` : sn || asm +} diff --git a/frontends/apps/simple-platform-vue/src/views/admin/SimpleFieldManagementView.vue b/frontends/apps/simple-platform-vue/src/views/admin/SimpleFieldManagementView.vue new file mode 100644 index 0000000..ccbbacb --- /dev/null +++ b/frontends/apps/simple-platform-vue/src/views/admin/SimpleFieldManagementView.vue @@ -0,0 +1,682 @@ + + + + + + + 字段管理 + 「默认」从 SimpleLite 拉取全部车型字段;「刷新」从数据库读取;「保存」将全部字段写入数据库。 + + + 默认 + 保存 + 刷新 + + + + + + + 车辆类型: + + + + {{ carTypeCn(c) }} + ({{ buildCarType(c.assemblyName, c.shortName) }}) + + + + + + 搜索: + + + 新增字段 + + + + + + + + + + + + + + + {{ row.isDefault ? '是' : '否' }} + + + + + 编辑 + 删除 + + + + + + + + + + + + + + {{ carTypeCn(c) }} + ({{ buildCarType(c.assemblyName, c.shortName) }}) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 取消 + 确定 + + + + + + + + + +
「默认」从 SimpleLite 拉取全部车型字段;「刷新」从数据库读取;「保存」将全部字段写入数据库。