| | |
| | | import { compareSelectionKey, type CompareItem } from '@/api/compareReports' |
| | | import { measurementChartOption, type ReportMetricKey } from './reportMeasurementChart' |
| | | |
| | | /** 完整所选范围参与图例身份,避免同名电池的不同循环或工步混淆。 */ |
| | | /** 完整所选范围作为内部曲线身份,简短图例同名时仍可分别显隐。 */ |
| | | export function compareSeriesName(item: CompareItem) { |
| | | const source = item.source |
| | | return `${source.name} (#${source.selection.testId}) · ${source.batteryGroup} · ${source.batteryNo} · 循环${source.selection.cycleNo} · ${source.process}` |
| | | } |
| | | |
| | | /** 页面和打印图例只展示测试名称与电池号。 */ |
| | | export function compareSeriesLabel(item: CompareItem) { |
| | | return `${item.source.name}-${item.source.batteryNo}` |
| | | } |
| | | |
| | | /** 对比页面与打印共享冻结测量的绘图口径。 */ |
| | | export function compareChartOption(metric: { key: ReportMetricKey; unit: string }, items: CompareItem[], colors: string[], hiddenSeriesKeys: string[] = []) { |
| | | return measurementChartOption(metric, items.map(item => ({ ...item, name: compareSeriesName(item), hidden: hiddenSeriesKeys.includes(compareSelectionKey(item.source.selection)) })), colors) |
| | | const option = measurementChartOption(metric, items.map(item => ({ ...item, name: compareSeriesName(item), hidden: hiddenSeriesKeys.includes(compareSelectionKey(item.source.selection)) })), colors) |
| | | const labels = new Map(items.map(item => [compareSeriesName(item), compareSeriesLabel(item)])) |
| | | return { ...option, legend: { ...option.legend, formatter: (name: string) => labels.get(name) ?? name } } |
| | | } |
| | |
| | | import { SVGRenderer } from 'echarts/renderers' |
| | | import { compareSelectionKey, getComparePrintVersion, type SavedCompareReport } from '@/api/compareReports' |
| | | import type { CompareMetricKey } from '@/stores/compareReports' |
| | | import { compareChartOption, compareSeriesName } from './compareReportChart' |
| | | import { compareChartOption, compareSeriesLabel } from './compareReportChart' |
| | | import { openSavedReportPrint } from './savedReportPrint' |
| | | import { compareDetailSources } from './compareSourceDetails' |
| | | import { reportDetailColumn, reportDetailsHtml } from './reportTestDetails' |
| | |
| | | const color = record.settings.seriesColors[index % record.settings.seriesColors.length] |
| | | const safeColor = color && /^#[0-9a-f]{6}$/i.test(color) ? color : '#52606d' |
| | | const hidden = record.settings.hiddenSeriesKeys?.includes(compareSelectionKey(item.source.selection)) |
| | | return `<span${hidden ? ' style="text-decoration:line-through;color:#8795a3"' : ''}><i style="background:${safeColor}"></i>${escapeHtml(compareSeriesName(item))}</span>` |
| | | return `<span${hidden ? ' style="text-decoration:line-through;color:#8795a3"' : ''}><i style="background:${safeColor}"></i>${escapeHtml(compareSeriesLabel(item))}</span>` |
| | | }).join('') |
| | | const charts = compareMetricDefinitions.filter(metric => record.settings.metrics.includes(metric.key)).map(metric => |
| | | `<article class="chart-card"><header><strong>${metric.title} (${escapeHtml(record.displayUnits[metric.key] || metric.unit)})</strong></header>${chartSvg(metric, record)}<div class="chart-inner-legend">${legend}</div></article>`).join('') |
| | |
| | | ] as const |
| | | export type ProjectMetric = typeof projectMetrics[number] |
| | | export interface ProjectPoint { position: number; source?: string; values: Partial<Record<ProjectMetric['key'], number | null>> } |
| | | export interface ProjectSeries { key: string; name: string; points: ProjectPoint[] } |
| | | export interface ProjectSeries { key: string; name: string; legendLabel?: string; points: ProjectPoint[] } |
| | | export const projectPalette = ['#3978f6', '#7d42d8', '#f08a3e', '#23a983', '#e15268', '#1b9ac8', '#b5892f', '#6957c8'] |
| | | |
| | | /** 控制台、拼接预览和打印共享图表样式;来源提示与横轴位置分开,不插值或补循环。 */ |
| | |
| | | const margin = low === undefined || high === undefined ? undefined : Math.max((high - low) * 0.15, 0.01) |
| | | return { animation: false, color: colors, grid: { top: 62, right: 34, bottom: 48, left: 62 }, |
| | | tooltip: { trigger: 'axis', renderMode: 'richText', formatter: (params: Array<{ seriesName: string; data: { value: [number | string, number | null]; source?: string } }>) => params.map(item => `${item.seriesName}\n${item.data.source || ''}\n${spliced ? '累计位置' : '循环'} ${item.data.value[0]}:${item.data.value[1] ?? '缺失'} ${metric.unit}`).join('\n\n') }, |
| | | legend: { top: 8, left: 12, right: 12, type: 'scroll', icon: 'roundRect', itemWidth: 14, itemHeight: 8, itemGap: 16, textStyle: { fontSize: 10, color: '#52606d' }, selected: Object.fromEntries(series.map(item => [item.name, !hidden.includes(item.key)])) }, |
| | | legend: { top: 8, left: 12, right: 12, type: 'scroll', icon: 'roundRect', itemWidth: 14, itemHeight: 8, itemGap: 16, textStyle: { fontSize: 10, color: '#52606d' }, formatter: (name: string) => series.find(item => item.name === name)?.legendLabel ?? name, selected: Object.fromEntries(series.map(item => [item.name, !hidden.includes(item.key)])) }, |
| | | xAxis: { type: spliced ? 'value' : 'category', boundaryGap: false, data: spliced ? undefined : positions.map(String), min: spliced ? positions[0] ?? 1 : undefined, max: spliced ? (positions.at(-1) === positions[0] ? (positions[0] ?? 1) + 1 : positions.at(-1)) : undefined, minInterval: 1, name: spliced ? '累计位置' : '循环次数', nameLocation: 'middle', nameGap: 30, axisLine: { lineStyle: { color: '#cbd8e4' } }, axisLabel: { color: '#8795a3', fontSize: 10 }, nameTextStyle: { color: '#52606d', fontSize: 10 } }, |
| | | yAxis: { type: 'value', name: metric.unit, nameLocation: 'middle', nameGap: 42, min: margin === undefined ? undefined : low! - margin, max: margin === undefined ? undefined : high! + margin, axisLabel: { color: '#8795a3', fontSize: 10, formatter: (value: number) => String(Number(value.toFixed(metric.key === 'dischargeCapacityF' ? 2 : 4))) }, splitLine: { lineStyle: { color: '#e8eef4' } } }, |
| | | // 最终电压2没有确认公式,保留空图。单点保留标记,缺测空点不连线。 |
| | |
| | | const series: ProjectSeries = { |
| | | key: `${source.selection.testId}|${source.selection.testBatteryId}`, |
| | | name: `${source.name} (#${source.selection.testId}) · ${source.group} · ${source.batteryNo}`, |
| | | // 只缩短显示文案;同名来源仍保留各自图例身份和显隐状态。 |
| | | legendLabel: `${source.name}-${source.batteryNo}`, |
| | | points: [], |
| | | } |
| | | for (const no of segment.originalCycleNos) { |
| | |
| | | |
| | | <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.id }}) · {{ item.batteryGroup }} · {{ item.batteryNo }} · 循环{{ item.cycle }} · {{ item.process }}</button></div></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> |
| | |
| | | await expect(popup.locator('body')).toContainText('3:原循环10') |
| | | await expect(popup.locator('svg')).toHaveCount(3) |
| | | // 同组同名电池跨测试仍有各自图例,打印不能合并来源段。 |
| | | await expect(popup.locator('svg').first()).toContainText('测试1 (#1) · 组9 · 1号') |
| | | await expect(popup.locator('svg').first()).toContainText('测试2 (#2) · 组9 · 1号') |
| | | await expect(popup.locator('svg').first()).toContainText('测试1-1号') |
| | | await expect(popup.locator('svg').first()).toContainText('测试2-1号') |
| | | await popup.close() |
| | | await page.setViewportSize({width:390,height:844}) |
| | | await expect.poll(() => page.locator('.app-main').evaluate(element => element.getBoundingClientRect().left)).toBe(0) |
| | |
| | | await expect(page.locator('.chart-empty')).toHaveCount(0) |
| | | await expect(page.locator('.report-test-detail')).not.toContainText('循环') |
| | | await expect(page.locator('.report-test-detail')).not.toContainText('工步') |
| | | await expect(page.locator('.chart-legend-bar')).toContainText('合并工步') |
| | | await expect(page.locator('.chart-legend-bar .legend-name')).toHaveText(['测试1-1号','测试2-1号']) |
| | | await expect(page.getByText('数据选择',{exact:true})).toHaveCount(0) |
| | | await page.screenshot({path:resolve(artifacts,'compare-desktop.png'),fullPage:true}) |
| | | const comparePopupPromise=page.waitForEvent('popup') |
| | | await page.getByRole('button',{name:'打印 / 另存 PDF'}).click() |
| | | const comparePopup=await comparePopupPromise |
| | | await expect(comparePopup.locator('.chart-card svg')).toHaveCount(5) |
| | | await expect(comparePopup.locator('.legend span')).toHaveText(['测试1-1号','测试2-1号']) |
| | | await expect(comparePopup.locator('.chart-card svg').first()).toContainText('测试1-1号') |
| | | for (const chart of await comparePopup.locator('.chart-card svg').all()) { |
| | | await expect(chart.locator('path[fill="none"][stroke="#3978f6"][stroke-width="2"]')).toHaveCount(1) |
| | | await expect(chart.locator('path[fill="none"][stroke="#7d42d8"][stroke-width="2"]')).toHaveCount(1) |