<script setup lang="ts">
|
import { computed, reactive, ref, watch } from 'vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { useRoute, useRouter } from 'vue-router'
|
import { use } from 'echarts/core'
|
import { LineChart } from 'echarts/charts'
|
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'
|
|
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 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>>({})
|
|
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 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() {
|
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”')
|
}
|
|
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')
|
}
|
</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>
|
</template>
|
|
<style scoped>
|
.formation-form-page{width:100%;padding-bottom:78px}.page-tools{height:34px;display:flex;align-items:center;justify-content:space-between}.page-tools>span{color:#7f8d99;font-size:10px;font-weight:600}.back-button{display:inline-flex;align-items:center;gap:6px;border:0;color:#52606d;background:transparent;font-size:13px;cursor:pointer}.primary-button,.secondary-button{height:34px;display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:0 13px;border:1px solid transparent;border-radius:3px;font-size:13px;cursor:pointer;white-space:nowrap}.primary-button{color:#fff;background:var(--blue);border-color:var(--blue)}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.report-name-panel,.setup-panel,.cycle-panel,.metrics-panel,.note-panel{background:#fff;border:1px solid #d8e1e9;margin-bottom:16px}.report-name-panel{padding:18px 22px}.report-name-panel label,.note-panel label{display:grid;grid-template-columns:72px minmax(260px,520px);align-items:center;gap:12px}.report-name-panel label>span,.note-panel label>span,.setup-grid label>span{color:#52606d;font-size:12px;font-weight:500}.report-name-panel :deep(.el-input__wrapper),.setup-grid :deep(.el-input__wrapper),.setup-grid :deep(.el-select__wrapper),.note-panel :deep(.el-textarea__inner),.cycle-table :deep(.el-input__wrapper){border-radius:3px;box-shadow:0 0 0 1px #d8e1e9 inset;background:#f8fafc}.panel-heading{min-height:48px;display:flex;align-items:center;justify-content:space-between;padding:0 24px;border-bottom:1px solid var(--line)}.panel-heading>div{display:flex;align-items:center;gap:9px}.panel-heading>strong,.panel-heading>div>strong{color:#26333f;font-size:13px}.panel-heading span{color:#8795a3;font-size:11px;margin-left:10px}.setup-grid{display:grid;grid-template-columns:repeat(2,minmax(240px,1fr));gap:18px;padding:20px 24px 12px}.setup-grid label{display:grid;grid-template-columns:72px minmax(0,1fr);align-items:center;gap:10px}.sample-toolbar{display:flex;align-items:center;justify-content:space-between;padding:4px 24px 10px;color:#52606d;font-size:12px}.sample-list{display:flex;flex-wrap:wrap;gap:10px;padding:0 24px 20px}.sample-chip{display:flex;align-items:center;gap:6px}.sample-chip :deep(.el-select){width:150px}.delete-link{border:0;color:#c45454;background:transparent;font-size:12px;cursor:pointer}.cycle-tabs{display:flex;gap:0;padding:16px 24px 0}.cycle-tabs button{min-width:84px;height:30px;color:#1677dd;background:#fff;border:1px solid #78b2f9;font-size:12px;cursor:pointer}.cycle-tabs button:first-child{border-radius:3px 0 0 3px}.cycle-tabs button:last-child{border-radius:0 3px 3px 0}.cycle-tabs button+button{border-left:0}.cycle-tabs button.active{color:#fff;background:#2188f5}.cycle-table-wrap{overflow-x:auto;padding:14px 24px 22px}.cycle-table{width:100%;min-width:900px;border-collapse:collapse;color:#52606d;font-size:12px}.cycle-table th,.cycle-table td{height:42px;padding:0 10px;border:1px solid #cbd8e4;text-align:center;white-space:nowrap}.cycle-table th{color:#fff;background:#3c6497;font-weight:600}.cycle-table th:nth-child(1){width:90px}.cycle-table th:nth-child(2){width:240px}.cycle-table th:nth-child(3){width:150px}.cycle-table th:nth-child(4),.cycle-table th:nth-child(5){width:130px}.charts-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;padding:18px 24px 24px}.chart-legend-bar{display:flex;align-items:center;gap:20px;padding:12px 24px;border-bottom:1px solid var(--line);color:#52606d;font-size:12px}.legend-item{display:inline-flex;align-items:center;gap:7px}.legend-color{width:14px;height:14px;border-radius:2px}.chart-card{border:1px solid #d8e1e9;background:#fff}.chart-card header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;color:#26333f;font-size:12px}.chart-card header span{color:#8795a3;font-size:10px}.metric-chart{width:100%;height:230px}.note-panel{padding:18px 24px}.note-panel :deep(.el-textarea){min-width:260px}.form-actions{position:sticky;bottom:0;z-index:4;display:flex;justify-content:flex-end;gap:10px;padding:14px 0;background:rgba(244,248,251,.96);border-top:1px solid #d8e1e9}@media(max-width:900px){.setup-grid,.charts-grid{grid-template-columns:1fr}.report-name-panel label,.note-panel label{grid-template-columns:1fr}.report-name-panel label>span,.note-panel label>span{margin-bottom:6px}.form-actions{position:static}}
|
.cycle-matrix{width:100%;border-collapse:collapse;color:#52606d;font-size:12px}.cycle-matrix th,.cycle-matrix td{height:40px;padding:0 10px;border:1px solid #cbd8e4;text-align:center;white-space:nowrap}.cycle-matrix th{color:#fff;background:#3c6497;font-weight:600}.cycle-matrix th:first-child{width:190px}.cycle-matrix th:last-child{width:90px}.cycle-matrix td:first-child{color:#31404d;background:#f2f7fb;font-weight:500;text-align:left}.cycle-matrix td:last-child{color:#8795a3;background:#fafcfe}.cycle-matrix :deep(.el-input__wrapper){border-radius:3px;box-shadow:0 0 0 1px #d8e1e9 inset;background:#f8fafc}.method-row td:first-child{border-left:3px solid #2188f5}.raw-value{color:#405261;font-family:'Cascadia Mono',Consolas,monospace}.auto-value{color:#2188f5;font-weight:500}.cycle-summary{display:flex;align-items:center;gap:28px;margin:0 24px 20px;padding:12px 14px;border:1px solid #d8e1e9;background:#f7fafc}.cycle-summary>div{display:flex;flex-direction:column;gap:3px}.cycle-summary span{color:#8795a3;font-size:10px}.cycle-summary strong{color:#33414e;font-size:12px}.cycle-summary>button{margin-left:auto}@media(max-width:900px){.cycle-summary{align-items:flex-start;flex-wrap:wrap}.cycle-summary>button{margin-left:0}}
|
.report-data-panel{background:#fff;border:1px solid #d8e1e9;margin-bottom:16px}.report-data-meta{display:flex;align-items:center;gap:12px}.report-data-meta label{display:flex;align-items:center;gap:6px;color:#52606d;font-size:11px;font-weight:500}.report-data-meta :deep(.el-select){width:190px}.report-data-meta :deep(.el-input){width:150px}.report-data-meta :deep(.el-input__wrapper),.report-data-meta :deep(.el-select__wrapper){min-height:30px;border-radius:3px;box-shadow:0 0 0 1px #d8e1e9 inset;background:#f8fafc}.report-data-table-wrap{overflow-x:auto;padding:18px 24px 0}.report-data-table{width:100%;border-collapse:collapse;color:#52606d;font-size:12px}.report-data-table th,.report-data-table td{height:38px;padding:0 10px;border:1px solid #cbd8e4;text-align:center;white-space:nowrap}.report-data-table th{color:#fff;background:#3c6497;font-weight:600}.report-data-table th:first-child,.report-data-table td:first-child{width:150px}.report-data-table td:first-child{color:#31404d;background:#f7fafc;font-weight:500}.report-data-table td:last-child,.report-data-table th:last-child{width:90px;color:#8795a3;background:#fafcfe}.report-data-table :deep(.el-select){width:100px}.report-data-table :deep(.el-select__wrapper),.report-data-table :deep(.el-input__wrapper){min-height:28px;border-radius:3px;box-shadow:0 0 0 1px #d8e1e9 inset;background:#fff}.group-index{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;margin-right:8px;border:1px solid #cbd8e4;border-radius:50%;color:#647380;background:#fff;font-size:10px}.report-data-actions{display:flex;align-items:center;gap:10px;padding:18px 24px 20px}.report-data-actions .primary-button{height:30px;padding:0 12px;font-size:12px}@media(max-width:900px){.report-data-meta{align-items:flex-start;flex-direction:column;gap:6px}.report-data-actions{flex-wrap:wrap}}
|
.fixed-meta-value,.fixed-sample{color:#405261;font-weight:500}.fixed-meta-value{min-width:110px;padding:6px 10px;border:1px solid #d8e1e9;background:#f8fafc}.fixed-sample{display:inline-block;min-width:72px;padding:5px 8px;border:1px solid #d8e1e9;border-radius:3px;background:#f8fafc}
|
.formation-form-page :deep(.formation-template-dialog){max-width:calc(100vw - 28px);border-radius:4px}.formation-form-page :deep(.formation-template-dialog .el-dialog__header){margin:0;padding:17px 20px;border-bottom:1px solid var(--line)}.formation-form-page :deep(.formation-template-dialog .el-dialog__body){padding:20px}.formation-form-page :deep(.formation-template-dialog .el-dialog__footer){padding:14px 20px;border-top:1px solid var(--line)}.battery-template-select{display:flex;align-items:center;gap:12px;margin-bottom:18px;color:#52606d;font-size:12px}.battery-template-select :deep(.el-select){width:260px}.battery-template-fields{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px 18px;max-height:260px;overflow:auto;padding:14px;border:1px solid #d8e1e9;background:#f8fafc}.template-options{display:flex;flex-direction:column;gap:10px}.template-options :deep(.el-radio){height:auto;min-height:42px;margin-right:0;white-space:normal}.battery-template-actions{display:flex;justify-content:flex-end;gap:8px}.template-empty{padding:30px 0;color:#8795a3;text-align:center}@media(max-width:680px){.battery-template-fields{grid-template-columns:repeat(2,minmax(0,1fr))}.battery-template-actions{flex-wrap:wrap}}
|
.report-data-table th:last-child{width:auto;color:#fff;background:#3c6497}.report-data-table td:last-child{width:auto;color:#52606d;background:#fff}
|
.cycle-list{display:flex;flex-direction:column}.cycle-block{padding-top:18px;border-bottom:8px solid #eef3f7}.cycle-block:last-child{border-bottom:0}.cycle-block-heading{display:flex;align-items:center;gap:12px;margin:0 24px;padding:9px 12px;border-left:3px solid #2188f5;background:#f2f7fb}.cycle-block-heading strong{color:#26333f;font-size:13px}.cycle-block-heading span{color:#8795a3;font-size:11px}.readonly-cycle-value{color:#405261;font-size:12px}.cycle-block .cycle-table-wrap{padding-top:12px}.cycle-block .cycle-summary{margin-bottom:18px}
|
.cycle-matrix th:last-child{width:auto;color:#fff;background:#3c6497}.cycle-matrix td:last-child{width:auto;color:#52606d;background:#fff}
|
.metric-switches{display:flex;flex-wrap:wrap;align-items:center;gap:20px 28px;min-height:58px;padding:0 24px;border-bottom:1px solid var(--line)}.metric-switches label{display:inline-flex;align-items:center;gap:7px;color:#52606d;font-size:12px}.metric-switches :deep(.el-switch){--el-switch-on-color:#3978f6}.cycle-chart-tabs{display:flex;gap:0;padding:16px 24px 0}.cycle-chart-tabs button{min-width:84px;height:30px;color:#1677dd;background:#fff;border:1px solid #78b2f9;font-size:12px;cursor:pointer}.cycle-chart-tabs button:first-child{border-radius:3px 0 0 3px}.cycle-chart-tabs button:last-child{border-radius:0 3px 3px 0}.cycle-chart-tabs button+button{border-left:0}.cycle-chart-tabs button.active{color:#fff;background:#2188f5}.metrics-panel>.chart-legend-bar{min-height:52px;display:flex;flex-wrap:wrap;align-items:center;gap:12px 24px;padding:0 24px;border-bottom:1px solid var(--line)}.metrics-panel>.chart-legend-bar>strong{color:#26333f;font-size:12px;font-weight:600}.chart-legend-item{display:inline-flex;align-items:center;gap:7px;min-width:0;color:#52606d;font-size:12px}.chart-legend-item span{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.legend-color-picker{width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center}.legend-color-picker :deep(.el-color-picker__trigger){width:14px!important;height:14px!important;padding:0!important;border:0!important;border-radius:2px;background:transparent!important;box-shadow:none!important;overflow:hidden}.legend-color-picker :deep(.el-color-picker__color){width:14px;height:14px;border:0;border-radius:2px;box-shadow:0 0 0 1px rgba(36,54,71,.18)}.legend-color-picker :deep(.el-color-picker__color-inner){border-radius:2px}.legend-color-picker :deep(.el-color-picker__icon),.legend-color-picker :deep(.el-color-picker__empty),.legend-color-picker :deep(.el-color-picker__trigger svg),.legend-color-picker :deep(.el-color-picker__trigger .el-icon){display:none!important}.legend-color-picker :deep(.el-color-picker__trigger:hover){box-shadow:0 0 0 2px rgba(43,120,163,.24)!important}:global(.legend-color-popper.el-popper){padding:0!important;border:1px solid #b9c9d8!important;border-radius:4px!important;box-shadow:0 10px 24px rgba(25,48,69,.18)!important;overflow:hidden}:global(.legend-color-popper .el-color-picker-panel){border:0!important;box-shadow:none!important}.metrics-panel .charts-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;padding:18px 24px 24px}.metrics-panel .chart-card{min-width:0;border:1px solid #edf1f5;background:#fff}.metrics-panel .chart-card header{height:38px;display:flex;align-items:center;gap:18px;padding:0 14px;color:#52606d;font-size:11px}.metrics-panel .chart-card header strong{margin-right:auto;color:#26333f;font-size:13px}.metrics-panel .metric-chart{height:210px}
|
@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; }
|
</style>
|