feat(steady): 实现台账指标树默认选中及图标展示功能

- 添加findFirstSelectableLedgerNode和findFirstLeafIndicator工具函数
- 实现台账树首次加载后默认选中第一个可查询监测点
- 实现指标树首次加载后默认选中第一个叶子指标
- 添加台账层级图标展示及样式配置
- 集成defaultCheckedKeys属性到台账和指标树组件
- 更新趋势查询参数移除bucket字段
- 修复数据质量标识默认值设置问题
This commit is contained in:
2026-05-18 16:30:02 +08:00
parent f9ed6c6245
commit 6755476969
11 changed files with 203 additions and 34 deletions

View File

@@ -42,7 +42,6 @@ export namespace SteadyDataView {
statTypes: SteadyTrendStatType[]
timeStart: string
timeEnd: string
bucket?: string
qualityFlag?: number
harmonicOrders?: number[]
}

View File

@@ -19,6 +19,7 @@
:key="selectorResetKey"
:tree-data="treeData"
:loading="loading"
:default-checked-keys="defaultCheckedKeys"
@refresh="emit('refresh')"
@change="emit('change', $event)"
/>
@@ -39,6 +40,7 @@ defineProps<{
collapsed: boolean
treeData: SteadyDataView.SteadyIndicatorNode[]
loading: boolean
defaultCheckedKeys: string[]
selectorResetKey: number
}>()

View File

@@ -13,6 +13,7 @@
node-key="treeKey"
show-checkbox
default-expand-all
:default-checked-keys="defaultCheckedKeys"
:expand-on-click-node="false"
:props="{ label: 'name', children: 'children' }"
@check="handleCheck"
@@ -29,7 +30,7 @@
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, nextTick, ref, watch } from 'vue'
import { Refresh } from '@element-plus/icons-vue'
import type { TreeInstance } from 'element-plus'
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
@@ -42,6 +43,7 @@ defineOptions({
const props = defineProps<{
treeData: SteadyDataView.SteadyIndicatorNode[]
loading: boolean
defaultCheckedKeys: string[]
}>()
const emit = defineEmits<{
@@ -70,6 +72,19 @@ const handleCheck = () => {
const checkedNodes = (treeRef.value?.getCheckedNodes(false, false) || []) as SteadyDataView.SteadyIndicatorNode[]
emit('change', collectLeafIndicators(checkedNodes))
}
const applyDefaultCheckedKeys = async () => {
await nextTick()
treeRef.value?.setCheckedKeys(props.defaultCheckedKeys, false)
}
watch(
() => [normalizedTreeData.value, props.defaultCheckedKeys],
() => {
applyDefaultCheckedKeys()
},
{ immediate: true }
)
</script>
<style scoped lang="scss">

View File

@@ -20,13 +20,19 @@
node-key="id"
show-checkbox
default-expand-all
:default-checked-keys="defaultCheckedKeys"
:expand-on-click-node="false"
:props="{ label: 'name', children: 'children' }"
@check="handleCheck"
>
<template #default="{ data }">
<div class="tree-node">
<span class="node-name">{{ data.name }}</span>
<span class="node-main">
<el-icon :class="['node-icon', `is-level-${normalizeLedgerLevel(data.level)}`]">
<component :is="resolveLedgerIcon(data.level)" />
</el-icon>
<span class="node-name">{{ data.name }}</span>
</span>
<span class="node-count">
<template v-if="Number(data.deviceCount) || Number(data.lineCount)">
{{ Number(data.deviceCount || 0) }} / {{ Number(data.lineCount || 0) }}
@@ -40,8 +46,9 @@
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Refresh } from '@element-plus/icons-vue'
import { nextTick, ref, watch } from 'vue'
import type { Component } from 'vue'
import { Folder, Location, Monitor, OfficeBuilding, Refresh } from '@element-plus/icons-vue'
import type { TreeInstance } from 'element-plus'
import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
@@ -49,10 +56,11 @@ defineOptions({
name: 'SteadyLedgerTree'
})
defineProps<{
const props = defineProps<{
treeData: SteadyDataView.SteadyLedgerNode[]
loading: boolean
keyword: string
defaultCheckedKeys: string[]
}>()
const emit = defineEmits<{
@@ -62,6 +70,26 @@ const emit = defineEmits<{
}>()
const treeRef = ref<TreeInstance>()
type LedgerLevel = SteadyDataView.SteadyLedgerNode['level']
const ledgerIcons: Record<LedgerLevel, Component> = {
0: OfficeBuilding,
1: Folder,
2: Monitor,
3: Location
}
const normalizeLedgerLevel = (value: unknown): LedgerLevel => {
const level = Number(value)
if (level === 0 || level === 1 || level === 2 || level === 3) {
return level
}
return 0
}
const resolveLedgerIcon = (value: unknown) => {
return ledgerIcons[normalizeLedgerLevel(value)]
}
const handleKeywordChange = (value: string) => {
emit('search', value)
@@ -70,6 +98,19 @@ const handleKeywordChange = (value: string) => {
const handleCheck = () => {
emit('change', (treeRef.value?.getCheckedNodes(false, false) || []) as SteadyDataView.SteadyLedgerNode[])
}
const applyDefaultCheckedKeys = async () => {
await nextTick()
treeRef.value?.setCheckedKeys(props.defaultCheckedKeys, false)
}
watch(
() => [props.treeData, props.defaultCheckedKeys],
() => {
applyDefaultCheckedKeys()
},
{ immediate: true }
)
</script>
<style scoped lang="scss">
@@ -105,6 +146,35 @@ const handleCheck = () => {
min-width: 0;
}
.node-main {
display: inline-flex;
min-width: 0;
align-items: center;
gap: 6px;
}
.node-icon {
flex: none;
font-size: 15px;
color: var(--el-text-color-secondary);
}
.node-icon.is-level-0 {
color: var(--el-color-primary);
}
.node-icon.is-level-1 {
color: var(--el-color-success);
}
.node-icon.is-level-2 {
color: var(--el-color-warning);
}
.node-icon.is-level-3 {
color: var(--el-color-info);
}
.node-name {
overflow: hidden;
text-overflow: ellipsis;

View File

@@ -25,17 +25,6 @@
</el-select>
</div>
<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>
<div class="toolbar-field">
<span class="toolbar-field__label">数据</span>
<el-select
@@ -44,8 +33,8 @@
placeholder="选择数据质量"
@update:model-value="updateField('qualityFlag', $event)"
>
<el-option label="仅有效数据" :value="1" />
<el-option label="仅无效数据" :value="0" />
<el-option label="仅有效数据" :value="0" />
<el-option label="仅无效数据" :value="1" />
</el-select>
</div>
@@ -93,13 +82,6 @@ const emit = defineEmits<{
reset: []
}>()
const bucketOptions = [
{ label: '1分钟', value: '1m' },
{ label: '5分钟', value: '5m' },
{ label: '10分钟', value: '10m' },
{ label: '30分钟', value: '30m' },
{ label: '1小时', value: '1h' }
]
const harmonicOrderOptions = Array.from({ length: 49 }, (_item, index) => index + 2)
const statLabelMap: Record<SteadyDataView.SteadyTrendStatType, string> = {
AVG: '平均值',
@@ -136,7 +118,7 @@ const handleTimeBaseDateChange = (value: Date) => {
<style scoped lang="scss">
.trend-toolbar {
display: grid;
grid-template-columns: minmax(312px, 1.4fr) repeat(3, minmax(178px, 0.8fr)) auto;
grid-template-columns: minmax(312px, 1.4fr) repeat(2, minmax(178px, 0.8fr)) auto;
gap: 10px;
align-items: center;
padding: 12px;

View File

@@ -6,6 +6,7 @@
:tree-data="ledgerTree"
:loading="loading.ledger"
:keyword="ledgerKeyword"
:default-checked-keys="defaultLedgerCheckedKeys"
@refresh="emit('refreshLedger')"
@search="emit('ledgerSearch', $event)"
@change="emit('ledgerChange', $event)"
@@ -30,6 +31,7 @@
:selector-reset-key="selectorResetKey"
:tree-data="indicatorTree"
:loading="loading.indicator"
:default-checked-keys="defaultIndicatorCheckedKeys"
@refresh="emit('refreshIndicator')"
@change="emit('indicatorChange', $event)"
/>
@@ -63,6 +65,8 @@ const props = defineProps<{
trend: boolean
}
ledgerKeyword: string
defaultLedgerCheckedKeys: string[]
defaultIndicatorCheckedKeys: string[]
indicatorPanelCollapsed: boolean
selectorResetKey: number
}>()

View File

@@ -7,6 +7,8 @@ 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 pageSource = fs.readFileSync(path.join(currentDir, '..', 'index.vue'), 'utf8')
const selectionRulesSource = fs.readFileSync(path.join(currentDir, '..', 'utils', 'selectionRules.ts'), 'utf8')
const expectations = [
[
@@ -18,6 +20,46 @@ const expectations = [
'indicator tree excludes half-checked parents when collecting checked nodes',
/getCheckedNodes\(\s*false\s*,\s*false\s*\)/,
read('SteadyIndicatorTree.vue')
],
[
'selection rules expose the first selectable ledger resolver',
/export const findFirstSelectableLedgerNode/,
selectionRulesSource
],
[
'selection rules expose the first leaf indicator resolver',
/export const findFirstLeafIndicator/,
selectionRulesSource
],
[
'steady page applies default selected ledger keys after ledger tree load',
/defaultLedgerCheckedKeys\.value\s*=/,
pageSource
],
[
'steady page applies default selected indicator keys after indicator tree load',
/defaultIndicatorCheckedKeys\.value\s*=/,
pageSource
],
[
'ledger tree receives default checked keys',
/defaultCheckedKeys/,
read('SteadyLedgerTree.vue')
],
[
'ledger tree renders level icons',
/<component\s+:is="resolveLedgerIcon\(data\.level\)"/,
read('SteadyLedgerTree.vue')
],
[
'ledger tree resolves icons by ledger level',
/const\s+ledgerIcons[\s\S]*0:[\s\S]*1:[\s\S]*2:[\s\S]*3:/,
read('SteadyLedgerTree.vue')
],
[
'indicator tree receives default checked keys',
/defaultCheckedKeys/,
read('SteadyIndicatorTree.vue')
]
]

View File

@@ -53,17 +53,21 @@ 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 stat bucket quality filters', /toolbar-field__label[\s\S]*统计:[\s\S]*toolbar-field__label[\s\S]*粒度:[\s\S]*toolbar-field__label[\s\S]*数据:/],
['toolbar labels stat quality filters', /toolbar-field__label[\s\S]*统计:[\s\S]*toolbar-field__label[\s\S]*数据:/],
['toolbar does not render bucket selector', /modelValue\.bucket|bucketOptions|粒度:|选择时间粒度/],
['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]*仅无效数据/],
['toolbar binds valid quality flag to zero', /<el-option\s+label="仅有效数据"\s+:value="0"\s*\/>/],
['utilities default to valid quality flag zero', /qualityFlag:\s*0/],
['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 bucket in trend query payload', /bucket:\s*formState\.bucket/],
['utilities do not send phases in trend query payload', /phases:\s*formState\.phases/],
['trend query params do not include bucket', /interface\s+SteadyTrendQueryParams\s*{[^}]*bucket\??:\s*string/],
['trend query params do not include phases', /phases:\s*string\[\]/],
['utilities build chart options', /export const buildSteadyTrendChartOptions/]
]
@@ -78,7 +82,6 @@ const sourceByExpectation = [
componentSource,
componentSource,
pageSource,
pageSource,
componentSource,
pageSource,
apiSource,
@@ -96,6 +99,8 @@ const sourceByExpectation = [
componentSource,
componentSource,
componentSource,
componentSource,
componentSource,
utilitySource,
utilitySource,
utilitySource,
@@ -103,6 +108,9 @@ const sourceByExpectation = [
utilitySource,
utilitySource,
utilitySource,
utilitySource,
utilitySource,
interfaceSource,
interfaceSource,
utilitySource
]

View File

@@ -10,6 +10,8 @@
:show-harmonic-orders="showHarmonicOrders"
:loading="loading"
:ledger-keyword="ledgerKeyword"
:default-ledger-checked-keys="defaultLedgerCheckedKeys"
:default-indicator-checked-keys="defaultIndicatorCheckedKeys"
:selector-reset-key="selectorResetKey"
@refresh-ledger="loadLedgerTree"
@ledger-search="handleLedgerSearch"
@@ -30,6 +32,8 @@ import type { SteadyDataView } from '@/api/steady/steadyDataView/interface'
import SteadyTrendWorkbench from './components/SteadyTrendWorkbench.vue'
import {
collectSelectedLineIds,
findFirstLeafIndicator,
findFirstSelectableLedgerNode,
hasHarmonicIndicator,
resolveAvailableStats,
validateTrendSelection
@@ -49,6 +53,8 @@ const trendForm = ref(defaultTrendFormState())
const ledgerKeyword = ref('')
const indicatorPanelCollapsed = ref(false)
const selectorResetKey = ref(0)
const defaultLedgerCheckedKeys = ref<string[]>([])
const defaultIndicatorCheckedKeys = ref<string[]>([])
const loading = reactive({
ledger: false,
indicator: false,
@@ -75,6 +81,10 @@ const loadLedgerTree = async (keyword = ledgerKeyword.value) => {
try {
const response = await getSteadyTrendLedgerTree(keyword ? { keyword } : undefined)
ledgerTree.value = unwrapData(response) || []
const firstLedgerNode = findFirstSelectableLedgerNode(ledgerTree.value)
// 台账树首次加载后默认选中第一个可查询监测点,避免趋势查询初始状态为空。
selectedLedgerNodes.value = firstLedgerNode ? [firstLedgerNode] : []
defaultLedgerCheckedKeys.value = firstLedgerNode ? [firstLedgerNode.id] : []
} finally {
loading.ledger = false
}
@@ -85,6 +95,11 @@ const loadIndicatorTree = async () => {
try {
const response = await getSteadyTrendIndicatorTree()
indicatorTree.value = unwrapData(response) || []
const firstIndicator = findFirstLeafIndicator(indicatorTree.value)
const firstIndicatorKey = firstIndicator?.id || firstIndicator?.indicatorCode
// 指标树首次加载后默认选中第一个叶子指标,并同步驱动统计类型默认值。
selectedIndicators.value = firstIndicator ? [firstIndicator] : []
defaultIndicatorCheckedKeys.value = firstIndicatorKey ? [firstIndicatorKey] : []
} finally {
loading.indicator = false
}
@@ -109,6 +124,8 @@ const resetTrendState = () => {
trendForm.value = defaultTrendFormState()
selectedLedgerNodes.value = []
selectedIndicators.value = []
defaultLedgerCheckedKeys.value = []
defaultIndicatorCheckedKeys.value = []
trendResult.value = null
selectorResetKey.value += 1
}

View File

@@ -38,6 +38,39 @@ export const collectLeafIndicators = (nodes: SteadyDataView.SteadyIndicatorNode[
return indicators
}
export const findFirstSelectableLedgerNode = (
nodes: SteadyDataView.SteadyLedgerNode[]
): SteadyDataView.SteadyLedgerNode | null => {
for (const node of nodes) {
if (node.level === 3 || node.selectable) {
return node
}
const childNode = findFirstSelectableLedgerNode(node.children || [])
if (childNode) return childNode
}
return null
}
export const findFirstLeafIndicator = (
nodes: SteadyDataView.SteadyIndicatorNode[]
): SteadyDataView.SteadyIndicatorNode | null => {
for (const node of nodes) {
if (node.children?.length) {
const childNode = findFirstLeafIndicator(node.children)
if (childNode) return childNode
continue
}
if (node.indicatorCode) {
return node
}
}
return null
}
export const hasHarmonicIndicator = (indicators: SteadyDataView.SteadyIndicatorNode[]) => {
return indicators.some(item => item.harmonic || Boolean(item.harmonicOrderStart || item.harmonicOrderEnd))
}

View File

@@ -6,7 +6,6 @@ export interface SteadyTrendFormState {
timeUnit: TimePeriodUnit
timeBaseDate: Date
statTypes: SteadyDataView.SteadyTrendStatType[]
bucket: string
qualityFlag?: number
harmonicOrders: number[]
}
@@ -19,8 +18,7 @@ export const defaultTrendFormState = (): SteadyTrendFormState => {
timeUnit: 'month',
timeBaseDate: baseDate,
statTypes: ['AVG'],
bucket: '10m',
qualityFlag: 1,
qualityFlag: 0,
harmonicOrders: []
}
}
@@ -41,7 +39,6 @@ export const buildSteadyTrendQueryPayload = (
statTypes: formState.statTypes,
timeStart: formatSteadyTrendQueryTime(formState.timeRange[0] || ''),
timeEnd: formatSteadyTrendQueryTime(formState.timeRange[1] || ''),
bucket: formState.bucket,
qualityFlag: formState.qualityFlag,
harmonicOrders: formState.harmonicOrders.length ? formState.harmonicOrders : undefined
}