|
|
|
@ -0,0 +1,320 @@ |
|
|
|
<template> |
|
|
|
<aside class="portal-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="internalLoading" 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="top-start" |
|
|
|
:offset="6" |
|
|
|
:open-delay="300" |
|
|
|
:append-to-body="true" |
|
|
|
popper-class="portal-tree-tooltip" |
|
|
|
effect="dark" |
|
|
|
> |
|
|
|
<span class="custom-tree-node"> |
|
|
|
<i :class="nodeIcon(data)" /> |
|
|
|
<span class="node-label">{{ data.label || data.name }}</span> |
|
|
|
</span> |
|
|
|
</el-tooltip> |
|
|
|
</el-tree> |
|
|
|
<div v-if="!internalLoading && !treeData.length" class="tree-empty"> |
|
|
|
<i class="el-icon-video-camera-solid" /> |
|
|
|
<span>没有匹配的设备通道</span> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</aside> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script> |
|
|
|
import { getChannelTree } from "@/api/portal"; |
|
|
|
|
|
|
|
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.stationName || ""} ${node.patroldeviceCode || ""} ${node.channelCode || ""}`.toLowerCase(); |
|
|
|
const matchesKeyword = !keyword || text.includes(keyword.toLowerCase()); |
|
|
|
const matchesStatus = node.areaFlag === "Yes" || !onlineStatus || String(node.patroldeviceOnlineStatus || "") === onlineStatus; |
|
|
|
if ((matchesKeyword && matchesStatus) || children.length) result.push({ ...node, children }); |
|
|
|
return result; |
|
|
|
}, []); |
|
|
|
} |
|
|
|
|
|
|
|
export default { |
|
|
|
name: "PortalDeviceTree", |
|
|
|
props: { |
|
|
|
selectedCode: { type: String, default: "" } |
|
|
|
}, |
|
|
|
data() { |
|
|
|
return { |
|
|
|
treeNodes: [], |
|
|
|
treeData: [], |
|
|
|
expandedKeys: [], |
|
|
|
keywords: "", |
|
|
|
onlineStatus: "1", |
|
|
|
internalLoading: false, |
|
|
|
treeProps: { children: "children", label: "label" } |
|
|
|
}; |
|
|
|
}, |
|
|
|
computed: { |
|
|
|
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 getChannelTree({ onlineStatus: this.onlineStatus, patroldeviceTypeFlag: "ipc" }); |
|
|
|
this.treeNodes = Array.isArray(response && 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(); |
|
|
|
this.$emit("channels-loaded", this.channelNodes.map(this.normalizeChannel)); |
|
|
|
} catch (error) { |
|
|
|
this.treeNodes = []; |
|
|
|
this.treeData = []; |
|
|
|
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), |
|
|
|
cameraName: node.channelName || node.label || node.name, |
|
|
|
name: node.channelName || node.label || node.name, |
|
|
|
channel: node.channelCode |
|
|
|
}; |
|
|
|
}, |
|
|
|
handleNodeClick(node) { |
|
|
|
if (!node.channelFlag) return; |
|
|
|
this.$emit("open", this.normalizeChannel(node)); |
|
|
|
}, |
|
|
|
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> |
|
|
|
.portal-tree { |
|
|
|
width: 100%; |
|
|
|
min-width: 260px; |
|
|
|
height: 100%; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
border: 1px solid rgba(63, 127, 216, 0.72); |
|
|
|
border-radius: 2px; |
|
|
|
background: transparent; |
|
|
|
color: #d7e9ff; |
|
|
|
box-sizing: border-box; |
|
|
|
font-family: "PingFang SC", "Microsoft YaHei", sans-serif !important; |
|
|
|
} |
|
|
|
|
|
|
|
.tree-title { |
|
|
|
height: 46px; |
|
|
|
padding: 0 14px; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: space-between; |
|
|
|
border-bottom: 1px solid rgba(41, 139, 255, 0.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-content { |
|
|
|
flex: 1; |
|
|
|
min-height: 0; |
|
|
|
overflow: auto; |
|
|
|
padding: 0 0 8px; |
|
|
|
scrollbar-width: thin; |
|
|
|
scrollbar-color: #2f7fe8 rgba(6, 27, 79, 0.7); |
|
|
|
} |
|
|
|
|
|
|
|
.tree-content::-webkit-scrollbar { |
|
|
|
width: 8px; |
|
|
|
height: 8px; |
|
|
|
} |
|
|
|
|
|
|
|
.tree-content::-webkit-scrollbar-track { |
|
|
|
background: rgba(6, 27, 79, 0.7); |
|
|
|
} |
|
|
|
|
|
|
|
.tree-content::-webkit-scrollbar-thumb { |
|
|
|
background: #2f7fe8; |
|
|
|
border: 1px solid #0d327d; |
|
|
|
border-radius: 4px; |
|
|
|
} |
|
|
|
|
|
|
|
.custom-tree-node { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
width: max-content; |
|
|
|
min-width: 100%; |
|
|
|
gap: 6px; |
|
|
|
font-size: 14px; |
|
|
|
white-space: nowrap; |
|
|
|
} |
|
|
|
|
|
|
|
.node-label { |
|
|
|
white-space: nowrap; |
|
|
|
} |
|
|
|
|
|
|
|
.tree-empty { |
|
|
|
padding: 40px 0; |
|
|
|
text-align: center; |
|
|
|
color: #7298c3; |
|
|
|
} |
|
|
|
|
|
|
|
.tree-empty i { |
|
|
|
display: block; |
|
|
|
margin-bottom: 10px; |
|
|
|
font-size: 28px; |
|
|
|
} |
|
|
|
|
|
|
|
::v-deep .el-tree { |
|
|
|
display: inline-block; |
|
|
|
width: max-content; |
|
|
|
min-width: 100%; |
|
|
|
background: transparent; |
|
|
|
color: inherit; |
|
|
|
font-family: "PingFang SC", "Microsoft YaHei", sans-serif !important; |
|
|
|
font-size: 14px; |
|
|
|
} |
|
|
|
|
|
|
|
::v-deep .el-tree-node__content, |
|
|
|
::v-deep .custom-tree-node, |
|
|
|
::v-deep .node-label { |
|
|
|
font-family: "PingFang SC", "Microsoft YaHei", sans-serif !important; |
|
|
|
font-weight: 400 !important; |
|
|
|
} |
|
|
|
|
|
|
|
::v-deep .el-tree-node__content { |
|
|
|
display: flex; |
|
|
|
height: 24px; |
|
|
|
} |
|
|
|
|
|
|
|
::v-deep .el-tree-node__expand-icon.is-leaf { |
|
|
|
display: inline-block; |
|
|
|
width: 12px; |
|
|
|
} |
|
|
|
|
|
|
|
::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> |
|
|
|
|
|
|
|
<style lang="scss"> |
|
|
|
.portal-tree-tooltip.el-tooltip__popper { |
|
|
|
color: #eaf6ff; |
|
|
|
background: #176bc2; |
|
|
|
border: 1px solid #4ba3ff; |
|
|
|
} |
|
|
|
|
|
|
|
.portal-tree-tooltip.el-tooltip__popper[x-placement^="top"] .popper__arrow { |
|
|
|
border-top-color: #176bc2; |
|
|
|
} |
|
|
|
</style> |