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.
 
 
 
 

380 lines
10 KiB

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