Compare commits
2 Commits
b5acf7652d
...
1688d8b200
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1688d8b200 | ||
|
|
b00b1293e4 |
|
|
@ -8,13 +8,44 @@
|
|||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import G6 from "@antv/g6";
|
||||
|
||||
// 注册自定义边
|
||||
G6.registerEdge('combo-border-edge', {
|
||||
draw(cfg, group) {
|
||||
const { sourceCombo, targetCombo } = cfg;
|
||||
if (!sourceCombo || !targetCombo) return;
|
||||
// 获取圆心和半径
|
||||
const { x: x1, y: y1, r: r1 } = sourceCombo;
|
||||
const { x: x2, y: y2, r: r2 } = targetCombo;
|
||||
// 计算两个圆心连线的单位向量
|
||||
const dx = x2 - x1, dy = y2 - y1;
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
const ux = dx / len, uy = dy / len;
|
||||
// 计算切点
|
||||
const sx = x1 + ux * r1;
|
||||
const sy = y1 + uy * r1;
|
||||
const tx = x2 - ux * r2;
|
||||
const ty = y2 - uy * r2;
|
||||
// 绘制曲线或直线
|
||||
return group.addShape('path', {
|
||||
attrs: {
|
||||
stroke: '#fff',
|
||||
lineWidth: 2,
|
||||
path: [
|
||||
['M', sx, sy],
|
||||
// 可加弯曲 ['Q', (sx+tx)/2+20, (sy+ty)/2-20, tx, ty]
|
||||
['L', tx, ty]
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 'line');
|
||||
|
||||
let graph = null;
|
||||
const containerRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
// --- 1. Data Preparation ---
|
||||
|
||||
// Initial node data (no changes needed here)
|
||||
const initialNodes = [
|
||||
{ id: '6010609377', name: '外贸发布BBS', default_avatar: '外贸发布BBS_waimaofabuBBS.png' },
|
||||
{ id: '6797803070', name: '爱锤盾海桃-霆恩启副', default_avatar: '爱锤盾海桃-霆恩启副_aichuidunhaitao-tingenqifu.png' },
|
||||
|
|
@ -38,7 +69,7 @@ onMounted(() => {
|
|||
{ id: '1854070075', name: '深海一万米', default_avatar: '深海一万米_shenhaiyiwanmi.png' },
|
||||
];
|
||||
|
||||
// Map node names to their cluster ID
|
||||
// The mapping of nodes to their clusters remains the same
|
||||
const clusterMapping = {
|
||||
'中国海警': 'cluster-0', '十八子91221': 'cluster-0', '大侠啊啊啊啊': 'cluster-0',
|
||||
'苍龙飞天79': 'cluster-1', '平安泸县': 'cluster-1', '新浪军事': 'cluster-1', '环球时报': 'cluster-1', '乐之567': 'cluster-1', 'CGTN记者团': 'cluster-1', '深海一万米': 'cluster-1',
|
||||
|
|
@ -48,36 +79,29 @@ onMounted(() => {
|
|||
'肥_谍_gg': 'cluster-5',
|
||||
};
|
||||
|
||||
const nodeSize = 50;
|
||||
|
||||
const nodes = initialNodes.map(node => ({
|
||||
...node,
|
||||
comboId: clusterMapping[node.name],
|
||||
label: '', // Labels are disabled for this design
|
||||
label: '',
|
||||
type: 'image',
|
||||
img: new URL(`/src/assets/user3/${node.default_avatar}`, import.meta.url).href,
|
||||
clipCfg: { show: true, type: 'circle', r: 25 },
|
||||
clipCfg: { show: true, type: 'circle', r: nodeSize / 2 },
|
||||
}));
|
||||
|
||||
// --- Fixed Combo Layout, Styles, and Connections ---
|
||||
// Define Combos with styles and relative sizes. Positions will be calculated by the layout engine.
|
||||
const combos = [
|
||||
{ id: 'cluster-0', label: '', r: 90, style: { fill: '#FF5E0014', stroke: '#FF5E007A' } },
|
||||
{ id: 'cluster-1', label: '', r: 110, style: { fill: '#BA21EB14', stroke: '#BA21EB7A' } },
|
||||
{ id: 'cluster-2', label: '', r: 80, style: { fill: '#2191EB29', stroke: '#2191EB' } },
|
||||
{ id: 'cluster-3', label: '', r: 130, style: { fill: '#FFD90A14', stroke: '#FFD90A7A' } },
|
||||
{ id: 'cluster-4', label: '', r: 75, style: { fill: '#3DD99414', stroke: '#3DD9947A' } },
|
||||
{ id: 'cluster-5', label: '', r: 100, style: { fill: '#D93D6B14', stroke: '#D93D6B7A' } },
|
||||
];
|
||||
|
||||
// Define fixed positions, sizes, and styles for each combo based on SwiftUI code and screenshot
|
||||
const comboLayoutData = {
|
||||
'cluster-0': { x: 250, y: 220, r: 120, style: { fill: '#FF5E0014', stroke: '#FF5E007A' } }, // Orange
|
||||
'cluster-1': { x: 500, y: 180, r: 120, style: { fill: '#BA21EB14', stroke: '#BA21EB7A' } }, // Purple
|
||||
'cluster-2': { x: 700, y: 500, r: 80, style: { fill: '#2191EB29', stroke: '#2191EB' } }, // Blue
|
||||
'cluster-3': { x: 800, y: 250, r: 137, style: { fill: '#FFD90A14', stroke: '#FFD90A7A' } }, // Yellow
|
||||
'cluster-4': { x: 180, y: 480, r: 80, style: { fill: '#3DD99414', stroke: '#3DD9947A' } }, // Green
|
||||
'cluster-5': { x: 450, y: 450, r: 110, style: { fill: '#D93D6B14', stroke: '#D93D6B7A' } }, // Pink/Red
|
||||
};
|
||||
|
||||
const combos = Object.entries(comboLayoutData).map(([id, { x, y, r, style }]) => ({
|
||||
id,
|
||||
label: '',
|
||||
x, y, fx: x, fy: y, // Fix combo positions
|
||||
r, // Set radius directly
|
||||
style
|
||||
}));
|
||||
|
||||
// Define connections between combos based on the target image
|
||||
// Re-introduce edges to define relationships for the force layout.
|
||||
// These connections are based on the visual overlaps in the original image.
|
||||
const comboConnections = [
|
||||
['cluster-0', 'cluster-1'], ['cluster-0', 'cluster-4'],
|
||||
['cluster-1', 'cluster-3'], ['cluster-1', 'cluster-5'],
|
||||
|
|
@ -86,39 +110,86 @@ onMounted(() => {
|
|||
['cluster-5', 'cluster-2'],
|
||||
];
|
||||
|
||||
// G6 needs edges between nodes, so we find one node from each combo to connect.
|
||||
const edges = comboConnections.map(([sourceComboId, targetComboId], i) => {
|
||||
const sourceNode = nodes.find(n => n.comboId === sourceComboId);
|
||||
const targetNode = nodes.find(n => n.comboId === targetComboId);
|
||||
return {
|
||||
id: `edge-${i}`,
|
||||
source: sourceNode.id,
|
||||
target: targetNode.id,
|
||||
};
|
||||
return { id: `edge-${i}`, source: sourceComboId, target: targetComboId };
|
||||
});
|
||||
|
||||
const data = { nodes, combos, edges };
|
||||
|
||||
// --- 2. G6 Graph Initialization ---
|
||||
const container = containerRef.value;
|
||||
const width = container.scrollWidth;
|
||||
const height = container.scrollHeight || 700;
|
||||
// 1. 统计每个combo的节点数量
|
||||
const comboNodeCount = {};
|
||||
nodes.forEach(node => {
|
||||
comboNodeCount[node.comboId] = (comboNodeCount[node.comboId] || 0) + 1;
|
||||
});
|
||||
|
||||
// 先排序
|
||||
const sortedCombos = combos
|
||||
.map(combo => ({ ...combo, count: comboNodeCount[combo.id] || 0 }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
// 计算每排最大半径
|
||||
const topCombos = sortedCombos.slice(0, 3);
|
||||
const bottomCombos = sortedCombos.slice(3, 6);
|
||||
const topMaxR = Math.max(...topCombos.map(c => c.r));
|
||||
const bottomMaxR = Math.max(...bottomCombos.map(c => c.r));
|
||||
|
||||
// 横向间距 = 前后两个combo半径之和 + 额外间隔
|
||||
function getX(i, combosArr, width) {
|
||||
let x = combosArr[0].r + 60; // 左边留空
|
||||
for (let j = 0; j < i; j++) {
|
||||
x += combosArr[j].r + combosArr[j + 1].r + 80; // 80为额外间隔
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
const width = container.scrollWidth || 1200;
|
||||
const height = container.scrollHeight || 800;
|
||||
const topY = height * 0.25;
|
||||
const bottomY = height * 0.75;
|
||||
|
||||
// 上排
|
||||
topCombos.forEach((combo, idx) => {
|
||||
combo.x = getX(idx, topCombos, width);
|
||||
combo.y = topY;
|
||||
});
|
||||
// 下排
|
||||
bottomCombos.forEach((combo, idx) => {
|
||||
combo.x = getX(idx, bottomCombos, width);
|
||||
combo.y = bottomY;
|
||||
});
|
||||
|
||||
// 更新 combos
|
||||
combos.forEach(c => {
|
||||
const found = [...topCombos, ...bottomCombos].find(sc => sc.id === c.id);
|
||||
c.x = found.x;
|
||||
c.y = found.y;
|
||||
});
|
||||
|
||||
// 5. 节点分布在各自combo圆内
|
||||
nodes.forEach(node => {
|
||||
const combo = combos.find(c => c.id === node.comboId);
|
||||
// 随机分布在combo圆内
|
||||
const angle = Math.random() * 2 * Math.PI;
|
||||
const radius = Math.random() * (combo.r - nodeSize / 2);
|
||||
node.x = combo.x + Math.cos(angle) * radius;
|
||||
node.y = combo.y + Math.sin(angle) * radius;
|
||||
});
|
||||
|
||||
// 6. 用 layout: null,G6 不再自动布局
|
||||
graph = new G6.Graph({
|
||||
container,
|
||||
width,
|
||||
height,
|
||||
layout: {
|
||||
type: 'comboForce',
|
||||
gravity: 80, // Increase gravity to pull nodes towards combo centers
|
||||
nodeSpacing: 25,
|
||||
preventOverlap: true,
|
||||
preventComboOverlap: true, // Should be redundant with fixed combos, but safe to keep
|
||||
},
|
||||
groupByTypes: false,
|
||||
layout: null,
|
||||
modes: {
|
||||
default: ['drag-canvas', 'zoom-canvas'], // Disable node and combo dragging
|
||||
default: ['drag-canvas', 'zoom-canvas', 'drag-combo', 'drag-node']
|
||||
},
|
||||
defaultNode: {
|
||||
size: 50,
|
||||
size: nodeSize,
|
||||
style: {
|
||||
lineWidth: 2,
|
||||
stroke: '#37ACD7',
|
||||
|
|
@ -128,15 +199,15 @@ onMounted(() => {
|
|||
},
|
||||
defaultEdge: {
|
||||
style: {
|
||||
stroke: '#37ACD7',
|
||||
lineWidth: 1,
|
||||
opacity: 0.5,
|
||||
stroke: '#FFFFFF',
|
||||
lineWidth: 2,
|
||||
opacity: 0.3,
|
||||
},
|
||||
},
|
||||
defaultCombo: { // This is now a base style, individual styles will override
|
||||
defaultCombo: {
|
||||
type: 'circle',
|
||||
style: {
|
||||
lineWidth: 1,
|
||||
lineWidth: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -168,7 +239,7 @@ onMounted(() => {
|
|||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #0b1120; // Match the dark background from the screenshot
|
||||
background: #0b1120; // Set a background similar to the image
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -10,5 +10,10 @@ export default defineConfig({
|
|||
"@": path.resolve(__dirname, "src"),
|
||||
"@assets": path.resolve(__dirname, "src/assets")
|
||||
}
|
||||
}
|
||||
},
|
||||
server: {
|
||||
watch: {
|
||||
usePolling: true, // 解决部分环境下热更新失效
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user