<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 CompareReportHistoryDialog from '@/components/CompareReportHistoryDialog.vue'
|
import { getUserInfo } from '@/api/auth'
|
import type { CompareMetricKey } from '@/stores/compareReports'
|
import { compareSelectionKey, createCompareReport, editCompareReport, getCompareReport, getCompareSources, previewCompareReport, refreshCompareSource,
|
type CompareCandidate, type CompareItem, type SavedCompareReport } from '@/api/compareReports'
|
import { compareMetricDefinitions as metricDefinitions, openCompareReportPrint } from '@/utils/compareReportPrint'
|
import { compareChartOption, compareSeriesName } from '@/utils/compareReportChart'
|
import { measurementRecordTime } from '@/utils/reportMeasurementChart'
|
import { compareDetailSources } from '@/utils/compareSourceDetails'
|
import ReportTestDetail from '@/components/ReportTestDetail.vue'
|
import { reportDetailColumn, snapshotReportDetails } from '@/utils/reportTestDetails'
|
import type { CompareSettings } from '@/api/compareReports'
|
|
use([CanvasRenderer, LineChart, GridComponent, LegendComponent, TooltipComponent])
|
|
const route = useRoute()
|
const router = useRouter()
|
const editingId = computed(() => {
|
const value = Number(route.params.id)
|
return Number.isFinite(value) && value > 0 ? value : null
|
})
|
const editingRecord = ref<SavedCompareReport | null>(null)
|
const readonly = computed(() => Boolean(editingId.value) && (route.query.mode !== 'edit' || !canEdit.value || Boolean(loadError.value)))
|
const canCreate = ref(false)
|
const canEdit = ref(false)
|
const canQuery = ref(false)
|
const canExport = ref(false)
|
const printing = ref(false)
|
const savedFormContent = ref('')
|
const historyVisible = ref(false)
|
const changeSummary = ref('')
|
const loadError = ref('')
|
const previewLoading = ref(false)
|
let previewRequest = 0
|
const previewError = ref('')
|
const reportLoading = ref(false)
|
const saving = ref(false)
|
const reportName = ref('')
|
const sourceDetailOverrides = ref<CompareSettings['sourceDetailOverrides']>({})
|
const description = ref('')
|
const batteryGroup = ref('')
|
type SelectedTest = CompareCandidate & { id: number; cycle: number }
|
const selectedTests = ref<SelectedTest[]>([])
|
const candidates = ref<CompareCandidate[]>([])
|
const candidateTests = computed(() => [...new Map(candidates.value.map(item => [item.selection.testId, toSelectedTest(item)])).values()])
|
const sourceItems = ref<CompareItem[]>([])
|
const candidatesLoading = ref(false)
|
const candidatesError = ref('')
|
const testDialogVisible = ref(false)
|
const selectedCandidateId = ref<number | null>(null)
|
const selectedBatteryNo = ref('')
|
const selectedCycle = ref<number | null>(null)
|
const selectedProcess = ref('')
|
|
const metricVisible = reactive<Record<CompareMetricKey, boolean>>({
|
voltage: true,
|
current: true,
|
positiveVoltage: true,
|
negativeVoltage: true,
|
temperature: true,
|
})
|
const seriesColors = ref(['#3978f6', '#7d42d8', '#f08a3e'])
|
const hiddenSeriesKeys = ref<string[]>([])
|
|
const visibleMetrics = computed(() => metricDefinitions.filter(metric => metricVisible[metric.key]))
|
const selectedCandidate = computed(() => candidateTests.value.find(item => item.id === selectedCandidateId.value) || null)
|
const sourceTests = computed(() => compareDetailSources(sourceItems.value).map(toSelectedTest))
|
const reportDetailColumns = computed(() => sourceTests.value.map(reportDetailColumn))
|
const chartSeries = computed(() => selectedTests.value)
|
const testCandidates = computed(() => candidates.value.filter(item => item.selection.testId === selectedCandidateId.value))
|
const batteryChoices = computed(() => [...new Set(testCandidates.value.map(item => item.batteryNo))])
|
const cycleChoices = computed(() => [...new Set(testCandidates.value.filter(item => item.batteryNo === selectedBatteryNo.value).map(item => item.selection.cycleNo))].sort((a, b) => a - b))
|
const processChoices = computed(() => testCandidates.value.filter(item => item.batteryNo === selectedBatteryNo.value && item.selection.cycleNo === selectedCycle.value).map(item => item.process))
|
|
function toSelectedTest(item: CompareCandidate): SelectedTest {
|
return { ...item, id: item.selection.testId, cycle: item.selection.cycleNo }
|
}
|
|
async function loadCandidates() {
|
candidatesLoading.value = true
|
candidatesError.value = ''
|
try {
|
candidates.value = await getCompareSources()
|
} catch (error) {
|
candidatesError.value = error instanceof Error ? error.message : '真实测试来源加载失败'
|
} finally {
|
candidatesLoading.value = false
|
}
|
}
|
|
function loadEditingReport() {
|
const record = editingRecord.value
|
if (!record) return
|
|
reportName.value = record.name
|
sourceDetailOverrides.value = record.settings.sourceDetailOverrides === undefined ? undefined : JSON.parse(JSON.stringify(record.settings.sourceDetailOverrides))
|
description.value = record.description || ''
|
sourceItems.value = record.items
|
selectedTests.value = record.items.map(item => toSelectedTest(item.source))
|
metricDefinitions.forEach(metric => { metricVisible[metric.key] = record.settings.metrics.includes(metric.key) })
|
seriesColors.value = [...record.settings.seriesColors]
|
hiddenSeriesKeys.value = [...(record.settings.hiddenSeriesKeys || [])]
|
savedFormContent.value = JSON.stringify(formContent())
|
}
|
|
function updateSeriesColor(index: number, color: string | null) {
|
if (color) seriesColors.value[index] = color
|
}
|
|
function toggleSeries(item: SelectedTest) {
|
if (readonly.value) return
|
// 详情按来源合列,曲线显隐仍按完整循环和工步范围独立保存。
|
const key = compareSelectionKey(item.selection)
|
hiddenSeriesKeys.value = hiddenSeriesKeys.value.includes(key)
|
? hiddenSeriesKeys.value.filter(value => value !== key) : [...hiddenSeriesKeys.value, key]
|
}
|
|
// 搜索跳转和浏览器后退会复用详情组件,旧请求的结果不能覆盖新报表。
|
watch(() => [editingId.value, route.query.mode, route.query.version] as const, async ([id, mode, version], _previous, onCleanup) => {
|
let cancelled = false
|
onCleanup(() => { cancelled = true })
|
editingRecord.value = null
|
selectedTests.value = []
|
sourceItems.value = []
|
reportName.value = ''
|
description.value = ''
|
sourceDetailOverrides.value = {}
|
seriesColors.value = ['#3978f6', '#7d42d8', '#f08a3e']
|
hiddenSeriesKeys.value = []
|
metricDefinitions.forEach(metric => { metricVisible[metric.key] = true })
|
loadError.value = ''
|
previewError.value = ''
|
canCreate.value = false
|
canEdit.value = false
|
canQuery.value = false
|
canExport.value = false
|
savedFormContent.value = ''
|
historyVisible.value = false
|
changeSummary.value = ''
|
reportLoading.value = true
|
try {
|
const info = await getUserInfo()
|
if (cancelled) return
|
canEdit.value = Boolean(info.permissions?.some(item => item === '*:*:*' || item === 'system:compare-report:edit'))
|
canQuery.value = Boolean(info.permissions?.some(item => item === '*:*:*' || item === 'system:compare-report:query'))
|
canExport.value = Boolean(info.permissions?.some(item => item === '*:*:*' || item === 'system:compare-report:export'))
|
canCreate.value = Boolean(info.permissions?.some(item => item === '*:*:*' || item === 'system:compare-report:add'))
|
if (id) {
|
if (mode === 'edit' && (!canEdit.value || version !== undefined)) throw new Error('历史版本只读或无编辑权限')
|
// 编辑权限包含读取最新编辑基准,取消或保存后仍可预览;历史查看单独要求查看权限。
|
const editingAccess = mode === 'edit' || (!canQuery.value && canEdit.value && version === undefined)
|
const record = await getCompareReport(id, version === undefined ? undefined : Number(version), editingAccess)
|
if (cancelled) return
|
editingRecord.value = record
|
loadEditingReport()
|
} else {
|
canCreate.value = Boolean(info.permissions?.some(item => item === '*:*:*' || item === 'system:compare-report:add'))
|
if (!canCreate.value) throw new Error('无新增对比报表权限')
|
await loadCandidates()
|
}
|
} catch (error) { if (!cancelled) loadError.value = error instanceof Error ? error.message : '报表加载失败' }
|
finally { if (!cancelled) reportLoading.value = false }
|
}, { immediate: true })
|
|
async function refreshPreview() {
|
const current = ++previewRequest
|
const selections = selectedTests.value.map(item => item.selection)
|
previewLoading.value = true
|
previewError.value = ''
|
sourceItems.value = []
|
try {
|
const items = await previewCompareReport(selections)
|
if (editingId.value || current !== previewRequest) return
|
sourceItems.value = items
|
} catch (error) { if (current === previewRequest) previewError.value = error instanceof Error ? error.message : '曲线预览失败' }
|
finally { if (current === previewRequest) previewLoading.value = false }
|
}
|
|
function openAddTest() {
|
void loadCandidates()
|
selectedCandidateId.value = null
|
selectedBatteryNo.value = ''
|
selectedCycle.value = null
|
selectedProcess.value = ''
|
testDialogVisible.value = true
|
}
|
|
function syncDialogDefaults() {
|
if (!selectedCandidate.value) return
|
selectedBatteryNo.value = selectedCandidate.value.batteryNo
|
selectedCycle.value = selectedCandidate.value.cycle
|
selectedProcess.value = selectedCandidate.value.process
|
}
|
|
async function addSelectedTest() {
|
if (previewLoading.value || saving.value) return
|
const candidate = testCandidates.value.find(item => item.batteryNo === selectedBatteryNo.value && item.selection.cycleNo === selectedCycle.value && item.process === selectedProcess.value)
|
if (!candidate) {
|
ElMessage.warning('请选择要加入对比的测试')
|
return
|
}
|
if (!selectedBatteryNo.value || !selectedCycle.value || !selectedProcess.value) {
|
ElMessage.warning('请完整选择电池、循环和工步')
|
return
|
}
|
if (selectedTests.value.some(item => compareSelectionKey(item.selection) === compareSelectionKey(candidate.selection))) {
|
ElMessage.warning('相同测试、电池和工步循环已加入对比信息')
|
return
|
}
|
selectedTests.value.push(toSelectedTest(candidate))
|
if (!seriesColors.value[selectedTests.value.length - 1]) seriesColors.value.push('#109854')
|
testDialogVisible.value = false
|
await refreshPreview()
|
}
|
|
async function removeTest(itemToRemove: SelectedTest) {
|
if (previewLoading.value || saving.value) return
|
if (selectedTests.value.length === 1) {
|
ElMessage.warning('至少保留一条对比测试')
|
return
|
}
|
const index = selectedTests.value.indexOf(itemToRemove)
|
hiddenSeriesKeys.value = hiddenSeriesKeys.value.filter(key => key !== compareSelectionKey(itemToRemove.selection))
|
if (index >= 0) { selectedTests.value.splice(index, 1); seriesColors.value.splice(index, 1) }
|
await refreshPreview()
|
}
|
|
function formContent() {
|
return {
|
name: reportName.value, description: description.value, experimentName: '',
|
settings: {
|
metrics: visibleMetrics.value.map(item => item.key), seriesColors: seriesColors.value.slice(0, selectedTests.value.length),
|
hiddenSeriesKeys: [...hiddenSeriesKeys.value],
|
batteryTemplateFields: [], specialFields: [], fieldStructure: [], detailFieldValues: {},
|
sourceDetailOverrides: snapshotReportDetails(reportDetailColumns.value, sourceDetailOverrides.value),
|
},
|
}
|
}
|
|
async function printReport() {
|
if (printing.value || saving.value || reportLoading.value || loadError.value || !canExport.value) return
|
const record = editingRecord.value
|
if (!record) { ElMessage.warning('请先保存报表后再打印'); return }
|
if (JSON.stringify(formContent()) !== savedFormContent.value || changeSummary.value.trim()) {
|
ElMessage.warning('有未保存修改,请先保存新版本后再打印')
|
return
|
}
|
printing.value = true
|
try {
|
await openCompareReportPrint(record.id, record.versionNo)
|
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '打印失败,请重试') }
|
finally { printing.value = false }
|
}
|
|
async function saveReport() {
|
if (saving.value || reportLoading.value || loadError.value || readonly.value || !(editingId.value ? canEdit.value : canCreate.value)) return
|
if (!reportName.value.trim()) {
|
ElMessage.warning('请输入报告名称')
|
return
|
}
|
if (selectedTests.value.length < 2) {
|
ElMessage.warning('请至少添加两条对比测试')
|
return
|
}
|
if (editingId.value && !changeSummary.value.trim()) {
|
ElMessage.warning('请输入修改说明')
|
return
|
}
|
saving.value = true
|
try {
|
const content = { ...formContent(), name: reportName.value.trim() }
|
if (editingId.value && editingRecord.value) {
|
await editCompareReport(editingId.value, { ...content, baseVersionNo: editingRecord.value.versionNo, changeSummary: changeSummary.value.trim() })
|
} else {
|
await createCompareReport({ ...content, selections: selectedTests.value.map(item => item.selection) })
|
}
|
ElMessage.success('对比报表已保存')
|
await router.push(editingId.value ? { path: `/report-compare/${editingId.value}/edit`, query: { mode: 'view' } } : '/report-compare')
|
} catch (error) {
|
ElMessage.error(error instanceof Error ? error.message : '保存失败,请重试')
|
} finally {
|
saving.value = false
|
}
|
}
|
|
async function generateFromLatestSource() {
|
if (saving.value || !editingRecord.value || !editingId.value) return
|
// 确认框与更新请求都绑定当前版本,导航或重载后不再用旧结果替换页面。
|
const baseRecord = editingRecord.value
|
let reason: string
|
try {
|
const result = await ElMessageBox.prompt('将重新读取当前来源并生成新版本,保留已有人工表格值。', '按最新来源生成版本', {
|
inputPlaceholder: '请输入更新来源说明', inputValidator: value => value?.trim() && value.trim().length <= 480 ? true : '请填写不超过480字的说明',
|
confirmButtonText: '生成新版本', cancelButtonText: '取消',
|
})
|
reason = result.value.trim()
|
} catch { return }
|
if (editingRecord.value !== baseRecord) return
|
saving.value = true
|
try {
|
const record = await refreshCompareSource(baseRecord.id, baseRecord.versionNo, reason)
|
if (editingRecord.value !== baseRecord) return
|
editingRecord.value = record
|
loadEditingReport()
|
ElMessage.success(`已生成版本 ${record.versionNo}`)
|
} catch (error) {
|
ElMessage.error(error instanceof Error ? error.message : '更新来源失败')
|
} finally { saving.value = false }
|
}
|
</script>
|
|
<template>
|
<div class="compare-form-page" :class="{ 'is-readonly': readonly }">
|
<el-alert v-if="loadError" :title="loadError" type="error" :closable="false" show-icon />
|
<div v-if="reportLoading">正在加载报表</div>
|
<el-alert v-if="previewError" :title="previewError" type="error" :closable="false" show-icon />
|
<div class="page-tools"><button class="back-button" type="button" @click="router.push('/report-compare')"><el-icon><ArrowLeft /></el-icon>返回对比报表</button><span>{{ readonly ? 'VIEW COMPARISON REPORT' : editingId ? 'EDIT COMPARISON REPORT' : 'NEW COMPARISON REPORT' }}</span></div>
|
|
<section class="report-name-panel">
|
<label><span>报表名称</span><el-input v-model="reportName" :readonly="readonly" maxlength="100" clearable placeholder="请输入报表名称" /></label>
|
<label><span>说明</span><el-input v-model="description" :readonly="readonly" maxlength="2000" type="textarea" placeholder="请输入说明" /></label>
|
<label v-if="editingId && !readonly"><span>修改说明</span><el-input v-model="changeSummary" maxlength="500" placeholder="请输入修改说明" /></label>
|
<span v-if="editingRecord">版本 {{ editingRecord.versionNo }} · {{ editingRecord.operatorName }} · {{ new Date(editingRecord.versionCreatedAt).toLocaleString() }} · {{ editingRecord.changeSummary }}</span>
|
</section>
|
|
<section class="compare-info-panel">
|
<header class="panel-heading"><div><strong>对比信息</strong><el-icon><ArrowUp /></el-icon></div></header>
|
<div class="compare-table-wrap">
|
<table class="compare-table">
|
<thead><tr><th>测试名称</th><th>测试类型</th><th>电池号</th><th>工步</th><th>循环</th><th v-if="!editingId">操作</th></tr></thead>
|
<tbody>
|
<tr v-for="(item, index) in selectedTests" :key="`${item.id}-${item.batteryGroup || batteryGroup}-${item.batteryNo}-${index}`"><td>{{ item.name }}</td><td>{{ item.type }}</td><td>{{ item.batteryNo }}</td><td>{{ item.process }}</td><td>{{ item.cycle }}</td><td v-if="!editingId"><button class="delete-link" type="button" @click="removeTest(item)">删除</button></td></tr>
|
<tr v-if="!selectedTests.length"><td :colspan="readonly ? 5 : 6" class="empty-cell">暂无对比测试</td></tr>
|
</tbody>
|
</table>
|
</div>
|
<button v-if="!editingId && canCreate" class="primary-button compact-button" type="button" :disabled="previewLoading || saving" @click="openAddTest"><el-icon><Plus /></el-icon>添加比较</button>
|
</section>
|
|
<ReportTestDetail v-model="sourceDetailOverrides" :columns="reportDetailColumns" :readonly="readonly" :disabled="saving || previewLoading || !!previewError" />
|
|
<section class="metrics-panel">
|
<div v-if="!readonly" 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 v-if="chartSeries.length" class="chart-legend-bar"><strong>对比图例</strong><div v-for="(item, index) in chartSeries" :key="compareSelectionKey(item.selection)" 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="`修改${item.name}颜色`" @change="updateSeriesColor(index, $event)" /><i v-else class="series-swatch" :style="{ background: seriesColors[index] }" /><button type="button" class="legend-name" :class="{ 'is-hidden': hiddenSeriesKeys.includes(compareSelectionKey(item.selection)) }" :aria-pressed="!hiddenSeriesKeys.includes(compareSelectionKey(item.selection))" :disabled="readonly" @click="toggleSeries(item)">{{ item.name }}-{{ item.batteryNo }}</button></div></div>
|
<!-- 报表切换和比对项移除时完整替换配置,避免图表库将颜色数组缩短误判为组件合并。 -->
|
<div v-if="!visibleMetrics.length" class="chart-empty">当前未启用曲线指标</div>
|
<div class="charts-grid"><article v-for="metric in visibleMetrics" :key="metric.key" class="chart-card"><header><strong>{{ metric.title }}</strong></header><VChart class="metric-chart" :option="compareChartOption(metric, sourceItems, seriesColors, hiddenSeriesKeys)" :update-options="{ notMerge: true }" autoresize @legendselectchanged="!readonly && (hiddenSeriesKeys = sourceItems.filter(item => !$event.selected[compareSeriesName(item)]).map(item => compareSelectionKey(item.source.selection)))" /><div v-if="!sourceItems.some(item => !hiddenSeriesKeys.includes(compareSelectionKey(item.source.selection)) && item.records.some(row => measurementRecordTime(metric.key, row) != null && (metric.key === 'positiveVoltage' ? row.positivePotential : metric.key === 'negativeVoltage' ? row.negativePotential : row[metric.key]) != null))" class="chart-empty">{{ sourceItems.length && hiddenSeriesKeys.length === sourceItems.length ? '当前全部曲线已隐藏' : '所选范围暂无该指标数据' }}</div></article></div>
|
</section>
|
|
<footer class="form-actions">
|
<button v-if="canExport" class="secondary-button" type="button" :disabled="printing || saving || reportLoading || !!loadError" @click="printReport"><el-icon><Printer /></el-icon>{{ printing ? '正在打开打印' : '打印 / 另存 PDF' }}</button>
|
<button v-if="readonly && canQuery && editingRecord" class="secondary-button" type="button" :disabled="reportLoading || saving" @click="historyVisible = true"><el-icon><Clock /></el-icon>历史版本</button>
|
<button v-if="readonly && editingRecord && route.query.version !== undefined" class="secondary-button" type="button" @click="router.push({ query: { mode: 'view' } })">查看最新版本</button>
|
<button v-if="readonly && canEdit && editingRecord && route.query.version === undefined" class="secondary-button" type="button" :disabled="reportLoading || saving || !!loadError" @click="generateFromLatestSource"><el-icon><Refresh /></el-icon>按最新来源生成版本</button>
|
<button v-if="readonly && canEdit && editingRecord && route.query.version === undefined" class="secondary-button" type="button" :disabled="reportLoading || saving || !!loadError" @click="router.push({ query: { mode: 'edit' } })"><el-icon><Edit /></el-icon>编辑报表</button>
|
<button v-if="readonly" class="primary-button" type="button" @click="router.push('/report-compare')">返回列表</button>
|
<button v-else-if="editingId" class="secondary-button" type="button" :disabled="saving" @click="router.push({ query: { mode: 'view' } })">取消编辑</button>
|
<button v-if="!readonly && (canCreate || canEdit)" class="primary-button save-button" type="button" :disabled="saving || reportLoading || !!loadError || previewLoading || !!previewError" @click="saveReport"><el-icon><Check /></el-icon>{{ saving ? '保存中' : editingId ? '保存新版本' : '保存报表' }}</button>
|
</footer>
|
|
<CompareReportHistoryDialog v-model="historyVisible" :report-id="editingId" :can-export="canExport" />
|
|
<el-dialog v-model="testDialogVisible" class="test-dialog" title="添加对比" width="540px" align-center destroy-on-close>
|
<div class="compare-select-form">
|
<label><span>选择测试</span><el-select v-model="selectedCandidateId" filterable clearable :loading="candidatesLoading" placeholder="输入测试名称或ID搜索" @change="syncDialogDefaults"><el-option v-for="item in candidateTests" :key="item.id" :label="`${item.name}(${item.id})`" :value="item.id" /></el-select></label>
|
<label><span>选择电池</span><el-select v-model="selectedBatteryNo" placeholder="请选择" :disabled="!selectedCandidateId" @change="selectedCycle = cycleChoices[0] ?? null; selectedProcess = processChoices[0] || ''"><el-option v-for="item in batteryChoices" :key="item" :label="item" :value="item" /></el-select></label>
|
<label><span>选择循环</span><el-select v-model="selectedCycle" placeholder="请选择" :disabled="!selectedCandidateId" @change="selectedProcess = processChoices[0] || ''"><el-option v-for="item in cycleChoices" :key="item" :label="`${item}`" :value="item" /></el-select></label>
|
<label><span>选择工步</span><div class="control-stack"><el-select v-model="selectedProcess" placeholder="请选择" :disabled="!selectedCandidateId"><el-option v-for="item in processChoices" :key="item" :label="item" :value="item" /></el-select></div></label>
|
<p v-if="candidatesError" class="select-error">{{ candidatesError }}</p><p v-else-if="!candidatesLoading && !candidateTests.length" class="select-empty">暂无检测完成且有已保存数据的测试</p>
|
</div>
|
<template #footer><div class="dialog-footer-left"><button class="primary-button" type="button" @click="addSelectedTest"><el-icon><Check /></el-icon>添加</button></div></template>
|
</el-dialog>
|
|
</div>
|
</template>
|
|
<style scoped>
|
.compare-form-page { width: 100%; padding-bottom: 80px; }
|
.compare-form-page.is-readonly :deep(.el-input), .compare-form-page.is-readonly :deep(.el-textarea) { pointer-events: none; }
|
.compare-form-page.is-readonly :deep(.el-input__wrapper), .compare-form-page.is-readonly :deep(.el-textarea__inner) { background: #f3f6f8; box-shadow: 0 0 0 1px #d8e1e9 inset; }
|
.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; }
|
.back-button:hover { color: var(--blue); }
|
.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; }
|
.primary-button:disabled { opacity: .55; cursor: wait; }
|
.secondary-button { color: #52606d; background: #fff; border-color: var(--line-strong); }
|
.secondary-button:hover { color: var(--blue); border-color: #9dc8f5; }
|
.report-name-panel, .compare-info-panel, .detail-panel, .metrics-panel { background: #fff; border: 1px solid #d8e1e9; }
|
.report-name-panel { padding: 18px 22px; margin-bottom: 16px; }
|
.report-name-panel label + label { margin-top: 12px; }
|
.report-name-panel > span { display: block; margin-top: 12px; overflow-wrap: anywhere; }
|
.series-swatch { flex: 0 0 12px; width: 12px; height: 12px; display: inline-block; }
|
.report-name-panel label { display: grid; grid-template-columns: 72px minmax(260px, 420px); align-items: center; gap: 12px; }
|
.report-name-panel label > span, .detail-meta label > span { color: #52606d; font-size: 12px; font-weight: 500; }
|
.report-name-panel :deep(.el-input__wrapper), .detail-meta :deep(.el-input__wrapper), .detail-meta :deep(.el-select__wrapper), .field-table :deep(.el-input__wrapper), .field-table :deep(.el-select__wrapper) { min-height: 34px; 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; color: #26333f; font-size: 13px; }
|
.panel-heading > div::before { content: ''; width: 3px; height: 16px; background: #2b78a3; }
|
.compare-info-panel, .detail-panel { margin-bottom: 16px; padding-bottom: 18px; }
|
.compare-table-wrap, .field-table-wrap { 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; }
|
.delete-link { border: 0; color: #d15b5b; background: transparent; font-size: 12px; cursor: pointer; }
|
.delete-link:hover { text-decoration: underline; }
|
.compact-button { margin: 16px 24px 0; }
|
.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; }
|
.field-table { min-width: 900px; }
|
.field-table th:first-child, .field-table td:first-child { width: 140px; text-align: left; }
|
.field-table.with-actions th:last-child, .field-table.with-actions td:last-child { width: 74px; }
|
.field-table th, .field-table td { white-space: normal; overflow-wrap: anywhere; padding-top: 6px; padding-bottom: 6px; }
|
.source-field-kind { display: block; color: #71808d; font-size: 11px; margin-top: 4px; }
|
.field-table .sample-row td { background: #fbfdff; }
|
.field-table .detail-meta-row td { background: #fbfdff; color: #26333f; }
|
.field-table .detail-meta-row td:not(:first-child) { font-weight: 500; }
|
.field-table :deep(.el-select), .field-table :deep(.el-input) { width: 100%; }
|
.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: 360px; overflow-wrap: anywhere; }
|
.legend-name { max-width: 100%; border: 0; padding: 4px 0; background: transparent; color: inherit; font: inherit; text-align: left; overflow-wrap: anywhere; cursor: pointer; }
|
.legend-name.is-hidden { color: #8795a3; text-decoration: line-through; }
|
.legend-name:disabled { cursor: default; }
|
.chart-empty { padding: 12px; color: #8795a3; font-size: 12px; text-align: center; }
|
.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; }
|
.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; }
|
.form-actions { position: sticky; bottom: 0; z-index: 20; min-height: 62px; display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 10px; margin-top: 16px; padding: 10px 16px; background: rgba(234, 240, 246, .96); border-top: 1px solid #cbd9e5; }
|
.form-actions .primary-button, .form-actions .secondary-button { min-width: 110px; }
|
.save-button { min-width: 110px; }
|
.compare-form-page :deep(.el-dialog) { max-width: calc(100vw - 28px); border-radius: 4px; }
|
.compare-form-page :deep(.el-dialog__header) { margin: 0; padding: 17px 20px; border-bottom: 1px solid var(--line); }
|
.compare-form-page :deep(.el-dialog__body) { padding: 20px; }
|
.compare-form-page :deep(.el-dialog__footer) { padding: 14px 20px; border-top: 1px solid var(--line); }
|
.compare-select-form { display: grid; gap: 14px; padding: 2px 16px 6px; }
|
.compare-select-form label { display: grid; grid-template-columns: 72px minmax(0, 1fr); align-items: start; gap: 12px; }
|
.compare-select-form label > span { padding-top: 9px; color: #52606d; font-size: 12px; }
|
.compare-select-form :deep(.el-select), .compare-select-form :deep(.el-input) { width: 100%; }
|
.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; }
|
.control-stack { display: grid; gap: 8px; }
|
.select-hint { color: #8a97a3; font-size: 11px; line-height: 1.4; }
|
.dialog-footer-left { display: flex; justify-content: flex-start; }
|
.template-options { display: grid; gap: 12px; }
|
.field-settings-table { overflow-x: auto; }
|
.field-settings-table table { width: 100%; min-width: 480px; border-collapse: collapse; table-layout: fixed; }
|
.field-settings-table th, .field-settings-table td { padding: 8px; text-align: left; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; }
|
.field-settings-table th:nth-child(3) { width: 90px; }
|
.field-settings-table th:last-child { width: 80px; }
|
.field-settings-table button { width: 28px; height: 28px; padding: 4px; border: 0; background: transparent; cursor: pointer; }
|
.field-settings-table button:disabled { opacity: .35; cursor: default; }
|
.field-table td:first-child { white-space: normal; overflow-wrap: anywhere; }
|
.template-options :deep(.el-radio) { width: 100%; height: auto; margin: 0; padding: 12px; white-space: normal; }
|
.battery-template-editor { min-height: 340px; padding: 4px 22px 18px; }
|
.battery-template-select { width: min(390px, 100%); display: grid; grid-template-columns: 88px minmax(0, 1fr); align-items: center; gap: 12px; margin: 0 auto 28px; }
|
.battery-template-select > span { color: #52606d; font-size: 12px; font-weight: 600; }
|
.battery-template-select :deep(.el-select) { width: 100%; }
|
.battery-template-fields { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px 28px; }
|
.battery-template-fields :deep(.el-checkbox) { min-width: 0; margin: 0; overflow: hidden; }
|
.battery-template-fields :deep(.el-checkbox__label) { min-width: 0; overflow: hidden; color: #52606d; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
|
.template-empty { padding: 56px 18px; color: #8795a3; text-align: center; border: 1px dashed #ccd9e5; }
|
.battery-template-actions, .template-dialog :deep(.el-dialog__footer) > div { display: flex; align-items: center; justify-content: center; gap: 12px; }
|
.battery-template-actions button { min-width: 96px; }
|
@media (max-width: 900px) { .charts-grid { grid-template-columns: minmax(0, 1fr); } }
|
@media (max-width: 680px) {
|
.report-name-panel { padding: 14px 12px; }
|
.report-name-panel label, .detail-meta { grid-template-columns: minmax(0, 1fr); gap: 7px; }
|
.panel-heading, .metrics-panel { padding-left: 12px; padding-right: 12px; }
|
.compare-table-wrap, .field-table-wrap { padding-left: 12px; padding-right: 12px; }
|
.compact-button { margin-left: 12px; }
|
.detail-actions { padding-left: 12px; padding-right: 12px; }
|
.metric-switches { gap: 14px; }
|
.chart-legend-bar { align-items: flex-start; flex-direction: column; gap: 10px; padding: 12px 0; }
|
.form-actions { justify-content: stretch; padding: 12px; }
|
.form-actions button { flex: 1; }
|
.battery-template-editor { padding: 0; }
|
.battery-template-select { grid-template-columns: minmax(0, 1fr); gap: 7px; }
|
.battery-template-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 12px; }
|
.battery-template-actions { flex-wrap: wrap; }
|
}
|
</style>
|