董国庆
2026-06-09 314e8f134065e0ba3dc8dbb2e5e37a0b2ca2d5e8
demo
1个文件已添加
9个文件已修改
585 ■■■■■ 已修改文件
src/service/api/testPlanManagement.ts 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/whitelist.ts 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanManagement/api.txt 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanManagement/components/AssignVehicleModal.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanManagement/components/ManageTestVehicleModal.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanManagement/constants.ts 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanManagement/detail.vue 200 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanManagement/index.vue 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/whitelistManagement/whitelist/components/WhitelistEditModal.vue 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/whitelistManagement/whitelist/index.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/testPlanManagement.ts
@@ -9,6 +9,10 @@
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/pageList', params)
}
export function deleteTestPlan(id: number | string) {
  return request.Delete<Service.ResponseResult<any>>(`/operation/api/v1/test-plan-demo/${id}`)
}
export function addTestPlan(data: {
  orderMode: number
  planName: string
@@ -80,7 +84,7 @@
export function interruptTestPlanDispatch(planId: number | string) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/dispatch/interrupt', null, {
    params: { planId },
    params: { planId, testPlanId: planId },
  })
}
@@ -90,14 +94,20 @@
  })
}
export function closeTestPlanDispatch(planId: number | string) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/dispatch/closeTest', null, {
    params: { planId },
  })
}
export function fetchTestPlanDetailOrderPageList(data: {
  endAddress?: string
  orderNumber?: string
  orderStatus?: number
  pageNum: number
  pageSize: number
  plateNumber?: string
  startAddress?: string
  status?: number
  testPlanId: number | string
}) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/detail/pageOrders', data)
@@ -106,11 +116,11 @@
export function exportTestPlanDetailOrders(data: {
  endAddress?: string
  orderNumber?: string
  orderStatus?: number
  pageNum?: number
  pageSize?: number
  plateNumber?: string
  startAddress?: string
  status?: number
  testPlanId: number | string
}) {
  const method = request.Post<Blob>('/operation/api/v1/test-plan-demo/detail/exportOrders', data, {
@@ -140,6 +150,14 @@
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/order/reassign', data)
}
export function assignTestPlanOrderVehicle(data: {
  orderId: number | string
  planId: number | string
  vehicleIds: Array<number | string>
}) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/test-plan-demo/dispatch/assignVehicle', data)
}
export function fetchTestPlanVehiclePageList(data: {
  pageNum: number
  pageSize: number
src/service/api/whitelist.ts
@@ -11,13 +11,18 @@
/**
 * 新增白名单
 */
export function addWhitelist(data: {
export interface AddWhitelistParams {
  account: string
  status: number
  isPriorityDispatch: number
  expireTime?: string
  isPriorityDispatch: boolean
  isTestUser?: boolean
  maxOrderNum?: number
}) {
  nickName: string
  remark?: string
  status: boolean
}
export function addWhitelist(data: AddWhitelistParams) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/whitelist/add', data)
}
src/views/testDemo/testPlanManagement/api.txt
@@ -402,4 +402,12 @@
​/api​/v1​/test-plan-demo​/detail​/listVehicles 车辆状态不分页 post
​/api​/v1​/test-plan-demo​/detail​/listVehicles 车辆状态不分页 post
计划管理列表操作接口:
​/api​/v1​/test-plan-demo​/{id}    删除测试计划(测试中不可删  deleted
/api/v1/test-plan-demo/dispatch/closeTest   关闭测试(已暂停状态可操作)  post  params   planId
/api/v1/test-plan-demo/dispatch/resume   继续测试(恢复已冻结订单)  post  params  planId
​/api​/v1​/test-plan-demo​/dispatch​/interrupt 中断测试(冻结所有待下发订单)  post  params  testPlanId
src/views/testDemo/testPlanManagement/components/AssignVehicleModal.vue
@@ -3,7 +3,7 @@
import { computed, ref, watch } from 'vue'
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'
import { assignTestPlanOrderVehicle, fetchTestPlanVehiclePageList, reassignTestPlanOrder } from '@/service/api/testPlanManagement'
interface VehicleRow {
  id: number | string
@@ -19,6 +19,7 @@
  visible: boolean
  planId?: number | string
  orderId?: number | string | null
  actionType?: 'assign' | 'reassign'
}>()
const emit = defineEmits<{
@@ -32,6 +33,9 @@
  get: () => props.visible,
  set: val => emit('update:visible', val),
})
const isAssignAction = computed(() => props.actionType !== 'reassign')
const modalTitle = computed(() => (isAssignAction.value ? '选择分派车辆' : '选择改派车辆'))
const model = ref({
  plateNo: '',
@@ -168,20 +172,26 @@
  submitting.value = true
  try {
    const res = await reassignTestPlanOrder({
      orderId: props.orderId,
      planId: props.planId,
      vehicleId: checkedRowKeys.value[0],
    })
    const res = isAssignAction.value
      ? await assignTestPlanOrderVehicle({
          orderId: props.orderId,
          planId: props.planId,
          vehicleIds: [checkedRowKeys.value[0]],
        })
      : await reassignTestPlanOrder({
          orderId: props.orderId,
          planId: props.planId,
          vehicleId: checkedRowKeys.value[0],
        })
    if (res.code !== 200 && res.code !== 0) {
      throw new Error(res.msg || '分派车辆失败')
      throw new Error(res.msg || (isAssignAction.value ? '分派车辆失败' : '改派车辆失败'))
    }
    message.success('分派车辆成功')
    message.success(isAssignAction.value ? '分派车辆成功' : '改派车辆成功')
    emit('success')
    show.value = false
  }
  catch (error: any) {
    message.error(error?.message || '分派车辆失败')
    message.error(error?.message || (isAssignAction.value ? '分派车辆失败' : '改派车辆失败'))
  }
  finally {
    submitting.value = false
@@ -235,7 +245,7 @@
      footer-style="padding: 16px 24px; border-top: 1px solid rgba(46, 61, 77, 0.1); flex-shrink: 0;"
    >
      <template #header>
        <span style="font-size: 16px; font-weight: bold">选择分派车辆</span>
        <span style="font-size: 16px; font-weight: bold">{{ modalTitle }}</span>
      </template>
      <template #header-extra>
        <div class="cursor-pointer" text @click="handleCancel">
@@ -345,7 +355,9 @@
      <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" :loading="submitting" @click="handleConfirm">确认下发</n-button>
          <n-button type="primary" class="footer-btn" :loading="submitting" @click="handleConfirm">
            {{ isAssignAction ? '确认分派' : '确认改派' }}
          </n-button>
        </div>
      </template>
    </n-card>
src/views/testDemo/testPlanManagement/components/ManageTestVehicleModal.vue
@@ -11,6 +11,7 @@
  vehicleCode: string
  plateNo: string
  model: string
  paused: boolean
  status: number
  statusName: string
}
@@ -84,6 +85,10 @@
  return statusOptions.find(item => item.value === status)?.label || '-'
}
function isPausedVehicle(row: VehicleRow) {
  return row.paused || row.status === 3 || row.statusName.includes('暂停')
}
async function getList() {
  if (!props.planId)
    return
@@ -108,6 +113,7 @@
      vehicleCode: item.vehicleCode || '-',
      plateNo: item.plateNumber || '-',
      model: item.vehicleModel || '-',
      paused: Number(item.paused ?? 0) === 1,
      status: Number(item.vehicleStatus ?? 0),
      statusName: item.vehicleStatusName || '',
    }))
@@ -267,8 +273,8 @@
}
function handleToggleStatus(row: VehicleRow) {
  const paused: 0 | 1 = row.status === 3 ? 0 : 1
  const successText = row.status === 3 ? '重启服务成功' : '暂停服务成功'
  const paused: 0 | 1 = isPausedVehicle(row) ? 0 : 1
  const successText = isPausedVehicle(row) ? '重启服务成功' : '暂停服务成功'
  runVehiclePause([row.id], paused, successText)
}
@@ -277,7 +283,18 @@
    message.warning('请选择至少一辆测试车辆')
    return
  }
  await runVehicleManage({ addVehicleIds: rows.map(item => item.id) }, '添加测试车辆成功')
  const existingIdSet = new Set(existingVehicles.value.map(item => item.id))
  const addVehicleIds = rows
    .map(item => item.id)
    .filter(id => !existingIdSet.has(id))
  if (!addVehicleIds.length) {
    message.info('未选择新的测试车辆')
    vehicleSelectVisible.value = false
    return
  }
  await runVehicleManage({ addVehicleIds }, '添加测试车辆成功')
  vehicleSelectVisible.value = false
}
@@ -334,7 +351,7 @@
          class="cursor-pointer-text"
          onClick={() => handleToggleStatus(row)}
        >
          {row.status === 3 ? '重启服务' : '暂停服务'}
          {isPausedVehicle(row) ? '重启服务' : '暂停服务'}
        </span>
      </NSpace>
    ),
@@ -483,7 +500,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" :loading="actionLoading" @click="handleSave">保存</n-button>
          <n-button type="primary" class="footer-btn" :loading="actionLoading" @click="handleSave">完成</n-button>
        </div>
      </template>
    </n-card>
src/views/testDemo/testPlanManagement/constants.ts
New file
@@ -0,0 +1,31 @@
export const TEST_PLAN_STATUS_OPTIONS = [
  { label: '待下发', value: 1 },
  { label: '测试中', value: 2 },
  { label: '已暂停', value: 3 },
  { label: '已完成', value: 4 },
  { label: '关闭测试', value: 5 },
]
export const TEST_PLAN_STATUS_LABEL_MAP: Record<number, string> = {
  1: '待下发',
  2: '测试中',
  3: '已暂停',
  4: '已完成',
  5: '关闭测试',
}
export const TEST_PLAN_STATUS_STYLE_MAP: Record<number, { textColor: string, color: string }> = {
  1: { textColor: '#764DFF', color: '#F6F0FF' },
  2: { textColor: '#0068FF', color: '#E6F4FF' },
  3: { textColor: '#FF9000', color: '#FFF8E6' },
  4: { textColor: '#0BB52D', color: '#DCF5DD' },
  5: { textColor: 'rgba(23, 31, 38, 0.8)', color: 'rgba(23, 31, 38, 0.05)' },
}
export function getTestPlanStatusLabel(status: number) {
  return TEST_PLAN_STATUS_LABEL_MAP[status] || '-'
}
export function getTestPlanStatusStyle(status: number) {
  return TEST_PLAN_STATUS_STYLE_MAP[status] || { textColor: '#333333', color: '#F5F5F5' }
}
src/views/testDemo/testPlanManagement/detail.vue
@@ -18,6 +18,7 @@
  interruptTestPlanDispatch,
  resumeTestPlanDispatch,
} from '@/service/api/testPlanManagement'
import { getTestPlanStatusLabel, getTestPlanStatusStyle } from './constants'
import AssignVehicleModal from './components/AssignVehicleModal.vue'
import ManageTestVehicleModal from './components/ManageTestVehicleModal.vue'
@@ -29,9 +30,7 @@
const currentPlanId = computed(() => String(route.query.id || ''))
const planName = ref('-')
const planStatus = ref('-')
const planStatusColor = ref('#0068FF')
const planStatusBg = ref('#E6F4FF')
const planStatusCode = ref<number>(0)
const dispatchStatus = ref<number>(0)
const interruptAllowed = ref(false)
const totalCount = ref(0)
@@ -49,7 +48,7 @@
  startAddress: null as string | null,
  endAddress: null as string | null,
  plateNumber: null as string | null,
  orderStatus: null as number | null,
  status: null as number | null,
})
const isVehicleSelectOpen = ref(false)
@@ -82,6 +81,7 @@
  status: number
  statusName: string
  statusColor: string
  statusBg: string
}
const orderData = ref<OrderRow[]>([])
@@ -90,42 +90,20 @@
const isAssignVehicleModalVisible = ref(false)
const isManageTestVehicleModalVisible = ref(false)
const currentAssignOrderId = ref<number | string | null>(null)
const currentAssignActionType = ref<'assign' | 'reassign'>('assign')
const pointOptions = ref<Array<{ label: string, value: string }>>([])
const PLAN_STATUS_STYLE_MAP: Record<number, { textColor: string, color: string }> = {
const VEHICLE_STATUS_STYLE_MAP: Record<number, { textColor: string, color: string }> = {
  0: { textColor: '#10B981', color: '#F6FFED' },
  1: { textColor: '#0068FF', color: '#E6F4FF' },
  2: { textColor: '#F59E0B', color: '#FFF7E6' },
  3: { textColor: '#10B981', color: '#F6FFED' },
  2: { textColor: '#A034FF', color: '#F4E6FF' },
  3: { textColor: '#F59E0B', color: '#FFF7E6' },
}
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 planStatusText = computed(() => getTestPlanStatusLabel(planStatusCode.value))
const planStatusStyle = computed(() => getTestPlanStatusStyle(planStatusCode.value))
const interruptButtonVisible = computed(() => planStatusCode.value === 2 || planStatusCode.value === 3)
const interruptButtonText = computed(() => (planStatusCode.value === 3 ? '继续测试' : '中断测试'))
const vehicleOptions = computed(() =>
  vehicleData.value.map(item => ({
@@ -135,31 +113,32 @@
)
const orderStatusOptions = [
  { label: '已取消', value: 0 },
  { label: '待下发', value: 0 },
  { label: '待接单', value: 1 },
  { label: '待出发', value: 2 },
  { label: '接驾中', value: 3 },
  { label: '待客上车', value: 4 },
  { label: '乘客已上车', value: 5 },
  { label: '待到达预约地', value: 3 },
  { label: '待乘客上车', value: 4 },
  { label: '服务中', value: 6 },
  { label: '待客下车', value: 7 },
  { label: '待付款', value: 8 },
  { label: '待评价', value: 9 },
  { label: '已完成', value: 10 },
]
const ORDER_STATUS_LABEL_MAP: Record<number, string> = {
  0: '已取消',
  0: '待下发',
  1: '待接单',
  2: '待出发',
  3: '接驾中',
  4: '待客上车',
  5: '乘客已上车',
  3: '待到达预约地',
  4: '待乘客上车',
  6: '服务中',
  7: '待客下车',
  8: '待付款',
  9: '待评价',
  10: '已完成',
}
const ORDER_STATUS_STYLE_MAP: Record<string, { textColor: string, color: string }> = {
  待下发: { textColor: '#764DFF', color: '#F6F0FF' },
  待接单: { textColor: '#0068FF', color: '#E6F4FF' },
  待出发: { textColor: '#0BB52D', color: '#DCF5DD' },
  待到达预约地: { textColor: '#FF9000', color: '#FFF8E6' },
  待乘客上车: { textColor: '#0068FF', color: '#E6F4FF' },
  服务中: { textColor: '#0BB52D', color: '#DCF5DD' },
  已完成: { textColor: 'rgba(23, 31, 38, 0.8)', color: 'rgba(23, 31, 38, 0.05)' },
  已取消: { textColor: 'rgba(23, 31, 38, 0.8)', color: 'rgba(23, 31, 38, 0.05)' },
}
const filterItems = computed(() => [
@@ -227,10 +206,10 @@
  },
  {
    label: '订单状态',
    path: 'orderStatus',
    path: 'status',
    render: () => (
      <n-select
        v-model:value={searchModel.value.orderStatus}
        v-model:value={searchModel.value.status}
        options={orderStatusOptions}
        placeholder="请选择"
        clearable
@@ -243,28 +222,24 @@
  const resolvedStatusName = statusName || (statusCode != null ? ORDER_STATUS_LABEL_MAP[statusCode] : '')
  if (!resolvedStatusName)
    return { textColor: '#8C9299', color: '#F5F5F5' }
  if (resolvedStatusName.includes('取消') || resolvedStatusName.includes('完成'))
    return { textColor: '#8C9299', color: '#F5F5F5' }
  if (resolvedStatusName.includes('服务') || resolvedStatusName.includes('待接单') || resolvedStatusName.includes('待出发') || resolvedStatusName.includes('接驾') || resolvedStatusName.includes('待客') || resolvedStatusName.includes('上车'))
    return { textColor: '#0068FF', color: '#E6F4FF' }
  if (resolvedStatusName.includes('付款') || resolvedStatusName.includes('评价'))
    return { textColor: '#F59E0B', color: '#FFF7E6' }
  return { textColor: '#10B981', color: '#F6FFED' }
  return ORDER_STATUS_STYLE_MAP[resolvedStatusName] || { textColor: '#8C9299', color: '#F5F5F5' }
}
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()] || ''
}
function resolveOrderActions(actionCodes: unknown) {
  if (!Array.isArray(actionCodes))
    return []
  return [...new Set(actionCodes.map(item => normalizeActionLabel(String(item))).filter(Boolean))]
function resolveOrderActions(status?: number) {
  switch (status) {
    case 0:
      return ['分派车辆', '删除']
    case 1:
      return ['取消订单', '详情', '删除']
    case 2:
    case 3:
    case 4:
      return ['取消订单', '改派', '删除']
    case 6:
      return ['关闭订单', '详情', '删除']
    default:
      return ['删除']
  }
}
function buildOrderQueryParams() {
@@ -273,7 +248,7 @@
    startAddress: searchModel.value.startAddress || undefined,
    endAddress: searchModel.value.endAddress || undefined,
    plateNumber: searchModel.value.plateNumber || undefined,
    orderStatus: searchModel.value.orderStatus ?? undefined,
    status: searchModel.value.status ?? undefined,
    testPlanId: currentPlanId.value,
  }
}
@@ -291,6 +266,19 @@
  return [...optionMap.entries()].map(([value, label]) => ({ value, label }))
}
function mapDispatchStatusToPlanStatus(status?: number) {
  switch (status) {
    case 1:
      return 2
    case 2:
      return 3
    case 3:
      return 4
    default:
      return 0
  }
}
async function fetchBaseDetail() {
  if (!currentPlanId.value)
    return
@@ -303,6 +291,9 @@
    }
    const detail = res.data || {}
    planName.value = detail.planName || '-'
    const detailStatusCode = Number(detail.status ?? detail.planStatus ?? 0)
    if (detailStatusCode)
      planStatusCode.value = detailStatusCode
    pointOptions.value = buildPointOptions(detail.points)
  }
  catch (error: any) {
@@ -327,14 +318,13 @@
    }
    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
    const monitorPlanStatusCode = Number(monitor.status ?? monitor.planStatus ?? 0) || mapDispatchStatusToPlanStatus(dispatchStatus.value)
    if (monitorPlanStatusCode)
      planStatusCode.value = monitorPlanStatusCode
  }
  catch (error: any) {
    message.error(error?.message || '获取监控信息失败')
@@ -361,8 +351,8 @@
    const pageData = res.data || {}
    const records = Array.isArray(pageData.records) ? pageData.records : []
    orderData.value = records.map((item: any) => {
      const statusCode = item.orderStatus != null ? Number(item.orderStatus) : undefined
      const statusName = item.orderStatusName || (statusCode != null ? ORDER_STATUS_LABEL_MAP[statusCode] : '-') || '-'
      const statusCode = item.status != null ? Number(item.status) : undefined
      const statusName = item.statusName || (statusCode != null ? ORDER_STATUS_LABEL_MAP[statusCode] : '-') || '-'
      const statusStyle = getOrderStatusStyle(statusCode, statusName)
      return {
        orderId: item.orderId ?? item.id,
@@ -375,7 +365,7 @@
        status: statusName,
        statusColor: statusStyle.textColor,
        statusBg: statusStyle.color,
        actions: resolveOrderActions(item.actionCodes),
        actions: resolveOrderActions(statusCode),
      }
    })
    orderCount.value = Number(pageData.total || 0)
@@ -428,6 +418,7 @@
        : []
    vehicleData.value = records.map((item: any) => {
      const status = Number(item.vehicleStatus ?? item.paused ?? 0)
      const statusStyle = VEHICLE_STATUS_STYLE_MAP[status] || { textColor: '#8C9299', color: '#F5F5F5' }
      return {
        id: item.vehicleId ?? item.id,
        plateNo: item.plateNumber || '-',
@@ -435,7 +426,8 @@
        fleetName: item.fleetName || '-',
        status,
        statusName: normalizeVehicleStatusName(status, item.vehicleStatusName || ''),
        statusColor: VEHICLE_STATUS_STYLE_MAP[status] || '#8C9299',
        statusColor: statusStyle.textColor,
        statusBg: statusStyle.color,
      }
    })
  }
@@ -475,7 +467,7 @@
        color={{ textColor: row.statusColor, color: row.statusBg }}
        bordered={false}
        size="small"
        style="border-radius: 4px;"
        style="border-radius: 4px; padding: 0 8px; height: 24px; line-height: 24px; font-weight: 400;"
      >
        {row.status}
      </NTag>
@@ -507,7 +499,16 @@
    title: '状态',
    key: 'statusName',
    align: 'left',
    render: row => <span style={{ color: row.statusColor }}>{row.statusName}</span>,
    render: row => (
      <NTag
        color={{ textColor: row.statusColor, color: row.statusBg }}
        bordered={false}
        size="small"
        style="border-radius: 4px; padding: 0 8px; height: 24px; line-height: 24px; font-weight: 400;"
      >
        {row.statusName}
      </NTag>
    ),
  },
])
@@ -526,7 +527,7 @@
    startAddress: null,
    endAddress: null,
    plateNumber: null,
    orderStatus: null,
    status: null,
  }
  orderPageParams.value.pageNum = 1
  fetchOrderList()
@@ -560,10 +561,6 @@
  finally {
    exportLoading.value = false
  }
}
function handleOperationLog() {
  message.info('操作日志待接入')
}
async function handleDeleteOrders(orderIds: Array<number | string>) {
@@ -615,6 +612,7 @@
  if (action === '分派车辆' || action === '改派') {
    currentAssignOrderId.value = row.orderId
    currentAssignActionType.value = action === '改派' ? 'reassign' : 'assign'
    isAssignVehicleModalVisible.value = true
    return
  }
@@ -672,6 +670,7 @@
async function handleAssignVehicleSuccess() {
  isAssignVehicleModalVisible.value = false
  currentAssignOrderId.value = null
  currentAssignActionType.value = 'assign'
  await fetchOrderList()
}
@@ -682,14 +681,14 @@
function handleInterrupt() {
  if (!currentPlanId.value)
    return
  if (dispatchStatus.value === 1 && !interruptAllowed.value) {
    message.warning('当前状态不允许中断下发')
  if (planStatusCode.value === 2 && !interruptAllowed.value) {
    message.warning('当前状态不允许中断测试')
    return
  }
  const isResume = dispatchStatus.value === 2
  const isResume = planStatusCode.value === 3
  dialog.warning({
    title: isResume ? '确认继续下发吗?' : '确认中断下发吗?',
    title: isResume ? '确认继续测试吗?' : '确认中断测试吗?',
    content: isResume ? '继续后将恢复已冻结订单的下发' : '中断后将冻结所有待下发订单',
    positiveText: '确定',
    negativeText: '取消',
@@ -699,13 +698,13 @@
          ? await resumeTestPlanDispatch(currentPlanId.value)
          : await interruptTestPlanDispatch(currentPlanId.value)
        if (res.code !== 200 && res.code !== 0) {
          throw new Error(res.msg || (isResume ? '继续下发失败' : '中断下发失败'))
          throw new Error(res.msg || (isResume ? '继续测试失败' : '中断测试失败'))
        }
        message.success(isResume ? '继续下发成功' : '中断下发成功')
        message.success(isResume ? '继续测试成功' : '中断测试成功')
        await Promise.all([fetchMonitorInfo(), fetchOrderList()])
      }
      catch (error: any) {
        message.error(error?.message || (isResume ? '继续下发失败' : '中断下发失败'))
        message.error(error?.message || (isResume ? '继续测试失败' : '中断测试失败'))
      }
    },
  })
@@ -745,12 +744,12 @@
            size="small"
            :bordered="false"
            :style="{
              color: planStatusColor,
              backgroundColor: planStatusBg,
              color: planStatusStyle.textColor,
              backgroundColor: planStatusStyle.color,
              borderRadius: '4px'
            }"
          >
            {{ planStatus }}
            {{ planStatusText }}
          </n-tag>
        </div>
        <div class="flex items-center gap-24px">
@@ -812,8 +811,6 @@
          <!-- 操作按钮区 -->
          <div class="flex gap-8px mb-12px shrink-0">
            <n-button :loading="exportLoading" @click="handleExportOrders">导出订单</n-button>
            <n-button @click="handleOperationLog">操作日志</n-button>
            <n-button @click="confirmDeleteOrders(orderCheckedRowKeys)">批量删除</n-button>
          </div>
          <!-- 表格 -->
@@ -901,6 +898,7 @@
      v-model:visible="isAssignVehicleModalVisible"
      :plan-id="currentPlanId"
      :order-id="currentAssignOrderId"
      :action-type="currentAssignActionType"
      @success="handleAssignVehicleSuccess"
    />
src/views/testDemo/testPlanManagement/index.vue
@@ -5,7 +5,8 @@
import { useRouter } from 'vue-router'
import { useBoolean } from '@/hooks'
import { useDialog, useMessage } from 'naive-ui'
import { fetchTestPlanPageList } from '@/service/api/testPlanManagement'
import { closeTestPlanDispatch, deleteTestPlan, fetchTestPlanPageList, interruptTestPlanDispatch, resumeTestPlanDispatch } from '@/service/api/testPlanManagement'
import { getTestPlanStatusLabel, getTestPlanStatusStyle, TEST_PLAN_STATUS_OPTIONS } from './constants'
const router = useRouter()
const dialog = useDialog()
@@ -37,33 +38,12 @@
const tableScrollX = 1200
const statusOptions = [
  { label: '待下发', value: 1 },
  { label: '测试中', value: 2 },
  { label: '已暂停', value: 3 },
  { label: '关闭测试', value: 4 },
  { label: '已完成', value: 5 },
]
function getStatusLabel(status: number) {
  return statusOptions.find(item => item.value === status)?.label || '-'
  return getTestPlanStatusLabel(status)
}
function getStatusColor(status: number) {
  switch (status) {
    case 1: // 待下发 (紫色)
      return { textColor: '#A034FF', color: '#F9F0FF' }
    case 2: // 测试中 (蓝色)
      return { textColor: '#0068FF', color: '#E6F4FF' }
    case 3: // 已暂停 (橙色)
      return { textColor: '#F59E0B', color: '#FFF7E6' }
    case 4: // 关闭测试 (灰色)
      return { textColor: '#9CA3AF', color: '#F5F5F5' }
    case 5: // 已完成 (绿色)
      return { textColor: '#10B981', color: '#F6FFED' }
    default:
      return { textColor: '#333333', color: '#F5F5F5' }
  }
  return getTestPlanStatusStyle(status)
}
function handleResetSearch() {
@@ -91,9 +71,9 @@
    listData.value = records.map((item: any) => ({
      id: item.id,
      planName: item.planName || '-',
      pointNum: Number(item.pointNum ?? item.pointCount ?? item.bindPointNum ?? 0),
      vehicleNum: Number(item.vehicleNum ?? item.bindVehicleNum ?? 0),
      orderNum: Number(item.orderNum ?? item.testOrderNum ?? 0),
      pointCount: Number(item.pointCount ?? 0),
      vehicleCount: Number(item.vehicleCount ?? 0),
      orderCount: Number(item.orderCount ?? 0),
      status: Number(item.status ?? 0),
    }))
    count.value = Number(res.data?.total ?? records.length ?? 0)
@@ -146,9 +126,18 @@
    maskClosable: false,
    autoFocus: false,
    onPositiveClick: async () => {
      // TODO: 调用删除接口
      message.success('删除成功')
      getList()
      try {
        const res = await deleteTestPlan(row.id)
        if (res.code !== 200 && res.code !== 0) {
          throw new Error(res.msg || '删除失败')
        }
        message.success('删除成功')
        await getList()
      }
      catch (error: any) {
        message.error(error?.message || '删除失败')
        throw error
      }
    },
  })
}
@@ -159,10 +148,20 @@
    content: '中断后测试计划将暂停',
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: () => {
      message.success('已中断')
      getList()
    }
    onPositiveClick: async () => {
      try {
        const res = await interruptTestPlanDispatch(row.id)
        if (res.code !== 200 && res.code !== 0) {
          throw new Error(res.msg || '中断测试失败')
        }
        message.success('已中断')
        await getList()
      }
      catch (error: any) {
        message.error(error?.message || '中断测试失败')
        throw error
      }
    },
  })
}
@@ -172,10 +171,20 @@
    content: '关闭后测试计划将终止',
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: () => {
      message.success('已关闭')
      getList()
    }
    onPositiveClick: async () => {
      try {
        const res = await closeTestPlanDispatch(row.id)
        if (res.code !== 200 && res.code !== 0) {
          throw new Error(res.msg || '关闭测试失败')
        }
        message.success('已关闭')
        await getList()
      }
      catch (error: any) {
        message.error(error?.message || '关闭测试失败')
        throw error
      }
    },
  })
}
@@ -185,10 +194,20 @@
    content: '测试计划将继续执行',
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: () => {
      message.success('已继续')
      getList()
    }
    onPositiveClick: async () => {
      try {
        const res = await resumeTestPlanDispatch(row.id)
        if (res.code !== 200 && res.code !== 0) {
          throw new Error(res.msg || '继续测试失败')
        }
        message.success('已继续')
        await getList()
      }
      catch (error: any) {
        message.error(error?.message || '继续测试失败')
        throw error
      }
    },
  })
}
@@ -227,7 +246,7 @@
  },
  {
    title: '关联点位',
    key: 'pointNum',
    key: 'pointCount',
    width: 140,
    align: 'left',
    resizable: true,
@@ -235,7 +254,7 @@
  },
  {
    title: '关联车辆数',
    key: 'vehicleNum',
    key: 'vehicleCount',
    width: 140,
    align: 'left',
    resizable: true,
@@ -243,7 +262,7 @@
  },
  {
    title: '测试订单数',
    key: 'orderNum',
    key: 'orderCount',
    width: 140,
    align: 'left',
    resizable: true,
@@ -281,7 +300,7 @@
        actions.push(<NButton text type="primary" onClick={() => handleClose(row)}>关闭测试</NButton>)
        actions.push(<NButton text type="primary" onClick={() => handleContinue(row)}>继续测试</NButton>)
      }
      if (row.status === 4 || row.status === 5) { // 关闭测试 / 已完成
      if (row.status === 4 || row.status === 5) { // 已完成 / 关闭测试
        actions.push(<NButton text type="primary" onClick={() => handleDetail(row)}>详情</NButton>)
      }
@@ -308,7 +327,7 @@
    render: () => (
      <n-select
        v-model:value={model.value.status}
        options={statusOptions}
        options={TEST_PLAN_STATUS_OPTIONS}
        placeholder="请选择"
        clearable
        onUpdate:show={(val: boolean) => isStatusSelectOpen.value = val}
@@ -407,7 +426,7 @@
      </div>
    </n-card>
    <OperationLogModal v-model:show="showOperationLogModal" />
    <OperationLogModal v-model:show="showOperationLogModal" :business-type="48" />
  </div>
</template>
src/views/userContent/whitelistManagement/whitelist/components/WhitelistEditModal.vue
@@ -11,16 +11,18 @@
  NModal,
  NSpace,
  NSwitch,
  useDialog,
  useMessage,
} from 'naive-ui'
import type { FormInst, FormRules } from 'naive-ui'
import dayjs from 'dayjs'
import { editWhitelist } from '@/service/api/whitelist'
import { addWhitelist, editWhitelist } from '@/service/api/whitelist'
type ModalMode = 'add' | 'edit'
interface Props {
  visible?: boolean
  rowData?: any
  mode?: ModalMode
}
interface Emits {
@@ -33,6 +35,7 @@
const props = withDefaults(defineProps<Props>(), {
  visible: false,
  rowData: null,
  mode: 'add',
})
// 弹窗对外事件:
@@ -41,6 +44,10 @@
const emit = defineEmits<Emits>()
// 全局消息提示实例。
const message = useMessage()
// 当前弹窗模式。
const isEditMode = computed(() => props.mode === 'edit')
// 弹窗标题统一跟随模式变化。
const modalTitle = computed(() => isEditMode.value ? '编辑白名单' : '新增白名单')
// 编辑表单数据结构。
// 这里保留了 status 字段,虽然当前编辑样式未展示该开关,但保存时仍需沿用原始状态值,避免误改。
@@ -125,38 +132,44 @@
// 监听弹窗显隐与传入数据。
// 每次打开编辑弹窗时都根据当前 rowData 重新生成表单,避免上一次编辑内容串到下一条数据。
function createFormModelFromRow(rowData: any): FormModel {
  return {
    ...createDefaultFormModel(),
    account: rowData?.account || '',
    nickName: rowData?.nickName || '',
    status: rowData?.status === 1 || rowData?.status === true,
    isPriorityDispatch:
      rowData?.isPriorityDispatch === 1
      || rowData?.isPriorityDispatch === true,
    // 编辑弹窗同时兼容列表页、详情页等不同来源字段命名,优先按当前业务字段回显,缺失时逐级兜底。
    expireDate: formatDateValue(
      rowData?.expireDate
      ?? rowData?.effectiveDate
      ?? rowData?.validityDate
      ?? rowData?.expireTime,
    ),
    // 最大下单量存在不同字段名,统一收口到表单内部的 maxOrderCount。
    maxOrderCount: toOptionalNumber(
      rowData?.maxOrderCount
      ?? rowData?.maxOrderNum
      ?? rowData?.orderLimitCount,
    ),
    isTestUser: rowData?.isTestUser === 1 || rowData?.isTestUser === true,
    remark: rowData?.remark || '',
  }
}
watch(
  () => props.visible,
  (val) => {
    if (val && props.rowData) {
      formModel.value = {
        ...createDefaultFormModel(),
        account: props.rowData.account || '',
        nickName: props.rowData.nickName || '',
        status: props.rowData.status === 1 || props.rowData.status === true,
        isPriorityDispatch:
          props.rowData.isPriorityDispatch === 1
          || props.rowData.isPriorityDispatch === true,
        // 编辑弹窗同时兼容列表页、详情页等不同来源字段命名,优先按当前业务字段回显,缺失时逐级兜底。
        expireDate: formatDateValue(
          props.rowData.expireDate
          ?? props.rowData.effectiveDate
          ?? props.rowData.validityDate
          ?? props.rowData.expireTime,
        ),
        // 最大下单量存在不同字段名,统一收口到表单内部的 maxOrderCount。
        maxOrderCount: toOptionalNumber(
          props.rowData.maxOrderCount
          ?? props.rowData.maxOrderNum
          ?? props.rowData.orderLimitCount,
        ),
        isTestUser: props.rowData.isTestUser === 1 || props.rowData.isTestUser === true,
        remark: props.rowData.remark || '',
      }
    }
    else {
  () => [props.visible, props.rowData, props.mode],
  ([visible]) => {
    if (!visible) {
      formModel.value = createDefaultFormModel()
      return
    }
    formModel.value = isEditMode.value && props.rowData
      ? createFormModelFromRow(props.rowData)
      : createDefaultFormModel()
  },
)
@@ -177,21 +190,35 @@
  try {
    await formRef.value?.validate()
    const res = await editWhitelist({
      id: props.rowData.id,
    const editPayload = {
      account: formModel.value.account,
      nickName: formModel.value.nickName,
      // 状态开关当前未展示在编辑 UI 中,但仍沿用表单模型中的状态,避免把原始状态丢掉。
      status: formModel.value.status ? 1 : 0,
      isPriorityDispatch: formModel.value.isPriorityDispatch ? 1 : 0,
      expireTime: formModel.value.expireDate ? dayjs(formModel.value.expireDate).format('YYYY-MM-DD') : undefined,
      maxOrderNum: formModel.value.maxOrderCount ?? undefined,
      isTestUser: formModel.value.isTestUser ? 1 : 0,
      remark: formModel.value.remark,
    })
    }
    const res = isEditMode.value
      ? await editWhitelist({
          id: props.rowData.id,
          ...editPayload,
        })
      : await addWhitelist({
          account: formModel.value.account,
          nickName: formModel.value.nickName,
          status: formModel.value.status,
          isPriorityDispatch: formModel.value.isPriorityDispatch,
          expireTime: formModel.value.expireDate ? dayjs(formModel.value.expireDate).format('YYYY-MM-DD') : undefined,
          maxOrderNum: formModel.value.maxOrderCount ?? undefined,
          isTestUser: formModel.value.isTestUser,
          remark: formModel.value.remark || undefined,
        })
    if (res.code === 200) {
      message.success('编辑白名单成功')
      message.success(res.msg || `${isEditMode.value ? '编辑' : '新增'}白名单成功`)
      emit('success')
      closeModal()
    }
@@ -207,7 +234,7 @@
    :show="visible"
    :mask-closable="false"
    preset="card"
    title="编辑白名单"
    :title="modalTitle"
    class="whitelist-modal"
    :style="{ width: '600px' }"
    :segmented="{
src/views/userContent/whitelistManagement/whitelist/index.vue
@@ -1,11 +1,10 @@
<script setup lang="tsx">
import type { DataTableColumns, FormInst } from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import { NButton, NTag, useDialog, useMessage } from 'naive-ui'
import { useBoolean } from '@/hooks'
import { downloadFile } from '@/utils'
import SearchPanel from '@/components/custom/SearchPanel.vue'
import OperationLogModal from '@/components/custom/OperationLogModal.vue'
import WhitelistAddModal from './components/WhitelistAddModal.vue'
import WhitelistEditModal from './components/WhitelistEditModal.vue'
import {
  deleteWhitelistByIds,
@@ -60,10 +59,10 @@
const totalCount = ref(0)
// 表格当前勾选行主键集合,用于批量冻结和批量导出。
const selectedRowKeys = ref<number[]>([])
// 新增弹窗显示状态。新增与编辑拆分后,避免单个弹窗内部维护过多分支。
const showAddModal = ref(false)
// 编辑弹窗显示状态。编辑弹窗单独承载编辑样式和字段,和新增逻辑彻底解耦。
const showEditModal = ref(false)
// 新增/编辑共用弹窗显示状态。
const showFormModal = ref(false)
// 当前弹窗模式:新增或编辑。
const modalMode = ref<'add' | 'edit'>('add')
// 当前操作的行数据。编辑、详情等动作都会基于该对象获取当前上下文。
const currentRow = ref<any>(null)
// 分页组件刷新 key,当前预留给需要强制刷新分页内部状态的场景。
@@ -354,10 +353,11 @@
}
// 打开新增弹窗。
// 新增场景不依赖 currentRow,因此先清空当前行上下文,避免残留编辑数据。
// 新增场景复用编辑样式弹窗,但表单数据从空白状态开始。
function handleAdd() {
  modalMode.value = 'add'
  currentRow.value = null
  showAddModal.value = true
  showFormModal.value = true
}
// 导入白名单。
@@ -450,8 +450,9 @@
    message.warning('冻结状态不允许编辑')
    return
  }
  modalMode.value = 'edit'
  currentRow.value = row
  showEditModal.value = true
  showFormModal.value = true
}
// 弹窗保存成功回调。
@@ -679,15 +680,10 @@
      </div>
    </n-card>
    <!-- 新增白名单弹窗 -->
    <WhitelistAddModal
      v-model:visible="showAddModal"
      @success="handleModalSuccess"
    />
    <!-- 编辑白名单弹窗 -->
    <!-- 新增/编辑白名单弹窗 -->
    <WhitelistEditModal
      v-model:visible="showEditModal"
      v-model:visible="showFormModal"
      :mode="modalMode"
      :row-data="currentRow"
      @success="handleModalSuccess"
    />