diff --git a/dist.zip b/dist.zip index ded88e3..8e7ab63 100644 Binary files a/dist.zip and b/dist.zip differ diff --git a/src/views/GroupEvolution/components/graphWorker.js b/src/views/GroupEvolution/components/graphWorker.js new file mode 100644 index 0000000..ade178d --- /dev/null +++ b/src/views/GroupEvolution/components/graphWorker.js @@ -0,0 +1,147 @@ +// 图表计算Web Worker + +// 处理力导向布局计算 +self.onmessage = function(e) { + const { type, data } = e.data; + + let result; + + switch(type) { + case 'CLUSTER_ANALYZE': + result = processClusterAnalyze(data); + break; + case 'HIGHLIGHT_NODES': + result = processHighlightNodes(data); + break; + case 'HIGHLIGHT_LINKS': + result = processHighlightLinks(data); + break; + case 'ANOMALOUS_GROUP_PROCESS': + result = processAnomalousGroup(data); + break; + default: + self.postMessage({ + success: false, + type, + error: `Unknown task type: ${type}` + }); + return; + } + + self.postMessage({ + success: true, + type, + result + }); +}; + +// 处理聚类分析 +function processClusterAnalyze(data) { + const { nodes, storeId } = data; + const clusterNodesMap = new Map(); + + // 创建cluster到nodes的映射 + nodes.forEach(node => { + const cluster = parseInt(node.type); + if (!clusterNodesMap.has(cluster)) { + clusterNodesMap.set(cluster, []); + } + clusterNodesMap.get(cluster).push(node); + }); + + let colorMap = { + 0: "75,241,184", // 绿色 + 1: "250,222,37", // 黄色 + 6: "69,192,242" // 蓝色 + }; + + const processedNodes = []; + const groups = []; + + // 处理节点颜色和分组 + clusterNodesMap.forEach((nodes, cluster) => { + const color = colorMap[cluster]; + + nodes.forEach(node => { + const processedNode = { ...node }; + processedNode.fillColor = color; + processedNode.color = color; + processedNodes.push(processedNode); + }); + + // 非异常群体模块需要添加分组 + if (storeId !== "anomalousGroup") { + groups.push({ + nodes: nodes.map(n => n.id), + options: { + shape: "polygon", + color: color, + alpha: 0.2 + } + }); + } + }); + + return { nodes: processedNodes, groups }; +} + +// 处理节点高亮 +function processHighlightNodes(data) { + const { nodes, newNodes } = data; + const newNodeIds = new Set(newNodes.map(node => node.name)); + + return nodes.map(node => ({ + ...node, + selected: newNodeIds.has(node.id) + })); +} + +// 处理连线高亮 +function processHighlightLinks(data) { + const { links, newLinks } = data; + const newLinkSet = new Set(newLinks.map(link => `${link.source}-${link.target}`)); + + return links.map(link => ({ + ...link, + selected: newLinkSet.has(`${link.source.id}-${link.target.id}`) + })); +} + +// 处理异常群体分析 +function processAnomalousGroup(data) { + const { nodes, currentUtc, abnormalData } = data; + const abnormalGroupConfigs = [ + { timeThreshold: "2024-06-19T08:57:55Z", groupKey: "groupA", color: "85, 125, 15" }, // 绿色 + { timeThreshold: "2024-06-19T10:58:03Z", groupKey: "groupB", color: "125, 114, 15" }, // 黄色 + { timeThreshold: "2024-06-19T12:58:04Z", groupKey: "groupC", color: "15, 106, 125" } // 蓝色 + ]; + + // 计算当前应激活的异常组配置 + const activeConfigs = abnormalGroupConfigs.filter(config => currentUtc >= config.timeThreshold); + + // 优化异常节点检测 - 预处理异常ID集合 + const abnormalMap = new Map(); + activeConfigs.forEach(config => { + const abnormalIds = abnormalData[config.groupKey]; + if (abnormalIds) { + abnormalMap.set(config.groupKey, new Set(abnormalIds)); + } + }); + + // 单次遍历处理所有异常节点 + return nodes.map(node => { + const processedNode = { ...node }; + + // 检查节点是否属于任何激活的异常组 + for (const config of activeConfigs) { + const abnormalIdsSet = abnormalMap.get(config.groupKey); + // 如果节点在异常组中,则设置对应的颜色 + if (abnormalIdsSet && abnormalIdsSet.has(node.id)) { + processedNode.fillColor = config.color; + break; // 一旦找到匹配的异常组,就不再检查其他组 + } + } + + return processedNode; + }); +} \ No newline at end of file diff --git a/src/views/GroupEvolution/components/groupGraph.vue b/src/views/GroupEvolution/components/groupGraph.vue index e1ec8ee..50ca1f1 100644 --- a/src/views/GroupEvolution/components/groupGraph.vue +++ b/src/views/GroupEvolution/components/groupGraph.vue @@ -41,6 +41,74 @@ import group2Leg from "@/assets/images/groupEvolution/legends-group2.png" import group3Leg from "@/assets/images/groupEvolution/legends-group3.png" import abnormalLeg from "@/assets/images/groupEvolution/legends-abnormal-group.png" +// 初始化Web Worker +let graphWorker = null +let workerCallbacks = new Map() +let workerTaskId = 0 + +const initWorker = () => { + if (!graphWorker) { + // 创建Web Worker实例 + graphWorker = new Worker(new URL("./graphWorker.js", import.meta.url)) + + // 设置Worker消息处理 + graphWorker.onmessage = (e) => { + const { success, type, result, error, taskId } = e.data + + // 查找对应的回调函数 + const callback = workerCallbacks.get(taskId) + if (callback) { + if (success) { + callback(null, result) + } else { + callback(new Error(`Worker error: ${error}`)) + } + workerCallbacks.delete(taskId) + } + } + + // 设置Worker错误处理 + graphWorker.onerror = (error) => { + console.error("Graph Worker error:", error) + } + } +} + +// 向Worker发送任务 +const postWorkerTask = (type, data) => { + return new Promise((resolve, reject) => { + if (!graphWorker) { + initWorker() + + // 如果Worker初始化失败,使用同步处理 + if (!graphWorker) { + reject(new Error("Web Worker not available")) + return + } + } + + const taskId = ++workerTaskId + workerCallbacks.set(taskId, (error, result) => { + if (error) { + reject(error) + } else { + resolve(result) + } + }) + graphWorker.postMessage({ type, data, taskId }) + }) +} + +// 终止Worker +const terminateWorker = () => { + if (graphWorker) { + graphWorker.terminate() + graphWorker = null + workerCallbacks.clear() + workerTaskId = 0 + } +} + const props = defineProps({ store: { required: true @@ -194,34 +262,57 @@ const clearEvents = () => { if (typeof graphVis.unregistEventListener === "function") { graphVis.unregistEventListener(type, event, handler) } else { - // 如果没有专门的移除方法,尝试其他方式(根据GraphVis实际API调整) - console.warn("GraphVis instance does not have unregistEventListener method") } }) eventListeners = [] } } -//公用连线或节点高亮函数 -const highLightAboutNodesOrLinks = (type) => { +// 公用连线或节点高亮函数(使用Web Worker) +const highLightAboutNodesOrLinks = async (type) => { graphVis.cancelAllSelected() const { newNodes, newLinks } = graph.value - if (type == "nodes") { - const newNodeIds = new Set(newNodes.map((node) => node.name)) - graphVis.nodes.forEach((node) => { - if (newNodeIds.has(node.id)) { - node.selected = true - } - }) - } else if (type == "links") { - const newLinkSet = new Set(newLinks.map((link) => `${link.source}-${link.target}`)) - graphVis.links.forEach((link) => { - if (newLinkSet.has(`${link.source.id}-${link.target.id}`)) { - link.selected = true - } - }) - } else { - return + + try { + let processedItems + + if (type == "nodes") { + // 使用Web Worker处理节点高亮 + processedItems = await postWorkerTask("HIGHLIGHT_NODES", { + nodes: graphVis.nodes, + newNodes + }) + graphVis.nodes = processedItems + } else if (type == "links") { + // 使用Web Worker处理连线高亮 + processedItems = await postWorkerTask("HIGHLIGHT_LINKS", { + links: graphVis.links, + newLinks + }) + graphVis.links = processedItems + } else { + return + } + + // 触发重绘 + graphVis.render() + } catch (error) { + // 降级处理:使用同步方法 + if (type == "nodes") { + const newNodeIds = new Set(newNodes.map((node) => node.name)) + graphVis.nodes.forEach((node) => { + if (newNodeIds.has(node.id)) { + node.selected = true + } + }) + } else if (type == "links") { + const newLinkSet = new Set(newLinks.map((link) => `${link.source}-${link.target}`)) + graphVis.links.forEach((link) => { + if (newLinkSet.has(`${link.source.id}-${link.target.id}`)) { + link.selected = true + } + }) + } } } const runDiffForceLayout = (layoutConfig, layoutType, isAsync) => { @@ -239,40 +330,55 @@ const runDiffForceLayout = (layoutConfig, layoutType, isAsync) => { highLightAboutNodesOrLinks("nodes") } - const handleAnomalousGroup = () => { - // 当前时间从 store 取 - const now = props.store.currentUtc - const abnormalData = props.store.graphAbnormalData - const abnormalGroupConfigs = [ - { timeThreshold: "2024-06-19T08:57:55Z", groupKey: "groupA", color: "85, 125, 15" }, // 绿色 - { timeThreshold: "2024-06-19T10:58:03Z", groupKey: "groupB", color: "125, 114, 15" }, // 黄色 - { timeThreshold: "2024-06-19T12:58:04Z", groupKey: "groupC", color: "15, 106, 125" } // 蓝色 - ] - // 计算当前应激活的异常组配置 - const activeConfigs = abnormalGroupConfigs.filter((config) => now >= config.timeThreshold) + const handleAnomalousGroup = async () => { + try { + // 使用Web Worker处理异常群体分析 + const processedNodes = await postWorkerTask("ANOMALOUS_GROUP_PROCESS", { + nodes: graphVis.nodes, + currentUtc: props.store.currentUtc, + abnormalData: props.store.graphAbnormalData + }) - // 优化异常节点检测 - 预处理异常ID集合 - const abnormalMap = new Map() - activeConfigs.forEach((config) => { - const abnormalIds = abnormalData[config.groupKey] - if (abnormalIds) { - abnormalMap.set(config.groupKey, new Set(abnormalIds)) - } - }) + graphVis.nodes = processedNodes + graphVis.render() + } catch (error) { + console.error("Error processing anomalous group:", error) - // 单次遍历处理所有异常节点 - graphVis.nodes = graphVis.nodes.map((node) => { - // 检查节点是否属于任何激活的异常组 - for (const config of activeConfigs) { - const abnormalIdsSet = abnormalMap.get(config.groupKey) - // 如果节点在异常组中,则设置对应的颜色 - if (abnormalIdsSet && abnormalIdsSet.has(node.id)) { - node.fillColor = config.color - break // 一旦找到匹配的异常组,就不再检查其他组 + // 降级处理:使用同步方法 + // 当前时间从 store 取 + const now = props.store.currentUtc + const abnormalData = props.store.graphAbnormalData + const abnormalGroupConfigs = [ + { timeThreshold: "2024-06-19T08:57:55Z", groupKey: "groupA", color: "85, 125, 15" }, // 绿色 + { timeThreshold: "2024-06-19T10:58:03Z", groupKey: "groupB", color: "125, 114, 15" }, // 黄色 + { timeThreshold: "2024-06-19T12:58:04Z", groupKey: "groupC", color: "15, 106, 125" } // 蓝色 + ] + // 计算当前应激活的异常组配置 + const activeConfigs = abnormalGroupConfigs.filter((config) => now >= config.timeThreshold) + + // 优化异常节点检测 - 预处理异常ID集合 + const abnormalMap = new Map() + activeConfigs.forEach((config) => { + const abnormalIds = abnormalData[config.groupKey] + if (abnormalIds) { + abnormalMap.set(config.groupKey, new Set(abnormalIds)) } - } - return node - }) + }) + + // 单次遍历处理所有异常节点 + graphVis.nodes = graphVis.nodes.map((node) => { + // 检查节点是否属于任何激活的异常组 + for (const config of activeConfigs) { + const abnormalIdsSet = abnormalMap.get(config.groupKey) + // 如果节点在异常组中,则设置对应的颜色 + if (abnormalIdsSet && abnormalIdsSet.has(node.id)) { + node.fillColor = config.color + break // 一旦找到匹配的异常组,就不再检查其他组 + } + } + return node + }) + } } new Map([ @@ -286,38 +392,60 @@ const runDiffForceLayout = (layoutConfig, layoutType, isAsync) => { } graphVis.excuteLocalLayout(layoutType, layoutConfig, isAsync, handleLayoutSuccess()) } -// 根据节点的cluster属性进行分组 -const clusterAnalyze = () => { - graphVis.removeAllGroup() // 清除原有分组 - // 创建cluster到nodes的映射 - const clusterNodesMap = new Map() - graphVis.nodes.forEach((node) => { - const cluster = parseInt(node.type) - if (!clusterNodesMap.has(cluster)) { - clusterNodesMap.set(cluster, []) - } - clusterNodesMap.get(cluster).push(node) - }) - let colorMap = { - 0: "75,241,184", // 绿色 - 1: "250,222,37", // 黄色 - 6: "69,192,242" // 蓝色 - } - clusterNodesMap.forEach((nodes, cluster) => { - const color = colorMap[cluster] - nodes.forEach((node) => { - node.fillColor = color - node.color = color +// 根据节点的cluster属性进行分组(使用Web Worker) +const clusterAnalyze = async () => { + try { + // 使用Web Worker处理聚类分析 + const { nodes: processedNodes, groups } = await postWorkerTask("CLUSTER_ANALYZE", { + nodes: graphVis.nodes, + storeId }) - if (storeId !== "anomalousGroup") { - graphVis.addNodesInGroup(nodes, { - shape: "polygon", //circle|rect|polygon|bubbleset - color: color, - alpha: 0.2 + + graphVis.removeAllGroup() // 清除原有分组 + graphVis.nodes = processedNodes + // 添加分组 + if (storeId !== "anomalousGroup" && groups && groups.length) { + groups.forEach((group) => { + const groupNodes = processedNodes.filter((node) => group.nodes.includes(node.id)) + if (groupNodes.length) { + graphVis.addNodesInGroup(groupNodes, group.options) + } }) } - }) - // graphVis.autoGroupLayout(graphVis.nodes) + + graphVis.render() + } catch (error) { + // 降级处理:使用同步方法 + graphVis.removeAllGroup() // 清除原有分组 + // 创建cluster到nodes的映射 + const clusterNodesMap = new Map() + graphVis.nodes.forEach((node) => { + const cluster = parseInt(node.type) + if (!clusterNodesMap.has(cluster)) { + clusterNodesMap.set(cluster, []) + } + clusterNodesMap.get(cluster).push(node) + }) + let colorMap = { + 0: "75,241,184", // 绿色 + 1: "250,222,37", // 黄色 + 6: "69,192,242" // 蓝色 + } + clusterNodesMap.forEach((nodes, cluster) => { + const color = colorMap[cluster] + nodes.forEach((node) => { + node.fillColor = color + node.color = color + }) + if (storeId !== "anomalousGroup") { + graphVis.addNodesInGroup(nodes, { + shape: "polygon", //circle|rect|polygon|bubbleset + color: color, + alpha: 0.2 + }) + } + }) + } } // 仅对“异常群体模块”生效:时间变化时强制重绘一次 @@ -370,7 +498,7 @@ onMounted(() => { }) // 添加更新图表的函数(优化版本:使用requestAnimationFrame和节流) -const updateChart = (newGraphData) => { +const updateChart = async (newGraphData) => { if (!graphVis) { initChart() return @@ -379,8 +507,12 @@ const updateChart = (newGraphData) => { try { graphVis.clearAll() graphVis.addGraph({ ...toRaw(newGraphData) }) + + // 初始化Web Worker + initWorker() + // 重新运行力导向布局 - clusterAnalyze() + await clusterAnalyze() runDiffForceLayout({ strength: -300, ajustCluster: true }, "simulation", true) } catch (error) { console.error("Error updating chart:", error) @@ -445,6 +577,9 @@ onUnmounted(() => { forceSimulator = null } + // 终止Web Worker + terminateWorker() + // 清除引用,帮助垃圾回收 currentSelectNode.value = null isPlay.value = false diff --git a/src/views/KeyNodeDiscern/opinionLeader/components/GraphPanel.vue b/src/views/KeyNodeDiscern/opinionLeader/components/GraphPanel.vue index fc40c12..77d8aa9 100644 --- a/src/views/KeyNodeDiscern/opinionLeader/components/GraphPanel.vue +++ b/src/views/KeyNodeDiscern/opinionLeader/components/GraphPanel.vue @@ -101,10 +101,9 @@ const startAutomaticPlay = () => { isAutoPlaying.value = true - // 每300ms移动2% animationTimer = setInterval(() => { // 计算新位置 - const newPosition = clickedPosition.value + 2 + const newPosition = clickedPosition.value + 0.1 // 检查是否到达终点 if (newPosition >= 100) { @@ -118,7 +117,7 @@ const startAutomaticPlay = () => { // 根据当前位置更新activeTimePoint updateActiveTimePointByPosition(newPosition) - }, 1000) + }, 100) } // 根据位置更新activeTimePoint