Files
CN_Tool_client/frontend/src/views/steady/steadyTrend/components/SteadyLedgerTree.vue
yexb 055e69fff7 feat(steady): 完善稳态数据视图功能
- 更新纵坐标刻度算法,优化小数趋势图范围显示
- 添加稳态趋势图全屏模式和共享工具组件
- 实现多图联动的鼠标悬停竖线同步功能
- 调整主线线宽分档策略,降低最大线宽限制
- 重构稳态趋势工具栏,优化谐波次数选择逻辑
- 添加周时间周期搜索支持和自定义时间范围选择
- 完善稳态数据表格和指示器浮动面板功能
- 优化稳态趋势图性能,添加LTB采样和动画控制
- 修复数据表格打开前的趋势数据验证问题
- 统一时间轴标签格式化和网格对齐处理
2026-05-27 08:06:12 +08:00

276 lines
6.7 KiB
Vue

<template>
<section class="card steady-tree-card" :class="{ 'is-collapsed': collapsed }">
<div v-show="collapsed" class="collapsed-panel">
<el-tooltip content="展开设备树" placement="right">
<el-button class="panel-toggle" type="primary" :icon="ArrowRight" circle @click="emit('toggle')" />
</el-tooltip>
</div>
<div v-show="!collapsed" class="expanded-panel">
<div class="panel-header">
<span class="panel-title">设备树</span>
<el-tooltip content="收缩设备树" placement="top">
<el-button class="panel-toggle" type="primary" :icon="ArrowLeft" circle @click="emit('toggle')" />
</el-tooltip>
</div>
<div class="tree-search-row">
<el-input
:model-value="keyword"
clearable
placeholder="搜索工程、项目、设备、监测点"
@update:model-value="handleKeywordChange"
></el-input>
<el-button :icon="Refresh" circle :loading="loading" @click="emit('refresh')" />
</div>
<el-scrollbar class="tree-scrollbar">
<el-tree
ref="treeRef"
class="ledger-tree"
:data="treeData"
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-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="shouldShowLedgerCount(data)">
{{ resolveLedgerCountText(data) }}
</template>
</span>
</div>
</template>
</el-tree>
</el-scrollbar>
</div>
</section>
</template>
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue'
import type { Component } from 'vue'
import { ArrowLeft, ArrowRight, Folder, Location, Monitor, OfficeBuilding, Refresh } from '@element-plus/icons-vue'
import type { TreeInstance } from 'element-plus'
import type { SteadyTrend } from '@/api/steady/steadyTrend/interface'
defineOptions({
name: 'SteadyLedgerTree'
})
const props = defineProps<{
treeData: SteadyTrend.SteadyLedgerNode[]
loading: boolean
keyword: string
defaultCheckedKeys: string[]
collapsed: boolean
}>()
const emit = defineEmits<{
refresh: []
search: [value: string]
change: [nodes: SteadyTrend.SteadyLedgerNode[]]
toggle: []
}>()
const treeRef = ref<TreeInstance>()
type LedgerLevel = SteadyTrend.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 shouldShowLedgerCount = (data: SteadyTrend.SteadyLedgerNode) => {
return Number(data.level) < 3 && (Number(data.deviceCount) > 0 || Number(data.lineCount) > 0)
}
const resolveLedgerCountText = (data: SteadyTrend.SteadyLedgerNode) => {
if (normalizeLedgerLevel(data.level) === 2) {
return String(Number(data.lineCount || 0))
}
return `${Number(data.deviceCount || 0)} / ${Number(data.lineCount || 0)}`
}
const handleKeywordChange = (value: string) => {
emit('search', value)
}
const handleCheck = () => {
emit('change', (treeRef.value?.getCheckedNodes(false, false) || []) as SteadyTrend.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">
.steady-tree-card {
display: flex;
flex-direction: column;
gap: 10px;
min-height: 0;
padding: 12px;
}
.steady-tree-card:not(.is-collapsed) {
height: 100%;
}
.steady-tree-card.is-collapsed {
position: absolute;
top: 0;
left: 0;
z-index: 4;
align-items: center;
width: 36px;
height: 36px;
min-height: 36px;
padding: 0;
overflow: visible;
background: transparent;
border: none;
box-shadow: none;
}
.collapsed-panel,
.expanded-panel {
min-height: 0;
}
.collapsed-panel {
display: flex;
width: 36px;
height: 36px;
align-items: center;
justify-content: center;
}
.expanded-panel {
display: flex;
flex: 1;
flex-direction: column;
gap: 10px;
}
.panel-header,
.tree-node {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.panel-toggle {
flex: none;
}
.tree-search-row {
display: flex;
align-items: center;
gap: 8px;
}
.tree-search-row :deep(.el-input) {
flex: 1 1 0;
min-width: 0;
}
.tree-scrollbar {
flex: 1;
min-height: 0;
}
.tree-node {
width: 100%;
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;
white-space: nowrap;
}
.node-count {
flex: none;
color: var(--el-text-color-secondary);
font-size: 12px;
}
.ledger-tree :deep(.el-tree-node__content) {
min-width: 0;
}
</style>