董国庆
2026-05-28 6e7b11cd012eca00cd90e110cd93afd64ecf6d00
Merge branch 'deploy-1.0' of http://120.76.84.145:10101/gitblit/r/H5/xp-robotaxi-web into deploy-1.0
5个文件已修改
377 ■■■■■ 已修改文件
src/service/api/copyText.ts 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanList/add.vue 336 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanList/detail.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/directServiceManagement/components/UserSelectModal.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/transaction/transactionStrategy/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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 } })
}
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"
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,
src/views/transaction/directServiceManagement/components/UserSelectModal.vue
@@ -122,7 +122,7 @@
      pageSize: pageSize.value,
      nickName: searchModel.value.nickName || undefined,
      account: searchModel.value.account || undefined,
      status: searchModel.value.status || undefined,
      status: searchModel.value.status,
    })
    if (res.code === 200 && res.data) {
@@ -139,9 +139,6 @@
      listData.value.forEach((item: UserItem) => {
        allLoadedData.value.set(item.id, item)
      })
    }
    else {
      window.$message?.error(res.msg || '获取用户列表失败')
    }
  }
  catch (error) {
src/views/transaction/transactionStrategy/index.vue
@@ -340,15 +340,15 @@
                <n-date-picker
                  v-model:value="model.exemptionStartTime" format="yyyy/MM/dd" type="date" clearable
                  class="mr-2 w-[200px]" :actions="null" :close-on-select="true" placeholder="开始时间"
                  :disabled="model.isLongValid === 1"
                  :disabled="model.isLongValid"
                />
                <span class="mr-2 text-13px">至</span>
                <n-date-picker
                  v-model:value="model.exemptionEndTime" format="yyyy/MM/dd" type="date" clearable
                  class="mr-2 w-[200px]" :actions="null" :close-on-select="true" placeholder="结束时间"
                  :disabled="model.isLongValid === 1" :is-date-disabled="isExemptionEndDateDisabled"
                  :disabled="model.isLongValid" :is-date-disabled="isExemptionEndDateDisabled"
                />
                <NCheckbox v-model:checked="model.isLongValid" :checked-value="1" :un-checked-value="0">
                <NCheckbox v-model:checked="model.isLongValid">
                  长期有效
                </NCheckbox>
              </n-form-item>