diff --git a/src-tauri/src/core/api.rs b/src-tauri/src/core/api.rs index 6632b04..60951ac 100644 --- a/src-tauri/src/core/api.rs +++ b/src-tauri/src/core/api.rs @@ -73,7 +73,7 @@ pub async fn get_nodes_update(config: CoreConfig) -> Result> if response.status().is_success() { let result: GetNodesResponse = response.json().await?; - info!("Successfully got nodes: {:?}", result.data); + debug!("Successfully got nodes: {:?}", result.data); Ok(result.data) } else { debug!("Failed to get nodes: {}", response.text().await?); diff --git a/src/pages/anti-forensics-forwarding/components/world-geo copy 3.tsx b/src/pages/anti-forensics-forwarding/components/world-geo copy 3.tsx new file mode 100644 index 0000000..3b3b8ee --- /dev/null +++ b/src/pages/anti-forensics-forwarding/components/world-geo copy 3.tsx @@ -0,0 +1,1595 @@ +import { useEffect, useRef, memo, useState } from "react"; +import * as echarts from "echarts"; +import type { EChartsType } from "echarts"; +import worldGeoJson from "@/assets/echarts-map/json/world.json"; +import { geoCoordMap, countryNameMap, countryCodeMap } from "@/data"; +import { getUrl } from "@/lib/utils"; +import React from "react"; + +// 连线动画的间隔时间(毫秒) +const LINE_ANIMATION_INTERVAL = 500; + +interface LinesItemType { + name: string; + country_code: string; + value: number[]; + color?: string; +} + +type LinesDataType = [LinesItemType, LinesItemType]; +type LinesType = [string, LinesDataType[]]; + +// 创建自定义提示框组件 +export interface CustomTooltipProps { + code: string; + logs: string[]; + onClose: () => void; +} + +export interface CounterHandle { + $dom: HTMLDivElement | null; + code: string; +} + +export const CustomTooltip = forwardRef( + ({ code, logs, onClose }, ref) => { + // 内部真正挂载在
上的 ref + const innerRef = useRef(null); + + // 将外部的 ref 指向 innerRef.current + useImperativeHandle(ref, () => ({ + $dom: innerRef.current, + code: code, + })); + + const [visibleLogs, setVisibleLogs] = useState([]); + const [isComplete, setIsComplete] = useState(false); + + // 定时渐显日志 + useEffect(() => { + if (!logs.length) { + setVisibleLogs([]); + setIsComplete(true); + return; + } + let idx = 0; + setVisibleLogs([]); + setIsComplete(false); + const timer = setInterval(() => { + if (idx < logs.length) { + setVisibleLogs((prev) => [...prev, logs[idx]]); + idx++; + } else { + clearInterval(timer); + setIsComplete(true); + } + }, 500); + return () => clearInterval(timer); + }, [logs]); + + // 自动滚动到底部 + const logsContainerRef = useRef(null); + useEffect(() => { + const el = logsContainerRef.current; + if (el && visibleLogs.length) { + el.scrollTop = el.scrollHeight; + } + }, [visibleLogs]); + + // 完成时批量操作示例 + useEffect(() => { + if (isComplete) { + // 这里举例给每条日志加个动画 class + const items = innerRef.current?.querySelectorAll("li"); + // items?.forEach((li) => li.classList.add("animate-pulse")); + } + }, [isComplete]); + + return ( +
+
+ +
+
+
+
+ 嵌套加密 +
+ +
+
+ {visibleLogs.length > 0 ? ( +
    + {visibleLogs.map((log, index) => ( +
  • + {log} +
  • + ))} +
+ ) : ( +
+ {logs.length > 0 ? "日志加载中..." : "暂无日志记录"} +
+ )} +
+
+
+
+ ); + } +); +// 创建单个国家的涟漪效果 +const createCountryRipple = (countryCode: string, color?: string) => { + const coords = geoCoordMap[countryCode]; + if (!coords) return null; + return { + name: countryCodeMap[countryCode] ?? "", + value: coords, + country_code: countryCode, + color: color || "#0ea5e9", + }; +}; + +export const WorldGeo = memo( + ({ + nestedEncryptionLogs, + nestedEncryption, + passAuthentication, + dynamicRouteGeneration, + tooltipClosed, + setTooltipClosed, + logs, + }: { + logs: any[]; + nestedEncryptionLogs: any[]; + nestedEncryption: any; + passAuthentication: any; + dynamicRouteGeneration: any; + tooltipType: string; + tooltipClosed: boolean; + setTooltipClosed: (value: boolean) => void; + }) => { + // 嵌套加密ref + const customTooltipRef = useRef(null); + const nestedEncryptionRefs = useRef[]>([]); + // 流量混淆ref + const proxyGeoRef = useRef(null); + const preMainToData = useRef<{ country_code: string }[]>([]); + const lineMidpointsRef = useRef< + { + id: string; + midpoint: number[]; + fromCountry: string; + toCountry: string; + }[] + >([]); + const labelContainerRef = useRef(null); + const labelsRef = useRef([]); + + // 添加状态来跟踪当前显示的连线索引 + const [nestedEncryptionLineIndex, setNestedEncryptionLineIndex] = + useState(-1); + const [dynamicRouteLineIndex, setDynamicRouteLineIndex] = useState(-1); + + // 添加状态来存储所有连线数据 + const [nestedEncryptionLines, setNestedEncryptionLines] = useState< + { from: string; to: string; color?: string }[] + >([]); + const [dynamicRouteLines, setDynamicRouteLines] = useState< + { from: string; to: string; color?: string }[] + >([]); + + // 添加状态来存储所有点 + const [allPoints, setAllPoints] = useState([]); + + // 使用ref来跟踪动画状态,避免重新渲染 + const animationTimerRef = useRef(null); + const dynamicAnimationTimerRef = useRef(null); + + // 添加状态来跟踪数据是否已经变化 + const nestedEncryptionKeyRef = useRef(""); + const dynamicRouteKeyRef = useRef(""); + + // 添加一个ref来跟踪图表是否已初始化 + const chartInitializedRef = useRef(false); + + // 初始化时提取所有点的函数 + const extractAllPoints = () => { + const points: any[] = []; + + // 从嵌套加密数据中提取点 + if (nestedEncryption && Array.isArray(nestedEncryption)) { + nestedEncryption.forEach((item: any) => { + if (item.data && Array.isArray(item.data)) { + item.data.forEach((dataItem: any) => { + // 添加起点到点集合 + const fromCode = dataItem.country_code.toUpperCase(); + const fromPoint = createCountryRipple(fromCode, item.color); + if ( + fromPoint && + !points.some((p) => p.country_code === fromCode) + ) { + points.push(fromPoint); + } + // 如果有终点,也添加到点集合 + if (dataItem.ingress_country_code) { + const toCode = dataItem.ingress_country_code.toUpperCase(); + const toPoint = createCountryRipple(toCode, item.color); + if (toPoint && !points.some((p) => p.country_code === toCode)) { + points.push(toPoint); + } + } + }); + } + }); + } + + // 从动态路由数据中提取点 + if (dynamicRouteGeneration && Array.isArray(dynamicRouteGeneration)) { + dynamicRouteGeneration.forEach((item: any) => { + if (item.data && Array.isArray(item.data)) { + item.data.forEach((dataItem: any) => { + // 添加起点到点集合 + const fromCode = dataItem.country_code.toUpperCase(); + const fromPoint = createCountryRipple(fromCode, item.color); + if ( + fromPoint && + !points.some((p) => p.country_code === fromCode) + ) { + points.push(fromPoint); + } + // 如果有终点,也添加到点集合 + if (dataItem.ingress_country_code) { + const toCode = dataItem.ingress_country_code.toUpperCase(); + const toPoint = createCountryRipple(toCode, item.color); + if (toPoint && !points.some((p) => p.country_code === toCode)) { + points.push(toPoint); + } + } + }); + } + }); + } + + return points; + }; + + // 修改初始化逻辑,确保在数据变化时立即提取点 + useEffect(() => { + // 提取所有点 + const points = extractAllPoints(); + if (points.length > 0) { + setAllPoints(points); + } + }, [nestedEncryption, dynamicRouteGeneration]); // 监听数据变化 + + // 启动嵌套加密连线动画的函数 + const startNestedEncryptionAnimation = ( + connections: { from: string; to: string; color?: string }[] + ) => { + if (connections.length === 0) return; + let index = 0; + + // 递归函数,用于按顺序显示连线 + const animateNextLine = () => { + setNestedEncryptionLineIndex(index); + index++; + if (index < connections.length) { + animationTimerRef.current = setTimeout( + animateNextLine, + LINE_ANIMATION_INTERVAL + ); + } + }; + + // 开始动画 + animateNextLine(); + }; + + // 处理嵌套加密数据变化 + useEffect(() => { + // 清除任何现有的动画定时器 + if (animationTimerRef.current) { + clearTimeout(animationTimerRef.current); + animationTimerRef.current = null; + } + + const allExtractedPoints: any[] = []; + + // 处理嵌套加密数据 + if (nestedEncryption && Array.isArray(nestedEncryption)) { + const points: any[] = []; + const connections: { from: string; to: string; color?: string }[] = []; + let shouldStartAnimation = false; + + nestedEncryption.forEach((item: any) => { + if (item.data && Array.isArray(item.data)) { + item.data.forEach((dataItem: any) => { + // 添加起点到点集合 + const fromCode = dataItem.country_code.toUpperCase(); + const fromPoint = createCountryRipple(fromCode, item.color); + if ( + fromPoint && + !points.some((p) => p.country_code === fromCode) + ) { + points.push(fromPoint); + if ( + !allExtractedPoints.some((p) => p.country_code === fromCode) + ) { + allExtractedPoints.push(fromPoint); + } + } + + // 如果有终点,也添加到点集合 + if (dataItem.ingress_country_code) { + const toCode = dataItem.ingress_country_code.toUpperCase(); + const toPoint = createCountryRipple(toCode, item.color); + if (toPoint && !points.some((p) => p.country_code === toCode)) { + points.push(toPoint); + if ( + !allExtractedPoints.some((p) => p.country_code === toCode) + ) { + allExtractedPoints.push(toPoint); + } + } + + // 检查是否需要开始连线动画 + if (item.isLine === true) { + connections.push({ + from: fromCode, + to: toCode, + color: item.color, + }); + shouldStartAnimation = true; + } + } + }); + } + }); + + // 生成当前数据的唯一键 + const currentKey = JSON.stringify(nestedEncryption); + + // 检查数据是否变化 + if ( + currentKey !== nestedEncryptionKeyRef.current || + shouldStartAnimation + ) { + nestedEncryptionKeyRef.current = currentKey; + setNestedEncryptionLines(connections); + + // 如果有连线数据且需要开始动画,重置索引并启动动画 + if (connections.length > 0 && shouldStartAnimation) { + setNestedEncryptionLineIndex(-1); // 重置索引 + // 启动连线动画 + setTimeout(() => { + startNestedEncryptionAnimation(connections); + }, 500); + } else if (!shouldStartAnimation) { + // 如果不需要连线,设置索引为-1 + setNestedEncryptionLineIndex(-1); + } + } + } + + // 更新所有点 + if (allExtractedPoints.length > 0) { + setAllPoints((prevPoints) => { + const newPoints = [...prevPoints]; + allExtractedPoints.forEach((point) => { + if (!newPoints.some((p) => p.country_code === point.country_code)) { + newPoints.push(point); + } else { + // 更新已存在点的颜色 + const existingIndex = newPoints.findIndex( + (p) => p.country_code === point.country_code + ); + if (existingIndex !== -1 && point.color) { + newPoints[existingIndex].color = point.color; + } + } + }); + return newPoints; + }); + } + }, [nestedEncryption]); + + // 启动动态路由连线动画的函数 + const startDynamicRouteAnimation = ( + connections: { from: string; to: string; color?: string }[] + ) => { + if (connections.length === 0) return; + let index = 0; + + // 递归函数,用于按顺序显示连线 + const animateNextLine = () => { + setDynamicRouteLineIndex(index); + index++; + if (index < connections.length) { + dynamicAnimationTimerRef.current = setTimeout( + animateNextLine, + LINE_ANIMATION_INTERVAL + ); + } + }; + + // 开始动画 + animateNextLine(); + }; + + // 处理动态路由数据变化 + useEffect(() => { + // 清除任何现有的动画定时器 + if (dynamicAnimationTimerRef.current) { + clearTimeout(dynamicAnimationTimerRef.current); + dynamicAnimationTimerRef.current = null; + } + + const allExtractedPoints: any[] = []; + + // 处理动态路由数据 + if (dynamicRouteGeneration && Array.isArray(dynamicRouteGeneration)) { + const points: any[] = []; + const connections: { from: string; to: string; color?: string }[] = []; + let shouldStartAnimation = false; + + dynamicRouteGeneration.forEach((item: any) => { + if (item.data && Array.isArray(item.data)) { + item.data.forEach((dataItem: any) => { + // 添加起点到点集合 + const fromCode = dataItem.country_code.toUpperCase(); + const fromPoint = createCountryRipple(fromCode, item.color); + if ( + fromPoint && + !points.some((p) => p.country_code === fromCode) + ) { + points.push(fromPoint); + if ( + !allExtractedPoints.some((p) => p.country_code === fromCode) + ) { + allExtractedPoints.push(fromPoint); + } + } + + // 如果有终点,也添加到点集合 + if (dataItem.ingress_country_code) { + const toCode = dataItem.ingress_country_code.toUpperCase(); + const toPoint = createCountryRipple(toCode, item.color); + if (toPoint && !points.some((p) => p.country_code === toCode)) { + points.push(toPoint); + if ( + !allExtractedPoints.some((p) => p.country_code === toCode) + ) { + allExtractedPoints.push(toPoint); + } + } + + // 检查是否需要开始连线动画 + if (item.isLine === true) { + connections.push({ + from: fromCode, + to: toCode, + color: item.color, + }); + shouldStartAnimation = true; + } + } + }); + } + }); + + // 生成当前数据的唯一键 + const currentKey = JSON.stringify(dynamicRouteGeneration); + + // 检查数据是否变化 + if (currentKey !== dynamicRouteKeyRef.current || shouldStartAnimation) { + dynamicRouteKeyRef.current = currentKey; + setDynamicRouteLines(connections); + + // 如果有连线数据且需要开始动画,重置索引并启动动画 + if (connections.length > 0 && shouldStartAnimation) { + setDynamicRouteLineIndex(-1); // 重置索引 + // 启动连线动画 + setTimeout(() => { + startDynamicRouteAnimation(connections); + }, 500); + } else if (!shouldStartAnimation) { + // 如果不需要连线,设置索引为-1 + setDynamicRouteLineIndex(-1); + } + } + } + + // 更新所有点 + if (allExtractedPoints.length > 0) { + setAllPoints((prevPoints) => { + const newPoints = [...prevPoints]; + allExtractedPoints.forEach((point) => { + if (!newPoints.some((p) => p.country_code === point.country_code)) { + newPoints.push(point); + } else { + // 更新已存在点的颜色 + const existingIndex = newPoints.findIndex( + (p) => p.country_code === point.country_code + ); + if (existingIndex !== -1 && point.color) { + newPoints[existingIndex].color = point.color; + } + } + }); + return newPoints; + }); + } + }, [dynamicRouteGeneration]); + + // 组件卸载时清除定时器 + useEffect(() => { + return () => { + if (animationTimerRef.current) { + clearTimeout(animationTimerRef.current); + animationTimerRef.current = null; + } + if (dynamicAnimationTimerRef.current) { + clearTimeout(dynamicAnimationTimerRef.current); + dynamicAnimationTimerRef.current = null; + } + }; + }, []); + + const getLineItem = ( + preCode: string, + nextCode: string, + color?: string + ): [LinesItemType, LinesItemType] => { + return [ + { + name: countryCodeMap[preCode] ?? "", + value: geoCoordMap[preCode] ?? [], + country_code: preCode, + color: color, + }, + { + name: countryCodeMap[nextCode] ?? "", + value: geoCoordMap[nextCode] ?? [], + country_code: nextCode, + color: color, + }, + ]; + }; + + const getLine = () => { + // 实现数据处理 + const solidData: LinesType[] = []; // 不再使用单一数组,而是分开存储 + + // 处理嵌套加密连线 - 放入单独的数组 + if (nestedEncryptionLineIndex >= 0 && nestedEncryptionLines.length > 0) { + const nestedLines: LinesDataType[] = []; + for ( + let i = 0; + i <= nestedEncryptionLineIndex && i < nestedEncryptionLines.length; + i++ + ) { + const connection = nestedEncryptionLines[i]; + nestedLines.push( + getLineItem(connection.from, connection.to, connection.color) + ); + } + if (nestedLines.length > 0) { + solidData.push(["nested", nestedLines]); + } + } + + // 处理动态路由连线 - 放入单独的数组 + if (dynamicRouteLineIndex >= 0 && dynamicRouteLines.length > 0) { + const dynamicLines: LinesDataType[] = []; + for ( + let i = 0; + i <= dynamicRouteLineIndex && i < dynamicRouteLines.length; + i++ + ) { + const connection = dynamicRouteLines[i]; + dynamicLines.push( + getLineItem(connection.from, connection.to, connection.color) + ); + } + if (dynamicLines.length > 0) { + solidData.push(["dynamic", dynamicLines]); + } + } + + // 虚线数据处理(保持原有逻辑) + const otherLineList: any = []; + + return { + solidData, + otherLineList, + ripplePoints: allPoints, // 使用 allPoints 确保点始终显示 + }; + }; + + // 定位自定义提示框 - 优化版本 + const positionCustomTooltip = useCallback(() => { + if (!nestedEncryptionRefs.current.length || !proxyGeoRef.current) return; + console.log(nestedEncryptionRefs.current, "nestedEncryptionRefs"); + nestedEncryptionRefs.current.forEach((ref) => { + const item = ref.current; + console.log(item,'item') + if (!item || !item.$dom) return; // Ensure the ref is not null + // 找到US点 + const coords = geoCoordMap[item.code.toLocaleUpperCase() ?? "GL"]; + if (!coords) return; + try { + // 将地理坐标转换为屏幕坐标 + const screenCoord = proxyGeoRef.current!.convertToPixel( + "geo", + coords + ); + if ( + screenCoord && + Array.isArray(screenCoord) && + screenCoord.length === 2 + ) { + // 设置提示框位置 + item!.$dom.style.left = `${screenCoord[0] + 232 + 7}px`; + item!.$dom.style.top = `${screenCoord[1] + 40 - 190}px`; + } + } catch (error) { + console.error("Error positioning tooltip:", error); + } + }); + // // 找到US点 + // const coords = geoCoordMap[nestedEncryption?.[0]?.code ?? "GL"]; + // if (!coords) return; + }, [ + nestedEncryptionRefs.current, + proxyGeoRef.current, + nestedEncryptionLogs, + ]); + // 处理关闭tooltip + const handleCloseTooltip = () => { + setTooltipClosed(false); + }; + + // 获取连线经纬度数据 + const convertData = (data: LinesDataType[]) => { + const res = []; + const midpoints = []; + + for (let index = 0; index < data.length; index++) { + const dataIndex = data[index]; + const fromCoord = geoCoordMap[dataIndex?.[0]?.country_code ?? ""]; + const toCoord = geoCoordMap[dataIndex?.[1]?.country_code ?? ""]; + const fromCountry = dataIndex?.[0]?.country_code ?? ""; + const toCountry = dataIndex?.[1]?.country_code ?? ""; + + if (fromCoord && toCoord) { + res.push({ + coords: [fromCoord, toCoord], + // 添加颜色属性 + lineStyle: { + color: dataIndex?.[0]?.color || "#0ea5e9", + }, + }); + + // 计算中点,考虑曲线的弧度 + const curveness = -0.4; // 与飞线弧度相同 + const x1 = fromCoord[0]; + const y1 = fromCoord[1]; + const x2 = toCoord[0]; + const y2 = toCoord[1]; + + // 计算控制点 + const cpx = (x1 + x2) / 2 - (y2 - y1) * curveness; + const cpy = (y1 + y2) / 2 - (x1 - x2) * curveness; + + // 计算曲线上的中点 (t=0.5 时的贝塞尔曲线点) + const midX = x1 * 0.25 + cpx * 0.5 + x2 * 0.25; + const midY = y1 * 0.25 + cpy * 0.5 + y2 * 0.25; + + midpoints.push({ + id: `line-label-${index}`, + midpoint: [midX, midY], + fromCountry, + toCountry, + }); + } + } + + return res; + }; + + // 创建双层点效果 - 大点 + const createDualLayerPoint = ( + lastExit: LinesItemType, + isMainPath: boolean = true, + color?: string + ) => { + // 创建数据数组,用于两个散点图层 + const pointData = lastExit + ? [lastExit].map((v) => { + return { + name: v.name, + value: v.value, + datas: { + country_code: v.country_code, + color: v.color, // 添加颜色属性 + }, + }; + }) + : []; + + // 根据是否是主路径设置不同的大小和颜色 + const outerSize = isMainPath ? 8 : 4; + const innerSize = isMainPath ? 4 : 2; + + // 使用传入的颜色或从数据中获取颜色,如果都没有则使用默认颜色 + const outerColor = color || lastExit?.color || "#0ea5e9"; + const innerColor = "#FFFFFF"; // 白色内层 + + return [ + { + // 外层蓝色点,带涟漪效果 + type: "effectScatter", + coordinateSystem: "geo", + zlevel: 3, + itemStyle: { + color: outerColor, + }, + symbol: "circle", + symbolSize: outerSize, + rippleEffect: { + period: 8, // 动画时间,值越小速度越快 + brushType: "stroke", // 波纹绘制方式 stroke + scale: 6, // 波纹圆环最大限制,值越大波纹越大 + brushWidth: 2, + }, + label: { + show: false, + }, + tooltip: { + show: false, + trigger: "item", + showContent: true, + alwaysShowContent: true, + formatter: (params: any) => { + return ` +
+ +
嵌套加密
+ +
+ `; + }, + backgroundColor: "transparent", + borderWidth: 0, + }, + data: pointData, + } as echarts.SeriesOption, + { + // 内层白色点,不带涟漪效果 + type: "scatter", // 使用普通scatter,不带特效 + coordinateSystem: "geo", + zlevel: 4, // 确保在蓝色点上方 + color: innerColor, + symbol: "circle", + symbolSize: innerSize, + label: { + show: false, + }, + data: pointData, + } as echarts.SeriesOption, + ]; + }; + + // 添加新方法:根据经纬度数组创建蓝色涟漪小点(不包含白色内层点) + const createRipplePointsFromCoordinates = ( + coordinates: [number, number][], + series: echarts.SeriesOption[] + ) => { + if (!coordinates || coordinates.length === 0) return; + + // 使用selectedApp.color或默认蓝色 + const outerColor = "#01FF5E"; + + // 只创建外层带涟漪效果的点 + series.push({ + type: "effectScatter", + coordinateSystem: "geo", + zlevel: 3, + color: outerColor, + symbol: "circle", + symbolSize: 6, + rippleEffect: { + period: 8, // 动画时间 + brushType: "stroke", // 波纹绘制方式 + scale: 6, // 波纹圆环最大限制 + brushWidth: 2, + }, + label: { + show: false, + }, + data: coordinates.map((coord) => ({ + name: "", // 可以根据需要添加名称 + value: coord, + })), + } as echarts.SeriesOption); + }; + + // 创建路径点的双层效果 + const createPathPoints = ( + dataItems: LinesDataType[], + isMainPath: boolean = true, + color?: string + ) => { + // 创建数据数组 + const pointData = dataItems.map((dataItem: LinesDataType) => { + return { + name: dataItem[0].name, + value: geoCoordMap[dataItem[0].country_code], + datas: { + country_code: dataItem[0].country_code, + color: dataItem[0].color, // 添加颜色属性 + }, + }; + }); + + // 根据是否是主路径设置不同的大小和颜色 + const outerSize = isMainPath ? 8 : 4; + const innerSize = isMainPath ? 4 : 2; + + // 使用传入的颜色或从数据中获取颜色,如果都没有则使用默认颜色 + const outerColor = color || dataItems[0]?.[0]?.color || "#0ea5e9"; + const innerColor = "#FFFFFF"; // 白色内层 + + return [ + { + // 外层蓝色点,带涟漪效果 + type: "effectScatter", + coordinateSystem: "geo", + zlevel: 3, + itemStyle: { + color: outerColor, + }, + symbol: "circle", + symbolSize: outerSize, + rippleEffect: { + period: 8, // 动画时间,值越小速度越快 + brushType: "stroke", // 波纹绘制方式 stroke + scale: 6, // 波纹圆环最大限制,值越大波纹越大 + brushWidth: 2, + }, + label: { + show: false, + }, + tooltip: { + show: false, + trigger: "item", + formatter: (params: any) => { + return ` +
+ +
嵌套加密
+ +
+ `; + }, + backgroundColor: "transparent", + borderWidth: 0, + }, + data: pointData, + } as echarts.SeriesOption, + { + // 内层白色点,不带涟漪效果 + type: "scatter", // 使用普通scatter,不带特效 + coordinateSystem: "geo", + zlevel: 4, // 确保在蓝色点上方 + color: innerColor, + symbol: "circle", + symbolSize: innerSize, + label: { + show: false, + }, + data: pointData, + } as echarts.SeriesOption, + ]; + }; + + // 创建带自定义提示框的涟漪点 + const createRipplePointsWithTooltip = (ripplePoints: any) => { + return { + type: "effectScatter", + coordinateSystem: "geo", + zlevel: 3, + // 使用函数动态设置每个点的颜色 + itemStyle: { + color: (params: any) => { + return params.data.datas?.color || "#0ea5e9"; // 使用点的颜色或默认颜色 + }, + }, + symbol: "circle", + symbolSize: 8, + rippleEffect: { + period: 8, + brushType: "stroke", + scale: 6, + brushWidth: 2, + }, + label: { + show: false, + formatter: (params: any) => { + return `{${params.data.datas.country_code}|}`; + }, + }, + // 添加提示框配置 + tooltip: { + show: false, + trigger: "item", + formatter: (params: any) => { + return ` +
+ +
嵌套加密
+ +
+ `; + }, + backgroundColor: "transparent", + borderWidth: 0, + }, + data: ripplePoints.map((point: any) => ({ + name: point.name, + value: point.value, + datas: { + country_code: point.country_code, + color: point.color, // 添加颜色属性 + }, + })), + } as echarts.SeriesOption; + }; + + // 连线 series + const getLianData = (series: echarts.SeriesOption[]) => { + const { solidData, otherLineList, ripplePoints } = getLine(); + + // 如果有需要显示涟漪效果的点,添加它们 + if (ripplePoints.length > 0) { + // 添加带自定义提示框的外层蓝色点 + series.push(createRipplePointsWithTooltip(ripplePoints)); + + // 添加内层白色点,不带涟漪效果 + series.push({ + type: "scatter", // 使用普通scatter,不带特效 + coordinateSystem: "geo", + zlevel: 4, // 确保在蓝色点上方 + color: "#FFFFFF", // 白色内层 + symbol: "circle", + symbolSize: 4, + label: { + show: false, + }, + data: ripplePoints.map((point) => ({ + name: point.name, + value: point.value, + datas: { + country_code: point.country_code, + color: point.color, // 添加颜色属性 + }, + })), + } as echarts.SeriesOption); + } + + // 处理每个连线组 + solidData.forEach((item) => { + // 如果没有连线数据,则跳过 + if (item[1].length === 0) { + return; + } + + // 为每条连线创建飞行线 + const pathColor = item[0] === "nested" ? "#0ea5e9" : "#F0FFA2"; // 根据类型设置默认颜色 + + // 添加飞行线 + series.push({ + name: item[0], + type: "lines", + zlevel: 1, + label: { + show: false, // 不使用内置标签 + }, + // 飞行线特效 + effect: { + show: true, // 是否显示 + period: 4, // 特效动画时间 + trailLength: 0.7, // 特效尾迹长度。取从 0 到 1 的值,数值越大尾迹越长 + color: pathColor, // 特效颜色 + symbolSize: [10, 20], + }, + // 线条样式 + lineStyle: { + curveness: -0.4, // 飞线弧度 + type: "solid", // 飞线类型 + color: pathColor, // 使用从数据中获取的颜色 + width: 1.5, // 飞线宽度 + opacity: 0.1, + }, + data: convertData(item[1]) as echarts.LinesSeriesOption["data"], + }); + + // 添加路径点的双层效果 + const pathPoints = createPathPoints(item[1], true, pathColor); + series.push(...pathPoints); + + // 添加出口节点的双层效果 + item[1].forEach((lineData) => { + const lastExit = lineData[1]; + if (lastExit) { + const exitNodes = createDualLayerPoint( + lastExit, + true, + lastExit.color || pathColor + ); + series.push(...exitNodes); + } + }); + }); + + // 处理其他线(保持原有逻辑) + otherLineList.forEach((line: any) => { + line.forEach((item: any) => { + const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null; + // 获取当前路径的颜色 + const pathColor = item[1]?.[0]?.[0]?.color || "#F0FFA2"; // 从第一个点获取颜色,如果没有则使用默认颜色 + + // 添加虚线 + series.push({ + name: item[0], + type: "lines", + zlevel: 1, + label: { + show: false, + }, + // 线条样式 + lineStyle: { + curveness: -0.4, // 飞线弧度 + type: [5, 5], // 飞线类型 + color: pathColor, // 使用从数据中获取的颜色 + width: 0.5, // 飞线宽度 + opacity: 0.6, + }, + data: convertData(item[1]) as echarts.LinesSeriesOption["data"], + }); + + // 添加路径点的双层效果(次要路径) + const pathPoints = createPathPoints(item[1], false, pathColor); + series.push(...pathPoints); + + // 添加出口节点的双层效果(次要路径) + if (lastExit) { + const exitNodes = createDualLayerPoint(lastExit, false, pathColor); + series.push(...exitNodes); + } + }); + }); + + return true; + }; + + // 创建A点和B点,并添加飞线和标签 + const createSpecialPoints = (series: echarts.SeriesOption[]) => { + // 定义点A和点B的坐标 + const pointA = geoCoordMap[passAuthentication[0]?.startPoint ?? "GL"]; + const pointB = geoCoordMap[passAuthentication[0]?.endPoint ?? "CA"]; + const newPointB = [pointB[0] + 14, pointB[1] + 10]; + + // 添加A点 - 带涟漪效果的双层点 + series.push( + // 外层带涟漪效果的点 + { + type: "effectScatter", + coordinateSystem: "geo", + zlevel: 3, + color: "#FF6B01", // 橙色外层 + symbol: "circle", + symbolSize: 8, + rippleEffect: { + period: 8, // 动画时间 + brushType: "stroke", // 波纹绘制方式 + scale: 6, // 波纹圆环最大限制 + brushWidth: 2, + }, + label: { + show: true, + position: [10, -50], + formatter: () => { + return "{name1|待认证节点}"; + }, + rich: { + name1: { + color: "#FF6B01", + align: "center", + lineHeight: 35, + fontSize: 18, + fontWeight: 600, + padding: [11, 16.52, 11, 16.52], + backgroundColor: "rgba(63, 6, 3, 0.5)", + }, + }, + backgroundColor: "transparent", + }, + data: [ + { + name: "格陵兰", + value: pointA, + }, + ], + } as echarts.SeriesOption, + // 内层白色点 + { + type: "scatter", // 普通scatter,不带特效 + coordinateSystem: "geo", + zlevel: 4, // 确保在外层点上方 + color: "#FFFFFF", // 白色内层 + symbol: "circle", + symbolSize: 4, + label: { + show: false, + }, + data: [ + { + name: "格陵兰", + value: pointA, + }, + ], + } as echarts.SeriesOption + ); + + // 添加B点 - 大型圆形区域 + series.push({ + type: "scatter", + coordinateSystem: "geo", + zlevel: 2, + color: "rgba(55, 255, 0, 0.50)", // 半透明绿色 + symbol: "circle", + symbolSize: 150, // 大尺寸圆形 + label: { + show: true, + position: [-70, -30], + formatter: () => { + return "{name2|权威节点团}"; + }, + rich: { + name2: { + color: "#37FF00", + align: "center", + lineHeight: 35, + fontSize: 18, + fontWeight: 600, + padding: [11, 16.52, 11, 16.52], + backgroundColor: "rgba(4, 59, 27, 0.5)", + }, + }, + backgroundColor: "transparent", + }, + data: [ + { + name: "加拿大", + value: pointB, + }, + ], + } as echarts.SeriesOption); + + // 添加A到B的飞线(无特效) + series.push({ + type: "lines", + zlevel: 1, + effect: { + show: false, // 关闭特效 + }, + lineStyle: { + curveness: -0.4, // 飞线弧度 + type: "solid", + color: "#FEAA18", // 飞线颜色 + width: 1.5, + opacity: 0.8, + }, + data: [ + { + coords: [pointA, newPointB], // 从A点到B点 + }, + ], + } as echarts.SeriesOption); + + // 计算飞线中点坐标(考虑曲率) + const x1 = pointA[0]; + const y1 = pointA[1]; + const x2 = newPointB[0]; + const y2 = newPointB[1]; + const curveness = -0.4; + + // 计算控制点 + const cpx = (x1 + x2) / 2 - (y2 - y1) * curveness; + const cpy = (y1 + y2) / 2 - (x1 - x2) * curveness; + + // 计算曲线上的中点 (t=0.5 时的贝塞尔曲线点) + const midX = x1 * 0.25 + cpx * 0.5 + x2 * 0.25; + const midY = y1 * 0.25 + cpy * 0.5 + y2 * 0.25; + + // 将中点添加到 lineMidpointsRef 中,以便使用 DOM 方式创建标签 + lineMidpointsRef.current.push({ + id: "special-line-label", + midpoint: [midX, midY], + fromCountry: "A", + toCountry: "B", + }); + + return series; + }; + + const getOption = () => { + const series: echarts.SeriesOption[] = []; + getLianData(series); + + if ( + passAuthentication.length && + passAuthentication[0]?.authenticationPoint + ) { + createSpecialPoints(series); // 添加特殊点和飞线 + createRipplePointsFromCoordinates( + passAuthentication[0]?.authenticationPoint || [], + series + ); + } + + const option = { + backgroundColor: "transparent", + // 全局提示框配置 + tooltip: { + show: true, + trigger: "item", + enterable: true, + confine: true, // 保持提示框在图表范围内 + appendToBody: true, // 将提示框附加到body以获得更好的定位 + position: function (pos: any) { + // 自定义定位逻辑(如果需要) + return [pos[0] + 10, pos[1] - 50]; // 从光标偏移 + }, + }, + // 底图样式 + geo: { + map: "world", // 地图类型 + roam: true, // 是否开启缩放 + zoom: 1, // 初始缩放大小 + layoutCenter: ["50%", "50%"], //地图位置 + scaleLimit: { + // 缩放等级 + min: 1, + max: 3, + }, + label: { + show: false, + }, + nameMap: countryNameMap, // 自定义地区的名称映射 + // 三维地理坐标系样式 + itemStyle: { + areaColor: "#020617", // 修改为要求的填充颜色 + borderColor: "#cbd5e1", // 修改为要求的边框颜色 + borderWidth: 1, // 边框宽度 + borderType: "dashed", // 修改为点线边框 + }, + emphasis: { + itemStyle: { + areaColor: "#172554", // 修改为鼠标悬停时的填充颜色 + borderColor: "#0ea5e9", // 修改为鼠标悬停时的边框颜色 + borderWidth: 1.2, // 修改为鼠标悬停时的边框宽度 + borderType: "solid", // 修改为实线边框 + }, + label: false, + }, + tooltip: { + show: true, + trigger: "item", + triggerOn: "click", // 提示框触发的条件 + enterable: true, // 鼠标是否可进入提示框浮层中,默认为false,如需详情内交互,如添加链接,按钮,可设置为 true + backgroundColor: "rgba(0,0,0,0.8)", + borderColor: "rgba(0,0,0,0.2)", + textStyle: { + color: "#fff", + }, + formatter: (parameters: { + name: string; + data: + | { + name: string; + datas: { tradingCountry: string }; + } + | undefined; + }) => { + if (parameters.data?.name) return parameters.data.name; + return parameters.name; + }, + }, + }, + series: series, + }; + + return option; + }; + + // 创建DOM标签 + const createDOMLabels = () => { + // 清除现有标签 + if (labelContainerRef.current) { + labelContainerRef.current.innerHTML = ""; + labelsRef.current?.forEach((item) => item?.remove()); + labelsRef.current = []; + } else { + // 创建标签容器 + const container = document.createElement("div"); + container.className = "line-labels-container"; + container.style.position = "absolute"; + container.style.top = "0"; + container.style.left = "0"; + container.style.pointerEvents = "none"; + container.style.zIndex = "1000"; + container.style.width = "100%"; + container.style.height = "100%"; + container.style.overflow = "hidden"; + + // 添加到地图容器 + const chartDom = document.getElementById("screenGeo"); + if (chartDom) { + chartDom.style.position = "relative"; + chartDom.appendChild(container); + labelContainerRef.current = container; + } + } + + // 创建新标签 + lineMidpointsRef.current.forEach((point, index) => { + const label = document.createElement("div"); + label.id = point.id; + label.className = "line-label"; + label.style.position = "absolute"; + label.style.textAlign = "center"; + label.style.transform = "translate(-50%, -50%)"; + label.style.whiteSpace = "nowrap"; + label.style.pointerEvents = "none"; + label.style.zIndex = "1001"; + + // 特殊线标签(A到B的线) + if (point.id === "special-line-label") { + label.style.backgroundColor = "#8B3700"; + label.style.color = "#FFB27A"; + label.style.padding = "5px 10px"; + label.style.borderRadius = "4px"; + label.style.fontSize = "18px"; + label.style.fontWeight = "normal"; + label.textContent = "SS签名"; + } + // 其他线标签 + else { + label.style.backgroundColor = "#8B3700"; + label.style.color = "#FFB27A"; + label.style.padding = "5px 10px"; + label.style.borderRadius = "4px"; + label.style.fontSize = "18px"; + label.style.fontWeight = "normal"; + label.textContent = "SS签名"; + } + + // 添加到容器 + labelContainerRef.current?.appendChild(label); + labelsRef.current.push(label); + }); + + // 更新标签位置 + updateLabelPositions(); + }; + + // 更新标签位置 + const updateLabelPositions = () => { + if (!proxyGeoRef.current || !labelContainerRef.current) return; + + lineMidpointsRef.current.forEach((point, index) => { + const label = labelsRef.current[index]; + if (!label) return; + + const pixelPoint = proxyGeoRef.current?.convertToPixel( + "geo", + point.midpoint + ); + + if (pixelPoint && Array.isArray(pixelPoint)) { + label.style.left = `${pixelPoint[0]}px`; + label.style.top = `${pixelPoint[1]}px`; + } + }); + }; + + const handleResize = () => { + proxyGeoRef.current?.resize(); + updateLabelPositions(); + if (tooltipClosed) { + positionCustomTooltip(); + } + }; + + // 更新图表 - 关键修改:使用 silent 参数避免重新渲染动画 + useEffect(() => { + if (!proxyGeoRef.current || !chartInitializedRef.current) return; + + // 获取当前系列 + const series: echarts.SeriesOption[] = []; + getLianData(series); + + // 使用 setOption 更新图表,但保持动画状态 + proxyGeoRef.current.setOption( + { series: series }, + { + notMerge: false, // 不合并会导致闪烁 + silent: true, // 静默更新,不触发动画重新开始 + lazyUpdate: true, // 延迟更新 + } + ); + }, [nestedEncryptionLineIndex, dynamicRouteLineIndex]); + + // 处理数据变化 - 关键修改:使用 silent 参数避免重新渲染动画 + useEffect(() => { + if (!proxyGeoRef.current || !chartInitializedRef.current) return; + + lineMidpointsRef.current = []; // 重置中点数据 + + // 获取当前系列 + const series: echarts.SeriesOption[] = []; + getLianData(series); + + if ( + passAuthentication.length && + passAuthentication[0]?.authenticationPoint + ) { + createSpecialPoints(series); + createRipplePointsFromCoordinates( + passAuthentication[0]?.authenticationPoint || [], + series + ); + } + + // 使用 setOption 更新图表,但保持动画状态 + proxyGeoRef.current.setOption( + { series: series }, + { + notMerge: false, // 不合并会导致闪烁 + silent: true, // 静默更新,不触发动画重新开始 + lazyUpdate: true, // 延迟更新 + } + ); + + // 创建DOM标签 + setTimeout(createDOMLabels, 100); + }, [nestedEncryption, dynamicRouteGeneration, passAuthentication]); + + // 初始化图表 + useEffect(() => { + const chartDom = document.getElementById("screenGeo"); + proxyGeoRef.current = echarts.init(chartDom); + + echarts.registerMap( + "world", + worldGeoJson as unknown as Parameters[1] + ); + + // 初始化时提取所有点 + const initialPoints = extractAllPoints(); + if (initialPoints.length > 0) { + setAllPoints(initialPoints); + } + + const option = getOption(); + option && proxyGeoRef.current?.setOption(option); + + // 标记图表已初始化 + chartInitializedRef.current = true; + + // 添加地图交互事件监听器 + proxyGeoRef.current?.on("georoam", updateLabelPositions); + + // 页面resize时触发 + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + proxyGeoRef.current?.off("georoam", updateLabelPositions); + + // 清理DOM标签 + if (labelContainerRef.current) { + labelContainerRef.current.remove(); + labelContainerRef.current = null; + labelsRef.current = []; + } + + proxyGeoRef.current?.dispose(); + proxyGeoRef.current = null; + }; + }, []); + + // 在地图初始化后定位tooltip + useEffect(() => { + if (tooltipClosed) { + positionCustomTooltip(); + } + }, [ + tooltipClosed, + nestedEncryption, + nestedEncryptionRefs, + nestedEncryptionLogs, + ]); + useEffect(() => { + // 根据 nestedEncryptionLogs 的长度创建 ref 数组 + nestedEncryptionRefs.current = nestedEncryptionLogs.map(() => + React.createRef() + ); + }, [nestedEncryptionLogs.length]); // 只在 logs 数量变化时重新创建 + + return ( +
+
+ {tooltipClosed && nestedEncryptionLogs.map((item, index) => { + // 确保 refs 已经初始化 + if (nestedEncryptionRefs.current.length <= index) return null; + + return ( + + ); + })} +
+ ); + } +); + +export default WorldGeo;