项目需求架构图
实现代码
index.vue
<template>
<!-- 外层div -->
<div class="topu-container" :style="{ minWidth: `${functionDomainList.length * 330}px` }">
<!-- 头部显示 -->
<div class="topu-heard">
<!-- 网关 -->
<el-tooltip effect="dark" :content="`供应商:${gatewayObj.supplierName || ''}`" placement="top">
<div class="topu-gateway" @click="handleNodeClick(gatewayObj)">
<span>{{ gatewayObj.ecuTypeCode || '' }}</span>
</div>
</el-tooltip>
<!-- 域 -->
<div class="topu-domain-list">
<div class="topu-domain" v-for="(item, index) in functionDomainList" :key="index">{{ item }}</div>
</div>
<el-checkbox @click.stop.native="() => { }" @change="$event => handleChange($event, gatewayObj)"
v-if="selection && gatewayObj.logicalEcuAddressHex && gatewayObj.status != 3"></el-checkbox>
</div>
<!-- ECU树结构 -->
<div class="topu-body">
<!-- 获取指定域下的ECU节点 -->
<div class="ecu-tree" v-for="(domain, index) in functionDomainList" :key="index">
<div>
<template v-for="item in getNodeTree(domain)">
<!-- 存在下属节点 -->
<EcuNodeTree v-if="item.children && item.children.length > 0" :node="item" :selection="selection"
:busType="busType" @change="handleChange" @click="handleNodeClick"
:key="`node-tree-${item.id}`" />
<!-- 不存在下属节点 -->
<el-tooltip v-else :key="`node-${item.id}`" effect="dark"
:content="`供应商:${item.supplierName || ''}`" placement="top">
<div class="ecu-node"
:class="[backgroundMap[item.status], getBusTypeColor(item)]"
@click="handleNodeClick(item)">
<span>{{ item.ecuTypeCode || '' }}</span>
<el-checkbox @click.stop.native="() => { }" @change="$event => handleChange($event, item)"
v-if="selection && item.logicalEcuAddressHex && item.status != 3"></el-checkbox>
</div>
</el-tooltip>
</template>
</div>
<!-- 以太网总线类型存在-横线 -->
<template v-for="key in getBusEthernetIndexList(getNodeTree(domain))">
<div class="ethernet-node" :style="{ top: `${getBusEthernetXTop(key)}px` }">{{ key }}</div>
</template>
<!-- Ethernet需要单独右侧一条线 -->
<div class="bus-ethernet" :style="{ height: `${getBusEthernetHeight(getNodeTree(domain))}px` }"></div>
</div>
</div>
<!-- 尾部显示 -->
<div class="topu-footer">
<div class="bus-legend">
<div class="bus-item" v-for="key in Object.keys(busColos)">
<div class="bus-line" :class="busColos[key]"></div>
<div class="ml-sm">{{ key }}</div>
</div>
</div>
<div class="status-legend">
<div class="legend-item" v-for="item in legendList">
<div class="legend-square" :class="item.color"></div>
<div>{{ item.text }}</div>
</div>
</div>
</div>
</div>
</template>
<script>
import EcuNodeTree from './ecuNode.vue'
import { arr2tree } from 'utils'
import { apiQueryBusPage } from 'api/CarManage/CarPartManage';
export default {
components: { EcuNodeTree },
props: {
// 总线类型颜色
busColos: {
type: Object,
default: () => {
return {
CAN: 'can',
CANFD: 'canfd',
Ethernet: 'ethernet',
LIN: 'lin',
KLIN: 'klin',
Other: 'other'
}
}
},
legendList: {
type: Array,
default: () => {
return [
{ value: 3, color: 'yellow', text: 'ECU无诊断数据' },
{ value: 2, color: 'red', text: 'ECU存在故障' },
{ value: 1, color: 'green', text: 'ECU不存在故障' },
{ value: 4, color: 'gray', text: 'ECU无通讯' }
]
}
},
// 节点信息
node: {
type: Array,
default: () => {
return []
}
},
// 域
functionDomainList: {
type: Array,
default: () => {
return []
}
},
// node背景色标识
backgroundMap: {
type: Object,
default: () => {
return {
0: '',
1: 'green',
2: 'red',
3: 'yellow',
4: 'gray',
5: 'gray'
}
}
},
// 是否可选
selection: {
type: Boolean,
default: false
}
},
data () {
return {
busType: {},
// 选中项
selections: []
}
},
computed: {
// 获取网关信息
gatewayObj () {
// 不存在父节点
return this.node.find(el => !el.parentId)
},
// 节点转换为树结构
ecuList () {
const list = this.node.filter(el => el.parentId)
return arr2tree(list)
}
},
created () {
this.getBusType()
},
methods: {
/**
* 获取指定域下的ECU节点
* @param {*} funcDomainCode 域名标识
*/
getNodeTree (funcDomainCode) {
return this.ecuList.filter(el => el.funcDomainCode == funcDomainCode)
},
/**
* 递归的方式获取指定域下的ECU节点中存在ethernet总线时的位置计算高度
* @param {*} nodelist 域下的节点
* @param {*} nodeLeveIndex 当前节点位置 位置从1计算不分层级
* @param {*} nodeIndex[] 存在ethernet总线时的节点位置
*/
getBusEthernet (nodelist, nodeLeveIndex = 0, nodeIndex = []) {
nodelist.forEach(el => {
// 当前节点位置默认位置
nodeLeveIndex += 1
// 判断是否存在子节点
if (el?.children?.length > 0) {
this.getBusEthernet(el.children, nodeLeveIndex, nodeIndex)
} else {
// 存在Ethernet总线时,存入
if (this.hasEthernet(el)) {
nodeIndex.push(nodeLeveIndex)
}
}
});
return nodeIndex
},
// 获取以太网总线类型总长度 - y轴
getBusEthernetHeight (nodelist) {
const nodeList = this.getBusEthernet(nodelist, 0, [])
const [ lastIndex ] = [ ...nodeList ].reverse()
return lastIndex ? ((lastIndex - 1) * 57 + 35) : 0
},
// 获取以太网总线类型 - x轴
getBusEthernetIndexList (nodelist) {
return this.getBusEthernet(nodelist, 0, []) || []
},
// 计算以太网总线类型 - x轴Top值
getBusEthernetXTop (index) {
return (index - 1) * 57 + 35
},
/**
* 获取总线类型颜色汇总
* @param {*} param0 当前几点总线类型
*/
getBusTypeColor ({ busType }) {
const colors = busType.split(',').map(el => {
return this.busColos[this.busType[Number(el)]] || 'other'
})
return colors
},
// 是否存在Ethernet 总线
hasEthernet ({ busType }) {
const typeList = busType.split(',').map(el => {
return this.busType[Number(el)]
})
const index = typeList.findIndex(el => el == 'Ethernet')
return index != -1
},
// 获取总线类型
async getBusType () {
const { code, data, msg } = await this.$ajax(apiQueryBusPage, { pageSize: 200, pageNumber: 1 })
if (code === this.$OK) {
this.busType = {
...Object.fromEntries(data.list.map(el => [ el.id, el.busType ]))
}
} else {
this.$message.error(msg)
}
},
/**
* 多选框选择事件
* @param {*} val 选中结果
* @param {*} node 节点
*/
handleChange (val, node) {
if (val) {
this.selections.push(node)
} else {
const index = this.selections.findIndex(el => el.id == node.id)
this.selections.splice(index, 1)
}
this.$emit('check', this.selections)
},
// 获取选中项
getChecked () {
return this.selections
},
// node点击事件
handleNodeClick (item) {
this.$emit('click', item)
}
}
}
</script>
<style lang="scss" scoped>
@import "../ecuTopu.scss";
</style>
子节点代码-ecuNode.vue
<template>
<div class="ecu-tree-node">
<el-tooltip effect="dark" :content="`供应商:${node.supplierName || ''}`" placement="top">
<div class="ecu-sub-node" :class="[backgroundMap[node.status], getBusTypeColor(node)]"
@click="$emit('handleNodeClick', node)">
<span>{{ node.ecuTypeCode || '' }}</span>
<el-checkbox @click.stop.native="() => { }" @change="$event => emit('change', $event, node)"
v-if="selection && node.logicalEcuAddressHex && node.status != 3"></el-checkbox>
</div>
</el-tooltip>
<div class="ecu-tree">
<template v-for="item in node.children">
<EcuNodeTree v-if="item.children && item.children.length > 0" :node="item" :selection="selection"
:busType="busType" @change="$event => $emit('change', $event)"
@click="$event => $emit('click', $event)" />
<el-tooltip v-else effect="dark" :content="`供应商:${item.supplierName || ''}`" placement="top">
<div class="ecu-node" :class="[backgroundMap[item.status], getBusTypeColor(item)]"
@click="$emit('click', item)">
<span>{{ item.ecuTypeCode || '' }}</span>
<el-checkbox @click.stop.native="() => { }" @change="$event => $emit('change', $event, item)"
v-if="selection && item.logicalEcuAddressHex && item.status != 3"></el-checkbox>
</div>
</el-tooltip>
</template>
</div>
</div>
</template>
<script>
export default {
name: 'EcuNodeTree',
props: {
node: {
type: Object,
default: () => { }
},
busType: {
type: Object,
default: () => { }
},
// 总线类型颜色
busColos: {
type: Object,
default: () => {
return {
CAN: 'can',
CANFD: 'canfd',
Ethernet: 'ethernet',
LIN: 'lin',
KLIN: 'klin',
Other: 'other'
}
}
},
backgroundMap: {
type: Object,
default: () => {
return {
0: '',
1: 'green',
2: 'red',
3: 'yellow',
4: 'gray',
5: 'gray'
}
}
},
// 是否可选
selection: {
type: Boolean,
default: false
}
},
methods: {
/**
* 获取总线类型颜色汇总
* @param {*} param0 当前几点总线类型
*/
getBusTypeColor ({ busType }) {
const colors = busType.split(',').map(el => {
return this.busColos[this.busType[Number(el)]] || 'other'
})
return colors
},
// 是否存在Ethernet 总线
hasEthernet ({ busType }) {
const typeList = busType.split(',').map(el => {
return this.busType[Number(el)]
})
const index = typeList.findIndex(el => el == 'Ethernet')
return index != -1
}
}
}
</script>
<style lang="scss" scoped>
@import "../../style/ecuTopu.scss";
</style>
CSS部分
.topu-container {
--node-height: '40px';
position: relative;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
background-color: #fff;
padding: 20px;
overflow-x: auto;
.topu-heard {
position: relative;
display: flex;
flex-direction: column;
border-radius: 4px;
padding: 0 20px;
border: 1px solid #DCDFE6;
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.01);
background: #EBECF0;
.topu-gateway {
position: relative;
text-align: center;
height: 42px;
line-height: 42px;
font-weight: bold;
}
.topu-domain-list {
display: flex;
flex-direction: row;
justify-content: space-between;
.topu-domain {
height: 42px;
line-height: 42px;
min-width: 300px;
flex: 1;
}
}
.el-checkbox {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
}
}
.topu-body {
position: relative;
display: flex;
flex: 1;
justify-content: space-between;
overflow-y: auto;
.ecu-tree {
position: relative;
width: 300px;
margin-left: 40px;
height: 100%;
.ecu-sub-node,
.ecu-node {
position: relative;
width: 200px;
height: var(--node-height);
line-height: 40px;
margin-top: 15px;
padding: 0 20px;
box-sizing: border-box;
border-radius: 4px;
background: #EBECF0;
border: 1px solid #DCDFE6;
z-index: 2;
.el-checkbox {
position: absolute;
right: 10px;
top: -3px;
}
}
.bus-ethernet {
position: absolute;
top: 0;
right: 30px;
width: 1.5px;
background: #F154B1;
}
.ethernet-node{
// tree节点margin-left:40px
// ethernetY轴 right: 30px
width: calc(100% - 70px);
height: 1.5px;
left: 40px;
position: absolute;
background: #F154B1;
}
}
}
.topu-footer {
display: flex;
justify-content: space-between;
padding: 0 30px;
height: 40px;
align-items: end;
.bus-legend {
display: flex;
.bus-item {
display: flex;
align-items: center;
margin-right: 20px;
.bus-line {
width: 20px;
height: 3px;
}
}
}
.status-legend {
display: flex;
justify-content: flex-end;
.legend-item {
display: flex;
flex-direction: row;
align-items: center;
font-size: 12px;
margin-right: 20px;
.legend-square {
width: 15px;
height: 15px;
margin-right: 5px;
}
}
}
}
}
.ecu-node {
&::before {
position: absolute;
content: '';
width: 3px;
height: 57px;
left: -21px;
top: -16px;
transform: scale(0.5, 1);
background-color: #1989FA;
}
&:last-of-type {
&::before {
height: 38px !important;
}
}
&::after {
position: absolute;
content: '';
width: 20px;
height: 3px;
left: -21px;
top: 20px;
transform: scale(1, 0.5);
}
}
.ecu-tree-node {
position: relative;
.ecu-node {
width: 160px !important;
}
&::before {
position: absolute;
content: '';
width: 3px;
height: calc(100% + 50px);
left: -20.2px;
top: -16px;
transform: scale(0.5, 1);
background-color: #1989FA;
}
&:last-of-type {
&::before {
height: 55px;
top: -32px;
}
}
}
.ecu-tree-node {
.ecu-tree-node {
&::before {
position: absolute;
content: '';
width: 3px;
height: calc(100% + 70px);
left: -21px;
top: -16px;
transform: scale(0.5, 1);
background-color: #1989FA;
}
}
}
.ecu-sub-node {
&::after {
position: absolute;
content: '';
width: 20px;
height: 3px;
left: -21px;
top: 20px;
transform: scale(1, 0.5);
background-color: #1989FA;
}
}
.can {
background: #F56C6C;
&::after {
background-color: #F56C6C;
}
}
.canfd {
background: #EA7232;
&::after {
background-color: #EA7232;
}
}
.ethernet {
background: #F154B1;
}
.lin {
background: #67C23A;
&::after {
background-color: #67C23A;
}
}
.klin {
background: #48B1DB;
&::after {
background-color: #48B1DB;
}
}
.other {
background: #8C64D0;
&::after {
background-color: #8C64D0;
}
}
.green {
background: #67C23A !important;
}
.yellow {
background: #E6A23C !important;
}
.red {
background: #F56C6C !important;
}
.gray {
background: #C0C4CC !important;
}