You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

170 lines
7.3 KiB

<template>
<aside class="nvr-tree">
<div class="tree-title">
<span><i class="el-icon-s-platform" /> 设备列表</span>
<strong>{{ channelNodes.length }}</strong>
</div>
<div class="tree-filters">
<el-select v-model="onlineStatus" size="small" placeholder="状态" clearable>
<el-option label="全部" value="" />
<el-option label="在线" value="1" />
<el-option label="离线" value="0" />
</el-select>
<el-input
v-model.trim="keywords"
size="small"
clearable
prefix-icon="el-icon-search"
placeholder="请输入内容"
@input="refreshTree"
/>
<el-button size="small" icon="el-icon-refresh" title="刷新列表" @click="loadTree" />
</div>
<div v-loading="isLoading" class="tree-content">
<el-tree
ref="treeRef"
:data="treeData"
:props="treeProps"
node-key="id"
highlight-current
:default-expanded-keys="expandedKeys"
@node-click="handleNodeClick"
>
<el-tooltip
slot-scope="{ data }"
:content="data.label || data.name || ''"
placement="right"
:open-delay="300"
effect="dark"
>
<span class="custom-tree-node">
<i :class="nodeIcon(data)" />
<span class="node-label">{{ data.label || data.name }}</span>
<i v-if="data.channelFlag" :class="data.patroldeviceOnlineStatus == 1 ? 'online-dot' : 'offline-dot'" />
</span>
</el-tooltip>
</el-tree>
<div v-if="!loading && !treeData.length" class="tree-empty">
<i class="el-icon-video-camera-solid" />
<span>没有匹配的设备通道</span>
</div>
</div>
<div class="tree-tip">单击通道打开预览或切换当前控制对象</div>
</aside>
</template>
<script>
import { getAreaEqubookTreeSelect } from '@/api/videosMonitor/index'
function flatten(nodes, result = []) {
;(nodes || []).forEach(node => {
result.push(node)
if (Array.isArray(node.children)) flatten(node.children, result)
})
return result
}
function filterTree(nodes, keyword, onlineStatus) {
return (nodes || []).reduce((result, node) => {
const children = filterTree(node.children, keyword, onlineStatus)
const text = `${node.label || ''} ${node.name || ''} ${node.patroldeviceCode || ''} ${node.channelCode || ''}`.toLowerCase()
const matches = !keyword || text.includes(keyword.toLowerCase())
const onlineMatches = node.areaFlag === 'Yes' || !onlineStatus || String(node.patroldeviceOnlineStatus) === onlineStatus
if (matches && onlineMatches || children.length) result.push({ ...node, children })
return result
}, [])
}
export default {
name: 'NvrDeviceTree',
props: {
config: { type: Object, default: () => ({}) },
channels: { type: Array, default: () => [] },
selectedCode: { type: String, default: '' },
loading: { type: Boolean, default: false },
openHint: { type: String, default: '单击通道打开实时预览' }
},
data() {
return {
treeNodes: [],
treeData: [],
expandedKeys: [],
keywords: '',
onlineStatus: '',
internalLoading: false,
treeProps: { children: 'children', label: 'label' }
}
},
computed: {
isLoading() { return this.loading || this.internalLoading },
channelNodes() { return flatten(this.treeNodes).filter(node => node.channelFlag) }
},
watch: {
onlineStatus: 'refreshTree',
selectedCode(value) {
const node = this.channelNodes.find(item => this.toCameraCode(item) === value)
if (node && this.$refs.treeRef) this.$refs.treeRef.setCurrentKey(node.id)
}
},
mounted() { this.loadTree() },
methods: {
async loadTree() {
this.internalLoading = true
try {
const response = await getAreaEqubookTreeSelect({ patroldeviceName: '', onlineStatus: '', patroldeviceTypeFlag: 'ipc' })
this.treeNodes = Array.isArray(response.data) ? response.data : []
this.expandedKeys = []
const walk = nodes => (nodes || []).forEach(node => { if (node.children && node.children.length) { this.expandedKeys.push(node.id); walk(node.children) } })
walk(this.treeNodes)
this.refreshTree()
const first = this.channelNodes[0]
if (first && !this.selectedCode) this.$emit('open', this.normalizeChannel(first))
} catch (error) {
this.treeNodes = []
this.$message.error(error.message || '设备树加载失败')
} finally { this.internalLoading = false }
},
refreshTree() { this.treeData = filterTree(this.treeNodes, this.keywords, this.onlineStatus) },
toCameraCode(node) { return node.cameraCode || node.channelCode || '' },
normalizeChannel(node) {
return { ...node, cameraCode: this.toCameraCode(node), name: node.label || node.name, channel: node.channelCode, nvrIp: node.nvrIp || '' }
},
handleNodeClick(node) {
if (!node.channelFlag) return
const channel = this.normalizeChannel(node)
this.$emit('open', channel)
},
nodeIcon(node) {
if (node.channelFlag) return 'el-icon-video-camera'
if (node.patroldeviceType == 13) return 'el-icon-position'
if (node.areaFlag === 'Yes') return 'el-icon-location-outline'
return 'el-icon-connection'
}
}
}
</script>
<style lang="scss" scoped>
.nvr-tree { width: 280px; min-width: 280px; height: 100%; display: flex; flex-direction: column; border-right: 1px solid rgba(41,139,255,.55); background: transparent; color: #d7e9ff; }
.tree-title { height: 46px; padding: 0 14px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid rgba(41,139,255,.35); color: #54ffff; font-size: 14px; }
.tree-title i { margin-right: 6px; }
.tree-title strong { min-width: 28px; text-align: center; color: #b7d8ff; }
.tree-filters { display: flex; gap: 6px; padding: 10px 8px; }
.tree-filters .el-select { width: 78px; }
.tree-filters .el-input { flex: 1; min-width: 0; }
.tree-filters .el-button { flex: 0 0 32px; padding: 0; border-color: #2787ed; color: #54ffff; background: #0a2b67; }
.tree-filters .el-button:hover, .tree-filters .el-button:focus { border-color: #2787ed; color: #ffffff; background: #176bc2; }
.tree-content { flex: 1; min-height: 0; overflow: auto; padding: 0 8px 8px; scrollbar-width: none; -ms-overflow-style: none; }
.tree-content::-webkit-scrollbar { display: none; }
.custom-tree-node { display: flex; align-items: center; width: 100%; gap: 6px; font-size: 14px; }
.node-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.online-dot, .offline-dot { width: 7px; height: 7px; border-radius: 50%; background: #20d47a; }
.offline-dot { background: #f05b68; }
.tree-empty { padding: 40px 0; text-align: center; color: #7298c3; }
.tree-empty i { display: block; margin-bottom: 10px; font-size: 28px; }
.tree-tip { padding: 9px 12px; border-top: 1px solid rgba(41,139,255,.3); color: #6f9ecc; font-size: 11px; }
::v-deep .el-tree { background: transparent; color: inherit; font-size: 14px; }
::v-deep .el-tree-node__content { height: 34px; }
::v-deep .el-tree-node__content:hover, ::v-deep .el-tree-node:focus > .el-tree-node__content { background: transparent; }
::v-deep .el-tree-node.is-current > .el-tree-node__content { color: #54ffff; background: transparent; }
</style>