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.
 
 
 
 

320 lines
7.9 KiB

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