| | |
| | | <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' |
| | |
| | | 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 ? '编辑测试计划' : '添加测试计划' |
| | |
| | | initCreatePlanId() |
| | | setInitialSnapshot() |
| | | } |
| | | nextTick(() => { |
| | | scrollContainerRef.value?.addEventListener('scroll', updateCurrentStepByScroll, { passive: true }) |
| | | }) |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | stopBatchPolling() |
| | | scrollContainerRef.value?.removeEventListener('scroll', updateCurrentStepByScroll) |
| | | }) |
| | | |
| | | watch(() => route.query.id, (id) => { |
| | |
| | | </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" |
| | |
| | | </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" |
| | |
| | | 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> |