yeg
2026-08-27 339de35256661dcbe04cf3862cb5bab170f0cb10
fix(test): preserve hidden configuration facts
2个文件已修改
140 ■■■■■ 已修改文件
src/api/tests.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/TestCreate.vue 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/tests.ts
@@ -40,7 +40,7 @@
  minValue: number | null
  maxValue: number | null
  unit: 'A' | 'V' | 'Ah' | 'Wh' | '℃'
  severity: 'WARNING'
  severity: string
}
export interface TestConfigurationPayload {
src/views/TestCreate.vue
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ElMessage } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import {
  createTest,
@@ -9,6 +9,7 @@
  updateTest,
  type TestConfigurationDetail,
  type TestConfigurationOptions,
  type TestBatteryPayload,
  type TestPlanStepPayload,
} from '@/api/tests'
@@ -21,6 +22,7 @@
  cabinetChannel: number | null
  collector: number | null
  expectedBarcode: string | null
  preservedFields: TestBatteryPayload['fields']
  temperature1: string
  temperature2: string
  temperature3: string
@@ -34,7 +36,6 @@
interface CustomChannelColumn {
  key: string
  label: string
  fieldName: string
}
interface BatteryGroupOption {
@@ -50,6 +51,7 @@
  min: string
  max: string
  unit: 'A' | 'V' | 'Ah' | 'Wh' | '℃'
  severity: string
}
const router = useRouter()
@@ -66,7 +68,6 @@
const monitorTimeoutMinutes = ref(30)
let batteryKey = 1
let thresholdKey = 1
let customChannelKey = 1
const form = reactive({
  name: '',
@@ -81,7 +82,6 @@
const testTypes = ref<Array<{ id: number; name: string }>>([])
const ownersLoading = ref(false)
const testTypesLoading = ref(false)
const sensorChannels = Array.from({ length: 12 }, (_, index) => `S-${String(index + 1).padStart(2, '0')}`)
const thresholdTypes: Array<{
  label: string
  value: string
@@ -95,13 +95,7 @@
  { 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>()
@@ -118,7 +112,7 @@
function createBatteryRow(batteryNo = '', groupId: number | null = null): BatteryRow {
  return {
    key: batteryKey++, selected: false, groupId, batteryNo, cabinet: null, cabinetChannel: null, collector: null,
    expectedBarcode: null,
    expectedBarcode: null, preservedFields: [],
    temperature1: '', temperature2: '', temperature3: '', positive1: '', positive2: '', negative1: '', negative2: '',
    customChannels: Object.fromEntries(customChannelColumns.value.map(column => [column.key, ''])),
  }
@@ -127,7 +121,7 @@
function createThresholdRow(type = ''): ThresholdRow {
  return {
    key: thresholdKey++, selected: false, type, min: '', max: '',
    unit: thresholdTypes.find(item => item.value === type)?.unit || 'V',
    unit: thresholdTypes.find(item => item.value === type)?.unit || 'V', severity: 'WARNING',
  }
}
@@ -196,21 +190,6 @@
  batteries.value = batteries.value.filter(item => !item.selected)
}
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 clearSensorSelection(item: BatteryRow, fieldId: string) {
  if (fieldId.startsWith('custom:')) {
    item.customChannels[fieldId.slice(7)] = ''
    return
  }
  item[fieldId as FixedSensorField] = ''
}
function channelsForCabinet(cabinetId: number | null) {
  if (!cabinetId) return []
  return (configurationOptions.value?.channels || []).filter(channel => channel.cabinetId === cabinetId)
@@ -220,69 +199,12 @@
  return batteries.value.some(other => other.key !== item.key && other.cabinetChannel === channelId)
}
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 addChannelField() {
  ElMessage.info('采集器通道结构待现场验证')
}
function applyOptions(options: TestConfigurationOptions) {
@@ -314,17 +236,6 @@
  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)
@@ -332,16 +243,14 @@
    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) || '' })
    row.preservedFields = battery.fields
    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,
    unit: item.unit, severity: item.severity || 'WARNING',
  }))
}
@@ -393,21 +302,12 @@
      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 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 '同一检测柜通道不可重复绑定'
  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 '至少需要配置一项预警阈值'
  for (let index = 0; index < thresholds.value.length; index += 1) {
@@ -419,13 +319,7 @@
}
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]
  return item.preservedFields
}
async function submit() {
@@ -459,7 +353,7 @@
        minValue: item.min === '' ? null : Number(item.min),
        maxValue: item.max === '' ? null : Number(item.max),
        unit: item.unit,
        severity: 'WARNING' as const,
        severity: item.severity,
      })),
    }
    if (testId.value) await updateTest({ ...payload, testId: testId.value })
@@ -506,7 +400,7 @@
      </header>
      <div class="section-actions">
        <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>
        <span>至少配置一块电池 · 采集器通道结构待现场验证</span>
      </div>
      <div class="config-table-wrap">
        <table class="config-table battery-table" :style="{ minWidth: `${1700 + customChannelColumns.length * 150}px` }">
@@ -521,12 +415,12 @@
              <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-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><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 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>
                <el-select v-model="item[field]" disabled placeholder="待现场验证" />
              </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>
                <el-select v-model="item.customChannels[column.key]" disabled placeholder="待现场验证" />
              </td>
            </tr>
            <tr v-if="!batteries.length"><td :colspan="12 + customChannelColumns.length" class="empty-row">尚未配置电池,请点击“添加电池”</td></tr>