2025-05-07 18:14:05 +08:00

1503 lines
48 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<HTMLDivElement>;
title: string;
imageSrc: string;
}) => {
const [visibleLogs, setVisibleLogs] = useState<string[]>([]);
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<HTMLDivElement>(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 (
<div
id="custom-fixed-tooltip"
ref={tooltipRef}
style={{
position: "fixed",
zIndex: 1000,
pointerEvents: "auto",
backgroundColor: "transparent",
}}
>
<div className="tooltip-content">
<img
className="line-img-hx"
src={getUrl("svg/anti-forensics-forwarding/Line.svg")}
alt=""
/>
<div className="fill"></div>
<div className="tip-box-hx">
<div className="flex justify-between items-center mb-2">
<div
className="label"
style={{ color: "white", fontWeight: "bold" }}
>
{title}
</div>
<img
className="close-icon"
src={getUrl("svg/Xwhite.svg")}
alt=""
style={{ cursor: "pointer" }}
onClick={onClose}
/>
</div>
{filteredLogs.length > 0 ? (
<div
ref={logsContainerRef}
className="logs-container mt-3 max-h-[335px] overflow-y-auto"
>
{visibleLogs.length > 0 ? (
<ul className="logs-list space-y-1.5">
{visibleLogs.map(
(log, index) =>
log &&
log.trim() !== "" && (
<li
key={index}
className="log-item text-sm text-white py-1 px-2 bg-black/20 rounded animate-fadeIn"
>
{log}
</li>
)
)}
</ul>
) : (
<div className="text-sm text-gray-400 italic">
{logs.length > 0 ? "日志加载中..." : "暂无日志记录"}
</div>
)}
{/* {!isComplete && filteredLogs.length > 0 && (
<div className="loading-indicator mt-2 text-xs text-blue-300">
处理中...
</div>
)} */}
</div>
) : (
<div className="text-sm text-gray-400 italic">
{logs.length > 0 ? "日志加载中..." : "暂无日志记录"}
</div>
)}
</div>
</div>
</div>
);
};
// 创建左侧自定义提示框组件
const CustomTooltipLeft = ({
logs = [],
onClose,
tooltipRef,
title,
imageSrc,
}: {
logs?: string[];
onClose: () => void;
tooltipRef: React.RefObject<HTMLDivElement>;
title: string;
imageSrc: string;
}) => {
const [visibleLogs, setVisibleLogs] = useState<string[]>([]);
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<HTMLDivElement>(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 (
<div
id="custom-fixed-tooltip2"
ref={tooltipRef}
style={{
position: "fixed",
zIndex: 1000,
pointerEvents: "auto",
backgroundColor: "transparent",
}}
>
<div className="tooltip-content">
<div className="fill-left"></div>
<div className="tip-box-left">
<div className="flex justify-between items-center mb-2">
<div
className="label"
style={{ color: "white", fontWeight: "bold" }}
>
{title}
</div>
<img
className="close-icon"
src={getUrl("svg/Xwhite.svg")}
alt=""
style={{ cursor: "pointer" }}
onClick={onClose}
/>
</div>
{filteredLogs.length > 0 ? (
<div
ref={logsContainerRef}
className="logs-container mt-3 max-h-[335px] overflow-y-auto"
>
{visibleLogs.length > 0 ? (
<ul className="logs-list space-y-1.5">
{visibleLogs.map(
(log, index) =>
log &&
log.trim() !== "" && (
<li
key={index}
className="log-item text-sm text-white py-1 px-2 bg-black/20 rounded animate-fadeIn"
>
{log}
</li>
)
)}
</ul>
) : (
<div className="text-sm text-gray-400 italic">
{logs.length > 0 ? "日志加载中..." : "暂无日志记录"}
</div>
)}
{/* {!isComplete && filteredLogs.length > 0 && (
<div className="loading-indicator mt-2 text-xs text-blue-300">
处理中...
</div>
)} */}
</div>
) : (
<div className="text-sm text-gray-400 italic">
{logs.length > 0 ? "日志加载中..." : "暂无日志记录"}
</div>
)}
</div>
<img
className="line-img-left"
src={getUrl("svg/anti-forensics-forwarding/LineLeft.svg")}
alt=""
/>
</div>
</div>
);
};
// 创建单个国家的涟漪效果
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<HTMLDivElement | null>(null);
// 流量混淆ref
const customTooltip2Ref = useRef<HTMLDivElement | null>(null);
const proxyGeoRef = useRef<EChartsType | null>(null);
const preMainToData = useRef<{ country_code: string }[]>([]);
const lineMidpointsRef = useRef<
{
id: string;
midpoint: number[];
fromCountry: string;
toCountry: string;
}[]
>([]);
const labelContainerRef = useRef<HTMLDivElement | null>(null);
const labelsRef = useRef<HTMLDivElement[]>([]);
// 添加状态来控制是否显示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<string, string> = {};
// 处理主路径数据
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 `
<div class="tip-box">
<img class="close-icon" src="${getUrl(
"svg/Xwhite.svg"
)}" alt="" />
<div class="label">嵌套加密</div>
<img class="encryption-img" width="100%" src="${getUrl(
"image/nested-encryption.png"
)}" alt="" />
</div>
`;
},
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 `
<div class="tip-box">
<img class="close-icon" src="${getUrl(
"svg/Xwhite.svg"
)}" alt="" />
<div class="label">嵌套加密</div>
<img class="encryption-img" width="100%" src="${getUrl(
"image/nested-encryption.png"
)}" alt="" />
</div>
`;
},
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 `
<div class="tip-box">
<img class="close-icon" src="${getUrl(
"svg/Xwhite.svg"
)}" alt="" />
<div class="label">嵌套加密</div>
<img class="encryption-img" width="100%" src="${getUrl(
"image/nested-encryption.png"
)}" alt="" />
</div>
`;
},
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<typeof echarts.registerMap>[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 (
<div className="flex-1 h-full flex flex-col">
<div id="screenGeo" className="flex-1"></div>
{/* 嵌套加密提示框 */}
{showTooltip1 && (
<CustomTooltip
logs={nestedEncryptionLogs}
onClose={handleCloseTooltip1}
tooltipRef={customTooltipRef}
title={CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.title}
imageSrc={getUrl("image/nested-encryption.png")}
/>
)}
{/* 流量混淆提示框 - 确保条件正确 */}
{showTooltip2 && (
<CustomTooltipLeft
logs={trafficObfuscationLogs}
onClose={handleCloseTooltip2}
tooltipRef={customTooltip2Ref}
title="流量混淆"
imageSrc={getUrl("image/traffic-obfuscation.png")}
/>
)}
</div>
);
}
);
// 添加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;
// }