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 dayjs from "dayjs"; // 连线动画的间隔时间(毫秒) const LINE_ANIMATION_INTERVAL = 500; interface LinesItemType { name: string; country_code: string; value: number[]; color?: string; } type LinesDataType = [LinesItemType, LinesItemType]; type LinesType = [string, LinesDataType[]]; // 创建自定义提示框组件 const CustomTooltip = ({ logs, onClose, tooltipRef, }: { logs: string[]; onClose: () => void; tooltipRef: React.RefObject; }) => { const [visibleLogs, setVisibleLogs] = useState([]); const [isComplete, setIsComplete] = useState(false); // 使用useEffect实现逐条显示日志的效果 useEffect(() => { console.log("logs-------", logs.length); if (!logs || logs.length === 0) return; // 重置状态 setVisibleLogs([]); setIsComplete(false); let currentIndex = 0; // 创建一个定时器,每500毫秒显示一条新日志 const timer = setInterval(() => { if (currentIndex < logs.length) { const curLog = `[${dayjs().format("YYYY-MM-DD HH:mm:ss")}] ${logs[currentIndex]}`; setVisibleLogs((prev) => [...prev, curLog]); currentIndex++; } else { clearInterval(timer); setIsComplete(true); } }, 1000); // 清理函数 return () => { clearInterval(timer); }; }, [logs]); // 当logs变化时重新开始动画 // 自动滚动到最新的日志 const logsContainerRef = useRef(null); useEffect(() => { if (logsContainerRef.current && visibleLogs.length > 0) { logsContainerRef.current.scrollTop = logsContainerRef.current.scrollHeight; } }, [visibleLogs]); 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( ({ nestedEncryption, passAuthentication, dynamicRouteGeneration, tooltipClosed, setTooltipClosed, logs, }: { logs: any[]; nestedEncryption: any; passAuthentication: any; dynamicRouteGeneration: any; tooltipType: string; tooltipClosed: boolean; setTooltipClosed: (value: boolean) => void; }) => { // 嵌套加密ref const customTooltipRef = useRef(null); // 流量混淆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 = () => { if (!customTooltipRef.current || !proxyGeoRef.current) return; // 找到US点 const coords = geoCoordMap[nestedEncryption?.[0]?.code ?? "GL"]; if (!coords) return; try { // 将地理坐标转换为屏幕坐标 const screenCoord = proxyGeoRef.current.convertToPixel("geo", coords); if ( screenCoord && Array.isArray(screenCoord) && screenCoord.length === 2 ) { // 设置提示框位置 customTooltipRef.current.style.left = `${screenCoord[0] + 232 + 7}px`; customTooltipRef.current.style.top = `${screenCoord[1] + 40 - 190}px`; } } catch (error) { console.error("Error positioning tooltip:", error); } }; // 处理关闭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]); return (
{tooltipClosed && ( )}
); } ); export default WorldGeo;