<template>
|
|
<aside class="portal-tree">
|
|
<div class="tree-title">
|
|
<span><i class="el-icon-s-platform" /> 设备列表</span>
|
|
<strong>{{ visibleChannelCount }}</strong>
|
|
</div>
|
|
<div class="tree-filters">
|
|
<div class="tree-filter-bar">
|
|
<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-button size="small" icon="el-icon-refresh" title="刷新列表" @click="loadTree" />
|
|
</div>
|
|
<el-input
|
|
v-model.trim="keywords"
|
|
class="tree-search"
|
|
size="small"
|
|
clearable
|
|
prefix-icon="el-icon-search"
|
|
placeholder="搜索"
|
|
@input="refreshTree"
|
|
/>
|
|
</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" :class="{ 'is-offline': isOffline(data) }">
|
|
<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: "" },
|
|
// 只跟首页场站选择,不吃动环巡检右上角的站
|
|
stationCode: { type: String, default: "" },
|
|
// 摄像头来源:动环 / 消防 / 安防;空表示不按来源过滤
|
|
deviceSource: { type: String, default: "" }
|
|
},
|
|
data() {
|
|
return {
|
|
treeNodes: [],
|
|
treeData: [],
|
|
expandedKeys: [],
|
|
keywords: "",
|
|
onlineStatus: "1",
|
|
internalLoading: false,
|
|
treeRequestId: 0,
|
|
treeProps: { children: "children", label: "label" }
|
|
};
|
|
},
|
|
computed: {
|
|
channelNodes() {
|
|
return flatten(this.treeNodes).filter(node => node.channelFlag);
|
|
},
|
|
// 标题数字跟当前筛选走:在线/离线/搜索后只统计树上还看得到的通道
|
|
visibleChannelCount() {
|
|
return flatten(this.treeData).filter(node => node.channelFlag).length;
|
|
}
|
|
},
|
|
watch: {
|
|
onlineStatus: "refreshTree",
|
|
stationCode() {
|
|
this.loadTree();
|
|
},
|
|
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() {
|
|
const requestId = ++this.treeRequestId;
|
|
this.internalLoading = true;
|
|
this.treeNodes = [];
|
|
this.treeData = [];
|
|
this.expandedKeys = [];
|
|
try {
|
|
const query = { onlineStatus: this.onlineStatus, patroldeviceTypeFlag: "ipc" };
|
|
if (this.stationCode) query.stationCode = this.stationCode;
|
|
if (this.deviceSource) query.deviceSource = this.deviceSource;
|
|
const response = await getChannelTree(query, { skipWorkspaceStation: true });
|
|
if (requestId !== this.treeRequestId) return;
|
|
const nodes = Array.isArray(response && response.data) ? response.data : [];
|
|
this.treeNodes = this.stationCode ? this.keepStation(nodes, this.stationCode) : nodes;
|
|
const walk = items => (items || []).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) {
|
|
if (requestId !== this.treeRequestId) return;
|
|
this.treeNodes = [];
|
|
this.treeData = [];
|
|
this.$message.error(error.message || "设备树加载失败");
|
|
} finally {
|
|
if (requestId === this.treeRequestId) this.internalLoading = false;
|
|
}
|
|
},
|
|
keepStation(nodes, stationCode) {
|
|
const matched = this.filterStation(nodes, stationCode);
|
|
return matched.length ? matched : nodes;
|
|
},
|
|
filterStation(nodes, stationCode) {
|
|
return (nodes || []).reduce((result, node) => {
|
|
if (node.channelFlag) {
|
|
if (String(node.stationCode || "") === String(stationCode)) result.push(node);
|
|
return result;
|
|
}
|
|
const children = this.filterStation(node.children, stationCode);
|
|
if (String(node.stationCode || "") === String(stationCode) || children.length) {
|
|
result.push({ ...node, children });
|
|
}
|
|
return result;
|
|
}, []);
|
|
},
|
|
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));
|
|
},
|
|
isOffline(node) {
|
|
return !!node.channelFlag && String(node.patroldeviceOnlineStatus) === "0";
|
|
},
|
|
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;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
padding: 10px 8px;
|
|
}
|
|
|
|
.tree-filter-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 6px;
|
|
}
|
|
|
|
.tree-filter-bar .el-select {
|
|
width: 88px;
|
|
}
|
|
|
|
.tree-search {
|
|
width: 100%;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.custom-tree-node.is-offline {
|
|
color: #7f8da8;
|
|
}
|
|
</style>
|
|
|
|
<style lang="scss">
|
|
.portal-tree-tooltip.el-tooltip__popper {
|
|
max-width: 420px;
|
|
padding: 8px 12px;
|
|
color: #eaf6ff;
|
|
background: #0b205b;
|
|
border: 1px solid #3f7fd8;
|
|
border-radius: 4px;
|
|
font-size: calc(14px * var(--portal-scale, 1));
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.portal-tree-tooltip.el-tooltip__popper[x-placement^="top"] .popper__arrow {
|
|
border-top-color: #0b205b;
|
|
}
|
|
</style>
|