From e92145a4d561e89f0f2a69f39c212a583c8de18c Mon Sep 17 00:00:00 2001
From: zjk <852185829@qq.com>
Date: 星期三, 27 五月 2026 17:41:31 +0800
Subject: [PATCH] Merge branch 'feat/deploy-1.0' of http://gitlab.xiaopeng.local:18080/robotaxi1/xp-robotaxi-web into feat/deploy-1.0

---
 src/views/testDemo/testPlanList/add.vue    |  336 +++++++++++++++++++++++++++++++++++++++++++++++
 src/service/api/copyText.ts                |   25 +++
 src/views/testDemo/testPlanList/detail.vue |    5 
 3 files changed, 356 insertions(+), 10 deletions(-)

diff --git a/src/service/api/copyText.ts b/src/service/api/copyText.ts
index 59b4fa6..9aa25b7 100644
--- a/src/service/api/copyText.ts
+++ b/src/service/api/copyText.ts
@@ -208,3 +208,28 @@
 export function fetchTestOrderDetail(id: number | string) {
   return request.Get<Service.ResponseResult<any>>(`/order/api/v1/testOrder/queryById?id=${id}`)
 }
+
+/** 批量测试 - 启动 */
+export function batchStartTestPlan(data: { testPlanId: number | string; lapCount?: number }) {
+  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/xp-test-plan/batchStart', data)
+}
+
+/** 批量测试 - 查询批次进度 */
+export function fetchBatchProgress(testPlanId: number | string) {
+  return request.Get<Service.ResponseResult<any>>('/operation/api/v1/xp-test-plan/batchProgress', { params: { testPlanId } })
+}
+
+/** 批量测试 - 查询车辆进度 */
+export function fetchVehicleProgress(batchId: number | string) {
+  return request.Get<Service.ResponseResult<any>>('/operation/api/v1/xp-test-plan/vehicleProgress', { params: { batchId } })
+}
+
+/** 批量测试 - 重试失败订单 */
+export function batchRetryTestPlan(testPlanId: number | string) {
+  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/xp-test-plan/batchRetry', null, { params: { testPlanId } })
+}
+
+/** 批量测试 - 取消批次 */
+export function batchCancelTestPlan(testPlanId: number | string) {
+  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/xp-test-plan/batchCancel', null, { params: { testPlanId } })
+}
diff --git a/src/views/testDemo/testPlanList/add.vue b/src/views/testDemo/testPlanList/add.vue
index 99ab97b..bef8926 100644
--- a/src/views/testDemo/testPlanList/add.vue
+++ b/src/views/testDemo/testPlanList/add.vue
@@ -1,5 +1,5 @@
 <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'
@@ -9,12 +9,17 @@
 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'
@@ -847,10 +852,6 @@
     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()
@@ -883,16 +884,241 @@
   }
 }
 
+// ==================== 批量测试相关 ====================
+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) => {
@@ -905,6 +1131,7 @@
   if (isEdit.value || isDetail.value) {
     getDetail()
     getOrderList()
+    refreshBatchData()
   }
 })
 </script>
@@ -1114,6 +1341,105 @@
       />
     </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"
diff --git a/src/views/testDemo/testPlanList/detail.vue b/src/views/testDemo/testPlanList/detail.vue
index 7652134..cfa2c68 100644
--- a/src/views/testDemo/testPlanList/detail.vue
+++ b/src/views/testDemo/testPlanList/detail.vue
@@ -214,11 +214,6 @@
     message.warning('请至少选择一个点位')
     return
   }
-  if (vehicles.value.length === 0) {
-    message.warning('请至少选择一辆车辆')
-    return
-  }
-
   const res = await editCruisePlan({
     id: planId.value,
     planName: name,

--
Gitblit v1.7.1