| | |
| | | <script setup lang="tsx"> |
| | | import { computed, h, onMounted, ref, watch } from 'vue' |
| | | import { computed, h, onMounted, onUnmounted, ref, watch } from 'vue' |
| | | import { useRoute, useRouter } from 'vue-router' |
| | | import SelectPointModal from '@/views/testDemo/specialPointManagement/components/SelectPointModal.vue' |
| | | import VehicleSelectModal from '@/views/assetManagement/fleetManagement/components/VehicleSelectModal.vue' |
| | |
| | | import { |
| | | addTestOrder, |
| | | addTestPlan, |
| | | batchCancelTestPlan, |
| | | batchRetryTestPlan, |
| | | batchStartTestPlan, |
| | | cancelAndCloseTestOrder, |
| | | changeTestOrderAmount, |
| | | deleteTestOrderById, |
| | | editTestPlan, |
| | | fetchBatchProgress, |
| | | fetchTestOrderPageList, |
| | | fetchTestPlanDetail, |
| | | fetchVehicleProgress, |
| | | reassignTestOrderById, |
| | | } from '@/service/api/copyText' |
| | | import { getAvailableVehicleList } from '@/service/api/transaction' |
| | |
| | | message.warning('请至少添加一个测试点位') |
| | | return |
| | | } |
| | | if (vehicles.value.length === 0) { |
| | | message.warning('请至少选择一辆车辆') |
| | | return |
| | | } |
| | | const invalidPointIndex = testPoints.value.findIndex((p) => { |
| | | const startId = String(p.startTestPointId || '').trim() |
| | | const endId = String(p.endTestPointId || '').trim() |
| | |
| | | } |
| | | } |
| | | |
| | | // ==================== 批量测试相关 ==================== |
| | | const lapCount = ref(1) |
| | | const batchProgress = ref<any>(null) |
| | | const vehicleProgressList = ref<any[]>([]) |
| | | let batchPollTimer: ReturnType<typeof setInterval> | null = null |
| | | |
| | | const batchStatusMap: Record<string, { label: string, type: 'default' | 'success' | 'warning' | 'error' | 'info' }> = { |
| | | RUNNING: { label: '运行中', type: 'warning' }, |
| | | SUCCESS: { label: '成功', type: 'success' }, |
| | | PARTIAL_SUCCESS: { label: '部分成功', type: 'warning' }, |
| | | FAILED: { label: '失败', type: 'error' }, |
| | | } |
| | | |
| | | function getBatchStatusLabel(status: string) { |
| | | return batchStatusMap[status]?.label || status || '-' |
| | | } |
| | | |
| | | function getBatchStatusType(status: string) { |
| | | return batchStatusMap[status]?.type || 'default' |
| | | } |
| | | |
| | | function getVehicleProgressStatusLabel(status: string) { |
| | | const map: Record<string, { label: string, type: 'default' | 'success' | 'warning' | 'error' | 'info' }> = { |
| | | RUNNING: { label: '运行中', type: 'warning' }, |
| | | COMPLETED: { label: '已完成', type: 'success' }, |
| | | ERROR: { label: '异常', type: 'error' }, |
| | | CANCELLED: { label: '已取消', type: 'default' }, |
| | | PENDING: { label: '待执行', type: 'info' }, |
| | | } |
| | | return map[status]?.label || status || '-' |
| | | } |
| | | |
| | | function getVehicleProgressStatusType(status: string) { |
| | | const map: Record<string, 'default' | 'success' | 'warning' | 'error' | 'info'> = { |
| | | RUNNING: 'warning', |
| | | COMPLETED: 'success', |
| | | ERROR: 'error', |
| | | CANCELLED: 'default', |
| | | PENDING: 'info', |
| | | } |
| | | return map[status] || 'default' |
| | | } |
| | | |
| | | const batchProgressPercent = computed(() => { |
| | | if (!batchProgress.value || !batchProgress.value.totalCount) return 0 |
| | | return Math.round((batchProgress.value.completedCount / batchProgress.value.totalCount) * 100) |
| | | }) |
| | | |
| | | const isBatchRunning = computed(() => batchProgress.value?.status === 'RUNNING') |
| | | |
| | | async function getBatchProgress() { |
| | | if (!planId.value) return |
| | | const res = await fetchBatchProgress(planId.value) |
| | | if (res.code === 200 && res.data) { |
| | | batchProgress.value = res.data |
| | | } |
| | | } |
| | | |
| | | async function getVehicleProgressList() { |
| | | if (!batchProgress.value || !batchProgress.value.batchId) return |
| | | const res = await fetchVehicleProgress(batchProgress.value.batchId) |
| | | if (res.code === 200) { |
| | | vehicleProgressList.value = res.data || [] |
| | | } |
| | | } |
| | | |
| | | async function refreshBatchData() { |
| | | await getBatchProgress() |
| | | await getVehicleProgressList() |
| | | } |
| | | |
| | | function startBatchPolling() { |
| | | stopBatchPolling() |
| | | batchPollTimer = setInterval(() => { |
| | | refreshBatchData() |
| | | }, 5000) |
| | | } |
| | | |
| | | function stopBatchPolling() { |
| | | if (batchPollTimer) { |
| | | clearInterval(batchPollTimer) |
| | | batchPollTimer = null |
| | | } |
| | | } |
| | | |
| | | async function handleBatchStart() { |
| | | if (!planId.value) { |
| | | message.warning('测试计划初始化中,请稍后再试') |
| | | return |
| | | } |
| | | if (vehicles.value.length === 0) { |
| | | message.warning('请先选择车辆') |
| | | return |
| | | } |
| | | if (testPoints.value.length === 0) { |
| | | message.warning('请至少添加一个测试点位') |
| | | return |
| | | } |
| | | if (!lapCount.value || lapCount.value < 1) { |
| | | message.warning('请输入有效的圈数(至少1圈)') |
| | | return |
| | | } |
| | | |
| | | dialog.create({ |
| | | title: '确认启动批量测试?', |
| | | content: `将为 ${vehicles.value.length} 辆车按 ${testPoints.value.length} 个点位组合生成链式巡游订单,每车跑 ${lapCount.value} 圈`, |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | maskClosable: false, |
| | | autoFocus: false, |
| | | onPositiveClick: async () => { |
| | | const res = await batchStartTestPlan({ testPlanId: planId.value, lapCount: lapCount.value }) |
| | | if (res.code === 200) { |
| | | message.success('批量测试已启动') |
| | | await refreshBatchData() |
| | | startBatchPolling() |
| | | } |
| | | else { |
| | | message.error(res?.message || '启动失败') |
| | | } |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | async function handleBatchRetry() { |
| | | if (!planId.value) return |
| | | const res = await batchRetryTestPlan(planId.value) |
| | | if (res.code === 200) { |
| | | message.success('重试已提交') |
| | | await refreshBatchData() |
| | | if (isBatchRunning.value) startBatchPolling() |
| | | } |
| | | else { |
| | | message.error(res?.message || '重试失败') |
| | | } |
| | | } |
| | | |
| | | async function handleBatchCancel() { |
| | | if (!planId.value) return |
| | | dialog.warning({ |
| | | title: '确认取消批量测试?', |
| | | content: '取消后未完成的订单将不再执行', |
| | | positiveText: '确定', |
| | | negativeText: '取消', |
| | | maskClosable: false, |
| | | autoFocus: false, |
| | | onPositiveClick: async () => { |
| | | const res = await batchCancelTestPlan(planId.value) |
| | | if (res.code === 200) { |
| | | message.success('已取消') |
| | | stopBatchPolling() |
| | | await refreshBatchData() |
| | | } |
| | | else { |
| | | message.error(res?.message || '取消失败') |
| | | } |
| | | }, |
| | | }) |
| | | } |
| | | |
| | | const vehicleProgressTableScrollX = 960 |
| | | const vehicleProgressColumns = computed<DataTableColumns<any>>(() => [ |
| | | { |
| | | title: '序号', |
| | | key: 'index', |
| | | width: 60, |
| | | align: 'left', |
| | | render: (_row, index) => index + 1, |
| | | }, |
| | | { |
| | | title: '车牌号', |
| | | key: 'plateNumber', |
| | | width: 120, |
| | | align: 'left', |
| | | }, |
| | | { |
| | | title: '当前圈数/总圈数', |
| | | key: 'currentLap', |
| | | width: 140, |
| | | align: 'left', |
| | | render: row => `${row.currentLap || 0}/${row.totalLaps || '-'}`, |
| | | }, |
| | | { |
| | | title: '当前段/总段数', |
| | | key: 'currentSegment', |
| | | width: 140, |
| | | align: 'left', |
| | | render: row => `${row.currentSegment || 0}/${row.totalSegments || '-'}`, |
| | | }, |
| | | { |
| | | title: '当前订单ID', |
| | | key: 'currentOrderId', |
| | | width: 140, |
| | | align: 'left', |
| | | render: row => row.currentOrderId || '-', |
| | | }, |
| | | { |
| | | title: '状态', |
| | | key: 'status', |
| | | width: 100, |
| | | align: 'left', |
| | | render: row => <n-tag type={getVehicleProgressStatusType(row.status)} size="small">{getVehicleProgressStatusLabel(row.status)}</n-tag>, |
| | | }, |
| | | { |
| | | title: '错误信息', |
| | | key: 'errorMsg', |
| | | width: 200, |
| | | align: 'left', |
| | | ellipsis: true, |
| | | render: row => row.errorMsg || '-', |
| | | }, |
| | | ]) |
| | | |
| | | // 监听批次状态,自动停止轮询 |
| | | watch(() => batchProgress.value?.status, (status) => { |
| | | if (status && status !== 'RUNNING') { |
| | | stopBatchPolling() |
| | | } |
| | | }) |
| | | |
| | | onMounted(() => { |
| | | if (isEdit.value || isDetail.value) { |
| | | planId.value = String(route.query.id || '') |
| | | getDetail() |
| | | getOrderList() |
| | | refreshBatchData() |
| | | } |
| | | else { |
| | | initCreatePlanId() |
| | | setInitialSnapshot() |
| | | } |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | stopBatchPolling() |
| | | }) |
| | | |
| | | watch(() => route.query.id, (id) => { |
| | |
| | | if (isEdit.value || isDetail.value) { |
| | | getDetail() |
| | | getOrderList() |
| | | refreshBatchData() |
| | | } |
| | | }) |
| | | </script> |
| | |
| | | /> |
| | | </n-card> |
| | | |
| | | <!-- 批量测试区域:详情/编辑模式下显示 --> |
| | | <template v-if="isDetail || isEdit"> |
| | | <n-card style="border-radius: 6px" content-style="padding: 24px;"> |
| | | <div class="mb-4 flex items-center justify-between"> |
| | | <div class="flex items-center"> |
| | | <div class="mr-2 bg-blue-500" style="width: 3px; height: 14px" /> |
| | | <span class="font-bold" style="font-size: 16px">批量测试</span> |
| | | </div> |
| | | <div class="flex items-center gap-3"> |
| | | <div class="flex items-center gap-2"> |
| | | <span class="text-sm">圈数:</span> |
| | | <n-input-number |
| | | v-model:value="lapCount" |
| | | :min="1" |
| | | :max="100" |
| | | style="width: 100px" |
| | | :disabled="isBatchRunning" |
| | | /> |
| | | </div> |
| | | <n-button |
| | | type="primary" |
| | | @click="handleBatchStart" |
| | | :disabled="isBatchRunning" |
| | | > |
| | | 启动批量测试 |
| | | </n-button> |
| | | <n-button |
| | | @click="handleBatchRetry" |
| | | :disabled="!batchProgress || batchProgress.errorCount === 0" |
| | | > |
| | | 重试失败 |
| | | </n-button> |
| | | <n-button |
| | | type="error" |
| | | @click="handleBatchCancel" |
| | | :disabled="!isBatchRunning" |
| | | > |
| | | 取消 |
| | | </n-button> |
| | | </div> |
| | | </div> |
| | | </n-card> |
| | | |
| | | <!-- 批次进度卡片 --> |
| | | <n-card v-if="batchProgress" style="border-radius: 6px" content-style="padding: 24px;"> |
| | | <div class="mb-4 flex items-center"> |
| | | <div class="mr-2 bg-blue-500" style="width: 3px; height: 14px" /> |
| | | <span class="font-bold" style="font-size: 16px">批次进度</span> |
| | | </div> |
| | | <div class="flex items-center gap-6 mb-4"> |
| | | <n-tag :type="getBatchStatusType(batchProgress.status)" size="medium"> |
| | | {{ getBatchStatusLabel(batchProgress.status) }} |
| | | </n-tag> |
| | | <span class="text-sm text-gray-500" v-if="batchProgress.startTime"> |
| | | 开始:{{ dayjs(batchProgress.startTime).format('YYYY/MM/DD HH:mm:ss') }} |
| | | </span> |
| | | <span class="text-sm text-gray-500" v-if="batchProgress.endTime"> |
| | | 结束:{{ dayjs(batchProgress.endTime).format('YYYY/MM/DD HH:mm:ss') }} |
| | | </span> |
| | | </div> |
| | | <n-progress |
| | | type="line" |
| | | :percentage="batchProgressPercent" |
| | | :status="batchProgress.status === 'FAILED' ? 'error' : batchProgress.status === 'SUCCESS' ? 'success' : 'default'" |
| | | :height="20" |
| | | :show-indicator="true" |
| | | class="mb-4" |
| | | /> |
| | | <div class="flex gap-8 text-sm"> |
| | | <div>总订单数:<span class="font-bold">{{ batchProgress.totalCount || 0 }}</span></div> |
| | | <div>已创建:<span class="font-bold text-blue-600">{{ batchProgress.createdCount || 0 }}</span></div> |
| | | <div>已完成:<span class="font-bold text-green-600">{{ batchProgress.completedCount || 0 }}</span></div> |
| | | <div>异常数:<span class="font-bold" :class="batchProgress.errorCount > 0 ? 'text-red-600' : ''">{{ batchProgress.errorCount || 0 }}</span></div> |
| | | </div> |
| | | </n-card> |
| | | |
| | | <!-- 车辆进度卡片 --> |
| | | <n-card v-if="batchProgress" style="border-radius: 6px; margin-bottom: 80px" content-style="padding: 24px;"> |
| | | <div class="mb-4 flex items-center"> |
| | | <div class="mr-2 bg-blue-500" style="width: 3px; height: 14px" /> |
| | | <span class="font-bold" style="font-size: 16px">车辆进度</span> |
| | | </div> |
| | | <n-data-table |
| | | :columns="vehicleProgressColumns" |
| | | :data="vehicleProgressList" |
| | | :bordered="false" |
| | | :scroll-x="vehicleProgressTableScrollX" |
| | | :header-height="18" |
| | | :row-height="18" |
| | | > |
| | | <template #empty> |
| | | <div class="flex-center"> |
| | | <img src="/src/assets/element-img/no-data.png" style="width:123px;height:125px"> |
| | | </div> |
| | | </template> |
| | | </n-data-table> |
| | | </n-card> |
| | | </template> |
| | | |
| | | <div |
| | | class="absolute bottom-0 left-0 right-0 flex justify-end gap-3 bg-white px-6 py-4" |
| | | style="box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); z-index: 100" |