import { useEffect, useMemo, useRef, memo, useState } from "react"; import * as echarts from "echarts"; // import 'echarts-gl'; // import { useQueryClient } from "@tanstack/react-query"; 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 { CONST_TOOLTIP_TYPE } from "@/pages/anti-forensics-forwarding"; interface LinesItemType { name: string; country_code: string; value: number[]; color?: string; // 添加颜色属性 } type LinesDataType = [LinesItemType, LinesItemType]; type LinesType = [string, LinesDataType[]]; // 创建右侧自定义提示框组件 const CustomTooltip = ({ logs = [], onClose, tooltipRef, title, }: { logs?: string[]; onClose: () => void; tooltipRef: React.RefObject; title: string; imageSrc: string; }) => { const [visibleLogs, setVisibleLogs] = useState([]); const [isComplete, setIsComplete] = useState(false); // 过滤掉空日志 const filteredLogs = useMemo(() => { return logs.filter((log) => log && log.trim() !== ""); }, [logs]); // 使用useEffect实现逐条显示日志的效果 useEffect(() => { if (!filteredLogs || filteredLogs.length === 0) return; // 重置状态 setVisibleLogs([]); setIsComplete(false); // 先显示第一条日志 setVisibleLogs([filteredLogs[0]]); // 如果只有一条日志,直接设置完成 if (filteredLogs.length === 1) { setIsComplete(true); return; } // 从第二条日志开始,每500毫秒显示一条 let currentIndex = 1; const timer = setInterval(() => { if (currentIndex < filteredLogs.length) { setVisibleLogs((prev) => [...prev, filteredLogs[currentIndex]]); currentIndex++; // 如果已经是最后一条,设置完成状态 if (currentIndex >= filteredLogs.length) { clearInterval(timer); setIsComplete(true); } } else { clearInterval(timer); setIsComplete(true); } }, 500); // 清理函数 return () => { clearInterval(timer); }; }, [filteredLogs]); // 当过滤后的日志变化时重新开始动画 // 自动滚动到最新的日志 const logsContainerRef = useRef(null); useEffect(() => { if (logsContainerRef.current && visibleLogs.length > 0) { logsContainerRef.current.scrollTop = logsContainerRef.current.scrollHeight; } }, [visibleLogs]); // 添加调试日志 useEffect(() => { console.log("CustomTooltip rendered", { title, logs: filteredLogs.length }); }, []); return (
{title}
{filteredLogs.length > 0 ? (
{visibleLogs.length > 0 ? (
    {visibleLogs.map( (log, index) => log && log.trim() !== "" && (
  • {log}
  • ) )}
) : (
{logs.length > 0 ? "日志加载中..." : "暂无日志记录"}
)} {/* {!isComplete && filteredLogs.length > 0 && (
处理中...
)} */}
) : (
{logs.length > 0 ? "日志加载中..." : "暂无日志记录"}
)}
); }; // 创建左侧自定义提示框组件 const CustomTooltipLeft = ({ logs = [], onClose, tooltipRef, title, imageSrc, }: { logs?: string[]; onClose: () => void; tooltipRef: React.RefObject; title: string; imageSrc: string; }) => { const [visibleLogs, setVisibleLogs] = useState([]); const [isComplete, setIsComplete] = useState(false); // 过滤掉空日志 const filteredLogs = useMemo(() => { return logs.filter((log) => log && log.trim() !== ""); }, [logs]); // 使用useEffect实现逐条显示日志的效果 useEffect(() => { if (!filteredLogs || filteredLogs.length === 0) return; // 重置状态 setVisibleLogs([]); setIsComplete(false); // 先显示第一条日志 setVisibleLogs([filteredLogs[0]]); // 如果只有一条日志,直接设置完成 if (filteredLogs.length === 1) { setIsComplete(true); return; } // 从第二条日志开始,每500毫秒显示一条 let currentIndex = 1; const timer = setInterval(() => { if (currentIndex < filteredLogs.length) { setVisibleLogs((prev) => [...prev, filteredLogs[currentIndex]]); currentIndex++; // 如果已经是最后一条,设置完成状态 if (currentIndex >= filteredLogs.length) { clearInterval(timer); setIsComplete(true); } } else { clearInterval(timer); setIsComplete(true); } }, 500); // 清理函数 return () => { clearInterval(timer); }; }, [filteredLogs]); // 当过滤后的日志变化时重新开始动画 // 自动滚动到最新的日志 const logsContainerRef = useRef(null); useEffect(() => { if (logsContainerRef.current && visibleLogs.length > 0) { logsContainerRef.current.scrollTop = logsContainerRef.current.scrollHeight; } }, [visibleLogs]); // 添加调试日志 useEffect(() => { console.log("CustomTooltipLeft rendered", { title, logs: filteredLogs.length, }); }, []); return (
{title}
{filteredLogs.length > 0 ? (
{visibleLogs.length > 0 ? (
    {visibleLogs.map( (log, index) => log && log.trim() !== "" && (
  • {log}
  • ) )}
) : (
{logs.length > 0 ? "日志加载中..." : "暂无日志记录"}
)} {/* {!isComplete && filteredLogs.length > 0 && (
处理中...
)} */}
) : (
{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, // 添加颜色属性 }; }; export const WorldGeo = memo( ({ dataInfo, selectedApp, tooltipType, tooltipClosed, nestedEncryptionLogs, trafficObfuscationLogs, setTooltipClosed, }: { dataInfo: any; selectedApp: any; tooltipType: string; tooltipClosed: boolean; nestedEncryptionLogs: string[]; trafficObfuscationLogs: string[]; setTooltipClosed: (value: boolean) => void; }) => { // const queryClient = useQueryClient() // 嵌套加密ref const customTooltipRef = useRef(null); // 流量混淆ref const customTooltip2Ref = useRef(null); 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([]); // 添加状态来控制是否显示tooltip const [showTooltip1, setShowTooltip1] = useState(true); const [showTooltip2, setShowTooltip2] = useState(true); // 添加调试日志 useEffect(() => { console.log("Tooltip state:", { tooltipClosed, tooltipType, showTooltip1, showTooltip2, }); }, [tooltipClosed, tooltipType, showTooltip1, showTooltip2]); const mainToData = useMemo(() => { const newList = [ dataInfo.passAuthentication, ...dataInfo.trafficObfuscation, ...dataInfo.nestedEncryption, ...dataInfo.dynamicRouteGeneration, ]; // 使用新的数据结构 const proxiesList = selectedApp && selectedApp ? [...newList, selectedApp] : newList ?? []; // 初始化数据数组 - 不再包含 startCountry const data: any = []; console.log(proxiesList, "proxiesList"); // 遍历代理列表 proxiesList.forEach((proxyItem: any) => { // 检查是否有数据数组 if (proxyItem.data && Array.isArray(proxyItem.data)) { // 遍历数据数组中的每个项目 proxyItem.data.forEach((item: any) => { // 如果有 ingress_country_code,则添加一对起点和终点 if (item.ingress_country_code) { // 添加起点(country_code) data.push({ country_code: item.country_code, type: "start", isLine: proxyItem.isLine, // 保存连线标志 color: proxyItem.color, // 保存颜色信息 }); // 添加终点(ingress_country_code) data.push({ country_code: item.ingress_country_code, type: "end", isLine: proxyItem.isLine, // 保存连线标志 color: proxyItem.color, // 保存颜色信息 }); } else { // 如果没有 ingress_country_code,只添加 country_code data.push({ country_code: item.country_code, isLine: proxyItem.isLine, // 保存连线标志 color: proxyItem.color, // 保存颜色信息 }); } }); } }); return data; }, [dataInfo, selectedApp]); // 定位自定义提示框 - 优化版本 const positionCustomTooltip = () => { console.log("Positioning tooltip1", { hasRef: !!customTooltipRef.current, hasChart: !!proxyGeoRef.current, }); if (!customTooltipRef.current || !proxyGeoRef.current) return; // 找到点 const coords = geoCoordMap[dataInfo.nestedEncryption?.[0]?.code ?? "GL"]; console.log("Tooltip1 coords:", coords); if (!coords) return; try { // 将地理坐标转换为屏幕坐标 const screenCoord = proxyGeoRef.current.convertToPixel("geo", coords); console.log("Tooltip1 screen coords:", screenCoord); if ( screenCoord && Array.isArray(screenCoord) && screenCoord.length === 2 ) { // 设置提示框位置 const left = `${screenCoord[0] + 232 + 7}px`; const top = `${screenCoord[1] + 40 + 15}px`; console.log("Setting tooltip1 position:", { left, top }); customTooltipRef.current.style.left = left; customTooltipRef.current.style.top = top; } } catch (error) { console.error("Error positioning tooltip1:", error); } }; // 定位自定义提示框2 - 优化版本 const positionCustomTooltip2 = () => { console.log("Positioning tooltip2", { hasRef: !!customTooltip2Ref.current, hasChart: !!proxyGeoRef.current, }); if (!customTooltip2Ref.current || !proxyGeoRef.current) return; // 找到点 const coords = geoCoordMap[dataInfo.trafficObfuscation?.[0]?.code ?? "ZA"]; console.log("Tooltip2 coords:", coords); if (!coords) return; try { // 将地理坐标转换为屏幕坐标 const screenCoord = proxyGeoRef.current.convertToPixel("geo", coords); console.log("Tooltip2 screen coords:", screenCoord); if ( screenCoord && Array.isArray(screenCoord) && screenCoord.length === 2 ) { // 设置提示框位置 const left = `${screenCoord[0] - 626 + 48}px`; const top = `${screenCoord[1] + 40 - 20}px`; console.log("Setting tooltip2 position:", { left, top }); customTooltip2Ref.current.style.left = left; customTooltip2Ref.current.style.top = top; } } catch (error) { console.error("Error positioning tooltip2:", error); } }; // 处理关闭tooltip const handleCloseTooltip1 = () => { setShowTooltip1(false); setTooltipClosed(false); }; // 处理关闭tooltip2 const handleCloseTooltip2 = () => { setShowTooltip2(false); setTooltipClosed(false); }; 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[] = [["main", []]]; // 使用"main"替代startCountry.country_code // 收集需要显示涟漪效果的所有点(包括连线和不连线的) const ripplePoints: any[] = []; // 收集每个点的颜色信息 const pointColors: Record = {}; // 处理主路径数据 for (let i = 0; i < mainToData.length; i++) { // 如果是最后一个元素,则跳过(因为没有下一个元素作为终点) if (i === mainToData.length - 1) continue; const currentItem = mainToData[i]; const nextItem = mainToData[i + 1]; // 获取当前国家代码 const countryCode = currentItem.country_code.toUpperCase(); // 获取颜色信息 const lineColor = currentItem.color || "#0ea5e9"; // 默认颜色 // 保存点的颜色信息 pointColors[countryCode] = lineColor; // 如果当前项是起点,下一项是终点 if (currentItem.type === "start" && nextItem.type === "end") { const startCode = countryCode; const endCode = nextItem.country_code.toUpperCase(); // 保存终点的颜色信息 pointColors[endCode] = lineColor; // 无论是否连线,都添加点的涟漪效果 const startPoint = createCountryRipple(startCode, lineColor); const endPoint = createCountryRipple(endCode, lineColor); if (startPoint) { ripplePoints.push(startPoint); } if (endPoint) { ripplePoints.push(endPoint); } // 检查是否应该绘制连线 if (currentItem.isLine !== false) { const lineItem = getLineItem(startCode, endCode, lineColor); solidData[0][1].push(lineItem); } // 跳过下一项,因为已经处理了 i++; } // 常规情况:当前项到下一项 else { const nextCountryCode = nextItem.country_code.toUpperCase(); const nextColor = nextItem.color || "#0ea5e9"; // 获取下一个点的颜色 // 保存下一个点的颜色信息 pointColors[nextCountryCode] = nextColor; // 无论是否连线,都添加点的涟漪效果 const currentPoint = createCountryRipple(countryCode, lineColor); const nextPoint = createCountryRipple(nextCountryCode, nextColor); if (currentPoint) { ripplePoints.push(currentPoint); } if (nextPoint) { ripplePoints.push(nextPoint); } // 检查是否应该绘制连线 if (currentItem.isLine !== false) { const lineItem = getLineItem( countryCode, nextCountryCode, lineColor ); solidData[0][1].push(lineItem); } } } // 虚线数据处理(保持原有逻辑) const otherLineList: any = []; return { solidData, otherLineList, ripplePoints, pointColors, }; }; // 获取连线经纬度数据 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 ?? ""; // 使用每条线自己的颜色 const lineColor = dataIndex?.[0]?.color || "#0ea5e9"; if (fromCoord && toCoord) { res.push({ coords: [fromCoord, toCoord], lineStyle: { color: lineColor, // 使用自定义颜色 }, // 保存颜色信息用于飞行特效 color: lineColor, }); // 计算中点,考虑曲线的弧度 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, }); } } // 更新中点引用 // lineMidpointsRef.current = midpoints; return res; }; // 创建双层点效果 - 大点 const createDualLayerPoint = ( lastExit: LinesItemType, isMainPath: boolean = true ) => { // 使用点自己的颜色 const pointColor = lastExit.color || "#0ea5e9"; // 创建数据数组,用于两个散点图层 const pointData = lastExit ? [lastExit].map((v) => { return { name: v.name, value: v.value, datas: { country_code: v.country_code, color: pointColor, // 保存颜色信息 }, }; }) : []; // 根据是否是主路径设置不同的大小和颜色 const outerSize = isMainPath ? 8 : 4; const innerSize = isMainPath ? 4 : 2; const innerColor = "#FFFFFF"; // 白色内层 return [ { // 外层彩色点,带涟漪效果 type: "effectScatter", coordinateSystem: "geo", zlevel: 3, itemStyle: { color: function (params: any) { return params.data.datas.color; }, }, 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) => { // const countryCode = params.data.datas.country_code; // const countryName = params.data.name; // 创建自定义HTML提示框 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[], color: string = "#01FF5E" ) => { if (!coordinates || coordinates.length === 0) return; // 只创建外层带涟漪效果的点 series.push({ type: "effectScatter", coordinateSystem: "geo", zlevel: 3, color: color, 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 ) => { // 创建数据数组 const pointData = dataItems.map((dataItem: LinesDataType) => { // 使用每个点自己的颜色 const pointColor = dataItem[0].color || "#0ea5e9"; return { name: dataItem[0].name, value: geoCoordMap[dataItem[0].country_code], datas: { country_code: dataItem[0].country_code, color: pointColor, // 保存颜色信息 }, }; }); // 根据是否是主路径设置不同的大小和颜色 const outerSize = isMainPath ? 8 : 4; const innerSize = isMainPath ? 4 : 2; const innerColor = "#FFFFFF"; // 白色内层 return [ { // 外层彩色点,带涟漪效果 type: "effectScatter", coordinateSystem: "geo", zlevel: 3, // 使用回调函数根据数据项设置颜色 itemStyle: { color: function (params: any) { return params.data.datas.color; }, }, 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: function (params: any) { return params.data.datas.color; }, }, 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 || "#0ea5e9", // 使用点自己的颜色 }, })), } 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, }, })), } as echarts.SeriesOption); } solidData.forEach((item) => { // 如果没有连线数据,则跳过 if (item[1].length === 0) { return; } // 处理每条线段 item[1].forEach((lineSegment, index) => { const fromPoint = lineSegment[0]; const toPoint = lineSegment[1]; const lineColor = fromPoint.color || "#0ea5e9"; // 添加单条飞行线 series.push({ name: `${item[0]}-${index}`, type: "lines", zlevel: 1, label: { show: false, // 不使用内置标签 }, // 飞行线特效 effect: { show: true, // 是否显示 period: 4, // 特效动画时间 trailLength: 0.7, // 特效尾迹长度。取从 0 到 1 的值,数值越大尾迹越长 color: lineColor, // 使用线段自己的颜色 // symbol: planePathImg, // 特效图形标记 symbolSize: [10, 20], }, // 线条样式 lineStyle: { curveness: -0.4, // 飞线弧度 type: "solid", // 飞线类型 width: 1.5, // 飞线宽度 opacity: 0.1, color: lineColor, // 使用线段自己的颜色 }, data: convertData([ [fromPoint, toPoint], ]) as echarts.LinesSeriesOption["data"], }); // 添加起点的双层效果 const startNodes = createDualLayerPoint(fromPoint, true); series.push(...startNodes); // 如果是最后一个线段,添加终点的双层效果 if (index === item[1].length - 1) { const endNodes = createDualLayerPoint(toPoint, true); series.push(...endNodes); } }); }); otherLineList.forEach((line: any) => { line.forEach((item: any) => { // 处理每条虚线段 item[1].forEach((lineSegment: any, index: number) => { const fromPoint = lineSegment[0]; const toPoint = lineSegment[1]; const lineColor = fromPoint.color || "#F0FFA2"; // 添加虚线 series.push({ name: `${item[0]}-dashed-${index}`, type: "lines", zlevel: 1, label: { show: false, }, // 线条样式 lineStyle: { curveness: -0.4, // 飞线弧度 type: [5, 5], // 飞线类型 color: lineColor, // 使用线段自己的颜色 width: 0.5, // 飞线宽度 opacity: 0.6, }, data: convertData([ [fromPoint, toPoint], ]) as echarts.LinesSeriesOption["data"], }); // 添加起点的双层效果 const startNodes = createDualLayerPoint(fromPoint, false); series.push(...startNodes); // 如果是最后一个线段,添加终点的双层效果 if (index === item[1].length - 1) { const endNodes = createDualLayerPoint(toPoint, false); series.push(...endNodes); } }); }); }); return true; }; // 创建A点和B点,并添加飞线和标签 const createSpecialPoints = (series: echarts.SeriesOption[]) => { // 定义点A和点B的坐标 const pointA = geoCoordMap[dataInfo.passAuthentication.startPoint ?? "GL"]; const pointB = geoCoordMap[dataInfo.passAuthentication.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); // getMianLineTipData(series); // 添加主线tip 暂时隐藏 createSpecialPoints(series); // 添加特殊点和飞线 if (dataInfo.passAuthentication?.authenticationPoint) { // 使用认证点的颜色 const authColor = dataInfo.passAuthentication.color || "#01FF5E"; createRipplePointsFromCoordinates( dataInfo.passAuthentication?.authenticationPoint || [], series, authColor ); } const option = { backgroundColor: "transparent", // 全局提示框配置 tooltip: { show: true, trigger: "item", enterable: true, confine: true, // 保持提示框在图表范围内 appendToBody: true, // 将提示框附加到body以获得更好的定位 // position: function(pos:any, params, dom, rect, size) { position: function (pos: any) { // 自定义定位逻辑(如果需要) return [pos[0] + 10, pos[1] - 50]; // 从光标偏移 }, }, // 底图样式 geo: { map: "world", // 地图类型 roam: true, // 是否开启缩放 zoom: 1, // 初始缩放大小 // center: [11.3316626, 19.5845024], // 地图中心点 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 = []; } 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(); // 重新定位tooltip if (showTooltip1) { positionCustomTooltip(); } if (showTooltip2) { positionCustomTooltip2(); } }; useEffect(() => { preMainToData.current?.some( (item, index) => item.country_code !== mainToData[index]?.country_code ) && proxyGeoRef.current?.clear(); preMainToData.current = mainToData; const option = getOption(); proxyGeoRef.current?.setOption(option); // 创建DOM标签 setTimeout(createDOMLabels, 100); }, [dataInfo, mainToData]); useEffect(() => { const chartDom = document.getElementById("screenGeo"); proxyGeoRef.current = echarts.init(chartDom); echarts.registerMap( "world", worldGeoJson as unknown as Parameters[1] ); const option = getOption(); option && proxyGeoRef.current?.setOption(option); // 添加地图交互事件监听器 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) { setShowTooltip1(true); setShowTooltip2(true); // 确保另一个是关闭的 setTimeout(() => { positionCustomTooltip(); positionCustomTooltip2(); }, 0); } else { // setShowTooltip1(false); // setShowTooltip2(false); } }, [ tooltipClosed, tooltipType, dataInfo.nestedEncryption, dataInfo.trafficObfuscation, ]); // 在地图初始化后定位tooltip useEffect(() => { positionCustomTooltip(); positionCustomTooltip2(); }, [showTooltip1, showTooltip2]); return (
{/* 嵌套加密提示框 */} {showTooltip1 && ( )} {/* 流量混淆提示框 - 确保条件正确 */} {showTooltip2 && ( )}
); } ); // 添加CSS样式 // 可以放在你的全局CSS文件中 // @keyframes fadeIn { // from { opacity: 0; transform: translateY(5px); } // to { opacity: 1; transform: translateY(0); } // } // // .animate-fadeIn { // animation: fadeIn 0.3s ease-out forwards; // }