|
@@ -1,6 +1,15 @@
|
|
|
<template>
|
|
|
- <div style="width: 100%; max-height: 600px; overflow: auto;padding-bottom: 10px;" class="content">
|
|
|
- <div style="height:600px;position: relative;" ref="container"></div>
|
|
|
+ <div>
|
|
|
+ <div class="graph-toolbar">
|
|
|
+ <button class="graph-btn" @click="zoomIn">放大</button>
|
|
|
+ <button class="graph-btn" @click="zoomOut">缩小</button>
|
|
|
+ <button class="graph-btn" @click="resetZoom">1:1</button>
|
|
|
+ <input v-model="searchText" @keyup.enter="searchNode" placeholder="搜索节点" class="graph-input" />
|
|
|
+ <button class="graph-btn graph-btn-primary" @click="searchNode">搜索</button>
|
|
|
+ </div>
|
|
|
+ <div style="width: 100%; max-height: 600px; overflow: auto;padding-bottom: 10px;" class="content">
|
|
|
+ <div style="height:600px;position: relative;" ref="container"></div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
@@ -12,6 +21,7 @@ export default {
|
|
|
data() {
|
|
|
return {
|
|
|
graph: null,
|
|
|
+ searchText: '',
|
|
|
}
|
|
|
},
|
|
|
mounted() {
|
|
@@ -42,6 +52,48 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
methods: {
|
|
|
+ zoomIn() {
|
|
|
+ if (this.graph) {
|
|
|
+ const curZoom = this.graph.getZoom();
|
|
|
+ this.graph.zoomTo(curZoom * 1.2, { x: 0, y: 0 });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ zoomOut() {
|
|
|
+ if (this.graph) {
|
|
|
+ const curZoom = this.graph.getZoom();
|
|
|
+ this.graph.zoomTo(curZoom / 1.2, { x: 0, y: 0 });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ resetZoom() {
|
|
|
+ if (this.graph) {
|
|
|
+ this.graph.zoomTo(1, { x: 0, y: 0 });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ searchNode() {
|
|
|
+ if (!this.graph || !this.searchText) return;
|
|
|
+ const nodes = this.graph.getNodes();
|
|
|
+ let found = false;
|
|
|
+ nodes.forEach(node => {
|
|
|
+ const model = node.getModel();
|
|
|
+ // 先清除所有高亮
|
|
|
+ this.graph.clearItemStates(node, 'searched');
|
|
|
+ if (
|
|
|
+ (model.fullLabel && model.fullLabel.indexOf(this.searchText) !== -1) ||
|
|
|
+ (model.label && model.label.indexOf(this.searchText) !== -1) ||
|
|
|
+ (model.id && model.id.indexOf(this.searchText) !== -1)
|
|
|
+ ) {
|
|
|
+ this.graph.focusItem(node, true, {
|
|
|
+ easing: 'easeCubic',
|
|
|
+ duration: 600
|
|
|
+ });
|
|
|
+ this.graph.setItemState(node, 'searched', true);
|
|
|
+ found = true;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (!found) {
|
|
|
+ this.$message && this.$message.warning('未找到该节点');
|
|
|
+ }
|
|
|
+ },
|
|
|
showSeeksGraph(data) {
|
|
|
if (this.graph) {
|
|
|
this.graph.destroy();
|
|
@@ -56,6 +108,7 @@ export default {
|
|
|
width: container.scrollWidth,
|
|
|
height: container.scrollHeight || 600,
|
|
|
fitView: true,
|
|
|
+ background: { color: '#f7faff' },
|
|
|
modes: {
|
|
|
default: ['drag-canvas', 'zoom-canvas', 'drag-node'],
|
|
|
},
|
|
@@ -69,28 +122,101 @@ export default {
|
|
|
},
|
|
|
defaultNode: {
|
|
|
size: 40,
|
|
|
- style: {
|
|
|
- fill: 'rgba(24, 144, 255, 0.2)', // 浅蓝半透明
|
|
|
- stroke: '#1890ff', // 实色边框
|
|
|
- lineWidth: 2 // 边框宽度
|
|
|
- },
|
|
|
labelCfg: {
|
|
|
- position: 'bottom', // 标签显示在节点下方
|
|
|
+ position: 'bottom',
|
|
|
style: {
|
|
|
fill: '#000',
|
|
|
- fontSize: 12
|
|
|
+ fontSize: 14
|
|
|
+ }
|
|
|
+ },
|
|
|
+ stateStyles: {
|
|
|
+ highlight: {
|
|
|
+ opacity: 1,
|
|
|
+ },
|
|
|
+ inactive: {
|
|
|
+ opacity: 0.15,
|
|
|
+ },
|
|
|
+ searched: {
|
|
|
+ stroke: '#1890ff',
|
|
|
+ lineWidth: 2,
|
|
|
+ shadowColor: '#1890ff',
|
|
|
+ opacity: 1,
|
|
|
+ fill: '#f7faff'
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
defaultEdge: {
|
|
|
- type: 'cubic', // 贝塞尔曲线
|
|
|
+ type: 'cubic',
|
|
|
style: {
|
|
|
endArrow: true,
|
|
|
},
|
|
|
- labelCfg: {
|
|
|
+ labelCfg: {
|
|
|
autoRotate: true,
|
|
|
+ },
|
|
|
+ stateStyles: {
|
|
|
+ highlight: {
|
|
|
+ opacity: 1,
|
|
|
+ },
|
|
|
+ inactive: {
|
|
|
+ opacity: 0.08,
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
+ },
|
|
|
+ plugins: [
|
|
|
+ new G6.Tooltip({
|
|
|
+ offsetX: 50,
|
|
|
+ offsetY: -500,
|
|
|
+ itemTypes: ['node'],
|
|
|
+ getContent: (e) => {
|
|
|
+ const outDiv = document.createElement('div');
|
|
|
+ outDiv.style.maxWidth = '320px';
|
|
|
+ outDiv.style.wordBreak = 'break-all';
|
|
|
+ const model = e.item.getModel();
|
|
|
+ // 显示完整名称,不受字数限制
|
|
|
+ const fullLabel = model.fullLabel || model.label || model.id || '未命名节点';
|
|
|
+ outDiv.innerHTML = `
|
|
|
+ <div>
|
|
|
+ <div style="margin-bottom: 5px;font-weight: bold;">${fullLabel}</div>
|
|
|
+ <span style="color:#666;font-size:12px;">${model.description || '暂无描述'}</span>
|
|
|
+ </div>`;
|
|
|
+ return outDiv;
|
|
|
+ }
|
|
|
+ })
|
|
|
+ ]
|
|
|
+ });
|
|
|
+
|
|
|
+ // 层级高亮
|
|
|
+ this.graph.on('node:mouseenter', (e) => {
|
|
|
+ const node = e.item;
|
|
|
+ const graph = this.graph;
|
|
|
+ // 先全部虚化
|
|
|
+ graph.getNodes().forEach(n => graph.setItemState(n, 'inactive', true));
|
|
|
+ graph.getEdges().forEach(edge => graph.setItemState(edge, 'inactive', true));
|
|
|
+ // 当前节点高亮
|
|
|
+ graph.setItemState(node, 'inactive', false);
|
|
|
+ graph.setItemState(node, 'highlight', true);
|
|
|
+ // 直接相连的边和节点高亮
|
|
|
+ node.getEdges().forEach(edge => {
|
|
|
+ graph.setItemState(edge, 'inactive', false);
|
|
|
+ graph.setItemState(edge, 'highlight', true);
|
|
|
+ const source = edge.getSource();
|
|
|
+ const target = edge.getTarget();
|
|
|
+ [source, target].forEach(n => {
|
|
|
+ graph.setItemState(n, 'inactive', false);
|
|
|
+ graph.setItemState(n, 'highlight', true);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ this.graph.on('node:mouseleave', (e) => {
|
|
|
+ const graph = this.graph;
|
|
|
+ // 恢复所有状态
|
|
|
+ graph.getNodes().forEach(n => {
|
|
|
+ graph.clearItemStates(n);
|
|
|
+ });
|
|
|
+ graph.getEdges().forEach(edge => {
|
|
|
+ graph.clearItemStates(edge);
|
|
|
+ });
|
|
|
});
|
|
|
|
|
|
|
|
@@ -103,41 +229,58 @@ export default {
|
|
|
if (edge.target) linkCount[edge.target] = (linkCount[edge.target] || 0) + 1;
|
|
|
});
|
|
|
|
|
|
- // 六档蓝色系(更有区分度)
|
|
|
- const strokeColors = [
|
|
|
- '#b3e5fc', // 0 浅蓝青
|
|
|
- '#4fc3f7', // 1 天蓝
|
|
|
- '#0288d1', // 2 深天蓝
|
|
|
- '#1976d2', // 3 标准蓝
|
|
|
- '#1565c0', // 4 深蓝
|
|
|
- '#0d47a1', // 5 靛蓝
|
|
|
- '#002171' // 6+ 极深蓝
|
|
|
- ];
|
|
|
- const fillColors = [
|
|
|
- 'rgba(179,229,252,0.2)',
|
|
|
- 'rgba(79,195,247,0.2)',
|
|
|
- 'rgba(2,136,209,0.2)',
|
|
|
- 'rgba(25,118,210,0.2)',
|
|
|
- 'rgba(21,101,192,0.2)',
|
|
|
- 'rgba(13,71,161,0.2)',
|
|
|
- 'rgba(0,33,113,0.2)'
|
|
|
- ];
|
|
|
+ // 随机颜色函数
|
|
|
+ const categoryColors = new Map();
|
|
|
+ const getRandomColor = () => {
|
|
|
+ const letters = '0123456789ABCDEF';
|
|
|
+ let color = '#';
|
|
|
+ for (let i = 0; i < 6; i++) {
|
|
|
+ color += letters[Math.floor(Math.random() * 16)];
|
|
|
+ }
|
|
|
+ return color;
|
|
|
+ };
|
|
|
+
|
|
|
+ const getCategoryColor = (category) => {
|
|
|
+ if (!category) return '#1890ff'; // 默认蓝色
|
|
|
+ if (!categoryColors.has(category)) {
|
|
|
+ categoryColors.set(category, getRandomColor());
|
|
|
+ }
|
|
|
+ return categoryColors.get(category);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 将十六进制颜色转换为rgba格式
|
|
|
+ const hexToRgba = (hex, alpha = 0.2) => {
|
|
|
+ const r = parseInt(hex.slice(1, 3), 16);
|
|
|
+ const g = parseInt(hex.slice(3, 5), 16);
|
|
|
+ const b = parseInt(hex.slice(5, 7), 16);
|
|
|
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
|
+ };
|
|
|
|
|
|
const graphData = {
|
|
|
nodes: data.nodes.map(node => {
|
|
|
const count = linkCount[node.id] || 0;
|
|
|
- const cappedCount = Math.min(count, 6);
|
|
|
const size = 20 + count * 10; // 不封顶
|
|
|
- const stroke = strokeColors[cappedCount];
|
|
|
- const fill = fillColors[cappedCount];
|
|
|
+ const categoryColor = getCategoryColor(node.category);
|
|
|
+ const fill = hexToRgba(categoryColor, 0.2);
|
|
|
+ console.log(categoryColor);
|
|
|
+
|
|
|
+ // 处理标签文本截断
|
|
|
+ const fullLabel = node.label || node.id || '';
|
|
|
+ const maxLength = 8; // 最大显示字数
|
|
|
+ const displayLabel = fullLabel.length > maxLength
|
|
|
+ ? fullLabel.substring(0, maxLength) + '...'
|
|
|
+ : fullLabel;
|
|
|
+
|
|
|
// 保留 comboId 字段
|
|
|
const nodeData = {
|
|
|
id: node.id,
|
|
|
- label: node.label,
|
|
|
+ label: displayLabel, // 使用截断后的标签
|
|
|
+ fullLabel: fullLabel, // 保存完整标签用于tooltip
|
|
|
+ description: node.description,
|
|
|
size,
|
|
|
style: {
|
|
|
fill,
|
|
|
- stroke,
|
|
|
+ stroke: categoryColor,
|
|
|
lineWidth: 2
|
|
|
}
|
|
|
};
|
|
@@ -166,11 +309,11 @@ export default {
|
|
|
this.graph.render();
|
|
|
|
|
|
this.graph.on('node:mouseenter', (e) => {
|
|
|
- this.graph.setItemState(e.item, 'hover', true);
|
|
|
+ this.graph.setItemState(e.item, 'focus', true);
|
|
|
});
|
|
|
|
|
|
this.graph.on('node:mouseleave', (e) => {
|
|
|
- this.graph.setItemState(e.item, 'hover', false);
|
|
|
+ this.graph.setItemState(e.item, 'focus', false);
|
|
|
});
|
|
|
},
|
|
|
handleResize() {
|
|
@@ -186,4 +329,45 @@ export default {
|
|
|
|
|
|
</script>
|
|
|
|
|
|
-<style></style>
|
|
|
+<style scoped>
|
|
|
+.graph-toolbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+.graph-btn {
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #3370ff;
|
|
|
+ color: #3370ff;
|
|
|
+ border-radius: 4px;
|
|
|
+ width: 60px;
|
|
|
+ height: 30px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ transition: background 0.2s, color 0.2s;
|
|
|
+ outline: none;
|
|
|
+}
|
|
|
+.graph-btn:hover {
|
|
|
+ background: #3370ff;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+.graph-btn-primary {
|
|
|
+ background: #3370ff;
|
|
|
+ border-color: #3370ff;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+.graph-btn-primary:hover {
|
|
|
+ background: #3370ff;
|
|
|
+ border-color: #3370ff;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+.graph-input {
|
|
|
+ padding: 0 10px;
|
|
|
+ border: 1px solid #ccc;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 14px;
|
|
|
+ height: 30px;
|
|
|
+ outline: none;
|
|
|
+}
|
|
|
+</style>
|