Files
admin-govern/src/components/tree/device.vue

729 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- 设备管理使用折叠面板渲染多个tree -->
<template>
<div :style="{ width: menuCollapse ? '40px' : props.width }" class="device-tree-root">
<!-- <Icon
v-show="menuCollapse"
@click="onMenuCollapse"
:name="menuCollapse ? 'el-icon-Expand' : 'el-icon-Fold'"
:class="menuCollapse ? 'unfold' : ''"
size="18px"
class="fold ml10 mt20 menu-collapse"
style="cursor: pointer"
/> -->
<div class="cn-tree" :style="{ opacity: menuCollapse ? 0 : 1 }">
<div class="tree-search mb10">
<el-input maxlength="32" show-word-limit
v-model.trim="filterText"
autocomplete="off"
placeholder="请输入内容"
clearable
>
<template #prepend>
<el-select v-model="treeType" @change="changeTreeType" style="min-width: 75px">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
<template #prefix>
<Icon name="el-icon-Search" style="font-size: 16px" />
</template>
</el-input>
</div>
<div class="comm-tabs mb10">
<el-tooltip
v-for="tab in commTabs"
:key="tab.key"
:content="tab.label"
placement="top"
:show-after="200"
>
<button
type="button"
class="comm-tab"
:class="[
`is-${tab.key}`,
{ 'is-active': commFilter === tab.key }
]"
@click="onCommFilter(tab.key)"
>
<Icon :name="tab.icon" :size="12" class="comm-tab__icon" />
<span class="comm-tab__count">{{ tab.count }}</span>
</button>
</el-tooltip>
</div>
<el-collapse
:accordion="true"
v-model="activeName"
class="device-collapse"
@change="changeDevice"
v-if="treeType == '1'"
v-loading="loading"
>
<el-collapse-item title="治理设备" name="0" v-if="zlDeviceData.length">
<div class="collapse-tree-panel collapse-tree-panel--govern">
<el-select v-model="process" clearable placeholder="请选择状态" class="mb10">
<el-option label="功能调试" value="2" />
<el-option label="出厂调试" value="3" />
<el-option label="正式投运" value="4" />
</el-select>
<el-tree
ref="treeRef1"
:props="defaultProps"
highlight-current
:filter-node-method="filterNode"
node-key="id"
:default-expand-all="false"
v-bind="treeAttrs"
:data="zlDevList"
class="collapse-tree"
@node-click="onTreeNodeClick"
>
<template #default="{ node, data: nodeData }">
<span class="custom-tree-node">
<Icon
:name="nodeData.icon"
style="font-size: 16px"
:style="{ color: nodeData.color }"
v-if="nodeData.icon"
/>
<span style="margin-left: 4px">{{ node.label }}</span>
</span>
</template>
</el-tree>
</div>
</el-collapse-item>
<el-collapse-item title="便携式设备" name="1" v-if="bxsDeviceData.length">
<div class="collapse-tree-panel">
<el-tree
ref="treeRef2"
:props="defaultProps"
highlight-current
:default-expand-all="false"
:filter-node-method="filterNode"
node-key="id"
:data="bxsDeviceData"
v-bind="treeAttrs"
class="collapse-tree"
@node-click="onTreeNodeClick"
>
<template #default="{ node, data: nodeData }">
<span class="custom-tree-node">
<Icon
:name="nodeData.icon"
style="font-size: 16px"
:style="{ color: nodeData.color }"
v-if="nodeData.icon"
/>
<span style="margin-left: 4px">{{ node.label }}</span>
</span>
</template>
</el-tree>
</div>
</el-collapse-item>
<el-collapse-item title="监测设备" name="2" v-if="frontDeviceData.length">
<div class="collapse-tree-panel">
<el-tree
ref="treeRef3"
:props="defaultProps"
highlight-current
:default-expand-all="false"
:filter-node-method="filterNode"
node-key="id"
:data="frontDeviceData"
v-bind="treeAttrs"
class="collapse-tree"
@node-click="onTreeNodeClick"
>
<template #default="{ node, data: nodeData }">
<span class="custom-tree-node">
<Icon
:name="nodeData.icon"
style="font-size: 16px"
:style="{ color: nodeData.color }"
v-if="nodeData.icon"
/>
<span style="margin-left: 4px">{{ node.label }}</span>
</span>
</template>
</el-tree>
</div>
</el-collapse-item>
</el-collapse>
<div v-if="treeType == '2'" class="engineering-tree-wrap" v-loading="loading">
<el-tree
ref="treeRef4"
:props="defaultProps"
highlight-current
:filter-node-method="filterNode"
node-key="id"
v-bind="treeAttrs"
:data="props.data"
class="engineering-tree"
:default-expand-all="false"
@node-click="onTreeNodeClick"
>
<template #default="{ node, data: nodeData }">
<span class="custom-tree-node">
<Icon
:name="nodeData.icon"
style="font-size: 16px"
:style="{ color: nodeData.color }"
v-if="nodeData.icon"
/>
<span style="margin-left: 4px">{{ node.label }}</span>
</span>
</template>
</el-tree>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import useCurrentInstance from '@/utils/useCurrentInstance'
import { ElTree, type CollapseModelValue } from 'element-plus'
import { ref, watch, onMounted, nextTick, computed, useAttrs } from 'vue'
import { collectDeviceLeaves } from './govern/lineTreeUtils'
import { collectDeviceApiLeaves } from './govern/deviceTreeUtils'
defineOptions({
name: 'govern/tree',
inheritAttrs: false
})
const emit = defineEmits(['changeDeviceType', 'changeTreeType'])
interface Props {
width?: string
canExpand?: boolean
type?: string
data?: any[]
height?: number
engineering?: boolean
/** line: getLineTree 四层叶子device: getDeviceTree 三层叶子 */
leafMode?: 'line' | 'device'
}
type CommFilter = 'all' | 'normal' | 'abnormal'
const props = withDefaults(defineProps<Props>(), {
width: '280px',
canExpand: true,
type: '',
data: () => [],
height: 0,
engineering: false,
leafMode: 'line'
})
const treeType = ref('2')
const options = [
{ label: '工程', value: '2' },
{ label: '设备', value: '1' },
]
const { proxy } = useCurrentInstance()
const attrs = useAttrs()
/** 去掉 node-click避免与本地处理重复/覆盖 */
const treeAttrs = computed(() => {
const { onNodeClick: _onNodeClick, ...rest } = attrs as Record<string, any>
return rest
})
const menuCollapse = ref(false)
const activeName = ref('0')
const filterText = ref('')
const process = ref('')
const loading = ref(false)
const commFilter = ref<CommFilter>('all')
const defaultProps = { label: 'name', value: 'id' }
const zlDeviceData = ref<any[]>([])
const zlDevList = ref<any[]>([])
const bxsDeviceData = ref<any[]>([])
const frontDeviceData = ref<any[]>([])
const treeRef1 = ref<InstanceType<typeof ElTree>>()
const treeRef2 = ref<InstanceType<typeof ElTree>>()
const treeRef3 = ref<InstanceType<typeof ElTree>>()
const treeRef4 = ref<InstanceType<typeof ElTree>>()
defineExpose({ treeRef1, treeRef2, treeRef3, treeRef4 })
const isCommNormal = (node: any) => Number(node?.comFlag) === 2
/** 设备叶子Platform 且无子节点,或 type=device */
function isDeviceLeaf(data: any): boolean {
if (!data) return false
if (data.type === 'device') return true
if (data.icon === 'el-icon-Platform' && (!data.children || !data.children.length)) return true
if (data.level === 2 && data.icon === 'el-icon-Platform') return true
return false
}
function collectDevices(nodes: any[], out: any[] = []): any[] {
nodes?.forEach(n => {
if (isDeviceLeaf(n)) out.push(n)
if (n.children?.length) collectDevices(n.children, out)
})
return out
}
const deviceList = computed(() => {
if (treeType.value === '2') return collectDevices(props.data || [])
const collectLeaves = props.leafMode === 'device' ? collectDeviceApiLeaves : collectDeviceLeaves
const { govern, portable, monitor } = collectLeaves(
zlDevList.value,
bxsDeviceData.value,
frontDeviceData.value
)
return [...govern, ...portable, ...monitor]
})
const deviceStats = computed(() => {
const list = deviceList.value
const total = list.length
const normal = list.filter(isCommNormal).length
return { total, normal, abnormal: total - normal }
})
const commTabs = computed(() => [
{ key: 'all' as const, label: '全部设备', count: deviceStats.value.total, icon: 'el-icon-Platform' },
{ key: 'normal' as const, label: '通讯正常', count: deviceStats.value.normal, icon: 'el-icon-Platform' },
{ key: 'abnormal' as const, label: '通讯异常', count: deviceStats.value.abnormal, icon: 'el-icon-Platform' }
])
function applyTreeFilter() {
nextTick(() => {
const val = filterText.value
if (treeType.value === '1') {
treeRef1.value?.filter(val)
treeRef2.value?.filter(val)
treeRef3.value?.filter(val)
} else {
treeRef4.value?.filter(val)
}
})
}
const onCommFilter = (key: CommFilter) => {
commFilter.value = key
applyTreeFilter()
}
function splitTreeData(val: any[]) {
zlDeviceData.value = []
bxsDeviceData.value = []
frontDeviceData.value = []
val.forEach(item => {
if (item.name === '治理设备') {
zlDeviceData.value = item.children ?? []
} else if (item.name === '便携式设备') {
bxsDeviceData.value = item.children ?? []
} else if (item.name === '监测设备') {
frontDeviceData.value = item.children ?? []
}
})
zlDevList.value = filterProcess(JSON.parse(JSON.stringify(zlDeviceData.value)))
}
function filterProcess(nodes: any[]): any[] {
if (!process.value) return nodes
return nodes
.map(node => {
const children = node.children ? filterProcess(node.children) : []
if (node.process == process.value || children.length > 0) {
return { ...node, children }
}
return null
})
.filter(Boolean)
}
function getActiveTreeRef() {
if (treeType.value === '2') return treeRef4.value
if (activeName.value === '0') return treeRef1.value
if (activeName.value === '1') return treeRef2.value
return treeRef3.value
}
function forwardNodeClick(data: any, node: any, ev?: any) {
const handler = (attrs as any).onNodeClick
if (!handler) return
if (Array.isArray(handler)) handler.forEach((fn: any) => fn?.(data, node, ev))
else handler(data, node, ev)
}
function onTreeNodeClick(data: any, node: any, ev?: any) {
forwardNodeClick(data, node, ev)
}
function resolveActiveName() {
if (zlDeviceData.value.length) return '0'
if (bxsDeviceData.value.length) return '1'
if (frontDeviceData.value.length) return '2'
return ''
}
function selectDevicePanel(panelName: string, node?: any) {
if (!node) return
emit('changeDeviceType', panelName, node)
nextTick(() => {
getActiveTreeRef()?.setCurrentKey(node.id)
})
}
const changeDevice = (val: CollapseModelValue) => {
if (Array.isArray(val) || val == null || val === '') return
const panelName = String(val)
const collectLeaves = props.leafMode === 'device' ? collectDeviceApiLeaves : collectDeviceLeaves
const { govern, portable, monitor } = collectLeaves(
zlDevList.value,
bxsDeviceData.value,
frontDeviceData.value
)
const panelMap: Record<string, { nodes: any[]; clearOthers: any[][] }> = {
'0': { nodes: govern, clearOthers: [portable, monitor] },
'1': { nodes: portable, clearOthers: [govern, monitor] },
'2': { nodes: monitor, clearOthers: [govern, portable] }
}
const panel = panelMap[panelName]
if (!panel) return
panel.clearOthers.forEach(list => list.forEach(item => (item.checked = false)))
selectDevicePanel(panelName, panel.nodes[0])
}
const setActiveName = () => {
activeName.value = resolveActiveName()
if (activeName.value) {
nextTick(() => changeDevice(activeName.value))
}
}
watch(
() => props.data,
val => {
if (!val?.length) return
splitTreeData(val)
if (treeType.value === '1') {
nextTick(() => setActiveName())
}
applyTreeFilter()
},
{ immediate: true, deep: true }
)
watch(filterText, () => {
applyTreeFilter()
})
watch(process, () => {
zlDevList.value = filterProcess(JSON.parse(JSON.stringify(zlDeviceData.value)))
if (activeName.value === '0') {
nextTick(() => changeDevice(activeName.value))
}
applyTreeFilter()
})
watch(treeType, () => {
applyTreeFilter()
})
const onMenuCollapse = () => {
menuCollapse.value = !menuCollapse.value
proxy.eventBus.emit('cnTreeCollapse', menuCollapse)
}
const filterNode = (_value: string, data: any, node: any): boolean => {
if (filterText.value) {
if (!data.name) return false
if (!chooseNode(filterText.value, data, node)) return false
}
if (commFilter.value === 'all') return true
if (isDeviceLeaf(data)) {
const normal = isCommNormal(data)
return commFilter.value === 'normal' ? normal : !normal
}
return !!filterText.value
}
const chooseNode = (value: string, data: any, node: any): boolean => {
if (data.name.indexOf(value) !== -1) return true
const level = node.level
if (level === 1) return false
let parentData = node.parent
for (let i = 0; i < level - 1; i++) {
if (parentData?.data?.name?.indexOf(value) !== -1) return true
parentData = parentData.parent
}
return false
}
const changeTreeType = (val: string) => {
loading.value = true
emit('changeTreeType', val)
if (val === '1') {
nextTick(() => setActiveName())
}
setTimeout(() => {
loading.value = false
}, 300)
}
onMounted(() => {
// treeType.value = props.engineering ? '2' : '1'
})
</script>
<style lang="scss" scoped>
.device-tree-root {
display: flex;
overflow: hidden;
height: 100%;
min-height: 0;
}
.cn-tree {
flex-shrink: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;
padding: 10px;
height: 100%;
min-height: 0;
max-height: 100%;
width: 100%;
overflow: hidden;
:deep(.el-tree) {
border: 1px solid var(--el-border-color);
}
:deep(.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content) {
background-color: var(--el-color-primary-light-7);
}
.tree-search {
display: flex;
align-items: center;
flex-shrink: 0;
}
.comm-tabs {
display: flex;
align-items: center;
// justify-content: flex-end;
gap: 6px;
flex-shrink: 0;
}
.comm-tab {
display: inline-flex;
align-items: center;
gap: 4px;
height: 22px;
padding: 0 4px 0 6px;
border-radius: 6px;
border: 1px solid #e5eaf3;
background: #fff;
color: #4e5969;
cursor: pointer;
transition: all 0.15s ease;
line-height: 1;
&__icon {
font-size: 12px;
}
&.is-all .comm-tab__icon,
&.is-all :deep(.comm-tab__icon),
&.is-all :deep(svg),
&.is-all :deep(i) {
color: var(--el-color-primary) !important;
}
&.is-normal .comm-tab__icon,
&.is-normal :deep(.comm-tab__icon),
&.is-normal :deep(svg),
&.is-normal :deep(i) {
color: #2ab914 !important;
}
&.is-abnormal .comm-tab__icon,
&.is-abnormal :deep(.comm-tab__icon),
&.is-abnormal :deep(svg),
&.is-abnormal :deep(i) {
color: #e26257 !important;
}
&__count {
min-width: 20px;
height: 16px;
padding: 0 4px;
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: #1d2129;
}
&:hover:not(.is-active) {
border-color: var(--el-color-primary-light-5);
color: var(--el-color-primary);
}
&.is-active {
color: #fff;
.comm-tab__icon,
:deep(.comm-tab__icon),
:deep(svg),
:deep(i) {
color: #fff !important;
}
.comm-tab__count {
background: #fff;
}
}
&.is-all.is-active {
background: var(--el-color-primary);
border-color: var(--el-color-primary);
.comm-tab__count {
color: var(--el-color-primary);
}
}
&.is-normal.is-active {
background: #2ab914;
border-color: #2ab914;
.comm-tab__count {
color: #2ab914;
}
}
&.is-abnormal.is-active {
background: #e26257;
border-color: #e26257;
.comm-tab__count {
color: #e26257;
}
}
}
.device-collapse {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
border: none;
:deep(.el-collapse-item) {
flex-shrink: 0;
.el-collapse-item__header {
height: 48px;
line-height: 48px;
}
&.is-active {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
.el-collapse-item__wrap {
flex: 1;
min-height: 0;
}
.el-collapse-item__content {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
padding-bottom: 0;
box-sizing: border-box;
}
}
}
}
.engineering-tree-wrap {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.engineering-tree {
flex: 1;
min-height: 0;
overflow: auto;
}
.collapse-tree-panel {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
height: 100%;
overflow: hidden;
&--govern > .el-select {
flex-shrink: 0;
}
}
.collapse-tree {
flex: 1;
min-height: 0;
overflow: auto;
}
.menu-collapse {
color: var(--el-color-primary);
}
}
.custom-tree-node {
display: flex;
align-items: center;
}
:deep(.el-input-group__prepend) {
background-color: var(--el-fill-color-blank);
}
:deep(.is-disabled) {
display: none !important;
}
</style>