| | |
| | | <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 |
| | |
| | | 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 { |
| | |
| | | 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() |
| | |
| | | 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: '', |
| | |
| | | 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, ''])), |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | 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 || '-') |
| | |
| | | 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) { |
| | |
| | | } |
| | | |
| | | 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 |
| | |
| | | } |
| | | |
| | | 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 |
| | |
| | | } |
| | | } |
| | | |
| | | 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() { |
| | |
| | | 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> |
| | |
| | | </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> |
| | |
| | | .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; } |
| | |
| | | .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; } |
| | |
| | | .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; } |
| | |
| | | .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; } |