|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="node" ref="container">
|
|
|
|
|
|
<!-- 主节点 -->
|
|
|
|
|
|
<div class="node-main" ref="nodeMain">
|
|
|
|
|
|
<div class="node-icon" ref="nodeIcon"></div>
|
|
|
|
|
|
<div class="node-title">{{ data.locationAlias }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 子节点 -->
|
|
|
|
|
|
<div class="children">
|
|
|
|
|
|
<div v-for="(child, index) in data.children" :key="index" class="child-item" :ref="(el) => (childRefs[index] = el)">
|
|
|
|
|
|
<div class="child-icon"></div>
|
|
|
|
|
|
<div class="child-info">
|
|
|
|
|
|
<div class="child-name">
|
|
|
|
|
|
工位名称: <span style="font-size: 12px">{{ child.locationAlias }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="child-code">编码:{{ wsData[data.deviceId]?.epcStr?.trimStart()}}</div>
|
|
|
|
|
|
<div class="child-time">时间:{{ parseTime(wsData[data.deviceId]?.recordTime) }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- SVG 折线 -->
|
|
|
|
|
|
<svg class="connector" ref="svg">
|
|
|
|
|
|
<polyline v-for="(line, index) in lines" :key="index" :points="line" stroke="#0156A5" fill="none" stroke-width="1" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, reactive, onMounted, nextTick, watchEffect } from 'vue';
|
|
|
|
|
|
import {parseTime} from '@/utils/ruoyi.js'
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
data: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
required: true
|
|
|
|
|
|
},
|
|
|
|
|
|
wsData: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
required: true
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const container = ref(null);
|
|
|
|
|
|
const nodeIcon = ref(null);
|
|
|
|
|
|
const childRefs = ref([]);
|
|
|
|
|
|
const lines = reactive(Array(props.data.children.length).fill(''));
|
|
|
|
|
|
const svg = ref(null);
|
|
|
|
|
|
|
|
|
|
|
|
const updateLines = () => {
|
|
|
|
|
|
if (!container.value || !nodeIcon.value) return;
|
|
|
|
|
|
const containerRect = container.value.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
childRefs.value.forEach((childEl, index) => {
|
|
|
|
|
|
if (!childEl) return;
|
|
|
|
|
|
|
|
|
|
|
|
const nodeRect = nodeIcon.value.getBoundingClientRect();
|
|
|
|
|
|
const childRect = childEl.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
const startX = nodeRect.right - containerRect.left;
|
|
|
|
|
|
const startY = nodeRect.top + nodeRect.height / 2 - containerRect.top;
|
|
|
|
|
|
const endX = childRect.left - containerRect.left;
|
|
|
|
|
|
const endY = childRect.top + childRect.height / 2 - containerRect.top;
|
|
|
|
|
|
|
|
|
|
|
|
const midX = startX + (endX - startX) / 2;
|
|
|
|
|
|
|
|
|
|
|
|
lines[index] = `${startX},${startY} ${midX},${startY} ${midX},${endY} ${endX},${endY}`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 更新 SVG 大小
|
|
|
|
|
|
const rect = container.value.getBoundingClientRect();
|
|
|
|
|
|
svg.value.setAttribute('width', rect.width);
|
|
|
|
|
|
svg.value.setAttribute('height', rect.height);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
updateLines();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 如果子节点数量或容器大小动态变化,可以监听更新折线
|
|
|
|
|
|
watchEffect(() => {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
updateLines();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.node {
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
width: 360px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
vertical-align: top;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|
break-inside: avoid;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
margin-left: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 主节点 */
|
|
|
|
|
|
.node-main {
|
|
|
|
|
|
width: 100px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.node-icon {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
|
width: 56px;
|
|
|
|
|
|
height: 56px;
|
|
|
|
|
|
background-image: url('@/assets/chart/906cbf773b3a37fc27e380375606fb27.png');
|
|
|
|
|
|
background-size: 100% 100%;
|
|
|
|
|
|
background-repeat: no-repeat;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.node-title {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: calc(50% + 30px);
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
font-size: 1vw;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 子节点列表 */
|
|
|
|
|
|
.children {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 260px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.child-item {
|
|
|
|
|
|
background: rgba(0, 255, 180, 0.15);
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
height: 75px;
|
|
|
|
|
|
margin-left: 20px;
|
|
|
|
|
|
background-image: url('@/assets/chart/efc42f5a4ed9491a16f6817d7fbe2336.png');
|
|
|
|
|
|
background-size: 100% 100%;
|
|
|
|
|
|
background-repeat: no-repeat;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.child-item:last-child {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.child-icon {
|
|
|
|
|
|
width: 50px;
|
|
|
|
|
|
height: 50px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
margin-right: 8px;
|
|
|
|
|
|
background-image: url('@/assets/chart/e06ce08b1a179471f9600f1d53ec3781.png');
|
|
|
|
|
|
background-size: 100% 100%;
|
|
|
|
|
|
background-repeat: no-repeat;
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
left: 12px;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.child-name {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 10px;
|
|
|
|
|
|
left: 74px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.child-code,
|
|
|
|
|
|
.child-time {
|
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.child-code {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 27px;
|
|
|
|
|
|
left: 74px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.child-time {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 46px;
|
|
|
|
|
|
left: 74px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* SVG 折线 */
|
|
|
|
|
|
.connector {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|