320 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			320 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
<template>
 | 
						||
  <div class="detailNode-component">
 | 
						||
    <img src="@/assets/images/icon/goback.png" alt="" class="goback" @click="handleGoback" />
 | 
						||
    <div class="container" ref="detailContainer" id="container"></div>
 | 
						||
  </div>
 | 
						||
</template>
 | 
						||
 | 
						||
<script setup>
 | 
						||
import { defineProps, onMounted, ref } from "vue";
 | 
						||
import * as echarts from "echarts";
 | 
						||
import { cropToCircleAsync } from "@/utils/transform";
 | 
						||
import anchorNeighbors from "@/assets/json/anchor_neighbors.json";
 | 
						||
import nodeHoverImg from "@/assets/images/nodeHover.png";
 | 
						||
import { useKeyNodeRecognitionStore } from "@/store/keyNodeRecognition/index";
 | 
						||
const keyNodeStore = useKeyNodeRecognitionStore();
 | 
						||
const props = defineProps({
 | 
						||
  communityNode: {
 | 
						||
    type: Object,
 | 
						||
    default: 0
 | 
						||
  }
 | 
						||
});
 | 
						||
const emit = defineEmits(["click:openDialog", "click:goback"]);
 | 
						||
const detailContainer = ref(null);
 | 
						||
const currentSelectedCommunity = ref({});
 | 
						||
let chart = null;
 | 
						||
 | 
						||
// 根据节点数量计算缩放比例
 | 
						||
const calculateInitialZoom = (nodes) => {
 | 
						||
  const NODE_COUNT = nodes.length;
 | 
						||
  if (NODE_COUNT > 1000) return 0.1;
 | 
						||
  if (NODE_COUNT > 200) return 0.2;
 | 
						||
  if (NODE_COUNT > 100) return 0.3;
 | 
						||
  if (NODE_COUNT > 50) return 0.4;
 | 
						||
  return 0.8; // 默认值
 | 
						||
};
 | 
						||
 | 
						||
const initChart = async () => {
 | 
						||
  currentSelectedCommunity.value = props.communityNode;
 | 
						||
 | 
						||
  chart = echarts.init(document.getElementById("container"));
 | 
						||
  //处理节点
 | 
						||
  //从锚点邻居数据集中查找出该锚点所有的邻居节点
 | 
						||
  let nodes = [];
 | 
						||
 | 
						||
  //批量将头像转换成base64
 | 
						||
  const extraInfo = keyNodeStore.anchorExtraInfos;
 | 
						||
  const keys = Object.keys(extraInfo);
 | 
						||
  for (const key of keys) {
 | 
						||
    const userInfo = extraInfo[key];
 | 
						||
    userInfo.avatar = await cropToCircleAsync(userInfo.avatar);
 | 
						||
  }
 | 
						||
 | 
						||
  //筛选出本社团中所有锚点的邻居节点并重新将需要补充的数据映射到该列表中
 | 
						||
  const filterResult = Object.entries(anchorNeighbors)
 | 
						||
    .filter(([anchorId]) => currentSelectedCommunity.value.anchorList.includes(anchorId))
 | 
						||
    .map((filteredList) => ({
 | 
						||
      ...extraInfo[filteredList[0]],
 | 
						||
      anchor: filteredList[0],
 | 
						||
      neighbors: filteredList[1].map((neighNode) => ({ name: neighNode, avatar: "" }))
 | 
						||
    }));
 | 
						||
 | 
						||
  //处理连线
 | 
						||
  let links = [];
 | 
						||
  filterResult.forEach(({ anchor, neighbors }) => {
 | 
						||
    (neighbors ?? []).forEach((neigh) => {
 | 
						||
      links.push({ source: anchor, target: neigh.name });
 | 
						||
    });
 | 
						||
  });
 | 
						||
 | 
						||
  // 合并所有锚点和邻居节点到一个数组并去重
 | 
						||
  let nodeSet = new Set();
 | 
						||
  filterResult.forEach((item) => {
 | 
						||
    //添加锚点自己
 | 
						||
    if (!nodeSet.has(item.anchor)) {
 | 
						||
      nodes.push({ name: item?.anchor, value: item?.anchor, category: 1, ...item });
 | 
						||
      nodeSet.add(item?.anchor);
 | 
						||
    }
 | 
						||
    //添加该锚点的邻居
 | 
						||
    (item.neighbors || []).forEach((n) => {
 | 
						||
      if (!nodeSet.has(n?.name)) {
 | 
						||
        nodes.push({ name: n.name, value: n.name, category: 0, avatar: n.avatar ?? "" });
 | 
						||
        nodeSet.add(n.name);
 | 
						||
      }
 | 
						||
    });
 | 
						||
  });
 | 
						||
 | 
						||
  const data = { nodes, links };
 | 
						||
  console.log(data);
 | 
						||
 | 
						||
  const categories = [
 | 
						||
    { name: "邻居账号", category: 0 },
 | 
						||
    { name: "锚点账号", category: 1 }
 | 
						||
  ];
 | 
						||
 | 
						||
  const option = {
 | 
						||
    //图例配置
 | 
						||
    legend: [
 | 
						||
      {
 | 
						||
        data: categories.map((c) => ({
 | 
						||
          name: c.name,
 | 
						||
          itemStyle: {
 | 
						||
            color:
 | 
						||
              c.category === 0
 | 
						||
                ? new echarts.graphic.LinearGradient(1, 0, 0, 0, [
 | 
						||
                    { offset: 0, color: "#49c3ed" },
 | 
						||
                    { offset: 0.5, color: "#5fa3e0" },
 | 
						||
                    { offset: 1, color: "#7286d4" }
 | 
						||
                  ])
 | 
						||
                : "#cccccc" // 锚点账号用灰色纯色(默认头像常用色)
 | 
						||
          },
 | 
						||
          // 锚点账号用圆形头像,普通账号保持默认
 | 
						||
          icon:
 | 
						||
            c.category === 1
 | 
						||
              ? "path://M50 50 m -50 0 a 50 50 0 1 0 100 0 a 50 50 0 1 0 -100 0 Z" // 精确圆形路径
 | 
						||
              : "circle" // 普通账号用ECharts内置circle
 | 
						||
        })),
 | 
						||
        right: 15,
 | 
						||
        bottom: 13,
 | 
						||
        icon: "circle",
 | 
						||
        orient: "vertical",
 | 
						||
        itemWidth: 16,
 | 
						||
        itemHeight: 16,
 | 
						||
        itemGap: 12,
 | 
						||
        backgroundColor: "rgba(0,67,125,0.56)", // 半透明深蓝
 | 
						||
        borderRadius: 8, // 圆角
 | 
						||
        borderColor: "#c2f2ff", // 淡蓝色边框
 | 
						||
        borderWidth: 0.3,
 | 
						||
        padding: [12, 20, 12, 20], // 上右下左
 | 
						||
        textStyle: {
 | 
						||
          color: "#fff",
 | 
						||
          fontSize: 16,
 | 
						||
          fontWeight: "normal"
 | 
						||
        }
 | 
						||
      }
 | 
						||
    ],
 | 
						||
    //hover上去的窗口
 | 
						||
    tooltip: {
 | 
						||
      trigger: "item",
 | 
						||
      trigger: "item",
 | 
						||
      backgroundColor: "rgba(0,0,0,0)", // 透明背景
 | 
						||
      borderColor: "rgba(0,0,0,0)", // 透明边框
 | 
						||
      borderWidth: 0,
 | 
						||
      extraCssText: "box-shadow:none;padding:0;",
 | 
						||
      formatter: function (params) {
 | 
						||
        if (params.dataType === "node" && params.data.category) {
 | 
						||
          return `<div
 | 
						||
      style="
 | 
						||
        height: 76px;
 | 
						||
        border-radius: 4px;
 | 
						||
        background: url('${nodeHoverImg}');
 | 
						||
        background-size: cover;
 | 
						||
        background-position: center;
 | 
						||
        background-repeat: no-repeat;
 | 
						||
        padding:20px 20px;
 | 
						||
        display: flex;
 | 
						||
        align-items: center;
 | 
						||
        justify-content: center;
 | 
						||
        flex-direction: column;
 | 
						||
      "
 | 
						||
    >
 | 
						||
      <div>
 | 
						||
        <div style="color: #fff; letter-spacing: 0.14px; display: flex; align-items: center">
 | 
						||
          <div style="font-size: 16px">${params.data.name}</div>
 | 
						||
          <div
 | 
						||
            style="
 | 
						||
              margin-left: 10px;
 | 
						||
              padding: 3px 7px;
 | 
						||
              background-color: rgba(171, 247, 255, 0.2);
 | 
						||
              border-radius: 2px;
 | 
						||
              font-size: 12px;
 | 
						||
            "
 | 
						||
          >
 | 
						||
            ${params.data.label}
 | 
						||
          </div>
 | 
						||
        </div>
 | 
						||
        <div style="display: flex; font-size: 14px; margin-top: 5px;color:#fff">
 | 
						||
          <div>关注数: ${params.data.atten}</div>
 | 
						||
          <div style="margin-left: 20px">粉丝数: ${params.data.fancy}</div>
 | 
						||
        </div>
 | 
						||
      </div>
 | 
						||
    </div>`;
 | 
						||
        } else if (params.dataType === "node" && !params.data.category) {
 | 
						||
          return `<div
 | 
						||
      style="
 | 
						||
        height: 56px;
 | 
						||
        border-radius: 4px;
 | 
						||
        background: url('${nodeHoverImg}');
 | 
						||
        background-size: cover;
 | 
						||
        background-position: center;
 | 
						||
        background-repeat: no-repeat;
 | 
						||
        padding:10px 10px;
 | 
						||
        display: flex;
 | 
						||
        align-items: center;
 | 
						||
        justify-content: center;
 | 
						||
        flex-direction: column;
 | 
						||
      "
 | 
						||
    >
 | 
						||
      <div>
 | 
						||
        <div style="color: #fff; letter-spacing: 0.14px; display: flex; align-items: center">
 | 
						||
          <div style="font-size: 16px">${params.data.name}</div>
 | 
						||
          <div
 | 
						||
            style="
 | 
						||
              margin-left: 10px;
 | 
						||
              padding: 3px 7px;
 | 
						||
              background-color: rgba(171, 247, 255, 0.2);
 | 
						||
              border-radius: 2px;
 | 
						||
              font-size: 12px;
 | 
						||
            "
 | 
						||
          >
 | 
						||
            普通自媒体
 | 
						||
          </div>
 | 
						||
        </div>
 | 
						||
      </div>
 | 
						||
    </div>`;
 | 
						||
        }
 | 
						||
      }
 | 
						||
    },
 | 
						||
    series: [
 | 
						||
      {
 | 
						||
        type: "graph",
 | 
						||
        zoom: calculateInitialZoom(data.nodes),
 | 
						||
        layout: "force",
 | 
						||
        animation: false,
 | 
						||
        draggable: true,
 | 
						||
        roam: true,
 | 
						||
        categories: categories,
 | 
						||
        force: {
 | 
						||
          initLayout: "circular",
 | 
						||
          edgeLength: data.nodes.length > 1000 ? 600 : 300,
 | 
						||
          repulsion: 5000,
 | 
						||
          gravity: 0.9,
 | 
						||
          friction: 0.1,
 | 
						||
          layoutAnimation: true,
 | 
						||
          coolingFactor: 0.1
 | 
						||
        },
 | 
						||
 | 
						||
        animationDurationUpdate: 3500, // 节点移动更平滑
 | 
						||
        data: data.nodes.map((node) => ({
 | 
						||
          ...node,
 | 
						||
          symbolSize: node.category == 1 ? 70 : 30,
 | 
						||
          symbol: node?.avatar ? `image://${node?.avatar}` : "",
 | 
						||
          z: node.category === 1 ? 10 : 1, // 设置层级
 | 
						||
          itemStyle: {
 | 
						||
            color: {
 | 
						||
              type: "radial",
 | 
						||
              x: 0.97,
 | 
						||
              y: 0.38,
 | 
						||
              r: 0.86,
 | 
						||
              colorStops:
 | 
						||
                node.category === 0
 | 
						||
                  ? [
 | 
						||
                      // anchorCount为0的节点渐变(深蓝到青)
 | 
						||
                      { offset: 0, color: "#49c3ed" }, // 最右侧
 | 
						||
                      { offset: 0.5, color: "#5fa3e0" }, // 中间
 | 
						||
                      { offset: 1, color: "#7286d4" } // 最左侧
 | 
						||
                    ]
 | 
						||
                  : [
 | 
						||
                      // anchorCount为1的节点渐变(原配色)
 | 
						||
                      { offset: 0, color: "#7ff2c1" },
 | 
						||
                      { offset: 0.37, color: "#85e7d2" },
 | 
						||
                      { offset: 1, color: "#8bdbe4" }
 | 
						||
                    ]
 | 
						||
            },
 | 
						||
            opacity: 1,
 | 
						||
            borderColor: "#46C6AD",
 | 
						||
            borderWidth: 0,
 | 
						||
            shadowBlur: 4,
 | 
						||
            shadowColor: "rgba(19, 27, 114, 0.25)",
 | 
						||
            shadowOffsetY: 4
 | 
						||
          }
 | 
						||
        })),
 | 
						||
        links: data.links,
 | 
						||
        lineStyle: {
 | 
						||
          color: "#37ACD7",
 | 
						||
          width: 1
 | 
						||
        }
 | 
						||
      }
 | 
						||
    ]
 | 
						||
  };
 | 
						||
 | 
						||
  chart.setOption(option);
 | 
						||
};
 | 
						||
 | 
						||
const handleClickNode = () => {
 | 
						||
  chart.on("click", function (params) {
 | 
						||
    if (params.dataType == "node" && params.data.category == 1) {
 | 
						||
      emit("click:openDialog", params.data);
 | 
						||
    }
 | 
						||
  });
 | 
						||
};
 | 
						||
 | 
						||
const handleGoback = () => {
 | 
						||
  emit("click:goback", "CommunityNode");
 | 
						||
};
 | 
						||
onMounted(async () => {
 | 
						||
  await initChart();
 | 
						||
  handleClickNode();
 | 
						||
});
 | 
						||
</script>
 | 
						||
 | 
						||
<style scoped lang="less">
 | 
						||
.detailNode-component {
 | 
						||
  width: 100%;
 | 
						||
  height: 100%;
 | 
						||
  .container {
 | 
						||
    width: 100%;
 | 
						||
    height: 500px;
 | 
						||
    display: flex;
 | 
						||
    align-items: center;
 | 
						||
    justify-content: center;
 | 
						||
  }
 | 
						||
  .goback {
 | 
						||
    position: absolute;
 | 
						||
    top: 10px;
 | 
						||
    left: 20px;
 | 
						||
    cursor: pointer;
 | 
						||
  }
 | 
						||
}
 | 
						||
</style>
 |