<script setup lang="ts">
|
import { computed, reactive, ref, watch } from 'vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
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 {
|
compareMetricDefinitions as metricDefinitions,
|
type CompareMetricDefinition,
|
} from '@/utils/compareReportPrint'
|
import type { CompareMetricKey } from '@/stores/compareReports'
|
import { useBatteryGroupFields } from '@/stores/batteryGroupFields'
|
import type { PersistedTestRecord, TestLogicStepMapping } from '@/api/tests'
|
|
interface ConsoleCell {
|
no: string
|
voltage: string
|
temperature: string
|
positive: string
|
negative: string
|
capacity: string
|
current: string
|
}
|
|
const props = withDefaults(defineProps<{
|
cells: ConsoleCell[]
|
durationMinutes?: number
|
testName?: string
|
batteryGroup?: string
|
records: PersistedTestRecord[]
|
logicStepMappings?: TestLogicStepMapping[]
|
}>(), {
|
durationMinutes: 5,
|
testName: '',
|
batteryGroup: '',
|
records: () => [],
|
logicStepMappings: () => [],
|
})
|
|
use([CanvasRenderer, LineChart, GridComponent, LegendComponent, TooltipComponent])
|
|
const palette = ['#3978f6', '#7d42d8', '#f08a3e', '#23a983', '#e15268', '#1b9ac8', '#b5892f', '#6957c8', '#4f8d44', '#d264a3', '#46748e', '#a95d37']
|
const seriesColors = ref<string[]>(props.cells.map((_, index) => palette[index % palette.length]))
|
const selectedStep = ref('')
|
const selectedCycle = ref('')
|
const selectedCellNos = ref<string[]>(props.cells.map(cell => cell.no))
|
const experimentName = ref(props.testName || 'XXX实验')
|
const batteryFields = ref(['额定容量', '电流', '内阻'])
|
const specialFields = ref(['温升'])
|
const detailFieldValues = reactive<Record<string, Record<string, string>>>({})
|
const templateDialogVisible = ref(false)
|
const templateMode = ref<'battery' | 'special'>('battery')
|
const selectedBatteryFields = ref<string[]>([])
|
const selectedSpecialTemplate = ref('循环寿命模板')
|
const { fields: managedGroupFields } = useBatteryGroupFields()
|
const metricVisible = reactive<Record<CompareMetricKey, boolean>>({
|
voltage: true,
|
current: true,
|
positiveVoltage: true,
|
negativeVoltage: true,
|
temperature: true,
|
})
|
|
const visibleMetrics = computed(() => metricDefinitions.filter(metric => metricVisible[metric.key]))
|
const detailCells = computed(() => props.cells)
|
const selectedCells = computed(() => props.cells.filter(cell => selectedCellNos.value.includes(cell.no)))
|
const detailFields = computed(() => [...batteryFields.value, ...specialFields.value])
|
const enabledBatteryFields = computed(() => managedGroupFields.value.filter(field => field.status === '启用'))
|
const batteryFieldOptions = computed(() => enabledBatteryFields.value.length
|
? enabledBatteryFields.value.map(field => field.name)
|
: ['额定容量', '电流', '内阻', '质量', '材料批次'])
|
const specialTemplates: Record<string, string[]> = {
|
循环寿命模板: ['容量保持率', '能量效率', '循环次数', '内阻变化'],
|
热特性模板: ['最高温度', '温差', '温升速率'],
|
}
|
const chartUpdateOptions = { notMerge: true, lazyUpdate: false }
|
|
interface StepOption {
|
value: string
|
label: string
|
cycleNo: number
|
stepNo?: number
|
logical?: TestLogicStepMapping
|
}
|
|
const cycleOptions = computed(() => [...new Set(props.records
|
.map(record => Number(record.cycleNo))
|
.filter(value => Number.isFinite(value) && value > 0))].sort((a, b) => a - b))
|
|
const stepOptions = computed<StepOption[]>(() => {
|
const actual = new Map<string, StepOption>()
|
const actualAcrossCycles = new Map<number, StepOption>()
|
for (const record of props.records) {
|
const cycleNo = Number(record.cycleNo)
|
const stepNo = Number(record.stepNo)
|
if (!Number.isFinite(cycleNo) || !Number.isFinite(stepNo) || cycleNo <= 0 || 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,
|
})
|
}
|
return [...actualAcrossCycles.values(), ...actual.values(), ...logicalAcrossCycles, ...logical]
|
})
|
|
function ensureDetailFieldValues() {
|
detailFields.value.forEach(field => {
|
if (!detailFieldValues[field]) detailFieldValues[field] = {}
|
detailCells.value.forEach(cell => {
|
if (detailFieldValues[field][cell.no] === undefined) detailFieldValues[field][cell.no] = ''
|
})
|
})
|
}
|
|
ensureDetailFieldValues()
|
|
watch(() => props.testName, value => {
|
if (value) experimentName.value = value
|
})
|
|
watch(() => props.cells, cells => {
|
const validCellNos = new Set(cells.map(cell => cell.no))
|
const retained = selectedCellNos.value.filter(no => validCellNos.has(no))
|
selectedCellNos.value = retained.length ? retained : cells.map(cell => cell.no)
|
seriesColors.value = detailCells.value.map((_, index) => seriesColors.value[index] || palette[index % palette.length])
|
ensureDetailFieldValues()
|
}, { deep: true })
|
|
watch(detailFields, ensureDetailFieldValues)
|
|
watch(stepOptions, options => {
|
if (!options.length) {
|
selectedStep.value = ''
|
return
|
}
|
if (!options.some(option => option.value === selectedStep.value)) selectedStep.value = options[0].value
|
}, { immediate: true })
|
|
watch(cycleOptions, options => {
|
const current = Number(selectedCycle.value)
|
if (selectedCycle.value && !options.includes(current)) selectedCycle.value = ''
|
})
|
|
async function addCustomField() {
|
try {
|
const result = await ElMessageBox.prompt('请输入要添加的字段名称', '添加项', {
|
confirmButtonText: '添加',
|
cancelButtonText: '取消',
|
inputPlaceholder: '例如:库仑效率',
|
inputValidator: value => value.trim() ? true : '请输入字段名称',
|
})
|
const field = result.value.trim()
|
if (detailFields.value.includes(field)) {
|
ElMessage.warning('该字段已经存在')
|
return
|
}
|
specialFields.value.push(field)
|
ensureDetailFieldValues()
|
} catch {
|
// 用户取消添加。
|
}
|
}
|
|
function removeCustomField(field: string) {
|
specialFields.value = specialFields.value.filter(item => item !== field)
|
delete detailFieldValues[field]
|
}
|
|
function openFieldTemplate(mode: 'battery' | 'special') {
|
templateMode.value = mode
|
selectedBatteryFields.value = [...batteryFields.value]
|
templateDialogVisible.value = true
|
}
|
|
function applyFieldTemplate() {
|
if (templateMode.value === 'battery') {
|
batteryFields.value = [...selectedBatteryFields.value]
|
specialFields.value = specialFields.value.filter(field => !batteryFields.value.includes(field))
|
} else {
|
specialFields.value = [...specialTemplates[selectedSpecialTemplate.value]].filter(field => !batteryFields.value.includes(field))
|
}
|
ensureDetailFieldValues()
|
templateDialogVisible.value = false
|
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 metricMatches(metric: CompareMetricKey, record: PersistedTestRecord) {
|
if (metric === 'voltage') return record.sourceType !== 'COLLECTOR' && record.voltage != null
|
if (metric === 'current') return record.sourceType !== 'COLLECTOR' && record.current != null
|
if (record.sourceType !== 'COLLECTOR') return false
|
const code = String(record.metricCode || '').toUpperCase()
|
if (metric === 'temperature') return code === 'TEMPERATURE' || code.includes('TEMPERATURE')
|
if (metric === 'positiveVoltage') return code === 'POSITIVE_POTENTIAL' || code.includes('POSITIVE_POTENTIAL')
|
if (metric === 'negativeVoltage') return code === 'NEGATIVE_POTENTIAL' || code.includes('NEGATIVE_POTENTIAL')
|
return false
|
}
|
|
function metricValue(metric: CompareMetricKey, record: PersistedTestRecord) {
|
if (metric === 'voltage') return Number(record.voltage)
|
if (metric === 'current') return Number(record.current)
|
return Number(record.metricValue)
|
}
|
|
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
|
return props.records.filter(record => {
|
if (cycle !== null && 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
|
})
|
}
|
|
const hasSelectedRecords = computed(() => selectedRecords().length > 0)
|
|
function recordTime(record: PersistedTestRecord) {
|
const value = record.deviceTime || record.recordedAt
|
const parsed = Date.parse(value || '')
|
return Number.isFinite(parsed) ? parsed : null
|
}
|
|
function chartOption(metric: CompareMetricDefinition) {
|
const source = selectedRecords()
|
const cellIndexes = selectedCells.value.map(cell => props.cells.findIndex(item => item.no === cell.no))
|
const seriesData = selectedCells.value.map(cell => {
|
return source.filter(record => record.batteryNo === cell.no && metricMatches(metric.key, record))
|
.map(record => ({ time: recordTime(record), value: metricValue(metric.key, record) }))
|
.filter(point => point.time !== null && Number.isFinite(point.value))
|
.sort((a, b) => (a.time as number) - (b.time as number))
|
})
|
const timestamps = [...new Set(seriesData.flat().map(point => point.time as number))].sort((a, b) => a - b)
|
const start = timestamps[0]
|
const values = seriesData.flat().map(point => point.value)
|
const dataMin = values.length ? Math.min(...values) : 0
|
const dataMax = values.length ? Math.max(...values) : 1
|
const margin = Math.max((dataMax - dataMin) * 0.16, metric.key === 'temperature' ? 1 : 0.08)
|
const createYAxis = (position: 'left' | 'right') => ({
|
type: 'value',
|
name: metric.unit,
|
position,
|
nameLocation: 'middle',
|
nameGap: 42,
|
nameRotate: position === 'left' ? 90 : -90,
|
min: Number((dataMin - margin).toFixed(2)),
|
max: Number((dataMax + margin).toFixed(2)),
|
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, margin: 8 },
|
})
|
|
return {
|
animation: false,
|
color: cellIndexes.map(index => seriesColors.value[index]),
|
grid: { top: 62, right: 62, bottom: 46, left: 62 },
|
tooltip: { trigger: 'axis' },
|
legend: {
|
top: 4,
|
right: 6,
|
type: 'scroll',
|
icon: 'roundRect',
|
itemWidth: 14,
|
itemHeight: 8,
|
itemGap: 12,
|
formatter: (name: string) => name.split('·').pop()?.trim() || name,
|
textStyle: { fontSize: 10, color: '#52606d' },
|
},
|
xAxis: {
|
type: 'category',
|
boundaryGap: false,
|
name: '设备时间',
|
nameLocation: 'middle',
|
nameGap: 29,
|
data: timestamps.map(value => start == null ? new Date(value).toISOString() : `${((value - start) / 3600000).toFixed(3)} h`),
|
axisLine: { lineStyle: { color: '#cbd8e4' } },
|
axisLabel: { color: '#8795a3', fontSize: 10, interval: 4 },
|
nameTextStyle: { color: '#52606d', fontSize: 10 },
|
},
|
yAxis: [createYAxis('left'), createYAxis('right')],
|
series: selectedCells.value.map((cell, index) => ({
|
name: cell.no,
|
type: 'line',
|
smooth: false,
|
showSymbol: false,
|
lineStyle: { width: 2 },
|
data: timestamps.map(timestamp => {
|
const point = seriesData[index].find(item => item.time === timestamp)
|
return point ? point.value : null
|
}),
|
})),
|
}
|
}
|
</script>
|
|
<template>
|
<div class="report-chart-page">
|
<section class="detail-panel">
|
<header class="panel-heading"><div><strong>测试详情</strong><el-icon><ArrowUp /></el-icon></div></header>
|
<div class="detail-meta"><label><span>实验</span><el-input v-model="experimentName" /></label></div>
|
<div class="field-table-wrap">
|
<table class="field-table" :style="{ minWidth: `${Math.max(900, detailCells.length * 220 + 214)}px` }">
|
<thead><tr><th>字段</th><th v-for="cell in detailCells" :key="cell.no">{{ cell.no }}</th><th>操作</th></tr></thead>
|
<tbody>
|
<tr class="detail-meta-row"><td>测试</td><td v-for="cell in detailCells" :key="`test-${cell.no}`">{{ testName }}</td><td>-</td></tr>
|
<tr class="detail-meta-row"><td>电池组</td><td v-for="cell in detailCells" :key="`group-${cell.no}`">{{ batteryGroup }}</td><td>-</td></tr>
|
<tr class="sample-row"><td>电池号</td><td v-for="cell in detailCells" :key="`sample-${cell.no}`">{{ cell.no }}</td><td>-</td></tr>
|
<tr v-for="field in detailFields" :key="field"><td>{{ field }}</td><td v-for="cell in detailCells" :key="`${field}-${cell.no}`"><el-input v-model="detailFieldValues[field][cell.no]" size="small" placeholder="请输入" /></td><td><button v-if="specialFields.includes(field)" class="delete-link" type="button" @click="removeCustomField(field)">删除</button><span v-else class="field-lock">-</span></td></tr>
|
<tr v-if="!detailFields.length"><td :colspan="detailCells.length + 2" class="empty-cell">暂无字段,请先选择电池组模板或添加特殊字段</td></tr>
|
</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>
|
</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>
|
</div>
|
</section>
|
|
<section class="metrics-panel">
|
<div class="metric-switches">
|
<label v-for="metric in metricDefinitions" :key="metric.key"><span>{{ metric.title }}</span><el-switch v-model="metricVisible[metric.key]" inline-prompt active-text="显" inactive-text="隐" /></label>
|
</div>
|
|
<div class="chart-legend-bar">
|
<strong>电池图例</strong>
|
<div v-for="cell in selectedCells" :key="cell.no" class="chart-legend-item">
|
<el-color-picker class="legend-color-picker" :model-value="seriesColors[cells.findIndex(item => item.no === cell.no)]" :show-alpha="false" popper-class="console-color-popper" :aria-label="`修改${cell.no}颜色`" @change="updateSeriesColor(cells.findIndex(item => item.no === cell.no), $event)" />
|
<span>{{ cell.no }}</span>
|
</div>
|
</div>
|
|
<div v-if="selectedCells.length && visibleMetrics.length && hasSelectedRecords" class="charts-grid">
|
<article v-for="metric in visibleMetrics" :key="metric.key" class="chart-card">
|
<header><strong>{{ metric.title }}</strong></header>
|
<VChart :key="`${metric.key}-${selectedStep}-${selectedCellNos.join('|')}`" class="metric-chart" :option="chartOption(metric)" :update-options="chartUpdateOptions" autoresize />
|
</article>
|
</div>
|
<div v-else class="chart-empty">暂无当前测试实时数据</div>
|
</section>
|
|
<el-dialog v-model="templateDialogVisible" :title="templateMode === 'battery' ? '选择电池组字段模板' : '选择特殊字段模板'" width="620px" align-center>
|
<el-checkbox-group v-if="templateMode === 'battery'" v-model="selectedBatteryFields" class="template-field-options"><el-checkbox v-for="field in batteryFieldOptions" :key="field" :value="field">{{ field }}</el-checkbox></el-checkbox-group>
|
<el-radio-group v-else v-model="selectedSpecialTemplate" class="template-field-options"><el-radio v-for="(fields, name) in specialTemplates" :key="name" :value="name" border>{{ name }}({{ fields.join('、') }})</el-radio></el-radio-group>
|
<template #footer><button class="secondary-button" type="button" @click="templateDialogVisible=false">取消</button><button class="primary-button" type="button" @click="applyFieldTemplate"><el-icon><Check /></el-icon>应用模板</button></template>
|
</el-dialog>
|
|
</div>
|
</template>
|
|
<style scoped>
|
.report-chart-page{min-width:0}.compare-info-panel,.detail-panel,.metrics-panel{min-width:0;border:1px solid #d8e1e9;background:#fff}.compare-info-panel,.detail-panel{margin-bottom:16px;padding-bottom:18px}.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;color:#26333f;font-size:13px}.panel-heading>div::before{content:'';width:3px;height:16px;background:#2b78a3}.panel-heading strong{color:#26333f;font-size:13px}.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;font-weight:500;cursor:pointer;white-space:nowrap}.primary-button{color:#fff;background:var(--blue);border-color:var(--blue)}.primary-button:hover{background:#176fcb;border-color:#176fcb}.secondary-button{color:#52606d;background:#fff;border-color:var(--line-strong)}.secondary-button:hover{color:var(--blue);border-color:#9dc8f5}.compare-table-wrap,.field-table-wrap{width:100%;max-width:100%;overflow-x:auto;padding:18px 24px 0}.compare-table,.field-table{width:100%;min-width:820px;table-layout:fixed;border-collapse:collapse;color:#52606d;font-size:13px}.compare-table th,.compare-table td,.field-table th,.field-table td{height:42px;padding:0 12px;border:1px solid #cbd8e4;text-align:center;white-space:nowrap}.compare-table th,.field-table th{color:#fff;background:#3c6497;font-size:12px;font-weight:600}.compare-table th:nth-child(1){width:22%}.compare-table th:nth-child(2){width:17%}.compare-table th:nth-child(3){width:18%}.compare-table th:nth-child(4){width:17%}.compare-table th:nth-child(5){width:14%}.compare-table th:last-child{width:12%}.compare-table tbody tr:hover,.field-table tbody tr:hover{background:#f7fbff}.compact-button{margin:16px 24px 0}.fixed-item{color:#8795a3;font-size:12px}.detail-meta{display:grid;grid-template-columns:minmax(280px,520px);gap:22px;padding:18px 24px 0}.detail-meta label{display:grid;grid-template-columns:48px minmax(0,1fr);align-items:center;gap:10px}.detail-meta label>span{color:#52606d;font-size:12px;font-weight:500}.detail-meta :deep(.el-input__wrapper),.field-table :deep(.el-input__wrapper),.compare-select-form :deep(.el-input__wrapper),.compare-select-form :deep(.el-select__wrapper){min-height:34px;border-radius:3px;box-shadow:0 0 0 1px #d8e1e9 inset;background:#f8fafc}.field-table{min-width:900px}.field-table th:first-child,.field-table td:first-child{width:140px;text-align:left}.field-table th:last-child,.field-table td:last-child{width:74px}.field-table .sample-row td,.field-table .detail-meta-row td{background:#fbfdff}.field-table .detail-meta-row td{color:#26333f}.field-table .detail-meta-row td:not(:first-child){font-weight:500}.field-table :deep(.el-input){width:100%}.delete-link{border:0;color:#d15b5b;background:transparent;font-size:12px;cursor:pointer}.delete-link:hover{text-decoration:underline}.field-lock{color:#8795a3}.empty-cell{height:72px!important;color:#8795a3}.detail-actions{display:flex;flex-wrap:wrap;gap:10px;padding:16px 24px 0}.metrics-panel{padding:0 24px 24px}.metric-switches{display:flex;flex-wrap:wrap;align-items:center;gap:20px 28px;min-height:58px;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}.chart-legend-bar{min-height:52px;display:flex;flex-wrap:wrap;align-items:center;gap:12px 24px;border-bottom:1px solid var(--line)}.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:260px;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(.console-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(.console-color-popper .el-color-picker-panel){border:0!important;box-shadow:none!important}.charts-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;padding-top:18px}.chart-card{min-width:0;border:1px solid #edf1f5;background:#fff}.chart-card>header{height:38px;display:flex;align-items:center;gap:18px;padding:0 14px;color:#52606d;font-size:11px}.chart-card>header strong{margin-right:auto;color:#26333f;font-size:13px}.metric-chart{width:100%;height:230px}.chart-empty{height:180px;display:flex;align-items:center;justify-content:center;color:#8795a3;font-size:12px}.compare-select-form{display:grid;gap:15px}.compare-select-form label{display:grid;grid-template-columns:84px minmax(0,1fr);align-items:center;gap:12px;color:#52606d;font-size:12px}.template-field-options{display:flex;flex-direction:column;gap:10px;max-height:280px;overflow:auto;padding:14px;border:1px solid #d8e1e9;background:#f8fafc}.template-field-options :deep(.el-checkbox),.template-field-options :deep(.el-radio){height:auto;min-height:34px;margin-right:0}.report-chart-page :deep(.el-dialog){max-width:calc(100vw - 28px);border-radius:4px}.report-chart-page :deep(.el-dialog__header){margin:0;padding:17px 20px;border-bottom:1px solid var(--line)}.report-chart-page :deep(.el-dialog__body){padding:20px}.report-chart-page :deep(.el-dialog__footer){display:flex;justify-content:flex-end;gap:8px;padding:14px 20px;border-top:1px solid var(--line)}@media(max-width:900px){.charts-grid{grid-template-columns:minmax(0,1fr)}}@media(max-width:680px){.panel-heading,.metric-switches,.chart-legend-bar{padding-left:12px;padding-right:12px}.compare-table-wrap,.detail-meta,.field-table-wrap,.detail-actions{padding-left:12px;padding-right:12px}.detail-meta{grid-template-columns:1fr}.metrics-panel{padding-left:12px;padding-right:12px}.chart-legend-bar{align-items:flex-start;flex-direction:column;gap:10px;padding-top:12px;padding-bottom:12px}.metric-switches{gap:14px}.charts-grid{grid-template-columns:1fr}.metric-chart{height:250px}.compare-select-form label{grid-template-columns:1fr}}
|
.data-select-panel{min-width:0;margin-bottom:16px;padding-bottom:18px;border:1px solid #d8e1e9;background:#fff}.data-select-content{display:grid;grid-template-columns:minmax(260px,360px) minmax(0,1fr);align-items:start;gap:28px;padding:18px 24px 0}.step-select,.battery-select{display:grid;gap:10px;color:#52606d;font-size:12px}.step-select{grid-template-columns:76px minmax(0,1fr);align-items:center}.battery-select{grid-template-columns:52px minmax(0,1fr);align-items:start}.battery-select>span{padding-top:7px}.step-select :deep(.el-select__wrapper){min-height:34px;border-radius:3px;box-shadow:0 0 0 1px #d8e1e9 inset;background:#f8fafc}.battery-select :deep(.el-checkbox-group){display:flex;flex-wrap:wrap;align-items:center;gap:8px 22px;min-height:34px}.battery-select :deep(.el-checkbox){margin-right:0}@media(max-width:900px){.data-select-content{grid-template-columns:1fr}}@media(max-width:680px){.data-select-content{gap:14px;padding:14px 12px 0}.step-select,.battery-select{grid-template-columns:1fr}.battery-select>span{padding-top:0}}
|
</style>
|