refactor(event): 重构事件列表和稳态数据视图组件结构
- 将事件列表页面逻辑拆分为 EventListTable 组件 - 新增 MeasurementPointDialog 和 VoltageToleranceDialog 弹窗组件 - 重构稳态数据视图为主工作台组件 SteadyTrendWorkbench - 移除不再使用的相别参数和相关逻辑 - 更新事件详情工具函数和接口参数映射 - 优化波形查看功能的数据传递方式 - 修正事件描述字段命名和严重程度解析逻辑
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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]
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ''
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user