JavaProjectRepo/business-css/frontend/src/components/antvx6/viewx6.vue
2026-01-17 14:59:23 +08:00

880 lines
20 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 chartsimg from '@/assets/x6/charts.png'
import Createscenario from '@/views/component/scenario/createscenario.vue'
import ScenarioModel from '@/views/component/scenario/index.vue'
import TableModels from './tableModel.vue';
import EditdeviceModel from './editdevice.vue';
import MaterialModels from './materialmodel.vue';
import ChangesettingsModels from './changesettings.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
console.log(customImageData.value[i])
addAttrText(customImageData.value[i])
}
}
}
})
}
function addAttrText(item:any){
item.attrs.text = item.deviceName
graph.addNode({
x: item.position.x ,
y: item.position.y + 160,
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 + 167,
width: 14,
height: 14,
correlationId: item.id,
type:'charts',
attrs: {
img: {
width: 14,
height: 14,
'xlink:href': chartsimg,
},
label: {
text: '',
},
},
})
graph.addNode({
shape: 'image-node',
x: item.position.x + 110 ,
y: item.position.y + 167,
width: 14,
height: 14,
correlationId: item.id,
type:'text',
attrs: {
img: {
width: 14,
height: 14,
'xlink:href': textimg,
},
label: {
text: '',
},
},
})
}
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,
},
},
},
true,
)
// Graph.registerNode(
// 'image-charts',
// {
// 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()
}
})
// #endregion
// #region 使用插件
// graph
// .use(
// new Transform({
// resizing: false, // 通过拖拽边缘调整节点大小
// rotating: false, // 允许旋转节点
// // scaling 不是 Transform 插件的有效配置项已移除
// }),
// )
// .use(
// new Selection({
// rubberband: true,
// showNodeSelectionBox: true,
// }),
// )
// .use(new Snapline())
// .use(new Keyboard())
// .use(new Clipboard())
// .use(new History())
// #endregion
graph.on('node:click', ({ e, node }) => {
if(node?.store?.data?.type == 'charts'){
deviceId.value = node?.store?.data?.correlationId
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)
const selectedNode:any = ref(null)
function closeAntvx6() {
router.push('/business/project/index')
}
function bigClick(){
graph.zoom(0.1)
}
function smallClick(){
graph.zoom(-0.1)
}
const isTableModel = ref(false)
function dialogTableModel(){ // 关闭变动设置弹窗
isTableModel.value = false;
}
</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="isDisplay = !isDisplay">
<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 class="line-style-box">
<div class="expansionandcontraction-box" v-if="isExpansionandcontraction == false" @click="isExpansionandcontraction = true">
<img src="@/assets/x6/expansionandcontraction-left.png">
</div>
<div v-if="isExpansionandcontraction == true" style="display: flex;align-items: center;">
<div class="expansionandcontraction-box" @click="isExpansionandcontraction = false">
<img src="@/assets/x6/expansionandcontraction-right.png">
</div>
<div class="style-content-box">
<div class="style-content-box-title">样式</div>
</div>
</div>
</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-id="isTableModel" :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>