JavaProjectRepo/business-css/frontend/src/components/antvx6/viewx6.vue

1033 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- AntV G6 Graph Component -->
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElMessage } from "element-plus";
import { useRoute,useRouter } from 'vue-router';
import {
Clipboard,
Graph,
History,
Keyboard,
Selection,
Shape,
Snapline,
Transform
} from '@antv/x6'
// @ts-ignore
import { updateProjects,projectsById} from "@/api/business/project";
import { getByScenario } from "@/api/business/scenario";
import textimg from '@/assets/x6/text.png'
import echartsimg from '@/assets/x6/charts.png'
import TableModels from './tableModel.vue';
import EchartsModels from './echartsModel.vue';
const route = useRoute();
const router = useRouter()
const emit = defineEmits([ 'closeAntvx6']);
const props = defineProps({
projectInfo: {
required: false,
type: Object,
default: {}
},
})
const deviceTypetype:any = ref('') // 设备类型
const isAdddevice = ref(false) // 是否添加设备
const isEditdevice = ref(false) // 是否编辑设备
const projectInfo:any = ref({}) // 项目信息
const isScenario = ref(false) //是否展示历史模拟场景
const isDisplay = ref(true) // 是否显示
const isExpansionandcontraction = ref(false) // 是否显示展开收起按钮
const scenarioResults:any = ref({}) // 设备信息
const deviceId = ref('') // 设备id
const customImageData:any = ref([]) // 图形化设备数组
// 为了协助代码演示
let graph: Graph
const scenarioId:any = ref('') // 场景id
function getScenarioResults(){
scenarioResults.value = {}
getByScenario({
scenarioId: scenarioId.value,
pageNum:1,
pageSize:999
}).then((res:any) => {
console.log(res.data.records)
scenarioResults.value = groupByDeviceId(res.data.records)
for (const key in scenarioResults.value) {
if (!Object.hasOwn(scenarioResults.value, key)) continue;
const element = scenarioResults.value[key];
for(let i = 0;i<customImageData.value.length;i++){
if(key == customImageData.value[i].id){
customImageData.value[i].scenarioResults = element
addAttrText(customImageData.value[i])
}
}
}
})
}
function addAttrText(item:any){
// item.attrs.text = item.deviceInfo.name
graph.addNode({
x: item.position.x ,
y: item.position.y + 150,
width: 100,
height: 40,
label: item.deviceInfo.name,
attrs: {
body: {
stroke: 'transparent',
fill: 'transparent',
strokeWidth: 1,
},
text: {
text: '',
fill: '#363636',
fontSize: 16,
},
},
})
graph.addNode({
x: item.position.x ,
y: item.position.y + 180,
width: 100,
height: 30,
label: 'keff' + item.scenarioResults[item.scenarioResults.length - 1].keffValue,
attrs: {
body: {
stroke: 'transparent',
fill: 'transparent',
strokeWidth: 1,
},
text: {
text: '',
fill: item.scenarioResults[item.scenarioResults.length - 1].keffValue > 0.98 ? '#ff4d4f' : '#363636',
fontSize: 14,
},
},
})
graph.addNode({
shape: 'image-node',
x: item.position.x + 135 ,
y: item.position.y + 186,
width: 14,
height: 14,
correlationId: item.id,
type:'echarts',
attrs: {
img: {
width: 14,
height: 14,
'xlink:href': echartsimg,
},
label: {
text: '',
},
},
})
graph.addNode({
shape: 'image-node',
x: item.position.x + 110 ,
y: item.position.y + 186,
width: 14,
height: 14,
correlationId: item.id,
type:'text',
attrs: {
img: {
width: 14,
height: 14,
'xlink:href': textimg,
},
label: {
text: '',
},
},
})
let tempData:any = []
if(item.scenarioResults[item.scenarioResults.length - 1].attrState){
let attrState = JSON.parse(item.scenarioResults[item.scenarioResults.length - 1].attrState)
for (const key in attrState) {
if (!Object.hasOwn(attrState, key)) continue;
const element = attrState[key];
tempData.push({
name: key,
value: element
})
}
}
appendAttrText(item,tempData)
}
function getName(code:any) {
let name = ''
switch (code) {
case 'width':
return name = "宽度cm";
break;
case 'height':
return name = "高度cm";
break;
case 'length':
return name = "长度cm";
break;
case 'diameter':
return name = "外径cm";
break;
case 'volume':
return name = "体积单位L";
break;
case 'flow_rate':
return name = "流量单位m3/h";
break;
case 'pulse_velocity':
return name = "脉冲速度单位Hz";
break;
case 'u_concentration':
return name = "铀浓度g/L";
break;
case 'uo2_density':
return name = "氧化铀密度g/cm3";
break;
case 'u_enrichment':
return name = "铀富集度(%";
break;
case 'pu_concentration':
return name = "钚浓度g/L";
break;
case 'puo2_density':
return name = "氧化钚密度g/cm3";
break;
case 'pu_isotope':
return name = "钚同位素比例PU-240占比%";
break;
case 'hno3_acidity':
return name = "硝酸酸度mol/L";
break;
case 'h2c2o4_concentration':
return name = "草酸浓度mol/L";
break;
case 'organic_ratio':
return name = "有机相比例%";
break;
case 'moisture_content':
return name = "含水率%";
break;
default:
return name = "";
}
return name
}
function appendAttrText(item:any,data:any){
if(isDisplay.value == false){
return
}
for(let i = 0;i<data.length;i++){
graph.addNode({
x: item.position.x,
y: item.position.y + 203 + i * 25,
width: 260,
height: 30,
// label: data[i].name + '' + data[i].value,
attrs: {
body: {
stroke: 'transparent',
fill: 'transparent',
strokeWidth: 1,
},
label: {
textAnchor: 'left',
refX: 0,
text: getName(data[i].name) + '' + data[i].value,
textWrap: {
width: 250,
height: 30,
ellipsis: true,
},
},
text: {
text: '',
fill: '#363636', // 蓝色文字
fontSize: 12,
},
},
})
}
}
function groupByDeviceId(data:any) { // 按设备id分组
// 边界检查确保输入是数组
if (!Array.isArray(data)) {
throw new Error('输入必须是数组类型');
}
return data.reduce((acc, item) => {
// 确保deviceId存在且不为空
const key = item.deviceId;
if (!key) {
console.warn('跳过无deviceId的元素:', item);
return acc;
}
// 若该deviceId分组不存在则初始化空数组
if (!acc[key]) {
acc[key] = [];
}
// 将当前元素加入对应分组
acc[key].push(item);
return acc;
}, {}); // 初始值为空对象
}
onMounted(() => {
scenarioId.value = route.query.scenarioId
// #region 初始化画布
graph = new Graph({
container: document.getElementById('graph-container') as HTMLElement,
grid: true,
mousewheel: {
enabled: true,
zoomAtMousePosition: true,
modifiers: 'ctrl',
minScale: 0.5,
maxScale: 3,
},
connecting: {
router: 'manhattan',
connector: {
name: 'rounded',
args: {
radius: 8,
},
},
anchor: 'center',
connectionPoint: 'anchor',
allowBlank: false,
snap: {
radius: 20,
},
createEdge() {
return new Shape.Edge({
attrs: {
line: {
stroke: '#A2B1C3',
strokeWidth: 2,
strokeDasharray: 0,
targetMarker: {
name: 'block',
width: 12,
height: 8,
},
sourceMarker: null,
},
},
zIndex: 0,
})
},
validateConnection({ targetMagnet }) {
return !!targetMagnet
},
},
highlighting: {
magnetAdsorbed: {
name: 'stroke',
args: {
attrs: {
fill: '#5F95FF',
stroke: '#5F95FF',
},
},
},
},
interacting: function (cellView) {
// 如果是 'rect' 形状,则禁止节点移动
return { nodeMovable: false }
// 其他形状的节点允许所有交互
return true
}
// interacting: {
// nodeMovable: false, // 禁止所有节点移动
// },
})
const ports = {
groups: {
top: {
position: 'top',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#5F95FF',
strokeWidth: 1,
fill: '#fff',
style: {
visibility: 'hidden',
},
},
},
},
right: {
position: 'right',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#5F95FF',
strokeWidth: 1,
fill: '#fff',
style: {
visibility: 'hidden',
},
},
},
},
bottom: {
position: 'bottom',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#5F95FF',
strokeWidth: 1,
fill: '#fff',
style: {
visibility: 'hidden',
},
},
},
},
left: {
position: 'left',
attrs: {
circle: {
r: 4,
magnet: true,
stroke: '#5F95FF',
strokeWidth: 1,
fill: '#fff',
style: {
visibility: 'hidden',
},
},
},
},
},
items: [
{
group: 'top',
},
{
group: 'right',
},
{
group: 'bottom',
},
{
group: 'left',
},
],
}
Graph.registerNode(
'custom-image',
{
inherit: 'rect',
width: 130,
height: 100,
markup: [
{
tagName: 'rect',
selector: 'body',
},
{
tagName: 'image',
},
{
tagName: 'text',
selector: 'label',
},
],
attrs: {
body: {
stroke: 'transparent',
fill: '#f8f8f8',
},
image: {
width: 62,
height:84,
refX: 35,
refY: 0,
},
label: {
refX: 45,
refY: 80,
textAnchor: 'start',
textVerticalAnchor: 'top',
fontSize: 14,
fill: '#000',
},
},
ports: { ...ports },
},
true,
)
Graph.registerNode(
'image-node',
{
inherit: 'rect',
width: 120,
height: 60,
markup: [
{
tagName: 'rect',
selector: 'body',
},
{
tagName: 'image',
selector: 'img',
},
{
tagName: 'text',
selector: 'label',
},
],
attrs: {
body: {
stroke: 'transparent',
strokeWidth: 1,
fill: 'transparent',
rx: 6,
ry: 6,
},
img: {
'xlink:href': 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png',
width: 24,
height: 24,
x: 0,
y: 0,
},
label: {
text: 'Image Node',
fill: '#333',
fontSize: 12,
refX: 0.5,
refY: 0.5,
textAnchor: 'middle',
dy: 10,
},
'.': {
class: 'custom-image-node',
},
},
},
true,
)
// Graph.registerNode(
// 'image-echarts',
// {
// inherit: 'rect',
// width: 120,
// height: 60,
// markup: [
// {
// tagName: 'rect',
// selector: 'body',
// },
// {
// tagName: 'image',
// selector: 'img',
// },
// {
// tagName: 'text',
// selector: 'label',
// },
// ],
// attrs: {
// body: {
// stroke: 'transparent',
// strokeWidth: 1,
// fill: 'transparent',
// rx: 6,
// ry: 6,
// },
// img: {
// 'xlink:href': 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png',
// width: 24,
// height: 24,
// x: 0,
// y: 0,
// },
// label: {
// text: 'Image Node',
// fill: '#333',
// fontSize: 12,
// refX: 0.5,
// refY: 0.5,
// textAnchor: 'middle',
// dy: 10,
// },
// },
// },
// true,
// )
projectsById({projectId:route.query.projectId}).then((res:any) => {
if(res.topology != null && res.topology != ''){
projectInfo.value = res
if (!graph || !projectInfo.value || !projectInfo.value.topology) return;
graph.clearCells();
const topology:any = JSON.parse(projectInfo.value.topology)
customImageData.value = []
if(!topology.designData)return
for(let i = 0;i<topology.designData.length;i++){
if(topology.designData[i].shape == 'custom-image'){
customImageData.value.push(topology.designData[i])
}
}
graph.fromJSON(topology.designData);
getScenarioResults()
}
})
graph.on('node:click', ( e:any ) => {
const node = e.node
if(node == null)return
if(node.store == null)return
if(node.store.data == null)return
if(node.store.data.type == 'echarts'){
deviceId.value = node.store.data.correlationId
isEchartsModel.value = true
return
}
if(node.store.data.type == 'text'){
deviceId.value = node.store.data.correlationId
isTableModel.value = true
return
}
})
})
const left = ref(0)
const top = ref(0)
const isMenuShow = ref(false)
function bigClick(){
graph.zoom(0.1)
}
function smallClick(){
graph.zoom(-0.1)
}
const isTableModel = ref(false) // 表格弹窗
const isEchartsModel = ref(false) // 图表弹窗
function dialogTableModel(){ // 关闭变动设置弹窗
isTableModel.value = false;
}
function dialogEchartsModel(){ // 关闭变动设置弹窗
isEchartsModel.value = false;
}
function isDisplayClick(){
if(isDisplay.value == true){
isDisplay.value = false
let tempGraph:any = graph.toJSON().cells
for(let i = 0;i<tempGraph.length;i++){
if(tempGraph[i].shape == 'rect' || tempGraph[i].shape == 'image-node'){
graph.removeCell(tempGraph[i].id)
}
}
}else{
isDisplay.value = true
for (const key in scenarioResults.value) {
if (!Object.hasOwn(scenarioResults.value, key)) continue;
for(let i = 0;i<customImageData.value.length;i++){
if(key == customImageData.value[i].id){
addAttrText(customImageData.value[i])
}
}
}
}
}
</script>
<template>
<div class="app-layout" @click="isMenuShow = false">
<div class="antvx6-header">
<div class="header-left-box">
<!-- <div class="return-icon-box" @click="closeAntvx6" title="返回工作台">
<img src="@/assets/x6/return.png" alt="图标" style="cursor: pointer;">
</div>
-->
<div class="project-name">{{ projectInfo.name }}</div>
<!-- <div class="return-icon-box" @click="analysisAdd" title="新增模拟分析">
<img src="@/assets/x6/add.png" alt="图标" style="cursor: pointer;">
</div>
<div class="return-icon-box" @click="simulationClick" title="历史模拟分析">
<img src="@/assets/x6/history.png" alt="图标" style="cursor: pointer;">
</div> -->
</div>
<div class="header-content-box">
<div class="operation-icon-box" @click="bigClick">
<img src="@/assets/x6/magnify.png">
<div class="operation-icon-text">放大</div>
</div>
<div class="operation-icon-box" @click="smallClick">
<img src="@/assets/x6/reduce.png">
<div class="operation-icon-text">缩小</div>
</div>
<div class="operation-icon-box" @click="isDisplayClick">
<img v-if="isDisplay" src="@/assets/x6/display.png">
<img v-else src="@/assets/x6/hide.png">
<div class="operation-icon-text">显示</div>
</div>
</div>
<div class="header-left-box"></div>
</div>
<div id="graph-container" style="width: 100%; height: calc(100% - 60px);">
<div class="context-menu" v-if="isMenuShow" :style="{left: left +'px', top: top+'px'}">
<!-- <img src="@/assets/x6/info.png" alt="图标" title="设备信息" style="cursor: pointer;" @click="EditdeviceClick">
<img src="@/assets/x6/material.png" alt="图标" title="物料信息" style="cursor: pointer;" @click="MaterialModelClick">
<img src="@/assets/x6/change.png" alt="图标" title="变动设置" style="cursor: pointer;" @click="ChangesettingsClick">
<img src="@/assets/x6/copy.png" alt="图标" title="复制" style="cursor: pointer;" @click="copyNode">
<img src="@/assets/x6/del.png" alt="图标" title="删除" style="cursor: pointer;"
@click="deleteNode"> -->
</div>
</div>
<el-dialog v-model="isTableModel" :close-on-click-modal="false" :modal="false" draggable :before-close="dialogTableModel" title="设备分析列表" append-to-body width="1430px">
<TableModels v-if="isTableModel" :deviceId="deviceId" :scenarioId="scenarioId"/>
</el-dialog>
<el-dialog v-model="isEchartsModel" :close-on-click-modal="false" :modal="false" draggable :before-close="dialogEchartsModel" title="设备分析图像" append-to-body width="1430px">
<EchartsModels v-if="isEchartsModel" :deviceId="deviceId" :scenarioId="scenarioId"/>
</el-dialog>
</div>
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.app-layout {
position: fixed;
z-index: 10;
display: flex;
flex-direction: column;
width: 100vw;
height: 100vh;
font-family: 'Segoe UI', sans-serif;
}
.toolbar {
padding: 10px;
background-color: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.style-group {
display: flex;
flex-direction: column;
gap: 8px;
margin-right: 20px;
}
.style-group h4 {
margin: 0 0 5px 0;
font-size: 14px;
color: #333;
font-weight: 600;
}
.toolbar button {
padding: 8px 16px;
border: 1px solid #d9d9d9;
border-radius: 4px;
background-color: #fff;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
white-space: nowrap;
}
.toolbar button:hover {
border-color: #1890ff;
color: #1890ff;
}
.toolbar button:active {
background-color: #f0f0f0;
}
/* Sidebar Styles */
.sidebar {
width: 200px;
background-color: #ffffff;
border-right: 1px solid #e0e0e0;
overflow-y: auto;
padding: 16px;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
}
.sidebar-section {
margin-bottom: 24px;
}
.sidebar-section h3 {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.device-item {
display: flex;
align-items: center;
padding: 10px;
margin-bottom: 8px;
background-color: #fafafa;
border: 1px solid #e0e0e0;
border-radius: 4px;
cursor: grab;
transition: all 0.2s ease;
}
.device-item:hover {
background-color: #f0f5ff;
border-color: #1890ff;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.2);
}
.device-item:active {
cursor: grabbing;
}
.device-icon {
font-size: 24px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
}
.device-name {
font-size: 13px;
color: #333;
font-weight: 500;
}
/* Graph Container Styles */
.graph-container {
flex: 1;
position: relative;
background-color: #fafafa;
overflow: hidden;
width: 100%;
height: 100%;
}
/* Drag and drop visual feedback */
.graph-container.drag-over {
background-color: #e6f7ff;
border: 2px dashed #1890ff;
}
.context-menu {
position: absolute;
z-index: 100;
background-color: #fff;
border: 1px solid #dfe3e8;
border-radius: 4px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.15);
width: 240px;
height: 40px;
display: flex;
justify-content: space-around;
align-items: center;
}
</style>
<style>
.antvx6-header {
width: 100%;
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.header-left-box {
width: 400px;
height: 60px;
display: flex;
align-items: center;
}
.header-content-box {
display: flex;
align-items: center;
}
.return-icon-box {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin-right: 10px;
}
.return-icon-box:hover {
background-color: #eeeeee;
border-radius: 4px;
}
.antvx6-header .project-name {
font-family: 'Arial Negreta', 'Arial Normal', 'Arial';
font-weight: 700;
font-style: normal;
font-size: 16px;
color: #333333;
margin-right: 10px;
}
.antvx6-header .operation-icon-box {
width: 46px;
height: 46px;
display: flex;
align-content: center;
justify-content: center;
flex-wrap: wrap;
cursor: pointer;
margin-right: 20px;
font-family: '微软雅黑';
font-weight: 400;
font-style: normal;
font-size: 12px;
color: #4B4B4B;
padding-top: 5px;
}
.antvx6-header .operation-icon-box:hover {
background-color: #eeeeee;
border-radius: 4px;
}
.operation-icon-text {
width: 100%;
text-align: center;
padding-top: 5px;
}
.line-style-box{
position: absolute;
right: 0;
height: 100%;
/* width: 100px; */
/* background-color: #d9d9d9; */
z-index: 10;
display: flex;
align-items: center;
}
.expansionandcontraction-box{
width:15px;
height: 64px;
background-color: #ffffff;
border: 1px solid #cfcfcf;
border-radius: 10px 0 0 10px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.style-content-box{
width: 300px;
height: calc(100vh - 60px);
border: 1px solid #cfcfcf;
border-top: none;
border-right: none;
background: #fff;
}
.style-content-box-title{
width: 100%;
height: 40px;
line-height: 40px;
text-align: left;
font-size: 14px;
font-family: 'Arial Negreta', 'Arial Normal', 'Arial';
font-weight: 700;
font-style: normal;
font-size: 14px;
color: #282828;
background-color: rgba(255, 255, 255, 1);
box-sizing: border-box;
border-bottom:1px solid rgba(238, 238, 238, 1);
padding-left: 15px;
}
</style>
<style>
.el-dialog {
padding: 0 !important;
border-radius: 10px !important;
border: 1px solid #363636 !important;
}
.el-dialog .el-dialog__header {
display: flex;
display: -webkit-flex;
justify-content: flex-start;
-webkit-justify-content: flex-start;
align-items: center;
-webkit-align-items: center;
padding: 10px 20px;
background-color: #f1f3f8 !important;
font-family: 'Arial Negreta', 'Arial Normal', 'Arial', sans-serif;
font-weight: 700;
font-style: normal;
font-size: 16px;
color: #1B1B1B;
text-align: left;
border-radius: 10px 10px 0 0;
height: 50px;
}
.el-dialog .el-dialog__close {
font-size: 22px;
color: rgb(80, 80, 80);
}
</style>
<style >
.custom-image-node {
cursor: pointer;
}
</style>