refactor(event): 重构事件列表和稳态数据视图组件结构

- 将事件列表页面逻辑拆分为 EventListTable 组件
- 新增 MeasurementPointDialog 和 VoltageToleranceDialog 弹窗组件
- 重构稳态数据视图为主工作台组件 SteadyTrendWorkbench
- 移除不再使用的相别参数和相关逻辑
- 更新事件详情工具函数和接口参数映射
- 优化波形查看功能的数据传递方式
- 修正事件描述字段命名和严重程度解析逻辑
This commit is contained in:
2026-05-18 08:46:42 +08:00
parent 609fdd5379
commit f9ed6c6245
39 changed files with 1943 additions and 755 deletions

View File

@@ -0,0 +1,115 @@
<template>
<aside class="indicator-floating-panel" :class="{ 'is-collapsed': collapsed }">
<el-button
class="indicator-toggle"
:icon="collapsed ? ArrowLeft : ArrowRight"
circle
@click="emit('update:collapsed', !collapsed)"
/>
<button
v-if="collapsed"
class="indicator-collapsed-trigger"
type="button"
@click="emit('update:collapsed', false)"
>
{{ collapsedLabel }}
</button>
<div v-show="!collapsed" class="indicator-panel-body">
<SteadyIndicatorTree
:key="selectorResetKey"
:tree-data="treeData"
:loading="loading"
@refresh="emit('refresh')"
@change="emit('change', $event)"
/>
</div>
</aside>
</template>
<script setup lang="ts">
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
import SteadyIndicatorTree from './SteadyIndicatorTree.vue'
defineOptions({
name: 'SteadyIndicatorFloatingPanel'
})
defineProps<{
collapsed: boolean
treeData: SteadyDataView.SteadyIndicatorNode[]
loading: boolean
selectorResetKey: number
}>()
const emit = defineEmits<{
'update:collapsed': [value: boolean]
refresh: []
change: [nodes: SteadyDataView.SteadyIndicatorNode[]]
}>()
const collapsedLabel = '\u7a33\u6001\u6307\u6807'
</script>
<style scoped lang="scss">
.indicator-floating-panel {
position: absolute;
top: 12px;
right: 12px;
bottom: 12px;
z-index: 2;
width: 360px;
transition: width 0.2s ease;
}
.indicator-floating-panel.is-collapsed {
width: 44px;
}
.indicator-toggle {
position: absolute;
top: 12px;
left: -18px;
z-index: 3;
}
.indicator-panel-body {
width: 100%;
height: 100%;
}
.indicator-panel-body :deep(.steady-tree-card) {
height: 100%;
box-shadow: var(--el-box-shadow-light);
}
.indicator-collapsed-trigger {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 8px 0;
border: 1px solid var(--el-border-color-light);
border-radius: 4px;
background: var(--el-bg-color);
box-shadow: var(--el-box-shadow-light);
color: var(--el-text-color-primary);
cursor: pointer;
font-size: 13px;
font-weight: 600;
line-height: 1.2;
writing-mode: vertical-rl;
}
.indicator-collapsed-trigger:hover {
border-color: var(--el-color-primary-light-5);
color: var(--el-color-primary);
}
@media (max-width: 1360px) {
.indicator-floating-panel {
width: 320px;
}
}
</style>

View File

@@ -67,7 +67,7 @@ const normalizedTreeData = computed(() => {
})
const handleCheck = () => {
const checkedNodes = (treeRef.value?.getCheckedNodes(false, true) || []) as SteadyDataView.SteadyIndicatorNode[]
const checkedNodes = (treeRef.value?.getCheckedNodes(false, false) || []) as SteadyDataView.SteadyIndicatorNode[]
emit('change', collectLeafIndicators(checkedNodes))
}
</script>

View File

@@ -68,7 +68,7 @@ const handleKeywordChange = (value: string) => {
}
const handleCheck = () => {
emit('change', (treeRef.value?.getCheckedNodes(false, true) || []) as SteadyDataView.SteadyLedgerNode[])
emit('change', (treeRef.value?.getCheckedNodes(false, false) || []) as SteadyDataView.SteadyLedgerNode[])
}
</script>

View File

@@ -1,65 +1,67 @@
<template>
<section class="card trend-toolbar">
<TimePeriodSearch
class="trend-toolbar__time"
:unit="modelValue.timeUnit"
:model-value="modelValue.timeBaseDate"
@update:unit="handleTimeUnitChange"
@update:model-value="handleTimeBaseDateChange"
/>
<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>
<el-select
:model-value="modelValue.phases"
multiple
collapse-tags
collapse-tags-tooltip
placeholder="选择相别"
@update:model-value="updateField('phases', $event)"
>
<el-option v-for="item in phaseOptions" :key="item" :label="resolvePhaseLabel(item)" :value="item" />
</el-select>
<div class="toolbar-field">
<span class="toolbar-field__label">统计</span>
<el-select
:model-value="modelValue.statTypes"
multiple
collapse-tags
collapse-tags-tooltip
placeholder="选择统计类型"
@update:model-value="updateField('statTypes', $event)"
>
<el-option v-for="item in statOptions" :key="item" :label="statLabelMap[item]" :value="item" />
</el-select>
</div>
<el-select
:model-value="modelValue.statTypes"
multiple
collapse-tags
collapse-tags-tooltip
placeholder="选择统计类型"
@update:model-value="updateField('statTypes', $event)"
>
<el-option v-for="item in statOptions" :key="item" :label="statLabelMap[item]" :value="item" />
</el-select>
<div class="toolbar-field">
<span class="toolbar-field__label">粒度</span>
<el-select
:model-value="modelValue.bucket"
placeholder="选择时间粒度"
@update:model-value="updateField('bucket', $event)"
>
<el-option v-for="item in bucketOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<el-select
:model-value="modelValue.bucket"
placeholder="选择时间粒度"
@update:model-value="updateField('bucket', $event)"
>
<el-option v-for="item in bucketOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<div class="toolbar-field">
<span class="toolbar-field__label">数据</span>
<el-select
:model-value="modelValue.qualityFlag"
clearable
placeholder="选择数据质量"
@update:model-value="updateField('qualityFlag', $event)"
>
<el-option label="仅有效数据" :value="1" />
<el-option label="仅无效数据" :value="0" />
</el-select>
</div>
<el-select
:model-value="modelValue.qualityFlag"
clearable
placeholder="选择数据质量"
@update:model-value="updateField('qualityFlag', $event)"
>
<el-option label="仅有效数据" :value="1" />
<el-option label="仅无效数据" :value="0" />
</el-select>
<el-select
v-if="showHarmonicOrders"
:model-value="modelValue.harmonicOrders"
class="harmonic-select"
multiple
collapse-tags
collapse-tags-tooltip
placeholder="选择谐波次数"
@update:model-value="updateField('harmonicOrders', $event)"
>
<el-option v-for="item in harmonicOrderOptions" :key="item" :label="`${item}次`" :value="item" />
</el-select>
<div v-if="showHarmonicOrders" class="toolbar-field harmonic-select">
<span class="toolbar-field__label">谐波次数</span>
<el-select
:model-value="modelValue.harmonicOrders"
multiple
collapse-tags
collapse-tags-tooltip
placeholder="选择谐波次数"
@update:model-value="updateField('harmonicOrders', $event)"
>
<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>
@@ -80,7 +82,6 @@ defineOptions({
const props = defineProps<{
modelValue: SteadyTrendFormState
phaseOptions: string[]
statOptions: SteadyDataView.SteadyTrendStatType[]
showHarmonicOrders: boolean
loading: boolean
@@ -106,14 +107,6 @@ const statLabelMap: Record<SteadyDataView.SteadyTrendStatType, string> = {
MIN: '最小值',
CP95: '95%概率大值'
}
const phaseLabelMap: Record<string, string> = {
A: 'A相',
B: 'B相',
C: 'C相',
T: '总相'
}
const resolvePhaseLabel = (phase: string) => phaseLabelMap[phase] || `${phase}`
const updateField = <K extends keyof SteadyTrendFormState>(field: K, value: SteadyTrendFormState[K]) => {
emit('update:modelValue', {
@@ -143,13 +136,37 @@ const handleTimeBaseDateChange = (value: Date) => {
<style scoped lang="scss">
.trend-toolbar {
display: grid;
grid-template-columns: minmax(260px, 1.3fr) repeat(4, minmax(132px, 0.7fr)) auto;
grid-template-columns: minmax(312px, 1.4fr) repeat(3, minmax(178px, 0.8fr)) 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: 312px;
}
.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;
}
.trend-toolbar__time {
flex: 1 1 0;
min-width: 260px;
}

View File

@@ -0,0 +1,131 @@
<template>
<div class="steady-trend-layout">
<aside class="selector-column">
<SteadyLedgerTree
:key="selectorResetKey"
:tree-data="ledgerTree"
:loading="loading.ledger"
:keyword="ledgerKeyword"
@refresh="emit('refreshLedger')"
@search="emit('ledgerSearch', $event)"
@change="emit('ledgerChange', $event)"
/>
</aside>
<main class="trend-main">
<SteadyTrendToolbar
v-model="trendFormProxy"
:stat-options="statOptions"
:show-harmonic-orders="showHarmonicOrders"
:loading="loading.trend"
@query="emit('queryTrend')"
@reset="emit('resetTrend')"
/>
<div class="trend-content">
<SteadyTrendChartPanel :trend-result="trendResult" :loading="loading.trend" />
<SteadyIndicatorFloatingPanel
v-model:collapsed="indicatorPanelCollapsedProxy"
:selector-reset-key="selectorResetKey"
:tree-data="indicatorTree"
:loading="loading.indicator"
@refresh="emit('refreshIndicator')"
@change="emit('indicatorChange', $event)"
/>
</div>
</main>
</div>
</template>
<script setup lang="ts">
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
import type { SteadyTrendFormState } from '../utils/trendPayload'
import SteadyIndicatorFloatingPanel from './SteadyIndicatorFloatingPanel.vue'
import SteadyLedgerTree from './SteadyLedgerTree.vue'
import SteadyTrendChartPanel from './SteadyTrendChartPanel.vue'
import SteadyTrendToolbar from './SteadyTrendToolbar.vue'
defineOptions({
name: 'SteadyTrendWorkbench'
})
const props = defineProps<{
ledgerTree: SteadyDataView.SteadyLedgerNode[]
indicatorTree: SteadyDataView.SteadyIndicatorNode[]
trendResult: SteadyDataView.SteadyTrendQueryResult | null
trendForm: SteadyTrendFormState
statOptions: SteadyDataView.SteadyTrendStatType[]
showHarmonicOrders: boolean
loading: {
ledger: boolean
indicator: boolean
trend: boolean
}
ledgerKeyword: string
indicatorPanelCollapsed: boolean
selectorResetKey: number
}>()
const emit = defineEmits<{
'update:trendForm': [value: SteadyTrendFormState]
'update:indicatorPanelCollapsed': [value: boolean]
refreshLedger: []
ledgerSearch: [value: string]
ledgerChange: [nodes: SteadyDataView.SteadyLedgerNode[]]
refreshIndicator: []
indicatorChange: [nodes: SteadyDataView.SteadyIndicatorNode[]]
queryTrend: []
resetTrend: []
}>()
const trendFormProxy = computed({
get: () => props.trendForm,
set: value => emit('update:trendForm', value)
})
const indicatorPanelCollapsedProxy = computed({
get: () => props.indicatorPanelCollapsed,
set: value => emit('update:indicatorPanelCollapsed', value)
})
</script>
<style scoped lang="scss">
.steady-trend-layout {
display: grid;
grid-template-columns: 320px minmax(0, 1fr);
gap: 12px;
width: 100%;
height: 100%;
min-height: 0;
}
.selector-column {
display: grid;
min-height: 0;
}
.trend-main {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 12px;
min-width: 0;
min-height: 0;
}
.trend-content {
position: relative;
min-width: 0;
min-height: 0;
}
.trend-content :deep(.trend-chart-panel) {
height: 100%;
}
@media (max-width: 1360px) {
.steady-trend-layout {
grid-template-columns: 280px minmax(0, 1fr);
}
}
</style>

View File

@@ -4,7 +4,7 @@ import path from 'node:path'
import { fileURLToPath } from 'node:url'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const srcRoot = path.resolve(currentDir, '../../..')
const srcRoot = path.resolve(currentDir, '../../../..')
const staticRouter = fs.readFileSync(path.join(srcRoot, 'routers/modules/staticRouter.ts'), 'utf8')
const dynamicRouter = fs.readFileSync(path.join(srcRoot, 'routers/modules/dynamicRouter.ts'), 'utf8')
const authStore = fs.readFileSync(path.join(srcRoot, 'stores/modules/auth.ts'), 'utf8')

View File

@@ -0,0 +1,34 @@
/* eslint-env node */
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const componentDir = path.join(currentDir, '..', 'components')
const read = file => fs.readFileSync(path.join(componentDir, file), 'utf8')
const expectations = [
[
'ledger tree excludes half-checked parents when collecting checked nodes',
/getCheckedNodes\(\s*false\s*,\s*false\s*\)/,
read('SteadyLedgerTree.vue')
],
[
'indicator tree excludes half-checked parents when collecting checked nodes',
/getCheckedNodes\(\s*false\s*,\s*false\s*\)/,
read('SteadyIndicatorTree.vue')
]
]
const failures = expectations.filter(([, pattern, source]) => !pattern.test(source))
if (failures.length) {
console.error('steadyDataView selection contract failed:')
for (const [name] of failures) {
console.error(`- ${name}`)
}
process.exit(1)
}
console.log('steadyDataView selection contract passed')

View File

@@ -4,11 +4,11 @@ import path from 'node:path'
import { fileURLToPath } from 'node:url'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const pageFile = path.join(currentDir, 'index.vue')
const apiFile = path.resolve(currentDir, '../../../api/steady/steadyDataView/index.ts')
const interfaceFile = path.resolve(currentDir, '../../../api/steady/steadyDataView/interface/index.ts')
const componentDir = path.join(currentDir, 'components')
const utilsDir = path.join(currentDir, 'utils')
const pageFile = path.join(currentDir, '..', 'index.vue')
const apiFile = path.resolve(currentDir, '../../../../api/steady/steadyDataView/index.ts')
const interfaceFile = path.resolve(currentDir, '../../../../api/steady/steadyDataView/interface/index.ts')
const componentDir = path.join(currentDir, '..', 'components')
const utilsDir = path.join(currentDir, '..', 'utils')
const read = file => fs.readFileSync(file, 'utf8')
const pageSource = read(pageFile)
@@ -30,13 +30,16 @@ const utilitySource = fs.existsSync(utilsDir)
: ''
const expectations = [
['page imports ledger tree panel', /SteadyLedgerTree/],
['page imports indicator tree panel', /SteadyIndicatorTree/],
['page imports trend toolbar', /SteadyTrendToolbar/],
['page imports trend chart panel', /SteadyTrendChartPanel/],
['page imports extracted trend workbench', /SteadyTrendWorkbench/],
['trend workbench component exists', fs.existsSync(path.join(componentDir, 'SteadyTrendWorkbench.vue'))],
['floating indicator panel component exists', fs.existsSync(path.join(componentDir, 'SteadyIndicatorFloatingPanel.vue'))],
['components import ledger tree panel', /SteadyLedgerTree/],
['components import indicator tree panel', /SteadyIndicatorTree/],
['components import trend toolbar', /SteadyTrendToolbar/],
['components import trend chart panel', /SteadyTrendChartPanel/],
['page does not import trend summary panel', /SteadyTrendSummaryPanel/],
['page does not import data table panel', /SteadyDataTablePanel/],
['page renders floating indicator panel', /indicator-floating-panel/],
['components render floating indicator panel', /indicator-floating-panel/],
['page defaults floating indicator panel expanded', /indicatorPanelCollapsed\s*=\s*ref\(false\)/],
['API exposes ledger tree endpoint', /\/steady\/data-view\/ledger-tree/],
['API exposes indicator tree endpoint', /\/steady\/data-view\/indicator-tree/],
@@ -50,24 +53,33 @@ const expectations = [
['components render indicator checkbox tree', /indicator-tree[\s\S]*show-checkbox[\s\S]*@check/],
['components reuse LineChart', /<LineChart/],
['toolbar uses shared time period search', /TimePeriodSearch/],
['toolbar labels phase options descriptively', /resolvePhaseLabel/],
['toolbar labels stat bucket quality filters', /toolbar-field__label[\s\S]*统计:[\s\S]*toolbar-field__label[\s\S]*粒度:[\s\S]*toolbar-field__label[\s\S]*数据:/],
['toolbar does not render phase selector', /modelValue\.phases|phaseOptions|resolvePhaseLabel/],
['toolbar labels bucket options descriptively', /bucketOptions[\s\S]*1分钟[\s\S]*1小时/],
['toolbar labels quality options descriptively', /仅有效数据[\s\S]*仅无效数据/],
['utilities collect selected line ids', /export const collectSelectedLineIds/],
['utilities validate selection limits', /export const validateTrendSelection[\s\S]*24/],
['utilities do not require phase selection', /if\s*\(!phases\.length\)/],
['utilities validate harmonic orders', /export const validateHarmonicOrders[\s\S]*6/],
['utilities build trend query payload', /export const buildSteadyTrendQueryPayload/],
['utilities strip milliseconds from trend query time', /formatSteadyTrendQueryTime[\s\S]*replace\(\s*\/\\\.\[\^.\]\+\$\//],
['utilities do not send phases in trend query payload', /phases:\s*formState\.phases/],
['trend query params do not include phases', /phases:\s*string\[\]/],
['utilities build chart options', /export const buildSteadyTrendChartOptions/]
]
const sourceByExpectation = [
pageSource,
componentSource,
componentSource,
componentSource,
componentSource,
componentSource,
componentSource,
componentSource,
pageSource,
pageSource,
pageSource,
pageSource,
pageSource,
pageSource,
componentSource,
pageSource,
apiSource,
apiSource,
@@ -88,11 +100,15 @@ const sourceByExpectation = [
utilitySource,
utilitySource,
utilitySource,
utilitySource,
utilitySource,
utilitySource,
interfaceSource,
utilitySource
]
const failures = expectations.filter(([name, pattern], index) => {
const matched = pattern.test(sourceByExpectation[index])
const matched = typeof pattern === 'boolean' ? pattern : pattern.test(sourceByExpectation[index])
return name.includes('does not') || name.includes('do not') ? matched : !matched
})

View File

@@ -4,11 +4,20 @@ import path from 'node:path'
import { fileURLToPath } from 'node:url'
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const pageFile = path.join(currentDir, 'index.vue')
const apiFile = path.resolve(currentDir, '../../../api/steady/steadyDataView/index.ts')
const interfaceFile = path.resolve(currentDir, '../../../api/steady/steadyDataView/interface/index.ts')
const pageFile = path.join(currentDir, '..', 'index.vue')
const componentDir = path.join(currentDir, '..', 'components')
const apiFile = path.resolve(currentDir, '../../../../api/steady/steadyDataView/index.ts')
const interfaceFile = path.resolve(currentDir, '../../../../api/steady/steadyDataView/interface/index.ts')
const source = fs.readFileSync(pageFile, 'utf8')
const componentSource = fs.existsSync(componentDir)
? fs
.readdirSync(componentDir)
.filter(file => file.endsWith('.vue'))
.map(file => fs.readFileSync(path.join(componentDir, file), 'utf8'))
.join('\n')
: ''
const viewSource = `${source}\n${componentSource}`
const apiSource = fs.readFileSync(apiFile, 'utf8')
const interfaceSource = fs.readFileSync(interfaceFile, 'utf8')
@@ -36,10 +45,13 @@ const forbiddenPatterns = [
const requiredPatterns = [
['page defines SteadyDataView component name', /name:\s*'SteadyDataView'/, source],
['page keeps trend chart panel', /SteadyTrendChartPanel/, source],
['page keeps right floating indicator panel', /indicator-floating-panel/, source],
['page renders extracted trend workbench', /<SteadyTrendWorkbench/, source],
['trend workbench component exists', /SteadyTrendWorkbench/, viewSource],
['floating indicator panel component exists', /SteadyIndicatorFloatingPanel/, viewSource],
['components keep trend chart panel', /SteadyTrendChartPanel/, viewSource],
['components keep right floating indicator panel', /indicator-floating-panel/, viewSource],
['indicator panel defaults expanded', /indicatorPanelCollapsed\s*=\s*ref\(false\)/, source],
['indicator panel supports collapsed state', /is-collapsed/, source],
['indicator panel supports collapsed state', /is-collapsed/, viewSource],
['API keeps trend query endpoint', /\/steady\/data-view\/trend\/query/, apiSource]
]

View File

@@ -1,77 +1,36 @@
<template>
<div class="table-box steady-data-view-page">
<div class="steady-trend-layout">
<aside class="selector-column">
<SteadyLedgerTree
:key="selectorResetKey"
:tree-data="ledgerTree"
:loading="loading.ledger"
:keyword="ledgerKeyword"
@refresh="loadLedgerTree"
@search="handleLedgerSearch"
@change="handleLedgerChange"
/>
</aside>
<main class="trend-main">
<SteadyTrendToolbar
v-model="trendForm"
:phase-options="phaseOptions"
:stat-options="statOptions"
:show-harmonic-orders="showHarmonicOrders"
:loading="loading.trend"
@query="handleQueryTrend"
@reset="resetTrendState"
/>
<div class="trend-content">
<SteadyTrendChartPanel :trend-result="trendResult" :loading="loading.trend" />
<aside class="indicator-floating-panel" :class="{ 'is-collapsed': indicatorPanelCollapsed }">
<el-button
class="indicator-toggle"
:icon="indicatorPanelCollapsed ? ArrowLeft : ArrowRight"
circle
@click="indicatorPanelCollapsed = !indicatorPanelCollapsed"
/>
<button
v-if="indicatorPanelCollapsed"
class="indicator-collapsed-trigger"
type="button"
@click="indicatorPanelCollapsed = false"
>
稳态指标
</button>
<div v-show="!indicatorPanelCollapsed" class="indicator-panel-body">
<SteadyIndicatorTree
:key="selectorResetKey"
:tree-data="indicatorTree"
:loading="loading.indicator"
@refresh="loadIndicatorTree"
@change="handleIndicatorChange"
/>
</div>
</aside>
</div>
</main>
</div>
<SteadyTrendWorkbench
v-model:trend-form="trendForm"
v-model:indicator-panel-collapsed="indicatorPanelCollapsed"
:ledger-tree="ledgerTree"
:indicator-tree="indicatorTree"
:trend-result="trendResult"
:stat-options="statOptions"
:show-harmonic-orders="showHarmonicOrders"
:loading="loading"
:ledger-keyword="ledgerKeyword"
:selector-reset-key="selectorResetKey"
@refresh-ledger="loadLedgerTree"
@ledger-search="handleLedgerSearch"
@ledger-change="handleLedgerChange"
@refresh-indicator="loadIndicatorTree"
@indicator-change="handleIndicatorChange"
@query-trend="handleQueryTrend"
@reset-trend="resetTrendState"
/>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
import { getSteadyTrendIndicatorTree, getSteadyTrendLedgerTree, querySteadyTrend } from '@/api/steady/steadyDataView'
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
import SteadyIndicatorTree from './components/SteadyIndicatorTree.vue'
import SteadyLedgerTree from './components/SteadyLedgerTree.vue'
import SteadyTrendChartPanel from './components/SteadyTrendChartPanel.vue'
import SteadyTrendToolbar from './components/SteadyTrendToolbar.vue'
import SteadyTrendWorkbench from './components/SteadyTrendWorkbench.vue'
import {
collectSelectedLineIds,
hasHarmonicIndicator,
resolveAvailablePhases,
resolveAvailableStats,
validateTrendSelection
} from './utils/selectionRules'
@@ -98,10 +57,6 @@ const loading = reactive({
const lineIds = computed(() => collectSelectedLineIds(selectedLedgerNodes.value))
const showHarmonicOrders = computed(() => hasHarmonicIndicator(selectedIndicators.value))
const phaseOptions = computed(() => {
const phases = resolveAvailablePhases(selectedIndicators.value)
return phases.length ? phases : ['A', 'B', 'C', 'T']
})
const statOptions = computed<SteadyDataView.SteadyTrendStatType[]>(() => {
const stats = resolveAvailableStats(selectedIndicators.value)
return stats.length ? stats : ['AVG', 'MAX', 'MIN', 'CP95']
@@ -162,7 +117,6 @@ const handleQueryTrend = async () => {
const selectionError = validateTrendSelection({
lineIds: lineIds.value,
indicators: selectedIndicators.value,
phases: trendForm.value.phases,
statTypes: trendForm.value.statTypes,
harmonicOrders: trendForm.value.harmonicOrders
})
@@ -171,7 +125,7 @@ const handleQueryTrend = async () => {
return
}
if (!trendForm.value.timeRange[0] || !trendForm.value.timeRange[1]) {
ElMessage.warning('请选择趋势时间范围')
ElMessage.warning('\u8bf7\u9009\u62e9\u8d8b\u52bf\u65f6\u95f4\u8303\u56f4')
return
}
@@ -179,7 +133,7 @@ const handleQueryTrend = async () => {
loading.trend = true
try {
// 趋势查询只驱动主图,右侧稳态指标作为筛选面板独立加载,避免额外摘要请求拖慢页面响应。
// 趋势查询只驱动主图,右侧指标作为筛选面板独立加载,避免额外请求拖慢页面响应。
const trendResponse = await querySteadyTrend(payload)
trendResult.value = unwrapData(trendResponse)
} finally {
@@ -190,19 +144,14 @@ const handleQueryTrend = async () => {
watch(
selectedIndicators,
() => {
const availablePhases = phaseOptions.value
const availableStats = statOptions.value
trendForm.value = {
...trendForm.value,
phases: trendForm.value.phases.filter(phase => availablePhases.includes(phase)),
statTypes: trendForm.value.statTypes.filter(stat => availableStats.includes(stat)),
harmonicOrders: showHarmonicOrders.value ? trendForm.value.harmonicOrders : []
}
if (!trendForm.value.phases.length) {
trendForm.value.phases = availablePhases.slice(0, Math.min(3, availablePhases.length))
}
if (!trendForm.value.statTypes.length) {
trendForm.value.statTypes = availableStats.slice(0, 1)
}
@@ -221,101 +170,4 @@ onMounted(() => {
min-height: 0;
overflow: hidden;
}
.steady-trend-layout {
display: grid;
grid-template-columns: 320px minmax(0, 1fr);
gap: 12px;
width: 100%;
height: 100%;
min-height: 0;
}
.selector-column {
display: grid;
min-height: 0;
}
.trend-main {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 12px;
min-width: 0;
min-height: 0;
}
.trend-content {
position: relative;
min-width: 0;
min-height: 0;
}
.trend-content :deep(.trend-chart-panel) {
height: 100%;
}
.indicator-floating-panel {
position: absolute;
top: 12px;
right: 12px;
bottom: 12px;
z-index: 2;
width: 360px;
transition: width 0.2s ease;
}
.indicator-floating-panel.is-collapsed {
width: 44px;
}
.indicator-toggle {
position: absolute;
top: 12px;
left: -18px;
z-index: 3;
}
.indicator-panel-body {
width: 100%;
height: 100%;
}
.indicator-panel-body :deep(.steady-tree-card) {
height: 100%;
box-shadow: var(--el-box-shadow-light);
}
.indicator-collapsed-trigger {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 8px 0;
border: 1px solid var(--el-border-color-light);
border-radius: 4px;
background: var(--el-bg-color);
box-shadow: var(--el-box-shadow-light);
color: var(--el-text-color-primary);
cursor: pointer;
font-size: 13px;
font-weight: 600;
line-height: 1.2;
writing-mode: vertical-rl;
}
.indicator-collapsed-trigger:hover {
border-color: var(--el-color-primary-light-5);
color: var(--el-color-primary);
}
@media (max-width: 1360px) {
.steady-trend-layout {
grid-template-columns: 280px minmax(0, 1fr);
}
.indicator-floating-panel {
width: 320px;
}
}
</style>

View File

@@ -42,16 +42,6 @@ export const hasHarmonicIndicator = (indicators: SteadyDataView.SteadyIndicatorN
return indicators.some(item => item.harmonic || Boolean(item.harmonicOrderStart || item.harmonicOrderEnd))
}
export const resolveAvailablePhases = (indicators: SteadyDataView.SteadyIndicatorNode[]) => {
const phaseSet = new Set<string>()
indicators.forEach(indicator => {
indicator.phaseCodes?.forEach(phase => phaseSet.add(phase))
})
return Array.from(phaseSet)
}
export const resolveAvailableStats = (indicators: SteadyDataView.SteadyIndicatorNode[]) => {
const statSet = new Set<SteadyDataView.SteadyTrendStatType>()
@@ -65,18 +55,16 @@ export const resolveAvailableStats = (indicators: SteadyDataView.SteadyIndicator
export const estimateTrendSeriesCount = (
lineIds: string[],
indicators: SteadyDataView.SteadyIndicatorNode[],
phases: string[],
statTypes: SteadyDataView.SteadyTrendStatType[],
harmonicOrders: number[]
) => {
const harmonicMultiplier = hasHarmonicIndicator(indicators) ? Math.max(harmonicOrders.length, 1) : 1
return indicators.reduce((count, indicator) => {
const indicatorPhases = indicator.phaseCodes?.length ? indicator.phaseCodes : phases
const selectedPhaseCount = indicatorPhases.filter(phase => phases.includes(phase)).length || indicatorPhases.length || 1
const phaseCount = indicator.phaseCodes?.length || 1
const fieldCount = Math.max(indicator.seriesFields?.length || indicator.baseFields?.length || 1, 1)
return count + lineIds.length * selectedPhaseCount * Math.max(statTypes.length, 1) * fieldCount * harmonicMultiplier
return count + lineIds.length * phaseCount * Math.max(statTypes.length, 1) * fieldCount * harmonicMultiplier
}, 0)
}
@@ -94,24 +82,22 @@ export const validateHarmonicOrders = (
export const validateTrendSelection = (params: {
lineIds: string[]
indicators: SteadyDataView.SteadyIndicatorNode[]
phases: string[]
statTypes: SteadyDataView.SteadyTrendStatType[]
harmonicOrders: number[]
}) => {
const { lineIds, indicators, phases, statTypes, harmonicOrders } = params
const { lineIds, indicators, statTypes, harmonicOrders } = params
if (!lineIds.length) return '请选择监测点'
if (!indicators.length) return '请选择趋势指标'
if (lineIds.length > 1 && indicators.length > 1) return '多监测点查询时只能选择 1 个指标'
if (!statTypes.length) return '请选择统计类型'
if (!phases.length) return '请选择相别'
const harmonicError = validateHarmonicOrders(indicators, harmonicOrders)
if (harmonicError) return harmonicError
const seriesCount = estimateTrendSeriesCount(lineIds, indicators, phases, statTypes, harmonicOrders)
const seriesCount = estimateTrendSeriesCount(lineIds, indicators, statTypes, harmonicOrders)
if (seriesCount > MAX_TREND_SERIES_COUNT) {
return '趋势曲线数量不能超过 24 条,请缩小监测点、指标、相别或统计类型范围'
return '趋势曲线数量不能超过 24 条,请缩小监测点、指标或统计类型范围'
}
return ''

View File

@@ -5,7 +5,6 @@ export interface SteadyTrendFormState {
timeRange: string[]
timeUnit: TimePeriodUnit
timeBaseDate: Date
phases: string[]
statTypes: SteadyDataView.SteadyTrendStatType[]
bucket: string
qualityFlag?: number
@@ -19,7 +18,6 @@ export const defaultTrendFormState = (): SteadyTrendFormState => {
timeRange: buildTimePeriodRange('month', baseDate),
timeUnit: 'month',
timeBaseDate: baseDate,
phases: ['A', 'B', 'C'],
statTypes: ['AVG'],
bucket: '10m',
qualityFlag: 1,
@@ -27,6 +25,11 @@ export const defaultTrendFormState = (): SteadyTrendFormState => {
}
}
const formatSteadyTrendQueryTime = (value: string) => {
// 后端趋势接口只接受 yyyy-MM-dd HH:mm:ss公共时间组件生成的毫秒需要在入参层收敛。
return value.replace(/\.[^.]+$/, '')
}
export const buildSteadyTrendQueryPayload = (
lineIds: string[],
indicators: SteadyDataView.SteadyIndicatorNode[],
@@ -36,9 +39,8 @@ export const buildSteadyTrendQueryPayload = (
lineIds,
indicatorCodes: indicators.map(item => item.indicatorCode).filter(Boolean) as string[],
statTypes: formState.statTypes,
phases: formState.phases,
timeStart: formState.timeRange[0] || '',
timeEnd: formState.timeRange[1] || '',
timeStart: formatSteadyTrendQueryTime(formState.timeRange[0] || ''),
timeEnd: formatSteadyTrendQueryTime(formState.timeRange[1] || ''),
bucket: formState.bucket,
qualityFlag: formState.qualityFlag,
harmonicOrders: formState.harmonicOrders.length ? formState.harmonicOrders : undefined