董国庆
2026-06-05 0bf152e9b1fb6e68fd87f21e7dfaee4cd7c4ee8a
demo计划引入接口
5个文件已修改
1606 ■■■■■ 已修改文件
src/service/api/testPlanManagement.ts 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanManagement/api.txt 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanManagement/components/AssignVehicleModal.vue 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanManagement/components/ManageTestVehicleModal.vue 300 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanManagement/detail.vue 861 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/testPlanManagement.ts
@@ -73,3 +73,96 @@
}) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/start', data)
}
export function fetchTestPlanMonitorDetail(testPlanId: number | string) {
  return request.Get<Service.ResponseResult<any>>(`/operation/api/v1/test-plan-demo/detail/monitor/${testPlanId}`)
}
export function interruptTestPlanDispatch(planId: number | string) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/dispatch/interrupt', null, {
    params: { planId },
  })
}
export function resumeTestPlanDispatch(planId: number | string) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/dispatch/resume', null, {
    params: { planId },
  })
}
export function fetchTestPlanDetailOrderPageList(data: {
  dispatchEndTime?: string
  dispatchStartTime?: string
  dispatchStatus?: number
  orderNumber?: string
  pageNum: number
  pageSize: number
  testPlanId: number | string
  vehicleId?: number | string
}) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/detail/pageOrders', data)
}
export function exportTestPlanDetailOrders(data: {
  dispatchEndTime?: string
  dispatchStartTime?: string
  dispatchStatus?: number
  orderNumber?: string
  pageNum?: number
  pageSize?: number
  testPlanId: number | string
  vehicleId?: number | string
}) {
  const method = request.Post<Blob>('/operation/api/v1/test-plan-demo/detail/exportOrders', data, {
    responseType: 'blob',
  })
  method.meta = { ...method.meta, isBlob: true }
  return method
}
export function cancelTestPlanOrder(orderId: number | string, testPlanId: number | string) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/detail/order/cancel', null, {
    params: { orderId, testPlanId },
  })
}
export function closeTestPlanOrder(orderId: number | string, planId: number | string) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/order/close', null, {
    params: { orderId, planId },
  })
}
export function reassignTestPlanOrder(data: {
  orderId: number | string
  planId: number | string
  vehicleId: number | string
}) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/order/reassign', data)
}
export function fetchTestPlanVehiclePageList(data: {
  pageNum: number
  pageSize: number
  plateNumber?: string
  testPlanId: number | string
  vehicleCode?: string
  vehicleStatus?: number
}) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/detail/pageVehicles', data)
}
export function pauseTestPlanVehicles(data: {
  testPlanId: number | string
  vehicleIds: Array<number | string>
  paused: 0 | 1
}) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/detail/vehicle/pause', data)
}
export function manageTestPlanVehicles(data: {
  addVehicleIds?: Array<number | string>
  planId: number | string
  removeVehicleIds?: Array<number | string>
}) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/vehicle/manage', data)
}
src/views/testDemo/testPlanManagement/api.txt
@@ -184,4 +184,167 @@
  "vehicleIds": [ 测试车辆ID列表
    0
  ]
}
详情接口:
/api/v1/test-plan-demo/detail/monitor/{testPlanId}   下发状态+订单统计+车辆状态统计  get
响应:{
  "code": 0,
  "data": {
    "dispatchStatus": 0, 下发状态 1=下发中 2=已中断 3=已完成
    "dispatchStatusName": "string", 下发状态文案
    "interruptAllowed": true, 是否允许中断下发
    "issuedCount": 0, 已下发数量
    "testPlanId": 0,
    "totalCount": 0, 订单总数
    "unissuedCount": 0, 未下发数量
    "vehicleStatusSummary": {
      "cruising": 0,
      "idle": 0,
      "servicePaused": 0,
      "serving": 0,
      "total": 0
    }
  },
  "msg": "string"
}
中断下发(冻结所有待下发订单) /api/v1/test-plan-demo/dispatch/interrupt  post  params
请求: planId
继续下发(恢复已冻结订单)  ​/api​/v1​/test-plan-demo​/dispatch​/resume  post  params
请求: planId
​/api​/v1​/test-plan-demo​/detail​/pageOrders  订单分页  post  json
请求:{
  "dispatchEndTime": "string",
  "dispatchStartTime": "string",
  "dispatchStatus": 0,
  "orderNumber": "string",
  "pageNum": 0,
  "pageSize": 0,
  "testPlanId": 0,
  "vehicleId": 0
}
响应:{
  "code": 0,
  "data": {
    "current": 0,
    "hasNextPage": true,
    "hasPrevPage": true,
    "pages": 0,
    "records": [
      {
        "actionCodes": [
          "string"
        ],
        "dispatchStatus": 0,
        "dispatchTime": "string",
        "endAddress": "string",
        "fleetId": 0,
        "fleetName": "string",
        "orderId": 0,
        "orderNumber": "string",
        "orderSource": 0,
        "orderStatus": 0,
        "orderStatusName": "string",
        "plateNumber": "string",
        "startAddress": "string",
        "vehicleId": 0
      }
    ],
    "size": 0,
    "startIndex": 0,
    "total": 0
  },
  "msg": "string"
}
导出订单列表  /api/v1/test-plan-demo/detail/exportOrders  post  json
请求:
取消订单  /api/v1/test-plan-demo/detail/order/cancel  post  params
请求:  orderId    testPlanId
关闭订单(仅服务中/待客下车的已下发订单可操作) /api/v1/test-plan-demo/order/close  post  params
请求: orderId   planId
改派/分派订单  ​/api​/v1​/test-plan-demo​/order​/reassign  post  json
请求: {
  "orderId": 0, 要改派的订单ID
  "planId": 0, 测试计划ID
  "vehicleId": 0 改派后的车辆ID
}
删除订单   /api/v1/test-plan-demo/order/delete   post  json
请求:{
  "orderIds": [
    0
  ],
  "planId": 0
}
车辆状态分页  /api/v1/test-plan-demo/detail/pageVehicles  post  json
请求:
{
  "pageNum": 0,
  "pageSize": 0,
  "plateNumber": "string",
  "testPlanId": 0,
  "vehicleCode": "string",
  "vehicleStatus": 0 车辆状态 0=空闲 1=服务中 2=巡游中 3=暂停服务,不传查全部
}
响应:{
  "code": 0,
  "data": {
    "current": 0,
    "hasNextPage": true,
    "hasPrevPage": true,
    "pages": 0,
    "records": [
      {
        "fleetId": 0,
        "fleetName": "string",
        "paused": 0,
        "plateNumber": "string",
        "vehicleCode": "string",
        "vehicleId": 0,
        "vehicleModel": "string",
        "vehicleStatus": 0,
        "vehicleStatusName": "string"
      }
    ],
    "size": 0,
    "startIndex": 0,
    "total": 0
  },
  "msg": "string"
}
暂停/恢复车辆(paused=1暂停,0恢复) /api/v1/test-plan-demo/detail/vehicle/pause  post  json
请求:{
  testPlanId:‘, 测试计划ID
  vehicleIds:[], 车辆ID列表
  paused:0 paused=1暂停,0恢复
}
管理测试车辆(增删,越权校验:只能操作属于该计划的车辆) /api​/v1​/test-plan-demo​/vehicle​/manage  post  json
请求: {
  "addVehicleIds": [
    0
  ],
  "planId": 0,
  "removeVehicleIds": [
    0
  ]
}
src/views/testDemo/testPlanManagement/components/AssignVehicleModal.vue
@@ -1,124 +1,137 @@
<script setup lang="tsx">
import type { DataTableColumns } from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { NModal, NCard, NInput, NButton, NDataTable, NSelect, NTag } from 'naive-ui'
import { NButton, NCard, NDataTable, NInput, NModal, NSelect, NTag, useMessage } from 'naive-ui'
import Pagination from '@/components/common/Pagination.vue'
import { fetchTestPlanVehiclePageList, reassignTestPlanOrder } from '@/service/api/testPlanManagement'
interface VehicleRow {
  id: number
  id: number | string
  vehicleCode: string
  plateNo: string
  model: string
  procurementSource: string
  fleet: string
  status: number
  statusName: string
}
const props = withDefaults(defineProps<{
const props = defineProps<{
  visible: boolean
}>(), {})
  planId?: number | string
  orderId?: number | string | null
}>()
const emit = defineEmits<{
  (e: 'update:visible', val: boolean): void
  (e: 'confirm', data: VehicleRow[]): void
  (e: 'success'): void
}>()
const message = useMessage()
const show = computed({
  get: () => props.visible,
  set: (val) => emit('update:visible', val)
  set: val => emit('update:visible', val),
})
const model = ref({
  plateNo: '',
  vehicleCode: '',
  status: null
  status: null as number | null,
})
const pageParams = ref({
  pageNum: 1,
  pageSize: 10
  pageSize: 10,
})
const listData = ref<VehicleRow[]>([])
const checkedRowKeys = ref<number[]>([])
const checkedRowKeys = ref<Array<number | string>>([])
const count = ref(0)
const loading = ref(false)
const allLoadedData = ref<Map<number, VehicleRow>>(new Map())
const submitting = ref(false)
const isStatusSelectOpen = ref(false)
const statusOptions = [
  { label: '行程中', value: 1 },
  { label: '空闲', value: 2 },
  { label: '空闲', value: 0 },
  { label: '服务中', value: 1 },
  { label: '巡游中', value: 2 },
  { label: '暂停服务', value: 3 },
]
function getStatusColor(status: number) {
  switch (status) {
    case 1: // 行程中
      return { textColor: '#0068FF', color: '#E6F4FF' }
    case 2: // 空闲
    case 0:
      return { textColor: '#10B981', color: '#F6FFED' }
    case 3: // 暂停服务
    case 1:
      return { textColor: '#0068FF', color: '#E6F4FF' }
    case 2:
      return { textColor: '#7C3AED', color: '#F3E8FF' }
    case 3:
      return { textColor: '#F59E0B', color: '#FFF7E6' }
    default:
      return { textColor: '#333333', color: '#F5F5F5' }
  }
}
function getStatusLabel(status: number) {
function getStatusLabel(status: number, statusName?: string) {
  if (statusName)
    return statusName
  return statusOptions.find(item => item.value === status)?.label || '-'
}
function getMockList() {
  return [
    { id: 1, vehicleCode: '073 D 204 77', plateNo: '粤PXV7029', model: 'Robotaxi', procurementSource: '自产', fleet: '车队名称车队名称车队名称', status: 1 },
    { id: 2, vehicleCode: '073 D 204 77', plateNo: '粤PXV7029', model: 'Robotaxi', procurementSource: '自产', fleet: '车队名称车队名称车队名称', status: 2 },
    { id: 3, vehicleCode: '073 D 204 77', plateNo: '粤PXV7029', model: 'Robotaxi', procurementSource: '自产', fleet: '车队名称车队名称车队名称', status: 2 },
    { id: 4, vehicleCode: '073 D 204 77', plateNo: '粤PXV7029', model: 'Robotaxi', procurementSource: '自产', fleet: '车队名称车队名称车队名称', status: 3 },
    { id: 5, vehicleCode: '073 D 204 77', plateNo: '粤PXV7029', model: 'Robotaxi', procurementSource: '自产', fleet: '车队名称车队名称车队名称', status: 3 },
  ]
}
async function getList() {
  if (!props.planId)
    return
function getList() {
  loading.value = true
  const sourceList = getMockList()
  const plateNo = model.value.plateNo.trim()
  const vehicleCode = model.value.vehicleCode.trim()
  const status = model.value.status
  const filteredList = sourceList.filter((item) => {
    const matchPlateNo = !plateNo || item.plateNo.includes(plateNo)
    const matchVehicleCode = !vehicleCode || item.vehicleCode.includes(vehicleCode)
    const matchStatus = !status || item.status === status
    return matchPlateNo && matchVehicleCode && matchStatus
  })
  count.value = 1000 // mock pagination total
  const start = (pageParams.value.pageNum - 1) * pageParams.value.pageSize
  const end = start + pageParams.value.pageSize
  // mock real slice if we had real data
  listData.value = filteredList
  listData.value.forEach((item) => {
    allLoadedData.value.set(item.id, item)
  })
  loading.value = false
  try {
    const res = await fetchTestPlanVehiclePageList({
      pageNum: pageParams.value.pageNum,
      pageSize: pageParams.value.pageSize,
      plateNumber: model.value.plateNo.trim() || undefined,
      testPlanId: props.planId,
      vehicleCode: model.value.vehicleCode.trim() || undefined,
      vehicleStatus: model.value.status ?? undefined,
    })
    if (res.code !== 200 && res.code !== 0) {
      throw new Error(res.msg || '获取车辆列表失败')
    }
    const pageData = res.data || {}
    const records = Array.isArray(pageData.records) ? pageData.records : []
    listData.value = records.map((item: any) => ({
      id: item.vehicleId ?? item.id,
      vehicleCode: item.vehicleCode || '-',
      plateNo: item.plateNumber || '-',
      model: item.vehicleModel || '-',
      fleet: item.fleetName || '-',
      status: Number(item.vehicleStatus ?? 0),
      statusName: item.vehicleStatusName || '',
    }))
    count.value = Number(pageData.total || 0)
  }
  catch (error: any) {
    listData.value = []
    count.value = 0
    message.error(error?.message || '获取车辆列表失败')
  }
  finally {
    loading.value = false
  }
}
watch(
  () => props.visible,
  (val) => {
  val => {
    if (val) {
      model.value = { plateNo: '', vehicleCode: '', status: null }
      pageParams.value = { pageNum: 1, pageSize: 10 }
      allLoadedData.value.clear()
      checkedRowKeys.value = []
      getList()
    }
    else {
      checkedRowKeys.value = []
      allLoadedData.value.clear()
    }
  }
  },
)
function handleSearch() {
@@ -129,6 +142,7 @@
function handleReset() {
  model.value = { plateNo: '', vehicleCode: '', status: null }
  pageParams.value.pageNum = 1
  pageParams.value.pageSize = 10
  getList()
}
@@ -142,12 +156,36 @@
  show.value = false
}
function handleConfirm() {
  const selectedRows = checkedRowKeys.value
    .map(id => allLoadedData.value.get(id))
    .filter((item): item is VehicleRow => Boolean(item))
  emit('confirm', selectedRows)
  show.value = false
async function handleConfirm() {
  if (!props.planId || !props.orderId) {
    message.warning('订单信息缺失')
    return
  }
  if (checkedRowKeys.value.length !== 1) {
    message.warning('请选择一辆车辆进行分派')
    return
  }
  submitting.value = true
  try {
    const res = await reassignTestPlanOrder({
      orderId: props.orderId,
      planId: props.planId,
      vehicleId: checkedRowKeys.value[0],
    })
    if (res.code !== 200 && res.code !== 0) {
      throw new Error(res.msg || '分派车辆失败')
    }
    message.success('分派车辆成功')
    emit('success')
    show.value = false
  }
  catch (error: any) {
    message.error(error?.message || '分派车辆失败')
  }
  finally {
    submitting.value = false
  }
}
const columns = computed<DataTableColumns<VehicleRow>>(() => [
@@ -156,30 +194,27 @@
    title: '序号',
    key: 'index',
    width: 80,
    render: (_, index) => (pageParams.value.pageNum - 1) * pageParams.value.pageSize + index + 1
    render: (_, index) => (pageParams.value.pageNum - 1) * pageParams.value.pageSize + index + 1,
  },
  { title: '车辆编号', key: 'vehicleCode', width: 160 },
  { title: '车辆编号', key: 'vehicleCode', width: 180 },
  { title: '车牌号', key: 'plateNo', width: 160 },
  { title: '型号', key: 'model', width: 160 },
  { title: '采购来源', key: 'procurementSource', width: 160 },
  { title: '当前车队', key: 'fleet', flex: 1, ellipsis: true },
  {
    title: '状态',
    key: 'status',
    width: 120,
    render: (row) => {
      return (
        <NTag
          color={getStatusColor(row.status)}
          bordered={false}
          size="small"
          style="border-radius: 4px;"
        >
          {getStatusLabel(row.status)}
        </NTag>
      )
    }
  }
    render: row => (
      <NTag
        color={getStatusColor(row.status)}
        bordered={false}
        size="small"
        style="border-radius: 4px;"
      >
        {getStatusLabel(row.status, row.statusName)}
      </NTag>
    ),
  },
])
</script>
@@ -310,7 +345,7 @@
      <template #footer>
        <div class="flex justify-end gap-3" style="gap: 12px;">
          <n-button class="footer-btn" @click="handleCancel">取消</n-button>
          <n-button type="primary" class="footer-btn" @click="handleConfirm">确认下发</n-button>
          <n-button type="primary" class="footer-btn" :loading="submitting" @click="handleConfirm">确认下发</n-button>
        </div>
      </template>
    </n-card>
src/views/testDemo/testPlanManagement/components/ManageTestVehicleModal.vue
@@ -1,24 +1,32 @@
<script setup lang="tsx">
import type { DataTableColumns } from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { NModal, NCard, NInput, NButton, NDataTable, NSelect, NTag, NSpace, useMessage, useDialog } from 'naive-ui'
import { NButton, NCard, NDataTable, NInput, NModal, NSelect, NSpace, NTag, useDialog, useMessage } from 'naive-ui'
import Pagination from '@/components/common/Pagination.vue'
import { fetchTestPlanVehiclePageList, manageTestPlanVehicles, pauseTestPlanVehicles } from '@/service/api/testPlanManagement'
import VehicleSelectModal from './VehicleSelectModal.vue'
interface VehicleRow {
  id: number
  id: number | string
  vehicleCode: string
  plateNo: string
  model: string
  status: number
  statusName: string
}
const props = withDefaults(defineProps<{
interface VehicleSelectRow {
  id: number | string
}
const props = defineProps<{
  visible: boolean
}>(), {})
  planId?: number | string
}>()
const emit = defineEmits<{
  (e: 'update:visible', val: boolean): void
  (e: 'add-vehicle'): void
  (e: 'success'): void
}>()
const message = useMessage()
@@ -26,90 +34,108 @@
const show = computed({
  get: () => props.visible,
  set: (val) => emit('update:visible', val)
  set: val => emit('update:visible', val),
})
const searchModel = ref({
  plateNo: '',
  vehicleCode: '',
  status: null
  status: null as number | null,
})
const pageParams = ref({
  pageNum: 1,
  pageSize: 10
  pageSize: 10,
})
const listData = ref<VehicleRow[]>([])
const checkedRowKeys = ref<number[]>([])
const checkedRowKeys = ref<Array<number | string>>([])
const count = ref(0)
const loading = ref(false)
const actionLoading = ref(false)
const isStatusSelectOpen = ref(false)
const vehicleSelectVisible = ref(false)
const statusOptions = [
  { label: '行程中', value: 1 },
  { label: '空闲', value: 2 },
  { label: '空闲', value: 0 },
  { label: '服务中', value: 1 },
  { label: '巡游中', value: 2 },
  { label: '暂停服务', value: 3 },
]
function getStatusColor(status: number) {
  switch (status) {
    case 1: // 行程中
      return { textColor: '#0068FF', color: '#E6F4FF' }
    case 2: // 空闲
    case 0:
      return { textColor: '#10B981', color: '#F6FFED' }
    case 3: // 暂停服务
    case 1:
      return { textColor: '#0068FF', color: '#E6F4FF' }
    case 2:
      return { textColor: '#7C3AED', color: '#F3E8FF' }
    case 3:
      return { textColor: '#F59E0B', color: '#FFF7E6' }
    default:
      return { textColor: '#333333', color: '#F5F5F5' }
  }
}
function getStatusLabel(status: number) {
function getStatusLabel(status: number, statusName?: string) {
  if (statusName)
    return statusName
  return statusOptions.find(item => item.value === status)?.label || '-'
}
function getMockList() {
  return [
    { id: 1, vehicleCode: '073 D 204 77', plateNo: '粤PXV7029', model: 'Robotaxi', status: 1 },
    { id: 2, vehicleCode: '073 D 204 77', plateNo: '粤PXV7029', model: 'Robotaxi', status: 2 },
    { id: 3, vehicleCode: '073 D 204 77', plateNo: '粤PXV7029', model: 'Robotaxi', status: 2 },
    { id: 4, vehicleCode: '073 D 204 77', plateNo: '粤PXV7029', model: 'Robotaxi', status: 3 },
    { id: 5, vehicleCode: '073 D 204 77', plateNo: '粤PXV7029', model: 'Robotaxi', status: 3 },
  ]
}
async function getList() {
  if (!props.planId)
    return
function getList() {
  loading.value = true
  const sourceList = getMockList()
  const plateNo = searchModel.value.plateNo.trim()
  const vehicleCode = searchModel.value.vehicleCode.trim()
  const status = searchModel.value.status
  const filteredList = sourceList.filter((item) => {
    const matchPlateNo = !plateNo || item.plateNo.includes(plateNo)
    const matchVehicleCode = !vehicleCode || item.vehicleCode.includes(vehicleCode)
    const matchStatus = !status || item.status === status
    return matchPlateNo && matchVehicleCode && matchStatus
  })
  count.value = 1000 // mock pagination total
  const start = (pageParams.value.pageNum - 1) * pageParams.value.pageSize
  const end = start + pageParams.value.pageSize
  listData.value = filteredList
  loading.value = false
  try {
    const res = await fetchTestPlanVehiclePageList({
      pageNum: pageParams.value.pageNum,
      pageSize: pageParams.value.pageSize,
      plateNumber: searchModel.value.plateNo.trim() || undefined,
      testPlanId: props.planId,
      vehicleCode: searchModel.value.vehicleCode.trim() || undefined,
      vehicleStatus: searchModel.value.status ?? undefined,
    })
    if (res.code !== 200 && res.code !== 0) {
      throw new Error(res.msg || '获取测试车辆失败')
    }
    const pageData = res.data || {}
    const records = Array.isArray(pageData.records) ? pageData.records : []
    listData.value = records.map((item: any) => ({
      id: item.vehicleId ?? item.id,
      vehicleCode: item.vehicleCode || '-',
      plateNo: item.plateNumber || '-',
      model: item.vehicleModel || '-',
      status: Number(item.vehicleStatus ?? 0),
      statusName: item.vehicleStatusName || '',
    }))
    count.value = Number(pageData.total || 0)
  }
  catch (error: any) {
    listData.value = []
    count.value = 0
    message.error(error?.message || '获取测试车辆失败')
  }
  finally {
    loading.value = false
  }
}
watch(
  () => props.visible,
  (val) => {
  val => {
    if (val) {
      searchModel.value = { plateNo: '', vehicleCode: '', status: null }
      pageParams.value = { pageNum: 1, pageSize: 10 }
      checkedRowKeys.value = []
      getList()
    }
  }
    else {
      checkedRowKeys.value = []
    }
  },
)
function handleSearch() {
@@ -120,6 +146,7 @@
function handleReset() {
  searchModel.value = { plateNo: '', vehicleCode: '', status: null }
  pageParams.value.pageNum = 1
  pageParams.value.pageSize = 10
  getList()
}
@@ -134,11 +161,63 @@
}
function handleSave() {
  message.success('保存成功')
  show.value = false
}
// 批量操作
async function runVehicleManage(params: { addVehicleIds?: Array<number | string>, removeVehicleIds?: Array<number | string> }, successText: string) {
  if (!props.planId)
    return
  actionLoading.value = true
  try {
    const res = await manageTestPlanVehicles({
      planId: props.planId,
      addVehicleIds: params.addVehicleIds || [],
      removeVehicleIds: params.removeVehicleIds || [],
    })
    if (res.code !== 200 && res.code !== 0) {
      throw new Error(res.msg || successText)
    }
    message.success(successText)
    checkedRowKeys.value = []
    await getList()
    emit('success')
  }
  catch (error: any) {
    message.error(error?.message || successText)
  }
  finally {
    actionLoading.value = false
  }
}
async function runVehiclePause(vehicleIds: Array<number | string>, paused: 0 | 1, successText: string) {
  if (!props.planId)
    return
  actionLoading.value = true
  try {
    const res = await pauseTestPlanVehicles({
      testPlanId: props.planId,
      vehicleIds,
      paused,
    })
    if (res.code !== 200 && res.code !== 0) {
      throw new Error(res.msg || successText)
    }
    message.success(successText)
    checkedRowKeys.value = []
    await getList()
    emit('success')
  }
  catch (error: any) {
    message.error(error?.message || successText)
  }
  finally {
    actionLoading.value = false
  }
}
function handleBatchRemove() {
  if (!checkedRowKeys.value.length) {
    message.warning('请先选择车辆')
@@ -149,11 +228,9 @@
    content: '移除后将从测试计划中删除',
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: () => {
      message.success('批量移除成功')
      checkedRowKeys.value = []
      getList()
    }
    onPositiveClick: async () => {
      await runVehicleManage({ removeVehicleIds: checkedRowKeys.value }, '批量移除成功')
    },
  })
}
@@ -162,9 +239,7 @@
    message.warning('请先选择车辆')
    return
  }
  message.success('批量暂停成功')
  checkedRowKeys.value = []
  getList()
  runVehiclePause(checkedRowKeys.value, 1, '批量暂停成功')
}
function handleBatchRestart() {
@@ -172,37 +247,50 @@
    message.warning('请先选择车辆')
    return
  }
  message.success('批量重启成功')
  checkedRowKeys.value = []
  getList()
  runVehiclePause(checkedRowKeys.value, 0, '批量重启成功')
}
function handleAddVehicle() {
  emit('add-vehicle')
  vehicleSelectVisible.value = true
}
// 单个操作
function handleRemove(row: VehicleRow) {
  dialog.warning({
    title: '确认移除该车辆吗?',
    content: '移除后将从测试计划中删除',
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: () => {
      message.success('移除成功')
      getList()
    }
    onPositiveClick: async () => {
      await runVehicleManage({ removeVehicleIds: [row.id] }, '移除成功')
    },
  })
}
function handleToggleStatus(row: VehicleRow) {
  if (row.status === 3) {
    message.success('重启服务成功')
  } else {
    message.success('暂停服务成功')
  }
  getList()
  const paused: 0 | 1 = row.status === 3 ? 0 : 1
  const successText = row.status === 3 ? '重启服务成功' : '暂停服务成功'
  runVehiclePause([row.id], paused, successText)
}
async function handleVehicleSelectConfirm(rows: VehicleSelectRow[]) {
  if (!rows.length) {
    message.warning('请选择至少一辆测试车辆')
    return
  }
  await runVehicleManage({ addVehicleIds: rows.map(item => item.id) }, '添加测试车辆成功')
  vehicleSelectVisible.value = false
}
const existingVehicles = computed(() =>
  listData.value.map(item => ({
    id: item.id,
    vehicleCode: item.vehicleCode,
    plateNo: item.plateNo,
    model: item.model,
    procurementSource: '-',
    fleet: '-',
  })),
)
const columns = computed<DataTableColumns<VehicleRow>>(() => [
  { type: 'selection', width: 60, align: 'center' },
@@ -210,51 +298,47 @@
    title: '序号',
    key: 'index',
    width: 80,
    render: (_, index) => (pageParams.value.pageNum - 1) * pageParams.value.pageSize + index + 1
    render: (_, index) => (pageParams.value.pageNum - 1) * pageParams.value.pageSize + index + 1,
  },
  { title: '车辆编号', key: 'vehicleCode', width: 160 },
  { title: '车辆编号', key: 'vehicleCode', width: 180 },
  { title: '车牌号', key: 'plateNo', width: 160 },
  { title: '型号', key: 'model', width: 160 },
  {
    title: '当前状态',
    key: 'status',
    width: 120,
    render: (row) => {
      return (
        <NTag
          color={getStatusColor(row.status)}
          bordered={false}
          size="small"
          style="border-radius: 4px;"
        >
          {getStatusLabel(row.status)}
        </NTag>
      )
    }
    render: row => (
      <NTag
        color={getStatusColor(row.status)}
        bordered={false}
        size="small"
        style="border-radius: 4px;"
      >
        {getStatusLabel(row.status, row.statusName)}
      </NTag>
    ),
  },
  {
    title: '操作',
    key: 'actions',
    width: 180,
    render: (row) => {
      return (
        <NSpace size={16}>
          <span
            class="cursor-pointer-text"
            onClick={() => handleRemove(row)}
          >
            移除
          </span>
          <span
            class="cursor-pointer-text"
            onClick={() => handleToggleStatus(row)}
          >
            {row.status === 3 ? '重启服务' : '暂停服务'}
          </span>
        </NSpace>
      )
    }
  }
    render: row => (
      <NSpace size={16}>
        <span
          class="cursor-pointer-text"
          onClick={() => handleRemove(row)}
        >
          移除
        </span>
        <span
          class="cursor-pointer-text"
          onClick={() => handleToggleStatus(row)}
        >
          {row.status === 3 ? '重启服务' : '暂停服务'}
        </span>
      </NSpace>
    ),
  },
])
</script>
@@ -399,11 +483,17 @@
      <template #footer>
        <div class="flex justify-end gap-3" style="gap: 12px;">
          <n-button class="footer-btn" @click="handleCancel">取消</n-button>
          <n-button type="primary" class="footer-btn" @click="handleSave">保存</n-button>
          <n-button type="primary" class="footer-btn" :loading="actionLoading" @click="handleSave">保存</n-button>
        </div>
      </template>
    </n-card>
  </n-modal>
  <VehicleSelectModal
    v-model:visible="vehicleSelectVisible"
    :existing-vehicles="existingVehicles"
    @confirm="handleVehicleSelectConfirm"
  />
</template>
<style scoped lang="scss">
src/views/testDemo/testPlanManagement/detail.vue
@@ -1,59 +1,159 @@
<script setup lang="tsx">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { NButton, NSpace, NTag, NForm, NFormItem, NInput, NSelect, NDataTable } from 'naive-ui'
import dayjs from 'dayjs'
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { NButton, NDataTable, NDatePicker, NForm, NFormItem, NInput, NSelect, NSpace, NTag, useDialog, useMessage } from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import { useMessage } from 'naive-ui'
import Pagination from '@/components/common/Pagination.vue'
import SearchPanel from '@/components/custom/SearchPanel.vue'
import { downloadFile } from '@/utils/common'
import {
  cancelTestPlanOrder,
  closeTestPlanOrder,
  deleteTestPlanOrders,
  exportTestPlanDetailOrders,
  fetchTestPlanDetail,
  fetchTestPlanDetailOrderPageList,
  fetchTestPlanMonitorDetail,
  fetchTestPlanVehiclePageList,
  interruptTestPlanDispatch,
  resumeTestPlanDispatch,
} from '@/service/api/testPlanManagement'
import AssignVehicleModal from './components/AssignVehicleModal.vue'
import ManageTestVehicleModal from './components/ManageTestVehicleModal.vue'
import VehicleSelectModal from './components/VehicleSelectModal.vue'
const route = useRoute()
const router = useRouter()
const dialog = useDialog()
const message = useMessage()
// 顶部数据
const planName = ref('2026年05月广州白云区巡游计划')
const planStatus = ref('下发中')
const totalCount = ref(100)
const issuedCount = ref(88)
const unissuedCount = ref(12)
const currentPlanId = computed(() => String(route.query.id || ''))
// ================= 左侧订单列表数据与逻辑 =================
const planName = ref('-')
const planStatus = ref('-')
const planStatusColor = ref('#0068FF')
const planStatusBg = ref('#E6F4FF')
const dispatchStatus = ref<number>(0)
const interruptAllowed = ref(false)
const totalCount = ref(0)
const issuedCount = ref(0)
const unissuedCount = ref(0)
const detailLoading = ref(false)
const monitorLoading = ref(false)
const orderLoading = ref(false)
const vehicleLoading = ref(false)
const exportLoading = ref(false)
const searchModel = ref({
  orderNo: '',
  user: '',
  vehicle: null,
  fleet: null,
  channel: null
  vehicleId: null as number | string | null,
  dispatchTimeRange: null as [number, number] | null,
})
const isVehicleSelectOpen = ref(false)
const isFleetSelectOpen = ref(false)
const isChannelSelectOpen = ref(false)
const orderCheckedRowKeys = ref<Array<number | string>>([])
const orderCount = ref(0)
const orderPageParams = ref({
  pageNum: 1,
  pageSize: 10,
})
const vehicleCount = ref(0)
const vehiclePageParams = ref({
  pageNum: 1,
  pageSize: 10,
})
interface OrderRow {
  orderId: number | string
  orderNo: string
  issueTime: string
  startPoint: string
  endPoint: string
  vehicle: string
  vehicleId?: number | string
  status: string
  statusColor: string
  statusBg: string
  actions: string[]
}
interface VehicleRow {
  id: number | string
  plateNo: string
  vehicleCode: string
  fleetName: string
  status: number
  statusName: string
  statusColor: string
}
const orderData = ref<OrderRow[]>([])
const vehicleData = ref<VehicleRow[]>([])
const isAssignVehicleModalVisible = ref(false)
const isManageTestVehicleModalVisible = ref(false)
const currentAssignOrderId = ref<number | string | null>(null)
const PLAN_STATUS_STYLE_MAP: Record<number, { textColor: string, color: string }> = {
  1: { textColor: '#0068FF', color: '#E6F4FF' },
  2: { textColor: '#F59E0B', color: '#FFF7E6' },
  3: { textColor: '#10B981', color: '#F6FFED' },
}
const VEHICLE_STATUS_STYLE_MAP: Record<number, string> = {
  0: '#10B981',
  1: '#0068FF',
  2: '#7C3AED',
  3: '#F59E0B',
}
const ACTION_LABEL_MAP: Record<string, string> = {
  cancel: '取消订单',
  cancel_order: '取消订单',
  cancelOrder: '取消订单',
  close: '关闭订单',
  close_order: '关闭订单',
  closeOrder: '关闭订单',
  delete: '删除',
  delete_order: '删除',
  deleteOrder: '删除',
  detail: '详情',
  view_detail: '详情',
  reassign: '改派',
  re_assign: '改派',
  assign: '分派车辆',
  allocate: '分派车辆',
  dispatch: '分派车辆',
}
const interruptButtonVisible = computed(() => dispatchStatus.value === 1 || dispatchStatus.value === 2)
const interruptButtonText = computed(() => (dispatchStatus.value === 2 ? '继续下发' : '中断下发'))
const vehicleOptions = computed(() =>
  vehicleData.value.map(item => ({
    label: item.plateNo,
    value: item.id,
  })),
)
const filterItems = computed(() => [
  {
    label: '订单号',
    path: 'orderNo',
    render: () => <n-input v-model:value={searchModel.value.orderNo} placeholder="请输入" clearable />
  },
  {
    label: '用户',
    path: 'user',
    render: () => <n-input v-model:value={searchModel.value.user} placeholder="请输入" clearable />
    render: () => <n-input v-model:value={searchModel.value.orderNo} placeholder="请输入" clearable />,
  },
  {
    label: '车辆',
    path: 'vehicle',
    path: 'vehicleId',
    render: () => (
      <n-select
        v-model:value={searchModel.value.vehicle}
        options={[]}
        v-model:value={searchModel.value.vehicleId}
        options={vehicleOptions.value}
        placeholder="请选择"
        clearable
        onUpdate:show={(val: boolean) => isVehicleSelectOpen.value = val}
        onUpdate:show={(val: boolean) => (isVehicleSelectOpen.value = val)}
      >
        {{
          arrow: () => (
@@ -72,230 +172,501 @@
                alt="no-data"
              />
            </div>
          )
          ),
        }}
      </n-select>
    )
    ),
  },
  {
    label: '车队',
    path: 'fleet',
    label: '下发时间',
    path: 'dispatchTimeRange',
    render: () => (
      <n-select
        v-model:value={searchModel.value.fleet}
        options={[]}
        placeholder="请选择"
      <n-date-picker
        v-model:value={searchModel.value.dispatchTimeRange}
        type="datetimerange"
        format="yyyy/MM/dd HH:mm:ss"
        clearable
        onUpdate:show={(val: boolean) => isFleetSelectOpen.value = val}
      >
        {{
          arrow: () => (
            <img
              src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
              class={`arrow-icon ${isFleetSelectOpen.value ? 'arrow-icon--open' : ''}`}
              style={{ width: '16px', height: '16px' }}
              alt="arrow"
            />
          ),
          empty: () => (
            <div class="flex-center">
              <img
                src="/src/assets/element-img/no-data.png"
                style={{ width: '50%' }}
                alt="no-data"
              />
            </div>
          )
        }}
      </n-select>
    )
        class="w-full"
      />
    ),
  },
  {
    label: '渠道',
    path: 'channel',
    render: () => (
      <n-select
        v-model:value={searchModel.value.channel}
        options={[]}
        placeholder="请选择"
        clearable
        onUpdate:show={(val: boolean) => isChannelSelectOpen.value = val}
      >
        {{
          arrow: () => (
            <img
              src="https://huishou-1323682843.cos.ap-nanjing.myqcloud.com/images/e120f81c-9f15-4e1a-9938-22252898edf9.png"
              class={`arrow-icon ${isChannelSelectOpen.value ? 'arrow-icon--open' : ''}`}
              style={{ width: '16px', height: '16px' }}
              alt="arrow"
            />
          ),
          empty: () => (
            <div class="flex-center">
              <img
                src="/src/assets/element-img/no-data.png"
                style={{ width: '50%' }}
                alt="no-data"
              />
            </div>
          )
        }}
      </n-select>
    )
  }
])
interface OrderRow {
  id: number
  orderNo: string
  issueTime: string
  startPoint: string
  endPoint: string
  vehicle: string
  status: string
  statusColor: string
  statusBg: string
  actions: string[]
function getOrderStatusStyle(statusName: string) {
  if (!statusName)
    return { textColor: '#8C9299', color: '#F5F5F5' }
  if (statusName.includes('待下发'))
    return { textColor: '#A034FF', color: '#F4E6FF' }
  if (statusName.includes('暂停'))
    return { textColor: '#F59E0B', color: '#FFF7E6' }
  if (statusName.includes('完成') || statusName.includes('关闭'))
    return { textColor: '#8C9299', color: '#F5F5F5' }
  if (statusName.includes('服务') || statusName.includes('待出发') || statusName.includes('待接单') || statusName.includes('待乘客'))
    return { textColor: '#0068FF', color: '#E6F4FF' }
  return { textColor: '#10B981', color: '#F6FFED' }
}
const orderCheckedRowKeys = ref<number[]>([])
function normalizeActionLabel(actionCode: string) {
  if (!actionCode)
    return ''
  if (actionCode.includes('取消订单') || actionCode.includes('关闭订单') || actionCode.includes('删除') || actionCode.includes('详情') || actionCode.includes('改派') || actionCode.includes('分派车辆'))
    return actionCode
  const normalizedKey = actionCode.replace(/-/g, '_')
  return ACTION_LABEL_MAP[actionCode] || ACTION_LABEL_MAP[normalizedKey] || ACTION_LABEL_MAP[actionCode.toLowerCase()] || ACTION_LABEL_MAP[normalizedKey.toLowerCase()] || ''
}
const orderData = ref<OrderRow[]>([
  { id: 1, orderNo: '121314', issueTime: '2022/07/05 12:09', startPoint: '成都天府新谷', endPoint: '天府广场', vehicle: '川A999999', status: '待接单', statusColor: '#0068FF', statusBg: '#E6F4FF', actions: ['取消订单', '详情', '删除'] },
  { id: 2, orderNo: '121314', issueTime: '2022/07/05 12:09', startPoint: '成都天府新谷', endPoint: '天府广场', vehicle: '川A999999', status: '待出发', statusColor: '#10B981', statusBg: '#F6FFED', actions: ['取消订单', '改派', '删除'] },
  { id: 3, orderNo: '121314', issueTime: '2022/07/05 12:09', startPoint: '成都天府新谷', endPoint: '天府广场', vehicle: '川A999999', status: '待到达预约地', statusColor: '#F59E0B', statusBg: '#FFF7E6', actions: ['取消订单', '改派', '删除'] },
  { id: 4, orderNo: '121314', issueTime: '2022/07/05 12:09', startPoint: '成都天府新谷', endPoint: '天府广场', vehicle: '川A999999', status: '待乘客上车', statusColor: '#0068FF', statusBg: '#E6F4FF', actions: ['取消订单', '改派', '删除'] },
  { id: 5, orderNo: '121314', issueTime: '2022/07/05 12:09', startPoint: '成都天府新谷', endPoint: '天府广场', vehicle: '川A999999', status: '服务中', statusColor: '#10B981', statusBg: '#F6FFED', actions: ['关闭订单', '详情', '删除'] },
  { id: 6, orderNo: '121314', issueTime: '2022/07/05 12:09', startPoint: '成都天府新谷', endPoint: '天府广场', vehicle: '川A999999', status: '已完成', statusColor: '#8C9299', statusBg: '#F5F5F5', actions: ['详情', '删除'] },
  { id: 7, orderNo: '121314', issueTime: '2022/07/05 12:09', startPoint: '成都天府新谷', endPoint: '天府广场', vehicle: '川A999999', status: '待下发', statusColor: '#A034FF', statusBg: '#F4E6FF', actions: ['分派车辆', '删除'] },
  { id: 8, orderNo: '121314', issueTime: '2022/07/05 12:09', startPoint: '成都天府新谷', endPoint: '天府广场', vehicle: '-', status: '待下发', statusColor: '#A034FF', statusBg: '#F4E6FF', actions: ['分派车辆', '删除'] },
  { id: 9, orderNo: '121314', issueTime: '2022/07/05 12:09', startPoint: '成都天府新谷', endPoint: '天府广场', vehicle: '-', status: '待下发', statusColor: '#A034FF', statusBg: '#F4E6FF', actions: ['分派车辆', '删除'] },
  { id: 10, orderNo: '121314', issueTime: '2022/07/05 12:09', startPoint: '成都天府新谷', endPoint: '天府广场', vehicle: '-', status: '待下发', statusColor: '#A034FF', statusBg: '#F4E6FF', actions: ['分派车辆', '删除'] },
])
function resolveOrderActions(actionCodes: unknown) {
  if (!Array.isArray(actionCodes))
    return []
  return [...new Set(actionCodes.map(item => normalizeActionLabel(String(item))).filter(Boolean))]
}
const isAssignVehicleModalVisible = ref(false)
const isManageTestVehicleModalVisible = ref(false)
const isVehicleSelectModalVisible = ref(false)
function buildOrderQueryParams() {
  const timeRange = searchModel.value.dispatchTimeRange
  return {
    orderNumber: searchModel.value.orderNo.trim() || undefined,
    vehicleId: searchModel.value.vehicleId || undefined,
    dispatchStartTime: timeRange?.[0] ? dayjs(timeRange[0]).format('YYYY-MM-DD HH:mm:ss') : undefined,
    dispatchEndTime: timeRange?.[1] ? dayjs(timeRange[1]).format('YYYY-MM-DD HH:mm:ss') : undefined,
    testPlanId: currentPlanId.value,
  }
}
async function fetchBaseDetail() {
  if (!currentPlanId.value)
    return
  detailLoading.value = true
  try {
    const res = await fetchTestPlanDetail(currentPlanId.value)
    if (res.code !== 200 && res.code !== 0) {
      throw new Error(res.msg || '获取测试计划详情失败')
    }
    planName.value = res.data?.planName || '-'
  }
  catch (error: any) {
    message.error(error?.message || '获取测试计划详情失败')
    planName.value = '-'
  }
  finally {
    detailLoading.value = false
  }
}
async function fetchMonitorInfo() {
  if (!currentPlanId.value)
    return
  monitorLoading.value = true
  try {
    const res = await fetchTestPlanMonitorDetail(currentPlanId.value)
    if (res.code !== 200 && res.code !== 0) {
      throw new Error(res.msg || '获取监控信息失败')
    }
    const monitor = res.data || {}
    dispatchStatus.value = Number(monitor.dispatchStatus || 0)
    planStatus.value = monitor.dispatchStatusName || '-'
    interruptAllowed.value = Boolean(monitor.interruptAllowed)
    totalCount.value = Number(monitor.totalCount || 0)
    issuedCount.value = Number(monitor.issuedCount || 0)
    unissuedCount.value = Number(monitor.unissuedCount || 0)
    const statusStyle = PLAN_STATUS_STYLE_MAP[dispatchStatus.value] || { textColor: '#8C9299', color: '#F5F5F5' }
    planStatusColor.value = statusStyle.textColor
    planStatusBg.value = statusStyle.color
  }
  catch (error: any) {
    message.error(error?.message || '获取监控信息失败')
  }
  finally {
    monitorLoading.value = false
  }
}
async function fetchOrderList() {
  if (!currentPlanId.value)
    return
  orderLoading.value = true
  try {
    const res = await fetchTestPlanDetailOrderPageList({
      ...buildOrderQueryParams(),
      pageNum: orderPageParams.value.pageNum,
      pageSize: orderPageParams.value.pageSize,
    })
    if (res.code !== 200 && res.code !== 0) {
      throw new Error(res.msg || '获取订单列表失败')
    }
    const pageData = res.data || {}
    const records = Array.isArray(pageData.records) ? pageData.records : []
    orderData.value = records.map((item: any) => {
      const statusName = item.orderStatusName || '-'
      const statusStyle = getOrderStatusStyle(statusName)
      return {
        orderId: item.orderId ?? item.id,
        orderNo: item.orderNumber || '-',
        issueTime: item.dispatchTime || '-',
        startPoint: item.startAddress || '-',
        endPoint: item.endAddress || '-',
        vehicle: item.plateNumber || '-',
        vehicleId: item.vehicleId,
        status: statusName,
        statusColor: statusStyle.textColor,
        statusBg: statusStyle.color,
        actions: resolveOrderActions(item.actionCodes),
      }
    })
    orderCount.value = Number(pageData.total || 0)
    orderCheckedRowKeys.value = []
  }
  catch (error: any) {
    orderData.value = []
    orderCount.value = 0
    message.error(error?.message || '获取订单列表失败')
  }
  finally {
    orderLoading.value = false
  }
}
function normalizeVehicleStatusName(status: number, rawName: string) {
  if (rawName)
    return rawName
  switch (status) {
    case 0:
      return '空闲'
    case 1:
      return '服务中'
    case 2:
      return '巡游中'
    case 3:
      return '暂停服务'
    default:
      return '-'
  }
}
async function fetchVehicleList() {
  if (!currentPlanId.value)
    return
  vehicleLoading.value = true
  try {
    const res = await fetchTestPlanVehiclePageList({
      pageNum: vehiclePageParams.value.pageNum,
      pageSize: vehiclePageParams.value.pageSize,
      testPlanId: currentPlanId.value,
    })
    if (res.code !== 200 && res.code !== 0) {
      throw new Error(res.msg || '获取测试车辆失败')
    }
    const pageData = res.data || {}
    const records = Array.isArray(pageData.records) ? pageData.records : []
    vehicleData.value = records.map((item: any) => {
      const status = Number(item.vehicleStatus ?? item.paused ?? 0)
      return {
        id: item.vehicleId ?? item.id,
        plateNo: item.plateNumber || '-',
        vehicleCode: item.vehicleCode || '-',
        fleetName: item.fleetName || '-',
        status,
        statusName: normalizeVehicleStatusName(status, item.vehicleStatusName || ''),
        statusColor: VEHICLE_STATUS_STYLE_MAP[status] || '#8C9299',
      }
    })
    vehicleCount.value = Number(pageData.total || 0)
  }
  catch (error: any) {
    vehicleData.value = []
    vehicleCount.value = 0
    message.error(error?.message || '获取测试车辆失败')
  }
  finally {
    vehicleLoading.value = false
  }
}
async function loadPageData() {
  if (!currentPlanId.value)
    return
  await Promise.all([
    fetchBaseDetail(),
    fetchMonitorInfo(),
    fetchOrderList(),
    fetchVehicleList(),
  ])
}
const orderColumns = computed<DataTableColumns<OrderRow>>(() => [
  { type: 'selection', width: 50, align: 'center' },
  { title: '订单编号', key: 'orderNo', width: 100 },
  { title: '下发时间', key: 'issueTime', width: 150 },
  { title: '起点', key: 'startPoint', width: 140 },
  { title: '终点', key: 'endPoint', width: 140 },
  { title: '接单车辆', key: 'vehicle', width: 120 },
  {
    title: '状态',
    key: 'status',
  { title: '订单编号', key: 'orderNo', width: 160 },
  { title: '下发时间', key: 'issueTime', width: 180 },
  { title: '起点', key: 'startPoint', width: 180, ellipsis: { tooltip: true } },
  { title: '终点', key: 'endPoint', width: 180, ellipsis: { tooltip: true } },
  { title: '接单车辆', key: 'vehicle', width: 140 },
  {
    title: '状态',
    key: 'status',
    width: 120,
    render: (row) => {
      return (
        <NTag
          color={{ textColor: row.statusColor, color: row.statusBg }}
          bordered={false}
          size="small"
          style="border-radius: 4px;"
        >
          {row.status}
        </NTag>
      )
    }
    render: row => (
      <NTag
        color={{ textColor: row.statusColor, color: row.statusBg }}
        bordered={false}
        size="small"
        style="border-radius: 4px;"
      >
        {row.status}
      </NTag>
    ),
  },
  {
    title: '操作',
    key: 'actions',
    width: 180,
    render: (row) => {
      return (
        <NSpace size={12}>
          {row.actions.map(action => (
            <span
  {
    title: '操作',
    key: 'actions',
    width: 220,
    render: row => (
      <NSpace size={12}>
        {row.actions.map(action => (
          <span
            key={`${row.orderId}-${action}`}
            class="cursor-pointer-text"
              onClick={() => handleOrderAction(action, row)}
            >
              {action}
            </span>
          ))}
        </NSpace>
      )
    }
  }
])
function handleOrderAction(action: string, row: OrderRow) {
  if (action === '分派车辆') {
    isAssignVehicleModalVisible.value = true
  } else {
    message.success(`点击了 ${action}`)
  }
}
function handleAssignVehicleConfirm(data: any[]) {
  message.success('分派车辆成功')
}
function handleSearch() {
  message.success('查询')
}
function handleReset() {
  searchModel.value = {
    orderNo: '',
    user: '',
    vehicle: null,
    fleet: null,
    channel: null
  }
}
// ================= 右侧测试车辆数据与逻辑 =================
interface VehicleRow {
  id: number
  plateNo: string
  status: string
  statusColor: string
}
const vehicleData = ref<VehicleRow[]>([
  { id: 1, plateNo: '粤PXV7029', status: '服务中', statusColor: '#0068FF' },
  { id: 2, plateNo: '粤PXV7029', status: '空闲', statusColor: '#10B981' },
  { id: 3, plateNo: '粤PXV7029', status: '空闲', statusColor: '#10B981' },
  { id: 4, plateNo: '粤PXV7029', status: '暂停服务', statusColor: '#F59E0B' },
  { id: 5, plateNo: '粤PXV7029', status: '服务中', statusColor: '#0068FF' },
  { id: 6, plateNo: '粤PXV7029', status: '空闲', statusColor: '#10B981' },
  { id: 7, plateNo: '粤PXV7029', status: '空闲', statusColor: '#10B981' },
  { id: 8, plateNo: '粤PXV7029', status: '空闲', statusColor: '#10B981' },
  { id: 9, plateNo: '粤PXV7029', status: '暂停服务', statusColor: '#F59E0B' },
            onClick={() => handleOrderAction(action, row)}
          >
            {action}
          </span>
        ))}
      </NSpace>
    ),
  },
])
const vehicleColumns = computed<DataTableColumns<VehicleRow>>(() => [
  { title: '车牌号', key: 'plateNo' },
  {
    title: '状态',
    key: 'status',
  { title: '车牌号', key: 'plateNo', width: 120 },
  {
    title: '状态',
    key: 'statusName',
    align: 'left',
    render: (row) => {
      return <span style={{ color: row.statusColor }}>{row.status}</span>
    }
  }
    render: row => <span style={{ color: row.statusColor }}>{row.statusName}</span>,
  },
])
function handleManageVehicle() {
  isManageTestVehicleModalVisible.value = true
}
function handleAddVehicleFromManage() {
  isVehicleSelectModalVisible.value = true
function handleSearch() {
  orderPageParams.value.pageNum = 1
  fetchOrderList()
}
function handleVehicleSelectConfirm(data: any[]) {
  message.success('添加测试车辆成功')
function handleReset() {
  searchModel.value = {
    orderNo: '',
    vehicleId: null,
    dispatchTimeRange: null,
  }
  orderPageParams.value.pageNum = 1
  fetchOrderList()
}
function handleOrderPageChange(page: number, size: number) {
  orderPageParams.value.pageNum = size !== orderPageParams.value.pageSize ? 1 : page
  orderPageParams.value.pageSize = size
  fetchOrderList()
}
function handleVehiclePageChange(page: number, size: number) {
  vehiclePageParams.value.pageNum = size !== vehiclePageParams.value.pageSize ? 1 : page
  vehiclePageParams.value.pageSize = size
  fetchVehicleList()
}
async function handleExportOrders() {
  if (!currentPlanId.value)
    return
  exportLoading.value = true
  try {
    const res = await exportTestPlanDetailOrders({
      ...buildOrderQueryParams(),
      pageNum: orderPageParams.value.pageNum,
      pageSize: orderPageParams.value.pageSize,
    })
    if (res instanceof Blob) {
      downloadFile(res, `测试计划订单列表_${Date.now()}.xlsx`)
      message.success('导出成功')
    }
  }
  catch (error: any) {
    message.error(error?.message || '导出订单失败')
  }
  finally {
    exportLoading.value = false
  }
}
function handleOperationLog() {
  message.info('操作日志待接入')
}
async function handleDeleteOrders(orderIds: Array<number | string>) {
  if (!currentPlanId.value) {
    message.warning('测试计划ID缺失')
    return
  }
  if (!orderIds.length) {
    message.warning('请选择需要删除的订单')
    return
  }
  try {
    const res = await deleteTestPlanOrders({
      orderIds,
      planId: currentPlanId.value,
    })
    if (res.code !== 200 && res.code !== 0) {
      throw new Error(res.msg || '删除订单失败')
    }
    message.success('删除成功')
    if (orderIds.length >= orderData.value.length && orderPageParams.value.pageNum > 1) {
      orderPageParams.value.pageNum--
    }
    await Promise.all([fetchOrderList(), fetchMonitorInfo()])
  }
  catch (error: any) {
    message.error(error?.message || '删除订单失败')
  }
}
function confirmDeleteOrders(orderIds: Array<number | string>) {
  dialog.warning({
    title: '确认删除订单吗?',
    content: '删除后无法恢复',
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: async () => {
      await handleDeleteOrders(orderIds)
    },
  })
}
function handleOrderAction(action: string, row: OrderRow) {
  if (!currentPlanId.value) {
    message.warning('测试计划ID缺失')
    return
  }
  if (action === '分派车辆' || action === '改派') {
    currentAssignOrderId.value = row.orderId
    isAssignVehicleModalVisible.value = true
    return
  }
  if (action === '删除') {
    confirmDeleteOrders([row.orderId])
    return
  }
  if (action === '详情') {
    message.info('订单详情待接入')
    return
  }
  if (action === '取消订单') {
    dialog.warning({
      title: '确认取消订单吗?',
      positiveText: '确定',
      negativeText: '取消',
      onPositiveClick: async () => {
        try {
          const res = await cancelTestPlanOrder(row.orderId, currentPlanId.value)
          if (res.code !== 200 && res.code !== 0) {
            throw new Error(res.msg || '取消订单失败')
          }
          message.success('取消订单成功')
          await Promise.all([fetchOrderList(), fetchMonitorInfo()])
        }
        catch (error: any) {
          message.error(error?.message || '取消订单失败')
        }
      },
    })
    return
  }
  if (action === '关闭订单') {
    dialog.warning({
      title: '确认关闭订单吗?',
      positiveText: '确定',
      negativeText: '取消',
      onPositiveClick: async () => {
        try {
          const res = await closeTestPlanOrder(row.orderId, currentPlanId.value)
          if (res.code !== 200 && res.code !== 0) {
            throw new Error(res.msg || '关闭订单失败')
          }
          message.success('关闭订单成功')
          await Promise.all([fetchOrderList(), fetchMonitorInfo()])
        }
        catch (error: any) {
          message.error(error?.message || '关闭订单失败')
        }
      },
    })
  }
}
async function handleAssignVehicleSuccess() {
  isAssignVehicleModalVisible.value = false
  currentAssignOrderId.value = null
  await fetchOrderList()
}
async function handleManageVehicleSuccess() {
  await Promise.all([fetchVehicleList(), fetchMonitorInfo(), fetchOrderList()])
}
function handleInterrupt() {
  message.warning('中断下发')
  if (!currentPlanId.value)
    return
  if (dispatchStatus.value === 1 && !interruptAllowed.value) {
    message.warning('当前状态不允许中断下发')
    return
  }
  const isResume = dispatchStatus.value === 2
  dialog.warning({
    title: isResume ? '确认继续下发吗?' : '确认中断下发吗?',
    content: isResume ? '继续后将恢复已冻结订单的下发' : '中断后将冻结所有待下发订单',
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: async () => {
      try {
        const res = isResume
          ? await resumeTestPlanDispatch(currentPlanId.value)
          : await interruptTestPlanDispatch(currentPlanId.value)
        if (res.code !== 200 && res.code !== 0) {
          throw new Error(res.msg || (isResume ? '继续下发失败' : '中断下发失败'))
        }
        message.success(isResume ? '继续下发成功' : '中断下发成功')
        await Promise.all([fetchMonitorInfo(), fetchOrderList()])
      }
      catch (error: any) {
        message.error(error?.message || (isResume ? '继续下发失败' : '中断下发失败'))
      }
    },
  })
}
watch(
  () => route.query.id,
  async id => {
    if (!id) {
      planName.value = '-'
      orderData.value = []
      vehicleData.value = []
      orderCount.value = 0
      vehicleCount.value = 0
      return
    }
    await loadPageData()
  },
  { immediate: true },
)
</script>
<template>
@@ -312,14 +683,13 @@
            planName
          }}</span>
          <n-tag
            type="info"
            size="small"
            :bordered="false"
            style="
              color: #0068ff;
              background-color: #e6f4ff;
              border-radius: 4px;
            "
            :style="{
              color: planStatusColor,
              backgroundColor: planStatusBg,
              borderRadius: '4px'
            }"
          >
            {{ planStatus }}
          </n-tag>
@@ -340,12 +710,14 @@
            >
          </div>
          <n-button
            v-if="interruptButtonVisible"
            type="error"
            ghost
            style="border-radius: 4px; height: 30px"
            @click="handleInterrupt"
            >中断下发</n-button
          >
            {{ interruptButtonText }}
          </n-button>
        </div>
      </div>
@@ -380,8 +752,9 @@
          <!-- 操作按钮区 -->
          <div class="flex gap-8px mb-12px shrink-0">
            <n-button>导出订单</n-button>
            <n-button>操作日志</n-button>
            <n-button :loading="exportLoading" @click="handleExportOrders">导出订单</n-button>
            <n-button @click="handleOperationLog">操作日志</n-button>
            <n-button @click="confirmDeleteOrders(orderCheckedRowKeys)">批量删除</n-button>
          </div>
          <!-- 表格 -->
@@ -390,11 +763,12 @@
              class="flex-1"
              :columns="orderColumns"
              :data="orderData"
              :row-key="(row) => row.id"
              :loading="orderLoading"
              :row-key="(row) => row.orderId"
              v-model:checked-row-keys="orderCheckedRowKeys"
              flex-height
              :bordered="false"
              :scroll-x="1100"
              :scroll-x="1280"
            >
              <template #empty>
                <div class="flex-center">
@@ -407,10 +781,11 @@
              </template>
            </n-data-table>
            <Pagination
              :count="1000"
              :page="1"
              :page-size="10"
              :count="orderCount"
              :page="orderPageParams.pageNum"
              :page-size="orderPageParams.pageSize"
              class="robotaxi-pagination mt-12px shrink-0"
              @change="handleOrderPageChange"
            />
          </div>
        </div>
@@ -436,6 +811,7 @@
              class="h-full"
              :columns="vehicleColumns"
              :data="vehicleData"
              :loading="vehicleLoading"
              :row-key="(row) => row.id"
              flex-height
              :bordered="false"
@@ -450,6 +826,13 @@
                </div>
              </template>
            </n-data-table>
            <Pagination
              :count="vehicleCount"
              :page="vehiclePageParams.pageNum"
              :page-size="vehiclePageParams.pageSize"
              class="robotaxi-pagination mt-12px shrink-0"
              @change="handleVehiclePageChange"
            />
          </div>
        </div>
      </div>
@@ -464,17 +847,15 @@
    <AssignVehicleModal
      v-model:visible="isAssignVehicleModalVisible"
      @confirm="handleAssignVehicleConfirm"
      :plan-id="currentPlanId"
      :order-id="currentAssignOrderId"
      @success="handleAssignVehicleSuccess"
    />
    <ManageTestVehicleModal
      v-model:visible="isManageTestVehicleModalVisible"
      @add-vehicle="handleAddVehicleFromManage"
    />
    <VehicleSelectModal
      v-model:visible="isVehicleSelectModalVisible"
      @confirm="handleVehicleSelectConfirm"
      :plan-id="currentPlanId"
      @success="handleManageVehicleSuccess"
    />
  </div>
</template>
@@ -493,6 +874,12 @@
.shadow-sm {
  box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.02);
}
:deep(.arrow-icon) {
  transition: transform 0.3s;
}
:deep(.arrow-icon--open) {
  transform: rotate(180deg);
}
:deep(.cursor-pointer-text) {
  cursor: pointer;
  font-size: 13px;