董国庆
2026-06-01 08e2dad2c6ec73a31c81f6e55e69335ebbb8d4d8
计划页面
3个文件已添加
2个文件已修改
857 ■■■■■ 已修改文件
src/assets/svg-icons/notice.svg 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanManagement/add.vue 450 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanManagement/components/OrderGenModeDescModal.vue 129 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanManagement/components/PointSelectModal.vue 274 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/testDemo/testPlanManagement/index.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svg-icons/notice.svg
New file
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="14.625" height="14.625" viewBox="0 0 14.625 14.625"><path d="M7.3125,14.625C11.351083,14.625,14.625,11.351083,14.625,7.3125C14.625,3.2739174,11.351083,0,7.3125,0C3.2739174,0,0,3.2739174,0,7.3125C0,11.351083,3.2739174,14.625,7.3125,14.625ZM7.3125,1.125C10.729762,1.125,13.5,3.8952379,13.5,7.3125C13.5,10.729762,10.729762,13.5,7.3125,13.5C3.8952379,13.5,1.125,10.729762,1.125,7.3125C1.125,3.8952379,3.8952379,1.125,7.3125,1.125Z" fill-rule="evenodd" fill="#0256FF" fill-opacity="1"/><rect x="6.75" y="10.125" width="1.125" height="1.125" rx="0" fill="#0256FF" fill-opacity="1"/><path d="M6.7500002,9L6.7500002,8.3611798Q6.7500002,7.9198027,7.0592413,7.5777512Q7.3212237,7.2879734,7.7100613,7.176924Q8.1544425,7.0500126000000005,8.435742600000001,6.6880937Q8.71875,6.3239772,8.71875,5.8660722Q8.71875,5.3036302,8.3109901,4.9041922Q7.898376900000001,4.4999999,7.3124993,4.5Q6.7266227,4.4999999,6.3140098,4.904192Q5.90625,5.3036302,5.90625,5.8660717L4.78125,5.866071Q4.781250067055225,4.8308314,5.52675748,4.10053861Q6.2674117,3.37499986588955,7.312499,3.375Q8.357588100000001,3.37499979883432,9.098242800000001,4.10053861Q9.84375,4.8308314,9.84375,5.8660722Q9.84375,6.7097642,9.3239932,7.3784819Q8.8170061,8.0307693,8.0190015,8.2586732Q7.875,8.299799,7.875,8.3611798L7.875,9L6.7500002,9Z" fill-rule="evenodd" fill="#0256FF" fill-opacity="1"/></svg>
src/views/testDemo/testPlanManagement/add.vue
@@ -1,17 +1,103 @@
<script setup lang="tsx">
import { ref, computed } from 'vue'
import { ref, computed, h } from 'vue'
import { useRouter } from 'vue-router'
import { NButton, NSpace, NTag, NTooltip, NIcon } from 'naive-ui'
import { NButton, NSpace, NTag, NTooltip, NIcon, NForm, NFormItem, NFormItemGi, NRadioGroup, NRadioButton, NInput, NSwitch, NDataTable, NGrid, useDialog } from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import { useMessage } from 'naive-ui'
import waitIcon from '@/assets/pricingManagement/wait.png'
import finishIcon from '@/assets/pricingManagement/finish.png'
import activeIcon from '@/assets/pricingManagement/active.png'
import noticeSvg from '@/assets/svg-icons/notice.svg'
import Pagination from '@/components/common/Pagination.vue'
import { useMessage, useDialog } from 'naive-ui'
import IconInfo from '~icons/ri/information-line'
import PointSelectModal from './components/PointSelectModal.vue'
import OrderGenModeDescModal from './components/OrderGenModeDescModal.vue'
const router = useRouter()
const message = useMessage()
const dialog = useDialog()
const message = useMessage()
const currentStep = ref<number>(1)
// ========== Modals ==========
const pointSelectModalVisible = ref(false)
const orderGenModeDescModalVisible = ref(false)
function handlePointSelect(selectedRows: any[]) {
  const existingIds = new Set(step1Data.value.map(item => item.id))
  const nextRows = selectedRows.filter(item => !existingIds.has(item.id))
  step1Data.value = [...step1Data.value, ...nextRows]
}
const stepItems = ['测试点位选择', '订单生成结果预览', '测试车辆选择']
// ========== Common Actions ==========
function handleClearPoints() {
  if (step1Data.value.length === 0)
    return
  dialog.warning({
    title: '是否确认一键清空?',
    content: '清空后,需重新选择测试点位',
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: () => {
      step1Data.value = []
      step1CheckedRowKeys.value = []
      message.success('已清空测试点位')
    }
  })
}
function handleClearVehicles() {
  if (step3Data.value.length === 0)
    return
  dialog.warning({
    title: '是否确认一键清空?',
    content: '清空后,需重新选择测试车辆',
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: () => {
      step3Data.value = []
      step3CheckedRowKeys.value = []
      message.success('已清空测试车辆')
    }
  })
}
function handleBatchRemovePoints() {
  if (step1CheckedRowKeys.value.length === 0) {
    message.warning('请先勾选需要移除的点位')
    return
  }
  dialog.warning({
    title: '是否确认批量移除?',
    content: '移除后,需重新选择测试点位',
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: () => {
      step1Data.value = step1Data.value.filter(item => !step1CheckedRowKeys.value.includes(item.id))
      step1CheckedRowKeys.value = []
      message.success('已移除选中的测试点位')
    }
  })
}
function handleBatchRemoveVehicles() {
  if (step3CheckedRowKeys.value.length === 0) {
    message.warning('请先勾选需要移除的车辆')
    return
  }
  dialog.warning({
    title: '是否确认批量移除?',
    content: '移除后,需重新选择测试车辆',
    positiveText: '确定',
    negativeText: '取消',
    onPositiveClick: () => {
      step3Data.value = step3Data.value.filter(item => !step3CheckedRowKeys.value.includes(item.id))
      step3CheckedRowKeys.value = []
      message.success('已移除选中的测试车辆')
    }
  })
}
// ================= Step 1: 测试点位选择 =================
const planName = ref('')
@@ -159,54 +245,79 @@
</script>
<template>
  <div class="roboTaxi flex flex-col h-full bg-[#F3F4F7] p-16px box-border">
    <div class="bg-white rounded-8px flex-1 flex flex-col min-h-0 overflow-hidden relative pb-64px">
  <div class="w-full h-full relative content flex flex-col bg-[#F3F4F7]">
    <div class="p-12px main flex flex-col flex-1 overflow-hidden">
      <!-- 顶部步骤条 -->
      <div class="pt-24px pb-24px px-60px border-b border-[#F0F2F5] flex justify-center">
        <div class="w-[600px]">
          <n-steps :current="currentStep" status="process">
            <n-step title="测试点位选择" />
            <n-step title="订单生成结果预览" />
            <n-step title="测试车辆选择" />
          </n-steps>
      <div class="mb-24px shrink-0 flex justify-center mt-12px">
        <div class="custom-steps">
          <div
            v-for="(stepTitle, index) in stepItems"
            :key="stepTitle"
            class="step-item"
          >
            <div class="step-node-wrap">
              <img
                :src="currentStep > index + 1 ? finishIcon : 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>
            <div
              v-if="index < stepItems.length - 1"
              class="step-line"
              :class="{ 'step-line--active': currentStep > index + 1 }"
            />
          </div>
        </div>
      </div>
      <!-- 内容区: 滚动 -->
      <div class="flex-1 overflow-auto p-24px flex flex-col min-h-0">
      <div class="box flex-1 overflow-auto p-24px flex flex-col min-h-0 bg-white rounded-8px">
        
        <!-- ================= Step 1 ================= -->
        <div v-show="currentStep === 1" class="flex flex-col h-full">
        <div v-show="currentStep === 1" class="flex-1 flex flex-col min-h-0">
          <!-- 表单 -->
          <div class="flex items-center space-x-40px mb-24px bg-[#F8FAFC] p-16px rounded-8px">
            <div class="flex items-center">
              <span class="text-[#FF4D4F] mr-4px">*</span>
              <span class="text-14px text-[#333] mr-12px whitespace-nowrap">计划名称</span>
              <n-input v-model:value="planName" placeholder="请输入" class="w-[300px]" />
            </div>
            <div class="flex items-center">
              <span class="text-[#FF4D4F] mr-4px">*</span>
              <span class="text-14px text-[#333] mr-12px whitespace-nowrap">订单生成方式</span>
              <n-radio-group v-model:value="orderGenMode" name="orderGenMode">
                <n-radio-button :value="1">全排列模式</n-radio-button>
                <n-radio-button :value="2">串烧模式</n-radio-button>
              </n-radio-group>
              <n-tooltip trigger="hover">
                <template #trigger>
                  <n-icon size="16" class="text-[#999] ml-8px cursor-pointer"><IconInfo /></n-icon>
                </template>
                订单生成方式说明
              </n-tooltip>
            </div>
          </div>
          <n-form label-placement="left" require-mark-placement="left" class="mb-12px shrink-0">
            <n-grid :cols="24" :x-gap="24">
              <n-form-item-gi :span="12" label="计划名称" required>
                <n-input v-model:value="planName" placeholder="请输入" class="w-full" />
              </n-form-item-gi>
              <n-form-item-gi :span="12" label="订单生成方式" required>
                <div class="flex items-center">
                  <n-radio-group v-model:value="orderGenMode" name="orderGenMode">
                    <n-radio-button :value="1">全排列模式</n-radio-button>
                    <n-radio-button :value="2">串烧模式</n-radio-button>
                  </n-radio-group>
                  <n-tooltip trigger="hover">
                    <template #trigger>
                      <img
                        :src="noticeSvg"
                        class="ml-8px cursor-pointer"
                        style="width: 16px; height: 16px;"
                        alt="notice"
                        @click="orderGenModeDescModalVisible = true"
                      />
                    </template>
                    订单生成方式说明
                  </n-tooltip>
                </div>
              </n-form-item-gi>
            </n-grid>
          </n-form>
          <!-- 列表操作 -->
          <div class="flex justify-between items-center mb-16px">
            <n-space>
              <n-button>一键清空</n-button>
              <n-button>批量移除</n-button>
            </n-space>
            <n-button type="primary" secondary style="background-color: #E6F4FF; border-color: #7ABDFF; color: #0068FF;">选择点位</n-button>
          <!-- 第一步表格操作区 -->
          <div class="mb-12px shrink-0 flex gap-3 justify-between items-center">
            <div class="flex gap-2">
              <n-button @click="handleClearPoints">一键清空</n-button>
              <n-button @click="handleBatchRemovePoints">批量移除</n-button>
            </div>
            <n-button type="primary" secondary @click="pointSelectModalVisible = true">选择点位</n-button>
          </div>
          <!-- 表格 -->
@@ -219,26 +330,33 @@
              v-model:checked-row-keys="step1CheckedRowKeys"
              flex-height
              :bordered="false"
              :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>
            <Pagination
              :count="1000"
              :page="1"
              :page-size="10"
              class="robotaxi-pagination mt-12px"
              class="robotaxi-pagination mt-12px shrink-0"
            />
          </div>
        </div>
        <!-- ================= Step 2 ================= -->
        <div v-show="currentStep === 2" class="flex flex-col h-full">
        <div v-show="currentStep === 2" class="flex-1 flex flex-col min-h-0">
          <!-- 列表操作 -->
          <div class="flex justify-between items-center mb-16px">
            <n-space>
          <div class="mb-12px shrink-0 flex gap-3 justify-between items-center">
            <div class="flex gap-2">
              <n-button>批量移除</n-button>
              <n-button>一键删除失败订单</n-button>
            </n-space>
            </div>
            <div class="flex items-center">
              <span class="text-14px text-[#333] mr-12px">车辆到达下车点是否停靠</span>
              <n-switch v-model:value="isStopAtDropoff" />
@@ -256,51 +374,62 @@
              flex-height
              :bordered="false"
              :scroll-x="1200"
              :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>
            <Pagination
              :count="1000"
              :page="1"
              :page-size="10"
              class="robotaxi-pagination mt-12px"
              class="robotaxi-pagination mt-12px shrink-0"
            />
          </div>
        </div>
        <!-- ================= Step 3 ================= -->
        <div v-show="currentStep === 3" class="flex flex-col h-full">
        <div v-show="currentStep === 3" class="flex-1 flex flex-col min-h-0">
          <!-- 顶部分类 -->
          <div class="mb-16px">
            <n-radio-group v-model:value="issueMode" name="issueMode" size="large">
          <div class="mb-16px shrink-0">
            <n-radio-group v-model:value="issueMode" name="issueMode" class="custom-radio-tabs">
              <n-radio-button :value="1">
                按订单顺序下发
                <n-tooltip trigger="hover">
                  <template #trigger>
                    <n-icon size="14" class="text-[#999] ml-4px align-middle"><IconInfo /></n-icon>
                  </template>
                  按订单顺序说明
                </n-tooltip>
                <span class="flex items-center">
                  按订单顺序下发
                  <n-tooltip trigger="hover">
                    <template #trigger>
                      <img :src="noticeSvg" class="ml-4px" style="width: 14.6px; height: 14.6px;" alt="notice" />
                    </template>
                    按订单顺序说明
                  </n-tooltip>
                </span>
              </n-radio-button>
              <n-radio-button :value="2">
                指定车辆下发
                <n-tooltip trigger="hover">
                  <template #trigger>
                    <n-icon size="14" class="text-[#999] ml-4px align-middle"><IconInfo /></n-icon>
                  </template>
                  指定车辆说明
                </n-tooltip>
                <span class="flex items-center">
                  指定车辆下发
                  <n-tooltip trigger="hover">
                    <template #trigger>
                      <img :src="noticeSvg" class="ml-4px" style="width: 14.6px; height: 14.6px;" alt="notice" />
                    </template>
                    指定车辆说明
                  </n-tooltip>
                </span>
              </n-radio-button>
            </n-radio-group>
          </div>
          <!-- 列表操作 -->
          <div class="flex justify-between items-center mb-16px">
            <n-space>
              <n-button>一键清空车辆</n-button>
              <n-button>批量移除</n-button>
            </n-space>
            <n-button type="primary" secondary style="background-color: #E6F4FF; border-color: #7ABDFF; color: #0068FF;">选择测试车辆</n-button>
          <!-- 第三步表格操作区 -->
          <div class="mb-12px shrink-0 flex gap-3 justify-between items-center">
            <div class="flex gap-2">
              <n-button class="n-button--secondary" @click="handleClearVehicles">一键清空车辆</n-button>
              <n-button class="n-button--secondary" @click="handleBatchRemoveVehicles">批量移除</n-button>
            </div>
            <n-button type="primary" secondary>选择测试车辆</n-button>
          </div>
          <!-- 表格 -->
@@ -313,41 +442,156 @@
              v-model:checked-row-keys="step3CheckedRowKeys"
              flex-height
              :bordered="false"
              :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>
            <Pagination
              :count="1000"
              :page="1"
              :page-size="10"
              class="robotaxi-pagination mt-12px"
              class="robotaxi-pagination mt-12px shrink-0"
            />
          </div>
        </div>
      </div>
      <!-- 底部操作按钮 -->
      <div class="absolute bottom-0 left-0 right-0 h-64px bg-white border-t border-[#F0F2F5] flex items-center justify-end px-24px z-10 shadow-[0_-2px_8px_rgba(0,0,0,0.05)]">
        <n-space>
          <n-button v-if="currentStep === 1" @click="handleCancel">取消</n-button>
          <n-button v-if="currentStep > 1" @click="handlePrev">上一步</n-button>
          <n-button v-if="currentStep < 3" type="primary" @click="handleNext">下一步</n-button>
          <n-button v-if="currentStep === 3" type="primary" @click="handleSubmit">开始下发</n-button>
        </n-space>
      </div>
    </div>
    <!-- 底部操作栏 -->
    <div class="footer flex justify-end items-center shrink-0">
      <NSpace>
        <NButton v-if="currentStep === 1" class="w-84px" @click="handleCancel">
          取消
        </NButton>
        <NButton v-if="currentStep > 1" class="w-84px" @click="handlePrev">
          上一步
        </NButton>
        <NButton v-if="currentStep < 3" type="primary" class="w-84px" @click="handleNext">
          下一步
        </NButton>
        <NButton v-if="currentStep === 3" type="primary" class="w-84px" @click="handleSubmit">
          开始下发
        </NButton>
      </NSpace>
    </div>
    <PointSelectModal
      v-model:visible="pointSelectModalVisible"
      @confirm="handlePointSelect"
    />
    <OrderGenModeDescModal
      v-model:visible="orderGenModeDescModalVisible"
    />
  </div>
</template>
<style scoped>
:deep(.n-steps) {
  --n-indicator-icon-size: 24px;
<style scoped lang="scss">
.custom-steps {
  width: 100%;
  margin: 0 auto;
  padding-top: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
}
:deep(.n-step-indicator) {
  width: 28px !important;
  height: 28px !important;
  font-size: 14px !important;
.step-item {
  display: flex;
  align-items: center;
}
</style>
.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;
}
.footer {
  position: sticky;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 64px;
  background-color: #fff;
  box-shadow: 0px 0px 5px 0px rgba(23, 31, 38, 0.05);
  padding: 16px 24px;
}
.box {
  padding: 24px;
  background-color: #fff;
  border-radius: 8px;
  width: 100%;
  box-sizing: border-box;
  --n-label-text-color: rgba(23, 31, 38, 0.8);
}
.n-button--secondary {
  box-sizing: border-box;
  color: #0068FF;
  border: 1px solid #7ABDFF;
  background-color: #E6F4FF;
}
:deep(.custom-radio-tabs) {
  .n-radio-button {
    height: 32px !important;
    line-height: 30px !important;
    font-size: 13px !important;
    padding: 0 16px !important;
  }
  .n-radio-button__state-border {
    border-radius: 4px;
  }
  .n-radio-button.n-radio-button--checked {
    color: #0068FF !important;
  }
  .n-radio-button.n-radio-button--checked .n-radio-button__state-border {
    border-color: #0068FF !important;
  }
  .n-radio-button .n-radio-button__radio-label {
    padding: 7px 16px !important;
    display: flex;
    align-items: center;
  }
}
</style>
src/views/testDemo/testPlanManagement/components/OrderGenModeDescModal.vue
New file
@@ -0,0 +1,129 @@
<script setup lang="tsx">
import { computed } from 'vue'
import { NModal, NCard } from 'naive-ui'
const props = defineProps<{
  visible: boolean
}>()
const emit = defineEmits<{
  (e: 'update:visible', val: boolean): void
}>()
const show = computed({
  get: () => props.visible,
  set: (val) => emit('update:visible', val)
})
function handleCancel() {
  show.value = false
}
</script>
<template>
  <n-modal
    v-model:show="show"
    preset="card"
    title="订单生成方式说明"
    class="w-[800px] order-gen-modal"
    :bordered="false"
  >
    <div class="flex flex-col gap-24px py-8px">
      <!-- 全排列模式 -->
      <div>
        <div class="flex items-center gap-8px mb-12px">
          <div class="w-4px h-16px bg-[#0068FF] rounded-sm"></div>
          <span class="text-16px font-bold text-[#333]">全排列模式</span>
        </div>
        <div class="bg-[#F7F8FA] rounded-8px p-20px">
          <div class="mb-12px text-[#333] text-14px">生成方式说明</div>
          <div class="flex items-center justify-between gap-32px">
            <div class="flex-1 text-[#666] text-14px leading-24px">
              输入A\B\C\D\E\F等多个点位(所有的遍历一次,正逆向,屏蔽过边界的道路和高德生成道路失败的订单,订单生成失败需要有标记),生成所有点位间的两两订单序列
              <br>
              如:A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、.....
            </div>
            <div class="w-160px h-160px bg-white rounded-8px shrink-0 flex items-center justify-center border border-[#E5E6EB]">
              <span class="text-[#CCC]">示意图占位</span>
            </div>
          </div>
        </div>
      </div>
      <!-- 串烧模式 -->
      <div>
        <div class="flex items-center gap-8px mb-12px">
          <div class="w-4px h-16px bg-[#0068FF] rounded-sm"></div>
          <span class="text-16px font-bold text-[#333]">串烧模式</span>
        </div>
        <div class="bg-[#F7F8FA] rounded-8px p-20px">
          <div class="mb-12px text-[#333] text-14px">生成方式说明</div>
          <div class="flex items-center justify-between gap-32px">
            <div class="flex-1 text-[#666] text-14px leading-24px">
              输入A\B\C\D\E\F等多个点位(所有的遍历一次,正逆向,屏蔽过边界的道路和高德生成道路失败的订单,订单生成失败需要有标记),生成所有点位间的两两订单序列
              <br>
              如:A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、A→B、.....
            </div>
            <div class="w-160px h-160px bg-white rounded-8px shrink-0 flex items-center justify-center border border-[#E5E6EB]">
              <span class="text-[#CCC]">示意图占位</span>
            </div>
          </div>
        </div>
      </div>
    </div>
  </n-modal>
</template>
<style scoped>
:deep(.order-gen-modal) {
  .n-card-header {
    padding: 24px 24px 16px;
    font-size: 16px;
    font-weight: bold;
    color: #333;
  }
  .n-card__content {
    padding: 0 24px 24px;
  }
}
.gap-24px {
  gap: 24px;
}
.gap-32px {
  gap: 32px;
}
.gap-8px {
  gap: 8px;
}
.mb-12px {
  margin-bottom: 12px;
}
.py-8px {
  padding-top: 8px;
  padding-bottom: 8px;
}
.p-20px {
  padding: 20px;
}
.w-4px {
  width: 4px;
}
.h-16px {
  height: 16px;
}
.text-16px {
  font-size: 16px;
}
.text-14px {
  font-size: 14px;
}
.leading-24px {
  line-height: 24px;
}
.w-160px {
  width: 160px;
}
.h-160px {
  height: 160px;
}
</style>
src/views/testDemo/testPlanManagement/components/PointSelectModal.vue
New file
@@ -0,0 +1,274 @@
<script setup lang="tsx">
import type { DataTableColumns } from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { NModal, NForm, NFormItemGi, NGrid, NInput, NSelect, NButton, NSpace, NDataTable, NTag } from 'naive-ui'
const props = defineProps<{
  visible: boolean
}>()
const emit = defineEmits<{
  (e: 'update:visible', val: boolean): void
  (e: 'confirm', data: any[]): void
}>()
const show = computed({
  get: () => props.visible,
  set: (val) => emit('update:visible', val)
})
interface PointRow {
  id: number
  pointName: string
  address: string
  pointType: number // 1: 测试, 2: 运营
  status: number // 1: 启用, 0: 禁用
}
const model = ref({
  pointName: '',
  pointType: null as number | null
})
const pageParams = ref({
  pageNum: 1,
  pageSize: 10
})
const pointTypeOptions = [
  { label: '测试', value: 1 },
  { label: '运营', value: 2 }
]
const listData = ref<PointRow[]>([])
const checkedRowKeys = ref<number[]>([])
const count = ref(0)
const loading = ref(false)
function getList() {
  loading.value = true
  // Mock data to match UI
  setTimeout(() => {
    listData.value = [
      { id: 1, pointName: '点位名称点位名称点位名称', address: '详细地址详细地址详细地址详细地址详细地址', pointType: 1, status: 1 },
      { id: 2, pointName: '点位名称点位名称点位名称', address: '详细地址详细地址详细地址详细地址详细地址', pointType: 2, status: 0 },
      { id: 3, pointName: '点位名称点位名称点位名称', address: '详细地址详细地址详细地址详细地址详细地址', pointType: 2, status: 1 },
      { id: 4, pointName: '点位名称点位名称点位名称', address: '详细地址详细地址详细地址详细地址详细地址', pointType: 2, status: 0 },
      { id: 5, pointName: '点位名称点位名称点位名称', address: '详细地址详细地址详细地址详细地址详细地址', pointType: 2, status: 1 },
      { id: 6, pointName: '点位名称点位名称点位名称', address: '详细地址详细地址详细地址详细地址详细地址', pointType: 2, status: 0 },
      { id: 7, pointName: '点位名称点位名称点位名称', address: '详细地址详细地址详细地址详细地址详细地址', pointType: 2, status: 0 },
      { id: 8, pointName: '点位名称点位名称点位名称', address: '详细地址详细地址详细地址详细地址详细地址', pointType: 2, status: 0 },
      { id: 9, pointName: '点位名称点位名称点位名称', address: '详细地址详细地址详细地址详细地址详细地址', pointType: 2, status: 0 }
    ]
    count.value = 50
    loading.value = false
  }, 300)
}
watch(
  () => props.visible,
  (val) => {
    if (val) {
      checkedRowKeys.value = []
      model.value = { pointName: '', pointType: null }
      pageParams.value.pageNum = 1
      getList()
    }
  }
)
function handleSearch() {
  pageParams.value.pageNum = 1
  getList()
}
function handleReset() {
  model.value = { pointName: '', pointType: null }
  pageParams.value.pageNum = 1
  getList()
}
function changePage(page: number, size: number) {
  pageParams.value.pageNum = page
  pageParams.value.pageSize = size
  getList()
}
function handleCancel() {
  show.value = false
}
function handleConfirm() {
  const selectedRows = listData.value.filter(row => checkedRowKeys.value.includes(row.id))
  emit('confirm', selectedRows)
  show.value = false
}
const columns = computed<DataTableColumns<PointRow>>(() => [
  { type: 'selection', width: 60, align: 'center' },
  { title: '序号', key: 'index', width: 80, render: (_, index) => (pageParams.value.pageNum - 1) * pageParams.value.pageSize + index + 1 },
  { title: '点位名称', key: 'pointName', width: 250 },
  { title: '详细地址', key: 'address', flex: 1 },
  { title: '点位类型', key: 'pointType', width: 120, render: (row) => row.pointType === 1 ? '测试' : '运营' },
  {
    title: '状态',
    key: 'status',
    width: 100,
    render: (row) => {
      if (row.status === 1) {
        return <NTag color={{ textColor: '#10B981', color: '#F6FFED' }} bordered={false} size="small" style="border-radius: 4px;">启用</NTag>
      }
      return <NTag color={{ textColor: '#333333', color: '#F5F5F5' }} bordered={false} size="small" style="border-radius: 4px;">禁用</NTag>
    }
  }
])
</script>
<template>
  <n-modal v-model:show="show">
    <n-card
      style="
        width: 1000px;
        height: 80vh;
        max-height: 800px;
        border-radius: 8px;
        display: flex;
        flex-direction: column;
      "
      :bordered="false"
      :segmented="{ content: true }"
      content-style="padding: 16px 24px; flex: 1; display: flex; flex-direction: column; overflow: hidden;"
      footer-style="padding: 16px 24px; border-top: 1px solid rgba(46, 61, 77, 0.1); flex-shrink: 0;"
    >
      <template #header>
        <span style="font-size: 16px; font-weight: bold">选择点位</span>
      </template>
      <template #header-extra>
        <div text @click="handleCancel">
          <span style="font-size: 18px; width: 10px; height: 10px">×</span>
        </div>
      </template>
      <div class="flex flex-col h-full overflow-hidden">
        <!-- 搜索区域 -->
        <div class="mb-3 flex items-end gap-24px" style="flex-shrink: 0">
          <div class="w-300px" style="width: 300px;">
            <div class="mb-2 text-sm text-[#333]">
              点位名称
            </div>
            <n-input
              v-model:value="model.pointName"
              placeholder="请输入"
              clearable
            />
          </div>
          <div class="w-300px" style="width: 300px;">
            <div class="mb-2 text-sm text-[#333]">
              点位类型
            </div>
            <n-select
              v-model:value="model.pointType"
              :options="pointTypeOptions"
              placeholder="请选择"
              clearable
            >
              <template #empty>
                <div class="flex-center">
                  <img src="/src/assets/element-img/no-data.png" style="width: 50%" alt="no-data">
                </div>
              </template>
            </n-select>
          </div>
          <div class="flex ml-auto gap-8px" style="gap: 8px;">
            <n-button style="width: 84px; height: 32px" @click="handleReset">
              重置
            </n-button>
            <n-button
              type="primary"
              style="width: 84px; height: 32px"
              @click="handleSearch"
            >
              查询
            </n-button>
          </div>
        </div>
        <!-- 表格区域 -->
        <div class="flex-1 overflow-hidden pt-12px" style="min-height: 0">
          <n-data-table
            v-model:checked-row-keys="checkedRowKeys"
            style="height: 100%"
            flex-height
            :columns="columns"
            :data="listData"
            :loading="loading"
            :bordered="false"
            :row-key="(row) => row.id"
            :pagination="false"
          >
            <template #empty>
              <div class="flex-center">
                <img
                  src="/src/assets/element-img/no-data.png"
                  style="width: 123px; height: 125px"
                  alt="no-data"
                >
              </div>
            </template>
          </n-data-table>
        </div>
        <Pagination
          :count="count"
          :page="pageParams.pageNum"
          :page-size="pageParams.pageSize"
          style="margin-top: 12px; flex-shrink: 0"
          @change="changePage"
        />
      </div>
      <template #footer>
        <div class="flex justify-end gap-3" style="gap: 12px;">
          <n-button class="footer-btn" @click="handleCancel">取消</n-button>
          <n-button type="primary" class="footer-btn" @click="handleConfirm">保存</n-button>
        </div>
      </template>
    </n-card>
  </n-modal>
</template>
<style scoped lang="scss">
.mb-3 {
  margin-bottom: 0.75rem;
}
.mb-2 {
  margin-bottom: 0.5rem;
}
.flex {
  display: flex;
}
.items-end {
  align-items: flex-end;
}
.gap-24px {
  gap: 24px;
}
.flex-1 {
  flex: 1 1 0%;
}
.justify-end {
  justify-content: flex-end;
}
.text-sm {
  font-size: 0.875rem;
  line-height: 1.25rem;
}
.footer-btn {
  padding: 7px 29px;
  height: auto;
  font-size: 13px;
  line-height: 18px;
}
</style>
src/views/testDemo/testPlanManagement/index.vue
@@ -3,9 +3,6 @@
import { NButton, NSpace, NTag, NTooltip } from 'naive-ui'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import OperationLogModal from '@/components/custom/OperationLogModal.vue'
import Pagination from '@/components/common/Pagination.vue'
import SearchPanel from '@/components/custom/SearchPanel.vue'
import { useBoolean } from '@/hooks'
import { useDialog, useMessage } from 'naive-ui'