yeg
2026-08-27 bcb1b2954bd942118ec284632c190c304ec93a12
fix(test): keep configuration facts authoritative
4个文件已修改
160 ■■■■■ 已修改文件
src/api/tests.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/stores/tests.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/TestCreate.vue 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/TestList.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/tests.ts
@@ -93,7 +93,6 @@
interface TestListResponse extends ApiResponse { total: number; rows: TestDto[] }
interface TestDetailResponse extends ApiResponse { data: TestConfigurationDetail }
interface TestOptionsResponse extends ApiResponse { data: TestConfigurationOptions }
interface TestIdentifierResponse extends ApiResponse { data: number }
export interface TestListResult { total: number; rows: TestRecord[] }
const statusMap: Record<TestStatusCode, TestStatus> = {
@@ -106,14 +105,14 @@
}
function mapTest(item: TestDto): TestRecord {
  const testStatus = item.testStatus || 'WAITING'
  const monitoringStatus = item.monitoringStatus || 'NOT_MONITORED'
  const testStatus = 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: statusMap[testStatus], monitoringStatus: monitoringStatusMap[monitoringStatus],
    editable: monitoringStatus === 'NOT_MONITORED',
    remark: item.remark || '-', status: testStatus, monitoringStatus,
    editable: item.monitoringStatus === 'NOT_MONITORED',
  }
}
@@ -133,9 +132,9 @@
export async function getTestDetail(testId: number): Promise<TestConfigurationDetail> {
  return (await request<TestDetailResponse>(`/system/test/${testId}`)).data
}
export async function createTest(payload: TestConfigurationPayload): Promise<number> {
  return (await request<TestIdentifierResponse>('/system/test', { method: 'POST', body: JSON.stringify(payload) })).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<void> {
  await request<ApiResponse>('/system/test', { method: 'PUT', body: JSON.stringify(payload) })
export async function updateTest(payload: TestConfigurationPayload & { testId: number }): Promise<TestConfigurationDetail> {
  return (await request<TestDetailResponse>('/system/test', { method: 'PUT', body: JSON.stringify(payload) })).data
}
src/stores/tests.ts
@@ -1,7 +1,7 @@
import { ref } from 'vue'
export type TestStatus = '待测试' | '检测中' | '暂停' | '检测中(异常)' | '完成确认中' | '检测完成' | '已终止'
export type MonitoringStatus = '未监测' | '等待设备启动' | '监测中' | '补采中' | '监测结束' | '已取消'
export type TestStatus = '待测试' | '检测中' | '暂停' | '检测中(异常)' | '完成确认中' | '检测完成' | '已终止' | '未知'
export type MonitoringStatus = '未监测' | '等待设备启动' | '监测中' | '补采中' | '监测结束' | '已取消' | '未知'
export interface TestRecord {
  id: number
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,
@@ -22,19 +22,6 @@
  cabinetChannel: number | null
  collector: number | null
  expectedBarcode: string
  temperature1: string
  temperature2: string
  temperature3: string
  positive1: string
  positive2: string
  negative1: string
  negative2: string
  customChannels: Record<string, string>
}
interface CustomChannelColumn {
  key: string
  label: string
}
interface BatteryGroupOption {
@@ -73,7 +60,6 @@
const selectedBatteryNos = ref<string[]>([])
let batteryKey = 1
let thresholdKey = 1
let customChannelKey = 1
let planStepKey = 1
const form = reactive({
@@ -92,7 +78,6 @@
const configurationOptions = ref<TestConfigurationOptions | null>(null)
const cabinetOptions = ref<Array<{ id: number; label: string }>>([])
const collectorOptions = ref<Array<{ id: number; label: string }>>([])
const sensorChannels = Array.from({ length: 12 }, (_, index) => `S-${String(index + 1).padStart(2, '0')}`)
const thresholdTypes = [
  { label: '温度', value: 'TEMPERATURE', unit: '℃' as const },
  { label: '电压', value: 'VOLTAGE', unit: 'V' as const },
@@ -100,29 +85,12 @@
  { label: '容量', value: 'CAPACITY', unit: 'Ah' as const },
  { label: '能量', value: 'ENERGY', unit: 'Wh' as const },
]
const fixedSensorFields = ['temperature1', 'temperature2', 'temperature3', 'positive1', 'positive2', 'negative1', 'negative2'] as const
type FixedSensorField = typeof fixedSensorFields[number]
const fixedSensorLabels: Record<FixedSensorField, string> = {
  temperature1: '主温度通道',
  temperature2: '温度通道 2',
  temperature3: '温度通道 3',
  positive1: '正电位主通道',
  positive2: '正电位通道 2',
  negative1: '负电位主通道',
  negative2: '负电位通道 2',
}
const fixedFieldByLabel = Object.fromEntries(
  Object.entries(fixedSensorLabels).map(([field, label]) => [label, field]),
) as Record<string, FixedSensorField>
const customChannelColumns = ref<CustomChannelColumn[]>([])
const batteryGroupOptions = ref<BatteryGroupOption[]>([])
function createBatteryRow(batteryNo = '', groupId: number | null = null): BatteryRow {
  return {
    key: batteryKey++, selected: false, groupId, batteryNo, cabinet: null, cabinetChannel: null, collector: null,
    expectedBarcode: '',
    temperature1: '', temperature2: '', temperature3: '', positive1: '', positive2: '', negative1: '', negative2: '',
    customChannels: Object.fromEntries(customChannelColumns.value.map(column => [column.key, ''])),
  }
}
@@ -193,81 +161,13 @@
  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)] = ''
  else item[fieldId as FixedSensorField] = ''
}
function isCabinetChannelUsed(item: BatteryRow, channel: number) {
  return batteries.value.some(other => other.key !== item.key && other.cabinetChannel === channel)
}
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) {
  if (item.cabinetChannel && !channelsForCabinet(item.cabinet).some(channel => channel.channelId === item.cabinetChannel)) {
    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
    } else 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 exists = [...Object.values(fixedSensorLabels), ...customChannelColumns.value.map(column => column.label)]
          .some(item => item.toLocaleLowerCase() === normalizedLabel)
        return exists ? '字段名称已存在' : true
      },
    })
    const label = result.value.trim()
    const key = `custom-channel-${customChannelKey++}`
    customChannelColumns.value.push({ key, label })
    batteries.value.forEach(item => { item.customChannels[key] = '' })
    ElMessage.success(`已添加“${label}”通道列`)
  } catch {
    // 用户取消输入时保留当前配置。
  }
}
@@ -322,11 +222,6 @@
  form.remark = detail.remark || ''
  form.monitorTimeoutMinutes = detail.monitorTimeoutMinutes
  const customFieldNames = new Set(
    detail.batteries.flatMap(item => item.fields.map(field => field.fieldName))
      .filter(fieldName => !fixedFieldByLabel[fieldName]),
  )
  customChannelColumns.value = [...customFieldNames].map(label => ({ key: `custom-channel-${customChannelKey++}`, label }))
  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)
@@ -334,12 +229,6 @@
    row.cabinetChannel = item.binding.channelId
    row.collector = item.collectorId
    row.expectedBarcode = item.binding.expectedBarcode || ''
    item.fields.forEach(field => {
      const fixedField = fixedFieldByLabel[field.fieldName]
      if (fixedField) row[fixedField] = field.fieldValue
      const customColumn = customChannelColumns.value.find(column => column.label === field.fieldName)
      if (customColumn) row.customChannels[customColumn.key] = field.fieldValue
    })
    return row
  })
  planSteps.value = detail.planSteps.map(item => ({
@@ -410,11 +299,6 @@
  const cabinetChannels = batteries.value.map(item => item.cabinetChannel)
  if (new Set(cabinetChannels).size !== cabinetChannels.length) return '同一检测柜通道不可重复绑定'
  const collectorChannels = batteries.value.flatMap(item => item.collector
    ? getSensorSelections(item).filter(selection => selection.value).map(selection => `${item.collector}/${selection.value}`)
    : [])
  if (new Set(collectorChannels).size !== collectorChannels.length) return '同一采集器的采集通道不可重复'
  if (!planSteps.value.length) return '至少需要配置一个计划工步'
  const stepIdentities = new Set<string>()
  for (let index = 0; index < planSteps.value.length; index += 1) {
@@ -457,10 +341,7 @@
      batteryNo: item.batteryNo.trim(),
      collectorId: item.collector,
      binding: { channelId: item.cabinetChannel as number, expectedBarcode: item.expectedBarcode.trim() || null },
      fields: [
        ...fixedSensorFields.map(field => ({ fieldName: fixedSensorLabels[field], fieldValue: item[field] })),
        ...customChannelColumns.value.map(column => ({ fieldName: column.label, fieldValue: item.customChannels[column.key] || '' })),
      ].filter(field => field.fieldValue),
      fields: [],
    })),
    planSteps: planSteps.value.map((item, index) => ({
      cycleNo: item.cycleNo, stepNo: item.stepNo, stepType: item.stepType.trim(), stepName: item.stepName.trim(),
@@ -525,15 +406,13 @@
    <section class="form-panel">
      <header class="panel-heading channel-heading"><div><i></i><strong>电池组和通道设置</strong><span>BATTERY & CHANNELS</span></div></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>
        <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>
      <div class="config-table-wrap">
        <table class="config-table battery-table" :style="{ minWidth: `${1880 + customChannelColumns.length * 150}px` }">
        <table class="config-table battery-table">
          <thead><tr>
            <th><el-checkbox v-model="allBatteriesSelected" aria-label="选择全部电池" /></th><th>电池号 *</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">
@@ -542,11 +421,9 @@
              <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="选填" @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>
              <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>
            </tr>
            <tr v-if="!batteries.length"><td :colspan="13 + customChannelColumns.length" class="empty-row">暂未配置电池,请点击“添加电池”。</td></tr>
            <tr v-if="!batteries.length"><td colspan="6" class="empty-row">暂未配置电池,请点击“添加电池”。</td></tr>
          </tbody>
        </table>
      </div>
@@ -655,11 +532,11 @@
.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-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; }
src/views/TestList.vue
@@ -104,6 +104,7 @@
    '检测中(异常)': 'abnormal',
    '完成确认中': 'pending',
    '已终止': 'abnormal',
    '未知': 'unknown',
  }[status]
}
@@ -306,6 +307,7 @@
.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; }