| | |
| | | } from '@/utils/compareReportPrint' |
| | | import type { CompareMetricKey } from '@/stores/compareReports' |
| | | import { useBatteryGroupFields } from '@/stores/batteryGroupFields' |
| | | import type { PersistedTestRecord, TestLogicStepMapping } from '@/api/tests' |
| | | import { saveTestConsoleDetail, type PersistedTestRecord, type TestConsoleDetail, type TestLogicStepMapping } from '@/api/tests' |
| | | |
| | | interface ConsoleCell { |
| | | no: string |
| | |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<{ |
| | | testId: number |
| | | cells: ConsoleCell[] |
| | | durationMinutes?: number |
| | | testName?: string |
| | | batteryGroup?: string |
| | | records: PersistedTestRecord[] |
| | | logicStepMappings?: TestLogicStepMapping[] |
| | | consoleDetail?: TestConsoleDetail | null |
| | | }>(), { |
| | | durationMinutes: 5, |
| | | testName: '', |
| | | batteryGroup: '', |
| | | records: () => [], |
| | | logicStepMappings: () => [], |
| | | consoleDetail: null, |
| | | }) |
| | | |
| | | use([CanvasRenderer, LineChart, GridComponent, LegendComponent, TooltipComponent]) |
| | |
| | | const batteryFields = ref(['额定容量', '电流', '内阻']) |
| | | const specialFields = ref(['温升']) |
| | | const detailFieldValues = reactive<Record<string, Record<string, string>>>({}) |
| | | const detailSaving = ref(false) |
| | | const templateDialogVisible = ref(false) |
| | | const templateMode = ref<'battery' | 'special'>('battery') |
| | | const selectedBatteryFields = ref<string[]>([]) |
| | |
| | | } |
| | | |
| | | const cycleOptions = computed(() => [...new Set(props.records |
| | | .filter(record => record.sourceType === 'SNAPSHOT') |
| | | .map(record => Number(record.cycleNo)) |
| | | .filter(value => Number.isFinite(value) && value > 0))].sort((a, b) => a - b)) |
| | | |
| | | const stepOptions = computed<StepOption[]>(() => { |
| | | const cycleNo = Number(selectedCycle.value) |
| | | if (!Number.isFinite(cycleNo) || cycleNo <= 0) return [] |
| | | const actual = new Map<string, StepOption>() |
| | | const actualAcrossCycles = new Map<number, StepOption>() |
| | | for (const record of props.records) { |
| | | const cycleNo = Number(record.cycleNo) |
| | | for (const record of props.records.filter(item => item.sourceType === 'SNAPSHOT')) { |
| | | const stepNo = Number(record.stepNo) |
| | | if (!Number.isFinite(cycleNo) || !Number.isFinite(stepNo) || cycleNo <= 0 || stepNo <= 0) continue |
| | | if (Number(record.cycleNo) !== cycleNo || !Number.isFinite(stepNo) || stepNo <= 0) continue |
| | | const value = `actual:${cycleNo}:${stepNo}` |
| | | if (!actual.has(value)) actual.set(value, { value, label: `${cycleNo}-${stepNo} ${record.stepName || '-'}`, cycleNo, stepNo }) |
| | | if (!actualAcrossCycles.has(stepNo)) actualAcrossCycles.set(stepNo, { |
| | | value: `actual:all:${stepNo}`, |
| | | label: `全部循环-${stepNo} ${record.stepName || '-'}`, |
| | | cycleNo: 0, |
| | | stepNo, |
| | | }) |
| | | } |
| | | const logical: StepOption[] = [] |
| | | const logicalAcrossCycles: StepOption[] = [] |
| | | for (const cycleNo of cycleOptions.value) { |
| | | for (const mapping of props.logicStepMappings || []) { |
| | | logical.push({ value: `logical:${cycleNo}:${mapping.logicalNo}`, label: `${cycleNo}-逻辑${mapping.logicalNo} ${mapping.logicalName}`, cycleNo, logical: mapping }) |
| | | } |
| | | } |
| | | for (const mapping of props.logicStepMappings || []) { |
| | | logicalAcrossCycles.push({ |
| | | value: `logical:all:${mapping.logicalNo}`, |
| | | label: `全部循环-逻辑${mapping.logicalNo} ${mapping.logicalName}`, |
| | | cycleNo: 0, |
| | | logical: mapping, |
| | | }) |
| | | const hit = props.records.some(record => record.sourceType === 'SNAPSHOT' && Number(record.cycleNo) === cycleNo |
| | | && Number(record.stepNo) >= mapping.startStepNo && Number(record.stepNo) <= mapping.endStepNo) |
| | | if (hit) logical.push({ value: `logical:${cycleNo}:${mapping.logicalNo}`, label: `${cycleNo}-逻辑${mapping.logicalNo} ${mapping.logicalName}`, cycleNo, logical: mapping }) |
| | | } |
| | | return [...actualAcrossCycles.values(), ...actual.values(), ...logicalAcrossCycles, ...logical] |
| | | return [...actual.values(), ...logical] |
| | | }) |
| | | |
| | | function ensureDetailFieldValues() { |
| | |
| | | watch(() => props.testName, value => { |
| | | if (value) experimentName.value = value |
| | | }) |
| | | |
| | | watch(() => props.consoleDetail, value => applyConsoleDetail(value), { immediate: true, deep: true }) |
| | | |
| | | watch(() => props.cells, cells => { |
| | | const validCellNos = new Set(cells.map(cell => cell.no)) |
| | |
| | | |
| | | watch(cycleOptions, options => { |
| | | const current = Number(selectedCycle.value) |
| | | if (selectedCycle.value && !options.includes(current)) selectedCycle.value = '' |
| | | }) |
| | | if (!options.length) selectedCycle.value = '' |
| | | else if (!options.includes(current)) selectedCycle.value = String(options[0]) |
| | | }, { immediate: true }) |
| | | |
| | | async function addCustomField() { |
| | | try { |
| | |
| | | delete detailFieldValues[field] |
| | | } |
| | | |
| | | async function saveConsoleDetail() { |
| | | detailSaving.value = true |
| | | try { |
| | | const detail: TestConsoleDetail = { |
| | | experimentName: experimentName.value.trim() || null, |
| | | batteryFields: [...batteryFields.value], |
| | | specialFields: [...specialFields.value], |
| | | fieldValues: JSON.parse(JSON.stringify(detailFieldValues)) as Record<string, Record<string, string>>, |
| | | } |
| | | const saved = await saveTestConsoleDetail(props.testId, detail) |
| | | applyConsoleDetail(saved.consoleDetail) |
| | | ElMessage.success('测试详情已保存') |
| | | } catch (error) { |
| | | ElMessage.error(error instanceof Error ? error.message : '测试详情保存失败') |
| | | } finally { detailSaving.value = false } |
| | | } |
| | | |
| | | function openFieldTemplate(mode: 'battery' | 'special') { |
| | | templateMode.value = mode |
| | | selectedBatteryFields.value = [...batteryFields.value] |
| | |
| | | ElMessage.success('字段模板已应用') |
| | | } |
| | | |
| | | async function saveSpecialTemplate() { |
| | | if (!specialFields.value.length) { |
| | | ElMessage.warning('请先添加特殊字段') |
| | | return |
| | | } |
| | | try { |
| | | const result = await ElMessageBox.prompt('请输入特殊字段模板名称', '另存特殊字段模板', { |
| | | confirmButtonText: '保存', |
| | | cancelButtonText: '取消', |
| | | inputPlaceholder: '输入模板名称', |
| | | inputValidator: value => value.trim() ? true : '请输入模板名称', |
| | | }) |
| | | ElMessage.success(`已另存为“${result.value.trim()}”`) |
| | | } catch { |
| | | // 用户取消另存。 |
| | | } |
| | | } |
| | | |
| | | function updateSeriesColor(index: number, color: string | null) { |
| | | if (color) seriesColors.value[index] = color |
| | | } |
| | | |
| | | function ensureSelectedCell() { |
| | | if (!selectedCellNos.value.length && props.cells.length) selectedCellNos.value = [props.cells[0].no] |
| | | } |
| | | |
| | | function metricMatches(metric: CompareMetricKey, record: PersistedTestRecord) { |
| | |
| | | return false |
| | | } |
| | | |
| | | function applyConsoleDetail(detail: TestConsoleDetail | null | undefined) { |
| | | if (!detail) return |
| | | experimentName.value = detail.experimentName || '' |
| | | if (Array.isArray(detail.batteryFields)) batteryFields.value = [...detail.batteryFields] |
| | | if (Array.isArray(detail.specialFields)) specialFields.value = [...detail.specialFields] |
| | | Object.keys(detailFieldValues).forEach(key => delete detailFieldValues[key]) |
| | | Object.entries(detail.fieldValues || {}).forEach(([field, values]) => { detailFieldValues[field] = { ...values } }) |
| | | ensureDetailFieldValues() |
| | | } |
| | | |
| | | function metricValue(metric: CompareMetricKey, record: PersistedTestRecord) { |
| | | if (metric === 'voltage') return Number(record.voltage) |
| | | if (metric === 'current') return Number(record.current) |
| | |
| | | function selectedRecords() { |
| | | const option = stepOptions.value.find(item => item.value === selectedStep.value) |
| | | if (!option) return [] |
| | | const cycle = selectedCycle.value ? Number(selectedCycle.value) : option.cycleNo > 0 ? option.cycleNo : null |
| | | const cycle = Number(selectedCycle.value) |
| | | if (!Number.isFinite(cycle) || cycle <= 0) return [] |
| | | return props.records.filter(record => { |
| | | if (cycle !== null && Number(record.cycleNo) !== cycle) return false |
| | | if (record.sourceType !== 'SNAPSHOT') return false |
| | | if (Number(record.cycleNo) !== cycle) return false |
| | | const stepNo = Number(record.stepNo) |
| | | if (option.logical) return stepNo >= option.logical.startStepNo && stepNo <= option.logical.endStepNo |
| | | return stepNo === option.stepNo |
| | |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | <div class="detail-actions"><button class="primary-button" type="button" @click="addCustomField"><el-icon><Plus /></el-icon>添加项</button><button class="secondary-button" type="button" @click="openFieldTemplate('battery')">电池组字段模板</button><button class="secondary-button" type="button" @click="openFieldTemplate('special')">选择特殊字段模板</button><button class="secondary-button" type="button" @click="saveSpecialTemplate">另存特殊字段模板</button></div> |
| | | <div class="detail-actions"><button class="primary-button" type="button" :disabled="detailSaving" @click="saveConsoleDetail"><el-icon><Check /></el-icon>保存详情</button><button class="secondary-button" type="button" @click="addCustomField"><el-icon><Plus /></el-icon>添加项</button><button class="secondary-button" type="button" @click="openFieldTemplate('battery')">电池组字段模板</button><button class="secondary-button" type="button" @click="openFieldTemplate('special')">选择特殊字段模板</button></div> |
| | | </section> |
| | | |
| | | <section class="data-select-panel"> |
| | | <header class="panel-heading"><div><strong>数据选择</strong><el-icon><ArrowUp /></el-icon></div></header> |
| | | <div class="data-select-content"> |
| | | <label class="step-select"><span>循环</span><el-select v-model="selectedCycle" clearable placeholder="跟随工步"><el-option v-for="item in cycleOptions" :key="item" :label="`循环 ${item}`" :value="String(item)" /></el-select></label> |
| | | <label class="step-select"><span>循环工步</span><el-select v-model="selectedStep" :disabled="!stepOptions.length" placeholder="暂无实际工步"><el-option v-for="item in stepOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></label> |
| | | <div class="battery-select"><span>电池号</span><el-checkbox-group v-model="selectedCellNos"><el-checkbox v-for="cell in cells" :key="cell.no" :value="cell.no">{{ cell.no }}</el-checkbox></el-checkbox-group></div> |
| | | <label class="step-select"><span>循环</span><el-select v-model="selectedCycle" :disabled="!cycleOptions.length" placeholder="暂无实时循环"><el-option v-for="item in cycleOptions" :key="item" :label="`循环 ${item}`" :value="String(item)" /></el-select></label> |
| | | <label class="step-select"><span>循环工步</span><el-select v-model="selectedStep" :disabled="!stepOptions.length" placeholder="暂无该循环工步"><el-option v-for="item in stepOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></label> |
| | | <div class="battery-select"><span>电池号</span><el-checkbox-group v-model="selectedCellNos" @change="ensureSelectedCell"><el-checkbox v-for="cell in cells" :key="cell.no" :value="cell.no">{{ cell.no }}</el-checkbox></el-checkbox-group></div> |
| | | </div> |
| | | </section> |
| | | |