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

604 lines
23 KiB
Vue
Raw Normal View History

2025-06-30 10:51:31 +00:00
<template>
2025-07-02 11:48:50 +00:00
<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">
<TransLinePicker v-model="queryParams.lineCode" isInitiated="true"></TransLinePicker>
</el-form-item>
<el-form-item label="产品类型" prop="productType">
<TransProductPicker placeholder="产品类型" v-model="queryParams.productType"
:transLineCode="queryParams.lineCode" @onChange="onProductChange"></TransProductPicker>
</el-form-item>
<el-form-item label="始发网点" prop="startSiteCode">
<EmisSiteSimplePicker placeholder="始发网点" v-model="queryParams.startSiteCode"></EmisSiteSimplePicker>
</el-form-item>
</SearchForm>
<div id="container"></div>
</div>
2025-06-30 10:51:31 +00:00
</template>
2025-07-02 11:48:50 +00:00
2025-06-30 10:51:31 +00:00
<script>
import G6 from "@antv/g6";
import TransProductPicker from "@/views/emis/EmisBaseTools/TransProductPicker.vue";
import TransLinePicker from "@/views/emis/EmisBaseTools/TransLinePicker.vue";
2025-07-02 11:48:50 +00:00
import EmisSiteSimplePicker from '@/views/emis/EmisBaseTools/EmisSiteSimplePicker.vue';
2025-06-30 10:51:31 +00:00
import { emisTransPlanRuleTree } from "@/api/emis/emisTransPlanRule";
2025-07-02 11:48:50 +00:00
import { fill } from "lodash";
2025-06-30 10:51:31 +00:00
export default {
2025-07-02 11:48:50 +00:00
name: "EmisTransPlanRuleTopoGraph",
components: {
TransProductPicker,
TransLinePicker,
EmisSiteSimplePicker
},
data() {
2025-06-30 10:51:31 +00:00
return {
2025-07-02 11:48:50 +00:00
treeData: {},
graph: null,
queryParams: {
pageNum: 1,
pageSize: 50,
lineCode: "",
productType: "",
productName: '',
startSiteCode: ''
},
// 显示搜索条件
showSearch: true,
//节点操作菜单
descriptionDiv: null,
2025-06-30 10:51:31 +00:00
};
},
2025-07-02 11:48:50 +00:00
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();
// 触摸移动逻辑...
},
2025-06-30 10:51:31 +00:00
});
2025-07-02 11:48:50 +00:00
// 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();
2025-06-30 10:51:31 +00:00
},
2025-07-02 11:48:50 +00:00
methods: {
/** 搜索按钮操作 */
handleQuery() {
this.getTreeData();
},
/** 重置按钮操作 */
resetQuery() {
this.queryParams = {
pageNum: 1,
pageSize: 50,
lineCode: "",
productType: "",
productName: ''
}
},
onProductChange(item, transType) {
this.queryParams.productName = item.prodName;
},
drawTreeGraph() {
this.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'],
2025-06-30 10:51:31 +00:00
});
2025-07-02 11:48:50 +00:00
if (this.graph) this.graph.destroy();
const container = document.getElementById("container");
this.graph = new G6.TreeGraph({
container,
width: document.getElementById('container').clientWidth + 10, //画布宽度等于浏览器宽度
height: document.getElementById('container').clientHeight + 10, //画布高度等于浏览器高度
2025-07-02 11:55:07 +00:00
fitView: false,
2025-07-02 11:48:50 +00:00
// fitViewPadding: [10, 50, 10, 50],
animate: true,
// plugins: [this.treeTooltip(), this.contextMenu],
plugins: [this.treeTooltip()],
defaultNode: {
type: "rect",
size: [250, 80],
collapsed: false,
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',
},
},
2025-07-02 11:48:50 +00:00
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,
// }
2025-07-02 11:48:50 +00:00
],
},
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);
2025-07-02 11:48:50 +00:00
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);
});
2025-07-02 11:48:50 +00:00
// 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 '';
}
2025-07-02 11:48:50 +00:00
},
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();
2025-07-04 09:11:11 +00:00
this.graph.fitCenter();
2025-07-02 11:48:50 +00:00
// 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;
2025-06-30 10:51:31 +00:00
}
}
2025-07-02 11:48:50 +00:00
}
2025-06-30 10:51:31 +00:00
</script>
2025-07-02 11:48:50 +00:00
<style scoped>
2025-06-30 10:51:31 +00:00
#container {
2025-07-02 11:48:50 +00:00
touch-action: none;
/* 禁用默认触摸滚动 */
width: 100%;
height: calc(100vh - 150px);
border: 1px solid #ccc;
overflow: hidden;
}
#container>>>.g6-component-tooltip {
background: #333;
color: #fff;
padding: 10px;
2025-07-02 11:48:50 +00:00
}
2025-06-30 10:51:31 +00:00
2025-07-02 11:48:50 +00:00
canvas {
cursor: pointer !important;
2025-06-30 10:51:31 +00:00
}
2025-07-02 11:48:50 +00:00
</style>