You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
besure_web/src/views/iot/deviceParamAnalysis/index.vue

750 lines
24 KiB
Vue

<template>
<el-row :gutter="20">
<el-col :span="6" :xs="24">
<ContentWrap class="h-1/1">
<div class="device-param-tree-toolbar">
<el-input
v-model="keyword"
clearable
:placeholder="t('DataCollection.DeviceParamAnalysis.treeSearchPlaceholder')"
class="device-param-tree-toolbar__input"
@input="handleKeywordChange"
/>
<el-button :loading="treeLoading" @click="handleTreeRefresh">
<Icon icon="ep:refresh" class="mr-5px" />
{{ t('DataCollection.DeviceParamAnalysis.refreshButtonText') }}
</el-button>
</div>
<div class="mt-12px">
<el-tree
ref="treeRef"
v-loading="treeLoading"
:data="treeData"
:props="treeProps"
:default-expanded-keys="defaultExpandedKeys"
node-key="id"
show-checkbox
check-strictly
@check="handleTreeCheck"
>
<template #default="{ data }">
<span class="device-param-tree-node">
<span class="device-param-tree-node__label">{{ data.label }}</span>
<el-tag v-if="data.tag === 'equipment'" type="success" size="small" effect="plain">&#35774;&#22791;</el-tag>
</span>
</template>
</el-tree>
</div>
</ContentWrap>
</el-col>
<el-col :span="18" :xs="24">
<div class="analysis-groups">
<ContentWrap v-if="!selectedGroups.length">
<el-empty :description="t('DataCollection.DeviceParamAnalysis.emptySelectNodeDescription')" />
</ContentWrap>
<ContentWrap v-for="group in selectedGroups" :key="group.id">
<el-form class="-mb-15px device-param-analysis-form" :inline="true" label-width="auto">
<el-form-item :label="t('DataCollection.DeviceParamAnalysis.formTimeLabel')">
<div>
<el-date-picker
v-model="group.dateRange"
type="datetimerange"
:start-placeholder="t('DataCollection.DeviceParamAnalysis.formTimeStartPlaceholder')"
:end-placeholder="t('DataCollection.DeviceParamAnalysis.formTimeEndPlaceholder')"
value-format="YYYY-MM-DD HH:mm:ss"
:shortcuts="dateShortcuts"
class="!w-360px"
:disabled-date="disableFutureDate"
@change="(value) => handleDateRangeChange(group, value)"
/>
<div class="mt-4px text-12px text-gray-500">最多选择 8 小时,且不能选择未来日期</div>
</div>
</el-form-item>
<el-form-item>
<el-button :disabled="group.chartLoading" @click="handleQuery(group)">
<Icon icon="ep:search" class="mr-5px" />
{{ t('DataCollection.DeviceParamAnalysis.searchButtonText') }}
</el-button>
<el-button :disabled="group.chartLoading" @click="resetQuery(group)">
<Icon icon="ep:refresh" class="mr-5px" />
{{ t('DataCollection.DeviceParamAnalysis.resetButtonText') }}
</el-button>
</el-form-item>
</el-form>
<el-alert :title="buildParamTitle(group.param)" type="info" :closable="false" class="mb-12px" />
<div v-loading="group.chartLoading">
<el-empty
v-if="group.chartState === 'empty'"
:description="t('DataCollection.DeviceParamAnalysis.emptyDescription')"
/>
<EChart
v-else-if="group.chartState === 'ready'"
:key="group.chartRenderKey"
:options="buildChartOption(group)"
height="360px"
/>
</div>
</ContentWrap>
</div>
</el-col>
</el-row>
</template>
<script setup lang="ts">
import type { EChartsOption } from 'echarts'
import dayjs from 'dayjs'
import { Echart as EChart } from '@/components/Echart'
import { OrganizationApi } from '@/api/mes/organization'
import { DeviceModelAttributeApi } from '@/api/iot/devicemodelattribute'
import { handleTree } from '@/utils/tree'
defineOptions({ name: 'DeviceParamAnalysis' })
const { t } = useI18n()
type TreeNodeType = 'device' | 'param'
type TreeNodeTag = 'equipment'
type DeviceTreeNode = {
id: string
label: string
type: TreeNodeType
tag?: TreeNodeTag
children?: DeviceTreeNode[]
orgClass?: string
deviceId?: number
modelId?: number
paramKey?: string
unit?: string
paramCount?: number
disabled?: boolean
}
type ApiTreeParameter = {
id: number
name: string
code?: string
unit?: string | null
}
type ApiTreeEquipment = {
id: number
name: string
parameters?: ApiTreeParameter[]
}
type ApiTreeOrg = {
id: number
name: string
orgClass?: string
parentId?: number | null
equipments?: ApiTreeEquipment[]
children?: ApiTreeOrg[]
}
const message = useMessage()
const treeRef = ref()
const treeLoading = ref(false)
const keyword = ref('')
const treeProps = { children: 'children', label: 'label', disabled: 'disabled' }
const treeData = ref<DeviceTreeNode[]>([])
const defaultExpandedKeys = ref<string[]>([])
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
const MAX_DATE_RANGE_HOURS = 8
const MAX_DATE_RANGE_MS = MAX_DATE_RANGE_HOURS * 60 * 60 * 1000
const lastValidDateRange = new Map<string, [string, string]>()
const disableFutureDate = (date: Date) => {
return dayjs(date).isAfter(dayjs(), 'day')
}
const buildDefaultDateRange = (): [string, string] => {
const end = dayjs()
const start = end.subtract(2, 'hour')
return [start.format(DATE_TIME_FORMAT), end.format(DATE_TIME_FORMAT)]
}
const createLastHoursShortcut = (hours: number) => {
return {
text: `最近${hours}小时`,
value: () => {
const end = dayjs().toDate()
const start = dayjs().subtract(hours, 'hour').toDate()
return [start, end]
}
}
}
const dateShortcuts = [
createLastHoursShortcut(1),
createLastHoursShortcut(2),
createLastHoursShortcut(4),
createLastHoursShortcut(8)
]
type ChartState = 'idle' | 'loading' | 'empty' | 'ready'
type SelectedGroup = {
id: string
param: DeviceTreeNode
deviceId?: number
modelId?: number
dateRange: [string, string]
requestSeq: number
chartLoading: boolean
chartState: ChartState
chartXAxis: string[]
chartSeries: Array<{ name: string; data: Array<number | null> }>
chartRenderKey: number
}
const selectedGroups = ref<SelectedGroup[]>([])
const lastCheckedSignature = ref<string>('')
let keywordTimer: number | undefined
let treeCheckTimer: number | undefined
let pendingCheckedNodes: DeviceTreeNode[] = []
let pendingCheckedKeys: Array<string | number> = []
const handleKeywordChange = () => {
if (keywordTimer) window.clearTimeout(keywordTimer)
keywordTimer = window.setTimeout(() => {
keywordTimer = undefined
loadTree()
}, 300)
}
const handleTreeRefresh = async () => {
if (keywordTimer) {
window.clearTimeout(keywordTimer)
keywordTimer = undefined
}
await loadTree()
}
const toFiniteId = (value: any): number | undefined => {
if (typeof value === 'number') return Number.isFinite(value) ? value : undefined
if (typeof value === 'string') {
if (/^\d+$/.test(value.trim())) return Number(value)
const n = Number(value)
return Number.isFinite(n) ? n : undefined
}
return undefined
}
const toNodeIds = (node: DeviceTreeNode): { deviceId?: number; modelId?: number } => {
const modelId = typeof node.modelId === 'number' && Number.isFinite(node.modelId) ? node.modelId : undefined
const deviceId = typeof node.deviceId === 'number' && Number.isFinite(node.deviceId) ? node.deviceId : undefined
if (typeof deviceId === 'number') return { deviceId, modelId }
const parts = String(node.id ?? '').split('-').filter(Boolean)
const last = parts.length ? parts[parts.length - 1] : undefined
const parsed = toFiniteId(last)
if (typeof parsed !== 'number' || !Number.isFinite(parsed) || parsed <= 0) {
return { modelId }
}
return { deviceId: parsed, modelId }
}
const buildParamTitle = (param: DeviceTreeNode) => {
const unitText = param.unit ? ` (${param.unit})` : ''
return t('DataCollection.DeviceParamAnalysis.selectedParamTitle', {
label: param.label,
unit: unitText
})
}
const buildChartOption = (group: SelectedGroup): EChartsOption => {
const unit = group.param?.unit
return {
tooltip: { trigger: 'axis' },
legend: { top: 10, left: 'center', type: 'scroll' },
grid: { left: 50, right: 60, top: 80, bottom: 40, containLabel: true },
xAxis: {
type: 'category',
boundaryGap: false,
data: group.chartXAxis
},
yAxis: {
type: 'value',
name: unit || ''
},
series: group.chartSeries.map((s) => ({
type: 'line',
name: s.name,
smooth: false,
showSymbol: false,
data: s.data
}))
}
}
const buildTreeFromApi = (orgs: ApiTreeOrg[]): DeviceTreeNode[] => {
const normalizeOrg = (org: any): ApiTreeOrg => {
const children = Array.isArray(org?.children) ? org.children.map(normalizeOrg) : undefined
const equipments = Array.isArray(org?.equipments) ? org.equipments : undefined
const parentId = typeof org?.parentId === 'number' ? org.parentId : Number(org?.parentId ?? 0) || 0
return {
id: Number(org?.id ?? 0) || 0,
name: String(org?.name ?? ''),
orgClass: org?.orgClass ? String(org.orgClass) : undefined,
parentId,
equipments,
children
}
}
const normalizedOrgs: ApiTreeOrg[] = Array.isArray(orgs) ? orgs.map(normalizeOrg) : []
const isNestedTree = normalizedOrgs.some((o) => typeof (o as any)?.children !== 'undefined')
const orgTree = isNestedTree ? normalizedOrgs : (handleTree(normalizedOrgs, 'id', 'parentId', 'children') as ApiTreeOrg[])
const toOrgNode = (org: ApiTreeOrg): DeviceTreeNode => {
const orgChildren = Array.isArray(org?.children) ? org.children.map(toOrgNode) : []
const equipments = Array.isArray(org?.equipments) ? org.equipments : []
const equipmentNodes: DeviceTreeNode[] = equipments.map((eq) => {
const params = Array.isArray(eq?.parameters) ? eq.parameters : []
const paramNodes: DeviceTreeNode[] = params.map((p) => ({
id: `param-${eq.id}-${p.id}`,
label: p?.name ?? p?.code ?? String(p?.id ?? ''),
type: 'param',
deviceId: toFiniteId(eq.id),
modelId: toFiniteId(p.id),
paramKey: p?.code ? String(p.code) : undefined,
unit: p?.unit ? String(p.unit) : undefined,
disabled: false
}))
return {
id: `equipment-${org.id}-${eq.id}`,
label: eq?.name ?? String(eq?.id ?? ''),
type: 'device',
tag: 'equipment',
deviceId: toFiniteId(eq.id),
paramCount: params.length,
children: paramNodes.length ? paramNodes : undefined,
disabled: true
}
})
const children = [...orgChildren, ...equipmentNodes]
return {
id: `org-${org.id}`,
label: org?.name ?? String(org?.id ?? ''),
type: 'device',
orgClass: org?.orgClass ? String(org.orgClass) : undefined,
children: children.length ? children : undefined,
disabled: true
}
}
return Array.isArray(orgTree) ? orgTree.map(toOrgNode) : []
}
const hasOrgClassInSubtree = (node: DeviceTreeNode, targetOrgClass: string): boolean => {
const children = Array.isArray(node?.children) ? node.children : []
for (const child of children) {
if (child?.orgClass === targetOrgClass) return true
if (hasOrgClassInSubtree(child, targetOrgClass)) return true
}
return false
}
const buildDefaultExpandedKeys = (nodes: DeviceTreeNode[], stopOrgClass: string): string[] => {
const keys: string[] = []
const walk = (list: DeviceTreeNode[]) => {
for (const node of list) {
if (node?.type === 'device' && node?.orgClass && node.orgClass !== stopOrgClass) {
if (hasOrgClassInSubtree(node, stopOrgClass)) {
keys.push(node.id)
}
}
const children = Array.isArray(node?.children) ? node.children : []
if (children.length) walk(children)
}
}
walk(nodes)
return Array.from(new Set(keys))
}
const extractApiOrgs = (res: any): ApiTreeOrg[] => {
if (Array.isArray(res)) return res as ApiTreeOrg[]
if (Array.isArray(res?.data)) return res.data as ApiTreeOrg[]
if (Array.isArray(res?.result)) return res.result as ApiTreeOrg[]
if (Array.isArray(res?.list)) return res.list as ApiTreeOrg[]
return []
}
const loadTree = async () => {
treeLoading.value = true
try {
const res = await OrganizationApi.deviceParameterAnalysis({ keyword: keyword.value || undefined, showDevices: 1 })
treeData.value = buildTreeFromApi(extractApiOrgs(res))
defaultExpandedKeys.value = buildDefaultExpandedKeys(treeData.value, 'pipeline')
await nextTick()
treeRef.value?.setExpandedKeys?.(defaultExpandedKeys.value)
if (keyword.value) {
treeRef.value?.setCurrentKey?.(undefined)
treeRef.value?.setCheckedKeys?.([])
selectedGroups.value = []
lastCheckedSignature.value = ''
}
} catch {
message.error(t('DataCollection.DeviceParamAnalysis.messageLoadTreeFailed'))
} finally {
treeLoading.value = false
}
}
const normalizeDateRangeValue = (value: any): [string, string] | undefined => {
if (!Array.isArray(value) || value.length !== 2) return undefined
const start = value[0]
const end = value[1]
if (!start || !end) return undefined
return [String(start), String(end)]
}
const ensureDateRangeWithin8Hours = (group: SelectedGroup, dateRange: [string, string]) => {
const now = dayjs()
let start = dayjs(dateRange[0])
let end = dayjs(dateRange[1])
if (!start.isValid() || !end.isValid()) return dateRange
if (start.isAfter(now) && end.isAfter(now)) {
end = now
start = now.subtract(2, 'hour')
} else {
if (end.isAfter(now)) end = now
if (start.isAfter(now)) start = end.subtract(2, 'hour')
}
const diffMs = end.valueOf() - start.valueOf()
if (diffMs < 0) {
end = now
start = now.subtract(2, 'hour')
const nextRange: [string, string] = [start.format(DATE_TIME_FORMAT), end.format(DATE_TIME_FORMAT)]
return nextRange
}
if (diffMs <= MAX_DATE_RANGE_MS) {
return [start.format(DATE_TIME_FORMAT), end.format(DATE_TIME_FORMAT)]
}
const clampedEnd = start.add(MAX_DATE_RANGE_HOURS, 'hour')
const finalEnd = clampedEnd.isAfter(now) ? now : clampedEnd
return [start.format(DATE_TIME_FORMAT), finalEnd.format(DATE_TIME_FORMAT)]
}
const ensureDateRange = (group: SelectedGroup) => {
if (!group.dateRange || group.dateRange.length !== 2) {
group.dateRange = buildDefaultDateRange()
lastValidDateRange.set(group.id, group.dateRange)
return
}
const normalized = normalizeDateRangeValue(group.dateRange)
if (!normalized) {
group.dateRange = buildDefaultDateRange()
lastValidDateRange.set(group.id, group.dateRange)
return
}
const nextRange = ensureDateRangeWithin8Hours(group, normalized)
group.dateRange = nextRange
lastValidDateRange.set(group.id, nextRange)
}
const handleDateRangeChange = (group: SelectedGroup, value: any) => {
const normalized = normalizeDateRangeValue(value) || normalizeDateRangeValue(group.dateRange)
if (!normalized) return
const now = dayjs()
const selectedStart = dayjs(normalized[0])
const selectedEnd = dayjs(normalized[1])
const hasFuture = (selectedStart.isValid() && selectedStart.isAfter(now)) || (selectedEnd.isValid() && selectedEnd.isAfter(now))
const nextRange = ensureDateRangeWithin8Hours(group, normalized)
const isClamped = nextRange[0] !== normalized[0] || nextRange[1] !== normalized[1]
if (hasFuture) {
message.warning('不能选择未来日期')
} else if (isClamped) {
message.warning('时间范围最多只能选择 8 小时')
}
group.dateRange = nextRange
lastValidDateRange.set(group.id, nextRange)
}
const resetChartData = (group: SelectedGroup) => {
group.chartXAxis = []
group.chartSeries = []
group.chartRenderKey++
}
const toNumber = (value: any) => {
if (typeof value === 'number') return value
if (typeof value === 'string') {
const n = Number(value)
return Number.isFinite(n) ? n : 0
}
if (Array.isArray(value)) return 0
if (value && typeof value === 'object') {
const maybe = value.value ?? value.val ?? value.count ?? value.amount
const n = Number(maybe)
return Number.isFinite(n) ? n : 0
}
return 0
}
const extractChartRows = (res: any): Record<string, any>[] => {
if (Array.isArray(res)) return res as Record<string, any>[]
if (Array.isArray(res?.data)) return res.data as Record<string, any>[]
if (Array.isArray(res?.result)) return res.result as Record<string, any>[]
if (Array.isArray(res?.list)) return res.list as Record<string, any>[]
return []
}
const fetchGroupChart = async (group: SelectedGroup, notify: boolean) => {
if (typeof group.deviceId !== 'number' || !Number.isFinite(group.deviceId)) return
ensureDateRange(group)
const [start, end] = group.dateRange
if (!start || !end) return
const buildDateTime = (date: string, suffix: string) => {
return date.includes(':') ? date : `${date} ${suffix}`
}
const requestSeq = ++group.requestSeq
group.chartState = 'loading'
group.chartLoading = true
resetChartData(group)
try {
const req: Record<string, any> = {
deviceId: group.deviceId,
collectionStartTime: buildDateTime(start, '00:00:00'),
collectionEndTime: buildDateTime(end, '23:59:59')
}
if (typeof group.modelId === 'number' && Number.isFinite(group.modelId)) {
req.modelId = group.modelId
}
const data = await DeviceModelAttributeApi.operationAnalysisDetails(req as any)
if (requestSeq !== group.requestSeq) return
const rows: Record<string, any>[] = extractChartRows(data)
if (!rows.length) {
group.chartXAxis = []
group.chartSeries = []
group.chartState = 'empty'
if (notify) {
message.warning(t('DataCollection.DeviceParamAnalysis.messageNodeNoParams'))
}
return
}
const firstRow = rows[0] || {}
const keys = Object.keys(firstRow)
if (keys.includes('collectTime') && keys.includes('attributeName') && keys.includes('addressValue')) {
const sortedRows = [...rows].sort((a, b) =>
String(a?.collectTime ?? '').localeCompare(String(b?.collectTime ?? ''))
)
if (requestSeq !== group.requestSeq) return
const xAxis = Array.from(new Set(sortedRows.map((r) => String(r?.collectTime ?? ''))))
const xIndexMap = new Map<string, number>()
xAxis.forEach((x, idx) => xIndexMap.set(x, idx))
group.chartXAxis = xAxis
const seriesMap = new Map<string, Array<number | null>>()
const ensureSeries = (name: string) => {
if (!seriesMap.has(name)) {
seriesMap.set(name, Array(xAxis.length).fill(null))
}
return seriesMap.get(name)!
}
sortedRows.forEach((row) => {
const time = String(row?.collectTime ?? '')
const idx = xIndexMap.get(time)
if (typeof idx !== 'number') return
const name = String(row?.attributeName ?? group.param?.label ?? '')
const series = ensureSeries(name)
series[idx] = toNumber(row?.addressValue)
})
group.chartSeries = Array.from(seriesMap.entries()).map(([name, data]) => ({ name, data }))
} else {
const xKey = keys.find((k) => /time|date|时间|日期/i.test(k))
if (xKey) {
const sortedRows = [...rows].sort((a, b) =>
String(a?.[xKey] ?? '').localeCompare(String(b?.[xKey] ?? ''))
)
if (requestSeq !== group.requestSeq) return
group.chartXAxis = sortedRows.map((r) => String(r?.[xKey] ?? ''))
const seriesKeys = keys.filter((k) => k !== xKey)
const seriesMap = new Map<string, Array<number | null>>()
const ensureSeries = (name: string) => {
if (!seriesMap.has(name)) {
seriesMap.set(name, Array(sortedRows.length).fill(null))
}
return seriesMap.get(name)!
}
sortedRows.forEach((row, rowIndex) => {
seriesKeys.forEach((k) => {
const v = row?.[k]
if (Array.isArray(v)) {
v.forEach((item) => {
const name = String(item?.attributeName ?? k)
const series = ensureSeries(name)
series[rowIndex] = toNumber(item?.addressValue ?? item?.value ?? item?.val)
})
return
}
const series = ensureSeries(k)
series[rowIndex] = toNumber(v)
})
})
group.chartSeries = Array.from(seriesMap.entries()).map(([name, data]) => ({ name, data }))
} else {
group.chartXAxis = keys
const values = keys.map((k) => toNumber(firstRow[k]))
group.chartSeries = [
{ name: group.param?.label || t('DataCollection.DeviceParamAnalysis.defaultSeriesName'), data: values }
]
}
}
group.chartState = group.chartXAxis.length && group.chartSeries.length ? 'ready' : 'empty'
} catch {
if (notify) {
message.error(t('DataCollection.DeviceParamAnalysis.messageFetchChartFailed'))
}
group.chartState = 'idle'
} finally {
if (requestSeq === group.requestSeq) {
group.chartLoading = false
}
}
}
const syncGroupsByCheckedNodes = (checkedNodes: DeviceTreeNode[]) => {
const ids = checkedNodes.map((n) => String(n.id))
const existingIds = new Set(selectedGroups.value.map((g) => g.id))
const nextIds = new Set(ids)
selectedGroups.value = selectedGroups.value.filter((g) => nextIds.has(g.id))
const toAdd = checkedNodes.filter((n) => !existingIds.has(String(n.id)))
const addedGroups: SelectedGroup[] = []
for (const node of toAdd) {
const { deviceId, modelId } = toNodeIds(node)
if (typeof deviceId !== 'number') {
continue
}
const group: SelectedGroup = {
id: String(node.id),
param: node,
deviceId,
modelId,
dateRange: buildDefaultDateRange(),
requestSeq: 0,
chartLoading: false,
chartState: 'idle',
chartXAxis: [],
chartSeries: [],
chartRenderKey: 0
}
selectedGroups.value.push(group)
lastValidDateRange.set(group.id, group.dateRange)
addedGroups.push(group)
}
return addedGroups
}
const handleTreeCheck = async (_node?: DeviceTreeNode, info?: any) => {
const checkedNodes = (info?.checkedNodes ?? []) as DeviceTreeNode[]
const checkedKeys = (info?.checkedKeys ?? []) as Array<string | number>
pendingCheckedNodes = Array.isArray(checkedNodes) ? checkedNodes : []
pendingCheckedKeys = Array.isArray(checkedKeys) ? checkedKeys : []
if (treeCheckTimer) {
window.clearTimeout(treeCheckTimer)
}
treeCheckTimer = window.setTimeout(async () => {
treeCheckTimer = undefined
const effectiveNodes = pendingCheckedNodes.filter((n) => n?.type === 'param')
const signature = pendingCheckedKeys
.map((k) => String(k ?? ''))
.filter(Boolean)
.sort()
.join(',')
if (signature === lastCheckedSignature.value) return
lastCheckedSignature.value = signature
const addedGroups = syncGroupsByCheckedNodes(effectiveNodes)
const needFetchGroups = selectedGroups.value.filter((g) => g.chartState === 'idle' && !g.chartLoading)
const ids = new Set<string>([...addedGroups.map((g) => g.id), ...needFetchGroups.map((g) => g.id)])
const groups = selectedGroups.value.filter((g) => ids.has(g.id))
await Promise.allSettled(groups.map((g) => fetchGroupChart(g, false)))
}, 0)
}
const handleQuery = async (group: SelectedGroup) => {
await fetchGroupChart(group, true)
}
const resetQuery = async (group: SelectedGroup) => {
group.dateRange = buildDefaultDateRange()
await fetchGroupChart(group, true)
}
onMounted(async () => {
await loadTree()
})
</script>
<style scoped lang="scss">
.device-param-analysis-form{
margin-bottom: 0px
}
.device-param-analysis-form :deep(.el-form-item__label) {
min-width: 100px;
}
.analysis-groups {
display: flex;
flex-direction: column;
gap: 12px;
}
.device-param-tree-toolbar {
display: flex;
align-items: center;
gap: 8px;
}
.device-param-tree-toolbar__input {
flex: 1;
min-width: 0;
}
.device-param-tree-node {
display: inline-flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.device-param-tree-node__label {
overflow: hidden;
text-overflow: ellipsis;
}
:deep(.el-tree) {
max-height: calc(100vh - 280px);
overflow: auto;
}
</style>