yeg
2026-09-09 1b0179bba53bcc75e8ae358a4a58e8c8fb33c043
src/views/FormationReportForm.vue
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import { use } from 'echarts/core'
@@ -7,263 +7,197 @@
import { CanvasRenderer } from 'echarts/renderers'
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import VChart from 'vue-echarts'
import { getCurrentUser } from '@/utils/auth'
import { useFormationReports, type FormationReportCycle } from '@/stores/formationReports'
import { useBatteryGroupFields } from '@/stores/batteryGroupFields'
import { formationCycleFields as cycleFields, formationFixedReportFields as fixedReportFields, formationMetricDefinitions, openFormationReportPrint, type FormationMetricDefinition } from '@/utils/formationReportPrint'
import { getUserInfo } from '@/api/auth'
import { getFormationSources, previewFormationReport, getFormationReport, createFormationReport, editFormationReport, refreshFormationSource,
  listFormationVersions, listFormationTemplates, createFormationTemplate, updateFormationTemplate,
  type FormationCandidate, type FormationMatrix, type FormationSettings, type FormationItem, type FormationField, type FormationVersion, type FormationTemplate, type SavedFormationReport } from '@/api/formationReports'
import { defaultFormationFields, formationSourceValue } from '@/utils/formationReportFields'
import { formationCellKey, formationInputValue, formationCalculatedValue, formationNumberText } from '@/utils/formationReportCalculations'
import { formationCurveMetrics, formationSeriesKey, formationSeriesColor } from '@/utils/formationCycleChart'
import { formationChargeSteps, formationStepChartOption, formationStepGroups } from '@/utils/formationStepChart'
import { openFormationSavedReportPrint } from '@/utils/formationSavedReportPrint'
use([CanvasRenderer, LineChart, GridComponent, LegendComponent, TooltipComponent])
const route = useRoute(); const router = useRouter(); const currentUser = getCurrentUser(); const { records, createReport, updateReport } = useFormationReports()
const { fields: managedGroupFields } = useBatteryGroupFields()
const editingId = computed(() => { const value = Number(route.params.id); return Number.isFinite(value) && value > 0 ? value : null })
const editingRecord = computed(() => editingId.value ? records.value.find(item => item.id === editingId.value) || null : null)
const route = useRoute(), router = useRouter()
const editingId = computed(() => Number(route.params.id) || null)
const readonly = computed(() => route.query.mode === 'view')
const reportName = ref(''); const testName = ref('SP2电池化成测试'); const batteryGroup = ref('SP2样品组'); const saving = ref(false)
const cycles = ref<FormationReportCycle[]>([
  { cycle: 1, method: '恒流充电 / 恒流放电', chargeVoltage: '4.20', dischargeCapacity: '98.6%', retention: '100%', result: '合格' },
  { cycle: 2, method: '恒流充电 / 恒流放电', chargeVoltage: '4.20', dischargeCapacity: '98.1%', retention: '99.5%', result: '合格' },
  { cycle: 3, method: '恒流充电 / 恒流放电', chargeVoltage: '4.20', dischargeCapacity: '97.6%', retention: '99.0%', result: '合格' },
])
const description = ref('汇总化成阶段的充放电数据、容量保持率和温升情况。')
const seriesColors = ref(['#3978f6', '#7d42d8', '#f08a3e'])
const formationMetricVisible = reactive<Record<FormationMetricDefinition['key'], boolean>>({ voltage: true, current: true, positiveVoltage: true, negativeVoltage: true, temperature: true })
const visibleFormationMetrics = computed(() => formationMetricDefinitions.filter(metric => formationMetricVisible[metric.key]))
const activeCycle = ref(1)
const reportColumns = reactive([
  { id: 1, name: 'SP2电池化成测试', samples: ['样品1', '样品2', '样品3'] },
  { id: 2, name: '高温化成测试', samples: ['样品1', '样品2', '样品3'] },
])
const batteryFields = ref<string[]>([])
const specialFields = ref<string[]>([])
const reportFieldValues = reactive<Record<string, string>>({})
const templateDialogVisible = ref(false)
const templateMode = ref<'battery' | 'special'>('battery')
const selectedTemplateKey = ref('cycle')
const batteryTemplateStorageKey = 'battery_detection_group_field_templates_v1'
const presetBatteryTemplateLabels: Record<string, string[]> = {
  '标准容量样品': ['额定容量', '电流', '内阻'],
  '标准循环样品': ['额定容量', '循环温度', '内阻'],
  '低温性能样品': ['目标温度', '静置时间', '环境备注'],
}
const batteryTemplateSelections = reactive<Record<string, number[]>>((() => {
  try {
    const stored = localStorage.getItem(batteryTemplateStorageKey)
    return stored ? JSON.parse(stored) as Record<string, number[]> : {}
  } catch {
    localStorage.removeItem(batteryTemplateStorageKey)
    return {}
  }
})())
const selectedBatteryTemplateName = ref('标准容量样品')
const selectedBatteryTemplateFieldIds = ref<number[]>([])
const specialTemplateStorageKey = 'battery_detection_compare_special_templates_v2'
const specialTemplateSelections = reactive<Record<string, string[]>>((() => {
  try {
    const stored = localStorage.getItem(specialTemplateStorageKey)
    return stored ? JSON.parse(stored) as Record<string, string[]> : {}
  } catch {
    localStorage.removeItem(specialTemplateStorageKey)
    return {}
  }
})())
const fieldTemplates = computed(() => [
  { key: 'cycle', name: '循环寿命模板', fields: ['容量保持率', '能量效率', '循环次数', '内阻变化'] },
  { key: 'thermal', name: '热特性模板', fields: ['最高温度', '温差', '温升速率'] },
  ...Object.entries(specialTemplateSelections).map(([name, fields]) => ({ key: `saved:${name}`, name, fields })),
])
const reportSamples = computed(() => reportColumns.flatMap(column => column.samples))
const enabledGroupFields = computed(() => managedGroupFields.value.filter(field => field.status === '启用'))
const batteryTemplateFieldOptions = computed(() => [...managedGroupFields.value].sort((a, b) => b.createdAt.localeCompare(a.createdAt)))
const batteryTemplateNames = computed(() => [
  '自定义',
  '标准容量样品',
  '标准循环样品',
  '低温性能样品',
  ...Object.keys(batteryTemplateSelections).filter(name => !['自定义', '标准容量样品', '标准循环样品', '低温性能样品'].includes(name)),
])
const selectedTemplate = computed(() => fieldTemplates.value.find(item => item.key === selectedTemplateKey.value) || fieldTemplates.value[0])
const reportDetailFields = computed(() => [
  ...fixedReportFields,
  ...batteryFields.value.map(field => ({ key: `battery:${field}`, label: field, mode: 'input', unit: '' })),
  ...specialFields.value.map(field => ({ key: `special:${field}`, label: field, mode: 'input', unit: '' })),
])
watch(testName, value => { reportColumns[0].name = value })
const cycleFieldValues = reactive<Record<string, string>>({})
const reportName = ref(''), description = ref(''), error = ref('')
const loading = ref(false), saving = ref(false), previewLoading = ref(false)
const permissions = ref<string[]>([])
const can = (action: string) => permissions.value.some(value => value === '*:*:*' || value === `system:formation-report:${action}`)
const candidates = ref<FormationCandidate[]>([]), selectedTestIds = ref<number[]>([])
const matrix = ref<FormationMatrix>({ items: [], cycleNos: [] })
const saved = ref<SavedFormationReport | null>(null)
const makeSettings = (): FormationSettings => ({fields:defaultFormationFields.map(field=>({...field})),values:{},chargeStepSelections:{},metrics:formationCurveMetrics.map(metric=>metric.key),seriesColors:{},hiddenSeriesKeys:[],activeCycle:null})
const settings = ref<FormationSettings>(makeSettings())
const dirty = ref(false)
const historyVisible=ref(false),history=ref<FormationVersion[]>([])
const busy = computed(()=>loading.value || saving.value || previewLoading.value)
const testOptions = computed(()=>[...new Map((editingId.value ? matrix.value.items.map(item=>item.source) : candidates.value).map(source=>[source.selection.testId,{id:source.selection.testId,name:source.name}])).values()])
const reportColumns = computed(()=>[...new Set(matrix.value.items.map(item=>item.source.selection.testId))].map(id=>({id,name:matrix.value.items.find(item=>item.source.selection.testId===id)!.source.name,items:matrix.value.items.filter(item=>item.source.selection.testId===id)})))
const detailFields = computed(()=>settings.value.fields.filter(field=>field.scope==='detail'))
const methodFields = computed(()=>settings.value.fields.filter(field=>field.scope==='cycle' && ['method','method2'].includes(field.key)))
const cycleFields = computed(()=>settings.value.fields.filter(field=>field.scope==='cycle' && !['method','method2'].includes(field.key)))
const activeCycle = computed({get:()=>settings.value.activeCycle??matrix.value.cycleNos[0]??null,set:(value:number|null)=>{settings.value.activeCycle=value;if(!readonly.value)dirty.value=true}})
const visibleMetrics = computed(()=>formationCurveMetrics.filter(metric=>settings.value.metrics?.includes(metric.key)))
const unknownSteps = computed(()=>matrix.value.items.flatMap(item=>matrix.value.cycleNos.flatMap(cycle=>(item.chargeSteps?.[cycle]??[]).filter(step=>!step.direction || step.direction==='UNKNOWN').map(step=>`${item.source.name}-${item.source.batteryNo} 循环${cycle} 工步${step.stepNo}`))).join(';'))
let loadSequence = 0, previewSequence = 0
const message = (cause: unknown) => cause instanceof Error ? cause.message : '请求失败,请重试'
function cycleValueKey(cycle: number, field: string, sample: string) { return `${cycle}|${field}|${sample}` }
function rawValue(cycle: number, fieldIndex: number, sampleIndex: number) {
  return (3.76 + cycle * 0.03 + fieldIndex * 0.018 + sampleIndex * 0.012).toFixed(3)
function loadSaved(record: SavedFormationReport) {
  saved.value=record;reportName.value=record.name;description.value=record.description??''
  matrix.value={items:record.items,cycleNos:record.cycleNos}
  settings.value=structuredClone(record.settings)
  selectedTestIds.value=[...new Set(record.items.map(item=>item.source.selection.testId))]
  dirty.value=false;error.value=''
}
function loadEditingRecord() {
  const record = editingRecord.value
  if (!record) return
  reportName.value = record.name; testName.value = record.testName; batteryGroup.value = record.batteryGroup; cycles.value = record.cycles.map(item => ({ ...item })); description.value = record.description
  reportColumns[0].name = record.testName
  reportColumns[1].name = `${record.testName} · 对照`
  const restoredSamples = record.samples.slice(0, 3)
  if (restoredSamples.length) reportColumns.forEach(column => { column.samples = [...restoredSamples] })
  Object.assign(cycleFieldValues, record.cycleFieldValues || {})
  Object.assign(reportFieldValues, record.reportFieldValues || {})
  if (record.batteryFields) batteryFields.value = [...record.batteryFields]
  if (record.specialFields) specialFields.value = [...record.specialFields]
  if (record.metrics) formationMetricDefinitions.forEach(metric => { formationMetricVisible[metric.key] = record.metrics!.includes(metric.key) })
  if (record.seriesColors?.length) seriesColors.value = [...record.seriesColors]
}
loadEditingRecord()
function reportValueKey(field: string, sample: string) { return `${field}|${sample}` }
function rawReportValue(fieldIndex: number, sampleIndex: number) { return (1.02 + fieldIndex * 0.06 + sampleIndex * 0.03).toFixed(2) }
function addReportField() {
  ElMessageBox.prompt('请输入要添加的字段名称', '添加项', { confirmButtonText: '添加', cancelButtonText: '取消', inputPlaceholder: '例如:环境备注', inputValidator: value => value.trim() ? true : '请输入字段名称' }).then(({ value }) => {
    const name = value.trim()
    if (reportDetailFields.value.some(field => field.label === name)) { ElMessage.warning('该字段已经存在'); return }
    specialFields.value.push(name)
    ElMessage.success(`已添加字段“${name}”`)
  }).catch(() => { /* 用户取消 */ })
}
function persistBatteryTemplates() { localStorage.setItem(batteryTemplateStorageKey, JSON.stringify(batteryTemplateSelections)) }
function resolveBatteryTemplateFieldIds(name: string) {
  const savedIds = batteryTemplateSelections[name]
  if (savedIds) return savedIds.filter(id => enabledGroupFields.value.some(field => field.id === id))
  const labels = presetBatteryTemplateLabels[name] || []
  return enabledGroupFields.value.filter(field => labels.includes(field.name)).map(field => field.id)
}
function loadBatteryTemplate(name: string) { selectedBatteryTemplateFieldIds.value = resolveBatteryTemplateFieldIds(name) }
function applyBatteryTemplateFields() {
  const fixedLabels = fixedReportFields.map(field => field.label)
  batteryFields.value = selectedBatteryTemplateFieldIds.value
    .map(id => enabledGroupFields.value.find(field => field.id === id)?.name)
    .filter((name): name is string => typeof name === 'string')
    .filter(name => !fixedLabels.includes(name))
  specialFields.value = specialFields.value.filter(field => !batteryFields.value.includes(field) && !fixedLabels.includes(field))
}
function switchBatteryTemplate(name: string) { loadBatteryTemplate(name); applyBatteryTemplateFields() }
function saveBatteryTemplate() { applyBatteryTemplateFields(); batteryTemplateSelections[selectedBatteryTemplateName.value] = [...selectedBatteryTemplateFieldIds.value]; persistBatteryTemplates(); ElMessage.success(`模板“${selectedBatteryTemplateName.value}”已保存`) }
async function saveBatteryTemplateAs() {
watch(()=>route.fullPath,async()=>{
  const seq=++loadSequence; ++previewSequence
  loading.value=true;previewLoading.value=false;error.value='';saved.value=null;matrix.value={items:[],cycleNos:[]};settings.value=makeSettings();selectedTestIds.value=[]
  reportName.value='';description.value='';dirty.value=false;historyVisible.value=false
  try {
    const result = await ElMessageBox.prompt('请输入新模板名称,当前字段勾选会保存为新模板。', '另存电池组字段模板', { confirmButtonText: '保存', cancelButtonText: '取消', inputPlaceholder: '输入模板名称', inputValidator: value => value.trim() ? true : '请输入模板名称' })
    const name = result.value.trim()
    if (batteryTemplateNames.value.includes(name)) { ElMessage.warning('模板名称已存在,请使用其他名称'); return }
    selectedBatteryTemplateName.value = name
    applyBatteryTemplateFields()
    batteryTemplateSelections[name] = [...selectedBatteryTemplateFieldIds.value]
    persistBatteryTemplates()
    ElMessage.success(`已另存为“${name}”`)
  } catch { /* 用户取消 */ }
}
function openFieldTemplate(mode: 'battery' | 'special') {
  templateMode.value = mode
  if (mode === 'battery') {
    if (!batteryTemplateNames.value.includes(selectedBatteryTemplateName.value)) selectedBatteryTemplateName.value = '自定义'
    loadBatteryTemplate(selectedBatteryTemplateName.value)
  } else {
    selectedTemplateKey.value = 'cycle'
  }
  templateDialogVisible.value = true
}
function applyFieldTemplate() {
  const fixedLabels = fixedReportFields.map(field => field.label)
  if (templateMode.value === 'battery') {
    applyBatteryTemplateFields()
  } else {
    specialFields.value = selectedTemplate.value.fields.filter(field => !fixedLabels.includes(field) && !batteryFields.value.includes(field))
  }
  templateDialogVisible.value = false
  ElMessage.success(`${templateMode.value === 'battery' ? '电池组字段' : '特殊字段'}模板已应用`)
}
async function saveSpecialTemplate() {
  if (!specialFields.value.length) { ElMessage.warning('请先添加或选择特殊字段'); return }
  try {
    const result = await ElMessageBox.prompt('请输入特殊字段模板名称,当前特殊字段会保存为新模板。', '另存特殊字段模板', { confirmButtonText: '保存', cancelButtonText: '取消', inputPlaceholder: '输入模板名称', inputValidator: value => value.trim() ? true : '请输入模板名称' })
    const name = result.value.trim()
    if (fieldTemplates.value.some(item => item.name === name)) { ElMessage.warning('特殊字段模板名称已存在,请使用其他名称'); return }
    specialTemplateSelections[name] = [...specialFields.value]
    localStorage.setItem(specialTemplateStorageKey, JSON.stringify(specialTemplateSelections))
    selectedTemplateKey.value = `saved:${name}`
    ElMessage.success(`已另存为“${name}”`)
  } catch { /* 用户取消 */ }
}
function updateSeriesColor(index: number, color: string | null) { if (color) seriesColors.value[index] = color }
function chartOption(metric: FormationMetricDefinition) {
  const names = reportSamples.value.slice(0, 3)
  const cycleOffset = Math.max(0, activeCycle.value - 1) * 0.012
  const visibleSeries = metric.values.slice(0, Math.max(1, names.length)).map(series => series.map(value => Number((value + cycleOffset).toFixed(3))))
  const visibleValues = visibleSeries.flat()
  const dataMax = Math.max(0, ...visibleValues)
  const axisMax = metric.unit === '℃' ? Math.ceil(dataMax / 10) * 10 : Math.ceil(dataMax * 10) / 10
  const maxDataLength = Math.max(1, ...visibleSeries.map(series => series.length))
  const timeLabels = Array.from({ length: maxDataLength }, (_, index) => `${Math.floor(index / 2)}:${index % 2 ? '30' : '00'}`)
  const createYAxis = (position: 'left' | 'right') => ({ type: 'value', name: metric.unit, position, min: 0, max: axisMax, nameTextStyle: { color: '#8795a3', fontSize: 10 }, axisLine: { show: true, lineStyle: { color: '#cbd8e4' } }, axisTick: { show: true, lineStyle: { color: '#cbd8e4' } }, splitLine: { show: position === 'left', lineStyle: { color: '#e8eef4' } }, axisLabel: { color: '#8795a3', fontSize: 10 } })
  return { animation: false, color: seriesColors.value, grid: { top: 40, right: 46, bottom: 42, left: 46 }, tooltip: { trigger: 'axis' }, legend: { top: 4, right: 6, icon: 'roundRect', itemWidth: 14, itemHeight: 8, textStyle: { fontSize: 11, color: '#52606d' } }, xAxis: { type: 'category', boundaryGap: false, name: '时间/h:mm', nameLocation: 'middle', nameGap: 28, data: timeLabels, axisLine: { lineStyle: { color: '#cbd8e4' } }, axisLabel: { color: '#8795a3', fontSize: 10, interval: 'auto' }, nameTextStyle: { color: '#26333f', fontSize: 11, fontWeight: 600 } }, yAxis: [createYAxis('left'), createYAxis('right')], series: names.map((name, index) => ({ name, type: 'line', smooth: true, showSymbol: false, lineStyle: { width: 2 }, data: visibleSeries[index] || visibleSeries[0] })) }
}
function exportPdf() {
  if (!reportName.value.trim()) {
    ElMessage.warning('请先输入报表名称')
    return
  }
  const opened = openFormationReportPrint({
    name: reportName.value.trim(),
    reportId: editingRecord.value?.id,
    creator: editingRecord.value?.creator || currentUser?.displayName || 'Admin',
    createdAt: editingRecord.value?.createdAt,
    testName: testName.value,
    batteryGroup: batteryGroup.value,
    samples: [...new Set(reportSamples.value)],
    cycles: cycles.value.map(item => ({ ...item })),
    cycleFieldValues: { ...cycleFieldValues },
    reportFieldValues: { ...reportFieldValues },
    batteryFields: [...batteryFields.value],
    specialFields: [...specialFields.value],
    metrics: visibleFormationMetrics.value.map(item => item.key),
    seriesColors: [...seriesColors.value],
    description: description.value.trim(),
  })
  if (!opened) {
    ElMessage.warning('浏览器阻止了报表窗口,请允许弹出窗口后重试')
    return
  }
  ElMessage.info('已打开打印版报表,请在打印窗口选择“另存为 PDF”')
}
    const info=await getUserInfo();if(seq!==loadSequence)return
    permissions.value=info.permissions??[]
    if(!can(editingId.value ? readonly.value ? 'query':'edit':'add'))throw new Error('无当前化成报表操作权限')
    if(editingId.value){
      const record=await getFormationReport(editingId.value,readonly.value&&route.query.version?Number(route.query.version):undefined,!readonly.value)
      if(seq===loadSequence)loadSaved(record)
    }else{
      const sources=await getFormationSources();if(seq===loadSequence)candidates.value=sources
    }
  }catch(cause){if(seq===loadSequence)error.value=message(cause)}finally{if(seq===loadSequence)loading.value=false}
},{immediate:true})
async function saveReport() {
  if (!reportName.value.trim()) { ElMessage.warning('请输入报表名称'); return }
  if (!testName.value || !batteryGroup.value) { ElMessage.warning('请选择测试和电池组'); return }
  saving.value = true
  await new Promise(resolve => window.setTimeout(resolve, 220))
  const payload = {
    name: reportName.value.trim(),
    creator: currentUser?.displayName || 'Admin',
    testName: testName.value,
    batteryGroup: batteryGroup.value,
    samples: [...new Set(reportSamples.value)],
    cycles: cycles.value.map(item => ({ ...item })),
    cycleFieldValues: { ...cycleFieldValues },
    reportFieldValues: { ...reportFieldValues },
    batteryFields: [...batteryFields.value],
    specialFields: [...specialFields.value],
    metrics: visibleFormationMetrics.value.map(item => item.key),
    seriesColors: [...seriesColors.value],
    description: description.value.trim(),
  }
  editingId.value ? updateReport(editingId.value, payload) : createReport(payload)
  saving.value = false
  ElMessage.success(editingId.value ? '化成报表已更新' : '化成报表已保存')
  await router.push('/report-formation')
/** 多选异步返回只归属当前选择,旧请求不能把另一测试数据带入现有表格。 */
async function selectTests() {
  const seq=++previewSequence
  const selections=selectedTestIds.value.flatMap(id=>candidates.value.filter(source=>source.selection.testId===id).map(source=>source.selection))
  error.value='';previewLoading.value=true;dirty.value=true
  if(!selections.length){matrix.value={items:[],cycleNos:[]};settings.value=makeSettings();previewLoading.value=false;return}
  try{
    const next=await previewFormationReport(selections);if(seq!==previewSequence)return
    settings.value.values=Object.fromEntries(Object.entries(settings.value.values).filter(([key])=>{
      const parts=key.split('|'),item=next.items.find(item=>formationSeriesKey(item)===parts.slice(0,2).join('|'))
      return item&&(parts[2]==='0'||item.cycleNos.includes(Number(parts[2])))
    }))
    matrix.value=next
    const extras=next.items.flatMap(item=>item.businessFields).filter(field=>!settings.value.fields.some(existing=>existing.key===field.fieldName))
    for(const field of extras)if(!settings.value.fields.some(existing=>existing.key===field.fieldName))settings.value.fields.push({key:field.fieldName,scope:'detail',kind:'battery',displayName:field.fieldName,unit:'',fieldDefinitionId:field.fieldDefinitionId})
    const sourceKeys=new Set(next.items.map(formationSeriesKey))
    settings.value.seriesColors=Object.fromEntries(Object.entries(settings.value.seriesColors??{}).filter(([key])=>sourceKeys.has(key)))
    settings.value.hiddenSeriesKeys=(settings.value.hiddenSeriesKeys??[]).filter(key=>sourceKeys.has(key))
    if(!next.cycleNos.includes(activeCycle.value??-1))settings.value.activeCycle=next.cycleNos[0]??null
  }catch(cause){if(seq===previewSequence){matrix.value={items:[],cycleNos:[]};error.value=message(cause)}}finally{if(seq===previewSequence)previewLoading.value=false}
}
function updateValue(item:FormationItem,cycle:number,field:FormationField,value:string){settings.value.values[formationCellKey(item,cycle,field.key)]=value;dirty.value=true}
function inputValue(item:FormationItem,cycle:number,field:FormationField){return formationInputValue(item,field,settings.value.values,cycle)}
function calculation(item:FormationItem,cycle:number,field:FormationField){return formationCalculatedValue(item,cycle,field.key,settings.value)}
function displayValue(item:FormationItem,cycle:number,field:FormationField){
  if(field.kind!=='calculated')return formationSourceValue(item,cycle,field)||'—'
  const key=formationCellKey(item,cycle,field.key)
  return formationNumberText(readonly.value&&saved.value ? saved.value.calculatedValues?.[key] : calculation(item,cycle,field).value)
}
function chargeCount(cycle:number){return Math.max(0,...matrix.value.items.map(item=>formationChargeSteps(item,cycle).length))}
function voltageTitle(item:FormationItem,cycle:number,index:number){const step=formationChargeSteps(item,cycle)[index];return step?`工步${step.stepNo} ${step.name||''} · 终止电压 (V)`:'无对应充电工步'}
function setMetric(key:typeof formationCurveMetrics[number]['key'],show:boolean){settings.value.metrics=formationCurveMetrics.filter(metric=>metric.key===key?show:settings.value.metrics?.includes(metric.key)).map(metric=>metric.key);if(!readonly.value)dirty.value=true}
function setColor(item:FormationItem,color:string|null){if(color){settings.value.seriesColors={...settings.value.seriesColors,[formationSeriesKey(item)]:color};dirty.value=true}}
function toggleSeries(item:FormationItem){const key=formationSeriesKey(item),hidden=new Set(settings.value.hiddenSeriesKeys??[]);hidden.has(key)?hidden.delete(key):hidden.add(key);settings.value.hiddenSeriesKeys=[...hidden];if(!readonly.value)dirty.value=true}
function legendChanged(event:{selected:Record<string,boolean>}){settings.value.hiddenSeriesKeys=Object.entries(event.selected).filter(([,shown])=>!shown).map(([key])=>key);if(!readonly.value)dirty.value=true}
async function saveReport(){
  if(!reportName.value.trim()){ElMessage.warning('请输入报表名称');return}
  if(!matrix.value.items.length || error.value){ElMessage.warning('请先选择测试并完成数据加载');return}
  if(Object.entries(settings.value.values).some(([key,value])=>key.endsWith('|manual')&&value.trim()&&!/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(value.trim()))){ElMessage.warning('数字仅允许非负整数或小数');return}
  let reason=''
  if(saved.value){try{reason=(await ElMessageBox.prompt('请输入本次修改说明','保存修改',{inputValidator:value=>Boolean(value?.trim())||'请输入修改说明'})).value.trim()}catch{return}}
  saving.value=true
  try{
    // Vue 的响应式代理先序列化成普通提交对象;只提交字段值,测量和公式由后端冻结。
    const payload={name:reportName.value.trim(),description:description.value,settings:JSON.parse(JSON.stringify(settings.value)) as FormationSettings}
    const record=saved.value?await editFormationReport(saved.value.id,{...payload,baseVersionNo:saved.value.versionNo,changeSummary:reason}):await createFormationReport({...payload,selections:matrix.value.items.map(item=>item.source.selection)})
    loadSaved(record);ElMessage.success('化成报表已保存')
    await router.replace({path:`/report-formation/${record.id}/edit`,query:{mode:'view'}})
  }catch(cause){ElMessage.error(message(cause))}finally{saving.value=false}
}
async function refreshSource(){
  if(!saved.value)return
  if(dirty.value){ElMessage.warning('请先保存当前修改,再更新来源');return}
  let reason='';try{reason=(await ElMessageBox.prompt('重新读取来源数据并生成新版本,保留报表人工字段值。请输入说明。','按最新来源生成',{inputValidator:value=>Boolean(value?.trim())||'请输入说明'})).value.trim()}catch{return}
  saving.value=true
  try{loadSaved(await refreshFormationSource(saved.value.id,saved.value.versionNo,reason));ElMessage.success('已生成最新来源版本')}catch(cause){ElMessage.error(message(cause))}finally{saving.value=false}
}
async function exportPdf(){
  if(!saved.value||dirty.value){ElMessage.warning('请先保存报表后再导出');return}
  try{await openFormationSavedReportPrint(saved.value.id,saved.value.versionNo)}catch(cause){ElMessage.error(message(cause))}
}
async function openHistory(){if(!saved.value)return;try{history.value=await listFormationVersions(saved.value.id);historyVisible.value=true}catch(cause){ElMessage.error(message(cause))}}
function viewVersion(version:number){historyVisible.value=false;router.push({path:`/report-formation/${editingId.value}/edit`,query:{mode:'view',version}})}
const templateDialogVisible=ref(false),templateMode=ref<'battery'|'special'>('battery'),templates=ref<FormationTemplate[]>([]),templateId=ref<number|null>(null),templateLoading=ref(false)
const templateFieldKeys=ref<string[]>([])
const availableFields=computed(()=>[...new Map([...settings.value.fields,...matrix.value.items.flatMap(item=>item.businessFields.map(field=>({key:field.fieldName,scope:'detail' as const,kind:'battery' as const,displayName:field.fieldName,unit:'',fieldDefinitionId:field.fieldDefinitionId})))].filter(field=>field.scope==='detail'&&field.kind=== (templateMode.value==='battery'?'battery':'manual')).map(field=>[field.key,field])).values()])
async function addReportField(){
  try{const name=(await ElMessageBox.prompt('请输入字段名称','添加项',{inputValidator:value=>Boolean(value?.trim())||'请输入字段名称'})).value.trim()
    if(name.includes('|')||name.length>100||settings.value.fields.some(field=>field.key===name)){ElMessage.warning('字段名称重复或无效');return}
    settings.value.fields.push({key:name,scope:'detail',kind:'manual',displayName:name,unit:''});dirty.value=true
  }catch{/* 用户取消 */}
}
async function openFieldTemplate(mode:'battery'|'special'){
  templateMode.value=mode;templateId.value=null;templateFieldKeys.value=settings.value.fields.filter(field=>field.scope==='detail'&&field.kind===(mode==='battery'?'battery':'manual')).map(field=>field.key)
  templateDialogVisible.value=true;templateLoading.value=true
  try{templates.value=await listFormationTemplates()}catch(cause){ElMessage.error(message(cause))}finally{templateLoading.value=false}
}
function selectTemplate(id:number|null){const template=templates.value.find(value=>value.id===id);if(template)templateFieldKeys.value=template.fields.filter(field=>field.scope==='detail'&&field.kind===(templateMode.value==='battery'?'battery':'manual')).map(field=>field.key)}
function applyFieldTemplate(){
  const kind=templateMode.value==='battery'?'battery':'manual',required=new Set(defaultFormationFields.filter(field=>field.scope==='detail').map(field=>field.key))
  const options=[...availableFields.value,...(templates.value.find(value=>value.id===templateId.value)?.fields??[])].filter(field=>field.scope==='detail'&&field.kind===kind)
  settings.value.fields=settings.value.fields.filter(field=>field.scope!=='detail'||field.kind!==kind||required.has(field.key))
  for(const key of templateFieldKeys.value){const field=options.find(field=>field.key===key);if(field&&!settings.value.fields.some(value=>value.key===key))settings.value.fields.push({...field})}
  const allowed=new Set(settings.value.fields.map(field=>field.key));settings.value.values=Object.fromEntries(Object.entries(settings.value.values).filter(([key])=>allowed.has(key.split('|').at(-1)!)))
  dirty.value=true;templateDialogVisible.value=false
}
async function saveFieldTemplate(asNew=false){
  try{const selected=templates.value.find(value=>value.id===templateId.value)
    const name=(await ElMessageBox.prompt('模板只保存字段选择,不包含测试和填写值。','保存化成字段模板',{inputValue:asNew?'':selected?.name??'',inputValidator:value=>Boolean(value?.trim())||'请输入名称'})).value.trim()
    applyFieldTemplate();const fields=settings.value.fields.filter(field=>field.scope==='detail'&&field.kind===(templateMode.value==='battery'?'battery':'manual'))
    if(!fields.length){ElMessage.warning('请先选择或添加字段');return}
    const existing=templates.value.find(value=>value.name===name)
    if(existing){if(!can('edit'))throw new Error('无编辑模板权限');await updateFormationTemplate(existing.id,name,fields)}else{if(!can('add'))throw new Error('无新增模板权限');await createFormationTemplate(name,fields)}
    ElMessage.success('模板已保存')
  }catch(cause){if(cause!=='cancel'&&cause!=='close')ElMessage.error(message(cause))}
}
</script>
<template>
  <div class="formation-form-page" :class="{ 'is-readonly': readonly }"><div class="page-tools"><button class="back-button" type="button" @click="router.push('/report-formation')"><el-icon><ArrowLeft /></el-icon>返回化成报表</button><span>{{ readonly ? 'VIEW FORMATION REPORT' : editingId ? 'EDIT FORMATION REPORT' : 'NEW FORMATION REPORT' }}</span></div>
    <section class="report-name-panel"><label><span>报表名称</span><el-input v-model="reportName" clearable placeholder="请输入报表名称" /></label></section>
    <section class="report-data-panel"><header class="panel-heading"><div><strong>报表数据</strong><span>测试详情字段</span></div></header><div class="report-data-table-wrap"><table class="report-data-table" :style="{ minWidth: `${Math.max(980, reportSamples.length * 150 + 160)}px` }"><thead><tr><th>电池组</th><th v-for="column in reportColumns" :key="column.id" :colspan="column.samples.length"><span class="group-index">{{ column.id }}</span>{{ column.name }}</th></tr><tr><th>电池号</th><template v-for="column in reportColumns" :key="`samples-${column.id}`"><th v-for="(sample, sampleIndex) in column.samples" :key="`${column.id}-${sampleIndex}`"><span class="fixed-sample">{{ sample }}</span></th></template></tr></thead><tbody><tr v-for="(field, fieldIndex) in reportDetailFields" :key="field.key"><td>{{ field.label }}</td><template v-for="(column, columnIndex) in reportColumns" :key="`fields-${column.id}`"><td v-for="(sample, sampleIndex) in column.samples" :key="`${field.key}-${column.id}-${sampleIndex}`"><el-input v-if="field.mode === 'input'" v-model="reportFieldValues[reportValueKey(field.key, `${column.id}-${sample}`)]" size="small" placeholder="请输入" /><span v-else-if="field.mode === 'raw'" class="raw-value">{{ rawReportValue(fieldIndex, sampleIndex + columnIndex) }}</span><span v-else class="auto-value">自动计算</span></td></template></tr></tbody></table></div><div v-if="!readonly" class="report-data-actions"><button class="primary-button" type="button" @click="addReportField"><el-icon><Plus /></el-icon>添加项</button><button class="secondary-button" type="button" @click="openFieldTemplate('battery')"><el-icon><DocumentCopy /></el-icon>电池组字段模板</button><button class="secondary-button" type="button" @click="openFieldTemplate('special')"><el-icon><DocumentCopy /></el-icon>选择特殊字段模板</button><button class="secondary-button" type="button" @click="saveSpecialTemplate"><el-icon><DocumentCopy /></el-icon>另存特殊字段模板</button></div></section>
    <section class="cycle-panel"><header class="panel-heading"><div><strong>化成循环明细</strong></div></header><div class="cycle-list"><article v-for="cycle in cycles" :key="cycle.cycle" class="cycle-block"><div class="cycle-block-heading"><strong>循环{{ cycle.cycle }}</strong></div><div class="cycle-table-wrap"><table class="cycle-matrix" :style="{ minWidth: `${Math.max(920, reportSamples.length * 170 + 190)}px` }"><thead><tr><th>指标字段</th><th v-for="(sample, sampleIndex) in reportSamples" :key="`${sample}-${sampleIndex}`">{{ sample }}</th></tr></thead><tbody><tr class="method-row"><td>测试方法</td><td v-for="(sample, sampleIndex) in reportSamples" :key="`${sample}-${sampleIndex}`"><el-input v-model="cycle.method" size="small" /></td></tr><tr v-for="(field, fieldIndex) in cycleFields" :key="field.key"><td>{{ field.label }}</td><td v-for="(sample, sampleIndex) in reportSamples" :key="`${field.key}-${sample}-${sampleIndex}`"><el-input v-if="field.mode === 'input'" v-model="cycleFieldValues[cycleValueKey(cycle.cycle, field.key, `${sample}-${sampleIndex}`)]" size="small" placeholder="请输入" /><span v-else-if="field.mode === 'raw'" class="raw-value">{{ rawValue(cycle.cycle, fieldIndex, sampleIndex) }}</span><span v-else class="auto-value">自动计算</span></td></tr></tbody></table></div></article></div></section>
    <section class="metrics-panel"><div v-if="!readonly" class="metric-switches"><label v-for="metric in formationMetricDefinitions" :key="metric.key"><span>{{ metric.title }}</span><el-switch v-model="formationMetricVisible[metric.key]" inline-prompt active-text="显" inactive-text="隐" /></label></div><div class="cycle-chart-tabs"><button v-for="cycle in cycles" :key="cycle.cycle" type="button" :class="{ active: activeCycle === cycle.cycle }" @click="activeCycle = cycle.cycle">循环{{ cycle.cycle }}</button></div><div class="chart-legend-bar"><strong>化成图例</strong><div v-for="(sample, index) in reportSamples.slice(0, 3)" :key="`${sample}-${index}`" class="chart-legend-item"><el-color-picker v-if="!readonly" class="legend-color-picker" :model-value="seriesColors[index]" :show-alpha="false" popper-class="legend-color-popper" :aria-label="`修改${sample}颜色`" @change="updateSeriesColor(index, $event)" /><span>{{ sample }}</span></div></div><div class="charts-grid"><article v-for="metric in visibleFormationMetrics" :key="metric.key" class="chart-card"><header><strong>{{ metric.title }}</strong><span>{{ metric.unit }}</span></header><VChart class="metric-chart" :option="chartOption(metric)" autoresize /></article></div></section>
    <section class="note-panel"><label><span>报表说明</span><el-input v-model="description" type="textarea" :rows="3" maxlength="300" show-word-limit /></label></section><footer class="form-actions"><button class="secondary-button" type="button" @click="exportPdf"><el-icon><Download /></el-icon>导出PDF</button><button class="secondary-button" type="button" @click="router.push('/report-formation')">{{ readonly ? '返回列表' : '取消' }}</button><button v-if="!readonly" class="primary-button" type="button" :disabled="saving" @click="saveReport"><el-icon><Check /></el-icon>{{ saving ? '保存中' : editingId ? '保存修改' : '保存报表' }}</button></footer>
    <el-dialog v-model="templateDialogVisible" class="formation-template-dialog" :title="templateMode === 'battery' ? '选择电池组字段模板' : '选择特殊字段模板'" :width="templateMode === 'battery' ? '720px' : '560px'" align-center>
      <div v-if="templateMode === 'battery'" class="battery-template-editor"><label class="battery-template-select"><span>选择模板</span><el-select v-model="selectedBatteryTemplateName" @change="switchBatteryTemplate"><el-option v-for="name in batteryTemplateNames" :key="name" :label="name" :value="name" /></el-select></label><el-checkbox-group v-if="batteryTemplateFieldOptions.length" v-model="selectedBatteryTemplateFieldIds" class="battery-template-fields" @change="applyBatteryTemplateFields"><el-checkbox v-for="field in batteryTemplateFieldOptions" :key="field.id" :value="field.id" :disabled="field.status === '禁用'" :title="`${field.name}(${field.type},${field.status})`">{{ field.name }}</el-checkbox></el-checkbox-group><div v-else class="template-empty">暂无字段,请先到电池组字段管理中添加字段</div></div>
      <el-radio-group v-else v-model="selectedTemplateKey" class="template-options"><el-radio v-for="item in fieldTemplates" :key="item.key" :value="item.key" border>{{ item.name }}({{ item.fields.join('、') }})</el-radio></el-radio-group>
      <template #footer><div v-if="templateMode === 'battery'" class="battery-template-actions"><button class="primary-button" type="button" @click="applyFieldTemplate"><el-icon><Check /></el-icon>确认</button><button class="primary-button" type="button" @click="saveBatteryTemplate"><el-icon><Check /></el-icon>保存</button><button class="secondary-button" type="button" @click="saveBatteryTemplateAs"><el-icon><DocumentCopy /></el-icon>另存模板</button></div><div v-else><button class="secondary-button" type="button" @click="templateDialogVisible = false">取消</button><button class="primary-button" type="button" @click="applyFieldTemplate">应用模板</button></div></template>
    </el-dialog>
  <div class="formation-form-page" :class="{ 'is-readonly': readonly }" v-loading="loading">
    <div class="page-tools"><button class="back-button" type="button" @click="router.push('/report-formation')"><el-icon><ArrowLeft /></el-icon>返回化成报表</button><span>{{ readonly ? 'VIEW FORMATION REPORT' : editingId ? 'EDIT FORMATION REPORT' : 'NEW FORMATION REPORT' }}</span></div>
    <el-alert v-if="error" :title="error" type="error" :closable="false" show-icon />
    <fieldset :disabled="busy || !!error" class="report-fieldset">
    <section class="report-name-panel"><label><span>报表名称</span><el-input v-model="reportName" :disabled="readonly" clearable maxlength="100" placeholder="请输入报表名称" @input="dirty=true" /></label><label><span>选择测试</span><el-select v-model="selectedTestIds" multiple filterable collapse-tags collapse-tags-tooltip :disabled="!!editingId || busy" placeholder="选择已完成的化成测试" @change="selectTests"><el-option v-for="test in testOptions" :key="test.id" :label="test.name" :value="test.id" /></el-select></label></section>
    <el-empty v-if="!matrix.items.length && !previewLoading" :description="editingId ? '该版本暂无来源数据' : '请选择化成测试'" />
    <div v-loading="previewLoading" v-if="matrix.items.length">
    <section class="report-data-panel"><header class="panel-heading"><div><strong>报表数据</strong><span>电池组字段</span></div></header>
      <div class="report-data-table-wrap"><table class="report-data-table" :style="{minWidth:`${Math.max(980,matrix.items.length*150+160)}px`}"><thead><tr><th>测试 / 电池组</th><th v-for="column in reportColumns" :key="column.id" :colspan="column.items.length">{{ column.name }} / {{ column.items[0]?.source.batteryGroup }}</th></tr><tr><th>电池号</th><th v-for="item in matrix.items" :key="formationSeriesKey(item)">{{ item.source.batteryNo }}</th></tr></thead><tbody>
        <tr v-for="field in detailFields" :key="field.key"><td>{{ field.displayName }}</td><td v-for="item in matrix.items" :key="formationSeriesKey(item)"><span v-if="field.kind==='calculated'" class="auto-value" :title="calculation(item,0,field).reason">{{ displayValue(item,0,field) }}</span><el-input v-else :model-value="inputValue(item,0,field)" :disabled="readonly" size="small" maxlength="500" placeholder="请输入" @update:model-value="updateValue(item,0,field,$event)" /></td></tr>
      </tbody></table></div>
      <div v-if="!readonly" class="report-data-actions"><button class="primary-button" type="button" @click="addReportField"><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="openFieldTemplate('special')">另存特殊字段模板</button></div>
    </section>
    <el-alert v-if="unknownSteps" :title="`以下工步方向未识别,未纳入充放电统计:${unknownSteps}`" type="warning" :closable="false" />
    <section class="cycle-panel"><header class="panel-heading"><div><strong>化成循环明细</strong><span>拐点两项暂为演示占位</span></div></header><el-empty v-if="!matrix.cycleNos.length" description="所选测试暂无已保存循环数据" /><div class="cycle-list"><article v-for="cycle in matrix.cycleNos" :key="cycle" class="cycle-block"><div class="cycle-block-heading"><strong>循环{{ cycle }}</strong></div><div class="cycle-table-wrap"><table class="cycle-matrix" :style="{minWidth:`${Math.max(920,matrix.items.length*170+190)}px`}"><thead><tr><th>指标字段</th><th v-for="item in matrix.items" :key="formationSeriesKey(item)">{{ item.source.name }}<br>{{ item.source.batteryNo }}</th></tr></thead><tbody>
      <tr v-for="field in methodFields" :key="field.key" class="method-row"><td>{{ field.displayName }}</td><td v-for="item in matrix.items" :key="formationSeriesKey(item)"><span v-if="!item.cycleNos.includes(cycle)">无该循环</span><el-input v-else :model-value="inputValue(item,cycle,field)" :disabled="readonly" size="small" maxlength="500" placeholder="请输入" @update:model-value="updateValue(item,cycle,field,$event)" /></td></tr>
      <tr v-for="index in chargeCount(cycle)" :key="`voltage-${index}`"><td>终止电压</td><td v-for="item in matrix.items" :key="formationSeriesKey(item)" :title="voltageTitle(item,cycle,index-1)"><span class="raw-value">{{ !item.cycleNos.includes(cycle)?'无该循环':formationNumberText(formationChargeSteps(item,cycle)[index-1]?.endVoltage) }}</span></td></tr>
      <tr v-for="field in cycleFields" :key="field.key"><td>{{ field.displayName }}{{ field.unit?` (${field.unit})`:'' }}</td><td v-for="item in matrix.items" :key="formationSeriesKey(item)"><span v-if="!item.cycleNos.includes(cycle)">无该循环</span><el-input v-else-if="field.kind==='manual'" :model-value="inputValue(item,cycle,field)" :type="field.key==='manual'?'number':'text'" :min="field.key==='manual'?0:undefined" step="any" :disabled="readonly" size="small" maxlength="500" placeholder="请输入" @update:model-value="updateValue(item,cycle,field,$event)" /><span v-else :class="field.kind==='calculated'?'auto-value':'raw-value'" :title="calculation(item,cycle,field).reason">{{ displayValue(item,cycle,field) }}</span></td></tr>
    </tbody></table></div></article></div></section>
    <section class="metrics-panel"><div class="metric-switches"><label v-for="metric in formationCurveMetrics" :key="metric.key"><span>{{ metric.title }}</span><el-switch :model-value="settings.metrics?.includes(metric.key)" inline-prompt active-text="显" inactive-text="隐" @update:model-value="setMetric(metric.key,!!$event)" /></label></div><div class="cycle-chart-tabs"><button v-for="cycle in matrix.cycleNos" :key="cycle" type="button" :class="{active:activeCycle===cycle}" @click="activeCycle=cycle">循环{{ cycle }}</button></div>
      <div class="chart-legend-bar"><strong>化成图例</strong><div v-for="(item,index) in matrix.items" :key="formationSeriesKey(item)" class="chart-legend-item"><el-color-picker v-if="!readonly" class="legend-color-picker" :model-value="formationSeriesColor(item,index,settings.seriesColors??{})" :show-alpha="false" :aria-label="`修改${item.source.name}-${item.source.batteryNo}颜色`" @change="setColor(item,$event)" /><button type="button" :class="{ 'hidden-legend':settings.hiddenSeriesKeys?.includes(formationSeriesKey(item)) }" @click="toggleSeries(item)">{{ item.source.name }}-{{ item.source.batteryNo }}</button></div></div>
      <template v-if="activeCycle!=null"><div v-for="group in formationStepGroups" :key="group.key" class="chart-zone"><h3>{{ group.title }}工步曲线</h3><div class="charts-grid"><article v-for="metric in visibleMetrics" :key="metric.key" class="chart-card"><header><strong>{{ metric.title }}</strong><span>{{ metric.unit }}</span></header><VChart class="metric-chart" :option="formationStepChartOption(metric,matrix.items,activeCycle,group.key,settings)" autoresize @legendselectchanged="legendChanged" /></article></div></div></template>
    </section>
    </div>
    <section class="note-panel"><label><span>报表说明</span><el-input v-model="description" :disabled="readonly" type="textarea" :rows="3" maxlength="2000" show-word-limit @input="dirty=true" /></label></section>
    </fieldset>
    <footer class="form-actions"><span v-if="saved">V{{ saved.versionNo }}</span><button v-if="saved&&can('query')" class="secondary-button" type="button" :disabled="busy" @click="openHistory">历史版本</button><button v-if="saved&&!readonly&&can('edit')" class="secondary-button" type="button" :disabled="busy||!!error" @click="refreshSource">按最新来源生成</button><button v-if="can('export')" class="secondary-button" type="button" :disabled="busy||!!error" @click="exportPdf"><el-icon><Download /></el-icon>导出PDF</button><button class="secondary-button" type="button" @click="router.push('/report-formation')">{{ readonly?'返回列表':'取消' }}</button><button v-if="!readonly&&can(editingId?'edit':'add')" class="primary-button" type="button" :disabled="busy||!!error||!matrix.items.length" @click="saveReport"><el-icon><Check /></el-icon>{{ saving?'保存中':editingId?'保存修改':'保存报表' }}</button></footer>
    <el-dialog v-model="templateDialogVisible" class="formation-template-dialog" :title="templateMode==='battery'?'选择电池组字段模板':'选择特殊字段模板'" width="720px" align-center><div v-loading="templateLoading" class="battery-template-editor"><label class="battery-template-select"><span>选择模板</span><el-select v-model="templateId" clearable placeholder="自定义" @change="selectTemplate"><el-option v-for="template in templates" :key="template.id" :label="template.name" :value="template.id" /></el-select></label><el-checkbox-group v-model="templateFieldKeys" class="battery-template-fields"><el-checkbox v-for="field in availableFields" :key="field.key" :value="field.key" :disabled="['Z','H','F','A','B'].includes(field.key)">{{ field.displayName }}</el-checkbox></el-checkbox-group></div><template #footer><div class="battery-template-actions"><button class="primary-button" type="button" :disabled="templateLoading" @click="applyFieldTemplate">确认</button><button class="primary-button" type="button" :disabled="templateLoading" @click="saveFieldTemplate()">保存</button><button class="secondary-button" type="button" :disabled="templateLoading" @click="saveFieldTemplate(true)">另存模板</button></div></template></el-dialog>
    <el-dialog v-model="historyVisible" title="历史版本" width="min(800px,calc(100vw - 28px))"><el-table :data="history"><el-table-column prop="versionNo" label="版本" width="80"/><el-table-column prop="operatorName" label="操作人"/><el-table-column label="时间"><template #default="{row}">{{ new Date(row.createdAt).toLocaleString() }}</template></el-table-column><el-table-column prop="changeSummary" label="修改说明"/><el-table-column label="操作" width="100"><template #default="{row}"><el-button link type="primary" @click="viewVersion(row.versionNo)">查看版本</el-button></template></el-table-column></el-table></el-dialog>
  </div>
</template>
@@ -280,4 +214,5 @@
@media(max-width:900px){.metrics-panel .charts-grid{grid-template-columns:minmax(0,1fr)}}@media(max-width:680px){.metric-switches{gap:14px;padding:0 12px}.metrics-panel>.chart-legend-bar{align-items:flex-start;flex-direction:column;gap:10px;padding:12px 12px}.cycle-chart-tabs{padding-left:12px}}
.formation-form-page.is-readonly :deep(.el-input), .formation-form-page.is-readonly :deep(.el-textarea) { pointer-events: none; }
.formation-form-page.is-readonly :deep(.el-input__wrapper), .formation-form-page.is-readonly :deep(.el-textarea__inner) { background: #f3f6f8; box-shadow: 0 0 0 1px #d8e1e9 inset; }
.report-fieldset{border:0;margin:0;padding:0;min-width:0}.report-name-panel{display:flex;flex-wrap:wrap;gap:24px}.report-name-panel label{flex:1;min-width:280px}.report-name-panel .el-select{width:100%}.chart-zone h3{padding:16px 24px 0;margin:0;color:#26333f;font-size:14px}.chart-legend-item button{border:0;background:transparent;color:#52606d;cursor:pointer}.hidden-legend{text-decoration:line-through;opacity:.5}.cycle-chart-tabs{flex-wrap:wrap;row-gap:8px}.form-actions{flex-wrap:wrap}.auto-value{font-variant-numeric:tabular-nums}@media(max-width:680px){.report-name-panel label{min-width:0;flex-basis:100%}.form-actions{position:static}.formation-form-page{padding-bottom:0}}
</style>