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

@@ -0,0 +1,222 @@
<template>
<section class="card trend-toolbar">
<div class="toolbar-field toolbar-field--time">
<span class="toolbar-field__label">时间</span>
<TimePeriodSearch
class="trend-toolbar__time"
:unit="modelValue.timeUnit"
:model-value="modelValue.timeBaseDate"
@update:unit="handleTimeUnitChange"
@update:model-value="handleTimeBaseDateChange"
/>
</div>
<div class="toolbar-field">
<span class="toolbar-field__label">统计</span>
<el-select
:model-value="modelValue.statType"
placeholder="选择统计类型"
@update:model-value="updateField('statType', $event)"
>
<el-option v-for="item in statOptions" :key="item" :label="statLabelMap[item]" :value="item" />
</el-select>
</div>
<div class="toolbar-field">
<span class="toolbar-field__label">数据质量</span>
<el-switch
:model-value="modelValue.qualityFlag ?? 0"
class="quality-switch"
width="72"
inline-prompt
active-text="有效"
inactive-text="无效"
:active-value="0"
:inactive-value="1"
@update:model-value="handleQualityFlagChange"
/>
</div>
<div v-if="showHarmonicOrders" class="toolbar-field harmonic-select">
<span class="toolbar-field__label">谐波次数</span>
<el-select
:model-value="modelValue.harmonicOrders"
multiple
placeholder="选择谐波次数"
@update:model-value="handleHarmonicOrdersChange"
>
<el-option v-for="item in harmonicOrderOptions" :key="item" :label="`${item}次`" :value="item" />
</el-select>
</div>
<div class="toolbar-actions">
<el-button type="primary" :loading="loading" @click="emit('query')">查询</el-button>
<el-button @click="emit('reset')">重置</el-button>
</div>
</section>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import type { SteadyTrend } from '@/api/steady/steadyTrend/interface'
import TimePeriodSearch from '@/views/components/TimePeriodSearch/index.vue'
import { buildTimePeriodRange, type TimePeriodUnit } from '@/views/components/TimePeriodSearch/timePeriod'
import { MAX_HARMONIC_ORDER_COUNT } from '../utils/selectionRules'
import type { SteadyTrendFormState } from '../utils/trendPayload'
defineOptions({
name: 'SteadyTrendToolbar'
})
const props = defineProps<{
modelValue: SteadyTrendFormState
statOptions: SteadyTrend.SteadyTrendStatType[]
showHarmonicOrders: boolean
loading: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: SteadyTrendFormState]
query: []
reset: []
}>()
const harmonicOrderOptions = Array.from({ length: 49 }, (_item, index) => index + 2)
const statLabelMap: Record<SteadyTrend.SteadyTrendStatType, string> = {
AVG: '平均值',
MAX: '最大值',
MIN: '最小值',
CP95: '95%概率大值'
}
const updateField = <K extends keyof SteadyTrendFormState>(field: K, value: SteadyTrendFormState[K]) => {
emit('update:modelValue', {
...props.modelValue,
[field]: value
})
}
const handleQualityFlagChange = (value: string | number | boolean) => {
updateField('qualityFlag', Number(value))
}
const normalizeHarmonicOrders = (orders: number[]) => {
return Array.from(
new Set(
orders
.map(item => Number(item))
.filter(item => Number.isInteger(item) && item >= 2 && item <= 50)
)
).sort((left, right) => left - right)
}
const updateHarmonicOrders = (orders: number[]) => {
const nextOrders = normalizeHarmonicOrders(orders)
if (nextOrders.length > MAX_HARMONIC_ORDER_COUNT) {
ElMessage.warning(`谐波次数最多选择 ${MAX_HARMONIC_ORDER_COUNT}`)
const currentOrders = normalizeHarmonicOrders(props.modelValue.harmonicOrders)
updateField(
'harmonicOrders',
currentOrders.length ? currentOrders : nextOrders.slice(0, MAX_HARMONIC_ORDER_COUNT)
)
return
}
updateField('harmonicOrders', nextOrders)
}
const handleHarmonicOrdersChange = (value: number[]) => {
updateHarmonicOrders(value)
}
const updateTimeRange = (unit: TimePeriodUnit, baseDate: Date) => {
emit('update:modelValue', {
...props.modelValue,
timeUnit: unit,
timeBaseDate: baseDate,
timeRange: buildTimePeriodRange(unit, baseDate)
})
}
const handleTimeUnitChange = (value: TimePeriodUnit) => {
updateTimeRange(value, props.modelValue.timeBaseDate)
}
const handleTimeBaseDateChange = (value: Date) => {
updateTimeRange(props.modelValue.timeUnit, value)
}
</script>
<style scoped lang="scss">
.trend-toolbar {
display: grid;
grid-template-columns: minmax(360px, 1.35fr) repeat(3, minmax(0, 1fr)) auto;
gap: 10px;
align-items: center;
padding: 12px;
}
.toolbar-field {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
}
.toolbar-field--time {
min-width: 0;
}
.toolbar-field__label {
flex: 0 0 auto;
color: #606266;
font-size: 14px;
white-space: nowrap;
}
.toolbar-field :deep(.el-select) {
flex: 1 1 0;
min-width: 0;
}
.quality-switch {
min-width: 72px;
}
.trend-toolbar__time {
flex: 1 1 0;
min-width: 0;
}
.trend-toolbar__time :deep(.time-period-search__unit) {
width: 72px;
flex: 0 0 72px;
}
.trend-toolbar__time :deep(.time-period-search__picker) {
width: 136px;
flex: 0 0 136px;
}
.harmonic-select {
grid-column: auto;
}
.toolbar-actions {
display: flex;
grid-column: 5;
justify-content: flex-end;
gap: 8px;
}
@media (max-width: 1280px) {
.trend-toolbar {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.toolbar-actions {
justify-content: flex-start;
}
}
</style>