| | |
| | | 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 |
| | |
| | | |
| | | 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 }, |
| | | }) |
| | | } |
| | | |
| | |
| | | }) |
| | | } |
| | | |
| | | 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) |
| | |
| | | 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, { |
| | |
| | | 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 |
| | |
| | | /** |
| | | * 新增白名单 |
| | | */ |
| | | 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) |
| | | } |
| | | |
| | |
| | | |
| | | |
| | | |
| | | /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 |
| | |
| | | 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 |
| | |
| | | visible: boolean |
| | | planId?: number | string |
| | | orderId?: number | string | null |
| | | actionType?: 'assign' | 'reassign' |
| | | }>() |
| | | |
| | | const emit = defineEmits<{ |
| | |
| | | get: () => props.visible, |
| | | set: val => emit('update:visible', val), |
| | | }) |
| | | |
| | | const isAssignAction = computed(() => props.actionType !== 'reassign') |
| | | const modalTitle = computed(() => (isAssignAction.value ? '选择分派车辆' : '选择改派车辆')) |
| | | |
| | | const model = ref({ |
| | | plateNo: '', |
| | |
| | | |
| | | 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 |
| | |
| | | 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"> |
| | |
| | | <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> |
| | |
| | | vehicleCode: string |
| | | plateNo: string |
| | | model: string |
| | | paused: boolean |
| | | status: number |
| | | statusName: string |
| | | } |
| | |
| | | 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 |
| | |
| | | vehicleCode: item.vehicleCode || '-', |
| | | plateNo: item.plateNumber || '-', |
| | | model: item.vehicleModel || '-', |
| | | paused: Number(item.paused ?? 0) === 1, |
| | | status: Number(item.vehicleStatus ?? 0), |
| | | statusName: item.vehicleStatusName || '', |
| | | })) |
| | |
| | | } |
| | | |
| | | 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) |
| | | } |
| | | |
| | |
| | | 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 |
| | | } |
| | | |
| | |
| | | class="cursor-pointer-text" |
| | | onClick={() => handleToggleStatus(row)} |
| | | > |
| | | {row.status === 3 ? '重启服务' : '暂停服务'} |
| | | {isPausedVehicle(row) ? '重启服务' : '暂停服务'} |
| | | </span> |
| | | </NSpace> |
| | | ), |
| | |
| | | <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> |
| New file |
| | |
| | | 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' } |
| | | } |
| | |
| | | interruptTestPlanDispatch, |
| | | resumeTestPlanDispatch, |
| | | } from '@/service/api/testPlanManagement' |
| | | import { getTestPlanStatusLabel, getTestPlanStatusStyle } from './constants' |
| | | import AssignVehicleModal from './components/AssignVehicleModal.vue' |
| | | import ManageTestVehicleModal from './components/ManageTestVehicleModal.vue' |
| | | |
| | |
| | | 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) |
| | |
| | | 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) |
| | |
| | | status: number |
| | | statusName: string |
| | | statusColor: string |
| | | statusBg: string |
| | | } |
| | | |
| | | const orderData = ref<OrderRow[]>([]) |
| | |
| | | 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 => ({ |
| | |
| | | ) |
| | | |
| | | 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(() => [ |
| | |
| | | }, |
| | | { |
| | | label: '订单状态', |
| | | path: 'orderStatus', |
| | | path: 'status', |
| | | render: () => ( |
| | | <n-select |
| | | v-model:value={searchModel.value.orderStatus} |
| | | v-model:value={searchModel.value.status} |
| | | options={orderStatusOptions} |
| | | placeholder="请选择" |
| | | clearable |
| | |
| | | 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() { |
| | |
| | | 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, |
| | | } |
| | | } |
| | |
| | | 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 |
| | |
| | | } |
| | | 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) { |
| | |
| | | } |
| | | 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 || '获取监控信息失败') |
| | |
| | | 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, |
| | |
| | | status: statusName, |
| | | statusColor: statusStyle.textColor, |
| | | statusBg: statusStyle.color, |
| | | actions: resolveOrderActions(item.actionCodes), |
| | | actions: resolveOrderActions(statusCode), |
| | | } |
| | | }) |
| | | orderCount.value = Number(pageData.total || 0) |
| | |
| | | : [] |
| | | 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 || '-', |
| | |
| | | fleetName: item.fleetName || '-', |
| | | status, |
| | | statusName: normalizeVehicleStatusName(status, item.vehicleStatusName || ''), |
| | | statusColor: VEHICLE_STATUS_STYLE_MAP[status] || '#8C9299', |
| | | statusColor: statusStyle.textColor, |
| | | statusBg: statusStyle.color, |
| | | } |
| | | }) |
| | | } |
| | |
| | | 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> |
| | |
| | | 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> |
| | | ), |
| | | }, |
| | | ]) |
| | | |
| | |
| | | startAddress: null, |
| | | endAddress: null, |
| | | plateNumber: null, |
| | | orderStatus: null, |
| | | status: null, |
| | | } |
| | | orderPageParams.value.pageNum = 1 |
| | | fetchOrderList() |
| | |
| | | finally { |
| | | exportLoading.value = false |
| | | } |
| | | } |
| | | |
| | | function handleOperationLog() { |
| | | message.info('操作日志待接入') |
| | | } |
| | | |
| | | async function handleDeleteOrders(orderIds: Array<number | string>) { |
| | |
| | | |
| | | if (action === '分派车辆' || action === '改派') { |
| | | currentAssignOrderId.value = row.orderId |
| | | currentAssignActionType.value = action === '改派' ? 'reassign' : 'assign' |
| | | isAssignVehicleModalVisible.value = true |
| | | return |
| | | } |
| | |
| | | async function handleAssignVehicleSuccess() { |
| | | isAssignVehicleModalVisible.value = false |
| | | currentAssignOrderId.value = null |
| | | currentAssignActionType.value = 'assign' |
| | | await fetchOrderList() |
| | | } |
| | | |
| | |
| | | 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: '取消', |
| | |
| | | ? 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 ? '继续测试失败' : '中断测试失败')) |
| | | } |
| | | }, |
| | | }) |
| | |
| | | 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"> |
| | |
| | | <!-- 操作按钮区 --> |
| | | <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> |
| | | |
| | | <!-- 表格 --> |
| | |
| | | v-model:visible="isAssignVehicleModalVisible" |
| | | :plan-id="currentPlanId" |
| | | :order-id="currentAssignOrderId" |
| | | :action-type="currentAssignActionType" |
| | | @success="handleAssignVehicleSuccess" |
| | | /> |
| | | |
| | |
| | | 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() |
| | |
| | | |
| | | 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() { |
| | |
| | | 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) |
| | |
| | | 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 |
| | | } |
| | | }, |
| | | }) |
| | | } |
| | |
| | | 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 |
| | | } |
| | | }, |
| | | }) |
| | | } |
| | | |
| | |
| | | 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 |
| | | } |
| | | }, |
| | | }) |
| | | } |
| | | |
| | |
| | | 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 |
| | | } |
| | | }, |
| | | }) |
| | | } |
| | | |
| | |
| | | }, |
| | | { |
| | | title: '关联点位', |
| | | key: 'pointNum', |
| | | key: 'pointCount', |
| | | width: 140, |
| | | align: 'left', |
| | | resizable: true, |
| | |
| | | }, |
| | | { |
| | | title: '关联车辆数', |
| | | key: 'vehicleNum', |
| | | key: 'vehicleCount', |
| | | width: 140, |
| | | align: 'left', |
| | | resizable: true, |
| | |
| | | }, |
| | | { |
| | | title: '测试订单数', |
| | | key: 'orderNum', |
| | | key: 'orderCount', |
| | | width: 140, |
| | | align: 'left', |
| | | resizable: true, |
| | |
| | | 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>) |
| | | } |
| | | |
| | |
| | | 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} |
| | |
| | | </div> |
| | | </n-card> |
| | | |
| | | <OperationLogModal v-model:show="showOperationLogModal" /> |
| | | <OperationLogModal v-model:show="showOperationLogModal" :business-type="48" /> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | 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 { |
| | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | visible: false, |
| | | rowData: null, |
| | | mode: 'add', |
| | | }) |
| | | |
| | | // 弹窗对外事件: |
| | |
| | | const emit = defineEmits<Emits>() |
| | | // 全局消息提示实例。 |
| | | const message = useMessage() |
| | | // 当前弹窗模式。 |
| | | const isEditMode = computed(() => props.mode === 'edit') |
| | | // 弹窗标题统一跟随模式变化。 |
| | | const modalTitle = computed(() => isEditMode.value ? '编辑白名单' : '新增白名单') |
| | | |
| | | // 编辑表单数据结构。 |
| | | // 这里保留了 status 字段,虽然当前编辑样式未展示该开关,但保存时仍需沿用原始状态值,避免误改。 |
| | |
| | | |
| | | // 监听弹窗显隐与传入数据。 |
| | | // 每次打开编辑弹窗时都根据当前 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() |
| | | }, |
| | | ) |
| | | |
| | |
| | | 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() |
| | | } |
| | |
| | | :show="visible" |
| | | :mask-closable="false" |
| | | preset="card" |
| | | title="编辑白名单" |
| | | :title="modalTitle" |
| | | class="whitelist-modal" |
| | | :style="{ width: '600px' }" |
| | | :segmented="{ |
| | |
| | | <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, |
| | |
| | | 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,当前预留给需要强制刷新分页内部状态的场景。 |
| | |
| | | } |
| | | |
| | | // 打开新增弹窗。 |
| | | // 新增场景不依赖 currentRow,因此先清空当前行上下文,避免残留编辑数据。 |
| | | // 新增场景复用编辑样式弹窗,但表单数据从空白状态开始。 |
| | | function handleAdd() { |
| | | modalMode.value = 'add' |
| | | currentRow.value = null |
| | | showAddModal.value = true |
| | | showFormModal.value = true |
| | | } |
| | | |
| | | // 导入白名单。 |
| | |
| | | message.warning('冻结状态不允许编辑') |
| | | return |
| | | } |
| | | modalMode.value = 'edit' |
| | | currentRow.value = row |
| | | showEditModal.value = true |
| | | showFormModal.value = true |
| | | } |
| | | |
| | | // 弹窗保存成功回调。 |
| | |
| | | </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" |
| | | /> |