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,316 @@
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
export const CHECKSQUARE_STAT_TYPES: SteadyDataView.SteadyTrendStatType[] = ['AVG', 'MAX', 'MIN', 'CP95']
export const CHECKSQUARE_HARMONIC_ORDER_MIN = 2
export const CHECKSQUARE_HARMONIC_ORDER_MAX = 50
export const CHECKSQUARE_HARMONIC_ORDERS = Array.from(
{ length: CHECKSQUARE_HARMONIC_ORDER_MAX - CHECKSQUARE_HARMONIC_ORDER_MIN + 1 },
(_item, index) => index + CHECKSQUARE_HARMONIC_ORDER_MIN
)
const CHECKSQUARE_STAT_LABEL_MAP: Record<SteadyDataView.SteadyTrendStatType, string> = {
AVG: '平均值',
MAX: '最大值',
MIN: '最小值',
CP95: 'CP95'
}
export const formatChecksquareStatType = (statType: SteadyDataView.SteadyTrendStatType | string) => {
return CHECKSQUARE_STAT_LABEL_MAP[statType as SteadyDataView.SteadyTrendStatType] || statType
}
export const formatBooleanText = (value?: boolean | null) => {
if (value === null || value === undefined) return '-'
return value ? '是' : '否'
}
export const formatMissingRate = (value?: number | null, text?: string | null) => {
if (text) return text
if (value === null || value === undefined || !Number.isFinite(Number(value))) return '-'
return `${(Number(value) * 100).toFixed(2)}%`
}
export const findStatSummary = (
item: SteadyDataView.SteadyChecksquareItem,
statType: SteadyDataView.SteadyTrendStatType
) => {
return item.statSummaries?.find(summary => summary.statType === statType)
}
export const formatStatMissingRate = (
item: SteadyDataView.SteadyChecksquareItem,
statType: SteadyDataView.SteadyTrendStatType
) => {
const summary = findStatSummary(item, statType)
if (!summary || summary.supported === false) return '-'
return formatMissingRate(summary.missingRate, summary.missingRateText)
}
export const resolveChecksquareRowName = (item: SteadyDataView.SteadyChecksquareItem) => {
const progressText = getHarmonicProgressText(item)
if (progressText) return `${item.indicatorName || item.indicatorCode}${progressText}`
if (!item.harmonicOrder) return item.indicatorName || item.indicatorCode
return `${item.harmonicOrder}`
}
export const getHarmonicProgressText = (item: SteadyDataView.SteadyChecksquareItem) => {
const children = item.children || []
if (!children.length || !children.some(child => child.harmonicOrder)) return ''
const totalCount = children.length
const resolvedCount = children.filter(child => isResolvedChecksquareItem(child)).length
if (resolvedCount >= totalCount) return ''
return `已完成 ${resolvedCount}/${totalCount}`
}
export const collectMissingSegments = (item: SteadyDataView.SteadyChecksquareItem | null) => {
if (!item) return []
return (item.statDetails || []).flatMap(detail =>
(detail.segments || [])
.filter(segment => segment.status === 'MISSING')
.map(segment => ({
...segment,
statType: detail.statType
}))
)
}
export const hasChecksquareDetail = (item: SteadyDataView.SteadyChecksquareItem) => {
return (item.statDetails || []).some(detail => (detail.segments || []).length)
}
const hasChecksquareHarmonicOrderRange = (indicator: SteadyDataView.SteadyIndicatorNode) => {
const start = Number(indicator.harmonicOrderStart)
const end = Number(indicator.harmonicOrderEnd)
if (!Number.isFinite(start) || !Number.isFinite(end)) return false
const rangeStart = Math.min(start, end)
const rangeEnd = Math.max(start, end)
return rangeStart <= CHECKSQUARE_HARMONIC_ORDER_MAX && rangeEnd >= CHECKSQUARE_HARMONIC_ORDER_MIN
}
export const isChecksquareHarmonicIndicator = (indicator: SteadyDataView.SteadyIndicatorNode) => {
// 只有指标目录明确包含 2-50 次谐波范围时,才预建谐波子行并执行逐次合并。
return hasChecksquareHarmonicOrderRange(indicator)
}
export const buildPendingChecksquareItem = (
indicator: SteadyDataView.SteadyIndicatorNode
): SteadyDataView.SteadyChecksquareItem => {
const indicatorCode = indicator.indicatorCode || indicator.id || indicator.treeKey || indicator.name
return {
itemKey: `pending|${indicatorCode}`,
indicatorCode,
indicatorName: indicator.name || indicatorCode,
children: isChecksquareHarmonicIndicator(indicator) ? buildPendingChecksquareHarmonicItems(indicator) : undefined,
statSummaries: [],
statDetails: []
}
}
export const buildPendingChecksquareHarmonicItems = (
indicator: SteadyDataView.SteadyIndicatorNode
): SteadyDataView.SteadyChecksquareItem[] => {
const indicatorCode = indicator.indicatorCode || indicator.id || indicator.treeKey || indicator.name
return CHECKSQUARE_HARMONIC_ORDERS.map(harmonicOrder => ({
itemKey: `pending|${indicatorCode}|${harmonicOrder}`,
indicatorCode,
indicatorName: indicator.name || indicatorCode,
harmonicOrder,
statSummaries: [],
statDetails: []
}))
}
export const buildPendingChecksquareResult = (
indicators: SteadyDataView.SteadyIndicatorNode[],
formState: { timeRange: string[] }
): SteadyDataView.SteadyChecksquareQueryResult => {
return {
lineId: '',
timeStart: formState.timeRange[0] || '',
timeEnd: formState.timeRange[1] || '',
items: indicators.map(buildPendingChecksquareItem)
}
}
const isResolvedChecksquareItem = (item: SteadyDataView.SteadyChecksquareItem) => {
return item.hasData !== undefined || item.expectedPointCount !== undefined || item.actualPointCount !== undefined
}
const normalizeChecksquareResultItemKey = (
item: SteadyDataView.SteadyChecksquareItem,
itemKey: string
): SteadyDataView.SteadyChecksquareItem => ({
...item,
itemKey
})
const isValidChecksquareHarmonicOrder = (value: number) => {
return Number.isInteger(value) && CHECKSQUARE_HARMONIC_ORDERS.includes(value)
}
const parseChecksquareHarmonicOrder = (value?: string | number | null) => {
const order = Number(value)
return isValidChecksquareHarmonicOrder(order) ? order : null
}
const parseChecksquareHarmonicOrderFromText = (value?: string | null) => {
if (!value) return null
const orderText = value.match(/(?:^|[^\d])([2-9]|[1-4]\d|50)(?:次|[^\d]|$)/)?.[1]
return parseChecksquareHarmonicOrder(orderText)
}
const resolveChecksquareHarmonicOrder = (item: SteadyDataView.SteadyChecksquareItem) => {
return (
parseChecksquareHarmonicOrder(item.harmonicOrder) ||
parseChecksquareHarmonicOrderFromText(item.itemKey) ||
parseChecksquareHarmonicOrderFromText(item.indicatorName) ||
parseChecksquareHarmonicOrderFromText(item.indicatorCode)
)
}
const sumNumber = (
items: SteadyDataView.SteadyChecksquareItem[],
getter: (item: SteadyDataView.SteadyChecksquareItem) => unknown
) => {
return items.reduce((total, item) => {
const value = Number(getter(item))
return Number.isFinite(value) ? total + value : total
}, 0)
}
const summarizeStatType = (
items: SteadyDataView.SteadyChecksquareItem[],
statType: SteadyDataView.SteadyTrendStatType
): SteadyDataView.SteadyChecksquareStatSummary => {
const summaries = items
.map(item => findStatSummary(item, statType))
.filter((summary): summary is SteadyDataView.SteadyChecksquareStatSummary => Boolean(summary))
const supportedSummaries = summaries.filter(summary => summary.supported !== false)
const expectedPointCount = supportedSummaries.reduce((total, summary) => total + (summary.expectedPointCount || 0), 0)
const actualPointCount = supportedSummaries.reduce((total, summary) => total + (summary.actualPointCount || 0), 0)
const missingPointCount = supportedSummaries.reduce((total, summary) => total + (summary.missingPointCount || 0), 0)
const maxContinuousMissingMinutes = Math.max(
0,
...supportedSummaries.map(summary => summary.maxContinuousMissingMinutes || 0)
)
return {
statType,
supported: supportedSummaries.length > 0,
hasData: supportedSummaries.length > 0 && supportedSummaries.every(summary => summary.hasData === true),
expectedPointCount,
actualPointCount,
missingPointCount,
missingRate: expectedPointCount ? missingPointCount / expectedPointCount : null,
maxContinuousMissingMinutes
}
}
export const buildHarmonicParentSummary = (
parentItem: SteadyDataView.SteadyChecksquareItem,
children: SteadyDataView.SteadyChecksquareItem[]
): SteadyDataView.SteadyChecksquareItem => {
if (!children.length || !children.every(item => isResolvedChecksquareItem(item))) {
return {
itemKey: parentItem.itemKey,
indicatorCode: parentItem.indicatorCode,
indicatorName: parentItem.indicatorName,
statSummaries: [],
statDetails: [],
children
}
}
const expectedPointCount = sumNumber(children, item => item.expectedPointCount)
const actualPointCount = sumNumber(children, item => item.actualPointCount)
const missingPointCount = sumNumber(children, item => item.missingPointCount)
const maxContinuousMissingMinutes = Math.max(0, ...children.map(item => item.maxContinuousMissingMinutes || 0))
const statSummaries = CHECKSQUARE_STAT_TYPES.map(statType => summarizeStatType(children, statType))
return {
...parentItem,
hasData: children.every(item => item.hasData === true),
expectedPointCount,
actualPointCount,
missingPointCount,
missingRate: expectedPointCount ? missingPointCount / expectedPointCount : null,
missingRateText: expectedPointCount ? undefined : '-',
maxContinuousMissingMinutes,
statSummaries,
statDetails: [],
children
}
}
export const mergeChecksquareIndicatorResult = (
currentResult: SteadyDataView.SteadyChecksquareQueryResult | null,
indicator: SteadyDataView.SteadyIndicatorNode,
indicatorResult: SteadyDataView.SteadyChecksquareQueryResult
): SteadyDataView.SteadyChecksquareQueryResult => {
const pendingResult = currentResult || {
lineId: indicatorResult.lineId || '',
lineName: indicatorResult.lineName,
timeStart: indicatorResult.timeStart || '',
timeEnd: indicatorResult.timeEnd || '',
intervalMinutes: indicatorResult.intervalMinutes,
items: [buildPendingChecksquareItem(indicator)]
}
const indicatorCode = indicator.indicatorCode
const resultItems = indicatorResult.items || []
const shouldMergeHarmonicItems = isChecksquareHarmonicIndicator(indicator)
const normalItems = shouldMergeHarmonicItems
? resultItems.filter(item => !resolveChecksquareHarmonicOrder(item))
: resultItems
const harmonicItems = shouldMergeHarmonicItems
? resultItems.filter(item => resolveChecksquareHarmonicOrder(item))
: []
const currentItem = pendingResult.items.find(item => item.indicatorCode === indicatorCode)
const mergedItem = normalItems[0] || currentItem || buildPendingChecksquareItem(indicator)
const currentChildren = currentItem?.children || mergedItem.children || []
const mergedChildren = currentChildren.length
? currentChildren.map(child => {
const replacement = harmonicItems.find(item => resolveChecksquareHarmonicOrder(item) === child.harmonicOrder)
return replacement
? normalizeChecksquareResultItemKey(
{
...replacement,
harmonicOrder: child.harmonicOrder
},
child.itemKey
)
: child
})
: harmonicItems
const replacement = {
...mergedItem,
itemKey: currentItem?.itemKey || mergedItem.itemKey,
indicatorName: indicator.name || mergedItem.indicatorName || mergedItem.indicatorCode,
children: mergedChildren.length ? mergedChildren : undefined
} as SteadyDataView.SteadyChecksquareItem
const finalReplacement = replacement.children?.length ? buildHarmonicParentSummary(replacement, replacement.children) : replacement
return {
...pendingResult,
lineId: indicatorResult.lineId || pendingResult.lineId,
lineName: indicatorResult.lineName || pendingResult.lineName,
timeStart: indicatorResult.timeStart || pendingResult.timeStart,
timeEnd: indicatorResult.timeEnd || pendingResult.timeEnd,
intervalMinutes: indicatorResult.intervalMinutes ?? pendingResult.intervalMinutes,
items: pendingResult.items.map(item => (item.indicatorCode === indicatorCode ? finalReplacement : item))
}
}