ai-coding
2026-07-02 992c2ef55bbee0782dda67e5fc5ec72794c2399a
Merge origin/release-0630 into develop
[ai-coding] [祝美莲]
32个文件已修改
1185 ■■■■■ 已修改文件
package.json 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/composables/useCommandCenterSocket.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes.static.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/commandCenter.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/command-center.constants.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/command-center.types.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/components/LayerControl.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/components/VehicleDetailDrawer/Header.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/components/VehicleDetailDrawer/OperatingDataSection.vue 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/components/VehicleDetailDrawer/RouteSection.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/components/VehicleDetailDrawer/index.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/index.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/map/PassengerCallLayer.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/map/VehicleLayer.ts 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/map/vehicle-marker.scss 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/commandCenter/map/vehicle-marker.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/components/FencePanel.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/list.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/map.scss 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/map.vue 215 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/businessOperations/operationWorkbench/workbench.constants.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/operations/taskOrder/pendingReassignment/components/ReassignModal.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/accountPermission/roleManagement/form.vue 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/operationWorkbench/components/FencePanel.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/operationWorkbench/list.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/operationWorkbench/map.scss 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/operationWorkbench/map.vue 250 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/operationWorkbench/workbench.constants.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanManagement/components/PointSelectModal.vue 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/order/orderManagement/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/agreementVersion.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/components/AgreementVersionModal.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -1,7 +1,7 @@
{
  "name": "xp-robotaxi-management",
  "type": "module",
  "version": "1.1.26",
  "version": "1.1.27",
  "private": true,
  "description": "a clean and concise back-end management template based on Vue3, Vite5, Typescript, and Naive UI.",
  "license": "MIT",
src/composables/useCommandCenterSocket.ts
@@ -2,13 +2,13 @@
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'
import type { CommandCenterKpiVo, CommandCenterVehicleDetailVo, PassengerCallVo, VehicleMonitorVo } from '@/views/businessOperations/commandCenter/command-center.types'
/**
 * 指挥中心 WebSocket 连接管理 composable
 * @returns isOnline - 是否在线;connect/disconnect - 连接/断开方法;subscribeCity/unsubscribeCity - 订阅/取消订阅城市
 */
export const useCommandCenterSocket = () => {
export function useCommandCenterSocket() {
  const store = useCommandCenterStore()
  const socket = createCommandCenterSocket()
@@ -53,6 +53,14 @@
      store.socketStatus.lastPushAt = Date.now()
    })
    socket.on(SOCKET_ON_EVENT.VEHICLE_DETAIL, (data: CommandCenterVehicleDetailVo) => {
      console.log('[CC-Socket] ⬇ vehicle_detail :', data)
      if (data.vehicleId === store.selectedVehicleId) {
        store.vehicleDetail = data
      }
      store.socketStatus.lastPushAt = Date.now()
    })
    socket.on(SOCKET_ON_EVENT.KPI, (data: CommandCenterKpiVo) => {
      console.log('[CC-Socket] ⬇ kpi', data)
      store.kpi = data
@@ -91,11 +99,24 @@
    socket.emit(SOCKET_EMIT_EVENT.UNSUBSCRIBE, { cityCode })
  }
  const subscribeVehicleDetail = (vehicleId: number) => {
    console.log('[CC-Socket] ⬆ emit subscribe_vehicle_detail, vehicleId:', vehicleId)
    socket.emit(SOCKET_EMIT_EVENT.SUBSCRIBE_VEHICLE_DETAIL, { vehicleId })
  }
  const unsubscribeVehicleDetail = (vehicleId?: number) => {
    const payload = vehicleId ? { vehicleId } : {}
    console.log('[CC-Socket] ⬆ emit unsubscribe_vehicle_detail', payload)
    socket.emit(SOCKET_EMIT_EVENT.UNSUBSCRIBE_VEHICLE_DETAIL, payload)
  }
  return {
    isOnline,
    connect,
    disconnect,
    subscribeCity,
    unsubscribeCity,
    subscribeVehicleDetail,
    unsubscribeVehicleDetail,
  }
}
src/router/routes.static.ts
@@ -1906,7 +1906,7 @@
  {
    name: 'testDemoOperationWorkbenchMap',
    path: '/testDemo/operationWorkbench/map',
    title: '测试运营工作台',
    title: '测试工作台',
    requiresAuth: true,
    icon: 'icon-park-outline:alarm',
    menuType: 'page',
src/store/commandCenter.ts
@@ -1,12 +1,12 @@
import { DEFAULT_CITY_CODE, DEFAULT_DISTRICT_CODE, DEFAULT_PROVINCE_CODE, STATIONS_CACHE_TTL } from '@/views/businessOperations/commandCenter/command-center.constants'
import type {
  CommandCenterKpiVo,
  CommandCenterVehicleDetailVo,
  FenceVo,
  LayerVisibility,
  PassengerCallVo,
  SocketStatus,
  StationVo,
  VehicleDetailVo,
  VehicleMonitorVo,
  XpOpenCity,
  XpPointVo,
@@ -33,7 +33,7 @@
  stationsCache: Record<string, StationsCacheEntry>
  points: XpPointVo[]
  selectedVehicleId: number | null
  vehicleDetail: VehicleDetailVo | null
  vehicleDetail: CommandCenterVehicleDetailVo | null
  layerVisibility: LayerVisibility
  socketStatus: SocketStatus
}
src/views/businessOperations/commandCenter/command-center.constants.ts
@@ -35,6 +35,8 @@
  AUTH: 'auth',
  SUBSCRIBE: 'subscribe',
  UNSUBSCRIBE: 'unsubscribe',
  SUBSCRIBE_VEHICLE_DETAIL: 'subscribe_vehicle_detail',
  UNSUBSCRIBE_VEHICLE_DETAIL: 'unsubscribe_vehicle_detail',
} as const
/** Socket.IO 服务端 → 客户端事件名 */
@@ -42,6 +44,7 @@
  AUTH_SUCCESS: 'auth_success',
  AUTH_FAILED: 'auth_failed',
  VEHICLES: 'vehicles',
  VEHICLE_DETAIL: 'vehicle_detail',
  KPI: 'kpi',
  PASSENGER_CALLS: 'passenger_calls',
} as const
src/views/businessOperations/commandCenter/command-center.types.ts
@@ -27,6 +27,20 @@
  positionUpdateTime: number
}
export interface CommandCenterVehicleDetailVo {
  vehicleId: number
  vin: string
  plateNumber: string
  monitorGroup: MonitorGroup
  monitorStatus: string
  stateCode: VehicleMonitorStateCode
  currentElectricity: number
  todayCompletedOrders: number
  routeInfo: CommandCenterRouteInfoVo | null
  currentLon: number
  currentLat: number
}
export interface CommandCenterKpiVo {
  cruisingCount: number
  hasOrderCount: number
@@ -46,6 +60,7 @@
export interface PassengerCallVo {
  orderId: number
  orderNumber: string
  appUserId: string
  startLon: number
  startLat: number
  startAddress: string
src/views/businessOperations/commandCenter/components/LayerControl.vue
@@ -9,7 +9,7 @@
    label: '车辆位置',
    active: store.layerVisibility.vehicle,
    toggle: () => store.toggleLayer('vehicle', !store.layerVisibility.vehicle),
    iconHtml: '',
    iconHtml: '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" fill="currentColor" width="16" height="16"><path d="M941.4 467.2l-117.6-3.2-56-155.2c-16-44.8-59.2-75.2-107.2-75.2H362.2c-48 0-91.2 30.4-107.2 75.2l-56 155.2-117.6 3.2C56.6 468.8 35 491.2 35 518.4v148.8c0 27.2 22.4 49.6 49.6 49.6h44.8c8 51.2 52.8 89.6 105.6 89.6s97.6-38.4 105.6-89.6h342.4c8 51.2 52.8 89.6 105.6 89.6s97.6-38.4 105.6-89.6h44.8c27.2 0 49.6-22.4 49.6-49.6V518.4c0-27.2-22.4-51.2-46.6-51.2zM235 758.4c-27.2 0-49.6-22.4-49.6-49.6s22.4-49.6 49.6-49.6 49.6 22.4 49.6 49.6-22.4 49.6-49.6 49.6zm553.6 0c-27.2 0-49.6-22.4-49.6-49.6s22.4-49.6 49.6-49.6 49.6 22.4 49.6 49.6-22.4 49.6-49.6 49.6zM253.4 462.4l44.8-131.2c8-24 30.4-40 56-40h315.2c25.6 0 48 16 56 40l44.8 131.2H253.4z"></path><path d="M512 296a25.6 25.6 0 0 1-25.6-25.6V134.4a25.6 25.6 0 0 1 51.2 0v136c0 14.1-11.5 25.6-25.6 25.6z"></path></svg>',
  },
  {
    key: 'passengerCall',
src/views/businessOperations/commandCenter/components/VehicleDetailDrawer/Header.vue
@@ -1,24 +1,24 @@
<script setup lang="ts">
import type { CommandCenterVehicleDetailVo } from '../../command-center.types'
import { MONITOR_COLOR_MAP } from '../../command-center.constants'
const props = defineProps<{ vehicle: CommandCenterVehicleDetailVo }>()
const colorConfig = computed(() => MONITOR_COLOR_MAP[props.vehicle.monitorGroup] || MONITOR_COLOR_MAP.ABNORMAL)
</script>
<template>
  <div class="vehicle-detail-header">
    <span
      class="vehicle-detail-header__dot"
      :style="{ background: colorConfig.color }"
    />
    <span class="vehicle-detail-header__plate">{{ detail.plateNumber || detail.vin }}</span>
    <NTag size="small" :color="{ color: colorConfig.color + '20', textColor: colorConfig.color, borderColor: colorConfig.color }">
      {{ detail.monitorStatus }}
    <span class="vehicle-detail-header__plate">{{ vehicle.plateNumber || vehicle.vin }}</span>
    <NTag size="small" :color="{ color: `${colorConfig.color}20`, textColor: colorConfig.color, borderColor: colorConfig.color }">
      {{ vehicle.monitorStatus }}
    </NTag>
  </div>
</template>
<script setup lang="ts">
import type { VehicleDetailVo } from '../../command-center.types'
import { MONITOR_COLOR_MAP } from '../../command-center.constants'
const props = defineProps<{ detail: VehicleDetailVo }>()
const colorConfig = computed(() => MONITOR_COLOR_MAP[props.detail.monitorGroup] || MONITOR_COLOR_MAP.ABNORMAL)
</script>
<style scoped lang="scss">
.vehicle-detail-header {
src/views/businessOperations/commandCenter/components/VehicleDetailDrawer/OperatingDataSection.vue
@@ -1,24 +1,3 @@
<template>
  <div class="operating-data">
    <div class="operating-data__title">运营数据</div>
    <div class="operating-data__item">
      <span class="operating-data__label">电量</span>
      <NProgress
        type="line"
        :percentage="electricity"
        :color="electricityColor"
        :rail-color="'rgba(0,0,0,0.06)'"
        style="flex: 1;"
      />
      <span class="operating-data__value">{{ electricity }}%</span>
    </div>
    <div class="operating-data__item">
      <span class="operating-data__label">今日完单</span>
      <span class="operating-data__value">{{ todayOrders }}</span>
    </div>
  </div>
</template>
<script setup lang="ts">
const props = defineProps<{
  electricity: number
@@ -34,6 +13,34 @@
})
</script>
<template>
  <div class="operating-data">
    <div class="operating-data__title">
      运营数据
    </div>
    <div class="operating-data__cards">
      <div class="operating-data__card">
        <span class="operating-data__label">电量</span>
        <div class="operating-data__electricity">
          <span class="operating-data__value" :style="{ color: electricityColor }">{{ electricity }}%</span>
          <NProgress
            type="line"
            :percentage="electricity"
            :color="electricityColor"
            rail-color="rgba(0,0,0,0.06)"
            :show-indicator="false"
            style="flex: 1;"
          />
        </div>
      </div>
      <div class="operating-data__card">
        <span class="operating-data__label">今日完单</span>
        <span class="operating-data__value">{{ todayOrders }}</span>
      </div>
    </div>
  </div>
</template>
<style scoped lang="scss">
.operating-data {
  padding: 12px 16px;
@@ -45,25 +52,36 @@
    margin-bottom: 12px;
  }
  &__item {
  &__cards {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 10px;
  }
  &__card {
    flex: 1;
    background: #f5f8fc;
    border-radius: 8px;
    padding: 12px;
    display: flex;
    flex-direction: column;
    gap: 8px;
  }
  &__label {
    font-size: 12px;
    color: rgba(0, 0, 0, 0.65);
    min-width: 60px;
    color: rgba(0, 0, 0, 0.45);
  }
  &__electricity {
    display: flex;
    align-items: center;
    gap: 8px;
  }
  &__value {
    font-size: 14px;
    font-size: 20px;
    font-weight: 600;
    color: rgba(0, 0, 0, 0.85);
    font-weight: 500;
    min-width: 40px;
    text-align: right;
  }
}
</style>
src/views/businessOperations/commandCenter/components/VehicleDetailDrawer/RouteSection.vue
@@ -2,39 +2,34 @@
  <div class="route-section">
    <div class="route-section__title">路线信息</div>
    <div class="route-section__content">
      <template v-if="detail.routeInfo">
        <NTimeline>
          <NTimelineItem color="#52c41a">
            <template #header>
              <span class="route-section__label">上车点</span>
            </template>
            <span class="route-section__address">📍 {{ detail.routeInfo.startAddress || '暂无位置' }}</span>
          </NTimelineItem>
          <NTimelineItem color="#1890ff">
            <template #header>
              <span class="route-section__label">车辆位置</span>
            </template>
            <span class="route-section__address">暂无位置</span>
          </NTimelineItem>
          <NTimelineItem color="#ff4d4f">
            <template #header>
              <span class="route-section__label">目的地</span>
            </template>
            <span class="route-section__address">🏁 {{ detail.routeInfo.endAddress || '暂无位置' }}</span>
          </NTimelineItem>
        </NTimeline>
      </template>
      <template v-else>
        <p>{{ detail.monitorStatus }}</p>
      </template>
      <NTimeline>
        <NTimelineItem color="#52c41a">
          <template #header>
            <span class="route-section__label">上车点</span>
          </template>
          <span class="route-section__address">📍 {{ routeInfo.startAddress || '暂无位置' }}</span>
        </NTimelineItem>
        <NTimelineItem color="#ff4d4f">
          <template #header>
            <span class="route-section__label">目的地</span>
          </template>
          <span class="route-section__address">🏁 {{ routeInfo.endAddress || '暂无位置' }}</span>
        </NTimelineItem>
      </NTimeline>
      <div v-if="routeInfo.eta" class="route-section__eta">
        预计到达:{{ Math.ceil(routeInfo.eta / 60) }} 分钟
      </div>
    </div>
  </div>
</template>
<script setup lang="ts">
import type { VehicleDetailVo } from '../../command-center.types'
import type { CommandCenterRouteInfoVo } from '../../command-center.types'
defineProps<{ detail: VehicleDetailVo }>()
defineProps<{
  routeInfo: CommandCenterRouteInfoVo
  monitorStatus: string
}>()
</script>
<style scoped lang="scss">
src/views/businessOperations/commandCenter/components/VehicleDetailDrawer/index.vue
@@ -1,3 +1,18 @@
<script setup lang="ts">
import { useCommandCenterStore } from '@/store/commandCenter'
import Header from './Header.vue'
import OperatingDataSection from './OperatingDataSection.vue'
const store = useCommandCenterStore()
const detail = computed(() => store.vehicleDetail)
function handleClose(show: boolean) {
  if (!show)
    store.closeVehicleDetail()
}
</script>
<template>
  <NDrawer
    :show="!!store.selectedVehicleId"
@@ -11,12 +26,11 @@
      :body-content-style="{ padding: 0, background: '#ffffff' }"
      :header-style="{ display: 'none' }"
    >
      <template v-if="store.vehicleDetail">
        <Header :detail="store.vehicleDetail" />
        <RouteSection :detail="store.vehicleDetail" />
      <template v-if="detail">
        <Header :vehicle="detail" />
        <OperatingDataSection
          :electricity="currentElectricity"
          :today-orders="currentTodayOrders"
          :electricity="detail.currentElectricity"
          :today-orders="detail.todayCompletedOrders"
        />
      </template>
      <div v-else class="drawer-loading">
@@ -25,31 +39,6 @@
    </NDrawerContent>
  </NDrawer>
</template>
<script setup lang="ts">
import { useCommandCenterStore } from '@/store/commandCenter'
import Header from './Header.vue'
import RouteSection from './RouteSection.vue'
import OperatingDataSection from './OperatingDataSection.vue'
const store = useCommandCenterStore()
const currentElectricity = computed(() => {
  if (store.vehicleDetail)
    return store.vehicleDetail.currentElectricity
  const v = store.vehicles.find(v => v.vehicleId === store.selectedVehicleId)
  return v?.currentElectricity ?? 0
})
const currentTodayOrders = computed(() => {
  return store.vehicleDetail?.todayCompletedOrders ?? 0
})
function handleClose(show: boolean) {
  if (!show)
    store.closeVehicleDetail()
}
</script>
<style scoped lang="scss">
.drawer-loading {
src/views/businessOperations/commandCenter/index.vue
@@ -12,11 +12,12 @@
import LayerControl from './components/LayerControl.vue'
import MapLegend from './components/MapLegend.vue'
import BackToOverview from './components/BackToOverview.vue'
import VehicleDetailDrawer from './components/VehicleDetailDrawer/index.vue'
defineOptions({ name: 'CommandCenter' })
const store = useCommandCenterStore()
const { isOnline, connect, disconnect, subscribeCity, unsubscribeCity } = useCommandCenterSocket()
const { isOnline, connect, disconnect, subscribeCity, unsubscribeCity, subscribeVehicleDetail, unsubscribeVehicleDetail } = useCommandCenterSocket()
const mapCanvasRef = ref<InstanceType<typeof MapCanvas> | null>(null)
let fenceLayer: FenceLayer | null = null
@@ -101,6 +102,19 @@
  passengerCallLayer = new PassengerCallLayer(map)
  pointLayer = new PointLayer(map)
  vehicleLayer.onClick((vehicleId) => {
    if (store.selectedVehicleId) {
      unsubscribeVehicleDetail(store.selectedVehicleId)
    }
    store.openVehicleDetail(vehicleId)
    vehicleLayer?.setHighlight(vehicleId)
    subscribeVehicleDetail(vehicleId)
    const vehicle = store.vehicles.find(v => v.vehicleId === vehicleId)
    if (vehicle) {
      mapCanvasRef.value?.mapInstance?.setZoomAndCenter(12, [vehicle.lon, vehicle.lat])
    }
  })
  fenceLayer.update(store.fences)
  pointLayer.update(store.points)
}
@@ -111,6 +125,16 @@
watch(() => store.vehicles, (list) => {
  vehicleLayer?.update(list)
})
watch(() => store.selectedVehicleId, (id, oldId) => {
  vehicleLayer?.setHighlight(id)
  if (!id) {
    if (oldId) {
      unsubscribeVehicleDetail(oldId)
    }
    mapCanvasRef.value?.fitToCity(store.cityCode)
  }
})
watch(() => store.passengerCalls, (list) => {
@@ -210,6 +234,10 @@
})
onUnmounted(() => {
  if (store.selectedVehicleId) {
    unsubscribeVehicleDetail(store.selectedVehicleId)
    store.closeVehicleDetail()
  }
  vehicleLayer?.destroy()
  passengerCallLayer?.destroy()
  fenceLayer?.destroy()
@@ -257,6 +285,7 @@
      <MapControls @reset="handleMapReset" @zoom-in="handleZoomIn" @zoom-out="handleZoomOut" />
      <BackToOverview />
    </div>
    <VehicleDetailDrawer />
  </div>
</template>
src/views/businessOperations/commandCenter/map/PassengerCallLayer.ts
@@ -14,7 +14,7 @@
    <div class="vehicle-info-card__body">
      <div class="vehicle-info-card__row">
        <span class="vehicle-info-card__label">用户ID</span>
        <span class="vehicle-info-card__value">${call.orderNumber}</span>
        <span class="vehicle-info-card__value">${call.appUserId}</span>
      </div>
      <div class="vehicle-info-card__row">
        <span class="vehicle-info-card__label">等待时长</span>
src/views/businessOperations/commandCenter/map/VehicleLayer.ts
@@ -11,6 +11,7 @@
  private visible = true
  private detailWindow: any = null
  private closeHandler: (() => void) | null = null
  private onClickCallback: ((vehicleId: number) => void) | null = null
  /**
   * @param map - 高德地图实例
@@ -65,13 +66,9 @@
        })
        marker._vehicleData = vehicle
        marker.on('mouseover', (e: any) => {
        marker.on('click', (e: any) => {
          const data = e.target._vehicleData as VehicleMonitorVo
          this.showVehicleDetail(data, e.target.getPosition())
        })
        marker.on('mouseout', () => {
          this.detailWindow?.close()
          this.onClickCallback?.(data.vehicleId)
        })
        marker.setMap(this.visible ? this.map : null)
@@ -114,17 +111,17 @@
   */
  setHighlight(selectedId: number | null) {
    this.markers.forEach((marker, id) => {
      const dom = marker.getContentDom?.()
      if (!dom)
        return
      if (selectedId === null) {
        marker.setExtData({ ...marker.getExtData(), opacity: 1 })
        const dom = marker.getContentDom?.()
        if (dom)
          dom.style.opacity = '1'
        dom.style.opacity = '1'
        dom.style.transform = 'scale(1)'
      }
      else {
        const opacity = id === selectedId ? 1 : 0.3
        const dom = marker.getContentDom?.()
        if (dom)
          dom.style.opacity = String(opacity)
        const isSelected = id === selectedId
        dom.style.opacity = isSelected ? '1' : '0.3'
        dom.style.transform = isSelected ? 'scale(1.5)' : 'scale(1)'
      }
    })
  }
@@ -164,6 +161,13 @@
    this.markers.clear()
  }
  /**
   * 设置车辆点击回调
   */
  onClick(cb: (vehicleId: number) => void) {
    this.onClickCallback = cb
  }
  /** 销毁图层,释放资源 */
  destroy() {
    this.clear()
@@ -171,6 +175,7 @@
      document.removeEventListener('closeVehicleInfoWindow', this.closeHandler)
      this.closeHandler = null
    }
    this.onClickCallback = null
    this.detailWindow = null
  }
}
src/views/businessOperations/commandCenter/map/vehicle-marker.scss
@@ -13,11 +13,9 @@
    transform: scale(1.4);
  }
  &__dot {
    width: 16px;
    height: 16px;
    border-radius: 50%;
    border: 2px solid rgba(255, 255, 255, 0.9);
  &__icon {
    width: 20px;
    height: 20px;
  }
  &__charging {
src/views/businessOperations/commandCenter/map/vehicle-marker.ts
@@ -13,7 +13,10 @@
  return `
    <div class="vehicle-marker ${isPulse ? 'vehicle-marker--pulse' : ''}">
      <div class="vehicle-marker__dot" style="background: ${colorConfig.color};"></div>
      <svg class="vehicle-marker__icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" fill="${colorConfig.color}">
        <path d="M941.4 467.2l-117.6-3.2-56-155.2c-16-44.8-59.2-75.2-107.2-75.2H362.2c-48 0-91.2 30.4-107.2 75.2l-56 155.2-117.6 3.2C56.6 468.8 35 491.2 35 518.4v148.8c0 27.2 22.4 49.6 49.6 49.6h44.8c8 51.2 52.8 89.6 105.6 89.6s97.6-38.4 105.6-89.6h342.4c8 51.2 52.8 89.6 105.6 89.6s97.6-38.4 105.6-89.6h44.8c27.2 0 49.6-22.4 49.6-49.6V518.4c0-27.2-22.4-51.2-46.6-51.2zM235 758.4c-27.2 0-49.6-22.4-49.6-49.6s22.4-49.6 49.6-49.6 49.6 22.4 49.6 49.6-22.4 49.6-49.6 49.6zm553.6 0c-27.2 0-49.6-22.4-49.6-49.6s22.4-49.6 49.6-49.6 49.6 22.4 49.6 49.6-22.4 49.6-49.6 49.6zM253.4 462.4l44.8-131.2c8-24 30.4-40 56-40h315.2c25.6 0 48 16 56 40l44.8 131.2H253.4z"></path>
        <path d="M512 296a25.6 25.6 0 0 1-25.6-25.6V134.4a25.6 25.6 0 0 1 51.2 0v136c0 14.1-11.5 25.6-25.6 25.6z"></path>
      </svg>
      ${isCharging ? '<span class="vehicle-marker__charging">⚡</span>' : ''}
    </div>
  `
src/views/businessOperations/operationWorkbench/components/FencePanel.vue
@@ -568,7 +568,7 @@
                  getFenceStatusTagType(Number(fenceDetailData?.status || 0))
                "
                :bordered="false"
                class="h-24px mx-4px"
                class="h-24px mx-4px flex-shrink-0"
              >
                {{ getFenceStatusText(Number(fenceDetailData?.status || 0)) }}
              </NTag>
src/views/businessOperations/operationWorkbench/list.vue
@@ -67,8 +67,8 @@
]
const fenceStatusOptions = [
  { label: '草稿', value: 0 },
  { label: '待生效', value: 1 },
  // { label: '草稿', value: 0 },
  // { label: '待生效', value: 1 },
  { label: '启用中', value: 2 },
  { label: '已停用', value: 3 },
]
src/views/businessOperations/operationWorkbench/map.scss
@@ -709,6 +709,52 @@
  color: #e02716;
}
.fence-hover-tooltip {
  position: absolute;
  z-index: 5;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 88px;
  max-width: 160px;
  padding: 7px 10px;
  border-radius: 6px;
  color: #fff;
  font-size: 13px;
  line-height: 18px;
  white-space: nowrap;
  pointer-events: none;
  transform: translate(-50%, -100%);
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
  &.is-enabled {
    background: #19b144;
  }
  &.is-disabled {
    background: #8f959e;
  }
  &.is-testing {
    background: #171f26;
  }
}
.fence-hover-tooltip__text {
  overflow: hidden;
  text-overflow: ellipsis;
}
.fence-hover-tooltip__arrow {
  position: absolute;
  bottom: -5px;
  left: 50%;
  width: 10px;
  height: 10px;
  background: inherit;
  transform: translateX(-50%) rotate(45deg);
}
.map-tips {
  position: absolute;
  top: 46px;
src/views/businessOperations/operationWorkbench/map.vue
@@ -524,7 +524,7 @@
      'white-space: normal',
      'word-break: break-word',
    ].join(';')
    text.innerHTML = '拖动蓝色实心点增加顶点<br />点击白色实心点删除顶点'
    text.innerHTML = '拖动绿色实心点增加顶点<br />点击白色实心点删除顶点'
    const arrow = document.createElement('div')
    arrow.style.cssText = [
      'position: absolute',
@@ -1412,7 +1412,19 @@
const stationPolygons = shallowRef<any[]>([])
const fencePolygons = shallowRef<any[]>([])
const currentCityBoundaryPaths = ref<LngLatTuple[][]>([])
const fenceHoverTooltipVisible = ref(false)
const fenceHoverTooltipText = ref('')
const fenceHoverTooltipType = ref<'enabled' | 'disabled' | 'testing'>('testing')
const fenceCreateOutsideCurrentCity = ref(false)
const hoveredFencePolygon = shallowRef<any | null>(null)
const hoveredFenceBaseStrokeWeight = ref<number | null>(null)
const fenceHoverTooltipPosition = reactive({
  x: 0,
  y: 0,
})
let fenceHoverTooltipTimer: ReturnType<typeof setTimeout> | null = null
let pendingFenceHoverKey = ''
let pendingFenceHoverPixel: { x: number, y: number } | null = null
// 围栏/场站点选后会有“立即聚焦 + 异步详情回来的二次聚焦”两条链路。
// 这里记录同一轮选中的自动聚焦请求,避免重复 fitView 抢走用户正在手动调整的视野。
const lastMapWheelViewportInteractionAt = ref(0)
@@ -1993,9 +2005,133 @@
function handleCityBoundaryMouseOut() {
  fenceCreateOutsideCurrentCity.value = false
  hideFenceHoverTooltip()
  resetFenceEditGuideHoverState()
  resetRestrictedEditGuideHoverState()
  resetStationEditGuideHoverState()
}
function clearFenceHoverTooltipTimer() {
  if (fenceHoverTooltipTimer) {
    clearTimeout(fenceHoverTooltipTimer)
    fenceHoverTooltipTimer = null
  }
}
function resetFenceHoverPolygonStyle() {
  if (hoveredFencePolygon.value && hoveredFenceBaseStrokeWeight.value !== null) {
    hoveredFencePolygon.value.setOptions({
      strokeWeight: hoveredFenceBaseStrokeWeight.value,
    })
  }
  hoveredFencePolygon.value = null
  hoveredFenceBaseStrokeWeight.value = null
}
function highlightFenceHoverPolygon(polygon: any, item: any) {
  if (Number(item?.areaType) !== 1)
    return
  if (hoveredFencePolygon.value === polygon)
    return
  resetFenceHoverPolygonStyle()
  const currentStrokeWeight = Number(polygon?.getExtData?.()?.baseStrokeWeight ?? 2)
  hoveredFencePolygon.value = polygon
  hoveredFenceBaseStrokeWeight.value = currentStrokeWeight
  polygon.setOptions({
    strokeWeight: currentStrokeWeight + 1.5,
  })
}
function hideFenceHoverTooltip() {
  clearFenceHoverTooltipTimer()
  pendingFenceHoverKey = ''
  pendingFenceHoverPixel = null
  resetFenceHoverPolygonStyle()
  fenceHoverTooltipVisible.value = false
  fenceHoverTooltipText.value = ''
  fenceHoverTooltipType.value = 'testing'
}
function getFenceHoverKey(item: any) {
  return `${String(item?.electronicId || '')}-${String(item?.id || '')}`
}
function resolveFenceOverlayType(item: any) {
  if (Number(item?.areaStatus || 1) === 2 || Number(item?.status) === 3)
    return 'disabled' as const
  if (Number(item?.status) === 2 || enabledFenceIdSet.value.has(String(item?.electronicId || '')))
    return 'enabled' as const
  return 'testing' as const
}
function getFenceHoverTooltipText(type: 'enabled' | 'disabled' | 'testing') {
  if (type === 'enabled')
    return '启用中围栏'
  if (type === 'disabled')
    return '已停用围栏'
  return '测试围栏'
}
function parseFencePriorityTime(value: unknown) {
  const timestamp = dayjs(String(value || '')).valueOf()
  return Number.isFinite(timestamp) ? timestamp : 0
}
function getFenceOverlayPriority(item: any) {
  const type = resolveFenceOverlayType(item)
  return {
    type,
    typePriority: type === 'testing' ? 2 : type === 'enabled' ? 1 : 0,
    updateTimePriority: parseFencePriorityTime(item?.updateTime),
    fallbackPriority: Number(item?.electronicId ?? item?.id ?? 0) || 0,
  }
}
function updateFenceHoverTooltipPositionByPixel(pixel?: { x: number, y: number } | null) {
  if (!map.value || !pixel)
    return false
  const container = map.value.getContainer?.()
  if (!container)
    return false
  const offsetX = 1
  const offsetY = 7
  const rawX = Number(pixel.x) + offsetX
  const rawY = Number(pixel.y) - offsetY
  const clampedX = Math.max(12, Math.min(rawX, container.clientWidth - 12))
  const clampedY = Math.max(12, Math.min(rawY, container.clientHeight - 12))
  fenceHoverTooltipPosition.x = clampedX
  fenceHoverTooltipPosition.y = clampedY
  return true
}
function scheduleFenceHoverTooltip(item: any, pixel?: { x: number, y: number } | null) {
  if (Number(item?.areaType) !== 1 || isFenceFormMode.value || isStationFormMode.value)
    return
  const hoverKey = getFenceHoverKey(item)
  pendingFenceHoverKey = hoverKey
  pendingFenceHoverPixel = pixel
  updateFenceHoverTooltipPositionByPixel(pixel)
  clearFenceHoverTooltipTimer()
  fenceHoverTooltipTimer = window.setTimeout(() => {
    if (pendingFenceHoverKey !== hoverKey)
      return
    const nextType = resolveFenceOverlayType(item)
    if (!updateFenceHoverTooltipPositionByPixel(pendingFenceHoverPixel))
      return
    fenceHoverTooltipType.value = nextType
    fenceHoverTooltipText.value = getFenceHoverTooltipText(nextType)
    fenceHoverTooltipVisible.value = true
    fenceHoverTooltipTimer = null
  }, 300)
}
function handleNativeMouseMove(event: MouseEvent) {
@@ -2372,7 +2508,7 @@
  }
  if (fenceDraftPoints.value.length < 3)
    return '请在地图上【非运营区域】绘制多边形区域,双击闭合'
  return '【蓝色运营区】可调整区域点位,请勿与其他运营区重叠'
  return '【绿色运营区】可调整区域点位,请勿与其他运营区重叠'
})
const stationMapTipText = computed(() => {
  if (!isStationFormMode.value)
@@ -8839,6 +8975,7 @@
function clearMapEntities() {
  if (!map.value)
    return
  hideFenceHoverTooltip()
  if (pointMarkers.value.length)
    map.value.remove(pointMarkers.value)
  if (cityBoundaryPolygons.value.length)
@@ -8943,6 +9080,22 @@
        }
        return false
      })
      .sort((prev: any, next: any) => {
        const prevPriority = getFenceOverlayPriority(prev)
        const nextPriority = getFenceOverlayPriority(next)
        if (prevPriority.typePriority !== nextPriority.typePriority)
          return prevPriority.typePriority - nextPriority.typePriority
        if (
          prevPriority.type === 'testing'
          && nextPriority.type === 'testing'
          && prevPriority.updateTimePriority !== nextPriority.updateTimePriority
        ) {
          return prevPriority.updateTimePriority - nextPriority.updateTimePriority
        }
        if (prevPriority.updateTimePriority !== nextPriority.updateTimePriority)
          return prevPriority.updateTimePriority - nextPriority.updateTimePriority
        return prevPriority.fallbackPriority - nextPriority.fallbackPriority
      })
      .map((item) => {
        const areaType = Number(item.areaType)
        const isDisabledFenceArea = Number(item.areaStatus || 1) === 2
@@ -8975,11 +9128,22 @@
          = areaType === 1 && isDisabledFenceArea && !isFenceSelectedDetail
        const isFenceReadonlyDimmed
          = areaType === 1 && !isActive && !isDisabledFenceArea
        const isFenceEnabled
          = areaType === 1 && resolveFenceOverlayType(item) === 'enabled'
        const isFenceDetailActive = isFenceSelectedDetail
        const isFenceHighlighted = isFenceDetailActive || isFenceEnabled
        // 停用运营区现已需要支持从地图点选进入详情并继续编辑;
        // 这里仅保留“停用禁行区不可点”,不再沿用旧逻辑把停用运营区一并禁点。
        const shouldBlockAreaClick
          = areaType === 2 && isDisabledFenceArea
        const fencePriority = getFenceOverlayPriority(item)
        const baseStrokeWeight = areaType === 2
          ? 0
          : isFenceHighlighted
              ? 2
              : isActive
                ? 3
                : 2
        const polygon = new (window as any).AMap.Polygon({
          path: points,
          fillColor:
@@ -8989,7 +9153,7 @@
                ? isReadonlyRestrictedActive
                  ? '#F14637'
                  : '#EF938B'
                : isFenceDetailActive
                : isFenceHighlighted
                  ? '#19B144'
                  : isFenceReadonlyDimmed
                    ? '#171F26'
@@ -9001,18 +9165,12 @@
                ? isReadonlyRestrictedActive
                  ? '#F14637'
                  : '#EF938B'
                : isFenceDetailActive
                : isFenceHighlighted
                  ? '#19B144'
                  : isFenceReadonlyDimmed
                    ? '#AEB0B2'
                    : '#AEB0B2',
          strokeWeight: areaType === 2
            ? 0
            : isFenceDetailActive
              ? 2
              : isActive
                ? 3
                : 2,
          strokeWeight: baseStrokeWeight,
          strokeOpacity:
            isFenceDisabledReadonly
              ? 0.3
@@ -9026,7 +9184,7 @@
                ? isReadonlyRestrictedActive
                  ? 1
                  : 1
                : isFenceDetailActive
                : isFenceHighlighted
                  ? 0.1
                  : isFenceReadonlyDimmed
                    ? 0.08
@@ -9043,9 +9201,12 @@
            && !isPointMapPickActive.value
            && !isFenceFormMode.value
            && !isStationFormMode.value,
          zIndex: isReadonlyRestrictedActive ? 230 : isActive ? 220 : 205,
          zIndex: areaType === 2
            ? (isReadonlyRestrictedActive ? 230 : 205)
            : 240 + fencePriority.typePriority * 40 + (isActive ? 8 : 0),
        })
        polygon.on('click', () => {
          hideFenceHoverTooltip()
          if (
            shouldBlockAreaClick
            || isPointMapPickActive.value
@@ -9084,11 +9245,25 @@
          }
          handleMapEntityClick('fence', currentFenceId)
        })
        polygon.on('mouseover', (event: any) => {
          highlightFenceHoverPolygon(polygon, item)
          scheduleFenceHoverTooltip(item, event?.pixel || null)
        })
        polygon.on('mousemove', (event: any) => {
          pendingFenceHoverPixel = event?.pixel || null
          updateFenceHoverTooltipPositionByPixel(pendingFenceHoverPixel)
        })
        polygon.on('mouseout', () => {
          if (pendingFenceHoverKey === getFenceHoverKey(item))
            pendingFenceHoverKey = ''
          hideFenceHoverTooltip()
        })
        polygon.setExtData({
          id: String(item.electronicId),
          type: 'fence',
          areaType,
          areaId: String(item.id || ''),
          baseStrokeWeight,
        })
        return polygon
      })
@@ -9654,6 +9829,18 @@
          class="map-content"
          :class="{ 'map-content--warning-cursor': fenceCreateOutsideCurrentCity }"
        />
        <div
          v-if="fenceHoverTooltipVisible"
          class="fence-hover-tooltip"
          :class="`is-${fenceHoverTooltipType}`"
          :style="{
            left: `${fenceHoverTooltipPosition.x}px`,
            top: `${fenceHoverTooltipPosition.y}px`,
          }"
        >
          <span class="fence-hover-tooltip__text">{{ fenceHoverTooltipText }}</span>
          <span class="fence-hover-tooltip__arrow" />
        </div>
        <MapInfoPopupCard
          v-if="showFenceEditMapInfoPopup"
          :style="mapInfoPopupStyle"
@@ -9748,7 +9935,7 @@
                    class="operate-item create-tool-trigger"
                    :class="{
                      'active': showCreateMenu,
                      'disabled': !!createToolDisabledReason,
                      // 'disabled': !!createToolDisabledReason,
                      'has-selection':
                        !showCreateMenu && !!selectedCreateToolOption,
                    }"
src/views/businessOperations/operationWorkbench/workbench.constants.ts
@@ -74,8 +74,8 @@
// 三类对象在左侧筛选区共用的状态筛选项。
export const statusOptionsMap: Record<WorkbenchTab, OptionItem[]> = {
  fence: [
    { label: "草稿", value: "draft" },
    { label: "待生效", value: "pending" },
    // { label: "草稿", value: "draft" },
    // { label: "待生效", value: "pending" },
    { label: "启用中", value: "enabled" },
    { label: "已停用", value: "disabled" },
  ],
src/views/operations/taskOrder/pendingReassignment/components/ReassignModal.vue
@@ -409,10 +409,8 @@
<style scoped lang="scss">
.footer-btn {
  padding: 7px 29px;
  height: auto;
  font-size: 13px;
  line-height: 18px;
  width: 84px;
  height: 32px;
}
:deep(.robotaxi-table-row--selected td) {
src/views/system/accountPermission/roleManagement/form.vue
@@ -14,7 +14,7 @@
  useDialog,
  useMessage,
} from 'naive-ui'
import { onActivated, onMounted, reactive, ref, watch } from 'vue'
import { nextTick, onActivated, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
  addRole,
@@ -135,6 +135,7 @@
})
const saveButtonDisabled = computed(() => isReadOnlyMode.value || !formModel.roleName.trim() || loading.value)
const batchActionDisabled = computed(() => isReadOnlyMode.value || selectedPermissionSections.value.length === 0)
const permissionDetailPanelRef = ref<HTMLElement | null>(null)
const rules: FormRules = {
  roleName: [
@@ -259,14 +260,21 @@
const filteredPermissions = computed(() => filterPermissionTree(currentPermissions.value, formModel.permissionKeyword))
function isSpecialActionNode(parentNode: PermissionNode, node: PermissionNode) {
  // 运营管理下“系统操作日志”的“导出”接口返回的是 C 类型,
  // 但业务上需要按右侧按钮权限展示,不能作为左侧子模块显示。
  return parentNode.routerName === 'adminOperationLog:list'
    && node.routerName === 'adminOperationLog:export'
}
function getChildModuleNodes(node: PermissionNode) {
  // 模块节点按菜单类型区分,C/M 都属于页面模块,F 才是按钮权限。
  // 不能再用“是否有 children”判断,否则没有按钮权限的 C 节点会被过滤掉。
  return (node.children || []).filter(item => item.menuType !== 'F')
  return (node.children || []).filter(item => item.menuType !== 'F' && !isSpecialActionNode(node, item))
}
function getActionNodes(node: PermissionNode) {
  return (node.children || []).filter(item => item.menuType === 'F')
  return (node.children || []).filter(item => item.menuType === 'F' || isSpecialActionNode(node, item))
}
function isModuleExpanded(key: string) {
@@ -374,6 +382,7 @@
  currentModuleKey.value = item.key
  if (item.expandable && !formModel.permissionKeyword.trim())
    toggleModuleExpanded(item.key)
  scrollToPermissionSection(item.key)
}
watch([currentAppTab, visibleModuleItems], () => {
@@ -404,8 +413,9 @@
    const hasChildModules = childModules.length > 0
    const actions = getActionNodes(currentNode)
    // 右侧只展示“有按钮权限”的叶子模块;没有子级菜单/按钮的模块不渲染。
    if (isNodeChecked(currentNode) && !hasChildModules && actions.length > 0) {
    // 右侧展示所有已选中的叶子模块。
    // 如果叶子模块没有按钮权限,也需要展示分组,但不显示“全选”。
    if (isNodeChecked(currentNode) && !hasChildModules) {
      sections.push({
        key: currentNode.key,
        node: currentNode,
@@ -484,6 +494,7 @@
// 勾选父节点时默认勾选全部子权限;取消父节点时同步清空子权限
function handleTogglePermission(node: PermissionNode, checked: boolean) {
  currentModuleKey.value = node.key
  const nextSet = new Set(checkedPermissionKeys.value)
  const hasChildren = !!node.children?.length
  const checkedKeys = hasChildren ? getModuleDescendantKeys(node) : [node.key]
@@ -505,6 +516,38 @@
  }
  checkedPermissionKeys.value = Array.from(nextSet)
  if (checked)
    scrollToPermissionSection(node.key)
}
function isSectionMatchedModule(sectionNode: PermissionNode, moduleKey: string) {
  let currentKey: string | null | undefined = sectionNode.key
  while (currentKey) {
    if (currentKey === moduleKey)
      return true
    currentKey = parentMap.get(currentKey)
  }
  return false
}
function scrollToPermissionSection(moduleKey: string) {
  nextTick(() => {
    const panel = permissionDetailPanelRef.value
    if (!panel)
      return
    const matchedSection = selectedPermissionSections.value.find(section =>
      isSectionMatchedModule(section.node, moduleKey))
    if (!matchedSection) {
      panel.scrollTo({ top: 0, behavior: 'smooth' })
      return
    }
    const sectionElements = Array.from(panel.querySelectorAll<HTMLElement>('.permission-section-row'))
    const targetElement = sectionElements.find(element => element.dataset.sectionKey === matchedSection.key)
    targetElement?.scrollIntoView({ behavior: 'smooth', block: 'start' })
  })
}
function getActionStatus(node: PermissionNode): 'checked' | 'partial' | 'unchecked' {
@@ -559,7 +602,22 @@
const displayedActionNodes = computed(() =>
  selectedPermissionSections.value.flatMap(section => section.actions))
function handleBatchSelectActions(matchers: RegExp[], successText: string) {
function areMatchedActionsAllChecked(matchers: RegExp[]) {
  const matchedActions = displayedActionNodes.value.filter(action =>
    matchers.some(pattern => pattern.test(action.menuName)))
  if (!matchedActions.length)
    return false
  const checkedKeySet = new Set(checkedPermissionKeys.value)
  return matchedActions.every(action => checkedKeySet.has(action.key))
}
function getBatchActionLabel(matchers: RegExp[], actionText: string) {
  return `${areMatchedActionsAllChecked(matchers) ? '批量不选中' : '批量选中'}${actionText}`
}
function handleBatchToggleActions(matchers: RegExp[], successText: string, cancelText: string) {
  if (!displayedActionNodes.value.length)
    return
@@ -572,20 +630,28 @@
  }
  const nextSet = new Set(checkedPermissionKeys.value)
  const allMatchedChecked = matchedActions.every(action => nextSet.has(action.key))
  matchedActions.forEach((action) => {
    if (allMatchedChecked) {
      nextSet.delete(action.key)
      delete permissionConfigMap[action.key]
      return
    }
    nextSet.add(action.key)
    ensurePermissionConfig(action.key)
  })
  checkedPermissionKeys.value = Array.from(nextSet)
  message.success(successText)
  message.success(allMatchedChecked ? cancelText : successText)
}
function handleBatchView() {
  handleBatchSelectActions([/查看/, /详情/], '已批量勾选查看权限')
  handleBatchToggleActions([/查看/, /详情/], '已批量勾选查看权限', '已批量取消查看权限')
}
function handleBatchEdit() {
  handleBatchSelectActions([/编辑/, /修改/], '已批量勾选编辑权限')
  handleBatchToggleActions([/编辑/, /修改/], '已批量勾选编辑权限', '已批量取消编辑权限')
}
function confirmBatchEdit() {
@@ -601,11 +667,11 @@
}
function handleBatchDelete() {
  handleBatchSelectActions([/删除/], '已批量勾选删除权限')
  handleBatchToggleActions([/删除/], '已批量勾选删除权限', '已批量取消删除权限')
}
function handleBatchExport() {
  handleBatchSelectActions([/导出/], '已批量勾选导出权限')
  handleBatchToggleActions([/导出/], '已批量勾选导出权限', '已批量取消导出权限')
}
function handleResetPermissions() {
@@ -801,7 +867,7 @@
              />
            </NFormItem>
            <NFormItem label="手机号是否脱敏" path="phoneDesensitization" class="basic-form-item">
            <NFormItem label="是否显示手机号" path="phoneDesensitization" class="basic-form-item">
              <NSwitch
                v-model:value="formModel.phoneDesensitization"
                :checked-value="1"
@@ -841,16 +907,16 @@
                <div class="permission-config-wrap">
                  <div class="permission-toolbar">
                    <NButton :disabled="batchActionDisabled" @click="handleBatchView">
                      批量选中查看
                      {{ getBatchActionLabel([/查看/, /详情/], '查看') }}
                    </NButton>
                    <NButton :disabled="batchActionDisabled" @click="handleBatchEdit">
                      批量选中编辑
                      {{ getBatchActionLabel([/编辑/, /修改/], '编辑') }}
                    </NButton>
                    <NButton :disabled="batchActionDisabled" @click="handleBatchDelete">
                      批量选中删除
                      {{ getBatchActionLabel([/删除/], '删除') }}
                    </NButton>
                    <NButton :disabled="batchActionDisabled" @click="handleBatchExport">
                      批量选中导出
                      {{ getBatchActionLabel([/导出/], '导出') }}
                    </NButton>
                    <NButton class="w-84px" :disabled="isReadOnlyMode" @click="handleResetPermissions">
                      重置
@@ -894,17 +960,19 @@
                      </div>
                    </div>
                    <div class="permission-detail-panel">
                    <div ref="permissionDetailPanelRef" class="permission-detail-panel">
                      <div v-if="selectedPermissionSections.length" class="permission-detail-inner">
                        <div
                          v-for="section in selectedPermissionSections"
                          :key="section.key"
                          class="permission-section-row"
                          :data-section-key="section.key"
                        >
                          <div class="section-header-row">
                            <div class="section-title-wrap">
                              <span class="section-node-title">{{ section.title }}</span>
                              <NCheckbox
                                v-if="section.actions.length"
                                :checked="getActionStatus(section.node) === 'checked'"
                                :indeterminate="getActionStatus(section.node) === 'partial'"
                                :disabled="isReadOnlyMode"
@@ -915,7 +983,16 @@
                            </div>
                          </div>
                          <div v-if="section.actions.length" class="action-list">
                          <div class="action-list">
                            <div
                              class="action-item"
                            >
                              <NCheckbox
                                checked
                                disabled
                              />
                              <span class="action-name">页面查看</span>
                            </div>
                            <div
                              v-for="action in section.actions"
                              :key="action.key"
@@ -1001,17 +1078,19 @@
                      </div>
                    </div>
                    <div class="permission-detail-panel">
                    <div ref="permissionDetailPanelRef" class="permission-detail-panel">
                      <div v-if="selectedPermissionSections.length" class="permission-detail-inner">
                        <div
                          v-for="section in selectedPermissionSections"
                          :key="section.key"
                          class="permission-section-row"
                          :data-section-key="section.key"
                        >
                          <div class="section-header-row">
                            <div class="section-title-wrap">
                              <span class="section-node-title">{{ section.title }}</span>
                              <NCheckbox
                                v-if="section.actions.length"
                                :checked="getActionStatus(section.node) === 'checked'"
                                :indeterminate="getActionStatus(section.node) === 'partial'"
                                :disabled="isReadOnlyMode"
@@ -1022,7 +1101,16 @@
                            </div>
                          </div>
                          <div v-if="section.actions.length" class="action-list">
                          <div class="action-list">
                            <div
                              class="action-item"
                            >
                              <NCheckbox
                                checked
                                disabled
                              />
                              <span class="action-name">页面查看</span>
                            </div>
                            <div
                              v-for="action in section.actions"
                              :key="action.key"
@@ -1088,7 +1176,7 @@
.role-form-page {
  display: flex;
  flex-direction: column;
  height: calc(100vh - 88px);
  height: calc(100vh - 88px - 28px);
  box-sizing: border-box;
}
src/views/testDemo/operationWorkbench/components/FencePanel.vue
@@ -1627,6 +1627,7 @@
  color: #0068ff;
  font-weight: 400;
  background: #e6f4ff;
  flex-shrink: 0;
  &.wait-test {
    background: #fff6ea;
src/views/testDemo/operationWorkbench/list.vue
@@ -82,7 +82,7 @@
const fenceStatusOptions = [
  { label: '草稿', value: 0 },
  { label: '待生效', value: 1 },
  // { label: '待生效', value: 1 },
  { label: '启用中', value: 2 },
  { label: '已停用', value: 3 },
  { label: '待测试', value: 4 },
@@ -2440,7 +2440,7 @@
              <NButton v-permission="2026061505" text type="primary" disabled={row.flowStatus === 5 } onClick={() => handleFenceToDetail(row, 'edit')}>
                编辑
              </NButton>
              <NButton v-permission="2026061506" text type="primary" disabled={row.flowStatus === 5 } onClick={() => handleFenceCopy(row)}>
              <NButton v-permission="2026061506" text type="primary" onClick={() => handleFenceCopy(row)}>
                复制
              </NButton>
            </NSpace>
src/views/testDemo/operationWorkbench/map.scss
@@ -739,6 +739,52 @@
  }
}
.fence-hover-tooltip {
  position: absolute;
  z-index: 5;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 88px;
  max-width: 160px;
  padding: 7px 10px;
  border-radius: 6px;
  color: #fff;
  font-size: 13px;
  line-height: 18px;
  white-space: nowrap;
  pointer-events: none;
  transform: translate(-50%, -100%);
  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
  &.is-enabled {
    background: #19b144;
  }
  &.is-disabled {
    background: #8f959e;
  }
  &.is-testing {
    background: #171f26;
  }
}
.fence-hover-tooltip__text {
  overflow: hidden;
  text-overflow: ellipsis;
}
.fence-hover-tooltip__arrow {
  position: absolute;
  bottom: -5px;
  left: 50%;
  width: 10px;
  height: 10px;
  background: inherit;
  transform: translateX(-50%) rotate(45deg);
}
.map-tips {
  position: absolute;
  top: 46px;
src/views/testDemo/operationWorkbench/map.vue
@@ -511,7 +511,7 @@
      'white-space: normal',
      'word-break: break-word',
    ].join(';')
    text.innerHTML = '拖动橙色实心点增加顶点<br />点击白色实心点删除顶点'
    text.innerHTML = `拖动${fenceCreateMode.value ? '橙色' : '蓝色'}实心点增加顶点<br />点击白色实心点删除顶点`
    const arrow = document.createElement('div')
    arrow.style.cssText = [
      'position: absolute',
@@ -540,6 +540,10 @@
    return
  }
  fenceEditGuideMarker.value.setPosition([lng, lat])
  const guideTextElement = fenceEditGuideContentElement?.querySelector?.('.fence-edit-guide__text') as HTMLDivElement | null
  if (guideTextElement) {
    guideTextElement.innerHTML = `拖动${fenceCreateMode.value ? '橙色' : '蓝色'}实心点增加顶点<br />点击白色实心点删除顶点`
  }
  applyFenceEditGuidePlacement(layout.placement, layout.offset)
}
@@ -1082,13 +1086,25 @@
const cityBoundaryHoverVisible = ref(false)
const cityBoundaryHoverText = ref('')
const cityBoundaryHoverOutsideSelected = ref(false)
const fenceHoverTooltipVisible = ref(false)
const fenceHoverTooltipText = ref('')
const fenceHoverTooltipType = ref<'enabled' | 'disabled' | 'testing'>('testing')
const fenceCreateOutsideCurrentCity = ref(false)
const hoveredFencePolygon = shallowRef<any | null>(null)
const hoveredFenceBaseStrokeWeight = ref<number | null>(null)
const cityBoundaryHoverPosition = reactive({
  x: 0,
  y: 0,
})
const fenceHoverTooltipPosition = reactive({
  x: 0,
  y: 0,
})
let cityBoundaryHoverQueryTimer: ReturnType<typeof setTimeout> | null = null
let cityBoundaryHoverQuerySeq = 0
let fenceHoverTooltipTimer: ReturnType<typeof setTimeout> | null = null
let pendingFenceHoverKey = ''
let pendingFenceHoverPixel: { x: number, y: number } | null = null
const cityBoundaryHoverTextCache = new Map<string, string>()
let skipFenceCreateCityChangeConfirm = false
let fenceCreateBoundaryModalOpening = false
@@ -1509,8 +1525,133 @@
  }
}
function clearFenceHoverTooltipTimer() {
  if (fenceHoverTooltipTimer) {
    clearTimeout(fenceHoverTooltipTimer)
    fenceHoverTooltipTimer = null
  }
}
function resetFenceHoverPolygonStyle() {
  if (hoveredFencePolygon.value && hoveredFenceBaseStrokeWeight.value !== null) {
    hoveredFencePolygon.value.setOptions({
      strokeWeight: hoveredFenceBaseStrokeWeight.value,
    })
  }
  hoveredFencePolygon.value = null
  hoveredFenceBaseStrokeWeight.value = null
}
function highlightFenceHoverPolygon(polygon: any, item: any) {
  if (Number(item?.areaType) !== 1)
    return
  if (hoveredFencePolygon.value === polygon)
    return
  resetFenceHoverPolygonStyle()
  const currentStrokeWeight = Number(polygon?.getExtData?.()?.baseStrokeWeight ?? 2)
  hoveredFencePolygon.value = polygon
  hoveredFenceBaseStrokeWeight.value = currentStrokeWeight
  polygon.setOptions({
    strokeWeight: currentStrokeWeight + 1.5,
  })
}
function hideFenceHoverTooltip() {
  clearFenceHoverTooltipTimer()
  pendingFenceHoverKey = ''
  pendingFenceHoverPixel = null
  resetFenceHoverPolygonStyle()
  fenceHoverTooltipVisible.value = false
  fenceHoverTooltipText.value = ''
  fenceHoverTooltipType.value = 'testing'
}
function getFenceHoverKey(item: any) {
  return `${String(item?.electronicId || '')}-${String(item?.id || '')}`
}
function resolveFenceOverlayType(item: any) {
  if (Number(item?.flowStatus) === 5 && Number(item?.status) === 2)
    return 'enabled' as const
  if (Number(item?.flowStatus) === 5 && Number(item?.status) === 3)
    return 'disabled' as const
  return 'testing' as const
}
function getFenceHoverTooltipText(type: 'enabled' | 'disabled' | 'testing') {
  if (type === 'enabled')
    return '启用中围栏'
  if (type === 'disabled')
    return '已停用围栏'
  return '测试围栏'
}
function parseFencePriorityTime(value: unknown) {
  const timestamp = dayjs(String(value || '')).valueOf()
  return Number.isFinite(timestamp) ? timestamp : 0
}
function getFenceOverlayPriority(item: any) {
  const type = resolveFenceOverlayType(item)
  return {
    type,
    typePriority: type === 'testing' ? 2 : type === 'enabled' ? 1 : 0,
    updateTimePriority: parseFencePriorityTime(item?.updateTime),
    fallbackPriority: Number(item?.electronicId ?? item?.id ?? 0) || 0,
  }
}
function updateFenceHoverTooltipPositionByPixel(pixel?: { x: number, y: number } | null) {
  if (!map.value || !pixel)
    return false
  const container = map.value.getContainer?.()
  if (!container)
    return false
  const tooltipWidth = 88
  const offsetX = 1
  const offsetY = 7
  const rawX = Number(pixel.x) + offsetX
  const rawY = Number(pixel.y) - offsetY
  const clampedX = Math.max(12, Math.min(rawX, container.clientWidth - 12))
  const clampedY = Math.max(12, Math.min(rawY, container.clientHeight - 12))
  fenceHoverTooltipPosition.x = clampedX
  fenceHoverTooltipPosition.y = clampedY
  return true
}
function scheduleFenceHoverTooltip(item: any, pixel?: { x: number, y: number } | null) {
  if (Number(item?.areaType) !== 1 || isFenceFormMode.value || isStationFormMode.value)
    return
  const hoverKey = getFenceHoverKey(item)
  pendingFenceHoverKey = hoverKey
  pendingFenceHoverPixel = pixel
  updateFenceHoverTooltipPositionByPixel(pixel)
  clearFenceHoverTooltipTimer()
  fenceHoverTooltipTimer = window.setTimeout(() => {
    if (pendingFenceHoverKey !== hoverKey)
      return
    const nextType = resolveFenceOverlayType(item)
    if (!updateFenceHoverTooltipPositionByPixel(pendingFenceHoverPixel))
      return
    fenceHoverTooltipType.value = nextType
    fenceHoverTooltipText.value = getFenceHoverTooltipText(nextType)
    fenceHoverTooltipVisible.value = true
    fenceHoverTooltipTimer = null
  }, 300)
}
function handleCityBoundaryMouseOut() {
  hideCityBoundaryHoverTooltip()
  hideFenceHoverTooltip()
  resetFenceEditGuideHoverState()
  resetRestrictedEditGuideHoverState()
}
@@ -2056,10 +2197,10 @@
  )
  if (activeRestrictedArea) {
    if (activeRestrictedArea.points.length < 3)
      return '请在地图上【橙色运营区域】绘制多边形区域,双击闭合'
      return `请在地图上【${getFenceDraftAccentLabel()}运营区域】绘制多边形区域,双击闭合`
    if (isFenceGeometryLocked.value)
      return '【红色禁行区】已选中'
    return '【红色禁行区】可调整区域点位,请保持在橙色运营区内'
    return `【红色禁行区】可调整区域点位,请保持在${getFenceDraftAccentLabel()}运营区内`
  }
  return ''
})
@@ -3207,6 +3348,14 @@
  })
}
function getFenceDraftAccentColor() {
  return fenceCreateMode.value ? '#F19216' : '#2F7BFF'
}
function getFenceDraftAccentLabel() {
  return fenceCreateMode.value ? '橙色' : '蓝色'
}
function syncFenceStationWarningState(points: LngLatTuple[] = fenceDraftPoints.value) {
  invalidFenceStationIds.value = []
  setFenceDraftPolygonStyle(fenceInteractionMode.value === 'operation-edit')
@@ -3216,9 +3365,10 @@
function setFenceDraftPolygonStyle(editable: boolean) {
  if (!fenceDraftPolygon.value)
    return
  const accentColor = getFenceDraftAccentColor()
  fenceDraftPolygon.value.setOptions({
    fillColor: '#F19216',
    strokeColor: '#F19216',
    fillColor: accentColor,
    strokeColor: accentColor,
    strokeWeight: 2,
    strokeOpacity: 1,
    fillOpacity: 0.1,
@@ -3276,10 +3426,11 @@
    return
  if (fenceDraftPolygon.value)
    map.value.remove(fenceDraftPolygon.value)
  const accentColor = getFenceDraftAccentColor()
  fenceDraftPolygon.value = new (window as any).AMap.Polygon({
    path: points,
    fillColor: '#F19216',
    strokeColor: '#F19216',
    fillColor: accentColor,
    strokeColor: accentColor,
    strokeWeight: 2,
    strokeOpacity: 1,
    fillOpacity: 0.1,
@@ -3312,12 +3463,12 @@
    fenceDraftPolygon.value,
    {
      controlPoint: {
        strokeColor: '#F19216',
        strokeColor: getFenceDraftAccentColor(),
        strokeOpacity: 1,
        strokeWeight: 2,
      },
      midControlPoint: { fillColor: '#F19216' },
      editOptions: { fillColor: '#F19216' },
      midControlPoint: { fillColor: getFenceDraftAccentColor() },
      editOptions: { fillColor: getFenceDraftAccentColor() },
    },
  )
  fencePolyEditor.value.open()
@@ -3392,12 +3543,13 @@
    return
  cleanupFenceEditorState()
  lastValidFenceDrawingPoints.value = []
  const accentColor = getFenceDraftAccentColor()
  fenceMouseTool.value = new (window as any).AMap.MouseTool(map.value)
  fenceMouseTool.value.polygon({
    strokeColor: '#F19216',
    strokeColor: accentColor,
    strokeOpacity: 1,
    strokeWeight: 2,
    fillColor: '#F19216',
    fillColor: accentColor,
    fillOpacity: 0.1,
    strokeStyle: 'solid',
    cursor: 'pointer',
@@ -8526,6 +8678,7 @@
function clearMapEntities() {
  if (!map.value)
    return
  hideFenceHoverTooltip()
  if (pointMarkers.value.length)
    map.value.remove(pointMarkers.value)
  if (cityBoundaryPolygons.value.length)
@@ -8622,6 +8775,22 @@
        }
        return false
      })
      .sort((prev: any, next: any) => {
        const prevPriority = getFenceOverlayPriority(prev)
        const nextPriority = getFenceOverlayPriority(next)
        if (prevPriority.typePriority !== nextPriority.typePriority)
          return prevPriority.typePriority - nextPriority.typePriority
        if (
          prevPriority.type === 'testing'
          && nextPriority.type === 'testing'
          && prevPriority.updateTimePriority !== nextPriority.updateTimePriority
        ) {
          return prevPriority.updateTimePriority - nextPriority.updateTimePriority
        }
        if (prevPriority.updateTimePriority !== nextPriority.updateTimePriority)
          return prevPriority.updateTimePriority - nextPriority.updateTimePriority
        return prevPriority.fallbackPriority - nextPriority.fallbackPriority
      })
      .map((item) => {
        const areaType = Number(item.areaType)
        const points = String(item.fencePoints)
@@ -8663,9 +8832,21 @@
        const isFenceEnabled
          = areaType === 1
            && enabledFenceIdSet.value.has(String(item.electronicId))
        const fencePriority = getFenceOverlayPriority(item)
        // 非启用中的选中围栏使用蓝色选中态;启用中的围栏即使被选中也保持绿色。
        const isFenceSelectedBlue = isFenceDetailActive && !isFenceEnabled
        const isFenceHighlighted = isFenceDetailActive || isFenceEnabled
        const baseStrokeWeight = areaType === 2
          ? 0
          : isFenceSelectedBlue
              ? 3
              : isFenceFlowCompletedDisabled
                ? 2
                : isFenceHighlighted
                  ? 2
                  : isActive
                    ? 3
                    : 2
        const polygon = new (window as any).AMap.Polygon({
          path: points,
          // 填充颜色
@@ -8699,17 +8880,7 @@
                      ? '#171F26'
                      : '#AEB0B2',
          // 线宽度
          strokeWeight: areaType === 2
            ? 0
            : isFenceSelectedBlue
              ? 3
              : isFenceFlowCompletedDisabled
                ? 2
                : isFenceHighlighted
                  ? 2
                  : isActive
                    ? 3
                    : 2,
          strokeWeight: baseStrokeWeight,
          // 线透明度
          strokeOpacity: areaType === 2 ? 0 : isFenceFlowCompletedDisabled ? 0.3 : isFenceHighlighted ? 1 : isFenceReadonlyDimmed ? 0.2 : 1,
          // 填充透明度
@@ -8741,9 +8912,12 @@
            && !isFenceCreateInteractionLocked.value
            && !isFenceFormMode.value
            && !isStationFormMode.value,
          zIndex: isReadonlyRestrictedActive ? 230 : isActive ? 220 : 205,
          zIndex: areaType === 2
            ? (isReadonlyRestrictedActive ? 230 : 205)
            : 240 + fencePriority.typePriority * 40 + (isActive ? 8 : 0),
        })
        polygon.on('click', () => {
          hideFenceHoverTooltip()
          if (
            isPointMapPickActive.value
            || isFenceCreateInteractionLocked.value
@@ -8782,11 +8956,25 @@
            renderMapEntities()
          handleMapEntityClick('fence', String(item.electronicId))
        })
        polygon.on('mouseover', (event: any) => {
          highlightFenceHoverPolygon(polygon, item)
          scheduleFenceHoverTooltip(item, event?.pixel || null)
        })
        polygon.on('mousemove', (event: any) => {
          pendingFenceHoverPixel = event?.pixel || null
          updateFenceHoverTooltipPositionByPixel(pendingFenceHoverPixel)
        })
        polygon.on('mouseout', () => {
          if (pendingFenceHoverKey === getFenceHoverKey(item))
            pendingFenceHoverKey = ''
          hideFenceHoverTooltip()
        })
        polygon.setExtData({
          id: String(item.electronicId),
          type: 'fence',
          areaType,
          areaId: String(item.id || ''),
          baseStrokeWeight,
        })
        return polygon
      })
@@ -9370,6 +9558,18 @@
          >
          {{ cityBoundaryHoverText }}
        </div>
        <div
          v-if="fenceHoverTooltipVisible"
          class="fence-hover-tooltip"
          :class="`is-${fenceHoverTooltipType}`"
          :style="{
            left: `${fenceHoverTooltipPosition.x}px`,
            top: `${fenceHoverTooltipPosition.y}px`,
          }"
        >
          <span class="fence-hover-tooltip__text">{{ fenceHoverTooltipText }}</span>
          <span class="fence-hover-tooltip__arrow" />
        </div>
        <MapInfoPopupCard
          v-if="showFenceEditMapInfoPopup"
          :style="mapInfoPopupStyle"
@@ -9463,7 +9663,7 @@
                    class="operate-item create-tool-trigger"
                    :class="{
                      'active': showCreateMenu,
                      'disabled': !!createToolDisabledReason,
                      // 'disabled': !!createToolDisabledReason,
                      'has-selection':
                        !showCreateMenu && !!selectedCreateToolOption,
                    }"
src/views/testDemo/operationWorkbench/workbench.constants.ts
@@ -77,9 +77,9 @@
    { label: "待测试", value: "wait-test" },
    { label: "测试中", value: "testing" },
    { label: "测试不通过", value: "test-fail" },
    { label: "可运营", value: "operable" },
    // { label: "可运营", value: "operable" },
    { label: "草稿", value: "draft" },
    { label: "待生效", value: "pending" },
    // { label: "待生效", value: "pending" },
    { label: "启用中", value: "enabled" },
    { label: "已停用", value: "disabled" },
  ],
src/views/testDemo/testPlanManagement/components/PointSelectModal.vue
@@ -6,7 +6,7 @@
import { NButton, NDataTable, NForm, NFormItem, NGi, NInput, NModal, NSelect } from 'naive-ui'
import SearchPanel from '@/components/custom/SearchPanel.vue'
import Pagination from '@/components/common/Pagination.vue'
import { fetchPointPageList } from '@/service/api/testOperationWorkbench'
import { fetchFenceAndStationAreaList, fetchPointPageList } from '@/service/api/testOperationWorkbench'
import { getMultiSelectRowClassName, shouldIgnoreTableRowClick, toggleMultiSelectRowKey } from '@/utils/table'
const props = withDefaults(defineProps<{
@@ -30,13 +30,24 @@
  id: string
  pointName: string
  address: string
  pointArea: string
  pointType: number // 1: 运营, 2: 测试
  status: string
}
interface PointAreaOption {
  label: string
  value: string
  type: number
  name: string
  ids: number | string
}
const model = ref({
  pointName: '',
  pointType: null as number | null,
  operationStatusList: null as number[] | null,
  fenceAndStationList: [] as string[],
})
const pageParams = ref({
@@ -49,15 +60,79 @@
  { label: '测试', value: 2 },
]
const pointStatusOptions = [
  { label: '待测试', value: 1 },
  { label: '测试中', value: 2 },
  { label: '测试不通过', value: 3 },
  { label: '可运营', value: 4 },
  { label: '待生效', value: 5 },
  { label: '启用中', value: 6 },
  { label: '已停用', value: 7 },
  { label: '已失效', value: 8 },
]
const listData = ref<PointRow[]>([])
const checkedRowKeys = ref<DataTableRowKey[]>([])
const count = ref(0)
const loading = ref(false)
const allLoadedData = ref<Map<DataTableRowKey, PointRow>>(new Map())
const pointAreaOptions = ref<PointAreaOption[]>([])
const saveDisabled = computed(() => checkedRowKeys.value.length === 0)
const tableScrollX = 900
function buildPointAreaOptionValue(item: { type: number, ids: number | string }) {
  return `${Number(item.type)}_${String(item.ids)}`
}
function getSelectedPointAreaList() {
  const selectedValueSet = new Set(model.value.fenceAndStationList || [])
  return pointAreaOptions.value
    .filter(item => selectedValueSet.has(item.value))
    .map(item => ({
      type: item.type,
      name: item.name,
      ids: item.ids,
    }))
}
function getPointAreaText(row?: any) {
  const electronicName = String(row?.electronicName || '').trim()
  const stationName = String(row?.stationName || '').trim()
  if (electronicName && stationName)
    return `[区]${electronicName} , [场]${stationName}`
  if (electronicName)
    return `[区]${electronicName}`
  if (stationName)
    return `[场]${stationName}`
  return '-'
}
function isPointDisabled(row?: Partial<PointRow> | null) {
  return String(row?.status || '').trim() === '已停用'
}
async function loadPointAreaOptions() {
  try {
    const res = await fetchFenceAndStationAreaList()
    const list = Array.isArray(res.data) ? res.data : []
    pointAreaOptions.value = list
      .filter((item: any) => item && item.type && item.name !== undefined && item.ids !== undefined)
      .map((item: any) => ({
        label: `${Number(item.type) === 1 ? '围栏' : '场站'}-${String(item.name || '-')}`,
        value: buildPointAreaOptionValue(item),
        type: Number(item.type),
        name: String(item.name || ''),
        ids: item.ids,
      }))
  }
  catch (error) {
    console.error('加载所属区域失败:', error)
    pointAreaOptions.value = []
  }
  const optionValueSet = new Set(pointAreaOptions.value.map(item => item.value))
  model.value.fenceAndStationList = (model.value.fenceAndStationList || []).filter(value => optionValueSet.has(value))
}
async function getList() {
@@ -66,6 +141,8 @@
    const res = await fetchPointPageList({
      pointName: model.value.pointName.trim() || undefined,
      pointType: model.value.pointType || undefined,
      operationStatusList: model.value.operationStatusList?.length ? model.value.operationStatusList : undefined,
      fenceAndStationList: getSelectedPointAreaList(),
      pageNum: pageParams.value.pageNum,
      pageSize: pageParams.value.pageSize,
    })
@@ -79,6 +156,7 @@
      id: String(item.id),
      pointName: item.pointName || '-',
      address: item.pointAddress || '-',
      pointArea: getPointAreaText(item),
      pointType: Number(item.pointType ?? 0),
      status: getPointStatusText(item.operationStatus ?? item.operationStatus ?? 0, item.verificationStatus),
    }))
@@ -107,12 +185,13 @@
  (val) => {
    if (val) {
      checkedRowKeys.value = props.existingPoints.map(item => item.id)
      model.value = { pointName: '', pointType: null }
      model.value = { pointName: '', pointType: null, operationStatusList: null, fenceAndStationList: [] }
      pageParams.value = { pageNum: 1, pageSize: 10 }
      allLoadedData.value.clear()
      props.existingPoints.forEach((item) => {
        allLoadedData.value.set(item.id, item)
      })
      loadPointAreaOptions()
      getList()
    }
    else {
@@ -128,7 +207,7 @@
}
function handleReset() {
  model.value = { pointName: '', pointType: null }
  model.value = { pointName: '', pointType: null, operationStatusList: null, fenceAndStationList: [] }
  pageParams.value.pageNum = 1
  getList()
}
@@ -188,11 +267,12 @@
    disabled: row => isPointDisabled(row),
  },
  { title: '序号', key: 'index', width: 80, render: (_, index) => (pageParams.value.pageNum - 1) * pageParams.value.pageSize + index + 1 },
  { title: '点位名称', key: 'pointName', width: 250 },
  { title: '详细地址', key: 'address', flex: 1 },
  { title: '点位名称', key: 'pointName', width: 160,ellipsis:{tooltip:true} },
  { title: '所属区域', key: 'pointArea', width: 160,ellipsis:{tooptip:true} },
  { title: '详细地址', key: 'address', width: 220 ,ellipsis:{tooltip:true}},
  { title: '点位类型', key: 'pointType', width: 120, render: row => row.pointType === 2 ? '测试' : '运营' },
  {
    title: '状态',
    title: '点位状态',
    key: 'status',
    width: 100,
    render: (row) => {
@@ -230,6 +310,25 @@
                />
              </NFormItem>
            </NGi>
            <NGi>
              <NFormItem label="所属区域" path="fenceAndStationList">
                <NSelect
                  v-model:value="model.fenceAndStationList"
                  :options="pointAreaOptions"
                  placeholder="请选择"
                  clearable
                  multiple
                  max-tag-count="responsive"
                >
                  <template #empty>
                    <div class="flex-center">
                      <img :src="noDataImg" style="width: 50%" alt="no-data">
                    </div>
                  </template>
                </NSelect>
              </NFormItem>
            </NGi>
            <NGi>
              <NFormItem label="点位类型" path="pointType">
                <NSelect
@@ -237,6 +336,24 @@
                  :options="pointTypeOptions"
                  placeholder="请选择"
                  clearable
                >
                  <template #empty>
                    <div class="flex-center">
                      <img :src="noDataImg" style="width: 50%" alt="no-data">
                    </div>
                  </template>
                </NSelect>
              </NFormItem>
            </NGi>
            <NGi>
            <NFormItem label="点位状态" path="operationStatusList">
                <NSelect
                  v-model:value="model.operationStatusList"
                  :options="pointStatusOptions"
                  placeholder="请选择"
                  clearable
                  multiple
                  max-tag-count="responsive"
                >
                  <template #empty>
                    <div class="flex-center">
@@ -268,6 +385,7 @@
          v-model:checked-row-keys="checkedRowKeys"
          style="height: 100%"
          flex-height
          :scroll-x="tableScrollX"
          :columns="columns"
          :data="listData"
          :loading="loading"
src/views/transaction/order/orderManagement/index.vue
@@ -5,6 +5,7 @@
import { usePermission } from '@/hooks/usePermission'
import { cancelAndCloseOrder, deleteOrder, exportOrderExcel, getFleetList, getOrderList, refundOrder } from '@/service/api/transaction'
import { operationPageList } from '@/service/api/vehicleConvoy'
import { fetchVehiclePageList } from '@/service/api/fleet'
import { getRegionCascaderNoArea } from '@/service/api/operations'
import { NButton, NSpace, NTag, useDialog, useMessage } from 'naive-ui'
import SearchPanel from '@/components/custom/SearchPanel.vue'
@@ -490,7 +491,7 @@
    render: () => <n-input v-model:value={model.value.nickName} />,
  },
  {
    label: t('transaction.vehicleNumber'),
    label: '接单车辆',
    path: 'plateNumber',
    render: () => (
      <n-select
@@ -787,7 +788,7 @@
    plateNumberOptions.value = []
    return
  }
  const res = await operationPageList({
  const res = await fetchVehiclePageList({
    pageNum: 1,
    pageSize: 99999,
  })
src/views/userContent/contentCenter/agreementVersion.vue
@@ -608,7 +608,7 @@
        <n-form label-placement="left" require-mark-placement="left">
          <div class="form-row">
            <n-form-item label="协议名称" required class="agreement-name-item">
              <NInput v-model:value="agreementName" placeholder="请输入" maxlength="20" clearable class="w-236px" />
              <NInput v-model:value="agreementName" placeholder="请输入" maxlength="30" clearable class="w-236px" />
            </n-form-item>
          </div>
        </n-form>
src/views/userContent/contentCenter/components/AgreementVersionModal.vue
@@ -49,8 +49,8 @@
      validator: () => {
        if (!plainTextLength.value)
          return new Error('请输入协议内容')
        if (plainTextLength.value > 200)
          return new Error('协议内容最多输入200个字符')
        if (plainTextLength.value > 5000)
          return new Error('协议内容最多输入5000个字符')
        return true
      },
      trigger: ['blur', 'change', 'input'],