ai-coding
2026-08-07 7fc89a7dca9475305454712d1802822e2fb756b5
feat(test-plan): 新增计划详情步骤条并支持滚动联动导航

Co-Authored-By: 祝美莲 <zhuml4@xiaopeng.com>
7个文件已删除
1个文件已修改
1371 ■■■■■ 已修改文件
.kiro/specs/dms-behavior-monitor/design.md 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.kiro/specs/dms-behavior-monitor/requirements.md 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.kiro/specs/dms-behavior-monitor/summary.md 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.kiro/specs/dms-behavior-monitor/tasks.md 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.kiro/tests/dms-behavior-monitor/test-cases.md 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.kiro/tests/dms-behavior-monitor/test-report.md 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.kiro/tests/dms-behavior-monitor/test-ui.cjs 235 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanList/add.vue 306 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.kiro/specs/dms-behavior-monitor/design.md
File was deleted
.kiro/specs/dms-behavior-monitor/requirements.md
File was deleted
.kiro/specs/dms-behavior-monitor/summary.md
File was deleted
.kiro/specs/dms-behavior-monitor/tasks.md
File was deleted
.kiro/tests/dms-behavior-monitor/test-cases.md
File was deleted
.kiro/tests/dms-behavior-monitor/test-report.md
File was deleted
.kiro/tests/dms-behavior-monitor/test-ui.cjs
File was deleted
src/views/testDemo/testPlanList/add.vue
@@ -1,5 +1,8 @@
<script setup lang="tsx">
import noDataImg from '@/assets/element-img/no-data.png'
import activeIcon from '@/assets/pricingManagement/active.png'
import waitIcon from '@/assets/pricingManagement/wait.png'
import type { ComponentPublicInstance } from 'vue'
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTabStore } from '@/store'
@@ -104,6 +107,41 @@
const pageTitle = ref(isDetail.value ? '测试计划详情' : isEdit.value ? '编辑测试计划' : '添加测试计划')
const initialSnapshot = ref('')
// 步骤条:与 testPlanManagement/add.vue 保持一致的视觉样式(图片图标 + 蓝色高亮)。
// 当前页面本身没有分步向导逻辑(所有区块可一次性保存),因此步骤条按滚动位置联动,
// 点击步骤标题可平滑滚动到对应区块,不改变原有的保存/取消行为。
const stepItems = ['基础信息', '测试车辆', '测试点位信息', '订单列表']
const currentStep = ref<number>(1)
const scrollContainerRef = ref<HTMLElement | null>(null)
const sectionRefs = ref<(HTMLElement | null)[]>([])
function setSectionRef(el: Element | ComponentPublicInstance | null, index: number) {
  sectionRefs.value[index] = el as HTMLElement | null
}
function handleStepClick(index: number) {
  const target = sectionRefs.value[index]
  if (!target)
    return
  target.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
function updateCurrentStepByScroll() {
  const container = scrollContainerRef.value
  if (!container)
    return
  const containerTop = container.getBoundingClientRect().top
  let activeIndex = 0
  sectionRefs.value.forEach((section, index) => {
    if (!section)
      return
    const offsetTop = section.getBoundingClientRect().top - containerTop
    if (offsetTop <= 80)
      activeIndex = index
  })
  currentStep.value = activeIndex + 1
}
function syncPageTitle() {
  pageTitle.value = isDetail.value ? '测试计划详情' : isEdit.value ? '编辑测试计划' : '添加测试计划'
@@ -1242,10 +1280,14 @@
    initCreatePlanId()
    setInitialSnapshot()
  }
  nextTick(() => {
    scrollContainerRef.value?.addEventListener('scroll', updateCurrentStepByScroll, { passive: true })
  })
})
onUnmounted(() => {
  stopBatchPolling()
  scrollContainerRef.value?.removeEventListener('scroll', updateCurrentStepByScroll)
})
watch(() => route.query.id, (id) => {
@@ -1264,93 +1306,139 @@
</script>
<template>
  <div class="h-full flex flex-col gap-4">
    <n-card style="border-radius: 6px" content-style="padding: 16px 24px;">
      <div class="text-lg font-bold">
        {{ pageTitle }}
      </div>
    </n-card>
    <n-card style="border-radius: 6px">
      <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-2">
        <div style="font-size: 13px">
          <span style="color: #ff4d4f">*</span>计划名称
        </div>
        <n-input
          v-model:value="planName" class="flex-1" placeholder="请输入" clearable style="width: 400px" maxlength="30"
          show-count :disabled="isReadOnly"
        />
      </div>
    </n-card>
    <n-card style="border-radius: 6px" content-style="padding: 24px;">
      <div class="mb-4 flex items-center justify-between gap-2">
        <!-- <div class="text-sm">
          测试车辆
        </div> -->
        <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 gap-2">
          <n-button style="width: 84px;" :disabled="isReadOnly" @click="handleResetVehicles">
            重置
          </n-button>
          <n-button style="width: 84px;" type="primary" secondary :disabled="isReadOnly" @click="handleSelectVehicle">
            选择
          </n-button>
        </div>
      </div>
      <n-data-table
        :columns="vehicleColumns" :data="vehicles" :bordered="false" :scroll-x="vehicleTableScrollX"
        :header-height="18" :row-height="18" :row-key="row => row.id"
      >
        <template #empty>
          <div class="flex-center">
            <img :src="noDataImg" style="width:123px;height:125px">
          </div>
        </template>
      </n-data-table>
    </n-card>
    <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>
        <n-button
          style="width: 84px;" type="primary" secondary :disabled="isReadOnly" @click="handleAddPoint"
  <div class="w-full h-full relative content flex flex-col">
    <!-- 顶部步骤条:样式与 testPlanManagement/add.vue 保持一致 -->
    <div class="mb-12px shrink-0 flex justify-center mt-12px">
      <div class="custom-steps">
        <div
          v-for="(stepTitle, index) in stepItems"
          :key="stepTitle"
          class="step-item"
          style="cursor: pointer"
          @click="handleStepClick(index)"
        >
          添加
        </n-button>
      </div>
      <n-data-table
        :columns="testPointColumns" :data="testPoints" :bordered="false" :scroll-x="testPointTableScrollX"
        :header-height="18" :row-height="18" :row-key="row => row.id"
      >
        <template #empty>
          <div class="flex-center">
            <img :src="noDataImg" style="width:123px;height:125px">
          <div class="step-node-wrap">
            <svg
              v-if="currentStep > index + 1"
              class="step-icon"
              viewBox="0 0 24 24"
              width="24"
              height="24"
              alt="step-icon"
            >
              <circle cx="12" cy="12" r="11" fill="#fff" stroke="var(--primary-color)" stroke-width="2" />
              <path d="M7 12.5 L10.5 16 L17 8.5" fill="none" stroke="var(--primary-color)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
            </svg>
            <img
              v-else
              :src="currentStep === index + 1 ? waitIcon : activeIcon"
              class="step-icon"
              alt="step-icon"
            >
            <span
              class="step-title"
              :class="{ 'step-title--active': currentStep === index + 1 }"
            >
              {{ stepTitle }}
            </span>
          </div>
        </template>
      </n-data-table>
    </n-card>
    <n-card
      style="border-radius: 6px;  min-height: 400px"
      content-style="display: flex; flex-direction: column; 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
            v-if="index < stepItems.length - 1"
            class="step-line"
            :class="{ 'step-line--active': currentStep > index + 1 }"
          />
        </div>
      </div>
    </div>
    <div ref="scrollContainerRef" class="flex-1 overflow-auto flex flex-col gap-4 pb-80px">
      <n-card style="border-radius: 6px" content-style="padding: 16px 24px;">
        <div class="text-lg font-bold">
          {{ pageTitle }}
        </div>
      </n-card>
      <n-card :ref="el => setSectionRef(el, 0)" style="border-radius: 6px">
        <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-2">
          <div style="font-size: 13px">
            <span style="color: #ff4d4f">*</span>计划名称
          </div>
          <n-input
            v-model:value="planName" class="flex-1" placeholder="请输入" clearable style="width: 400px" maxlength="30"
            show-count :disabled="isReadOnly"
          />
        </div>
      </n-card>
      <n-card :ref="el => setSectionRef(el, 1)" style="border-radius: 6px" content-style="padding: 24px;">
        <div class="mb-4 flex items-center justify-between gap-2">
          <!-- <div class="text-sm">
            测试车辆
          </div> -->
          <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 gap-2">
            <n-button style="width: 84px;" :disabled="isReadOnly" @click="handleResetVehicles">
              重置
            </n-button>
            <n-button style="width: 84px;" type="primary" secondary :disabled="isReadOnly" @click="handleSelectVehicle">
              选择
            </n-button>
          </div>
        </div>
        <n-data-table
          :columns="vehicleColumns" :data="vehicles" :bordered="false" :scroll-x="vehicleTableScrollX"
          :header-height="18" :row-height="18" :row-key="row => row.id"
        >
          <template #empty>
            <div class="flex-center">
              <img :src="noDataImg" style="width:123px;height:125px">
            </div>
          </template>
        </n-data-table>
      </n-card>
      <n-card :ref="el => setSectionRef(el, 2)" 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>
          <n-button
            style="width: 84px;" type="primary" secondary :disabled="isReadOnly" @click="handleAddPoint"
          >
            添加
          </n-button>
        </div>
        <n-data-table
          :columns="testPointColumns" :data="testPoints" :bordered="false" :scroll-x="testPointTableScrollX"
          :header-height="18" :row-height="18" :row-key="row => row.id"
        >
          <template #empty>
            <div class="flex-center">
              <img :src="noDataImg" style="width:123px;height:125px">
            </div>
          </template>
        </n-data-table>
      </n-card>
      <n-card
        :ref="el => setSectionRef(el, 3)"
        style="border-radius: 6px;  min-height: 400px"
        content-style="display: flex; flex-direction: column; 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-form
        :model="orderSearchModel"
@@ -1528,6 +1616,7 @@
        </n-data-table>
      </n-card>
    </template>
    </div>
    <div
      class="absolute bottom-0 left-0 right-0 flex justify-end gap-3 bg-white px-6 py-4"
@@ -1683,4 +1772,53 @@
    border: 1px solid #7ABDFF;
    background-color: #E6F4FF;
  }
.custom-steps {
  width: 100%;
  margin: 0 auto;
  padding-top: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.step-item {
  display: flex;
  align-items: center;
}
.step-node-wrap {
  display: flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
}
.step-icon {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
}
.step-title {
  font-size: 14px;
  line-height: 22px;
  font-weight: 500;
  color: rgba(23, 31, 38, 0.8);
}
.step-title--active {
  color: #0068ff;
}
.step-line {
  width: 160px;
  height: 2px;
  margin: 0 8px;
  background-color: rgba(23, 31, 38, 0.1);
}
.step-line--active {
  background-color: #0068ff;
}
</style>