| | |
| | | <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, |
| | |
| | | updateTest, |
| | | type TestConfigurationDetail, |
| | | type TestConfigurationOptions, |
| | | type TestBatteryPayload, |
| | | type TestPlanStepPayload, |
| | | } from '@/api/tests' |
| | | |
| | |
| | | cabinetChannel: number | null |
| | | collector: number | null |
| | | expectedBarcode: string | null |
| | | preservedFields: TestBatteryPayload['fields'] |
| | | temperature1: string |
| | | temperature2: string |
| | | temperature3: string |
| | |
| | | interface CustomChannelColumn { |
| | | key: string |
| | | label: string |
| | | fieldName: string |
| | | } |
| | | |
| | | interface BatteryGroupOption { |
| | |
| | | min: string |
| | | max: string |
| | | unit: 'A' | 'V' | 'Ah' | 'Wh' | '℃' |
| | | severity: string |
| | | } |
| | | |
| | | const router = useRouter() |
| | |
| | | const monitorTimeoutMinutes = ref(30) |
| | | let batteryKey = 1 |
| | | let thresholdKey = 1 |
| | | let customChannelKey = 1 |
| | | |
| | | const form = reactive({ |
| | | name: '', |
| | |
| | | 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 |
| | |
| | | { 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>() |
| | |
| | | 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, ''])), |
| | | } |
| | |
| | | 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', |
| | | } |
| | | } |
| | | |
| | |
| | | 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) |
| | |
| | | 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) { |
| | |
| | | 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.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', |
| | | })) |
| | | } |
| | | |
| | |
| | | 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) { |
| | |
| | | } |
| | | |
| | | 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() { |
| | |
| | | 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 }) |
| | |
| | | </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` }"> |
| | |
| | | <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> |