325 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
			
		
		
	
	
			325 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			Vue
		
	
	
	
	
	
<template>
 | 
						||
  <div class="communityNode-component">
 | 
						||
    <div id="container" class="container" ref="teamContainer"></div>
 | 
						||
  </div>
 | 
						||
</template>
 | 
						||
 | 
						||
<script setup>
 | 
						||
import { defineProps, onMounted, ref, defineEmits, defineExpose, watch } from "vue";
 | 
						||
import * as echarts from "echarts";
 | 
						||
import anchor from "@/assets/json/anchor_community.json";
 | 
						||
import community from "@/assets/json/community_nodes.json";
 | 
						||
import nodeHoverImg from "@/assets/images/nodeHover.png";
 | 
						||
import { storeToRefs } from "pinia";
 | 
						||
import { useKeyNodeRecognitionStore } from "@/store/keyNodeRecognition/index";
 | 
						||
import csvUrl from "@/assets/json/community_edges.csv?url";
 | 
						||
const keyNodeStore = useKeyNodeRecognitionStore();
 | 
						||
const { currentUser } = storeToRefs(keyNodeStore);
 | 
						||
const props = defineProps({});
 | 
						||
const emit = defineEmits(["click:anchorNode"]);
 | 
						||
const teamContainer = ref(null);
 | 
						||
 | 
						||
//当前在userPanel中选择的某个user
 | 
						||
const currentSelectedUser = ref(null);
 | 
						||
let chart = null;
 | 
						||
//当选中userPanel中的某个用户的时候,更新当前组件的用户
 | 
						||
watch(currentUser, (val) => {
 | 
						||
  if (val) {
 | 
						||
    currentSelectedUser.value = val;
 | 
						||
    const option = chart.getOption();
 | 
						||
 | 
						||
    //实现选中只高亮一个
 | 
						||
    chart.dispatchAction({
 | 
						||
      //先让所有的先取消高亮
 | 
						||
      type: "downplay",
 | 
						||
      seriesIndex: 0
 | 
						||
    });
 | 
						||
    const nodeIndex = option.series[0].data.findIndex((item) =>
 | 
						||
      item.anchorList.includes(currentSelectedUser.value.name)
 | 
						||
    );
 | 
						||
    chart.dispatchAction({
 | 
						||
      //再让单独一个高亮
 | 
						||
      type: "highlight",
 | 
						||
      dataIndex: nodeIndex
 | 
						||
    });
 | 
						||
 | 
						||
    // 2. 平移到节点
 | 
						||
  }
 | 
						||
});
 | 
						||
 | 
						||
const initChart = async () => {
 | 
						||
  // 初始化图表
 | 
						||
  chart = echarts.init(document.getElementById("container"));
 | 
						||
 | 
						||
  //处理data
 | 
						||
  //统计每一个锚点在某些社团,这个社团包含锚点的个数(key: 社团id, value: 这个社团含有几个锚点)
 | 
						||
  //统计每个社团包含的锚点id列表
 | 
						||
  const anchorCommunityInfo = {};
 | 
						||
  Object.entries(anchor).forEach(([anchorId, communityId]) => {
 | 
						||
    if (!anchorCommunityInfo[communityId]) {
 | 
						||
      //如果该社团不包含锚点
 | 
						||
      anchorCommunityInfo[communityId] = []; //就初始化该社团的锚点数组
 | 
						||
    }
 | 
						||
    anchorCommunityInfo[communityId].push(anchorId); //把这个社团中的所有锚点id存到列表中(处理一对多的关系:一个社团对应多个锚点)
 | 
						||
  });
 | 
						||
 | 
						||
  //包含锚点的社团 (绿色闪烁的节点)
 | 
						||
  const includeAnchorCommunity = Object.entries(anchorCommunityInfo).map(
 | 
						||
    ([communityId, anchorList]) => ({
 | 
						||
      name: parseInt(communityId), // 社团id
 | 
						||
      anchorCount: anchorList.length, // 该社团包含锚点的个数
 | 
						||
      anchorList: anchorList // 该社团包含的锚点id数组
 | 
						||
    })
 | 
						||
  );
 | 
						||
 | 
						||
  // 创建包含锚点的社团名称集合(提高查找效率)
 | 
						||
  const includedCommunityId = includeAnchorCommunity.map((node) => node.name.toString());
 | 
						||
 | 
						||
  //筛选出不包含锚点的社团(不闪烁的蓝色节点)
 | 
						||
  const uncontainedAnchorCommunity = Object.keys(community)
 | 
						||
    .filter((node) => !includedCommunityId.includes(node))
 | 
						||
    .map((node) => ({ name: parseInt(node), anchorCount: 0 }));
 | 
						||
 | 
						||
  //合并成功后,统一设置id,避免重复id出现导致echarts渲染失败,并且取出每个社团中有多少个节点
 | 
						||
  const nodes = includeAnchorCommunity.concat(uncontainedAnchorCommunity).map((node, index) => ({
 | 
						||
    id: index,
 | 
						||
    total: Object.values(community)[node.name].length,
 | 
						||
    category: node.anchorCount > 0 ? 1 : 0, //(1:含有锚点的社团分类,0:不含锚点的社团)
 | 
						||
    ...node
 | 
						||
  }));
 | 
						||
 | 
						||
  //处理links
 | 
						||
  // 先把csv文件里的数据进行格式化
 | 
						||
  let links = [];
 | 
						||
  const linksCsv = await fetch(csvUrl);
 | 
						||
  const csvText = await linksCsv.text();
 | 
						||
  const lines = csvText.split("\n");
 | 
						||
  links = lines
 | 
						||
    .slice(1)
 | 
						||
    .map((line) => {
 | 
						||
      if (!line) return null;
 | 
						||
      const values = line.split(",");
 | 
						||
      return {
 | 
						||
        source: parseInt(values[0]),
 | 
						||
        target: parseInt(values[1]),
 | 
						||
        edge: parseInt(values[2])
 | 
						||
      };
 | 
						||
    })
 | 
						||
    .filter(Boolean);
 | 
						||
 | 
						||
  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, // 从左到右渐变(x0=1, x1=0)
 | 
						||
                    [
 | 
						||
                      { offset: 0, color: "#49c3ed" }, // 起始色(最右)
 | 
						||
                      { offset: 0.5, color: "#5fa3e0" }, // 中间色
 | 
						||
                      { offset: 1, color: "#7286d4" } // 结束色(最左)
 | 
						||
                    ]
 | 
						||
                  ) // 分类0的渐变
 | 
						||
                : new echarts.graphic.LinearGradient(
 | 
						||
                    1,
 | 
						||
                    0,
 | 
						||
                    0,
 | 
						||
                    0, // 从左到右渐变 (x0=1, x1=0)
 | 
						||
                    [
 | 
						||
                      { offset: 0, color: "#7ff2c1" },
 | 
						||
                      { offset: 0.37, color: "#85e7d2" },
 | 
						||
                      { offset: 1, color: "#8bdbe4" }
 | 
						||
                    ]
 | 
						||
                  ) // 分类1的渐变
 | 
						||
          }
 | 
						||
        })),
 | 
						||
        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") {
 | 
						||
          return `<div
 | 
						||
                    style="
 | 
						||
                    width: 107px;
 | 
						||
                    height: 68px;
 | 
						||
                    border-radius: 4px;
 | 
						||
                    background: url('${nodeHoverImg}');
 | 
						||
                    background-size: cover;
 | 
						||
                    background-position: center;
 | 
						||
                    background-repeat: no-repeat;
 | 
						||
                    display: flex;
 | 
						||
                    align-items: center;
 | 
						||
                    justify-content: center;
 | 
						||
                    flex-direction: column;
 | 
						||
                    ">
 | 
						||
                <div style="color:#fff;letter-spacing: 0.14px;">
 | 
						||
                    <div >节点数:${params.data.total}</div>
 | 
						||
                    <div>锚点数:${params.data.anchorCount}</div>
 | 
						||
              </div>
 | 
						||
            </div>`;
 | 
						||
        }
 | 
						||
        return "";
 | 
						||
      }
 | 
						||
    },
 | 
						||
 | 
						||
    //权值设置样式
 | 
						||
    edgeLabel: {
 | 
						||
      show: false,
 | 
						||
      position: "middle",
 | 
						||
      formatter: function (params) {
 | 
						||
        return params.data.edge;
 | 
						||
      },
 | 
						||
      fontSize: 14
 | 
						||
    },
 | 
						||
    emphasis: {
 | 
						||
      edgeLabel: {
 | 
						||
        show: true,
 | 
						||
        color: "#fff",
 | 
						||
        fontSize: 18,
 | 
						||
        textShadowColor: "#fff",
 | 
						||
        textShadowBlur: 0,
 | 
						||
        textShadowOffsetX: 0,
 | 
						||
        textShadowOffsetY: 0
 | 
						||
      }
 | 
						||
    },
 | 
						||
    series: [
 | 
						||
      {
 | 
						||
        type: "graph",
 | 
						||
        layout: "force",
 | 
						||
        animation: false,
 | 
						||
        draggable: true,
 | 
						||
        roam: true,
 | 
						||
        zoom: 0.4,
 | 
						||
        categories: categories,
 | 
						||
        force: {
 | 
						||
          initLayout: null,
 | 
						||
          edgeLength: 1000,
 | 
						||
          repulsion: 1000,
 | 
						||
          gravity: 0.4,
 | 
						||
          friction: 0.6,
 | 
						||
          layoutAnimation: true,
 | 
						||
          coolingFactor: 0.1
 | 
						||
        },
 | 
						||
 | 
						||
        animationDurationUpdate: 3500, // 节点移动更平滑
 | 
						||
        data: data.nodes.map((node) => ({
 | 
						||
          ...node,
 | 
						||
          symbolSize: node.total / 20 < 30 ? 30 : node.total / 20,
 | 
						||
          itemStyle: {
 | 
						||
            color: {
 | 
						||
              type: "radial",
 | 
						||
              x: 0.97,
 | 
						||
              y: 0.38,
 | 
						||
              r: 0.86,
 | 
						||
              colorStops:
 | 
						||
                node.anchorCount === 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: 1,
 | 
						||
            shadowBlur: 4,
 | 
						||
            borderType: "dashed",
 | 
						||
            shadowColor: "rgba(19, 27, 114, 0.25)"
 | 
						||
          },
 | 
						||
          emphasis: {
 | 
						||
            itemStyle: {
 | 
						||
              shadowBlur: 20,
 | 
						||
              shadowColor: "#c4a651",
 | 
						||
              borderColor: "#fcd267",
 | 
						||
              borderWidth: node.category == 0 ? 1 : 5,
 | 
						||
              borderType: "solid"
 | 
						||
            }
 | 
						||
          }
 | 
						||
        })),
 | 
						||
        links: data.links,
 | 
						||
        lineStyle: {
 | 
						||
          color: "#37ACD7",
 | 
						||
          width: 1
 | 
						||
        }
 | 
						||
      }
 | 
						||
    ]
 | 
						||
  };
 | 
						||
 | 
						||
  chart.setOption(option);
 | 
						||
};
 | 
						||
 | 
						||
const handleClickNode = () => {
 | 
						||
  chart.on("click", function (params) {
 | 
						||
    //params.data.name才是社团id
 | 
						||
    if (params.dataType === "node" && params.data.category == 1) {
 | 
						||
      emit("click:anchorNode", params.data);
 | 
						||
    }
 | 
						||
  });
 | 
						||
};
 | 
						||
 | 
						||
onMounted(() => {
 | 
						||
  initChart();
 | 
						||
  handleClickNode();
 | 
						||
});
 | 
						||
</script>
 | 
						||
 | 
						||
<style scoped lang="less">
 | 
						||
.communityNode-component {
 | 
						||
  width: 100%;
 | 
						||
  height: 100%;
 | 
						||
  .container {
 | 
						||
    width: 100%;
 | 
						||
    height: 450px;
 | 
						||
    display: flex;
 | 
						||
    align-items: center;
 | 
						||
    justify-content: center;
 | 
						||
  }
 | 
						||
}
 | 
						||
</style>
 |