emis-frontend/src/views/emis/emisTransPlanRuleTopoGraph/index.vue

666 lines
25 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.

<template>
<div class="app-container">
<!-- 查询 -->
<SearchForm slot="pageHeader" :model="queryParams" ref="queryForm" size="mini" :maxShow="30" label-width="68px"
v-show="showSearch" @search="handleQuery" @reset="resetQuery">
<!-- 线路查询 -->
<el-form-item label="线路" prop="lineCode">
<TransLineAllPicker v-model="queryParams.lineCode" isInitiated="true"></TransLineAllPicker>
</el-form-item>
<el-form-item label="产品类型" prop="productType">
<TransProductAllPicker placeholder="产品类型" v-model="queryParams.productType"
:transLineCode="queryParams.lineCode" @onChange="onProductChange"></TransProductAllPicker>
</el-form-item>
<el-form-item label="始发网点" prop="startSiteCode">
<EmisSiteSimplePicker placeholder="始发网点" v-model="queryParams.startSiteCode"></EmisSiteSimplePicker>
</el-form-item>
</SearchForm>
<div slot="toolbar">
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini"
@click="exportImage">导 出</el-button>
</el-col>
</el-row>
</div>
<div id="container">
<!-- mini 地图 -->
<div class="minimap-container" id="minimap"></div>
</div>
</div>
</template>
<script>
import G6 from "@antv/g6";
import TransLineAllPicker from '@/views/emis/EmisBaseTools/TransLineAllPicker.vue';
import TransProductAllPicker from '@/views/emis/EmisBaseTools/TransProductAllPicker.vue';
import EmisSiteSimplePicker from '@/views/emis/EmisBaseTools/EmisSiteSimplePicker.vue';
import { emisTransPlanRuleTree } from "@/api/emis/emisTransPlanRule";
export default {
name: "EmisTransPlanRuleTopoGraph",
components: {
TransLineAllPicker,
TransProductAllPicker,
EmisSiteSimplePicker
},
data() {
return {
treeData: {},
graph: null,
queryParams: {
pageNum: 1,
pageSize: 50,
lineCode: "",
productType: "",
productName: '',
startSiteCode: ''
},
// 显示搜索条件
showSearch: true,
//节点操作菜单
descriptionDiv: null,
};
},
mounted() {
// 注册自定义缩放行为
G6.registerBehavior("fixed-zoom-canvas", {
getEvents() {
return {
wheel: "onWheel",
mousewheel: "onWheel",
touchmove: "onTouchMove",
touchstart: "onTouchStart",
touchend: "onTouchEnd",
};
},
// 设置 passive 为 false
getDefaultCfg() {
return {
direction: "both",
sensitivity: 1,
minZoom: 0.2,
maxZoom: 10,
// 关键配置:禁用被动事件监听器
wheelEvent: { passive: false },
touchEvent: { passive: false },
};
},
// 处理滚轮事件
onWheel(e) {
// 阻止默认行为
// e.preventDefault();
// 调用原始缩放逻辑
const self = this;
const graph = self.graph;
const point = { x: e.clientX, y: e.clientY };
const canvasPoint = graph.getPointByClient(point.x, point.y);
// 缩放逻辑...
const zoom = e.deltaY > 0 ? 0.9 : 1.1;
graph.zoom(zoom, { x: canvasPoint.x, y: canvasPoint.y });
},
// 处理触摸事件
onTouchMove(e) {
e.preventDefault();
// 触摸移动逻辑...
},
});
// this.splitChild(this.treeData);
this.drawTreeGraph();
},
beforeRouteEnter(to, from, next) {
// 组件创建前 保存当前背景颜色
document.documentElement.style.overflow = 'hidden';
next();
},
beforeRouteLeave(to, from, next) {
// 组件销毁前 恢复样式
document.documentElement.style.overflow = '';
next();
},
methods: {
/** 搜索按钮操作 */
handleQuery() {
this.getTreeData();
},
/** 重置按钮操作 */
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 50,
lineCode: "",
productType: "",
productName: ''
}
},
onProductChange(item) {
if (item) {
this.queryParams.productName = item.prodName;
}
},
drawTreeGraph() {
// 节点操作菜单
const contextMenu = new G6.Menu({
getContent(evt) {
return `
<h3>操作节点</h3>
<button class='edit-btn'>编辑</button>
<button class='del-btn'>删除</button>
`;
},
handleMenuClick: (target, item) => {
console.log(target, item);
},
// offsetX and offsetY include the padding of the parent container
// 需要加上父级容器的 padding-left 16 与自身偏移量 10
offsetX: 16 + 10,
// 需要加上父级容器的 padding-top 24 、画布兄弟元素高度、与自身偏移量 10
offsetY: 0,
// the types of items that allow the menu show up
// 在哪些类型的元素上响应
itemTypes: ['node', 'edge', 'canvas'],
});
const minimap = new G6.Minimap({
container:'minimap',
size: [190, 190],
type: 'delegate',
delegateStyle: {
// 代理元素样式配置
fill: '#b12d29', // 填充色(半透明白色)
// stroke: 'black', // 描边色(蓝色)
}
});
this.graph = new G6.TreeGraph({
container,
width: document.getElementById('container').clientWidth, //画布宽度等于浏览器宽度
height: document.getElementById('container').clientHeight, //画布高度等于浏览器高度
fitView: false,
// fitViewPadding: [10, 50, 10, 50],
animate: true,
// plugins: [this.treeTooltip(), this.contextMenu],
plugins: [this.treeTooltip(), minimap],
defaultNode: {
type: "rect",
size: [250, 80],
collapsed: false, // false 展开 true 收起
style: {
// fill: "#fff",
// lineWidth: 2,
// cursor: "pointer",
},
// labelCfg: {
// position: "right",
// offset: 10,
// style: {
// fill: "#333",
// fontSize: 20,
// stroke: "#fff",
// background: {
// fill: "#ffffff",
// padding: [2, 2, 2, 2],
// },
// },
// },
anchorPoints: [
[0, 0.5],
[1, 0.5],
],
icon: {
show: true,
width: 25,
height: 25,
},
},
defaultEdge: {
// type: "cubic-horizontal",
type: "polyline",
style: {
endArrow: true
},
labelCfg: {
position: "end",
refX: -15,
style: {
fontSize: 16,
background: {
fill: "#ffffff",
padding: [2, 2, 2, 2],
},
},
},
},
nodeStateStyles: {
closest: {
fill: '#f00',
},
},
modes: {
default: [
{
type: "collapse-expand",
onChange: function onChange(item, collapsed) {
const data = item?.get("model");
data.collapsed = collapsed;
const model = {
id: data.id,
// labelCfg: { position: !collapsed ? "top" : "right" },
};
item.update(model);
item.refresh();
return true;
},
},
"drag-canvas",
"fixed-zoom-canvas",
"drag-node",
// {
// type: 'drag-node',
// enableDelegate: true,
// }
],
},
layout: {
type: "compactBox",
direction: "LR",
getHeight: function getHeight() {
return 30;
},
getWidth: function getWidth() {
return 16;
},
getVGap: function getVGap() {
return 30;
},
getHGap: function getHGap() {
return 150; // 节点水平间距
},
},
//选中节点
nodeStateStyles: {
active: {
// fill: "l(0) 0:#FF4500 1:#32CD32",
// stroke: "l(0) 0:#FF4500 1:#32CD32",
// fill: "#B12D29",
// stroke:"#B12D29",
// lineWidth: 5,
},
selected: {
// fill: "l(0) 0:#FF4500 1:#32CD32",
// fill: "#B12D29",
stroke: "#B12D29",
lineWidth: 3,
},
},
});
this.graph.node((node) => {
if (!node.id.includes("expand")) {
return {
label: node.label || this.formatLabel(node),
icon: this.formatIcon(node),
size: node.size || 40,
// labelCfg: { position: this.setLabelPos(node) },
style: {
// fill: this.getNodeColor(),
stroke: '#B12D29',
},
};
} else {
return {
// label: node.label || this.formatLabel(node),
label: '更多...',
icon: this.formatIcon(node),
size: 50,
type: 'circle',
// labelCfg: { position: this.setLabelPos(node) },
labelCfg: { position: 'right' },
style: {
// fill: this.getNodeColor(),
fill: '#B12D29',
stroke: '#B12D29',
},
};
}
});
let selfGrowthNum = 0;
this.graph.edge((edge) => {
// let {source, target } = edge // 也可以根据 link的属性不同自定义 连线颜色和label颜色,因为是测试数据,因此就用一个自增长的数判断奇偶性来进行区分,以便明白其中定制化的方法
selfGrowthNum++;
return {
style: {
opacity: 0.5,
// stroke: selfGrowthNum % 2 ? "#ADD8E6" : "#FFDEAD",
stroke: '#B12D29',
lineWidth: 2,
},
labelCfg: {
position: "end",
style: {
fontSize: 16,
fill: selfGrowthNum % 2 ? "#ADD8E6" : "#FFDEAD",
},
},
// label: selfGrowthNum % 2 ? "even" : "odd", //边标签
};
});
this.graph.on("node:mouseenter", (evt) => {
const { item } = evt;
// this.graph.setItemState(item, "active", true);
});
this.graph.on("node:mouseleave", (evt) => {
const { item } = evt;
// this.graph.setItemState(item, "active", false);
});
const animateCfg = { duration: 200, easing: "easeCubic" };
this.graph.on("node:click", (evt) => {
const { item } = evt;
const node = item?.get("model");
//点击更多 显示锁起来的下一层节点
if (node.id.includes("expand")) {
const parentNode = this.graph.getNeighbors(item, "source")[0].get("model");
this.graph.updateChildren(parentNode.childrenBak, parentNode.id);
}
setTimeout(() => {
if (!node.id.includes("expand")) {
this.graph.focusItem(item, true, animateCfg);
this.graph.getNodes().forEach((node) => {
this.graph.clearItemStates(node);
});
this.graph.setItemState(item, "selected", true);
}
}, 500);
});
this.graph.on("canvas:click", () => {
this.graph.getNodes().forEach((node) => {
this.graph.clearItemStates(node);
});
});
let minDisNode;
this.graph.on('node:dragstart', (e) => {
console.log('dragstart')
minDisNode = undefined;
});
this.graph.on('node:drag', (e) => {
console.log('node:drag')
minDisNode = undefined;
const item = e.item;
const model = item.getModel();
const nodes = this.graph.getNodes();
let minDis = Infinity;
nodes.forEach((inode) => {
this.graph.setItemState(inode, 'closest', false);
const node = inode.getModel();
if (node.id === model.id) return;
const dis = (node.x - e.x) * (node.x - e.x) + (node.y - e.y) * (node.y - e.y);
if (dis < minDis) {
minDis = dis;
minDisNode = inode;
}
});
// console.log('minDisNode1', minDisNode);
// if (minDis < 2000)
this.graph.setItemState(minDisNode, 'closest', true);
// else minDisNode = undefined;
// console.log('minDisNode2',minDisNode)
});
// 节点拖动
// this.graph.on('node:dragend', (e) => {
// // console.log('dragend')
// const descriptionDiv = document.createElement('div');
// descriptionDiv.innerHTML =
// 'Move a subtree to a new parent by dragging the root node of the subtree.';
// container.appendChild(descriptionDiv);
// // console.log('minDisNode', minDisNode)
// if (!minDisNode) {
// descriptionDiv.innerHTML = 'Failed. No node close to the dragged node.';
// return;
// }
// const item = e.item;
// const id = item.getID();
// const data = this.graph.findDataById(id);
// // if the minDisNode is a descent of the dragged node, return
// let isDescent = false;
// const minDisNodeId = minDisNode.getID();
// // console.log('dragend', minDisNodeId, isDescent, data, id);
// console.log('目的节点==》',this.graph.findDataById(minDisNodeId)) //目的节点
// console.log('拖动节点==》', this.graph.findDataById(id)) //拖动节点
// G6.Util.traverseTree(data, (d) => {
// if (d.id === minDisNodeId) isDescent = true;
// });
// if (isDescent) {
// descriptionDiv.innerHTML = 'Failed. The target node is a descendant of the dragged node.';
// return;
// }
// this.graph.removeChild(id);
// setTimeout(() => {
// const newParentData = this.graph.findDataById(minDisNodeId);
// let newChildren = newParentData.children;
// if (newChildren) newChildren.push(data);
// else newChildren = [data];
// this.graph.updateChildren(newChildren, minDisNodeId);
// descriptionDiv.innerHTML = 'Success.';
// }, 600);
// });
// this.graph.data(this.treeData);
// this.graph.render();
this.graph.zoom(0.9);
this.graph.fitCenter();
this.graph.get("canvas").set("localRefresh", false);
},
// label 过长截断 显示...
formatLabel(node) {
const hasChildren = node.childrenBak?.length || node.children?.length;
// const ellipsis = node.id.length > 15 ? "..." : "";
return `${this.labelStr(node)}`;
},
// 叶子节点 图标处理 截取ID 前两个字符串
formatIcon(node) {
if (node.id) {
node.icon = {
// text: node.id.slice(0, 2),
text: '+',
fill: "#fff",
stroke: "#fff",
textBaseline: "middle",
fontSize: 20,
width: 25,
height: 25,
show: true,
};
}
},
// 叶子节点 背景颜色随机填充
getNodeColor() {
const colors = ["#8470FF", "#A020F0", "#C0FF3E", "#FF4500", "#66d6d1"];
return colors[Math.floor(Math.random() * colors.length)];
},
// 根据节点展开收起状态 动态变更 label 显示位置,展开时在上,收起时在右
setLabelPos(node) {
return !node.collapsed && node.children?.length ? "top" : "right";
},
// label 显示... 时,显示提示 tip
treeTooltip() {
return new G6.Tooltip({
offsetX: 10,
offsetY: 20,
shouldBegin(e) {
// return e.item?.get("model")?.label?.includes("...");
return e.item._cfg.model.label;
},
getContent(e) {
if (e.item._cfg.model.startSiteName) {
return `<div class="tooltip">
<div>${e.item._cfg.model.startSiteName}->${e.item._cfg.model.nextSiteName}</div>
<div>${e.item._cfg.model.supplierName || ''}</div>
</div>`
} else {
return '';
}
},
itemTypes: ["node"],
});
},
// 叶子节点超过 5(xxx)条, 折叠叶子节点,显示展开更多
splitChild(node) {
node.childrenBak = node.children ? [...node.children] : [];
let result = [];
if (node.children) {
result = node.children.slice(0, 2);
if (node.children.length > 2) {
// result.push({ id: `expand-${node.id}`, label: " 展开更多..." });
result.push({ id: `expand-${node.id}`, label: "" });
}
node.children = result;
node.children.forEach((child) => {
this.splitChild(child);
});
}
},
processTree(nodes) {
return nodes.map(node => {
// const currentPath = parentPath ? `${parentPath}/${node.name}` : node.name;
const newNode = { ...node, id: node.treeId };
if (node.children) {
newNode.children = this.processTree(node.children);
}
return newNode;
});
},
//获取树形数据
async getTreeData() {
const loadingInstance = this.$loading({
text: "正在加载...",
});
this.list = [];
this.edges = [];
const res = await emisTransPlanRuleTree(this.queryParams); //测试数据
this.treeData = res.rows;
//获取产品下的网点
// let { list: newlist, edges: newEdges } = this.treeDataToNodelist(
// this.treeData,
// this.queryParams.productType,
// );
this.treeData = { "id": '1', "label": this.queryParams.productName, "x": 500, "y": 500, "children": this.processTree(this.treeData) }
// 折叠节点
this.splitChild(this.treeData);
this.graph.data(this.treeData);
this.graph.render();
this.graph.fitCenter();
// setTimeout(() => {
// this.list = this.list.concat(newlist);
// this.edges = this.edges.concat(newEdges);
// this.graph.data({
// nodes: this.list,
// edges: this.edges,
// });
// this.graph.render();
// }, 500);
loadingInstance.close();
},
//格式化标签
labelStr(node) {
let str;
const hasChildren = node.childrenBak?.length || node.children?.length || 0;
if (node.startSiteName) {
str = `${node.startSiteName}->${node.nextSiteName}\n${node.supplierName ? node.supplierName.replace(/(.{20})/g, '$1\n') : ''} \n (${hasChildren})`
} else {
str = node.label;
}
return str;
},
async exportImage() {
this.graph.toFullDataURL((res) => {
// 为了防止下载的图片影响到G6显示,这里作一个深度拷贝
const canvas = document.getElementById('container').childNodes[1].cloneNode(true);
console.log('canvas',document.getElementById('container').childNodes);
const img = new Image();
img.onload = function () {
canvas.width = img.width + 32;
canvas.height = img.height + 32;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 16, 16);
// 下载
const oA = document.createElement('a');
oA.download = '组批次规则图';
oA.href = canvas.toDataURL('image/png');
document.body.appendChild(oA);
oA.click();
oA.remove(); // 下载之后把创建的元素删除
};
img.src = res;
}, 'image/png', {
imageConfig: {
padding: 16
}
});
}
}
}
</script>
<style scoped>
#container {
touch-action: none;
/* 禁用默认触摸滚动 */
width: 100%;
height: calc(100vh - 150px);
border: 1px solid #ccc;
position: relative;
}
.minimap-container {
position: absolute;
left: 0;
bottom: 0;
width: 200px;
height: 200px;
border: 1px solid #ccc;
}
#container>>>.g6-component-tooltip {
background: #333;
color: #fff;
padding: 10px;
}
.minimap-container >>>.g6-minimap-viewport {
outline: 2px solid black !important;
}
/* canvas {
cursor: pointer !important;
} */
.minimap-container {
margin: 0;
}
</style>