JavaProjectRepo/business-css/frontend/src/components/antvx6/viewx6.vue
2026-01-17 13:41:47 +08:00

1022 lines
26 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 Createscenario from '@/views/component/scenario/createscenario.vue'
import ScenarioModel from '@/views/component/scenario/index.vue'
import AdddeviceModel from './adddevice.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 isMaterialModel = ref(false) // 是否物料信息
const projectInfo:any = ref({}) // 项目信息
const isScenario = ref(false) //是否展示历史模拟场景
const isDisplay = ref(true) // 是否显示
const isExpansionandcontraction = ref(false) // 是否显示展开收起按钮
// 为了协助代码演示
let graph: Graph
const scenarioId:any = ref('') // 场景id
function getScenarioResults(){
getByScenario({
scenarioId: scenarioId.value,
pageNum:1,
pageSize:999
}).then((res:any) => {
console.log(res.data.records)
res.data.records
})
}
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,
)
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)
if(!topology.designData)return
// 确保每个单元格都有 shape 属性
// const validCells = Array.isArray(topology.designData) ? topology.designData.filter(cell => cell.shape) : []
// 如果有有效单元格,则加载到图中
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:contextmenu', ({ e, node }) => {
selectedNode.value = node
e.preventDefault()
// 显示自定义上下文菜单,包含箭头样式选项
const pos = node.position?.() || { x: 0, y: 0 }
showContextMenu(pos.x, pos.y)
})
function showContextMenu(x: number, y: number) {
left.value = x + 260
top.value = y - 50
isMenuShow.value = true
// 创建并显示上下文菜单
// 包含箭头样式选项
}
// 监听Graph中节点创建事件
// graph.on('node:added', (args:any) => {
// console.log('节点已添加到画布', args)
// const { node } = args
// // 检查是否是管线节点
// if (node.data && node.data.lineStyle) {
// // 应用管线样式到连接线
// const lineStyle = node.data.lineStyle
// graph.options.connecting.createEdge = () => {
// return new Shape.Edge({
// attrs: {
// line: {
// stroke: '#A2B1C3',
// strokeWidth: 2,
// strokeDasharray: lineStyle.strokeDasharray,
// targetMarker: lineStyle.targetMarker,
// sourceMarker: lineStyle.sourceMarker
// }
// },
// zIndex: 0
// })
// }
// // 移除管线节点,因为它只是一个样式选择器
// setTimeout(() => {
// graph.removeNode(node)
// }, 100)
// } else {
// if(node.store && node.store.data && node.store.data.shape === 'rect'){
// return
// }
// nodeId.value = node.id
// if(node.store.data.attrs.text.text == '扁平槽'){
// deviceTypetype.value = 'FlatTank'
// }else if(node.store.data.attrs.text.text == '圆柱槽'){
// deviceTypetype.value = 'CylindricalTank'
// }else if(node.store.data.attrs.text.text == '环形槽'){
// deviceTypetype.value = 'AnnularTank'
// }else if(node.store.data.attrs.text.text == '管束槽'){
// deviceTypetype.value = 'TubeBundleTank'
// }else if(node.store.data.attrs.text.text == '萃取柱'){
// deviceTypetype.value = 'ExtractionColumn'
// }else if(node.store.data.attrs.text.text == '流化床'){
// deviceTypetype.value = 'FluidizedBed'
// }else if(node.store.data.attrs.text.text == '锥底环形槽'){
// deviceTypetype.value = 'ACFTank'
// }
// // 设置固定大小
// node.size(160, 160)
// // 去掉背景节点
// node.attr('body/fill', 'none')
// // 图片居中
// node.attr('image/refX', 0)
// node.attr('image/refY', 0)
// node.attr('image/width', 160) // 修改宽度
// node.attr('image/height', 160) // 修改高度
// // 删除节点上的文字
// node.attr('text/text', '')
// node.attr('label/text', '')
// isAdddevice.value = true
// }
// })
// #endregion
// #endregion
})
const left = ref(0)
const top = ref(0)
const isMenuShow = ref(false)
const selectedNode:any = ref(null)
function deleteNode() { // 删除节点
graph.removeNode(selectedNode.value)
isMenuShow.value = false
}
function copyNode() { // 复制节点
if (selectedNode.value) {
try {
// 获取当前节点的位置
const position = selectedNode.value.position()
// 创建新节点,使用节点的原始数据
const newNode = selectedNode.value.clone()
// 设置新位置偏移50px
newNode.position(position.x + 50, position.y + 50)
// 添加到画布
graph.addNode(newNode)
isMenuShow.value = false
} catch (error) {
console.error('节点复制失败:', error)
}
}
}
function closeAntvx6() {
router.push('/business/project/index')
}
function revokeClick(){
graph.undo()
}
function removeClick(){
graph.clearCells();
}
function bigClick(){
graph.zoom(0.1)
}
function smallClick(){
graph.zoom(-0.1)
}
const nodeId = ref("")
function analysisAdd(){
dialogVisible.value = true;
}
const dialogVisible = ref(false)
function closeCreatescenario(){ // 关闭新增模拟分析弹窗
dialogVisible.value = false;
}
function simulationClick() { // 打开模拟分析
isScenario.value = true;
}
//取消按钮
function handleClose() {
isScenario.value = false;
}
function dialogAdddevice(){ // 打开新增设备弹窗
graph.undo()
isAdddevice.value = false;
}
const selectedDevice = ref({})
function closeAdddevice(e:any){ // 关闭新增设备弹窗
selectedDevice.value = e
let retrievedNode:any = graph.getCellById(nodeId.value)
retrievedNode.store.data.deviceInfo = e
isAdddevice.value = false;
}
const deviceInfo:any = ref({}) // 是否编辑设备
function EditdeviceClick(){ // 打开编辑设备弹窗
deviceInfo.value = selectedNode.value.store.data.deviceInfo
nodeId.value = selectedNode.value.id
if(deviceInfo.value != null){
deviceTypetype.value = deviceInfo.value.type
}
isEditdevice.value = true;
}
function closeEditdevice(e:any){ // 关闭编辑设备弹窗
selectedDevice.value = e
let retrievedNode:any = graph.getCellById(nodeId.value)
retrievedNode.store.data.deviceInfo = e
isEditdevice.value = false;
}
function dialogEditdevice(){ // 关闭设备弹窗
isEditdevice.value = false;
}
const materialInfo:any = ref({}) // 是否物料信息
const selectedMaterial:any = ref({})
function MaterialModelClick(){ // 打开物料信息弹窗
deviceInfo.value = selectedNode.value.store.data.deviceInfo
materialInfo.value = {}
if(selectedNode.value.store.data.materialInfo != null){
materialInfo.value = selectedNode.value.store.data.materialInfo
}
nodeId.value = selectedNode.value.id
if(materialInfo.value != null){
deviceTypetype.value = materialInfo.value.type
}
isMaterialModel.value = true;
}
function dialogMaterialModel(){ // 关闭物料信息弹窗
isMaterialModel.value = false;
}
function closeMaterialModel(e:any){ // 关闭物料信息弹窗
if(e == false){
isMaterialModel.value = false;
return
}
selectedMaterial.value = e
let retrievedNode:any = graph.getCellById(nodeId.value)
retrievedNode.store.data.materialInfo = e
isMaterialModel.value = false;
}
const isChangesettings = ref(false)
const changesettingsData:any = ref([]) // 是否变动设置
function dialogChangesettings(){ // 关闭变动设置弹窗
isChangesettings.value = false;
}
function ChangesettingsClick(){ // 打开变动设置弹窗
changesettingsData.value = []
if(selectedNode.value.store.data.deviceInfo == null){
ElMessage({
type: "error",
message: "请先添加设备信息",
});
return
}
if(selectedNode.value.store.data.materialInfo == null){
ElMessage({
type: "error",
message: "请先添加物料信息",
});
return
}
deviceInfo.value = selectedNode.value.store.data.deviceInfo
materialInfo.value = selectedNode.value.store.data.materialInfo
if(selectedNode.value.store.data.changesettings != null){
changesettingsData.value = selectedNode.value.store.data.changesettings
}
nodeId.value = selectedNode.value.id
if(materialInfo.value != null){
deviceTypetype.value = materialInfo.value.type
}
isChangesettings.value = true;
}
function closeChangesettingsModel(e:any){ // 关闭变动设置弹窗
if(e == false){
isChangesettings.value = false;
return
}
selectedMaterial.value = e
let retrievedNode:any = graph.getCellById(nodeId.value)
retrievedNode.store.data.changesettings = e
isChangesettings.value = false;
}
function setFormulaInit(formula:any) {
// const formula = "【(圆柱槽B)*0.2+(环形槽A)*1.0+0.5】";
// 匹配*号后、+或)前的数字(支持整数和小数)
const regex = /\*(\d+\.?\d*)/g;
const coefficients = [];
let match;
// 循环提取所有匹配项
while ((match = regex.exec(formula)) !== null) {
coefficients.push(parseFloat(match[1])); // 转为数字类型
}
console.log(coefficients); // 输出: [0.2, 1.0]
return coefficients
}
function getBias(formula:any){
// 从后往前找最后一个+的位置
// 匹配最后一个+号后面的数字(支持整数、小数)
const regex = /\+(\d+\.?\d*)$/;
const match = formula.match(regex);
const constant = parseFloat(match[1]);
return constant
}
function saveDesign() { // 保存设计
try {
// 获取画布内容并转换为JSON
const designData:any = graph.toJSON()
let cells:any = []
if(designData !=null && designData.cells.length>0){
cells = designData.cells
}
let devices = []
for(let i=0;i<cells.length;i++){
if(cells[i].shape == 'custom-image'){
let tempData:any = {}
if(cells[i].changesettings && cells[i].changesettings.length > 0){
for(let j=0;j<cells[i].changesettings.length;j++){
if(cells[i].changesettings[j].formula != '' && cells[i].changesettings[j].formula != null){
const key = cells[i].changesettings[j].name as string
const unit = cells[i].changesettings[j].unit
const name = cells[i].changesettings[j].name
const delay= cells[i].changesettings[j].delay
let tempSources = []
for(let k=0;k<setFormulaInit(cells[i].changesettings[j].formula).length;k++){
tempSources.push({
entityType: 'material', //圆柱槽B
entityId: cells[i].materialInfo.materialId , //materialId : cdeeca2e-1e0c-4bdd-b946-f347c104752c
property: name, // 铀浓度g/L
coefficient: setFormulaInit(cells[i].changesettings[j].formula)[k], //0.2
delay: { enabled: true, time: delay, unit: 's' } // 5
})
}
tempData[key] = {
name: tempData[key],
type:'influence',
unit:unit,
base:0,
bias: getBias(cells[i].changesettings[j].formula) || 0,
sources:tempSources,
expression:cells[i].changesettings[j].formula
}
}
}
}
devices.push({
deviceId:cells[i].id,
name:cells[i].deviceInfo.name,
type:cells[i].deviceInfo.type,
properties:{},
material: cells[i].materialInfo != null ? {
materialId:cells[i].id,
name:cells[i].materialInfo.name,
properties:tempData
}:{}
})
}
}
const topology = {
projectId: projectInfo.value.projectId,
name: projectInfo.value.name,
devices:devices,
pipelines:[],
systemboundaries:[],
globalDisplay:{
device:{
showProperties:[]
},
material:{
showProperties:[]
},
},
designData:cells
}
console.log(topology)
// 添加保存信息
const saveData = {
projectId: projectInfo.value.projectId,
topology: JSON.stringify(topology)
}
updateProjects(saveData).then((res:any) => {
if(res === true){
ElMessage({
type: "success",
message: "保存成功",
});
}
});
// 返回保存的数据
return saveData
} catch (error) {
console.error('保存设计失败:', error)
return null
}
}
</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>
</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>