ai-coding
2026-06-23 3f1b14c9c293b1e51e8fa781c047514f4b71f134
feat:指挥中心联调

Co-Authored-By: '刘文鑫' <p-liuwx1@xiaopeng.com>
1个文件已添加
19个文件已修改
742 ■■■■■ 已修改文件
.env 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
command-center-api.md 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/composables/useCommandCenterSocket.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/commandCenter.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/commandCenter.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/typings/env.d.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/socket.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/command-center.constants.ts 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/command-center.types.ts 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/components/MapCanvas.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/components/StatusHeader/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/components/VehicleDetailDrawer/RouteSection.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/components/VehicleDetailDrawer/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/index.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/map/FenceLayer.ts 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/map/PassengerCallLayer.ts 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/map/RouteLayer.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/map/StationLayer.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/map/VehicleLayer.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/map/vehicle-marker.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env
@@ -34,3 +34,6 @@
VITE_SSO_HOST =
VITE_SSO_APP_ID = RoboTaxi-admin-predeploy
VITE_SSO_REDIRECT_URL =
# 指挥中心 Socket.IO 地址(为空时使用当前域名)
VITE_COMMAND_CENTER_SOCKET_URL =
command-center-api.md
New file
@@ -0,0 +1,315 @@
# 指挥中心接口文档(commandCenter)
> 基于 `feat-dashboard-lianjd` 分支对比 `develop` 的新增接口整理,模块 `xp-robotaxi-admin-boot`。
> 给前端的接口分两部分:**REST**(4 个 GET 接口) + **Socket.IO**(实时推送)。
---
## 一、基础信息
| 项             | 值                                                                                                                                              |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| 后端模块       | `xp-robotaxi-admin-boot`                                                                                                                        |
| 网关前缀       | 管理后台域名 + `/api/v1/commandCenter`(与 cockpit 同套 SSO 鉴权)                                                                              |
| 鉴权方式       | 管理后台 SSO Token(`userType=system`),由网关 `AuthFilter` 校验后注入 Header;REST 通过 `@RequiresPermissions("commandCenter:view")` 二次校验 |
| 统一响应包装   | `R<T> = { code: number, msg: string, data: T }`,`code=200` 表示成功                                                                            |
---
## 二、REST 接口
### 2.1 查询单车详情
- **路径**:`GET /api/v1/commandCenter/vehicle/detail`
- **权限**:`commandCenter:view`
- **入参**(Query):
| 字段        | 类型   | 必填 | 说明    |
| ----------- | ------ | ---- | ------- |
| `vehicleId` | `Long` | 是   | 车辆 ID |
- **返回**:`R<CommandCenterVehicleDetailVo>` — 见 [4.1](#41-commandcentervehicledetailvo)
- **示例**:
```http
GET /api/v1/commandCenter/vehicle/detail?vehicleId=10086
```
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "vehicleId": 10086,
    "vin": "LXP****",
    "plateNumber": "粤B-XXXXX",
    "monitorGroup": "HAS_ORDER",
    "monitorStatus": "接驾中",
    "stateCode": "PICK_UP",
    "currentElectricity": 76,
    "todayCompletedOrders": 12,
    "routeInfo": {
      "startLon": 113.93,
      "startLat": 22.53,
      "endLon": 113.95,
      "endLat": 22.55,
      "startAddress": "XX 大厦",
      "endAddress": "XX 公园",
      "eta": 480
    },
    "currentLon": 113.94,
    "currentLat": 22.54
  }
}
```
---
### 2.2 查询城市电子围栏列表
- **路径**:`GET /api/v1/commandCenter/fences`
- **权限**:`commandCenter:view`
- **入参**(Query):
| 字段       | 类型     | 必填 | 说明                                         |
| ---------- | -------- | ---- | -------------------------------------------- |
| `cityCode` | `String` | 是   | 城市编码(与 `xp_open_city.city_code` 对应) |
- **返回**:`R<List<CommandCenterFenceVo>>` — 见 [4.2](#42-commandcenterfencevo)
```http
GET /api/v1/commandCenter/fences?cityCode=440300
```
---
### 2.3 查询城市场站列表
- **路径**:`GET /api/v1/commandCenter/stations`
- **权限**:`commandCenter:view`
- **入参**(Query):
| 字段       | 类型     | 必填 | 说明     |
| ---------- | -------- | ---- | -------- |
| `cityCode` | `String` | 是   | 城市编码 |
- **返回**:`R<List<XpStation>>` — 见 [4.3](#43-xpstation场站精简)
---
### 2.4 查询已开通城市列表
- **路径**:`GET /api/v1/commandCenter/regions`
- **权限**:`commandCenter:view`
- **入参**:无
- **返回**:`R<List<XpOpenCity>>` — 见 [4.4](#44-xpopencity开通城市)
```http
GET /api/v1/commandCenter/regions
```
---
## 三、Socket.IO 接口
### 3.1 连接信息
| 项           | 值                                              |
| ------------ | ----------------------------------------------- |
| URL          | `wss://<管理后台域名>/ws`(或 `ws://` for dev) |
| Namespace    | `/commandCenter`                                |
| 协议         | Socket.IO v4(netty-socketio 2.0.11 服务端)    |
| 默认命名空间 | **显式拒连**,必须使用 `/commandCenter`         |
| 心跳         | 服务端默认 `pingInterval=25s, pingTimeout=60s`  |
前端示例(socket.io-client v4):
```js
import { io } from "socket.io-client";
const socket = io("/commandCenter", {
  path: "/ws",
  transports: ["websocket"],
});
socket.on("connect", () => {
  socket.emit("auth", { token: getSsoToken() });
});
```
---
### 3.2 客户端 → 服务端事件
#### `auth` — 鉴权(连接后**必须**第一时间发送)
入参:`CommandCenterAuthDto`
| 字段    | 类型     | 必填 | 说明                             |
| ------- | -------- | ---- | -------------------------------- |
| `token` | `String` | 是   | 管理后台 SSO Token(同 REST 用) |
服务端会用 token 解析出运维登录用户,要求权限里包含 `commandCenter:view`,否则下发 `auth_failed` 并断开。
---
#### `subscribe` — 订阅城市/围栏
入参:`CommandCenterSubscribeDto`
| 字段           | 类型     | 必填 | 说明                                         |
| -------------- | -------- | ---- | -------------------------------------------- |
| `cityCode`     | `String` | 是   | 城市编码,加入房间 `city_{cityCode}`         |
| `electronicId` | `Long`   | 否   | 围栏 ID,传则同时加入 `fence_{electronicId}` |
**前置条件**:必须先收到 `auth_success` 后再调用,否则服务端回 `auth_failed`。
---
#### `unsubscribe` — 取消订阅
入参同 `subscribe`。**注意**:切换城市时前端必须先 `unsubscribe` 旧城市,再 `subscribe` 新城市,否则会收到多城市数据。
---
### 3.3 服务端 → 客户端事件
| 事件名            | 触发时机                                   | 推送频率                        | 负载类型                                                |
| ----------------- | ------------------------------------------ | ------------------------------- | ------------------------------------------------------- |
| `auth_success`    | `auth` 通过后立即推送一次                  | 一次                            | `{ userId: string }`                                    |
| `auth_failed`     | token 为空 / 鉴权失败 / 未鉴权就 subscribe | 一次(随后断开)                | `{ message: string }`                                   |
| `vehicles`        | 已订阅城市的车辆位置/状态聚合              | **每 2 秒**                     | `VehicleMonitorVo[]` — 见 [4.5](#45-vehiclemonitorvo)   |
| `kpi`             | 城市维度 KPI 指标                          | **每 5 秒**(复用 2s 聚合结果) | `CommandCenterKpiVo` — 见 [4.6](#46-commandcenterkpivo) |
| `passenger_calls` | 待接单乘客呼单列表                         | **每 5 秒**                     | `PassengerCallVo[]` — 见 [4.7](#47-passengercallvo)     |
推送范围:以 Socket.IO Room 维度广播,仅推到加入了 `city_{cityCode}` 房间的客户端;前端可同时订阅多个城市但要自行去重。
---
## 四、数据结构
> 字段类型一栏使用 Java 类型,TS 对应:`Long`→`number`(如担心大数溢出可改为 `string`,由前端 JSON 解析时统一处理)、`Double/Float/BigDecimal`→`number`、`LocalDateTime`→ ISO 字符串 `yyyy-MM-dd HH:mm:ss`(+8)。
### 4.1 `CommandCenterVehicleDetailVo`
| 字段                   | 类型                       | 说明                                                                     |
| ---------------------- | -------------------------- | ------------------------------------------------------------------------ |
| `vehicleId`            | `Long`                     | 车辆 ID                                                                  |
| `vin`                  | `String`                   | 车架号                                                                   |
| `plateNumber`          | `String`                   | 车牌号                                                                   |
| `monitorGroup`         | `String`                   | 监控分组,见 [MonitorGroupEnum](#48-monitorgroupenum)                    |
| `monitorStatus`        | `String`                   | 监控状态文案(如"接驾中")                                               |
| `stateCode`            | `String`                   | 状态枚举 code,见 [VehicleMonitorStateEnum](#49-vehiclemonitorstateenum) |
| `currentElectricity`   | `Integer`                  | 当前电量 0-100                                                           |
| `todayCompletedOrders` | `Integer`                  | 今日已完成订单数                                                         |
| `routeInfo`            | `CommandCenterRouteInfoVo` | 当前订单路线信息,无进行中订单时为 `null`                                |
| `currentLon`           | `Double`                   | 当前经度                                                                 |
| `currentLat`           | `Double`                   | 当前纬度                                                                 |
### 4.2 `CommandCenterFenceVo`
| 字段             | 类型                          | 说明                                                |
| ---------------- | ----------------------------- | --------------------------------------------------- |
| `electronicId`   | `Long`                        | 围栏 ID                                             |
| `electronicName` | `String`                      | 围栏名称                                            |
| `cityCode`       | `String`                      | 城市编码                                            |
| `status`         | `Integer`                     | 围栏状态                                            |
| `areas`          | `List<XpElectronicFenceArea>` | 围栏区域,含 `fencePoints`("经度*纬度,经度*纬度") |
`XpElectronicFenceArea` 关键字段:`id` `electronicId` `areaName` `areaType`(1=运营区 2=禁行区)`areaStatus`(1=启用 2=禁用)`fencePoints`。
### 4.3 `XpStation`(场站,精简)
前端用得到的字段:`id` `stationName` `stationNumber` `status`(0=草稿 1=待启用 2=启用中 3=已停用 4=失效)`cityCode` `cityName` `electronicId` `centerLon` `centerLat` `fencePoints` `stationArea` `sortNo`。完整字段见 [XpStation.java](file:///d:/projects/xp-robotaxi/xp-robotaxi-api/src/main/java/com/xiaopeng/api/model/operation/XpStation.java)。
### 4.4 `XpOpenCity`(开通城市)
| 字段                            | 类型      | 说明                            |
| ------------------------------- | --------- | ------------------------------- |
| `id`                            | `Long`    | 主键                            |
| `provinceName` / `provinceCode` | `String`  | 省                              |
| `cityName` / `cityCode`         | `String`  | 市(前端订阅时使用 `cityCode`) |
| `districtName` / `districtCode` | `String`  | 区县                            |
| `status`                        | `Integer` | 1=启用 0=禁用(接口只返回启用) |
### 4.5 `VehicleMonitorVo`
| 字段                 | 类型      | 说明                           |
| -------------------- | --------- | ------------------------------ |
| `vehicleId`          | `Long`    | 车辆 ID                        |
| `vin`                | `String`  | 车架号                         |
| `plateNumber`        | `String`  | 车牌号                         |
| `monitorGroup`       | `String`  | 监控分组 code,见 4.8          |
| `monitorStatus`      | `String`  | 监控状态文案                   |
| `stateCode`          | `String`  | 状态 code,见 4.9              |
| `lon`                | `Double`  | 经度                           |
| `lat`                | `Double`  | 纬度                           |
| `course`             | `Double`  | 航向角                         |
| `currentElectricity` | `Integer` | 当前电量                       |
| `speed`              | `Float`   | 当前速度 km/h                  |
| `orderId`            | `Long`    | 进行中订单 ID(无单为 `null`) |
| `charging`           | `Boolean` | 是否充电中                     |
| `positionUpdateTime` | `Long`    | 位置上报时间戳(毫秒)         |
### 4.6 `CommandCenterKpiVo`
| 字段                   | 类型         | 说明                     |
| ---------------------- | ------------ | ------------------------ |
| `cruisingCount`        | `Integer`    | 巡游中车辆数             |
| `hasOrderCount`        | `Integer`    | 有单车辆数(接驾+送驾)  |
| `pickUpCount`          | `Integer`    | 接驾中                   |
| `dropOffCount`         | `Integer`    | 送驾中                   |
| `occupiedCount`        | `Integer`    | 占用中(充电+整备+回场) |
| `chargingCount`        | `Integer`    | 充电中                   |
| `preparingCount`       | `Integer`    | 整备中                   |
| `returningCount`       | `Integer`    | 回场中                   |
| `abnormalCount`        | `Integer`    | 异常(含 ODD 越界)      |
| `todayCompletedOrders` | `Integer`    | 今日完单数               |
| `avgOrdersPerVehicle`  | `BigDecimal` | 单车平均完单数           |
### 4.7 `PassengerCallVo`
| 字段                    | 类型     | 说明                                  |
| ----------------------- | -------- | ------------------------------------- |
| `orderId`               | `Long`   | 订单 ID                               |
| `orderNumber`           | `String` | 订单号                                |
| `startLon` / `startLat` | `Double` | 起点                                  |
| `startAddress`          | `String` | 起点地址                              |
| `endLon` / `endLat`     | `Double` | 终点                                  |
| `endAddress`            | `String` | 终点地址                              |
| `createTime`            | `String` | 下单时间,`yyyy-MM-dd HH:mm:ss`(+8) |
| `waitingSeconds`        | `Long`   | 已等待秒数(用于呼单超时高亮)        |
### 4.8 `MonitorGroupEnum`
| code        | desc   |
| ----------- | ------ |
| `CRUISING`  | 巡游中 |
| `HAS_ORDER` | 有单   |
| `OCCUPIED`  | 占用中 |
| `ABNORMAL`  | 异常   |
### 4.9 `VehicleMonitorStateEnum`
| code        | desc     | 所属分组    |
| ----------- | -------- | ----------- |
| `CRUISING`  | 巡游中   | `CRUISING`  |
| `PICK_UP`   | 接驾中   | `HAS_ORDER` |
| `DROP_OFF`  | 送驾中   | `HAS_ORDER` |
| `CHARGING`  | 充电中   | `OCCUPIED`  |
| `PREPARING` | 整备中   | `OCCUPIED`  |
| `RETURNING` | 回场中   | `OCCUPIED`  |
| `ABNORMAL`  | 车辆异常 | `ABNORMAL`  |
| `ODD_OUT`   | ODD 越界 | `ABNORMAL`  |
---
## 五、前端接入要点(备忘)
1. **登录态**:复用现有管理后台 SSO,无需单独登录。
2. **路由前缀**:所有 REST 接口都在 `/api/v1/commandCenter` 下,与 cockpit 的 `/cockpit/*` 不冲突。
3. **Socket.IO 时序**:`connect` → `emit('auth', { token })` → `on('auth_success')` → `emit('subscribe', { cityCode })` → `on('vehicles' | 'kpi' | 'passenger_calls')`。
4. **切换城市**:先 `unsubscribe` 再 `subscribe`,避免多城市数据叠加。
5. **断线重连**:socket.io-client 自带重连;重连后需重新 `auth` 与 `subscribe`(服务端不保存订阅状态)。
6. **大屏刷新频率**:车辆 2s、KPI/呼单 5s;前端不要再做客户端拉取,纯被动接收推送。
src/composables/useCommandCenterSocket.ts
@@ -1,6 +1,7 @@
import { local } from '@/utils'
import { useCommandCenterStore } from '@/store/commandCenter'
import { createCommandCenterSocket, destroyCommandCenterSocket } from '@/utils/socket'
import { ONLINE_TIMEOUT_MS, SOCKET_EMIT_EVENT, SOCKET_ON_EVENT } from '@/views/businessOperations/commandCenter/command-center.constants'
import type { CommandCenterKpiVo, PassengerCallVo, VehicleMonitorVo } from '@/views/businessOperations/commandCenter/command-center.types'
/**
@@ -13,7 +14,7 @@
  /** 判断是否在线(已连接且 5 秒内有推送) */
  const isOnline = computed(() => {
    return store.socketStatus.connected && (Date.now() - store.socketStatus.lastPushAt < 5000)
    return store.socketStatus.connected && (Date.now() - store.socketStatus.lastPushAt < ONLINE_TIMEOUT_MS)
  })
  /** 建立 WebSocket 连接并监听事件 */
@@ -23,33 +24,33 @@
    socket.on('connect', () => {
      store.socketStatus.connected = true
      const token = local.get('accessToken')
      socket.emit('auth', { token })
      socket.emit(SOCKET_EMIT_EVENT.AUTH, { token })
    })
    socket.on('disconnect', () => {
      store.socketStatus.connected = false
    })
    socket.on('auth_success', () => {
      socket.emit('subscribe', { cityCode: store.cityCode })
    socket.on(SOCKET_ON_EVENT.AUTH_SUCCESS, () => {
      socket.emit(SOCKET_EMIT_EVENT.SUBSCRIBE, { cityCode: store.cityCode })
    })
    socket.on('auth_failed', (payload: { message: string }) => {
    socket.on(SOCKET_ON_EVENT.AUTH_FAILED, (payload: { message: string }) => {
      window.$message?.error(`鉴权失败:${payload.message}`)
      socket.disconnect()
    })
    socket.on('vehicles', (list: VehicleMonitorVo[]) => {
    socket.on(SOCKET_ON_EVENT.VEHICLES, (list: VehicleMonitorVo[]) => {
      store.vehicles = list
      store.socketStatus.lastPushAt = Date.now()
    })
    socket.on('kpi', (data: CommandCenterKpiVo) => {
    socket.on(SOCKET_ON_EVENT.KPI, (data: CommandCenterKpiVo) => {
      store.kpi = data
      store.socketStatus.lastPushAt = Date.now()
    })
    socket.on('passenger_calls', (list: PassengerCallVo[]) => {
    socket.on(SOCKET_ON_EVENT.PASSENGER_CALLS, (list: PassengerCallVo[]) => {
      store.passengerCalls = list
      store.socketStatus.lastPushAt = Date.now()
    })
@@ -67,7 +68,7 @@
   * @param cityCode - 城市编码
   */
  const subscribeCity = (cityCode: string) => {
    socket.emit('subscribe', { cityCode })
    socket.emit(SOCKET_EMIT_EVENT.SUBSCRIBE, { cityCode })
  }
  /**
@@ -75,7 +76,7 @@
   * @param cityCode - 城市编码
   */
  const unsubscribeCity = (cityCode: string) => {
    socket.emit('unsubscribe', { cityCode })
    socket.emit(SOCKET_EMIT_EVENT.UNSUBSCRIBE, { cityCode })
  }
  return {
src/service/api/commandCenter.ts
@@ -1,40 +1,34 @@
import { request } from '@/service/http'
import type { FenceVo, Region, StationVo, VehicleDetailVo } from '@/views/businessOperations/commandCenter/command-center.types'
/** 指挥中心 API 前缀 */
const PREFIX = '/api/v1/commandCenter'
import type { FenceVo, StationVo, VehicleDetailVo, XpOpenCity } from '@/views/businessOperations/commandCenter/command-center.types'
import { API_PREFIX } from '@/views/businessOperations/commandCenter/command-center.constants'
/**
 * 获取指挥中心区域列表
 * @returns 区域列表
 * 获取已开通城市列表
 */
export const fetchCommandCenterRegions = () => {
  return request.Get<Service.ResponseResult<Region[]>>(`${PREFIX}/regions`)
  return request.Get<Service.ResponseResult<XpOpenCity[]>>(`${API_PREFIX}/regions`)
}
/**
 * 获取指定城市的围栏列表
 * @param cityCode - 城市编码
 * @returns 围栏列表
 */
export const fetchCommandCenterFences = (cityCode: string) => {
  return request.Get<Service.ResponseResult<FenceVo[]>>(`${PREFIX}/fences`, { params: { cityCode } })
  return request.Get<Service.ResponseResult<FenceVo[]>>(`${API_PREFIX}/fences`, { params: { cityCode } })
}
/**
 * 获取指定城市的站点列表
 * @param cityCode - 城市编码
 * @returns 站点列表
 */
export const fetchCommandCenterStations = (cityCode: string) => {
  return request.Get<Service.ResponseResult<StationVo[]>>(`${PREFIX}/stations`, { params: { cityCode } })
  return request.Get<Service.ResponseResult<StationVo[]>>(`${API_PREFIX}/stations`, { params: { cityCode } })
}
/**
 * 获取车辆详情
 * @param vehicleId - 车辆 ID
 * @returns 车辆详情
 */
export const fetchVehicleDetail = (vehicleId: string) => {
  return request.Get<Service.ResponseResult<VehicleDetailVo>>(`${PREFIX}/vehicle/detail`, { params: { vehicleId } })
export const fetchVehicleDetail = (vehicleId: number) => {
  return request.Get<Service.ResponseResult<VehicleDetailVo>>(`${API_PREFIX}/vehicle/detail`, { params: { vehicleId } })
}
src/store/commandCenter.ts
@@ -1,14 +1,14 @@
import { DEFAULT_CITY_CODE } from '@/views/businessOperations/commandCenter/command-center.constants'
import { DEFAULT_CITY_CODE, STATIONS_CACHE_TTL } from '@/views/businessOperations/commandCenter/command-center.constants'
import type {
  CommandCenterKpiVo,
  FenceVo,
  LayerVisibility,
  PassengerCallVo,
  Region,
  SocketStatus,
  StationVo,
  VehicleDetailVo,
  VehicleMonitorVo,
  XpOpenCity,
} from '@/views/businessOperations/commandCenter/command-center.types'
import { fetchCommandCenterFences, fetchCommandCenterRegions, fetchCommandCenterStations, fetchVehicleDetail } from '@/service/api/commandCenter'
@@ -21,21 +21,18 @@
/** 指挥中心 Store 状态 */
interface CommandCenterState {
  cityCode: string
  regions: Region[]
  regions: XpOpenCity[]
  vehicles: VehicleMonitorVo[]
  kpi: CommandCenterKpiVo | null
  passengerCalls: PassengerCallVo[]
  fences: FenceVo[]
  stations: StationVo[]
  stationsCache: Record<string, StationsCacheEntry>
  selectedVehicleId: string | null
  selectedVehicleId: number | null
  vehicleDetail: VehicleDetailVo | null
  layerVisibility: LayerVisibility
  socketStatus: SocketStatus
}
/** 站点缓存有效期:10 分钟 */
const STATIONS_CACHE_TTL = 10 * 60 * 1000
/**
 * 指挥中心全局状态管理
@@ -106,7 +103,7 @@
     * 加载车辆详情
     * @param vehicleId - 车辆 ID
     */
    async fetchVehicleDetail(vehicleId: string) {
    async fetchVehicleDetail(vehicleId: number) {
      const { data } = await fetchVehicleDetail(vehicleId)
      if (data)
        this.vehicleDetail = data
@@ -125,7 +122,7 @@
     * 打开车辆详情面板
     * @param vehicleId - 车辆 ID
     */
    openVehicleDetail(vehicleId: string) {
    openVehicleDetail(vehicleId: number) {
      this.selectedVehicleId = vehicleId
    },
src/typings/env.d.ts
@@ -40,6 +40,8 @@
  readonly VITE_SSO_REDIRECT_URL: string
  /** 后端服务的环境类型 */
  readonly MODE: ServiceEnvType
  /** 指挥中心 Socket.IO 地址 */
  readonly VITE_COMMAND_CENTER_SOCKET_URL?: string
}
interface ImportMeta {
src/utils/socket.ts
@@ -1,5 +1,6 @@
import { io } from 'socket.io-client'
import type { Socket } from 'socket.io-client'
import { SOCKET_CONFIG } from '@/views/businessOperations/commandCenter/command-center.constants'
/** 单例 Socket 实例 */
let instance: Socket | null = null
@@ -14,11 +15,12 @@
  const url = import.meta.env.VITE_COMMAND_CENTER_SOCKET_URL || ''
  instance = io(`${url}/commandCenter`, {
    transports: ['websocket', 'polling'],
  instance = io(`${url}${SOCKET_CONFIG.NAMESPACE}`, {
    path: SOCKET_CONFIG.PATH,
    transports: [...SOCKET_CONFIG.TRANSPORTS],
    reconnection: true,
    reconnectionDelay: 2000,
    reconnectionDelayMax: 8000,
    reconnectionDelay: SOCKET_CONFIG.RECONNECTION_DELAY,
    reconnectionDelayMax: SOCKET_CONFIG.RECONNECTION_DELAY_MAX,
    autoConnect: false,
  })
src/views/businessOperations/commandCenter/command-center.constants.ts
@@ -12,12 +12,87 @@
  ABNORMAL: { color: '#FF4D4F', label: '异常' },
}
export const CHARGING_COLOR = '#FAAD14'
export const DEFAULT_CITY_CODE = '440100'
/** 指挥中心 API 路径前缀 */
export const API_PREFIX = '/api/v1/commandCenter'
/** Socket.IO 配置 */
export const SOCKET_CONFIG = {
  NAMESPACE: '/commandCenter',
  PATH: '/ws',
  TRANSPORTS: ['websocket'] as const,
  RECONNECTION_DELAY: 2000,
  RECONNECTION_DELAY_MAX: 8000,
} as const
/** Socket.IO 客户端 → 服务端事件名 */
export const SOCKET_EMIT_EVENT = {
  AUTH: 'auth',
  SUBSCRIBE: 'subscribe',
  UNSUBSCRIBE: 'unsubscribe',
} as const
/** Socket.IO 服务端 → 客户端事件名 */
export const SOCKET_ON_EVENT = {
  AUTH_SUCCESS: 'auth_success',
  AUTH_FAILED: 'auth_failed',
  VEHICLES: 'vehicles',
  KPI: 'kpi',
  PASSENGER_CALLS: 'passenger_calls',
} as const
/** 在线判断阈值(毫秒),超过此时间无推送视为离线 */
export const ONLINE_TIMEOUT_MS = 5000
/** 车辆监控状态枚举 code */
export const VEHICLE_STATE = {
  CRUISING: 'CRUISING',
  PICK_UP: 'PICK_UP',
  DROP_OFF: 'DROP_OFF',
  CHARGING: 'CHARGING',
  PREPARING: 'PREPARING',
  RETURNING: 'RETURNING',
  ABNORMAL: 'ABNORMAL',
  ODD_OUT: 'ODD_OUT',
} as const
/** 站点缓存有效期(毫秒) */
export const STATIONS_CACHE_TTL = 10 * 60 * 1000
/** 地图样式 */
export const MAP_STYLE = 'amap://styles/dark'
/** 图层 z-index */
export const LAYER_Z_INDEX = {
  FENCE: 10,
  STATION: 20,
  PASSENGER_CALL: 40,
  VEHICLE: 50,
  ROUTE: 60,
} as const
/** 围栏颜色配置 */
export const FENCE_STYLE = {
  RESTRICTED: { stroke: '#FF4D4F', fill: '#FF4D4F', strokeStyle: 'solid' as const },
  OPERATIONAL: { stroke: '#1890FF', fill: '#1890FF', strokeStyle: 'dashed' as const },
  STROKE_WEIGHT: 2,
  STROKE_OPACITY: 0.8,
  FILL_OPACITY: 0.1,
} as const
/** 路线颜色 */
export const ROUTE_COLOR = {
  PICKUP: '#1890FF',
  DROPOFF: '#52C41A',
} as const
export const LEGEND_ITEMS = [
  { color: MONITOR_COLOR_MAP.CRUISING.color, label: '巡游中' },
  { color: MONITOR_COLOR_MAP.HAS_ORDER.color, label: '有单' },
  { color: MONITOR_COLOR_MAP.OCCUPIED.color, label: '占用中' },
  { color: MONITOR_COLOR_MAP.ABNORMAL.color, label: '异常' },
  { color: '#FAAD14', label: '⚡ 充电中', icon: 'charging' },
  { color: CHARGING_COLOR, label: '⚡ 充电中', icon: 'charging' },
]
src/views/businessOperations/commandCenter/command-center.types.ts
@@ -1,95 +1,124 @@
export type MonitorGroup = 'CRUISING' | 'HAS_ORDER' | 'OCCUPIED' | 'ABNORMAL'
export type MonitorStatus =
  | '巡游中'
  | '接驾中'
  | '送驾中'
  | '充电中'
  | '整备中'
  | '回场中'
  | 'ODD越界'
  | '离线'
export type VehicleMonitorStateCode =
  | 'CRUISING'
  | 'PICK_UP'
  | 'DROP_OFF'
  | 'CHARGING'
  | 'PREPARING'
  | 'RETURNING'
  | 'ABNORMAL'
  | 'ODD_OUT'
export interface VehicleMonitorVo {
  vehicleId: string
  vehicleId: number
  vin: string
  plateNumber: string
  monitorGroup: MonitorGroup
  monitorStatus: string
  stateCode: VehicleMonitorStateCode
  lon: number
  lat: number
  monitorGroup: MonitorGroup
  monitorStatus: MonitorStatus
  charging: boolean
  course: number
  currentElectricity: number
  todayCompletedOrders: number
  lastHeartbeat: string
  speed: number
  orderId: number | null
  charging: boolean
  positionUpdateTime: number
}
export interface CommandCenterKpiVo {
  cruisingCount: number
  hasOrderCount: number
  hasOrderPickupCount: number
  hasOrderDropoffCount: number
  pickUpCount: number
  dropOffCount: number
  occupiedCount: number
  occupiedChargingCount: number
  occupiedMaintenanceCount: number
  occupiedReturningCount: number
  chargingCount: number
  preparingCount: number
  returningCount: number
  abnormalCount: number
  todayCompletedOrders: number
  averageOrdersPerVehicle: number
  avgOrdersPerVehicle: number
}
export interface PassengerCallVo {
  callId: string
  lon: number
  lat: number
  orderId: number
  orderNumber: string
  startLon: number
  startLat: number
  startAddress: string
  endLon: number
  endLat: number
  endAddress: string
  createTime: string
  waitingSeconds: number
  address: string
  status: 'waiting' | 'dispatched' | 'cancelled'
}
export interface XpElectronicFenceArea {
  id: number
  electronicId: number
  areaName: string
  areaType: number // 1=运营区 2=禁行区
  areaStatus: number // 1=启用 2=禁用
  fencePoints: string // "经度*纬度,经度*纬度"
}
export interface FenceVo {
  fenceId: string
  name: string
  type: 'ODD' | 'RESTRICTED'
  points: Array<{ lon: number, lat: number }>
  electronicId: number
  electronicName: string
  cityCode: string
  status: number
  areas: XpElectronicFenceArea[]
}
export interface StationVo {
  stationId: string
  name: string
  id: number
  stationName: string
  stationNumber: string
  status: number
  cityCode: string
  cityName: string
  electronicId: number
  centerLon: number
  centerLat: number
  type: string
  fencePoints: string
  stationArea: number
  sortNo: number
}
export interface Region {
  regionCode: string
  regionName: string
  children?: Region[]
export interface XpOpenCity {
  id: number
  provinceName: string
  provinceCode: string
  cityName: string
  cityCode: string
  districtName: string
  districtCode: string
  status: number
}
export interface RouteInfo {
  startAddress: string
export interface CommandCenterRouteInfoVo {
  startLon: number
  startLat: number
  endAddress: string
  endLon: number
  endLat: number
  eta: string
  startAddress: string
  endAddress: string
  eta: number // seconds
}
export interface VehicleDetailVo {
  vehicleId: string
  vehicleId: number
  vin: string
  plateNumber: string
  monitorGroup: MonitorGroup
  monitorStatus: MonitorStatus
  charging: boolean
  monitorStatus: string
  stateCode: VehicleMonitorStateCode
  currentElectricity: number
  todayCompletedOrders: number
  lon: number
  lat: number
  routeInfo: RouteInfo | null
  routeInfo: CommandCenterRouteInfoVo | null
  currentLon: number
  currentLat: number
}
export interface LayerVisibility {
src/views/businessOperations/commandCenter/components/MapCanvas.vue
@@ -8,6 +8,7 @@
<script setup lang="ts">
import { useAMapLoader } from '@/composables/useAMapLoader'
import { cityCenterMap, INITIAL_ZOOM } from '@/views/businessOperations/operationWorkbench/workbench.constants'
import { DEFAULT_CITY_CODE, MAP_STYLE } from '@/views/businessOperations/commandCenter/command-center.constants'
import { useCommandCenterStore } from '@/store/commandCenter'
import MapErrorFallback from './MapErrorFallback.vue'
@@ -23,11 +24,11 @@
    loadError.value = false
    await load()
    const AMap = (window as any).AMap
    const cityConfig = cityCenterMap[store.cityCode] || cityCenterMap['440100']
    const cityConfig = cityCenterMap[store.cityCode] || cityCenterMap[DEFAULT_CITY_CODE]
    mapInstance.value = new AMap.Map(mapContainer.value, {
      zoom: cityConfig.zoom || INITIAL_ZOOM,
      center: cityConfig.center,
      mapStyle: 'amap://styles/dark',
      mapStyle: MAP_STYLE,
      resizeEnable: true,
    })
  }
src/views/businessOperations/commandCenter/components/StatusHeader/index.vue
@@ -15,19 +15,19 @@
        title="有单"
        :value="kpi?.hasOrderCount ?? 0"
        :color="MONITOR_COLOR_MAP.HAS_ORDER.color"
        :subtitle="`接驾 ${kpi?.hasOrderPickupCount ?? 0} · 送驾 ${kpi?.hasOrderDropoffCount ?? 0}`"
        :subtitle="`接驾 ${kpi?.pickUpCount ?? 0} · 送驾 ${kpi?.dropOffCount ?? 0}`"
      />
      <KpiCard
        title="占用中"
        :value="kpi?.occupiedCount ?? 0"
        :color="MONITOR_COLOR_MAP.OCCUPIED.color"
        :subtitle="`充电 ${kpi?.occupiedChargingCount ?? 0} · 整备 ${kpi?.occupiedMaintenanceCount ?? 0} · 回场 ${kpi?.occupiedReturningCount ?? 0}`"
        :subtitle="`充电 ${kpi?.chargingCount ?? 0} · 整备 ${kpi?.preparingCount ?? 0} · 回场 ${kpi?.returningCount ?? 0}`"
      />
      <KpiCard
        title="今日完单"
        :value="kpi?.todayCompletedOrders ?? 0"
        color="rgba(0,0,0,0.85)"
        :subtitle="`均单/车 ${kpi?.averageOrdersPerVehicle ?? 0}`"
        :subtitle="`均单/车 ${kpi?.avgOrdersPerVehicle ?? 0}`"
      />
    </div>
    <div class="status-header__right">
src/views/businessOperations/commandCenter/components/VehicleDetailDrawer/RouteSection.vue
@@ -2,16 +2,16 @@
  <div class="route-section">
    <div class="route-section__title">路线信息</div>
    <div class="route-section__content">
      <template v-if="detail.monitorStatus === '巡游中'">
      <template v-if="detail.stateCode === VEHICLE_STATE.CRUISING">
        <p>当前位置 → 巡游路线</p>
      </template>
      <template v-else-if="detail.monitorStatus === '接驾中' && detail.routeInfo">
      <template v-else-if="detail.stateCode === VEHICLE_STATE.PICK_UP && detail.routeInfo">
        <p>车辆位置 → {{ detail.routeInfo.startAddress }}</p>
        <p class="route-section__eta">预计到达:{{ detail.routeInfo.eta }}</p>
        <p class="route-section__eta">预计到达:{{ formatEta(detail.routeInfo.eta) }}</p>
      </template>
      <template v-else-if="detail.monitorStatus === '送驾中' && detail.routeInfo">
      <template v-else-if="detail.stateCode === VEHICLE_STATE.DROP_OFF && detail.routeInfo">
        <p>{{ detail.routeInfo.startAddress }} → 车辆位置 → {{ detail.routeInfo.endAddress }}</p>
        <p class="route-section__eta">预计到达:{{ detail.routeInfo.eta }}</p>
        <p class="route-section__eta">预计到达:{{ formatEta(detail.routeInfo.eta) }}</p>
      </template>
      <template v-else>
        <p>{{ detail.monitorStatus }}</p>
@@ -22,8 +22,14 @@
<script setup lang="ts">
import type { VehicleDetailVo } from '../../command-center.types'
import { VEHICLE_STATE } from '../../command-center.constants'
defineProps<{ detail: VehicleDetailVo }>()
function formatEta(seconds: number): string {
  const min = Math.ceil(seconds / 60)
  return min >= 60 ? `${Math.floor(min / 60)}小时${min % 60}分钟` : `${min}分钟`
}
</script>
<style scoped lang="scss">
src/views/businessOperations/commandCenter/components/VehicleDetailDrawer/index.vue
@@ -42,10 +42,7 @@
})
const currentTodayOrders = computed(() => {
  if (store.vehicleDetail)
    return store.vehicleDetail.todayCompletedOrders
  const v = store.vehicles.find(v => v.vehicleId === store.selectedVehicleId)
  return v?.todayCompletedOrders ?? 0
  return store.vehicleDetail?.todayCompletedOrders ?? 0
})
function handleClose(show: boolean) {
src/views/businessOperations/commandCenter/index.vue
@@ -27,6 +27,7 @@
import { useCommandCenterStore } from '@/store/commandCenter'
import { useVehicleDetail } from '@/composables/useVehicleDetail'
import { DEFAULT_CITY_OPTIONS } from '@/views/businessOperations/operationWorkbench/workbench.constants'
import { VEHICLE_STATE } from './command-center.constants'
import { FenceLayer } from './map/FenceLayer'
import { StationLayer } from './map/StationLayer'
import { VehicleLayer } from './map/VehicleLayer'
@@ -126,11 +127,11 @@
    return
  }
  routeLayer?.clear()
  if (detail.monitorStatus === '接驾中' && detail.routeInfo) {
    routeLayer?.drawPickup(detail.lon, detail.lat, detail.routeInfo.startLon, detail.routeInfo.startLat)
  if (detail.stateCode === VEHICLE_STATE.PICK_UP && detail.routeInfo) {
    routeLayer?.drawPickup(detail.currentLon, detail.currentLat, detail.routeInfo.startLon, detail.routeInfo.startLat)
  }
  else if (detail.monitorStatus === '送驾中' && detail.routeInfo) {
    routeLayer?.drawDropoff(detail.lon, detail.lat, detail.routeInfo.endLon, detail.routeInfo.endLat)
  else if (detail.stateCode === VEHICLE_STATE.DROP_OFF && detail.routeInfo) {
    routeLayer?.drawDropoff(detail.currentLon, detail.currentLat, detail.routeInfo.endLon, detail.routeInfo.endLat)
  }
})
src/views/businessOperations/commandCenter/map/FenceLayer.ts
@@ -1,4 +1,5 @@
import type { FenceVo } from '../command-center.types'
import { FENCE_STYLE, LAYER_Z_INDEX } from '../command-center.constants'
/**
 * 围栏图层 — 管理地图上 ODD/禁行区域多边形的渲染与交互
@@ -16,6 +17,17 @@
  }
  /**
   * 解析围栏点字符串 "经度*纬度,经度*纬度" → [[lon, lat], ...]
   */
  private parseFencePoints(fencePoints: string): number[][] {
    if (!fencePoints) return []
    return fencePoints.split(',').map((pair) => {
      const [lon, lat] = pair.split('*').map(Number)
      return [lon, lat]
    }).filter(([lon, lat]) => !isNaN(lon) && !isNaN(lat))
  }
  /**
   * 全量更新围栏列表(先清除再重绘)
   * @param fences - 围栏数据列表
   */
@@ -26,36 +38,46 @@
      return
    fences.forEach((fence) => {
      const path = fence.points.map(p => [p.lon, p.lat])
      const isRestricted = fence.type === 'RESTRICTED'
      if (!fence.areas) return
      const polygon = new AMap.Polygon({
        path,
        strokeColor: isRestricted ? '#FF4D4F' : '#1890FF',
        strokeWeight: 2,
        strokeStyle: isRestricted ? 'solid' : 'dashed',
        strokeOpacity: 0.8,
        fillColor: isRestricted ? '#FF4D4F' : '#1890FF',
        fillOpacity: 0.1,
        zIndex: 10,
      })
      fence.areas.forEach((area) => {
        if (area.areaStatus !== 1) return // 只渲染启用的区域
      polygon.on('mousemove', (e: any) => {
        const label = isRestricted ? '禁行区' : '运营区域 (ODD边界)'
        const infoWindow = new AMap.InfoWindow({
          content: `<div style="padding:4px 8px;font-size:12px;">${fence.name || label}</div>`,
          offset: new AMap.Pixel(0, -10),
          closeWhenClickMap: true,
        const path = this.parseFencePoints(area.fencePoints)
        if (path.length < 3) return
        const isRestricted = area.areaType === 2
        const style = isRestricted ? FENCE_STYLE.RESTRICTED : FENCE_STYLE.OPERATIONAL
        const polygon = new AMap.Polygon({
          path,
          strokeColor: style.stroke,
          strokeWeight: FENCE_STYLE.STROKE_WEIGHT,
          strokeStyle: style.strokeStyle,
          strokeOpacity: FENCE_STYLE.STROKE_OPACITY,
          fillColor: style.fill,
          fillOpacity: FENCE_STYLE.FILL_OPACITY,
          zIndex: LAYER_Z_INDEX.FENCE,
        })
        infoWindow.open(this.map, e.lnglat)
      })
      polygon.on('mouseout', () => {
        this.map.clearInfoWindow()
      })
        polygon.on('mousemove', (e: any) => {
          const label = area.areaName || (isRestricted ? '禁行区' : '运营区域 (ODD边界)')
          const infoWindow = new AMap.InfoWindow({
            content: `<div style="padding:4px 8px;font-size:12px;">${fence.electronicName} - ${label}</div>`,
            offset: new AMap.Pixel(0, -10),
            closeWhenClickMap: true,
          })
          infoWindow.open(this.map, e.lnglat)
        })
      polygon.setMap(this.visible ? this.map : null)
      this.polygons.push(polygon)
        polygon.on('mouseout', () => {
          this.map.clearInfoWindow()
        })
        polygon.setMap(this.visible ? this.map : null)
        this.polygons.push(polygon)
      })
    })
  }
src/views/businessOperations/commandCenter/map/PassengerCallLayer.ts
@@ -1,11 +1,12 @@
import type { PassengerCallVo } from '../command-center.types'
import { LAYER_Z_INDEX } from '../command-center.constants'
/**
 * 乘客呼单图层 — 管理地图上乘客呼叫标注的渲染与交互
 */
export class PassengerCallLayer {
  private map: any
  private markers = new Map<string, any>()
  private markers = new Map<number, any>()
  private visible = true
  private infoWindow: any = null
@@ -32,7 +33,7 @@
    if (!AMap)
      return
    const currentIds = new Set(list.map(c => c.callId))
    const currentIds = new Set(list.map(c => c.orderId))
    this.markers.forEach((marker, id) => {
      if (!currentIds.has(id)) {
@@ -42,18 +43,18 @@
    })
    list.forEach((call) => {
      if (this.markers.has(call.callId)) {
        const marker = this.markers.get(call.callId)
        marker.setPosition([call.lon, call.lat])
      if (this.markers.has(call.orderId)) {
        const marker = this.markers.get(call.orderId)
        marker.setPosition([call.startLon, call.startLat])
        marker._callData = call
        return
      }
      const marker = new AMap.Marker({
        position: [call.lon, call.lat],
        position: [call.startLon, call.startLat],
        content: '<div style="width:20px;height:20px;font-size:16px;line-height:20px;text-align:center;">🙋</div>',
        offset: new AMap.Pixel(-10, -10),
        zIndex: 40,
        zIndex: LAYER_Z_INDEX.PASSENGER_CALL,
      })
      marker._callData = call
@@ -61,7 +62,7 @@
        const data = e.target._callData as PassengerCallVo
        const waitMin = Math.round(data.waitingSeconds / 60)
        this.infoWindow.setContent(
          `<div style="padding:4px 8px;font-size:12px;white-space:nowrap;">乘客呼单 · 等待 ${waitMin} 分钟 · ${data.address}</div>`,
          `<div style="padding:4px 8px;font-size:12px;white-space:nowrap;">乘客呼单 · 等待 ${waitMin} 分钟 · ${data.startAddress}</div>`,
        )
        this.infoWindow.open(this.map, e.target.getPosition())
      })
@@ -71,7 +72,7 @@
      })
      marker.setMap(this.visible ? this.map : null)
      this.markers.set(call.callId, marker)
      this.markers.set(call.orderId, marker)
    })
  }
src/views/businessOperations/commandCenter/map/RouteLayer.ts
@@ -1,3 +1,5 @@
import { LAYER_Z_INDEX, ROUTE_COLOR } from '../command-center.constants'
/**
 * 路线图层 — 管理地图上接驾/送驾路线的绘制
 */
src/views/businessOperations/commandCenter/map/StationLayer.ts
@@ -1,4 +1,5 @@
import type { StationVo } from '../command-center.types'
import { LAYER_Z_INDEX } from '../command-center.constants'
/**
 * 站点图层 — 管理地图上充电站/维修站等站点标注
@@ -30,12 +31,12 @@
        position: [station.centerLon, station.centerLat],
        content: '<div style="font-size:20px;line-height:1;">🔌</div>',
        offset: new AMap.Pixel(-10, -10),
        zIndex: 20,
        zIndex: LAYER_Z_INDEX.STATION,
      })
      marker.on('mouseover', (e: any) => {
        const infoWindow = new AMap.InfoWindow({
          content: `<div style="padding:4px 8px;font-size:12px;">${station.name}</div>`,
          content: `<div style="padding:4px 8px;font-size:12px;">${station.stationName}</div>`,
          offset: new AMap.Pixel(0, -20),
        })
        infoWindow.open(this.map, e.target.getPosition())
src/views/businessOperations/commandCenter/map/VehicleLayer.ts
@@ -1,4 +1,5 @@
import type { VehicleMonitorVo } from '../command-center.types'
import { LAYER_Z_INDEX } from '../command-center.constants'
import { createVehicleMarkerContent, createVehicleTooltipContent } from './vehicle-marker'
/**
@@ -6,16 +7,16 @@
 */
export class VehicleLayer {
  private map: any
  private markers = new Map<string, any>()
  private markers = new Map<number, any>()
  private visible = true
  private onSelect: ((vehicleId: string) => void) | null = null
  private onSelect: ((vehicleId: number) => void) | null = null
  private infoWindow: any = null
  /**
   * @param map - 高德地图实例
   * @param onSelect - 点击车辆时的回调
   */
  constructor(map: any, onSelect?: (vehicleId: string) => void) {
  constructor(map: any, onSelect?: (vehicleId: number) => void) {
    this.map = map
    this.onSelect = onSelect || null
    const AMap = (window as any).AMap
@@ -57,7 +58,7 @@
          position: [vehicle.lon, vehicle.lat],
          content: createVehicleMarkerContent(vehicle),
          offset: new AMap.Pixel(-12, -12),
          zIndex: 50,
          zIndex: LAYER_Z_INDEX.VEHICLE,
        })
        marker._vehicleData = vehicle
@@ -86,7 +87,7 @@
   * 高亮选中车辆,其他车辆降低透明度
   * @param selectedId - 选中车辆 ID,null 时恢复全部
   */
  setHighlight(selectedId: string | null) {
  setHighlight(selectedId: number | null) {
    this.markers.forEach((marker, id) => {
      if (selectedId === null) {
        marker.setExtData({ ...marker.getExtData(), opacity: 1 })
src/views/businessOperations/commandCenter/map/vehicle-marker.ts
@@ -1,5 +1,5 @@
import type { VehicleMonitorVo } from '../command-center.types'
import { MONITOR_COLOR_MAP } from '../command-center.constants'
import { MONITOR_COLOR_MAP, VEHICLE_STATE } from '../command-center.constants'
/**
 * 创建车辆地图标注的 HTML 内容
@@ -8,7 +8,7 @@
 */
export const createVehicleMarkerContent = (vehicle: VehicleMonitorVo): string => {
  const colorConfig = MONITOR_COLOR_MAP[vehicle.monitorGroup] || MONITOR_COLOR_MAP.ABNORMAL
  const isPulse = vehicle.monitorStatus === 'ODD越界'
  const isPulse = vehicle.stateCode === VEHICLE_STATE.ODD_OUT
  const isCharging = vehicle.charging
  return `