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

@@ -45,6 +45,11 @@ assert.deepEqual(buildTimePeriodRange('day', new Date(2026, 4, 13)), [
'2026-05-13 23:59:59.999'
])
assert.deepEqual(buildTimePeriodRange('week', new Date(2026, 4, 13)), [
'2026-05-11 00:00:00.000',
'2026-05-17 23:59:59.999'
])
assert.deepEqual(buildTimePeriodRange('month', new Date(2026, 4, 13)), [
'2026-05-01 00:00:00.000',
'2026-05-31 23:59:59.999'
@@ -60,17 +65,25 @@ assert.deepEqual(buildTimePeriodRange('month', shiftTimePeriod('month', new Date
'2026-04-30 23:59:59.999'
])
assert.deepEqual(buildTimePeriodRange('week', shiftTimePeriod('week', new Date(2026, 4, 13), -1)), [
'2026-05-04 00:00:00.000',
'2026-05-10 23:59:59.999'
])
assert.equal(formatTimePeriodDateTime(new Date(2026, 4, 13, 8, 9, 10, 11)), '2026-05-13 08:09:10.011')
assert.equal(getTimePeriodPickerType('day'), 'date')
assert.equal(getTimePeriodPickerFormat('month'), 'YYYY-MM')
assert.equal(resolveTimePeriodUnitLabel('year'), '年')
assert.equal(resolveTimePeriodUnitLabel('custom'), '自定义')
const componentExpectations = [
['component renders unit selector', /time-period-search__unit[\s\S]*timePeriodUnitOptions/],
['component renders unit selector', /time-period-search__unit[\s\S]*visibleTimePeriodUnitOptions/],
['component renders previous period button', /ArrowLeft[\s\S]*上一个/],
['component renders current period button', /Clock[\s\S]*当前/],
['component renders next period button', /ArrowRight[\s\S]*下一个/],
['component renders date picker by selected unit', /getTimePeriodPickerType\(props\.unit\)/],
['component supports custom datetime range picker', /type="datetimerange"[\s\S]*handleRangeChange/],
['component can limit visible units by props', /visibleUnits\?:\s*TimePeriodUnit\[\][\s\S]*visibleTimePeriodUnitOptions/],
['component uses fixed eventList-compatible picker width', /time-period-search__picker[\s\S]*width:\s*112px;[\s\S]*flex:\s*0 0 112px;/]
]

View File

@@ -1,10 +1,16 @@
<template>
<div class="time-period-search">
<el-select class="time-period-search__unit" :model-value="unit" @update:model-value="handleUnitChange">
<el-option v-for="item in timePeriodUnitOptions" :key="item.value" :label="item.label" :value="item.value" />
<el-option
v-for="item in visibleTimePeriodUnitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-button
v-if="!isCustomUnit"
class="time-period-search__button"
:icon="ArrowLeft"
:title="`上一个${unitLabel}`"
@@ -12,6 +18,7 @@
/>
<el-date-picker
v-if="!isCustomUnit"
class="time-period-search__picker"
:model-value="baseDate"
:type="getTimePeriodPickerType(props.unit)"
@@ -22,7 +29,23 @@
@update:model-value="handleDateChange"
/>
<el-date-picker
v-else
class="time-period-search__range-picker"
:model-value="rangeValue"
type="datetimerange"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss.SSS"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
:clearable="false"
:editable="false"
@update:model-value="handleRangeChange"
/>
<el-button
v-if="!isCustomUnit"
class="time-period-search__button"
:icon="ArrowRight"
:title="`下一个${unitLabel}`"
@@ -30,6 +53,7 @@
/>
<el-button
v-if="!isCustomUnit"
class="time-period-search__button"
:icon="Clock"
:title="`当前${unitLabel}`"
@@ -57,15 +81,25 @@ defineOptions({
const props = defineProps<{
unit: TimePeriodUnit
modelValue: Date | string | number
rangeValue?: string[]
visibleUnits?: TimePeriodUnit[]
}>()
const emit = defineEmits<{
'update:unit': [value: TimePeriodUnit]
'update:modelValue': [value: Date]
'update:rangeValue': [value: string[]]
}>()
const baseDate = computed(() => new Date(props.modelValue))
const unitLabel = computed(() => resolveTimePeriodUnitLabel(props.unit))
const isCustomUnit = computed(() => props.unit === 'custom')
const visibleTimePeriodUnitOptions = computed(() => {
if (!props.visibleUnits?.length) return timePeriodUnitOptions
return timePeriodUnitOptions.filter(item => props.visibleUnits?.includes(item.value))
})
const rangeValue = computed(() => props.rangeValue || [])
const handleUnitChange = (value: TimePeriodUnit) => {
emit('update:unit', value)
@@ -76,6 +110,11 @@ const handleDateChange = (value: Date | string | number | null) => {
emit('update:modelValue', new Date(value))
}
const handleRangeChange = (value: string[] | null) => {
if (!value?.length) return
emit('update:rangeValue', value)
}
const shiftPeriod = (offset: number) => {
emit('update:modelValue', shiftTimePeriod(props.unit, baseDate.value, offset))
}
@@ -103,6 +142,12 @@ const setCurrentPeriod = () => {
flex: 0 0 112px;
}
.time-period-search__range-picker {
width: 360px;
flex: 1 1 360px;
min-width: 280px;
}
.time-period-search__button {
width: 28px;
flex: 0 0 28px;

View File

@@ -1,21 +1,27 @@
export type TimePeriodUnit = 'day' | 'month' | 'year'
export type TimePeriodUnit = 'day' | 'week' | 'month' | 'year' | 'custom'
export const timePeriodUnitOptions: { label: string; value: TimePeriodUnit }[] = [
{ label: '日', value: 'day' },
{ label: '周', value: 'week' },
{ label: '月', value: 'month' },
{ label: '年', value: 'year' }
{ label: '年', value: 'year' },
{ label: '自定义', value: 'custom' }
]
const datePickerTypeMap: Record<TimePeriodUnit, 'date' | 'month' | 'year'> = {
day: 'date',
week: 'date',
month: 'month',
year: 'year'
year: 'year',
custom: 'date'
}
const datePickerFormatMap: Record<TimePeriodUnit, string> = {
day: 'YYYY-MM-DD',
week: 'YYYY-MM-DD',
month: 'YYYY-MM',
year: 'YYYY'
year: 'YYYY',
custom: 'YYYY-MM-DD'
}
const padTimeValue = (value: number, length = 2) => String(value).padStart(length, '0')
@@ -52,6 +58,23 @@ export const buildTimePeriodRange = (unit: TimePeriodUnit, date: Date): string[]
]
}
if (unit === 'week') {
const dayOfWeek = date.getDay()
const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek
const weekStart = new Date(year, month, day + mondayOffset, 0, 0, 0, 0)
const weekEnd = new Date(
weekStart.getFullYear(),
weekStart.getMonth(),
weekStart.getDate() + 6,
23,
59,
59,
999
)
return [formatTimePeriodDateTime(weekStart), formatTimePeriodDateTime(weekEnd)]
}
if (unit === 'year') {
return [
formatTimePeriodDateTime(new Date(year, 0, 1, 0, 0, 0, 0)),
@@ -78,6 +101,11 @@ export const shiftTimePeriod = (unit: TimePeriodUnit, date: Date, offset: number
return nextDate
}
if (unit === 'week') {
nextDate.setDate(nextDate.getDate() + offset * 7)
return nextDate
}
// 月份切换以 1 日为锚点,避免 31 日切到短月份时发生日期溢出。
nextDate.setDate(1)
nextDate.setMonth(nextDate.getMonth() + offset)