yeg
2026-08-27 0c60347d7bdae891997f298ac726610d558cdbb3
fix(test): preserve existing creation page
3个文件已修改
658 ■■■■■ 已修改文件
src/api/tests.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/TestCreate.vue 614 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/TestList.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/tests.ts
@@ -10,8 +10,7 @@
  testName?: string
  testTypeId?: number
  batteryNo?: string
  testStatus?: TestStatusCode
  monitoringStatus?: MonitoringStatusCode
  status?: string
  beginCreateTime?: string
  endCreateTime?: string
}
@@ -103,16 +102,18 @@
  NOT_MONITORED: '未监测', WAITING_DEVICE_START: '等待设备启动', MONITORING: '监测中',
  BACKFILLING: '补采中', MONITORING_ENDED: '监测结束', CANCELLED: '已取消',
}
const legacyStatusMap: Record<string, TestStatusCode> = {
  '0': 'WAITING', '1': 'RUNNING', '2': 'RUNNING_ABNORMAL', '3': 'COMPLETED',
}
function mapTest(item: TestDto): TestRecord {
  const testStatus = item.testStatus ? statusMap[item.testStatus] : '未知'
  const status = item.testStatus ? statusMap[item.testStatus] : '未知'
  const monitoringStatus = item.monitoringStatus ? monitoringStatusMap[item.monitoringStatus] : '未知'
  return {
    id: item.testId, name: item.testName || '-', type: item.testTypeName || '-', batteryNo: '-', batteryGroup: '-',
    batteryCount: Number(item.batteryCount) || 0, createdAt: item.createTime || '-', startedAt: item.startedAtUtc || '-',
    endedAt: item.endedAtUtc || '-', purpose: item.testPurpose || '-', owner: item.principalName || '-',
    remark: item.remark || '-', status: testStatus, monitoringStatus,
    editable: item.monitoringStatus === 'NOT_MONITORED',
    remark: item.remark || '-', status, monitoringStatus, editable: item.monitoringStatus === 'NOT_MONITORED',
  }
}
@@ -120,7 +121,8 @@
  const searchParams = new URLSearchParams()
  Object.entries(params).forEach(([key, value]) => {
    if (value === undefined || value === '') return
    searchParams.set(key, String(value))
    if (key === 'status') searchParams.set('testStatus', legacyStatusMap[String(value)] || String(value))
    else searchParams.set(key, String(value))
  })
  const result = await request<TestListResponse>(`/system/test/list?${searchParams.toString()}`)
  return { total: Number(result.total) || 0, rows: (result.rows || []).map(mapTest) }
@@ -129,12 +131,15 @@
export async function getTestConfigurationOptions(): Promise<TestConfigurationOptions> {
  return (await request<TestOptionsResponse>('/system/test/options')).data
}
export async function getTestDetail(testId: number): Promise<TestConfigurationDetail> {
  return (await request<TestDetailResponse>(`/system/test/${testId}`)).data
}
export async function createTest(payload: TestConfigurationPayload): Promise<TestConfigurationDetail> {
  return (await request<TestDetailResponse>('/system/test', { method: 'POST', body: JSON.stringify(payload) })).data
}
export async function updateTest(payload: TestConfigurationPayload & { testId: number }): Promise<TestConfigurationDetail> {
  return (await request<TestDetailResponse>('/system/test', { method: 'PUT', body: JSON.stringify(payload) })).data
}
src/views/TestCreate.vue
@@ -1,17 +1,16 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import {
  createTest,
  getTestConfigurationOptions,
  getTestDetail,
  updateTest,
  type TestConfigurationDetail,
  type TestConfigurationOptions,
  type TestConfigurationPayload,
  type TestPlanStepPayload,
} from '@/api/tests'
type ThresholdUnit = 'A' | 'V' | 'Ah' | 'Wh' | '℃'
interface BatteryRow {
  key: number
@@ -21,7 +20,21 @@
  cabinet: number | null
  cabinetChannel: number | null
  collector: number | null
  expectedBarcode: string
  expectedBarcode: string | null
  temperature1: string
  temperature2: string
  temperature3: string
  positive1: string
  positive2: string
  negative1: string
  negative2: string
  customChannels: Record<string, string>
}
interface CustomChannelColumn {
  key: string
  label: string
  fieldName: string
}
interface BatteryGroupOption {
@@ -36,17 +49,7 @@
  type: string
  min: string
  max: string
  unit: ThresholdUnit
}
interface PlanStepRow {
  key: number
  selected: boolean
  cycleNo: number
  stepNo: number
  stepType: string
  stepName: string
  parametersJson: string
  unit: 'A' | 'V' | 'Ah' | 'Wh' | '℃'
}
const router = useRouter()
@@ -58,9 +61,12 @@
const batteryDialogVisible = ref(false)
const selectedBatteryGroup = ref<number | null>(null)
const selectedBatteryNos = ref<string[]>([])
const configurationOptions = ref<TestConfigurationOptions | null>(null)
const preservedPlanSteps = ref<TestPlanStepPayload[]>([])
const monitorTimeoutMinutes = ref(30)
let batteryKey = 1
let thresholdKey = 1
let planStepKey = 1
let customChannelKey = 1
const form = reactive({
  name: '',
@@ -68,29 +74,53 @@
  testTypeId: null as number | null,
  purpose: '',
  remark: '',
  monitorTimeoutMinutes: 30,
  syncMode: '默认同步检测柜',
})
const owners = ref<Array<{ id: number; name: string; userName: string }>>([])
const testTypes = ref<Array<{ id: number; name: string }>>([])
const ownersLoading = ref(false)
const testTypesLoading = ref(false)
const configurationOptions = ref<TestConfigurationOptions | null>(null)
const cabinetOptions = ref<Array<{ id: number; label: string }>>([])
const collectorOptions = ref<Array<{ id: number; label: string }>>([])
const thresholdTypes = [
const sensorChannels = Array.from({ length: 12 }, (_, index) => `S-${String(index + 1).padStart(2, '0')}`)
const thresholdTypes: Array<{
  label: string
  value: string
  unit: 'A' | 'V' | 'Ah' | 'Wh' | '℃'
  disabled?: boolean
}> = [
  { label: '温度', value: 'TEMPERATURE', unit: '℃' as const },
  { label: '电压', value: 'VOLTAGE', unit: 'V' as const },
  { label: '电流', value: 'CURRENT', unit: 'A' as const },
  { label: '容量', value: 'CAPACITY', unit: 'Ah' as const },
  { label: '能量', value: 'ENERGY', unit: 'Wh' as const },
  { label: '检测柜返回项(待现场验证)', value: 'CABINET_RETURN', unit: 'V' as const, disabled: true },
]
const fixedSensorFields = ['temperature1', 'temperature2', 'temperature3', 'positive1', 'positive2', 'negative1', 'negative2'] as const
type FixedSensorField = typeof fixedSensorFields[number]
const reservedChannelLabels = ['主温度通道', '温度通道 2', '温度通道 3', '正电位主通道', '正电位通道 2', '负电位主通道', '负电位通道 2']
const customChannelColumns = ref<CustomChannelColumn[]>([])
const fixedFieldNames: Record<FixedSensorField, string> = {
  temperature1: 'temperature1', temperature2: 'temperature2', temperature3: 'temperature3',
  positive1: 'positive1', positive2: 'positive2', negative1: 'negative1', negative2: 'negative2',
}
const batteryGroupOptions = ref<BatteryGroupOption[]>([])
const cabinetOptions = computed(() => {
  const cabinets = new Map<number, string>()
  configurationOptions.value?.channels.forEach(channel => {
    cabinets.set(channel.cabinetId, [channel.cabinetCode, channel.cabinetName].filter(Boolean).join(' '))
  })
  return [...cabinets].map(([id, label]) => ({ id, label }))
})
const collectorOptions = computed(() => (configurationOptions.value?.collectors || []).map(item => ({
  id: item.collectorId,
  label: item.collectorName || item.collectorCode,
})))
function createBatteryRow(batteryNo = '', groupId: number | null = null): BatteryRow {
  return {
    key: batteryKey++, selected: false, groupId, batteryNo, cabinet: null, cabinetChannel: null, collector: null,
    expectedBarcode: '',
    expectedBarcode: null,
    temperature1: '', temperature2: '', temperature3: '', positive1: '', positive2: '', negative1: '', negative2: '',
    customChannels: Object.fromEntries(customChannelColumns.value.map(column => [column.key, ''])),
  }
}
@@ -101,13 +131,8 @@
  }
}
function createPlanStepRow(): PlanStepRow {
  return { key: planStepKey++, selected: false, cycleNo: 1, stepNo: 1, stepType: '', stepName: '', parametersJson: '{}' }
}
const batteries = ref<BatteryRow[]>([])
const thresholds = ref<ThresholdRow[]>([createThresholdRow('VOLTAGE')])
const planSteps = ref<PlanStepRow[]>([createPlanStepRow()])
const thresholds = ref<ThresholdRow[]>([createThresholdRow('TEMPERATURE'), createThresholdRow('VOLTAGE')])
const groupBatteryNos = computed(() => batteryGroupOptions.value.find(group => group.id === selectedBatteryGroup.value)?.batteries || [])
const availableGroupBatteryNos = computed(() => groupBatteryNos.value.filter(batteryNo => !isBatteryAdded(batteryNo)))
const selectedBatteryGroupName = computed(() => batteryGroupOptions.value.find(group => group.id === selectedBatteryGroup.value)?.name || '-')
@@ -116,18 +141,18 @@
  get: () => batteries.value.length > 0 && batteries.value.every(item => item.selected),
  set: (checked: boolean) => batteries.value.forEach(item => { item.selected = checked }),
})
const allThresholdsSelected = computed({
  get: () => thresholds.value.length > 0 && thresholds.value.every(item => item.selected),
  set: (checked: boolean) => thresholds.value.forEach(item => { item.selected = checked }),
})
const allPlanStepsSelected = computed({
  get: () => planSteps.value.length > 0 && planSteps.value.every(item => item.selected),
  set: (checked: boolean) => planSteps.value.forEach(item => { item.selected = checked }),
})
const allGroupBatteriesSelected = computed({
  get: () => availableGroupBatteryNos.value.length > 0
    && availableGroupBatteryNos.value.every(batteryNo => selectedBatteryNos.value.includes(batteryNo)),
  set: (checked: boolean) => { selectedBatteryNos.value = checked ? [...availableGroupBatteryNos.value] : [] },
  set: (checked: boolean) => {
    selectedBatteryNos.value = checked ? [...availableGroupBatteryNos.value] : []
  },
})
function isBatteryAdded(batteryNo: string) {
@@ -145,11 +170,18 @@
}
function confirmAddBatteries() {
  if (!selectedBatteryGroup.value) return void ElMessage.warning('请先选择电池组')
  if (batteries.value.length && batteries.value[0].groupId !== selectedBatteryGroup.value) {
    return void ElMessage.warning('同一测试只能选择一个电池组')
  if (!selectedBatteryGroup.value) {
    ElMessage.warning('请先选择电池组')
    return
  }
  if (!selectedBatteryNos.value.length) return void ElMessage.warning('请至少选择一块电池')
  if (batteries.value.length && batteries.value[0].groupId !== selectedBatteryGroup.value) {
    ElMessage.warning('同一测试只能选择一个电池组')
    return
  }
  if (!selectedBatteryNos.value.length) {
    ElMessage.warning('请至少选择一块电池')
    return
  }
  const newBatteryNos = selectedBatteryNos.value.filter(batteryNo => !isBatteryAdded(batteryNo))
  batteries.value.push(...newBatteryNos.map(batteryNo => createBatteryRow(batteryNo, selectedBatteryGroup.value)))
  batteryDialogVisible.value = false
@@ -157,54 +189,172 @@
}
function removeBatteries() {
  if (!batteries.value.some(item => item.selected)) return void ElMessage.warning('请先选择需要删除的电池')
  if (!batteries.value.some(item => item.selected)) {
    ElMessage.warning('请先选择需要删除的电池')
    return
  }
  batteries.value = batteries.value.filter(item => !item.selected)
}
function isCabinetChannelUsed(item: BatteryRow, channel: number) {
  return batteries.value.some(other => other.key !== item.key && other.cabinetChannel === channel)
function getSensorSelections(item: BatteryRow) {
  return [
    ...fixedSensorFields.map(field => ({ id: field, value: item[field] })),
    ...customChannelColumns.value.map(column => ({ id: `custom:${column.key}`, value: item.customChannels[column.key] || '' })),
  ]
}
function handleCabinetChange(item: BatteryRow) {
  if (item.cabinetChannel && !channelsForCabinet(item.cabinet).some(channel => channel.channelId === item.cabinetChannel)) {
    item.cabinetChannel = null
function clearSensorSelection(item: BatteryRow, fieldId: string) {
  if (fieldId.startsWith('custom:')) {
    item.customChannels[fieldId.slice(7)] = ''
    return
  }
  item[fieldId as FixedSensorField] = ''
}
function channelsForCabinet(cabinetId: number | null) {
  return configurationOptions.value?.channels.filter(item => item.cabinetId === cabinetId) || []
  if (!cabinetId) return []
  return (configurationOptions.value?.channels || []).filter(channel => channel.cabinetId === cabinetId)
}
function handleThresholdTypeChange(item: ThresholdRow) {
  item.unit = thresholdTypes.find(option => option.value === item.type)?.unit || item.unit
function isCabinetChannelUsed(item: BatteryRow, channelId: number) {
  return batteries.value.some(other => other.key !== item.key && other.cabinetChannel === channelId)
}
async function loadConfiguration() {
function isSensorChannelUsed(item: BatteryRow, fieldId: string, channel: string) {
  if (!item.collector) return false
  return batteries.value.some(other => {
    if (other.key !== item.key && other.collector !== item.collector) return false
    return getSensorSelections(other).some(selection => {
      if (other.key === item.key && selection.id === fieldId) return false
      return selection.value === channel
    })
  })
}
function handleCabinetChange(item: BatteryRow) {
  item.cabinetChannel = null
}
function handleCollectorChange(item: BatteryRow) {
  if (!item.collector) {
    getSensorSelections(item).forEach(selection => clearSensorSelection(item, selection.id))
    return
  }
  const occupiedChannels = new Set(
    batteries.value
      .filter(other => other.key !== item.key && other.collector === item.collector)
      .flatMap(other => getSensorSelections(other).map(selection => selection.value).filter(Boolean)),
  )
  const currentChannels = new Set<string>()
  let clearedCount = 0
  getSensorSelections(item).forEach(selection => {
    if (!selection.value) return
    if (occupiedChannels.has(selection.value) || currentChannels.has(selection.value)) {
      clearSensorSelection(item, selection.id)
      clearedCount += 1
      return
    }
    currentChannels.add(selection.value)
  })
  if (clearedCount) ElMessage.warning(`已清空 ${clearedCount} 个重复通道,请重新选择`)
}
async function addChannelField() {
  try {
    const result = await ElMessageBox.prompt('输入字段名称后,将在表格最后新增一个可选择采集器通道的列。', '添加采集器字段', {
      confirmButtonText: '添加',
      cancelButtonText: '取消',
      inputPlaceholder: '例如:壳体温度通道',
      inputValidator: (value: string) => {
        const label = value.trim()
        if (!label) return '请输入字段名称'
        if (label.length > 16) return '字段名称不能超过 16 个字'
        const normalizedLabel = label.toLocaleLowerCase()
        const labelExists = [...reservedChannelLabels, ...customChannelColumns.value.map(column => column.label)]
          .some(item => item.toLocaleLowerCase() === normalizedLabel)
        return labelExists ? '字段名称已存在' : true
      },
    })
    const label = result.value.trim()
    const key = `custom-channel-${customChannelKey++}`
    customChannelColumns.value.push({ key, label, fieldName: `custom:${label}` })
    batteries.value.forEach(item => { item.customChannels[key] = '' })
    ElMessage.success(`已添加“${label}”通道列`)
  } catch {
    // 用户取消输入时保持当前配置。
  }
}
function applyOptions(options: TestConfigurationOptions) {
  configurationOptions.value = options
  owners.value = options.users.map(item => ({
    id: item.userId,
    name: item.nickName || item.userName,
    userName: item.userName,
  }))
  testTypes.value = options.testTypes.map(item => ({ id: item.testTypeId, name: item.testTypeName }))
  const groups = new Map<number, BatteryGroupOption>()
  options.batteries.forEach(item => {
    const group = groups.get(item.groupId) || { id: item.groupId, name: item.groupName, batteries: [] }
    group.batteries.push(item.batteryNo)
    groups.set(item.groupId, group)
  })
  batteryGroupOptions.value = [...groups.values()]
}
function applyDetail(detail: TestConfigurationDetail) {
  if (detail.monitoringStatus !== 'NOT_MONITORED') {
    throw new Error('该测试已经开始监测,不能修改配置')
  }
  form.name = detail.testName
  form.principalId = detail.principalId
  form.testTypeId = detail.testTypeId
  form.purpose = detail.testPurpose
  form.remark = detail.remark || ''
  monitorTimeoutMinutes.value = detail.monitorTimeoutMinutes || 30
  preservedPlanSteps.value = detail.planSteps || []
  const fixedNames = new Set(Object.values(fixedFieldNames))
  const customFieldNames = new Set<string>()
  detail.batteries.forEach(battery => battery.fields.forEach(field => {
    if (!fixedNames.has(field.fieldName)) customFieldNames.add(field.fieldName)
  }))
  customChannelColumns.value = [...customFieldNames].map(fieldName => ({
    key: `custom-channel-${customChannelKey++}`,
    label: fieldName.startsWith('custom:') ? fieldName.slice(7) : fieldName,
    fieldName,
  }))
  batteries.value = detail.batteries.map(battery => {
    const row = createBatteryRow(battery.batteryNo, battery.groupId)
    const channel = configurationOptions.value?.channels.find(item => item.channelId === battery.binding.channelId)
    row.cabinet = channel?.cabinetId || null
    row.cabinetChannel = battery.binding.channelId
    row.collector = battery.collectorId
    row.expectedBarcode = battery.binding.expectedBarcode
    const values = new Map(battery.fields.map(field => [field.fieldName, field.fieldValue]))
    fixedSensorFields.forEach(field => { row[field] = values.get(fixedFieldNames[field]) || '' })
    customChannelColumns.value.forEach(column => { row.customChannels[column.key] = values.get(column.fieldName) || '' })
    return row
  })
  thresholds.value = detail.thresholds.map(item => ({
    key: thresholdKey++, selected: false, type: item.metricCode,
    min: item.minValue === null ? '' : String(item.minValue),
    max: item.maxValue === null ? '' : String(item.maxValue),
    unit: item.unit,
  }))
}
async function loadPage() {
  loading.value = true
  ownersLoading.value = true
  testTypesLoading.value = true
  try {
    const options = await getTestConfigurationOptions()
    configurationOptions.value = options
    owners.value = options.users.map(item => ({ id: item.userId, name: item.nickName || item.userName, userName: item.userName }))
    testTypes.value = options.testTypes.map(item => ({ id: item.testTypeId, name: item.testTypeName }))
    cabinetOptions.value = [...new Map(options.channels.map(item => [item.cabinetId, {
      id: item.cabinetId, label: `${item.cabinetName}(${item.cabinetCode})`,
    }])).values()]
    collectorOptions.value = options.collectors.map(item => ({
      id: item.collectorId, label: `${item.collectorName}(${item.collectorCode})`,
    }))
    const groups = new Map<number, BatteryGroupOption>()
    options.batteries.forEach(item => {
      const group = groups.get(item.groupId) || { id: item.groupId, name: item.groupName, batteries: [] }
      group.batteries.push(item.batteryNo)
      groups.set(item.groupId, group)
    })
    batteryGroupOptions.value = [...groups.values()]
    if (testId.value) await loadDetail(testId.value)
    applyOptions(await getTestConfigurationOptions())
    if (testId.value) applyDetail(await getTestDetail(testId.value))
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '测试配置加载失败')
    if (testId.value) await router.push('/test-list')
    if (isEdit.value) await router.push('/test-list')
  } finally {
    loading.value = false
    ownersLoading.value = false
@@ -212,69 +362,20 @@
  }
}
async function loadDetail(id: number) {
  const detail = await getTestDetail(id)
  if (detail.monitoringStatus !== 'NOT_MONITORED') throw new Error('开始监测后不能修改测试配置')
  form.name = detail.testName
  form.principalId = detail.principalId
  form.testTypeId = detail.testTypeId
  form.purpose = detail.testPurpose
  form.remark = detail.remark || ''
  form.monitorTimeoutMinutes = detail.monitorTimeoutMinutes
  batteries.value = detail.batteries.map(item => {
    const channel = configurationOptions.value?.channels.find(option => option.channelId === item.binding.channelId)
    const row = createBatteryRow(item.batteryNo, item.groupId)
    row.cabinet = channel?.cabinetId || null
    row.cabinetChannel = item.binding.channelId
    row.collector = item.collectorId
    row.expectedBarcode = item.binding.expectedBarcode || ''
    return row
  })
  planSteps.value = detail.planSteps.map(item => ({
    key: planStepKey++, selected: false, cycleNo: item.cycleNo, stepNo: item.stepNo,
    stepType: item.stepType, stepName: item.stepName, parametersJson: JSON.stringify(item.parameters),
  }))
  thresholds.value = detail.thresholds.map(item => ({
    key: thresholdKey++, selected: false, type: item.metricCode,
    min: item.minValue === null ? '' : String(item.minValue), max: item.maxValue === null ? '' : String(item.maxValue),
    unit: item.unit,
  }))
}
function addPlanStep() {
  const row = createPlanStepRow()
  const previous = planSteps.value.at(-1)
  if (previous) {
    row.cycleNo = previous.cycleNo
    row.stepNo = previous.stepNo + 1
  }
  planSteps.value.push(row)
}
function removePlanSteps() {
  if (!planSteps.value.some(item => item.selected)) return void ElMessage.warning('请先选择需要删除的计划工步')
  planSteps.value = planSteps.value.filter(item => !item.selected)
}
function addThreshold() {
  thresholds.value.push(createThresholdRow())
}
function removeThresholds() {
  if (!thresholds.value.some(item => item.selected)) return void ElMessage.warning('请先选择需要删除的阈值')
  thresholds.value = thresholds.value.filter(item => !item.selected)
function handleThresholdTypeChange(item: ThresholdRow) {
  item.unit = thresholdTypes.find(option => option.value === item.type)?.unit || item.unit
}
function parseParameters(value: string): Record<string, unknown> | null {
  try {
    const parsed: unknown = JSON.parse(value)
    return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
      ? parsed as Record<string, unknown>
      : null
  } catch {
    return null
function removeThresholds() {
  if (!thresholds.value.some(item => item.selected)) {
    ElMessage.warning('请先选择需要删除的阈值')
    return
  }
  thresholds.value = thresholds.value.filter(item => !item.selected)
}
function validate() {
@@ -282,189 +383,174 @@
  if (!form.principalId) return '请选择测试负责人'
  if (!form.testTypeId) return '请选择测试类型'
  if (!form.purpose.trim()) return '请输入测试目的'
  if (!Number.isInteger(form.monitorTimeoutMinutes) || form.monitorTimeoutMinutes < 1) return '监测超时必须为正整数'
  if (!batteries.value.length) return '至少需要配置一块电池'
  const groupIds = new Set(batteries.value.map(item => item.groupId))
  if (groupIds.has(null) || groupIds.size !== 1) return '同一测试只能选择一个电池组'
  const requiredFields: Array<[keyof BatteryRow, string]> = [
    ['batteryNo', '电池号'], ['groupId', '电池组'], ['cabinet', '检测柜'], ['cabinetChannel', '检测柜通道'],
  ]
  for (let index = 0; index < batteries.value.length; index += 1) {
    const item = batteries.value[index]
    if (!item.batteryNo.trim()) return `第 ${index + 1} 行缺少电池号`
    if (!item.cabinet) return `第 ${index + 1} 行请选择检测柜`
    if (!item.cabinetChannel) return `第 ${index + 1} 行请选择主要检测柜通道`
    for (const [field, label] of requiredFields) {
      const value = batteries.value[index][field]
      if (value === null || value === undefined || !String(value).trim()) return `第 ${index + 1} 行请选择${label}`
    }
    if (!batteries.value[index].collector && getSensorSelections(batteries.value[index]).some(item => item.value)) {
      return `第 ${index + 1} 行配置采集器通道前请先选择采集器`
    }
  }
  const batteryNos = batteries.value.map(item => item.batteryNo.trim())
  if (new Set(batteryNos).size !== batteryNos.length) return '测试内电池号不可重复'
  const groupIds = new Set(batteries.value.map(item => item.groupId))
  if (groupIds.size !== 1) return '同一测试只能选择一个电池组'
  const cabinetChannels = batteries.value.map(item => item.cabinetChannel)
  if (new Set(cabinetChannels).size !== cabinetChannels.length) return '同一检测柜通道不可重复绑定'
  if (!planSteps.value.length) return '至少需要配置一个计划工步'
  const stepIdentities = new Set<string>()
  for (let index = 0; index < planSteps.value.length; index += 1) {
    const item = planSteps.value[index]
    if (!Number.isInteger(item.cycleNo) || item.cycleNo < 1 || !Number.isInteger(item.stepNo) || item.stepNo < 1) {
      return `第 ${index + 1} 行计划工步序号必须为正整数`
    }
    if (!item.stepType.trim() || !item.stepName.trim()) return `请完整填写第 ${index + 1} 行计划工步`
    if (!parseParameters(item.parametersJson)) return `第 ${index + 1} 行工步参数必须是 JSON 对象`
    const identity = `${item.cycleNo}/${item.stepNo}`
    if (stepIdentities.has(identity)) return '计划工步的循环号和工步号不可重复'
    stepIdentities.add(identity)
  }
  const collectorChannels = batteries.value.flatMap(item => [
    ...fixedSensorFields.map(field => item[field]),
    ...customChannelColumns.value.map(column => item.customChannels[column.key]),
  ].filter(Boolean).map(channel => `${item.collector}/${channel}`))
  if (new Set(collectorChannels).size !== collectorChannels.length) return '同一采集器的采集通道不可重复'
  if (!thresholds.value.length) return '至少需要配置一项预警阈值'
  const metricCodes = new Set<string>()
  for (let index = 0; index < thresholds.value.length; index += 1) {
    const item = thresholds.value[index]
    if (!item.type) return `请选择第 ${index + 1} 行阈值类型`
    if (metricCodes.has(item.type)) return '同一阈值类型不可重复配置'
    metricCodes.add(item.type)
    if (item.min === '' && item.max === '') return `第 ${index + 1} 行阈值至少填写最小值或最大值`
    if (item.min !== '' && item.max !== '' && Number(item.min) > Number(item.max)) {
      return `第 ${index + 1} 行阈值最小值不能大于最大值`
    }
    if (!item.type || item.min === '' || item.max === '') return `请完整填写第 ${index + 1} 行预警阈值`
    if (Number(item.min) >= Number(item.max)) return `第 ${index + 1} 行阈值最小值必须小于最大值`
  }
  return ''
}
function buildPayload(): TestConfigurationPayload {
  return {
    testName: form.name.trim(),
    principalId: form.principalId as number,
    testTypeId: form.testTypeId as number,
    testPurpose: form.purpose.trim(),
    monitorTimeoutMinutes: form.monitorTimeoutMinutes,
    remark: form.remark.trim() || null,
    batteries: batteries.value.map(item => ({
      groupId: item.groupId as number,
      batteryNo: item.batteryNo.trim(),
      collectorId: item.collector,
      binding: { channelId: item.cabinetChannel as number, expectedBarcode: item.expectedBarcode.trim() || null },
      fields: [],
    })),
    planSteps: planSteps.value.map((item, index) => ({
      cycleNo: item.cycleNo, stepNo: item.stepNo, stepType: item.stepType.trim(), stepName: item.stepName.trim(),
      parameters: parseParameters(item.parametersJson) as Record<string, unknown>, sortOrder: index + 1,
    })),
    thresholds: thresholds.value.map(item => ({
      metricCode: item.type,
      minValue: item.min === '' ? null : Number(item.min),
      maxValue: item.max === '' ? null : Number(item.max),
      unit: item.unit,
      severity: 'WARNING',
    })),
  }
function buildBatteryFields(item: BatteryRow) {
  const fixedFields = fixedSensorFields
    .filter(field => item[field])
    .map(field => ({ fieldName: fixedFieldNames[field], fieldValue: item[field] }))
  const customFields = customChannelColumns.value
    .filter(column => item.customChannels[column.key])
    .map(column => ({ fieldName: column.fieldName, fieldValue: item.customChannels[column.key] }))
  return [...fixedFields, ...customFields]
}
async function submit() {
  const validationError = validate()
  if (validationError) return void ElMessage.warning(validationError)
  const error = validate()
  if (error) {
    ElMessage.warning(error)
    return
  }
  submitting.value = true
  try {
    const payload = buildPayload()
    if (testId.value) {
      await updateTest({ ...payload, testId: testId.value })
      ElMessage.success('测试配置已更新')
    } else {
      await createTest(payload)
      ElMessage.success('测试已创建并加入测试列表')
    const payload = {
      testName: form.name.trim(),
      principalId: form.principalId!,
      testTypeId: form.testTypeId!,
      testPurpose: form.purpose.trim(),
      monitorTimeoutMinutes: monitorTimeoutMinutes.value,
      remark: form.remark.trim() || null,
      batteries: batteries.value.map(item => ({
        groupId: item.groupId!,
        batteryNo: item.batteryNo.trim(),
        collectorId: item.collector,
        binding: {
          channelId: item.cabinetChannel!,
          expectedBarcode: item.expectedBarcode,
        },
        fields: buildBatteryFields(item),
      })),
      planSteps: preservedPlanSteps.value,
      thresholds: thresholds.value.map(item => ({
        metricCode: item.type,
        minValue: item.min === '' ? null : Number(item.min),
        maxValue: item.max === '' ? null : Number(item.max),
        unit: item.unit,
        severity: 'WARNING' as const,
      })),
    }
    if (testId.value) await updateTest({ ...payload, testId: testId.value })
    else await createTest(payload)
    ElMessage.success(isEdit.value ? '测试配置已保存' : '测试已创建并加入测试列表')
    await router.push('/test-list')
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '测试配置保存失败')
  } catch (submitError) {
    ElMessage.error(submitError instanceof Error ? submitError.message : '测试保存失败')
  } finally {
    submitting.value = false
  }
}
onMounted(loadConfiguration)
onMounted(() => {
  loadPage()
})
</script>
<template>
  <div v-loading="loading" class="create-page">
    <div class="page-tools">
      <button class="back-button" type="button" @click="router.push('/test-list')"><el-icon><ArrowLeft /></el-icon>返回测试列表</button>
      <span>{{ isEdit ? 'EDIT TEST CONFIGURATION' : 'NEW TEST CONFIGURATION' }}</span>
      <span>NEW TEST CONFIGURATION</span>
    </div>
    <section class="form-panel">
      <header class="panel-heading">
        <div><i></i><strong>{{ isEdit ? '编辑测试参数' : '测试参数' }}</strong><span>TEST PARAMETERS</span></div>
        <div><i></i><strong>测试参数</strong><span>TEST PARAMETERS</span></div>
        <small><b>*</b> 为必填项</small>
      </header>
      <div class="parameter-grid">
        <label><span><b>*</b>测试名称</span><el-input v-model="form.name" maxlength="100" show-word-limit placeholder="输入测试名称" /></label>
        <label><span><b>*</b>测试名称</span><el-input v-model="form.name" maxlength="40" show-word-limit placeholder="输入测试名称" /></label>
        <label><span><b>*</b>测试负责人</span><el-select v-model="form.principalId" :loading="ownersLoading" filterable clearable placeholder="请选择负责人"><el-option v-for="item in owners" :key="item.id" :label="item.name" :value="item.id"><span>{{ item.name }}</span><small v-if="item.userName && item.userName !== item.name">({{ item.userName }})</small></el-option></el-select></label>
        <label><span><b>*</b>测试类型</span><el-select v-model="form.testTypeId" :loading="testTypesLoading" filterable clearable placeholder="请选择测试类型"><el-option v-for="item in testTypes" :key="item.id" :label="item.name" :value="item.id" /></el-select></label>
        <label><span><b>*</b>监测超时(分钟)</span><el-input-number v-model="form.monitorTimeoutMinutes" :min="1" :max="1440" :step="1" controls-position="right" /></label>
        <label class="textarea-field"><span><b>*</b>测试目的</span><el-input v-model="form.purpose" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="输入测试目的" /></label>
        <label class="textarea-field"><span>备注</span><el-input v-model="form.remark" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="输入备注(选填)" /></label>
        <label class="textarea-field"><span><b>*</b>测试目的</span><el-input v-model="form.purpose" type="textarea" :rows="4" maxlength="200" show-word-limit placeholder="输入测试目的" /></label>
        <label class="textarea-field"><span>备注</span><el-input v-model="form.remark" type="textarea" :rows="4" maxlength="200" show-word-limit placeholder="输入备注(选填)" /></label>
      </div>
    </section>
    <section class="form-panel">
      <header class="panel-heading channel-heading"><div><i></i><strong>电池组和通道设置</strong><span>BATTERY & CHANNELS</span></div></header>
      <header class="panel-heading channel-heading">
        <div><i></i><strong>电池组和通道设置</strong><span>BATTERY & CHANNELS</span></div>
        <label class="sync-setting"><span>同步方式</span><el-select v-model="form.syncMode"><el-option label="默认同步检测柜" value="默认同步检测柜" /></el-select></label>
      </header>
      <div class="section-actions">
        <div><button class="primary-button" type="button" @click="openBatteryDialog"><el-icon><Plus /></el-icon>添加电池</button><button class="secondary-button danger" type="button" @click="removeBatteries"><el-icon><Delete /></el-icon>删除所选</button></div>
        <span>同一测试使用一个电池组;主要检测柜通道不可重复</span>
        <div><button class="primary-button" type="button" @click="openBatteryDialog"><el-icon><Plus /></el-icon>添加电池</button><button class="secondary-button" type="button" @click="addChannelField"><el-icon><Plus /></el-icon>添加项</button><button class="secondary-button danger" type="button" @click="removeBatteries"><el-icon><Delete /></el-icon>删除所选</button></div>
        <span>至少配置一块电池 · 同一采集器通道不可重复</span>
      </div>
      <div class="config-table-wrap">
        <table class="config-table battery-table">
        <table class="config-table battery-table" :style="{ minWidth: `${1700 + customChannelColumns.length * 150}px` }">
          <thead><tr>
            <th><el-checkbox v-model="allBatteriesSelected" aria-label="选择全部电池" /></th><th>电池号 *</th><th>检测柜 *</th><th>主要检测柜通道 *</th><th>预期条码</th><th>采集器</th>
            <th><el-checkbox v-model="allBatteriesSelected" aria-label="选择全部电池" /></th><th>电池号 *</th><th>检测柜 *</th><th>检测柜通道 *</th><th>采集器</th>
            <th>主温度通道</th><th>温度通道 2</th><th>温度通道 3</th><th>正电位主通道</th><th>正电位通道 2</th><th>负电位主通道</th><th>负电位通道 2</th>
            <th v-for="column in customChannelColumns" :key="column.key" class="custom-channel-header">{{ column.label }}</th>
          </tr></thead>
          <tbody>
            <tr v-for="item in batteries" :key="item.key">
              <td><el-checkbox v-model="item.selected" :aria-label="`选择电池 ${item.batteryNo || item.key}`" /></td>
              <td><el-input v-model="item.batteryNo" disabled /></td>
              <td><el-input v-model="item.batteryNo" disabled placeholder="电池号" /></td>
              <td><el-select v-model="item.cabinet" placeholder="选择" @change="handleCabinetChange(item)"><el-option v-for="option in cabinetOptions" :key="option.id" :label="option.label" :value="option.id" /></el-select></td>
              <td><el-select v-model="item.cabinetChannel" clearable :disabled="!item.cabinet" :placeholder="item.cabinet ? '选择' : '请先选择检测柜'"><el-option v-for="option in channelsForCabinet(item.cabinet)" :key="option.channelId" :label="isCabinetChannelUsed(item, option.channelId) ? `${option.displayNo || `CH-${option.chlid}`}(已占用)` : option.displayNo || `CH-${option.chlid}`" :value="option.channelId" :disabled="isCabinetChannelUsed(item, option.channelId)" /></el-select></td>
              <td><el-input v-model="item.expectedBarcode" maxlength="100" placeholder="选填" /></td>
              <td><el-select v-model="item.collector" clearable placeholder="选填"><el-option v-for="option in collectorOptions" :key="option.id" :label="option.label" :value="option.id" /></el-select></td>
              <td><el-select v-model="item.collector" clearable placeholder="选择(选填)" @change="handleCollectorChange(item)"><el-option v-for="option in collectorOptions" :key="option.id" :label="option.label" :value="option.id" /></el-select></td>
              <td v-for="field in fixedSensorFields" :key="field">
                <el-select v-model="item[field]" clearable :disabled="!item.collector" :placeholder="item.collector ? '选择' : '请先选择采集器'"><el-option v-for="option in sensorChannels" :key="option" :label="isSensorChannelUsed(item, field, option) ? `${option}(已占用)` : option" :value="option" :disabled="isSensorChannelUsed(item, field, option)" /></el-select>
              </td>
              <td v-for="column in customChannelColumns" :key="column.key">
                <el-select v-model="item.customChannels[column.key]" clearable :disabled="!item.collector" :placeholder="item.collector ? '选择' : '请先选择采集器'"><el-option v-for="option in sensorChannels" :key="option" :label="isSensorChannelUsed(item, `custom:${column.key}`, option) ? `${option}(已占用)` : option" :value="option" :disabled="isSensorChannelUsed(item, `custom:${column.key}`, option)" /></el-select>
              </td>
            </tr>
            <tr v-if="!batteries.length"><td colspan="6" class="empty-row">暂未配置电池,请点击“添加电池”。</td></tr>
          </tbody>
        </table>
      </div>
    </section>
    <section class="form-panel plan-panel">
      <header class="panel-heading"><div><i></i><strong>计划工步</strong><span>TEST PLAN STEPS</span></div></header>
      <div class="section-actions"><div><button class="primary-button" type="button" @click="addPlanStep"><el-icon><Plus /></el-icon>添加工步</button><button class="secondary-button danger" type="button" @click="removePlanSteps"><el-icon><Delete /></el-icon>删除所选</button></div></div>
      <div class="config-table-wrap compact-table-wrap">
        <table class="config-table plan-table">
          <thead><tr><th><el-checkbox v-model="allPlanStepsSelected" aria-label="选择全部计划工步" /></th><th>循环号 *</th><th>工步号 *</th><th>工步类型 *</th><th>工步名称 *</th><th>参数(JSON 对象)*</th></tr></thead>
          <tbody>
            <tr v-for="item in planSteps" :key="item.key">
              <td><el-checkbox v-model="item.selected" :aria-label="`选择计划工步 ${item.key}`" /></td>
              <td><el-input-number v-model="item.cycleNo" :min="1" :step="1" controls-position="right" /></td>
              <td><el-input-number v-model="item.stepNo" :min="1" :step="1" controls-position="right" /></td>
              <td><el-input v-model="item.stepType" maxlength="50" placeholder="例如 CHARGE" /></td>
              <td><el-input v-model="item.stepName" maxlength="100" placeholder="输入工步名称" /></td>
              <td><el-input v-model="item.parametersJson" placeholder="{}" /></td>
            </tr>
            <tr v-if="!planSteps.length"><td colspan="6" class="empty-row">暂未设置计划工步</td></tr>
            <tr v-if="!batteries.length"><td :colspan="12 + customChannelColumns.length" class="empty-row">尚未配置电池,请点击“添加电池”</td></tr>
          </tbody>
        </table>
      </div>
    </section>
    <section class="form-panel threshold-panel">
      <header class="panel-heading"><div><i></i><strong>预警阈值</strong><span>ALERT THRESHOLDS</span></div><small>单位按 SI 业务单位保存</small></header>
      <div class="section-actions"><div><button class="primary-button" type="button" @click="addThreshold"><el-icon><Plus /></el-icon>添加阈值</button><button class="secondary-button danger" type="button" @click="removeThresholds"><el-icon><Delete /></el-icon>删除所选</button></div></div>
      <header class="panel-heading"><div><i></i><strong>预警阈值</strong><span>ALERT THRESHOLDS</span></div><small>可配置温度、电压、电流、容量;检测柜返回项待现场验证</small></header>
      <div class="section-actions">
        <div><button class="primary-button" type="button" @click="addThreshold"><el-icon><Plus /></el-icon>添加阈值</button><button class="secondary-button danger" type="button" @click="removeThresholds"><el-icon><Delete /></el-icon>删除所选</button></div>
      </div>
      <div class="config-table-wrap threshold-wrap">
        <table class="config-table threshold-table">
          <thead><tr><th><el-checkbox v-model="allThresholdsSelected" aria-label="选择全部阈值" /></th><th>阈值类型 *</th><th>最小值</th><th>最大值</th><th>单位</th></tr></thead>
          <thead><tr><th><el-checkbox v-model="allThresholdsSelected" aria-label="选择全部阈值" /></th><th>阈值类型</th><th>最小值</th><th>最大值</th></tr></thead>
          <tbody>
            <tr v-for="item in thresholds" :key="item.key">
              <td><el-checkbox v-model="item.selected" :aria-label="`选择阈值 ${item.key}`" /></td>
              <td><el-select v-model="item.type" placeholder="选择类型" @change="handleThresholdTypeChange(item)"><el-option v-for="option in thresholdTypes" :key="option.value" :label="option.label" :value="option.value" /></el-select></td>
              <td><el-input v-model="item.min" type="number" placeholder="选填" /></td>
              <td><el-input v-model="item.max" type="number" placeholder="选填" /></td>
              <td><span class="unit-value">{{ item.unit }}</span></td>
              <td><el-select v-model="item.type" placeholder="选择类型" @change="handleThresholdTypeChange(item)"><el-option v-for="option in thresholdTypes" :key="option.value" :label="option.label" :value="option.value" :disabled="option.disabled" /></el-select></td>
              <td><el-input v-model="item.min" type="number" placeholder="输入最小值" /></td>
              <td><el-input v-model="item.max" type="number" placeholder="输入最大值" /></td>
            </tr>
            <tr v-if="!thresholds.length"><td colspan="5" class="empty-row">暂未设置预警阈值</td></tr>
            <tr v-if="!thresholds.length"><td colspan="4" class="empty-row">暂未设置预警阈值</td></tr>
          </tbody>
        </table>
      </div>
@@ -476,20 +562,35 @@
    </footer>
    <el-dialog v-model="batteryDialogVisible" class="battery-dialog" width="560px" align-center destroy-on-close>
      <template #header><div class="dialog-title"><strong>添加电池组</strong><span>ADD BATTERY GROUP</span></div></template>
      <template #header>
        <div class="dialog-title"><strong>添加电池组</strong><span>ADD BATTERY GROUP</span></div>
      </template>
      <div class="battery-dialog-body">
        <label class="battery-group-search"><span>电池组</span><el-select v-model="selectedBatteryGroup" filterable clearable placeholder="输入或选择电池组" @change="handleBatteryGroupChange"><el-option v-for="group in batteryGroupOptions" :key="group.id" :label="group.name" :value="group.id" :disabled="batteries.length > 0 && batteries[0].groupId !== group.id" /></el-select></label>
        <label class="battery-group-search">
          <span>电池组</span>
          <el-select v-model="selectedBatteryGroup" filterable clearable placeholder="输入电池组名称搜索" @change="handleBatteryGroupChange">
            <el-option v-for="group in batteryGroupOptions" :key="group.id" :label="group.name" :value="group.id" :disabled="batteries.length > 0 && batteries[0].groupId !== group.id" />
          </el-select>
        </label>
        <div class="battery-picker-table">
          <table>
            <thead><tr><th><el-checkbox v-model="allGroupBatteriesSelected" :disabled="!availableGroupBatteryNos.length" aria-label="选择全部可用电池" /></th><th>电池组</th><th>电池号</th><th>状态</th></tr></thead>
            <thead><tr><th><el-checkbox v-model="allGroupBatteriesSelected" :disabled="!availableGroupBatteryNos.length" aria-label="选择全部可用电池" /></th><th>电池组</th><th>电池名</th><th>状态</th></tr></thead>
            <tbody>
              <tr v-for="batteryNo in groupBatteryNos" :key="batteryNo" :class="{ added: isBatteryAdded(batteryNo) }"><td><el-checkbox v-model="selectedBatteryNos" :value="batteryNo" :disabled="isBatteryAdded(batteryNo)" :aria-label="`选择电池 ${batteryNo}`" /></td><td>{{ selectedBatteryGroupName }}</td><td>{{ batteryNo }}</td><td><span :class="['battery-state', { used: isBatteryAdded(batteryNo) }]">{{ isBatteryAdded(batteryNo) ? '已添加' : '可添加' }}</span></td></tr>
              <tr v-if="!selectedBatteryGroup"><td colspan="4" class="picker-empty">请先选择一个电池组</td></tr>
              <tr v-for="batteryNo in groupBatteryNos" :key="batteryNo" :class="{ added: isBatteryAdded(batteryNo) }">
                <td><el-checkbox v-model="selectedBatteryNos" :value="batteryNo" :disabled="isBatteryAdded(batteryNo)" :aria-label="`选择电池 ${batteryNo}`" /></td>
                <td>{{ selectedBatteryGroupName }}</td>
                <td>{{ batteryNo }}</td>
                <td><span :class="['battery-state', { used: isBatteryAdded(batteryNo) }]">{{ isBatteryAdded(batteryNo) ? '已添加' : '可添加' }}</span></td>
              </tr>
              <tr v-if="!selectedBatteryGroup"><td colspan="4" class="picker-empty">请选择或搜索电池组</td></tr>
            </tbody>
          </table>
        </div>
      </div>
      <template #footer><div class="dialog-footer"><button class="secondary-button" type="button" @click="batteryDialogVisible = false">取消</button><button class="primary-button" type="button" @click="confirmAddBatteries"><el-icon><Check /></el-icon>提交</button></div></template>
      <template #footer>
        <div class="dialog-footer"><button class="secondary-button" type="button" @click="batteryDialogVisible = false">取消</button><button class="primary-button" type="button" @click="confirmAddBatteries"><el-icon><Check /></el-icon>提交</button></div>
      </template>
    </el-dialog>
  </div>
</template>
@@ -511,11 +612,14 @@
.parameter-grid label { min-width: 0; display: flex; flex-direction: column; gap: 7px; }
.parameter-grid label > span { color: #52606d; font-size: 12px; font-weight: 500; }
.textarea-field { grid-column: span 1; }
.parameter-grid :deep(.el-select), .parameter-grid :deep(.el-input-number) { width: 100%; }
.parameter-grid :deep(.el-select), .sync-setting :deep(.el-select) { width: 100%; }
.create-page :deep(.el-input__wrapper), .create-page :deep(.el-select__wrapper), .create-page :deep(.el-textarea__inner) { border-radius: 3px; box-shadow: 0 0 0 1px #d8e1e9 inset; background: #f8fafc; }
.create-page :deep(.el-input__wrapper), .create-page :deep(.el-select__wrapper) { min-height: 34px; }
.create-page :deep(.el-textarea__inner) { resize: vertical; }
.channel-heading { min-height: 54px; }
.sync-setting { display: flex; align-items: center; gap: 10px; }
.sync-setting > span { color: #52606d; font-size: 12px; white-space: nowrap; }
.sync-setting :deep(.el-select) { width: 190px; }
.section-actions { min-height: 50px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 14px; }
.section-actions > div { display: flex; gap: 8px; }
.section-actions > span { color: #7b8794; font-size: 11px; }
@@ -527,27 +631,20 @@
.secondary-button:hover { color: var(--blue); border-color: #9dc8f5; }
.secondary-button.danger { color: #a84848; }
.config-table-wrap { overflow-x: auto; border-top: 1px solid var(--line); }
.config-table { width: 100%; table-layout: fixed; border-collapse: collapse; color: #52606d; font-size: 12px; }
.config-table { width: 100%; min-width: 1700px; table-layout: fixed; border-collapse: collapse; color: #52606d; font-size: 12px; }
.config-table th, .config-table td { height: 48px; padding: 6px; border-right: 1px solid #cbd9e5; border-bottom: 1px solid #d8e1e9; text-align: center; }
.config-table th { height: 40px; color: #fff; background: #255884; font-size: 11px; font-weight: 600; }
.config-table th:first-child { width: 46px; }
.config-table th:nth-child(2) { width: 130px; }
.config-table th.custom-channel-header { width: 150px; }
.config-table th:last-child, .config-table td:last-child { border-right: 0; }
.config-table tbody tr:last-child td { border-bottom: 0; }
.config-table :deep(.el-select), .config-table :deep(.el-input), .config-table :deep(.el-input-number) { width: 100%; }
.config-table :deep(.el-select), .config-table :deep(.el-input) { width: 100%; }
.config-table :deep(.el-input__wrapper), .config-table :deep(.el-select__wrapper) { min-height: 32px; padding: 0 8px; background: #fff; }
.battery-table { min-width: 980px; }
.compact-table-wrap { width: calc(100% - 28px); margin: 0 14px 14px; border: 1px solid var(--line); }
.plan-table { min-width: 980px; }
.plan-table th:nth-child(2), .plan-table th:nth-child(3) { width: 110px; }
.plan-table th:nth-child(4) { width: 170px; }
.plan-table th:nth-child(5) { width: 220px; }
.threshold-panel { padding-bottom: 14px; }
.threshold-wrap { width: min(760px, calc(100% - 28px)); margin: 0 14px; border: 1px solid var(--line); }
.threshold-table { min-width: 720px; }
.threshold-wrap { width: min(620px, 100%); margin: 0 14px; border: 1px solid var(--line); }
.threshold-table { min-width: 600px; }
.threshold-table th:nth-child(2) { width: 210px; }
.threshold-table th:last-child { width: 90px; }
.unit-value { display: inline-flex; min-width: 40px; height: 26px; align-items: center; justify-content: center; color: #255884; background: #eaf2f8; border: 1px solid #c7dce9; }
.empty-row { height: 88px !important; color: #8795a3; }
.submit-bar { position: sticky; bottom: 0; z-index: 20; min-height: 62px; display: flex; align-items: center; justify-content: flex-end; gap: 10px; padding: 10px 16px; background: rgba(234, 240, 246, .96); border-top: 1px solid #cbd9e5; }
.submit-button { min-width: 110px; }
@@ -583,6 +680,7 @@
  .parameter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .channel-heading { align-items: flex-start; flex-direction: column; padding-top: 12px; padding-bottom: 12px; }
}
@media (max-width: 680px) {
  .create-page { gap: 12px; }
  .page-tools > span { display: none; }
@@ -591,7 +689,9 @@
  .parameter-grid { grid-template-columns: 1fr; gap: 14px; padding: 14px 12px; }
  .section-actions { align-items: flex-start; flex-direction: column; padding: 10px 12px; }
  .section-actions > span { line-height: 1.5; }
  .compact-table-wrap, .threshold-wrap { width: calc(100% - 24px); margin-right: 12px; margin-left: 12px; }
  .sync-setting { width: 100%; }
  .sync-setting :deep(.el-select) { flex: 1; width: auto; }
  .threshold-wrap { width: calc(100% - 24px); margin: 0 12px; }
  .submit-bar { margin: 0 -14px -14px; padding-right: 14px; }
  .create-page :deep(.battery-dialog .el-dialog__body) { padding: 16px 12px; }
  .battery-group-search { grid-template-columns: 1fr; gap: 7px; }
src/views/TestList.vue
@@ -2,14 +2,14 @@
import { onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { getTestList, type TestStatusCode } from '@/api/tests'
import { getTestList } from '@/api/tests'
import { getTestTypeList } from '@/api/testTypes'
import type { TestRecord, TestStatus } from '@/stores/tests'
interface FilterState {
  name: string
  typeId: number | null
  status: TestStatusCode | ''
  status: string
  batteryNo: string
  dateRange: [string, string] | null
}
@@ -19,14 +19,11 @@
  name: string
}
const statuses: Array<{ label: TestStatus; value: TestStatusCode }> = [
  { label: '待测试', value: 'WAITING' },
  { label: '检测中', value: 'RUNNING' },
  { label: '暂停', value: 'PAUSED' },
  { label: '检测中(异常)', value: 'RUNNING_ABNORMAL' },
  { label: '完成确认中', value: 'COMPLETION_PENDING' },
  { label: '检测完成', value: 'COMPLETED' },
  { label: '已终止', value: 'TERMINATED' },
const statuses: Array<{ label: TestStatus; value: string }> = [
  { label: '待测试', value: '0' },
  { label: '检测中', value: '1' },
  { label: '检测中(异常)', value: '2' },
  { label: '检测完成', value: '3' },
]
const router = useRouter()
const testTypes = ref<TestTypeOption[]>([])
@@ -57,7 +54,7 @@
      testName: query.name.trim() || undefined,
      testTypeId: query.typeId || undefined,
      batteryNo: query.batteryNo.trim() || undefined,
      testStatus: query.status || undefined,
      status: query.status || undefined,
      beginCreateTime: query.dateRange?.[0] ? `${query.dateRange[0]} 00:00:00` : undefined,
      endCreateTime: query.dateRange?.[1] ? `${query.dateRange[1]} 23:59:59` : undefined,
    })
@@ -104,7 +101,7 @@
    '检测中(异常)': 'abnormal',
    '完成确认中': 'pending',
    '已终止': 'abnormal',
    '未知': 'unknown',
    '未知': 'paused',
  }[status]
}
@@ -224,10 +221,7 @@
              <td :title="item.purpose">{{ item.purpose }}</td>
              <td>{{ item.owner }}</td>
              <td :title="item.remark">{{ item.remark }}</td>
              <td>
                <span class="status-tag" :class="statusClass(item.status)"><i></i>{{ item.status }}</span>
                <small v-if="item.monitoringStatus" class="monitoring-status">{{ item.monitoringStatus }}</small>
              </td>
              <td><span class="status-tag" :class="statusClass(item.status)"><i></i>{{ item.status }}</span></td>
              <td class="action-column">
                <div class="row-actions">
                  <button type="button" @click="openConsole(item)">控制台</button>
@@ -307,7 +301,6 @@
.status-tag.completed { color: #23865e; background: #edf9f3; border-color: #b8dfcd; }
.status-tag.paused { color: #697682; background: #f2f4f6; border-color: #ccd4da; }
.status-tag.abnormal { color: #b34f4f; background: #fff1f1; border-color: #e9bcbc; }
.status-tag.unknown { color: #697682; background: #f2f4f6; border-color: #ccd4da; }
.row-actions { display: flex; align-items: center; justify-content: center; gap: 12px; }
.row-actions button { border: 0; color: var(--blue); background: transparent; font-size: 12px; font-weight: 500; cursor: pointer; }
.row-actions .delete-action { color: #ad4a4a; }