| | |
| | | <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, |
| | |
| | | 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 { |
| | |
| | | const selectedBatteryNos = ref<string[]>([]) |
| | | let batteryKey = 1 |
| | | let thresholdKey = 1 |
| | | let customChannelKey = 1 |
| | | let planStepKey = 1 |
| | | |
| | | const form = reactive({ |
| | |
| | | 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 }, |
| | |
| | | { 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, ''])), |
| | | } |
| | | } |
| | | |
| | |
| | | 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 { |
| | | // 用户取消输入时保留当前配置。 |
| | | } |
| | | } |
| | | |
| | |
| | | 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) |
| | |
| | | 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 => ({ |
| | |
| | | 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) { |
| | |
| | | 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(), |
| | |
| | | <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"> |
| | |
| | | <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> |
| | |
| | | .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; } |