diff --git a/docs/fixes/路径导航手动充电暂停恢复接口文档.md b/docs/fixes/路径导航手动充电暂停恢复接口文档.md new file mode 100644 index 0000000..699ae02 --- /dev/null +++ b/docs/fixes/路径导航手动充电暂停恢复接口文档.md @@ -0,0 +1,697 @@ +# 路径导航、手动充电、小车暂停/恢复运行接口文档 + +## 接口概述 + +**接口名称:** AMR小车控制接口 +**接口描述:** 提供AMR小车的暂停/恢复运行控制、手动发布充电任务和手动下发移动任务(路径导航)功能。在某个场景中选择一个小车,如果该小车正在运行中,可以下发暂停运行指令,将小车置为暂停状态;如果小车是暂停状态,可以下发恢复运行指令,使小车恢复到之前的运行状态;也可以手动为小车发布充电任务(需要小车处于空载状态);还可以手动为小车下发移动任务,指定小车移动到目标站点 + +--- + +## 目录 + +- [1. 暂停小车运行](#1-暂停小车运行) +- [2. 恢复小车运行](#2-恢复小车运行) +- [3. 手动发布充电任务](#3-手动发布充电任务) +- [4. 手动下发小车移动任务](#4-手动下发小车移动任务) +- [通用规范](#通用规范) +- [错误处理](#错误处理) +- [调用示例](#调用示例) +- [注意事项](#注意事项) + +--- + +## 1. 暂停小车运行 + +### 接口信息 + +| 项目 | 内容 | +|------|------| +| **请求方法** | PUT | +| **请求路径** | `/amr/{id}/pause` | +| **接口标识** | pauseAmr | +| **权限要求** | `amr:vwed_amr:edit` | + +### 接口描述 + +如果小车正在运行中(`navigationalState` 为 `"driving"`),可以下发暂停运行指令,将小车置为暂停状态。 + +### 请求参数 + +#### 路径参数 + +| 参数名 | 类型 | 必填 | 说明 | 示例值 | +|--------|------|------|------|--------| +| id | String | 是 | AMR的ID | `"amr_001"` | + +#### 请求体参数 + +| 参数名 | 类型 | 必填 | 说明 | 示例值 | +|--------|------|------|------|--------| +| sceneId | String | 是 | 场景ID | `"scene_001"` | + +### 请求示例 + +```json +PUT /amr/amr_001/pause +Content-Type: application/json + +{ + "sceneId": "scene_001" +} +``` + +### 响应数据 + +#### 成功响应 + +**HTTP状态码:** 200 +**业务状态码:** 200 + +```json +{ + "success": true, + "message": "暂停小车运行成功!", + "code": 200, + "result": "暂停小车运行成功!", + "timestamp": 1728547200000 +} +``` + +#### 错误响应 + +**参数错误(AMR ID为空)** + +```json +{ + "success": false, + "message": "AMR ID为空!", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**参数错误(场景ID为空)** + +```json +{ + "success": false, + "message": "场景ID为空!", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**业务错误(小车不存在、不在线、不是运行状态或指令发送失败)** + +```json +{ + "success": false, + "message": "暂停小车运行失败!可能是小车不存在、不在线、不是运行状态或指令发送失败,请查看日志获取详细信息。", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + + + +--- + +## 2. 恢复小车运行 + +### 接口信息 + +| 项目 | 内容 | +|------|------| +| **请求方法** | PUT | +| **请求路径** | `/amr/{id}/resume` | +| **接口标识** | resumeAmr | +| **权限要求** | `amr:vwed_amr:edit` | + +### 接口描述 + +如果小车是暂停状态(`navigationalState` 为 `"paused"`),可以下发恢复运行指令,使小车恢复到之前的运行状态。 + +### 请求参数 + +#### 路径参数 + +| 参数名 | 类型 | 必填 | 说明 | 示例值 | +|--------|------|------|------|--------| +| id | String | 是 | AMR的ID | `"amr_001"` | + +#### 请求体参数 + +| 参数名 | 类型 | 必填 | 说明 | 示例值 | +|--------|------|------|------|--------| +| sceneId | String | 是 | 场景ID | `"scene_001"` | + +### 请求示例 + +```json +PUT /amr/amr_001/resume +Content-Type: application/json + +{ + "sceneId": "scene_001" +} +``` + +### 响应数据 + +#### 成功响应 + +**HTTP状态码:** 200 +**业务状态码:** 200 + +```json +{ + "success": true, + "message": "恢复小车运行成功!", + "code": 200, + "result": "恢复小车运行成功!", + "timestamp": 1728547200000 +} +``` + +#### 错误响应 + +**参数错误(AMR ID为空)** + +```json +{ + "success": false, + "message": "AMR ID为空!", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**参数错误(场景ID为空)** + +```json +{ + "success": false, + "message": "场景ID为空!", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**业务错误(小车不存在、不在线、不是暂停状态或指令发送失败)** + +```json +{ + "success": false, + "message": "恢复小车运行失败!可能是小车不存在、不在线、不是暂停状态或指令发送失败,请查看日志获取详细信息。", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + + + +--- + +## 3. 手动发布充电任务 + +### 接口信息 + +| 项目 | 内容 | +|------|------| +| **请求方法** | GET | +| **请求路径** | `/amr/manuallSetCharging/{sceneId}/{amrId}` | +| **接口标识** | manuallSetCharging | +| **权限要求** | 无特殊权限要求(或使用默认权限) | + +### 接口描述 + +手动为指定场景中的小车发布充电任务。**注意:需要进行充电的小车必须是空载状态(`isLoading = false`)**。 + +接口会设置小车的充电标志(`is_need_charge = true`),由调度器算法生成充电任务。 + +### 请求参数 + +#### 路径参数 + +| 参数名 | 类型 | 必填 | 说明 | 示例值 | +|--------|------|------|------|--------| +| sceneId | String | 是 | 场景ID | `"scene_001"` | +| amrId | String | 是 | AMR的ID | `"amr_001"` | + +### 请求示例 + +``` +GET /amr/manuallSetCharging/scene_001/amr_001 +``` + +### 响应数据 + +#### 成功响应 + +**HTTP状态码:** 200 +**业务状态码:** 200 + +```json +{ + "success": true, + "message": "手动发布充电任务成功!", + "code": 200, + "result": "手动发布充电任务成功!", + "timestamp": 1728547200000 +} +``` + +#### 错误响应 + +**参数错误(场景ID或AMR ID为空)** + +```json +{ + "success": false, + "message": "场景ID或AMR id为空!", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**业务错误(小车不存在)** + +```json +{ + "success": false, + "message": "小车不存在!", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**业务错误(小车不是空载状态)** + +```json +{ + "success": false, + "message": "小车当前不是空载状态,无法发送充电指令!", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**业务错误(场景实例不存在或小车不在场景中)** + +```json +{ + "success": false, + "message": "手动发布充电任务失败!可能是场景实例不存在或小车不在场景中,请查看日志获取详细信息。", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + + + +--- + +## 4. 手动下发小车移动任务 + +### 接口信息 + +| 项目 | 内容 | +|------|------| +| **请求方法** | POST | +| **请求路径** | `/task/manual/move` | +| **接口标识** | manualMoveTask | +| **权限要求** | `task:vwed_task:add` | + +### 接口描述 + +手动为指定场景中的小车下发移动任务,使小车移动到目标站点。接口会自动创建任务、任务块和移动动作,并将任务推送到调度器队列中执行。 + +**注意:** 小车必须满足以下条件才能下发移动任务: +- 小车必须存在 +- 小车必须在线(`isOnline = 1`) +- 小车必须开启接单(`isAcceptTask = 1`) +- 小车当前没有正在执行的任务(`currentTaskName` 和 `curTaskId` 为空) +- 小车状态必须为空闲(`amrStatus = 0`) +- 小车必须关联到目标场景 + +### 请求参数 + +#### 请求体参数 + +| 参数名 | 类型 | 必填 | 说明 | 示例值 | +|--------|------|------|------|--------| +| sceneId | String | 是 | 场景ID | `"scene_001"` | +| amrId | String | 是 | AMR的ID | `"amr_001"` | +| taskName | String | 是 | 任务名称 | `"手动移动任务"` | +| targetStationName | String | 是 | 目标站点名称 | `"station_001"` | +| description | String | 否 | 任务描述 | `"手动下发的小车移动任务"` | +| operator | String | 否 | 操作人,默认为"system" | `"admin"` | +| priority | Integer | 是 | 任务优先级,范围1-1000,默认500 | `500` | + +### 请求示例 + +```json +POST /task/manual/move +Content-Type: application/json + +{ + "sceneId": "scene_001", + "amrId": "amr_001", + "taskName": "手动移动任务", + "targetStationName": "station_001", + "description": "手动下发的小车移动任务", + "operator": "admin", + "priority": 500 +} +``` + +### 响应数据 + +#### 成功响应 + +**HTTP状态码:** 200 +**业务状态码:** 200 + +```json +{ + "success": true, + "message": "操作成功", + "code": 200, + "result": { + "taskId": "task_123456", + "taskBlockId": "block_123456", + "vwedTaskId": "vwed_task_123456", + "amrId": "amr_001", + "amrName": "AMR-001", + "currentStationName": "station_002", + "targetStationName": "station_001" + }, + "timestamp": 1728547200000 +} +``` + +**响应字段说明:** + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| taskId | String | 创建的任务ID | +| taskBlockId | String | 创建的任务块ID | +| vwedTaskId | String | VWED任务ID | +| amrId | String | 小车ID | +| amrName | String | 小车名称 | +| currentStationName | String | 小车当前站点名称 | +| targetStationName | String | 目标站点名称 | + +#### 错误响应 + +**参数错误(场景ID为空)** + +```json +{ + "success": false, + "message": "场景ID不能为空", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**参数错误(AMR ID为空)** + +```json +{ + "success": false, + "message": "AMR id不能为空", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**参数错误(任务名称为空)** + +```json +{ + "success": false, + "message": "任务名称不能为空", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**参数错误(目标站点为空)** + +```json +{ + "success": false, + "message": "目标站点不能为空", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**参数错误(优先级范围错误)** + +```json +{ + "success": false, + "message": "优先级范围为1-1000", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**业务错误(场景ID不存在)** + +```json +{ + "success": false, + "message": "场景ID不存在", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**业务错误(小车不存在)** + +```json +{ + "success": false, + "message": "指定的小车不存在", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**业务错误(小车未上线)** + +```json +{ + "success": false, + "message": "小车未上线,无法下发任务", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**业务错误(小车未开启接单)** + +```json +{ + "success": false, + "message": "小车未开启接单,无法下发任务", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**业务错误(小车正在执行任务)** + +```json +{ + "success": false, + "message": "小车正在执行任务,无法重复下发", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**业务错误(小车状态非空闲)** + +```json +{ + "success": false, + "message": "小车状态非空闲,暂不能执行新任务", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + +**业务错误(小车未关联到目标场景)** + +```json +{ + "success": false, + "message": "小车未关联到目标场景,无法调度", + "code": 500, + "result": null, + "timestamp": 1728547200000 +} +``` + + + +--- + +## 通用规范 + +### 统一返回结构 + +所有接口均采用统一的返回结构: + +```json +{ + "success": boolean, // 操作是否成功 + "message": string, // 提示信息 + "code": number, // 状态码(200表示成功,其他表示失败) + "result": any, // 业务数据(成功时返回具体数据,失败时为null) + "timestamp": number // 时间戳 +} +``` + +### 小车状态说明 + +小车的导航状态(`navigationalState`)可能的值: + +- `"driving"`:正在运行中 +- `"paused"`:已暂停 +- `"finish"`:已完成/空闲 + +### 操作前提条件 + +**暂停操作前提:** +- 小车必须存在 +- 小车必须在线(`isOnline = 1`) +- 小车必须处于运行状态(`navigationalState = "driving"`) + +**恢复操作前提:** +- 小车必须存在 +- 小车必须在线(`isOnline = 1`) +- 小车必须处于暂停状态(`navigationalState = "paused"`) + +**手动发布充电任务前提:** +- 小车必须存在 +- 小车必须处于空载状态(`isLoading = false`) +- 场景实例必须存在 +- 小车必须在场景中 + +**手动下发移动任务前提:** +- 小车必须存在 +- 小车必须在线(`isOnline = 1`) +- 小车必须开启接单(`isAcceptTask = 1`) +- 小车当前没有正在执行的任务(`currentTaskName` 和 `curTaskId` 为空) +- 小车状态必须为空闲(`amrStatus = 0`) +- 小车必须关联到目标场景 + +--- + +## 错误处理 + +### 常见错误码 + +| 错误码 | 说明 | 处理建议 | +|--------|------|----------| +| 200 | 操作成功 | - | +| 500 | 操作失败 | 查看错误信息,检查小车状态和日志 | + +### 错误信息说明 + +- **"AMR ID为空!"**:路径参数中的AMR ID未提供或为空 +- **"场景ID为空!"**:请求体中的场景ID未提供或为空 +- **"暂停小车运行失败!可能是小车不存在、不在线、不是运行状态或指令发送失败,请查看日志获取详细信息。"**:业务逻辑检查失败或指令发送失败 +- **"恢复小车运行失败!可能是小车不存在、不在线、不是暂停状态或指令发送失败,请查看日志获取详细信息。"**:业务逻辑检查失败或指令发送失败 +- **"场景ID或AMR id为空!"**:路径参数中的场景ID或AMR ID未提供或为空 +- **"小车不存在!"**:指定的小车不存在 +- **"小车当前不是空载状态,无法发送充电指令!"**:小车正在载货,无法发布充电任务 +- **"手动发布充电任务失败!可能是场景实例不存在或小车不在场景中,请查看日志获取详细信息。"**:场景实例不存在或小车不在场景中 +- **"场景ID不能为空"**:请求体中的场景ID未提供或为空 +- **"AMR id不能为空"**:请求体中的AMR ID未提供或为空 +- **"任务名称不能为空"**:请求体中的任务名称未提供或为空 +- **"目标站点不能为空"**:请求体中的目标站点未提供或为空 +- **"优先级范围为1-1000"**:请求体中的优先级不在有效范围内 +- **"场景ID不存在"**:指定的场景ID不存在 +- **"指定的小车不存在"**:指定的小车不存在 +- **"小车未上线,无法下发任务"**:小车不在线 +- **"小车未开启接单,无法下发任务"**:小车未开启接单功能 +- **"小车正在执行任务,无法重复下发"**:小车当前有正在执行的任务 +- **"小车状态非空闲,暂不能执行新任务"**:小车状态不是空闲状态 +- **"小车未关联到目标场景,无法调度"**:小车未关联到指定的场景 + + + +--- + +## 注意事项 + +1. **权限验证**: + - 暂停/恢复接口需要 `amr:vwed_amr:edit` 权限 + - 手动发布充电任务接口可能不需要特殊权限 + - 手动下发移动任务接口需要 `task:vwed_task:add` 权限 + +2. **参数校验**: + - `sceneId` 为必填参数,前端需要进行相应的表单验证 + - AMR ID通过路径参数传递,必须提供有效的AMR ID + +3. **状态检查**: + - 暂停操作要求小车必须处于运行状态(`navigationalState = "driving"`) + - 恢复操作要求小车必须处于暂停状态(`navigationalState = "paused"`) + - 小车必须在线(`isOnline = 1`)才能执行暂停/恢复操作 + - 手动发布充电任务要求小车必须处于空载状态(`isLoading = false`) + - 手动下发移动任务要求小车必须在线、开启接单、无在执行任务、状态为空闲,且必须关联到目标场景 + +4. **错误处理**:建议前端对各种错误情况进行友好的提示处理,特别是状态不匹配的情况 + +5. **日志记录**:接口调用会自动记录操作日志,便于问题追踪 + +6. **指令类型**:接口使用 VDA5050 标准的 `pause` 和 `resume` 即时动作类型,需要确保设备支持这些指令类型 + +7. **场景隔离**:接口支持场景ID参数,符合多场景架构设计 + +8. **异步操作**:指令发送是异步的,接口返回成功仅表示指令已发送,不代表小车状态已立即改变。建议前端在操作后适当延迟再查询小车状态 + +9. **手动发布充电任务特殊说明**: + - 该接口不会直接发送充电指令给小车,而是设置充电标志 + - 调度器算法会检测到充电标志并自动生成充电任务 + - 小车会按照调度器的任务安排前往充电站进行充电 + - 充电任务的执行时间取决于调度器的任务调度策略 + +10. **手动下发移动任务特殊说明**: + - 该接口会自动创建任务、任务块和移动动作 + - 任务块会自动推送到 Redis 队列中,由调度器调度执行 + - 小车会按照调度器的任务安排移动到目标站点 + - 任务的执行时间取决于调度器的任务调度策略和任务优先级 + - 接口会返回创建的任务ID、任务块ID等信息,可用于后续查询任务状态 + +--- + + + diff --git a/src/apis/amr/api.ts b/src/apis/amr/api.ts index dd1cfac..c09886f 100644 --- a/src/apis/amr/api.ts +++ b/src/apis/amr/api.ts @@ -6,6 +6,8 @@ const enum API { 抢占控制权 = '/amr/controlAmr', 设置接单状态 = '/amr/acceptTask', 手动发布充电任务 = '/amr/manuallSetCharging', + 暂停AMR = '/amr', + 恢复AMR = '/amr', } // 只保留右键菜单需要的API函数 @@ -85,3 +87,43 @@ export async function setAvailable(id: string): Promise { export async function setUnavailable(id: string): Promise { return await setAcceptTask(id, false); } + +/** + * 暂停AMR运行 + * @param amrId AMR的ID(实际传递的是AMR名称) + * @param sceneId 场景ID + * @returns 是否成功 + */ +export async function pauseAmr(amrId: string, sceneId: string): Promise { + type B = { sceneId: string }; + type D = { success: boolean; message: string; code: number; result: any; timestamp: number }; + + try { + const response = await http.put(`${API.暂停AMR}/${amrId}/pause`, { sceneId }); + // response 是 D 类型,包含 success 属性 + return (response as any).success; + } catch (error) { + console.error('暂停AMR失败:', error); + return false; + } +} + +/** + * 恢复AMR运行 + * @param amrId AMR的ID(实际传递的是AMR名称) + * @param sceneId 场景ID + * @returns 是否成功 + */ +export async function resumeAmr(amrId: string, sceneId: string): Promise { + type B = { sceneId: string }; + type D = { success: boolean; message: string; code: number; result: any; timestamp: number }; + + try { + const response = await http.put(`${API.恢复AMR}/${amrId}/resume`, { sceneId }); + // response 是 D 类型,包含 success 属性 + return (response as any).success; + } catch (error) { + console.error('恢复AMR失败:', error); + return false; + } +} diff --git a/src/components/context-menu/context-menu.vue b/src/components/context-menu/context-menu.vue index 4f9710b..cfcbd22 100644 --- a/src/components/context-menu/context-menu.vue +++ b/src/components/context-menu/context-menu.vue @@ -172,9 +172,11 @@ const handleActionComplete = (data: { success: boolean; action: string; message? if (data.success) { message.success(`${actionName}操作成功`); } else { - // 使用从子组件传递过来的更具体的错误信息 - const errorMessage = data.message || `${actionName}操作失败`; - message.error(errorMessage); + // 只有当 message 有值时才显示错误信息,避免与全局 HTTP 错误提示重复 + if (data.message) { + message.error(data.message); + } + // 如果 message 为空,则不显示任何错误提示(让 HTTP 层的错误提示显示) } emit('actionComplete', data); diff --git a/src/components/context-menu/robot-menu.vue b/src/components/context-menu/robot-menu.vue index 5812dcc..f908731 100644 --- a/src/components/context-menu/robot-menu.vue +++ b/src/components/context-menu/robot-menu.vue @@ -159,7 +159,7 @@ interface Props { } interface Emits { - (e: 'actionComplete', data: { action: RobotAction; robot: RobotInfo; success: boolean }): void; + (e: 'actionComplete', data: { action: RobotAction; robot: RobotInfo; success: boolean; message?: string }): void; (e: 'customImage', data: { robotInfo: RobotInfo }): void; } @@ -257,11 +257,12 @@ const handleRobotAction = async (action: RobotAction | 'custom_image', actionNam console.log('机器人操作结果:', result); - // 发送操作完成事件 + // 发送操作完成事件,传递完整的执行结果 emit('actionComplete', { action, robot: robotInfo.value, success: result.success, + message: result.message, // 传递 message,让上层决定是否显示错误 }); } catch (error) { console.error(`机器人${actionName}操作失败:`, error); @@ -270,6 +271,7 @@ const handleRobotAction = async (action: RobotAction | 'custom_image', actionNam action, robot: robotInfo.value, success: false, + message: undefined, // 异常情况下不传递 message,让全局异常处理显示错误 }); } }; diff --git a/src/services/context-menu/robot-menu.service.ts b/src/services/context-menu/robot-menu.service.ts index 4ad4d6c..5062fcb 100644 --- a/src/services/context-menu/robot-menu.service.ts +++ b/src/services/context-menu/robot-menu.service.ts @@ -103,15 +103,39 @@ async function executeAmrAction( result = await AmrApi.setUnavailable(robotInfo.id); break; - case 'pause': - // 暂停 - 有对应API - result = await AmrApi.controlAmr(robotInfo.id, 'pause'); + case 'pause': { + // 暂停 - 使用新的API + const sceneId = editorStore.getEditorValue()?.getSceneId(); + if (!sceneId) { + console.error('暂停AMR失败: 场景ID未提供'); + return { + success: false, + message: '暂停AMR失败: 场景ID未提供', + }; + } + // 注意:根据接口文档,这里传递的是AMR的名称而不是ID + // RobotInfo 接口中使用 label 属性表示机器人名称 + const amrName = robotInfo.label || robotInfo.id; + result = await AmrApi.pauseAmr(amrName, sceneId); break; - - case 'resume': - // 继续 - 有对应API - result = await AmrApi.controlAmr(robotInfo.id, 'resume'); + } + + case 'resume': { + // 继续 - 使用新的API + const sceneId = editorStore.getEditorValue()?.getSceneId(); + if (!sceneId) { + console.error('恢复AMR失败: 场景ID未提供'); + return { + success: false, + message: '恢复AMR失败: 场景ID未提供', + }; + } + // 注意:根据接口文档,这里传递的是AMR的名称而不是ID + // RobotInfo 接口中使用 label 属性表示机器人名称 + const amrName = robotInfo.label || robotInfo.id; + result = await AmrApi.resumeAmr(amrName, sceneId); break; + } case 'start': // 启动 - 有对应API @@ -195,9 +219,10 @@ async function executeAmrAction( }; } + // API 调用失败时不需要额外的错误提示,因为 http 层已经处理了错误提示 return { success: result, - message: result ? `${action}操作成功` : '', + message: result ? `${action}操作成功` : '', // 失败时返回空字符串,让全局错误处理显示具体错误 }; } catch (error) {