210 lines
5.1 KiB
Vue
210 lines
5.1 KiB
Vue
<template>
|
|
<div style="width:100%;height:100%" id="svgContainer">
|
|
<svg id="mySvg"></svg>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import dagreD3 from "dagre-d3";
|
|
import * as d3 from "d3";
|
|
|
|
export default {
|
|
name: "degraD3",
|
|
props: {
|
|
nodes: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
edges: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
rankdir: {
|
|
type: String,
|
|
default: "DL",
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
g: null,
|
|
inner: null,
|
|
render: null,
|
|
};
|
|
},
|
|
watch: {
|
|
nodes: {
|
|
handler() {
|
|
this.updateGraph();
|
|
},
|
|
deep: true,
|
|
},
|
|
edges: {
|
|
handler() {
|
|
this.updateGraph();
|
|
},
|
|
deep: true,
|
|
},
|
|
},
|
|
mounted() {
|
|
this.initGraph();
|
|
},
|
|
methods: {
|
|
/** 初始化图 */
|
|
initGraph() {
|
|
this.g = new dagreD3.graphlib.Graph({ multigraph: true }).setGraph({
|
|
rankdir: this.rankdir,
|
|
nodesep: 40,
|
|
edgesep: 25,
|
|
ranksep: 20,
|
|
marginx: 80,
|
|
marginy: 10,
|
|
});
|
|
|
|
this.render = new dagreD3.render();
|
|
let svg = d3.select("#mySvg").attr("preserveAspectRatio", "xMidYMid meet");
|
|
this.inner = svg.append("g");
|
|
|
|
// 缩放支持
|
|
let zoom = d3.zoom().on("zoom", (event) => {
|
|
this.inner.attr("transform", event.transform);
|
|
});
|
|
svg.call(zoom);
|
|
|
|
this.renderGraph();
|
|
this.bindNodeClick(svg);
|
|
},
|
|
|
|
/** 渲染节点和边 */
|
|
renderGraph() {
|
|
let that = this;
|
|
that.inner.selectAll("*").remove(); // 清空内部元素
|
|
// 清空原有节点和边
|
|
that.g.nodes().forEach((v) => that.g.removeNode(v));
|
|
that.g.edges().forEach((e) => that.g.removeEdge(e));
|
|
|
|
// 添加节点
|
|
that.nodes.forEach((item) => {
|
|
if (item.id && item.label) {
|
|
that.g.setNode(item.id, {
|
|
label: item.label,
|
|
shape: item.shape,
|
|
toolText: item.label,
|
|
style: "fill:#fff;stroke:#000",
|
|
labelStyle: "fill:#000;",
|
|
rx: 5,
|
|
ry: 5,
|
|
});
|
|
}
|
|
});
|
|
|
|
const edgeGroups = {};
|
|
that.edges.forEach((edge) => {
|
|
const key = `${edge.source}_${edge.target}`;
|
|
if (!edgeGroups[key]) edgeGroups[key] = [];
|
|
edgeGroups[key].push(edge);
|
|
});
|
|
|
|
// 定义几种可选曲线
|
|
const curves = [d3.curveBasis, d3.curveCardinal, d3.curveBundle.beta(0.8)];
|
|
|
|
// 按分组循环,避免重叠
|
|
Object.values(edgeGroups).forEach((edges) => {
|
|
edges.forEach((edge, index) => {
|
|
let edgeColor = "green";
|
|
if (edge.label === "驳回" || edge.label === "退回") edgeColor = "red";
|
|
|
|
that.g.setEdge(
|
|
edge.source,
|
|
edge.target,
|
|
{
|
|
label: edge.label,
|
|
id: edge.id,
|
|
// ✅ 给每条边做区分
|
|
curve: curves[index % curves.length],
|
|
labeloffset: 16 + index * 10, // 标签错开
|
|
minlen: 2 + index, // 强制 dagre 给一点空间
|
|
style: `fill:none;stroke:${edgeColor};stroke-width:1.5px`,
|
|
},
|
|
`${edge.source}_${edge.target}_${edge.id}`
|
|
);
|
|
});
|
|
});
|
|
|
|
// 渲染图
|
|
this.render(this.inner, this.g);
|
|
|
|
// 获取 svg 的宽高
|
|
const svgWidth = document.getElementById("svgContainer").clientWidth;
|
|
const svgHeight = document.getElementById("svgContainer").clientHeight;
|
|
|
|
d3.select("#mySvg")
|
|
.attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`);
|
|
|
|
const graphHeight = this.g.graph().height;
|
|
const graphWidth = this.g.graph().width;
|
|
|
|
// ===== 自动缩放和居中 =====
|
|
const scale = Math.min(svgWidth / graphWidth, svgHeight / graphHeight);
|
|
|
|
const translateX = (svgWidth - graphWidth * scale) / 2;
|
|
const translateY = (svgHeight - graphHeight * scale) / 4;
|
|
|
|
this.inner.attr("transform", `translate(${translateX},${translateY}) scale(${scale})`);
|
|
},
|
|
|
|
/** 更新图形 */
|
|
updateGraph() {
|
|
if (!this.g) return;
|
|
this.renderGraph();
|
|
},
|
|
|
|
/** 绑定点击事件 */
|
|
bindNodeClick(svg) {
|
|
let that = this;
|
|
svg.selectAll("g.node").on("click", function (event, d) {
|
|
svg.selectAll("g.node")._groups[0].forEach((item) => {
|
|
d3.select(item).select("rect").style("fill", "#fff");
|
|
});
|
|
|
|
const node = d3.select(this);
|
|
const currentColor = node.select("rect").style("fill");
|
|
if (currentColor === "rgb(255, 255, 255)") {
|
|
node.select("rect").style("fill", "#f00");
|
|
} else {
|
|
node.select("rect").style("fill", "#fff");
|
|
}
|
|
|
|
let batch = "";
|
|
that.nodes.forEach((item) => {
|
|
if (item.id === d) batch = item.label;
|
|
});
|
|
that.$emit("nodeClick", batch);
|
|
});
|
|
},
|
|
|
|
/** 缩放演示 */
|
|
scaleUp() {
|
|
var svg = document.getElementById("mySvg");
|
|
svg.style.transform = "scale(0.5)";
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
svg {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.node rect {
|
|
stroke: #606266;
|
|
fill: #fff;
|
|
}
|
|
|
|
.edgePath path {
|
|
stroke: #606266;
|
|
fill: #333;
|
|
stroke-width: 1.5px;
|
|
}
|
|
</style>
|