|
|
<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>
|