feat(steady): 完善稳态数据视图功能

- 更新纵坐标刻度算法,优化小数趋势图范围显示
- 添加稳态趋势图全屏模式和共享工具组件
- 实现多图联动的鼠标悬停竖线同步功能
- 调整主线线宽分档策略,降低最大线宽限制
- 重构稳态趋势工具栏,优化谐波次数选择逻辑
- 添加周时间周期搜索支持和自定义时间范围选择
- 完善稳态数据表格和指示器浮动面板功能
- 优化稳态趋势图性能,添加LTB采样和动画控制
- 修复数据表格打开前的趋势数据验证问题
- 统一时间轴标签格式化和网格对齐处理
This commit is contained in:
2026-05-27 08:06:12 +08:00
parent b9ddfb5275
commit 055e69fff7
83 changed files with 9616 additions and 226 deletions

View File

@@ -5,10 +5,20 @@ import { fileURLToPath } from 'node:url'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const viewDir = path.join(currentDir, '..')
const read = file => fs.readFileSync(path.join(viewDir, file), 'utf8')
const read = file => {
const filePath = path.join(viewDir, file)
return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : ''
}
const toolbarSource = read('components/SteadyTrendToolbar.vue')
const chartPanelSource = read('components/SteadyTrendChartPanel.vue')
const fullscreenSource = read('components/SteadyTrendFullscreen.vue')
const chartToolsSource = read('components/SteadyTrendChartTools.vue')
const lineChartSource = fs.readFileSync(
path.join(viewDir, '..', '..', '..', 'components', 'echarts', 'line', 'index.vue'),
'utf8'
)
const trendPayloadSource = read('utils/trendPayload.ts')
const trendOptionsSource = read('utils/trendOptions.ts')
const selectionRulesSource = read('utils/selectionRules.ts')
@@ -46,14 +56,89 @@ const expectations = [
/v-for="group in chartGroups"[\s\S]*:options="group\.options"/,
chartPanelSource
],
[
'chart loading mask stays outside the fullscreen dialog tree',
/<section\s+class="card trend-chart-panel"(?![^>]*v-loading)[\s\S]*<div[^>]*class="chart-panel-body"[^>]*v-loading="loading"[\s\S]*<SteadyTrendFullscreen/,
chartPanelSource
],
[
'normal chart list caps visible charts at three while smaller counts fill the viewport',
/(?=[\s\S]*normalVisibleChartCount[\s\S]*Math\.min\(chartGroups\.value\.length,\s*3\))(?=[\s\S]*:style="\{\s*'--steady-trend-visible-chart-count':\s*normalVisibleChartCount\s*\}")/,
chartPanelSource
],
[
'fullscreen chart list caps visible charts at six while smaller counts fill the viewport',
/(?=[\s\S]*fullscreenVisibleChartCount[\s\S]*Math\.min\(chartGroups\.value\.length,\s*6\))(?=[\s\S]*:visible-chart-count="fullscreenVisibleChartCount")/,
chartPanelSource
],
[
'chart groups divide the current viewport by visible chart count',
/\.chart-group\s*\{[\s\S]*flex:\s*0 0\s*calc\(\s*\(100% - var\(--steady-trend-chart-gap\)\s*\*\s*\(var\(--steady-trend-visible-chart-count\)\s*-\s*1\)\)\s*\/\s*var\(--steady-trend-visible-chart-count\)\s*\)/,
chartPanelSource
],
[
'chart groups include their border in the divided height to avoid false scrollbars',
/\.chart-group\s*\{[\s\S]*box-sizing:\s*border-box/,
chartPanelSource
],
[
'chart panel delegates fullscreen rendering to a dedicated component',
/<SteadyTrendFullscreen[\s\S]*v-model="fullscreenVisible"[\s\S]*:chart-groups="chartGroups"[\s\S]*:visible-chart-count="fullscreenVisibleChartCount"[\s\S]*@chart-data-zoom="handleChartDataZoom"/,
chartPanelSource
],
[
'chart panel passes shared trend tools into fullscreen',
/<SteadyTrendFullscreen[\s\S]*:tool-groups="fullscreenToolGroups"[\s\S]*:is-tool-active="isTrendToolActive"[\s\S]*:is-tool-disabled="isTrendToolDisabled"[\s\S]*:get-tool-tooltip="getTrendToolTooltip"[\s\S]*@tool-action="handleTrendToolAction"/,
chartPanelSource
],
[
'fullscreen tool groups omit nested fullscreen action',
/fullscreenToolGroups[\s\S]*item\.action\s*!==\s*'fullscreen'/,
chartPanelSource
],
[
'chart panel does not use Element Plus dialog for steady fullscreen',
/^(?![\s\S]*<el-dialog[\s\S]*steady-trend-fullscreen)(?![\s\S]*fullscreen-chart-body)[\s\S]*$/,
chartPanelSource
],
[
'fullscreen component renders through a fixed body teleport',
/<Teleport\s+to="body">[\s\S]*class="steady-trend-fullscreen"[\s\S]*position:\s*fixed[\s\S]*inset:\s*0/,
fullscreenSource
],
[
'fullscreen component has an explicit close button and Esc handling',
/@click="closeFullscreen"[\s\S]*event\.key\s*!==\s*'Escape'[\s\S]*window\.addEventListener\('keydown',\s*handleKeydown\)/,
fullscreenSource
],
[
'fullscreen component gives chart body a concrete flex viewport',
/\.steady-trend-fullscreen__body\s*\{[\s\S]*display:\s*flex[\s\S]*flex:\s*1\s+1\s+auto[\s\S]*min-height:\s*0[\s\S]*\.steady-trend-fullscreen__chart-list\s*\{[\s\S]*flex:\s*1\s+1\s+auto[\s\S]*overflow-y:\s*auto/,
fullscreenSource
],
[
'fullscreen keeps trend tool groups in a real workspace row below the title bar',
/<main class="steady-trend-fullscreen__body">[\s\S]*<div class="steady-trend-fullscreen__tool-row">[\s\S]*<SteadyTrendChartTools[\s\S]*<\/div>[\s\S]*class="steady-trend-fullscreen__chart-list"/,
fullscreenSource
],
[
'fullscreen title header does not contain trend tool groups',
/<header class="steady-trend-fullscreen__header">(?:(?!SteadyTrendChartTools)[\s\S])*<\/header>/,
fullscreenSource
],
[
'fullscreen tool row reserves layout space instead of floating over charts',
/\.steady-trend-fullscreen__tool-row\s*\{(?=[\s\S]*position:\s*static)(?=[\s\S]*display:\s*flex)(?=[\s\S]*flex:\s*none)[\s\S]*\}/,
fullscreenSource
],
[
'chart panel syncs grouped charts through LineChart group and dataZoom events',
/<LineChart[\s\S]*:options="group\.options"[\s\S]*:group="group\.group"[\s\S]*@chart-data-zoom="handleChartDataZoom"/,
chartPanelSource
],
[
'chart panel only shows trend point count without bucket prefix',
/trendResult\.displayPointCount\s*\|\|\s*0[\s\S]*点/,
'chart panel shows total point count with explicit label',
/class="panel-meta"[\s\S]*trendResult\.displayPointCount\s*\|\|\s*0/,
chartPanelSource
],
[
@@ -62,38 +147,50 @@ const expectations = [
chartPanelSource
],
[
'chart panel renders waveform-style trend tool groups',
/trend-tool-groups[\s\S]*trendToolGroups[\s\S]*handleTrendToolAction/,
'chart panel renders shared trend tool groups',
/<SteadyTrendChartTools[\s\S]*:tool-groups="trendToolGroups"[\s\S]*@tool-action="handleTrendToolAction"/,
chartPanelSource
],
[
'chart panel keeps trend toolbar next to point count on the left',
/\.panel-header\s*\{[\s\S]*justify-content:\s*flex-start/,
'chart panel keeps total point count on the right of the trend toolbar with 15px spacing',
/\.panel-meta\s*\{[\s\S]*margin-left:\s*15px/,
chartPanelSource
],
[
'shared chart tools component renders the trend tool buttons',
/trend-tool-groups[\s\S]*v-for="group in toolGroups"[\s\S]*v-for="item in group\.items"[\s\S]*@click="emit\('tool-action', item\.action\)"/,
chartToolsSource
],
[
'fullscreen renders shared trend tool groups',
/<SteadyTrendChartTools[\s\S]*:tool-groups="toolGroups"[\s\S]*@tool-action="emit\('tool-action', \$event\)"/,
fullscreenSource
],
[
'chart panel exposes core trend toolbar actions except marker and data export',
/x-zoom-in[\s\S]*x-zoom-out[\s\S]*y-zoom-in[\s\S]*y-zoom-out[\s\S]*box-zoom[\s\S]*reset[\s\S]*pan[\s\S]*fullscreen[\s\S]*download-image/,
chartPanelSource
],
['chart panel exposes a wheel zoom toggle action', /wheel-zoom/, chartPanelSource],
[
'chart panel exposes a wheel zoom toggle action',
/wheel-zoom/,
chartPanelSource
],
[
'chart panel defaults wheel zoom disabled',
/const\s+wheelZoomEnabled\s*=\s*ref\(false\)/,
'chart panel places wheel zoom after pan in the viewport toolbar',
/items:\s*\[[\s\S]*action:\s*'pan'[\s\S]*action:\s*'wheel-zoom'/,
chartPanelSource
],
['chart panel defaults wheel zoom disabled', /const\s+wheelZoomEnabled\s*=\s*ref\(false\)/, chartPanelSource],
[
'chart panel passes wheel zoom state into grouped options',
/buildSteadyTrendChartGroups\([^)]*trendXZoomRange\.value[\s\S]*wheelZoomEnabled:\s*wheelZoomEnabled\.value/,
chartPanelSource
],
[
'chart panel defaults to first tenth of the x range',
/DEFAULT_STEADY_TREND_X_ZOOM_RANGE\s*:\s*SteadyTrendZoomRange\s*=\s*\{\s*start:\s*0,\s*end:\s*10\s*\}/,
'chart panel keeps full range as fallback x zoom range',
/DEFAULT_STEADY_TREND_X_ZOOM_RANGE\s*:\s*SteadyTrendZoomRange\s*=\s*\{\s*start:\s*0,\s*end:\s*100\s*\}/,
chartPanelSource
],
[
'chart panel defaults x zoom range by 20 30 60 day thresholds',
/STEADY_TREND_HALF_RANGE_DAYS\s*=\s*20[\s\S]*STEADY_TREND_QUARTER_RANGE_DAYS\s*=\s*30[\s\S]*STEADY_TREND_TENTH_RANGE_DAYS\s*=\s*60[\s\S]*resolveSteadyTrendDefaultZoomRange[\s\S]*timeRangeDays\s*>\s*STEADY_TREND_TENTH_RANGE_DAYS[\s\S]*end:\s*10[\s\S]*timeRangeDays\s*>\s*STEADY_TREND_QUARTER_RANGE_DAYS[\s\S]*end:\s*25[\s\S]*timeRangeDays\s*>\s*STEADY_TREND_HALF_RANGE_DAYS[\s\S]*end:\s*50/,
chartPanelSource
],
[
@@ -102,8 +199,8 @@ const expectations = [
chartPanelSource
],
[
'chart panel compares reset state against the default x zoom range',
/const isDefaultTrendXZoomRange[\s\S]*DEFAULT_STEADY_TREND_X_ZOOM_RANGE[\s\S]*const canResetTrendChart[\s\S]*!isDefaultTrendXZoomRange\.value/,
'chart panel compares reset state against the current data-derived x zoom range',
/const defaultTrendXZoomRange\s*=\s*ref<SteadyTrendZoomRange>[\s\S]*const isDefaultTrendXZoomRange[\s\S]*defaultTrendXZoomRange\.value[\s\S]*const canResetTrendChart[\s\S]*!isDefaultTrendXZoomRange\.value/,
chartPanelSource
],
[
@@ -117,8 +214,8 @@ const expectations = [
chartPanelSource
],
[
'chart panel resets x zoom range to the default first tenth when trend result changes',
/const\s+resetTrendToolState\s*=\s*\(\)\s*=>\s*\{[\s\S]*trendXZoomRange\.value\s*=\s*\{\s*\.\.\.DEFAULT_STEADY_TREND_X_ZOOM_RANGE\s*\}[\s\S]*watch\(\s*\(\)\s*=>\s*props\.trendResult[\s\S]*resetTrendToolState\(\)/,
'chart panel resets x zoom range to the current data-derived default when trend result changes',
/const\s+resetTrendToolState\s*=\s*\(\)\s*=>\s*\{[\s\S]*trendXZoomRange\.value\s*=\s*\{\s*\.\.\.defaultTrendXZoomRange\.value\s*\}[\s\S]*watch\(\s*\(\)\s*=>\s*props\.trendResult[\s\S]*defaultTrendXZoomRange\.value\s*=\s*resolveSteadyTrendDefaultZoomRange\(props\.trendResult\)[\s\S]*resetTrendToolState\(\)/,
chartPanelSource
],
[
@@ -162,6 +259,31 @@ const expectations = [
/dataZoom:\s*\[[\s\S]*start:\s*zoomRange\.start[\s\S]*end:\s*zoomRange\.end/,
trendOptionsSource
],
[
'chart options enlarge slider dataZoom handles for easier horizontal dragging',
/handleSize:\s*'300%'/,
trendOptionsSource
],
[
'chart options highlight slider dataZoom handles on hover',
/emphasis:\s*\{[\s\S]*handleStyle:\s*\{[\s\S]*borderColor:\s*'#409eff'[\s\S]*shadowBlur:\s*6/,
trendOptionsSource
],
[
'chart options show pointer cursor on slider dataZoom handles',
/handleSize:\s*'300%'[\s\S]*cursor:\s*'pointer'[\s\S]*handleStyle:/,
trendOptionsSource
],
[
'chart slider dataZoom only syncs after dragging settles for dense trend charts',
/handleSize:\s*'300%'[\s\S]*realtime:\s*false[\s\S]*cursor:\s*'pointer'/,
trendOptionsSource
],
[
'line chart overrides slider dataZoom handle cursor to pointer on hover',
/isSliderDataZoomResizeHandle[\s\S]*target\?\.type\s*===\s*'path'[\s\S]*viewportRoot\.style\.cursor\s*=\s*'pointer'/,
lineChartSource
],
[
'chart options accept wheel zoom option',
/interface\s+SteadyTrendChartBuildOptions[\s\S]*wheelZoomEnabled\?:\s*boolean/,
@@ -177,6 +299,21 @@ const expectations = [
/tooltip:\s*\{[\s\S]*axisPointer:\s*\{[\s\S]*type:\s*'line'/,
trendOptionsSource
],
[
'chart options hide hover x-axis pointer labels to avoid overlapping time axis labels',
/tooltip:\s*\{[\s\S]*axisPointer:\s*\{[\s\S]*type:\s*'line'[\s\S]*label:\s*\{[\s\S]*show:\s*false/,
trendOptionsSource
],
[
'chart tooltip avoids heavy immediate transitions on dense trend charts',
/tooltip:\s*\{[\s\S]*showDelay:\s*80[\s\S]*hideDelay:\s*80[\s\S]*transitionDuration:\s*0/,
trendOptionsSource
],
[
'chart options register hidden toolbox dataZoom for external box zoom',
/toolbox:\s*\{[\s\S]*show:\s*true[\s\S]*itemSize:\s*0[\s\S]*left:\s*-100[\s\S]*feature:\s*\{[\s\S]*dataZoom:\s*\{[\s\S]*yAxisIndex:\s*'none'[\s\S]*brushStyle:/,
trendOptionsSource
],
[
'chart options calculate visible point count from shared zoom range',
/resolveSteadyTrendVisiblePointCount[\s\S]*zoomRange\.end\s*-\s*zoomRange\.start[\s\S]*Math\.ceil/,
@@ -184,7 +321,7 @@ const expectations = [
],
[
'chart series line width uses visible point count after x zoom',
/width:\s*resolveSteadyTrendLineWidth\(\s*resolveSteadyTrendVisiblePointCount\(series\.points\?\.length\s*\|\|\s*0,\s*zoomRange\)\s*\)/,
/const\s+pointCount\s*=\s*series\.points\?\.length\s*\|\|\s*0[\s\S]*const\s+visiblePointCount\s*=\s*resolveSteadyTrendVisiblePointCount\(pointCount,\s*zoomRange\)[\s\S]*width:\s*resolveSteadyTrendLineWidth\(visiblePointCount\)/,
trendOptionsSource
],
[
@@ -233,13 +370,28 @@ const expectations = [
trendOptionsSource
],
[
'chart x axis only keeps first and last labels like waveform',
/buildSteadyTimeAxisLabelFormatter[\s\S]*index\s*!==\s*0\s*&&\s*index\s*!==\s*lastIndex[\s\S]*return\s*''/,
'chart x axis formats time axis labels by range span',
/STEADY_TREND_ONE_DAY_MS[\s\S]*buildSteadyTimeAxisLabelFormatter[\s\S]*isRangeOverOneDay[\s\S]*formatSteadyTimeAxisDailyLabel[\s\S]*formatSteadyTimeAxisHourMinuteLabel/,
trendOptionsSource
],
[
'chart x axis keeps only the first year for same-year daily labels',
/formatSteadyTimeAxisDailyLabel[\s\S]*date\.getFullYear\(\)\s*===\s*firstYear[\s\S]*formatSteadyTimeAxisShortDateLabel[\s\S]*const\s+firstYear/,
trendOptionsSource
],
[
'chart x axis first label uses rich padding to avoid touching the y axis line',
/formatSteadyTimeAxisFirstLabel[\s\S]*return `\{first\|\$\{label\}\}`[\s\S]*rich:\s*\{[\s\S]*first:\s*\{[\s\S]*padding:\s*\[0,\s*0,\s*0,\s*6\]/,
trendOptionsSource
],
[
'chart x axis label layout follows waveform except unit display',
/axisLabel:\s*\{[\s\S]*hideOverlap:\s*false[\s\S]*interval:\s*0[\s\S]*margin:\s*showTimeAxis\s*\?\s*10\s*:\s*0[\s\S]*formatter:\s*buildSteadyTimeAxisLabelFormatter\(timeLabels\)/,
/axisLabel:\s*\{[\s\S]*hideOverlap:\s*true[\s\S]*showMinLabel:\s*true[\s\S]*showMaxLabel:\s*false[\s\S]*margin:\s*showTimeAxis\s*\?\s*16\s*:\s*0[\s\S]*alignMinLabel:\s*'left'[\s\S]*alignMaxLabel:\s*'right'[\s\S]*width:\s*72[\s\S]*overflow:\s*'truncate'[\s\S]*formatter:\s*buildSteadyTimeAxisLabelFormatter\(timeRange\)/,
trendOptionsSource
],
[
'chart x axis label moves down without increasing bottom grid space',
/bottom:\s*showTimeAxis\s*\?\s*40\s*:\s*8[\s\S]*axisLabel:\s*\{[\s\S]*margin:\s*showTimeAxis\s*\?\s*16\s*:\s*0/,
trendOptionsSource
],
[
@@ -264,8 +416,33 @@ const expectations = [
],
['chart options read phase colors from shared theme utility', /resolvePhaseThemeColor/, trendOptionsSource],
[
'chart legend only uses phase name',
/const formatSeriesName[\s\S]*return series\.phase \|\| series\.seriesKey/,
'chart legend combines harmonic order and phase when harmonic order exists',
/const formatSeriesName[\s\S]*const harmonicOrder = resolveHarmonicOrder\(series\)[\s\S]*return harmonicOrder \? `\$\{harmonicOrder\}次_\$\{phaseLabel\}` : phaseLabel/,
trendOptionsSource
],
[
'chart series are sorted by harmonic order before phase so same orders stay together in the legend',
/const sortSteadyTrendSeries[\s\S]*resolveHarmonicOrder\(left\.series\)[\s\S]*resolvePhaseOrder\(left\.series\)[\s\S]*const sortedSeriesList = sortSteadyTrendSeries\((?:seriesList|displaySeriesList)\)[\s\S]*series: sortedSeriesList\.map/,
trendOptionsSource
],
[
'chart options keep phase colors while styling harmonic orders by line type',
/lineStyle:\s*\{[\s\S]*type:\s*resolveHarmonicLineType\(series,\s*pointCount\)[\s\S]*opacity:\s*resolveHarmonicLineOpacity\(series\)/,
trendOptionsSource
],
[
'chart options force solid harmonic lines for large point counts',
/STEADY_TREND_LARGE_POINT_COUNT\s*=\s*20000[\s\S]*resolveHarmonicLineType[\s\S]*pointCount\s*>=\s*STEADY_TREND_LARGE_POINT_COUNT[\s\S]*return 'solid'/,
trendOptionsSource
],
[
'chart series disable animation and use lttb sampling for large point counts',
/resolveSteadyTrendSampling[\s\S]*pointCount\s*>=\s*STEADY_TREND_LARGE_POINT_COUNT\s*\?\s*'lttb'[\s\S]*animation:\s*false[\s\S]*sampling:\s*resolveSteadyTrendSampling\(pointCount,\s*visiblePointCount\)/,
trendOptionsSource
],
[
'chart disables lttb sampling after x zoom reduces visible points',
/STEADY_TREND_RAW_VISIBLE_POINT_COUNT[\s\S]*resolveSteadyTrendSampling\s*=\s*\([^)]*visiblePointCount[\s\S]*visiblePointCount\s*<=\s*STEADY_TREND_RAW_VISIBLE_POINT_COUNT[\s\S]*return undefined[\s\S]*const\s+visiblePointCount\s*=\s*resolveSteadyTrendVisiblePointCount\(pointCount,\s*zoomRange\)[\s\S]*sampling:\s*resolveSteadyTrendSampling\(pointCount,\s*visiblePointCount\)/,
trendOptionsSource
],
[