feat:新增一些日志功能

This commit is contained in:
liyuanhu 2025-04-25 18:39:15 +08:00
parent a9d30c05c8
commit 6357dfb24c
12 changed files with 1271 additions and 2610 deletions

View File

@ -7,6 +7,20 @@ body {
}
.scrollbar-visible {
scrollbar-width: auto;
/* For Firefox */
-ms-overflow-style: scrollbar;
/* For Internet Explorer and Edge */
}
.scrollbar-visible::-webkit-scrollbar {
display: block;
/* For Chrome, Safari, and Opera */
height: 10px;
}
::selection {
// background-color: #18181b;
background-color: #1E3A8A;
@ -17,7 +31,7 @@ body {
::-webkit-scrollbar {
width: 6px;
height: 6px;
/* background-color: red; */
// background-color: red;
}
/* ::-webkit-scrollbar-track {
@ -103,4 +117,4 @@ body {
/* Internet Explorer/Edge (旧版) */
user-select: none;
/* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */
}
}

View File

@ -9,100 +9,102 @@ import TitleSvg from "@/assets/svg/layout/title.svg?react";
import ChevronDownSvg from "@/assets/svg/layout/chevron-down.svg?react";
import Decentralized from "@/assets/svg/layout/decentralized.svg?react";
import PoolSvg from '@/assets/svg/layout/pool.svg?react'
import HomeSvg from '@/assets/svg/layout/home.svg?react'
import AntiDarkAnalysisNetworkSvg from '@/assets/svg/layout/anti-dark-analysis-network.svg?react'
import PoolSvg from "@/assets/svg/layout/pool.svg?react";
import HomeSvg from "@/assets/svg/layout/home.svg?react";
import AntiDarkAnalysisNetworkSvg from "@/assets/svg/layout/anti-dark-analysis-network.svg?react";
import "./index.scss";
import type { RootState } from "@/store";
export default function Layout() {
const [_, setActive] = useState(0);
const { coreVersion } = useSelector(
(state: RootState) => state.serviceReducer
);
const [_, setActive] = useState(0);
const { coreVersion } = useSelector(
(state: RootState) => state.serviceReducer
);
const navList = [
{
id: "new-home",
title: "首页",
icon: <HomeSvg className="w-5 h-5" />,
},
{
id: "home",
title: "去中心化的弹性网络",
icon: <Decentralized className="w-5 h-5" />,
},
{
id: "anti-forensics-forwarding",
title: "面向溯源对抗的数据转发",
icon: <PoolSvg className="w-5 h-5" />,
},
{
id: "anti-dark-analysis-network",
title: "抗暗特征分析的隐匿网络应用",
icon: <AntiDarkAnalysisNetworkSvg className="w-5 h-5" />,
},
// {
// id: 'proxies',
// title: '节点池',
// icon: <PoolSvg className="w-5 h-5" />,
// },
];
const navList = [
{
id: "new-home",
title: "首页",
icon: <HomeSvg className="w-5 h-5" />,
},
{
id: "home",
title: "去中心化的弹性网络",
icon: <Decentralized className="w-5 h-5" />,
},
{
id: "anti-forensics-forwarding",
title: "面向溯源对抗的数据转发",
icon: <PoolSvg className="w-5 h-5" />,
},
{
id: "anti-dark-analysis-network",
title: "抗暗特征分析的隐匿网络应用",
icon: <AntiDarkAnalysisNetworkSvg className="w-5 h-5" />,
},
// {
// id: 'proxies',
// title: '节点池',
// icon: <PoolSvg className="w-5 h-5" />,
// },
];
const handleClickMenu = (index: number) => {
setActive(index);
};
const handleClickMenu = (index: number) => {
setActive(index);
};
return (
<div data-tauri-drag-region className="layout flex">
<div
data-tauri-drag-region
className={cn(
"px-2 py-3 select-none side w-[240px] h-full overflow-hidden flex flex-col gap-y-3 flex-shrink-0 relative",
isMac && "mt-6"
)}
>
<header className="flex items-center">
<LogoSvg className="w-8 h-8" />
<div className="ml-[9px] flex flex-col items-center justify-center">
<TitleSvg />
<AnonymousSvg />
{/* <span className="text-white text-[18px] font-bold tracking-wide">匿名反溯源网络系统</span> */}
{/* <span className="text-white text-[8px] font-medium">Anonymous anti traceability network system</span> */}
</div>
</header>
<nav className="flex flex-col flex-1 gap-y-2">
{navList.map((item, index) => {
return (
<NavLink
key={item.id}
to={"/" + item.id}
className={({ isActive }) =>
cn(
"pl-[11px] py-2 flex items-center gap-2 rounded text-white text-sm",
isActive && "bg-[#213265] "
)
}
onClick={() => handleClickMenu(index)}
>
{item.icon}
<span>{item.title}</span>
</NavLink>
);
})}
</nav>
<footer className="h-[55px] p-4 items-center gap-2 inline-flex absolute left-4 bottom-[20px]">
<div className="w-[156px] text-white/40 text-sm font-normal leading-tight">
<div>:{coreVersion || "v0.0.1"}</div>
{/* <div>环境DEV</div> */}
</div>
<ChevronDownSvg fill="rgba(255, 255, 255, 0.4)" />
</footer>
</div>
<main className="flex-1 bg-white mt-10 mb-1 mr-1 rounded-xl overflow-hidden min-w-[1693px]">
<Outlet />
</main>
return (
<div data-tauri-drag-region className="layout flex">
<div
data-tauri-drag-region
className={cn(
"px-2 py-3 select-none side w-[240px] h-full overflow-hidden flex flex-col gap-y-3 flex-shrink-0 relative",
isMac && "mt-6"
)}
>
<header className="flex items-center">
<LogoSvg className="w-8 h-8" />
<div className="ml-[9px] flex flex-col items-center justify-center">
<TitleSvg />
<AnonymousSvg />
{/* <span className="text-white text-[18px] font-bold tracking-wide">匿名反溯源网络系统</span> */}
{/* <span className="text-white text-[8px] font-medium">Anonymous anti traceability network system</span> */}
</div>
</header>
<nav className="flex flex-col flex-1 gap-y-2">
{navList.map((item, index) => {
return (
<NavLink
key={item.id}
to={"/" + item.id}
className={({ isActive }) =>
cn(
"pl-[11px] py-2 flex items-center gap-2 rounded text-white text-sm",
isActive && "bg-[#213265] "
)
}
onClick={() => handleClickMenu(index)}
>
{item.icon}
<span>{item.title}</span>
</NavLink>
);
})}
</nav>
<footer className="h-[55px] p-4 items-center gap-2 inline-flex absolute left-4 bottom-[20px]">
<div className="w-[156px] text-white/40 text-sm font-normal leading-tight">
<div>:{coreVersion || "v0.0.1"}</div>
{/* <div>环境DEV</div> */}
</div>
<ChevronDownSvg fill="rgba(255, 255, 255, 0.4)" />
</footer>
</div>
<main className="flex-1 bg-white mt-10 mb-1 mr-1 rounded-xl overflow-y-hidden overflow-x-auto w-full scrollbar-visible">
<div className="min-w-[1693px] h-full">
<Outlet />
</div>
);
</main>
</div>
);
}

View File

@ -1,949 +0,0 @@
import { useEffect, useMemo, useRef, memo } 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 "..";
const planePathImg =
"image://data:image/svg+xml;charset=utf-8;base64,PHN2ZyB3aWR0aD0iNjciIGhlaWdodD0iMTAyIiB2aWV3Qm94PSIwIDAgNjcgMTAyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMF9mXzYxMTdfMjEyNDA3KSI+CjxwYXRoIGQ9Ik0zNC4yMTA5IDkxLjE4ODZMNTMuNjU3OCA0MC45NThDNTQuOTM4IDM3LjY1MTMgNTUuNzk4MyAzNC4xNTkyIDU1LjM1NjMgMzAuNjQxQzU0LjQzNTcgMjMuMzEyOCA1MC40Njg0IDExLjAyMDggMzQuMjExMiAxMS4wMjA4QzE5LjE5MDMgMTEuMDIwOCAxMy45MTEgMjEuNTE0NiAxMi4wNTU0IDI4Ljg5MTJDMTAuOTAxIDMzLjQ4MDYgMTEuOTkyNiAzOC4yMTg2IDEzLjgyMzEgNDIuNTgyN0wzNC4yMTA5IDkxLjE4ODZaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfNjExN18yMTI0MDcpIi8+CjwvZz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfZl82MTE3XzIxMjQwNyIgeD0iMC44OTE3NDQiIHk9IjAuMzMxOTkiIHdpZHRoPSI2NS4yNzA3IiBoZWlnaHQ9IjEwMS41NDUiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iNS4zNDQ0MSIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzYxMTdfMjEyNDA3Ii8+CjwvZmlsdGVyPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNjExN18yMTI0MDciIHgxPSIzNS4yODI2IiB5MT0iMTAuODU2NCIgeDI9IjM1LjI4MjYiIHkyPSI4Ni44NTY0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiMwMEYyRkYiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTUwMEZGIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==";
interface LinesItemType {
name: string;
country_code: string;
value: number[];
}
type LinesDataType = [LinesItemType, LinesItemType];
type LinesType = [string, LinesDataType[]];
// 创建单个国家的涟漪效果
const createCountryRipple = (countryCode: string) => {
const coords = geoCoordMap[countryCode];
if (!coords) return null;
return {
name: countryCodeMap[countryCode] ?? "",
value: coords,
country_code: countryCode,
};
};
export const WorldGeo = memo(
({
screenData,
selectedApp,
tooltipType,
tooltipClosed,
setTooltipClosed,
}: {
screenData: any;
selectedApp: any;
tooltipType: string;
tooltipClosed: boolean;
setTooltipClosed: (value: boolean) => void;
}) => {
// const queryClient = useQueryClient()
const customTooltipRef = 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[]>([]);
const mainToData = useMemo(() => {
// 使用新的数据结构
const proxiesList =
selectedApp && selectedApp?.jumpList
? [selectedApp.jumpList]
: screenData?.proxy_info?.proxies ?? [];
// 初始化数据数组 - 不再包含 startCountry
const data: any = [];
// 遍历代理列表
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, // 保存连线标志
});
// 添加终点ingress_country_code
data.push({
country_code: item.ingress_country_code,
type: "end",
isLine: proxyItem.isLine, // 保存连线标志
});
} else {
// 如果没有 ingress_country_code只添加 country_code
data.push({
country_code: item.country_code,
isLine: proxyItem.isLine, // 保存连线标志
});
}
});
}
});
return data;
}, [screenData, selectedApp]);
// 创建自定义提示框DOM元素
const createCustomTooltip = () => {
// 如果已经存在自定义提示框,则移除它
if (document.getElementById("custom-fixed-tooltip")) {
document.getElementById("custom-fixed-tooltip")?.remove();
}
// 创建自定义提示框
const tooltip = document.createElement("div");
tooltip.id = "custom-fixed-tooltip";
tooltip.style.position = "fixed";
tooltip.style.zIndex = "1000";
tooltip.style.pointerEvents = "auto";
tooltip.style.backgroundColor = "transparent";
// 设置提示框内容
const currentTooltipType =
CONST_TOOLTIP_TYPE[
tooltipType as keyof typeof CONST_TOOLTIP_TYPE
] || CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION;
tooltip.innerHTML = `
<div class="tooltip-content">
<img class="line-img" src="${getUrl(
"svg/anti-forensics-forwarding/Line.svg"
)}" alt="" />
<div class="fill"></div>
<div class="tip-box">
<div>
<div class="label" style="color: white; font-weight: bold;">${
currentTooltipType.title
}</div>
<img class="close-icon" src="${getUrl(
"svg/Xwhite.svg"
)}" alt=""
style="cursor: pointer; " />
</div>
<img class="${
CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.type ===
currentTooltipType.type
? "encryption-img"
: "traffic-obfuscation-img"
}" src="${getUrl(
CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.type ===
currentTooltipType.type
? "image/nested-encryption.png"
: "image/traffic-obfuscation.png"
)}" alt="" />
</div>
</div>
`;
// 添加到DOM
document.body.appendChild(tooltip);
customTooltipRef.current = tooltip;
// 添加关闭按钮事件
const closeButton = tooltip.querySelector(".close-icon");
if (closeButton) {
closeButton.addEventListener("click", () => {
setTooltipClosed(false);
tooltip.remove();
customTooltipRef.current = null;
});
}
// 定位提示框
positionCustomTooltip();
};
// 定位自定义提示框 - 优化版本
const positionCustomTooltip = () => {
if (!customTooltipRef.current || !proxyGeoRef.current) return;
// 找到US点
const coords = geoCoordMap["CA"];
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);
}
};
// 主线每个节点tip竖线的经纬度,修改tip 竖线的高度也可以用这个
const mianLineData = (data: typeof mainToData) => {
return (
data
.map((item: any) => {
const countryCode = item.country_code.toUpperCase();
const coords = geoCoordMap[countryCode] as
| [number, number]
| undefined;
if (!coords) return null;
return {
name: countryCodeMap[countryCode],
coords: [coords, [coords[0], coords[1] + 4]],
value: countryCode,
};
})
.filter((v: any) => !!v) ?? []
);
};
const getLineItem = (
preCode: string,
nextCode: string
): [LinesItemType, LinesItemType] => {
return [
{
name: countryCodeMap[preCode] ?? "",
value: geoCoordMap[preCode] ?? [],
country_code: preCode,
},
{
name: countryCodeMap[nextCode] ?? "",
value: geoCoordMap[nextCode] ?? [],
country_code: nextCode,
},
];
};
const getLine = () => {
// 实现数据处理
const solidData: LinesType[] = [["main", []]]; // 使用"main"替代startCountry.country_code
// 收集需要显示涟漪效果的所有点(包括连线和不连线的)
const ripplePoints: any[] = [];
// 处理主路径数据
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();
// 如果当前项是起点,下一项是终点
if (currentItem.type === "start" && nextItem.type === "end") {
const startCode = countryCode;
const endCode = nextItem.country_code.toUpperCase();
// 无论是否连线,都添加点的涟漪效果
const startPoint = createCountryRipple(startCode);
const endPoint = createCountryRipple(endCode);
if (startPoint) ripplePoints.push(startPoint);
if (endPoint) ripplePoints.push(endPoint);
// 检查是否应该绘制连线
if (currentItem.isLine !== false) {
solidData[0]?.[1].push(getLineItem(startCode, endCode));
}
// 跳过下一项,因为已经处理了
i++;
}
// 常规情况:当前项到下一项
else {
const nextCountryCode = nextItem.country_code.toUpperCase();
// 无论是否连线,都添加点的涟漪效果
const currentPoint = createCountryRipple(countryCode);
const nextPoint = createCountryRipple(nextCountryCode);
if (currentPoint) ripplePoints.push(currentPoint);
if (nextPoint) ripplePoints.push(nextPoint);
// 检查是否应该绘制连线
if (currentItem.isLine !== false) {
solidData[0]?.[1].push(
getLineItem(countryCode, nextCountryCode)
);
}
}
}
// 虚线数据处理(保持原有逻辑)
const pathList =
screenData?.path_list?.filter(
(v: any) => v.name !== screenData?.proxy_info?.name
) ?? [];
const otherLineList = pathList.map(() => {});
return {
solidData,
otherLineList,
ripplePoints,
};
};
// 获取连线经纬度数据
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([fromCoord, toCoord]);
// 计算中点,考虑曲线的弧度
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 pointData = lastExit
? [lastExit].map((v) => {
return {
name: v.name,
value: v.value,
datas: {
country_code: v.country_code,
},
};
})
: [];
// 根据是否是主路径设置不同的大小和颜色
const outerSize = isMainPath ? 8 : 4;
const innerSize = isMainPath ? 4 : 2;
// Use selectedApp.color if available, otherwise default to blue
const outerColor = selectedApp?.color || "#0ea5e9"; // Use selectedApp.color with fallback
const innerColor = "#FFFFFF"; // 白色内层
return [
{
// 外层蓝色点,带涟漪效果
type: "effectScatter",
coordinateSystem: "geo",
zlevel: 3,
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) => {
// 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 createPathPoints = (
dataItems: LinesDataType[],
isMainPath: boolean = true
) => {
// 创建数据数组
const pointData = dataItems.map((dataItem: LinesDataType) => {
return {
name: dataItem[0].name,
value: geoCoordMap[dataItem[0].country_code],
datas: {
country_code: dataItem[0].country_code,
},
};
});
// 根据是否是主路径设置不同的大小和颜色
const outerSize = isMainPath ? 8 : 4;
const innerSize = isMainPath ? 4 : 2;
// Use selectedApp.color if available, otherwise default to blue
const outerColor = selectedApp?.color || "#0ea5e9"; // Use selectedApp.color with fallback
const innerColor = "#FFFFFF"; // 白色内层
return [
{
// 外层蓝色点,带涟漪效果
type: "effectScatter",
coordinateSystem: "geo",
zlevel: 3,
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) => {
// 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 createRipplePointsWithTooltip = (ripplePoints: any) => {
// Use selectedApp.color if available, otherwise default to blue
const outerColor = selectedApp?.color || "#0ea5e9"; // Use selectedApp.color with fallback
return {
type: "effectScatter",
coordinateSystem: "geo",
zlevel: 3,
color: outerColor,
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) => {
// 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: ripplePoints.map((point: any) => ({
name: point.name,
value: point.value,
datas: {
country_code: point.country_code,
},
})),
} 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;
}
const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
// 添加飞行线
series.push({
name: item[0],
type: "lines",
zlevel: 1,
label: {
show: false, // 不使用内置标签
},
// 飞行线特效
effect: {
show: true, // 是否显示
period: 4, // 特效动画时间
trailLength: 0.7, // 特效尾迹长度。取从 0 到 1 的值,数值越大尾迹越长
symbol: planePathImg, // 特效图形标记
symbolSize: [10, 20],
},
// 线条样式
lineStyle: {
curveness: -0.4, // 飞线弧度
type: "solid", // 飞线类型
color: selectedApp?.color || "#0ea5e9", // Use selectedApp.color with fallback
width: 1.5, // 飞线宽度
opacity: 0.1,
},
data: convertData(
item[1]
) as echarts.LinesSeriesOption["data"],
});
// 添加路径点的双层效果
const pathPoints = createPathPoints(item[1], true);
series.push(...pathPoints);
// 添加出口节点的双层效果
if (lastExit) {
const exitNodes = createDualLayerPoint(lastExit, true);
series.push(...exitNodes);
}
});
otherLineList.forEach((line: any) => {
line.forEach((item: any) => {
const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
// 添加虚线
series.push({
name: item[0],
type: "lines",
zlevel: 1,
label: {
show: false,
},
// 线条样式
lineStyle: {
curveness: -0.4, // 飞线弧度
type: [5, 5], // 飞线类型
color: "#F0FFA2", // 飞线颜色
width: 0.5, // 飞线宽度
opacity: 0.6,
},
data: convertData(
item[1]
) as echarts.LinesSeriesOption["data"],
});
// 添加路径点的双层效果(次要路径)
const pathPoints = createPathPoints(item[1], false);
series.push(...pathPoints);
// 添加出口节点的双层效果(次要路径)
if (lastExit) {
const exitNodes = createDualLayerPoint(lastExit, false);
series.push(...exitNodes);
}
});
});
return true;
};
// 主线tip series
const getMianLineTipData = (series: echarts.SeriesOption[] = []) => {
const rich = Object.keys(countryCodeMap).reduce((object, code) => {
object[code] = {
color: "transparent",
height: 20,
width: 20,
align: "left",
backgroundColor: {
image: getUrl(
`image/res/flag/${code.toUpperCase()}.png`
), // 动态生成国旗图标
},
};
return object;
}, {} as { [key: string]: { [key: string]: number | string | unknown } });
series.push(
// 柱状体的主干
{
name: "solidTip",
type: "lines",
zlevel: 5,
effect: {
show: false,
symbolSize: 5, // 图标大小
},
lineStyle: {
width: 0, // 尾迹线条宽度
color: "#F0FFA2",
opacity: 1, // 尾迹线条透明度
curveness: 0, // 尾迹线条曲直度
},
label: {
show: true,
position: "end",
color: "#fff",
formatter: (parameters) => {
return `{left|} {gap1|}{${parameters.value}|}{gap2|}{name|${parameters.name}}{gap3|}{right|}`;
},
rich: {
left: {
color: "transparent",
height: 35,
width: 8,
align: "center",
backgroundColor: {
image: `data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMSIgaGVpZ2h0PSI1OCIgdmlld0JveD0iMCAwIDExIDU4IiBmaWxsPSJub25lIj4KPHBhdGggZD0iTTExIDU2LjA4ODRIMVY0Ni4wODg0IiBzdHJva2U9IiNGRkZERDMiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIi8+CjxwYXRoIGQ9Ik0xIDExVjFIMTEiIHN0cm9rZT0iI0ZGRkREMyIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiLz4KPHBhdGggZD0iTTguNzI5NDkgMTlWMzkiIHN0cm9rZT0iI0ZGRkREMyIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiLz4KPC9zdmc+`, // 动态生成国旗图标
},
},
gap1: {
height: 35,
width: 8,
},
...rich,
gap2: {
height: 35,
width: 6,
},
name: {
color: "#fff",
align: "center",
lineHeight: 35,
fontSize: 14,
padding: [2, 0, 0, 0],
},
gap3: {
height: 35,
width: 8,
},
right: {
color: "transparent",
height: 35,
width: 8,
align: "center",
backgroundColor: {
image: `data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNCIgaGVpZ2h0PSI1OCIgdmlld0JveD0iMCAwIDE0IDU4IiBmaWxsPSJub25lIj4KPHBhdGggZD0iTTEyLjczMDIgNDYuMDQzOVY1Ni4wNDM5SDIuNzMwMjIiIHN0cm9rZT0iI0ZGRkREMyIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiLz4KPHBhdGggZD0iTTIuNzMwMjIgMS4wNDM5NUgxMi43MzAyVjExLjA0MzkiIHN0cm9rZT0iI0ZGRkREMyIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiLz4KPHBhdGggZD0iTTEgMTkuMDQzOVYzOS4wNDM5IiBzdHJva2U9IiNGRkZERDMiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIi8+Cjwvc3ZnPg==`, // 动态生成国旗图标
},
},
},
backgroundColor: "#080A00",
},
silent: true,
data: mianLineData(mainToData),
}
);
};
const isCN = (code: string) => {
return ["HK", "MO", "TW", "CN"].includes(code.toUpperCase());
};
const getOption = () => {
const series: echarts.SeriesOption[] = [];
getLianData(series);
getMianLineTipData(series);// 添加主线tip 暂时隐藏
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.backgroundColor = '#8B3700';
label.style.color = '#FFB27A';
label.style.padding = '5px 10px';
label.style.borderRadius = '4px';
label.style.fontSize = '18px';
label.style.fontWeight = 'normal';
label.style.textAlign = 'center';
label.style.transform = 'translate(-50%, -50%)';
label.style.whiteSpace = 'nowrap';
label.style.pointerEvents = 'none';
label.style.zIndex = '1001';
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();
};
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);
}, [screenData, 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;
};
}, []);
useEffect(() => {
if (tooltipClosed) {
createCustomTooltip();
}
}, [tooltipClosed, tooltipType]);
return (
<div className="flex-1 h-full flex flex-col">
<div id="screenGeo" className="flex-1"></div>
</div>
);
}
);

View File

@ -1,876 +0,0 @@
import { useEffect, useMemo, useRef, memo } 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 "..";
const planePathImg =
"image://data:image/svg+xml;charset=utf-8;base64,PHN2ZyB3aWR0aD0iNjciIGhlaWdodD0iMTAyIiB2aWV3Qm94PSIwIDAgNjcgMTAyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMF9mXzYxMTdfMjEyNDA3KSI+CjxwYXRoIGQ9Ik0zNC4yMTA5IDkxLjE4ODZMNTMuNjU3OCA0MC45NThDNTQuOTM4IDM3LjY1MTMgNTUuNzk4MyAzNC4xNTkyIDU1LjM1NjMgMzAuNjQxQzU0LjQzNTcgMjMuMzEyOCA1MC40Njg0IDExLjAyMDggMzQuMjExMiAxMS4wMjA4QzE5LjE5MDMgMTEuMDIwOCAxMy45MTEgMjEuNTE0NiAxMi4wNTU0IDI4Ljg5MTJDMTAuOTAxIDMzLjQ4MDYgMTEuOTkyNiAzOC4yMTg2IDEzLjgyMzEgNDIuNTgyN0wzNC4yMTA5IDkxLjE4ODZaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfNjExN18yMTI0MDcpIi8+CjwvZz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfZl82MTE3XzIxMjQwNyIgeD0iMC44OTE3NDQiIHk9IjAuMzMxOTkiIHdpZHRoPSI2NS4yNzA3IiBoZWlnaHQ9IjEwMS41NDUiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iNS4zNDQ0MSIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzYxMTdfMjEyNDA3Ii8+CjwvZmlsdGVyPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNjExN18yMTI0MDciIHgxPSIzNS4yODI2IiB5MT0iMTAuODU2NCIgeDI9IjM1LjI4MjYiIHkyPSI4Ni44NTY0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiMwMEYyRkYiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTUwMEZGIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==";
interface LinesItemType {
name: string;
country_code: string;
value: number[];
}
type LinesDataType = [LinesItemType, LinesItemType];
type LinesType = [string, LinesDataType[]];
// 创建单个国家的涟漪效果
const createCountryRipple = (countryCode: string) => {
const coords = geoCoordMap[countryCode];
if (!coords) return null;
return {
name: countryCodeMap[countryCode] ?? "",
value: coords,
country_code: countryCode,
};
};
export const WorldGeo = memo(
({
screenData,
selectedApp,
tooltipType,
tooltipClosed,
setTooltipClosed,
}: {
screenData: any;
selectedApp: any;
tooltipType: string;
tooltipClosed: boolean;
setTooltipClosed: (value: boolean) => void;
}) => {
// const queryClient = useQueryClient()
const customTooltipRef = useRef<HTMLDivElement | null>(null);
const proxyGeoRef = useRef<EChartsType | null>(null);
const preMainToData = useRef<{ country_code: string }[]>([]);
const mainToData = useMemo(() => {
// 使用新的数据结构
const proxiesList =
selectedApp && selectedApp?.jumpList
? [selectedApp.jumpList]
: screenData?.proxy_info?.proxies ?? [];
// 初始化数据数组 - 不再包含 startCountry
const data: any = [];
// 遍历代理列表
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, // 保存连线标志
});
// 添加终点ingress_country_code
data.push({
country_code: item.ingress_country_code,
type: "end",
isLine: proxyItem.isLine, // 保存连线标志
});
} else {
// 如果没有 ingress_country_code只添加 country_code
data.push({
country_code: item.country_code,
isLine: proxyItem.isLine, // 保存连线标志
});
}
});
}
});
return data;
}, [screenData, selectedApp]);
// 创建自定义提示框DOM元素
const createCustomTooltip = () => {
// 如果已经存在自定义提示框,则移除它
if (document.getElementById("custom-fixed-tooltip")) {
document.getElementById("custom-fixed-tooltip")?.remove();
}
// 创建自定义提示框
const tooltip = document.createElement("div");
tooltip.id = "custom-fixed-tooltip";
tooltip.style.position = "fixed";
tooltip.style.zIndex = "1000";
tooltip.style.pointerEvents = "auto";
tooltip.style.backgroundColor = "transparent";
// 设置提示框内容
const currentTooltipType =
CONST_TOOLTIP_TYPE[
tooltipType as keyof typeof CONST_TOOLTIP_TYPE
] || CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION;
tooltip.innerHTML = `
<div class="tooltip-content">
<img class="line-img" src="${getUrl(
"svg/anti-forensics-forwarding/Line.svg"
)}" alt="" />
<div class="fill"></div>
<div class="tip-box">
<div>
<div class="label" style="color: white; font-weight: bold;">${
currentTooltipType.title
}</div>
<img class="close-icon" src="${getUrl(
"svg/Xwhite.svg"
)}" alt=""
style="cursor: pointer; " />
</div>
<img class="${
CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.type ===
currentTooltipType.type
? "encryption-img"
: "traffic-obfuscation-img"
}" src="${getUrl(
CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.type ===
currentTooltipType.type
? "image/nested-encryption.png"
: "image/traffic-obfuscation.png"
)}" alt="" />
</div>
</div>
`;
// 添加到DOM
document.body.appendChild(tooltip);
customTooltipRef.current = tooltip;
// 添加关闭按钮事件
const closeButton = tooltip.querySelector(".close-icon");
if (closeButton) {
closeButton.addEventListener("click", () => {
setTooltipClosed(false);
tooltip.remove();
customTooltipRef.current = null;
});
}
// 定位提示框
positionCustomTooltip();
};
// 定位自定义提示框 - 优化版本
const positionCustomTooltip = () => {
if (!customTooltipRef.current || !proxyGeoRef.current) return;
// 找到US点
const coords = geoCoordMap["CA"];
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] + screenCoord[0] / 2 + 18
// }px`;
// customTooltipRef.current.style.top = `${
// screenCoord[1] - screenCoord[1] / 2 + 7
// }px`;
// 设置提示框位置
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);
}
};
// 主线每个节点tip竖线的经纬度
const mianLineData = (data: typeof mainToData) => {
return (
data
.map((item: any) => {
const countryCode = item.country_code.toUpperCase();
const coords = geoCoordMap[countryCode] as
| [number, number]
| undefined;
if (!coords) return null;
return {
name: countryCodeMap[countryCode],
coords: [coords, [coords[0], coords[1] + 4]],
value: countryCode,
};
})
.filter((v: any) => !!v) ?? []
);
};
const getLineItem = (
preCode: string,
nextCode: string
): [LinesItemType, LinesItemType] => {
return [
{
name: countryCodeMap[preCode] ?? "",
value: geoCoordMap[preCode] ?? [],
country_code: preCode,
},
{
name: countryCodeMap[nextCode] ?? "",
value: geoCoordMap[nextCode] ?? [],
country_code: nextCode,
},
];
};
const getLine = () => {
// 实现数据处理
const solidData: LinesType[] = [["main", []]]; // 使用"main"替代startCountry.country_code
// 收集需要显示涟漪效果的所有点(包括连线和不连线的)
const ripplePoints: any[] = [];
// 处理主路径数据
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();
// 如果当前项是起点,下一项是终点
if (currentItem.type === "start" && nextItem.type === "end") {
const startCode = countryCode;
const endCode = nextItem.country_code.toUpperCase();
// 无论是否连线,都添加点的涟漪效果
const startPoint = createCountryRipple(startCode);
const endPoint = createCountryRipple(endCode);
if (startPoint) ripplePoints.push(startPoint);
if (endPoint) ripplePoints.push(endPoint);
// 检查是否应该绘制连线
if (currentItem.isLine !== false) {
solidData[0]?.[1].push(getLineItem(startCode, endCode));
}
// 跳过下一项,因为已经处理了
i++;
}
// 常规情况:当前项到下一项
else {
const nextCountryCode = nextItem.country_code.toUpperCase();
// 无论是否连线,都添加点的涟漪效果
const currentPoint = createCountryRipple(countryCode);
const nextPoint = createCountryRipple(nextCountryCode);
if (currentPoint) ripplePoints.push(currentPoint);
if (nextPoint) ripplePoints.push(nextPoint);
// 检查是否应该绘制连线
if (currentItem.isLine !== false) {
solidData[0]?.[1].push(
getLineItem(countryCode, nextCountryCode)
);
}
}
}
// 虚线数据处理(保持原有逻辑)
const pathList =
screenData?.path_list?.filter(
(v: any) => v.name !== screenData?.proxy_info?.name
) ?? [];
const otherLineList = pathList.map(() => {});
return {
solidData,
otherLineList,
ripplePoints,
};
};
// 获取连线经纬度数据
const convertData = (data: LinesDataType[]) => {
const res = [];
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 ?? ""];
if (fromCoord && toCoord) {
res.push([fromCoord, toCoord]);
}
}
return res;
};
// 创建双层点效果 - 大点
const createDualLayerPoint = (
lastExit: LinesItemType,
isMainPath: boolean = true
) => {
// 创建数据数组,用于两个散点图层
const pointData = lastExit
? [lastExit].map((v) => {
return {
name: v.name,
value: v.value,
datas: {
country_code: v.country_code,
},
};
})
: [];
// 根据是否是主路径设置不同的大小和颜色
const outerSize = isMainPath ? 8 : 4;
const innerSize = isMainPath ? 4 : 2;
// Use selectedApp.color if available, otherwise default to blue
const outerColor = selectedApp?.color || "#0ea5e9"; // Use selectedApp.color with fallback
const innerColor = "#FFFFFF"; // 白色内层
return [
{
// 外层蓝色点,带涟漪效果
type: "effectScatter",
coordinateSystem: "geo",
zlevel: 3,
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) => {
// 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 createPathPoints = (
dataItems: LinesDataType[],
isMainPath: boolean = true
) => {
// 创建数据数组
const pointData = dataItems.map((dataItem: LinesDataType) => {
return {
name: dataItem[0].name,
value: geoCoordMap[dataItem[0].country_code],
datas: {
country_code: dataItem[0].country_code,
},
};
});
// 根据是否是主路径设置不同的大小和颜色
const outerSize = isMainPath ? 8 : 4;
const innerSize = isMainPath ? 4 : 2;
// Use selectedApp.color if available, otherwise default to blue
const outerColor = selectedApp?.color || "#0ea5e9"; // Use selectedApp.color with fallback
const innerColor = "#FFFFFF"; // 白色内层
return [
{
// 外层蓝色点,带涟漪效果
type: "effectScatter",
coordinateSystem: "geo",
zlevel: 3,
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) => {
// 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 createRipplePointsWithTooltip = (ripplePoints: any) => {
// Use selectedApp.color if available, otherwise default to blue
const outerColor = selectedApp?.color || "#0ea5e9"; // Use selectedApp.color with fallback
return {
type: "effectScatter",
coordinateSystem: "geo",
zlevel: 3,
color: outerColor,
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) => {
// 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: ripplePoints.map((point: any) => ({
name: point.name,
value: point.value,
datas: {
country_code: point.country_code,
},
})),
} as echarts.SeriesOption;
};
// 连线 series
const getLianData = (series: echarts.SeriesOption[]) => {
const { solidData, otherLineList, ripplePoints } = getLine();
console.log(solidData, "solidData");
console.log(otherLineList, "otherLineList");
console.log(ripplePoints, "ripplePoints");
// 如果有需要显示涟漪效果的点,添加它们
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;
}
const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
// 添加飞行线
series.push({
name: item[0], // todo 需要在飞线中间添加label
type: "lines",
zlevel: 1,
label: {
show: false,
position: "middle",
formatter: (params: any) => {
// 使用自定义样式的文本
return (
"{text|SS签名}"
);
},
rich: {
text: {
color: "#FFB27A", // 字体颜色为 #FFB27A
fontSize: 18, // 字体大小为 18px
padding: [10,10,10,10], // padding 上下为 4左右为 8
backgroundColor: "#8B3700", // 背景颜色为 #8B3700
borderRadius: 4, // 可选:添加圆角效果
},
},
// 不需要额外的背景色,因为已经在 rich 中设置了
backgroundColor: "transparent",
padding: [0, 0, 0, 0],
},
// 飞行线特效
effect: {
show: true, // 是否显示
period: 4, // 特效动画时间
trailLength: 0.7, // 特效尾迹长度。取从 0 到 1 的值,数值越大尾迹越长
symbol: planePathImg, // 特效图形标记
symbolSize: [10, 20],
},
// 线条样式
lineStyle: {
curveness: -0.4, // 飞线弧度
type: "solid", // 飞线类型
color: selectedApp?.color || "#0ea5e9", // Use selectedApp.color with fallback
width: 1.5, // 飞线宽度
opacity: 0.1,
},
data: convertData(
item[1]
) as echarts.LinesSeriesOption["data"],
});
// 添加路径点的双层效果
const pathPoints = createPathPoints(item[1], true);
series.push(...pathPoints);
// 添加出口节点的双层效果
if (lastExit) {
const exitNodes = createDualLayerPoint(lastExit, true);
series.push(...exitNodes);
}
});
otherLineList.forEach((line: any) => {
line.forEach((item: any) => {
const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
// 添加虚线
series.push({
name: item[0],
type: "lines",
zlevel: 1,
label: {
show: false,
},
// 线条样式
lineStyle: {
curveness: -0.4, // 飞线弧度
type: [5, 5], // 飞线类型
color: "#F0FFA2", // 飞线颜色
width: 0.5, // 飞线宽度
opacity: 0.6,
},
data: convertData(
item[1]
) as echarts.LinesSeriesOption["data"],
});
// 添加路径点的双层效果(次要路径)
const pathPoints = createPathPoints(item[1], false);
series.push(...pathPoints);
// 添加出口节点的双层效果(次要路径)
if (lastExit) {
const exitNodes = createDualLayerPoint(lastExit, false);
series.push(...exitNodes);
}
});
});
return true;
};
// 主线tip series
const getMianLineTipData = (series: echarts.SeriesOption[] = []) => {
const rich = Object.keys(countryCodeMap).reduce((object, code) => {
object[code] = {
color: "transparent",
height: 20,
width: 20,
align: "left",
backgroundColor: {
image: getUrl(
`image/res/flag/${code.toUpperCase()}.png`
), // 动态生成国旗图标
},
};
return object;
}, {} as { [key: string]: { [key: string]: number | string | unknown } });
series.push(
// 柱状体的主干
{
name: "solidTip",
type: "lines",
zlevel: 5,
effect: {
show: false,
symbolSize: 5, // 图标大小
},
lineStyle: {
width: 2, // 尾迹线条宽度
color: "#F0FFA2",
opacity: 1, // 尾迹线条透明度
curveness: 0, // 尾迹线条曲直度
},
label: {
show: true,
position: "end",
color: "#fff",
formatter: (parameters) => {
return `{left|} {gap1|}{${parameters.value}|}{gap2|}{name|${parameters.name}}{gap3|}{right|}`;
},
rich: {
left: {
color: "transparent",
height: 35,
width: 8,
align: "center",
backgroundColor: {
image: `data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMSIgaGVpZ2h0PSI1OCIgdmlld0JveD0iMCAwIDExIDU4IiBmaWxsPSJub25lIj4KPHBhdGggZD0iTTExIDU2LjA4ODRIMVY0Ni4wODg0IiBzdHJva2U9IiNGRkZERDMiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIi8+CjxwYXRoIGQ9Ik0xIDExVjFIMTEiIHN0cm9rZT0iI0ZGRkREMyIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiLz4KPHBhdGggZD0iTTguNzI5NDkgMTlWMzkiIHN0cm9rZT0iI0ZGRkREMyIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiLz4KPC9zdmc+`, // 动态生成国旗图标
},
},
gap1: {
height: 35,
width: 8,
},
...rich,
gap2: {
height: 35,
width: 6,
},
name: {
color: "#fff",
align: "center",
lineHeight: 35,
fontSize: 14,
padding: [2, 0, 0, 0],
},
gap3: {
height: 35,
width: 8,
},
right: {
color: "transparent",
height: 35,
width: 8,
align: "center",
backgroundColor: {
image: `data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNCIgaGVpZ2h0PSI1OCIgdmlld0JveD0iMCAwIDE0IDU4IiBmaWxsPSJub25lIj4KPHBhdGggZD0iTTEyLjczMDIgNDYuMDQzOVY1Ni4wNDM5SDIuNzMwMjIiIHN0cm9rZT0iI0ZGRkREMyIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiLz4KPHBhdGggZD0iTTIuNzMwMjIgMS4wNDM5NUgxMi43MzAyVjExLjA0MzkiIHN0cm9rZT0iI0ZGRkREMyIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiLz4KPHBhdGggZD0iTTEgMTkuMDQzOVYzOS4wNDM5IiBzdHJva2U9IiNGRkZERDMiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIi8+Cjwvc3ZnPg==`, // 动态生成国旗图标
},
},
},
backgroundColor: "#080A00",
},
silent: true,
data: mianLineData(mainToData),
}
);
};
const isCN = (code: string) => {
return ["HK", "MO", "TW", "CN"].includes(code.toUpperCase());
};
const getRegions = () => {
const codeList: string[] = [];
const regionsData = mainToData;
regionsData.forEach((item: any) =>
codeList.push(
isCN(item.country_code)
? "CN"
: item.country_code.toUpperCase()
)
);
const regions = codeList.map((item) => {
return {
name: countryCodeMap[item], // 中国
itemStyle: {
color: "#172554",
areaColor: "#172554",
borderColor: "#0ea5e9", // 边框颜色
borderWidth: 1.2, // 边框宽度
borderType: "solid", // 修改为实线边框
},
};
});
return regions;
};
const getOption = () => {
const series: echarts.SeriesOption[] = [];
getLianData(series);
// getMianLineTipData(series);// 添加主线tip 暂时隐藏
const regions = getRegions();
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;
},
},
regions,
},
series: series,
};
return option;
};
const handleResize = () => {
proxyGeoRef.current?.resize();
};
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);
}, [screenData, 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);
// 页面resize时触发
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
proxyGeoRef.current?.dispose();
proxyGeoRef.current = null;
};
}, []);
useEffect(() => {
if (tooltipClosed) {
createCustomTooltip();
}
}, [tooltipClosed, tooltipType]);
return (
<div className="flex-1 h-full flex flex-col">
<div id="screenGeo" className="flex-1"></div>
</div>
);
}
);

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, memo } from "react";
import { useEffect, useMemo, useRef, memo, useState } from "react";
import * as echarts from "echarts";
// import 'echarts-gl';
// import { useQueryClient } from "@tanstack/react-query";
@ -6,9 +6,8 @@ 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";
const planePathImg =
"image://data:image/svg+xml;charset=utf-8;base64,PHN2ZyB3aWR0aD0iNjciIGhlaWdodD0iMTAyIiB2aWV3Qm94PSIwIDAgNjcgMTAyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMF9mXzYxMTdfMjEyNDA3KSI+CjxwYXRoIGQ9Ik0zNC4yMTA5IDkxLjE4ODZMNTMuNjU3OCA0MC45NThDNTQuOTM4IDM3LjY1MTMgNTUuNzk4MyAzNC4xNTkyIDU1LjM1NjMgMzAuNjQxQzU0LjQzNTcgMjMuMzEyOCA1MC40Njg0IDExLjAyMDggMzQuMjExMiAxMS4wMjA4QzE5LjE5MDMgMTEuMDIwOCAxMy45MTEgMjEuNTE0NiAxMi4wNTU0IDI4Ljg5MTJDMTAuOTAxIDMzLjQ4MDYgMTEuOTkyNiAzOC4yMTg2IDEzLjgyMzEgNDIuNTgyN0wzNC4yMTA5IDkxLjE4ODZaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfNjExN18yMTI0MDcpIi8+CjwvZz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfZl82MTE3XzIxMjQwNyIgeD0iMC44OTE3NDQiIHk9IjAuMzMxOTkiIHdpZHRoPSI2NS4yNzA3IiBoZWlnaHQ9IjEwMS41NDUiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iNS4zNDQ0MSIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzYxMTdfMjEyNDA3Ii8+CjwvZmlsdGVyPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNjExN18yMTI0MDciIHgxPSIzNS4yODI2IiB5MT0iMTAuODU2NCIgeDI9IjM1LjI4MjYiIHkyPSI4Ni44NTY0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiMwMEYyRkYiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTUwMEZGIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==";
interface LinesItemType {
name: string;
country_code: string;
@ -17,6 +16,147 @@ interface LinesItemType {
}
type LinesDataType = [LinesItemType, LinesItemType];
type LinesType = [string, LinesDataType[]];
// 创建左侧自定义提示框组件
const CustomTooltipLeft = ({
logs = [],
onClose,
tooltipRef,
title,
}: {
logs?: string[],
onClose: () => void,
tooltipRef: React.RefObject<HTMLDivElement>,
title: 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]);
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-[450px] 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">
...
</div>
)}
{/* {!isComplete && filteredLogs.length > 0 && (
<div className="loading-indicator mt-2 text-xs text-blue-300">
...
</div>
)} */}
</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];
@ -28,6 +168,7 @@ const createCountryRipple = (countryCode: string, color?: string) => {
color: color || "#0ea5e9", // 添加颜色属性,如果没有则使用默认颜色
};
};
export const WorldGeo = memo(
({
currentValue,
@ -35,11 +176,13 @@ export const WorldGeo = memo(
tooltipType,
tooltipClosed,
setTooltipClosed,
trafficObfuscationLogs,
}: {
currentValue: any;
newHomeProxies: any;
tooltipType: string;
tooltipClosed: boolean;
trafficObfuscationLogs:any;
setTooltipClosed: (value: boolean) => void;
}) => {
// const queryClient = useQueryClient()
@ -59,6 +202,13 @@ export const WorldGeo = memo(
>([]);
const labelContainerRef = useRef<HTMLDivElement | null>(null);
const labelsRef = useRef<HTMLDivElement[]>([]);
// 添加状态来控制是否显示tooltip
const [showTooltip1, setShowTooltip1] = useState(false);
const [showTooltip2, setShowTooltip2] = useState(false);
const mainToData = useMemo(() => {
// 使用新的数据结构
const proxiesList = currentValue ?? [];
@ -99,69 +249,7 @@ export const WorldGeo = memo(
});
return data;
}, [currentValue]);
// 创建自定义提示框DOM元素
const createCustomTooltip = () => {
// 如果已经存在自定义提示框,则移除它
if (document.getElementById("custom-fixed-tooltip")) {
document.getElementById("custom-fixed-tooltip")?.remove();
}
// 创建自定义提示框
const tooltip = document.createElement("div");
tooltip.id = "custom-fixed-tooltip";
tooltip.style.position = "fixed";
tooltip.style.zIndex = "1000";
tooltip.style.pointerEvents = "auto";
tooltip.style.backgroundColor = "transparent";
// 设置提示框内容
const currentTooltipType =
CONST_TOOLTIP_TYPE[tooltipType as keyof typeof CONST_TOOLTIP_TYPE] ||
CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION;
tooltip.innerHTML = `
<div class="tooltip-content">
<img class="line-img" src="${getUrl(
"svg/anti-forensics-forwarding/Line.svg"
)}" alt="" />
<div class="fill"></div>
<div class="tip-box">
<div>
<div class="label" style="color: white; font-weight: bold;">${
currentTooltipType.title
}</div>
<img class="close-icon" src="${getUrl(
"svg/Xwhite.svg"
)}" alt=""
style="cursor: pointer; " />
</div>
<img class="${
CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.type ===
currentTooltipType.type
? "encryption-img"
: "traffic-obfuscation-img"
}" src="${getUrl(
CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.type === currentTooltipType.type
? "image/nested-encryption.png"
: "image/traffic-obfuscation.png"
)}" alt="" />
</div>
</div>
`;
// 添加到DOM
document.body.appendChild(tooltip);
customTooltipRef.current = tooltip;
// 添加关闭按钮事件
const closeButton = tooltip.querySelector(".close-icon");
if (closeButton) {
closeButton.addEventListener("click", () => {
setTooltipClosed(false);
tooltip.remove();
customTooltipRef.current = null;
});
}
// 定位提示框
positionCustomTooltip();
};
// 定位自定义提示框 - 优化版本
const positionCustomTooltip = () => {
if (!customTooltipRef.current || !proxyGeoRef.current) return;
@ -184,57 +272,8 @@ export const WorldGeo = memo(
console.error("Error positioning tooltip:", error);
}
};
// 创建自定义提示框DOM元素
const createCustomTooltip2 = () => {
// 如果已经存在自定义提示框,则移除它
if (document.getElementById("custom-fixed-tooltip2")) {
document.getElementById("custom-fixed-tooltip2")?.remove();
}
// 创建自定义提示框
const tooltip = document.createElement("div");
tooltip.id = "custom-fixed-tooltip2";
tooltip.style.position = "fixed";
tooltip.style.zIndex = "1000";
tooltip.style.pointerEvents = "auto";
tooltip.style.backgroundColor = "transparent";
tooltip.innerHTML = `
<div class="tooltip-content">
<div class="fill-left"></div>
<div class="tip-box">
<div>
<div class="label" style="color: white; font-weight: bold;"></div>
<img class="close-icon" src="${getUrl(
"svg/Xwhite.svg"
)}" alt=""
style="cursor: pointer; " />
</div>
<img class="traffic-obfuscation-img" src="${getUrl(
"image/traffic-obfuscation.png"
)}" alt="" />
</div>
<img class="line-img-left" src="${getUrl(
"svg/anti-forensics-forwarding/LineLeft.svg"
)}" alt="" />
</div>
`;
// 添加到DOM
document.body.appendChild(tooltip);
customTooltip2Ref.current = tooltip;
// 添加关闭按钮事件
const closeButton = tooltip.querySelector(".close-icon");
if (closeButton) {
closeButton.addEventListener("click", () => {
setTooltipClosed(false);
tooltip.remove();
customTooltip2Ref.current = null;
});
}
// 定位提示框
positionCustomTooltip2();
};
// 定位自定义提示框 - 优化版本
// 定位自定义提示框2 - 优化版本
const positionCustomTooltip2 = () => {
if (!customTooltip2Ref.current || !proxyGeoRef.current) return;
// 找到US点
@ -250,16 +289,25 @@ export const WorldGeo = memo(
) {
// 设置提示框位置
customTooltip2Ref.current.style.left = `${
screenCoord[0] - 626 + 20
screenCoord[0] - 626 + 53
}px`;
customTooltip2Ref.current.style.top = `${
screenCoord[1] + 40 - 218
screenCoord[1] + 40 - 222
}px`;
}
} catch (error) {
console.error("Error positioning tooltip:", error);
}
};
// 处理关闭tooltip2
const handleCloseTooltip2 = () => {
setShowTooltip2(false);
setTooltipClosed(false);
};
const getLineItem = (
preCode: string,
nextCode: string
@ -277,28 +325,25 @@ export const WorldGeo = memo(
},
];
};
const getLine = () => {
// 实现数据处理
const solidData: LinesType[] = [["main", []]]; // 使用"main"替代startCountry.country_code
// 收集需要显示涟漪效果的所有点(包括连线和不连线的)
const ripplePoints: any[] = [];
// 处理主路径数据
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 color = currentItem.color || "#0ea5e9"; // 获取颜色,如果没有则使用默认颜色
// 如果当前项是起点,下一项是终点
if (currentItem.type === "start" && nextItem.type === "end") {
const startCode = countryCode;
const endCode = nextItem.country_code.toUpperCase();
// 无论是否连线,都添加点的涟漪效果
const startPoint = createCountryRipple(startCode, color);
const endPoint = createCountryRipple(
@ -307,7 +352,6 @@ export const WorldGeo = memo(
);
if (startPoint) ripplePoints.push(startPoint);
if (endPoint) ripplePoints.push(endPoint);
// 检查是否应该绘制连线
if (currentItem.isLine !== false) {
const lineItem = getLineItem(startCode, endCode);
@ -316,14 +360,12 @@ export const WorldGeo = memo(
lineItem[1].color = nextItem.color || color;
solidData[0]?.[1].push(lineItem);
}
// 跳过下一项,因为已经处理了
i++;
}
// 常规情况:当前项到下一项
else {
const nextCountryCode = nextItem.country_code.toUpperCase();
// 无论是否连线,都添加点的涟漪效果
const currentPoint = createCountryRipple(countryCode, color);
const nextPoint = createCountryRipple(
@ -332,7 +374,6 @@ export const WorldGeo = memo(
);
if (currentPoint) ripplePoints.push(currentPoint);
if (nextPoint) ripplePoints.push(nextPoint);
// 检查是否应该绘制连线
if (currentItem.isLine !== false) {
const lineItem = getLineItem(countryCode, nextCountryCode);
@ -343,7 +384,6 @@ export const WorldGeo = memo(
}
}
}
// 虚线数据处理(保持原有逻辑)
const otherLineList: any = [];
return {
@ -352,6 +392,7 @@ export const WorldGeo = memo(
ripplePoints,
};
};
// 获取连线经纬度数据
const convertData = (data: LinesDataType[]) => {
const res = [];
@ -394,6 +435,7 @@ export const WorldGeo = memo(
// lineMidpointsRef.current = midpoints;
return res;
};
// 创建双层点效果 - 大点
const createDualLayerPoint = (
lastExit: LinesItemType,
@ -480,6 +522,7 @@ export const WorldGeo = memo(
} as echarts.SeriesOption,
];
};
// 添加新方法:根据经纬度数组创建蓝色涟漪小点(不包含白色内层点)
const createRipplePointsFromCoordinates = (
coordinates: [number, number][],
@ -511,6 +554,7 @@ export const WorldGeo = memo(
})),
} as echarts.SeriesOption);
};
// 创建路径点的双层效果
const createPathPoints = (
dataItems: LinesDataType[],
@ -593,6 +637,7 @@ export const WorldGeo = memo(
} as echarts.SeriesOption,
];
};
// 创建带自定义提示框的涟漪点
const createRipplePointsWithTooltip = (ripplePoints: any) => {
return {
@ -652,6 +697,7 @@ export const WorldGeo = memo(
})),
} as echarts.SeriesOption;
};
// 连线 series
const getLianData = (series: echarts.SeriesOption[]) => {
const { solidData, otherLineList, ripplePoints } = getLine();
@ -688,7 +734,6 @@ export const WorldGeo = memo(
const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
// 获取当前路径的颜色
const pathColor = item[1]?.[0]?.[0]?.color || "#0ea5e9"; // 从第一个点获取颜色,如果没有则使用默认颜色
// 添加飞行线
series.push({
name: item[0],
@ -716,11 +761,9 @@ export const WorldGeo = memo(
},
data: convertData(item[1]) as echarts.LinesSeriesOption["data"],
});
// 添加路径点的双层效果
const pathPoints = createPathPoints(item[1], true, pathColor);
series.push(...pathPoints);
// 添加出口节点的双层效果
if (lastExit) {
const exitNodes = createDualLayerPoint(lastExit, true, pathColor);
@ -732,7 +775,6 @@ export const WorldGeo = memo(
const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
// 获取当前路径的颜色
const pathColor = item[1]?.[0]?.[0]?.color || "#F0FFA2"; // 从第一个点获取颜色,如果没有则使用默认颜色
// 添加虚线
series.push({
name: item[0],
@ -906,6 +948,7 @@ export const WorldGeo = memo(
});
return series;
};
const getOption = () => {
const series: echarts.SeriesOption[] = [];
getLianData(series);
@ -917,14 +960,12 @@ export const WorldGeo = memo(
currentValue[0]?.authenticationPoint
) {
console.log(currentValue, "values");
createSpecialPoints(series); // 添加特殊点和飞线
createRipplePointsFromCoordinates(
currentValue[0]?.authenticationPoint || [],
series
);
}
const option = {
backgroundColor: "transparent",
// 全局提示框配置
@ -1000,6 +1041,7 @@ export const WorldGeo = memo(
};
return option;
};
// 创建DOM标签
const createDOMLabels = () => {
// 清除现有标签
@ -1065,6 +1107,7 @@ export const WorldGeo = memo(
// 更新标签位置
updateLabelPositions();
};
// 更新标签位置
const updateLabelPositions = () => {
if (!proxyGeoRef.current || !labelContainerRef.current) return;
@ -1081,10 +1124,20 @@ export const WorldGeo = memo(
}
});
};
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
@ -1095,6 +1148,7 @@ export const WorldGeo = memo(
// 创建DOM标签
setTimeout(createDOMLabels, 100);
}, [newHomeProxies, mainToData]);
useEffect(() => {
const chartDom = document.getElementById("screenGeo");
proxyGeoRef.current = echarts.init(chartDom);
@ -1121,34 +1175,68 @@ export const WorldGeo = memo(
proxyGeoRef.current = null;
};
}, []);
// 处理tooltip的显示和隐藏
useEffect(() => {
if (tooltipType !== "PASS_AUTHENTICATION") {
lineMidpointsRef.current = [];
}
if (tooltipClosed) {
if (tooltipType === "NESTED_ENCRYPTION") {
createCustomTooltip();
setShowTooltip1(true);
// 在下一个渲染周期后定位tooltip
setTimeout(() => {
positionCustomTooltip();
}, 0);
}
if (tooltipType === "TRAFFIC_OBFUSCATION") {
createCustomTooltip2();
setShowTooltip2(true);
// 在下一个渲染周期后定位tooltip
setTimeout(() => {
positionCustomTooltip2();
}, 0);
}
} else {
customTooltipRef.current?.remove();
customTooltip2Ref.current?.remove();
customTooltipRef.current = null;
customTooltip2Ref.current = null;
setShowTooltip1(false);
setShowTooltip2(false);
}
return () => {
customTooltipRef.current?.remove();
customTooltip2Ref.current?.remove();
customTooltipRef.current = null;
customTooltip2Ref.current = null;
};
}, [tooltipClosed, tooltipType, currentValue]);
// 在地图初始化后定位tooltip
useEffect(() => {
if (showTooltip1) {
positionCustomTooltip();
}
if (showTooltip2) {
positionCustomTooltip2();
}
}, [showTooltip1, showTooltip2]);
return (
<div className="flex-1 h-full flex flex-col">
<div id="screenGeo" className="flex-1"></div>
{/* 流量混淆提示框 */}
{showTooltip2 && (
<CustomTooltipLeft
logs={trafficObfuscationLogs}
onClose={handleCloseTooltip2}
tooltipRef={customTooltip2Ref}
title="流量混淆"
/>
)}
</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;
// }

View File

@ -81,11 +81,10 @@
font-weight: 500;
// line-height: 24px;
}
.tip-box {
.tip-box-left{
position: relative;
width: 626px;
height: 281px;
width: 600px;
height: 400px;
padding: 20.85px 20.353px;
background: rgba(0, 11.82, 33.10, 0.10);
border-radius: 8px;
@ -93,7 +92,30 @@
outline-offset: -0.46px;
backdrop-filter: blur(5.50px);
.close-icon , .close-icon2 {
}
.line-img-left {
width: 216.86px;
// margin-top: 30px;
right: -216.86px;
top: 60px;
position: absolute;
}
.tip-box-hx {
position: relative;
width: 600px;
height: 400px;
margin-left: 312.221px;
// min-height: 200px;
// max-height: 600px;
padding: 20.85px 20.353px;
background: rgba(0, 11.82, 33.10, 0.10);
border-radius: 8px;
outline: 0.46px solid white;
outline-offset: -0.46px;
backdrop-filter: blur(5.50px);
.close-icon,
.close-icon2 {
width: 16px;
height: 16px;
position: absolute;
@ -116,7 +138,7 @@
margin-left: 16px;
}
.traffic-obfuscation-img{
.traffic-obfuscation-img {
width: 597px;
height: 241px;
margin-left: 16px;
@ -127,85 +149,13 @@
position: relative;
display: flex;
.line-img {
.line-img-hx {
width: 312.221px;
}
.line-img-left{
width: 216.86px;
margin-top: 30px;
}
.fill {
width: 9.165px;
height: 9.165px;
border-radius: 50%;
background-color: #18E4FF;
// margin-top: 80px;
top: 80px;
left: 0px;
position: absolute;
left: 307.5px;
top: 77.5px;
z-index: 99;
}
.fill-left {
width: 9.165px;
height: 9.165px;
border-radius: 50%;
background-color: #18E4FF;
position: absolute;
right:210.5px;
top: 82.5px;
z-index: 99;
}
}
// // 轮播项目
// .carousel-item {
// flex: 0 0 auto;
// }
// // View Transitions 自定义样式
// @keyframes slide-from-right {
// from {
// transform: translateX(40px);
// opacity: 0;
// }
// }
// @keyframes slide-to-left {
// to {
// transform: translateX(-40px);
// opacity: 0;
// }
// }
// @keyframes slide-from-left {
// from {
// transform: translateX(-40px);
// opacity: 0;
// }
// }
// @keyframes slide-to-right {
// to {
// transform: translateX(40px);
// opacity: 0;
// }
// }
// // 自定义 View Transitions 动画
// ::view-transition-old(web3-item-1-4),
// ::view-transition-old(web3-item-2-4) {
// animation: 0.8s slide-to-left ease-in-out;
// }
// ::view-transition-new(web3-item-1-0),
// ::view-transition-new(web3-item-2-0) {
// animation: 0.8s slide-from-left ease-in-out;
// }
// // 确保过渡期间元素可见
// ::view-transition-group(*) {
// animation-duration: 0.8s;
// }

View File

@ -30,8 +30,6 @@ import FacebookSvg from "@/assets/svg/anti-forensics-forwarding/Facebook.svg?rea
import FacebookActiveSvg from "@/assets/svg/anti-forensics-forwarding/FacebookActive.svg?react";
import { RootState } from "@/store";
import "./index.scss";
import {
getApplicationDiversion,
@ -172,10 +170,17 @@ const AntiDarkAnalysisNetwork = () => {
const [selectedApp, setSelectedApp] = useState<any>(null);
const [dataInfo, setDataInfo] = useState<any>(null);
const [trafficObfuscationLogs, setTrafficObfuscationLogs] = useState<any>([
"初始化嵌套加密...",
"生成密钥对222...",
"应用第一层加密...",
"应用第二层加密...",
"应用第三层加密...",
"加密完成,准备传输...",
]);
const currentValue = useMemo(() => {
let value = dataInfo;
switch (tooltipType) {
case CONST_TOOLTIP_TYPE.APP_DIVERSION.type:
value = selectedApp ? [selectedApp] : [];
@ -184,7 +189,7 @@ const AntiDarkAnalysisNetwork = () => {
break;
}
return value;
}, [tooltipType, selectedApp,dataInfo]);
}, [tooltipType, selectedApp, dataInfo]);
const handleClickApp = (item: any) => {
setSelectedApp(item);
@ -201,6 +206,7 @@ const AntiDarkAnalysisNetwork = () => {
case CONST_TOOLTIP_TYPE.TRAFFIC_OBFUSCATION.type:
const trafficObfuscation = await getTrafficObfuscation();
value = [trafficObfuscation.data];
setTrafficObfuscationLogs(trafficObfuscation.logs);
break;
case CONST_TOOLTIP_TYPE.DYNAMIC_ROUTE_GENERATOR.type:
const dynamicRouteGeneration = await getDynamicRouteGeneration();
@ -219,7 +225,6 @@ const AntiDarkAnalysisNetwork = () => {
default:
break;
}
console.log(value,'valuevalue')
setDataInfo(value);
};
@ -227,7 +232,7 @@ const AntiDarkAnalysisNetwork = () => {
const appDiversion = useMemo(() => {
return Apps.map((item) => {
const findApp = appData.find(
(appItem:any) => item.name === appItem.name
(appItem: any) => item.name === appItem.name
);
return {
...item,
@ -245,7 +250,7 @@ const AntiDarkAnalysisNetwork = () => {
useEffect(() => {
getDataInfo();
},[tooltipType])
}, [tooltipType]);
useEffect(() => {
initData();
@ -276,6 +281,7 @@ const AntiDarkAnalysisNetwork = () => {
</div>
<div className="mt-2 w-full h-full flex-1">
<WorldGeo
trafficObfuscationLogs={trafficObfuscationLogs}
currentValue={currentValue}
newHomeProxies={newHomeProxies}
tooltipType={tooltipType}

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, memo, useState } from "react";
import { useEffect, useRef, memo, useState } from "react";
import * as echarts from "echarts";
// import 'echarts-gl';
// import { useQueryClient } from "@tanstack/react-query";
@ -16,6 +16,136 @@ interface LinesItemType {
}
type LinesDataType = [LinesItemType, LinesItemType];
type LinesType = [string, LinesDataType[]];
// 创建自定义提示框组件
const CustomTooltip = ({
logs,
onClose,
tooltipRef,
}: {
logs: string[];
onClose: () => void;
tooltipRef: React.RefObject<HTMLDivElement>;
}) => {
const [visibleLogs, setVisibleLogs] = useState<string[]>([]);
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) {
setVisibleLogs((prev) => [...prev, logs[currentIndex]]);
currentIndex++;
} else {
clearInterval(timer);
setIsComplete(true);
}
}, 500);
// 清理函数
return () => {
clearInterval(timer);
};
}, [logs]); // 当logs变化时重新开始动画
// 自动滚动到最新的日志
const logsContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (logsContainerRef.current && visibleLogs.length > 0) {
logsContainerRef.current.scrollTop =
logsContainerRef.current.scrollHeight;
}
}, [visibleLogs]);
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" }}
>
</div>
<img
className="close-icon"
src={getUrl("svg/Xwhite.svg")}
alt=""
style={{ cursor: "pointer" }}
onClick={onClose}
/>
</div>
<div
ref={logsContainerRef}
className="logs-container mt-3 max-h-[450px] overflow-y-auto"
>
{visibleLogs.length > 0 ? (
<ul className="logs-list space-y-1.5">
{visibleLogs.map((log, index) => (
<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 && logs.length > 0 && (
<div className="loading-indicator mt-2 text-xs text-blue-300">
...
</div>
)} */}
</div>
</div>
</div>
</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;
// }
// 创建单个国家的涟漪效果
const createCountryRipple = (countryCode: string, color?: string) => {
const coords = geoCoordMap[countryCode];
@ -27,13 +157,17 @@ const createCountryRipple = (countryCode: string, color?: string) => {
color: color || "#0ea5e9", // 添加颜色属性,如果没有则使用默认颜色
};
};
export const WorldGeo = memo(
({
nestedEncryption,
passAuthentication,
dynamicRouteGeneration,
tooltipClosed,
setTooltipClosed,
logs,
}: {
logs:any[];
nestedEncryption: any;
passAuthentication: any;
dynamicRouteGeneration: any;
@ -56,33 +190,38 @@ export const WorldGeo = memo(
>([]);
const labelContainerRef = useRef<HTMLDivElement | null>(null);
const labelsRef = useRef<HTMLDivElement[]>([]);
// 添加状态来跟踪当前显示的连线索引
const [nestedEncryptionLineIndex, setNestedEncryptionLineIndex] = useState(-1);
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 [nestedEncryptionLines, setNestedEncryptionLines] = useState<
{ from: string; to: string; color?: string }[]
>([]);
const [dynamicRouteLines, setDynamicRouteLines] = useState<
{ from: string; to: string; color?: string }[]
>([]);
// 添加状态来存储所有点
const [allPoints, setAllPoints] = useState<any[]>([]);
// 使用ref来跟踪动画状态避免重新渲染
const animationTimerRef = useRef<NodeJS.Timeout | null>(null);
const dynamicAnimationTimerRef = useRef<NodeJS.Timeout | null>(null);
// 添加状态来跟踪数据是否已经变化
const nestedEncryptionKeyRef = useRef<string>("");
const dynamicRouteKeyRef = useRef<string>("");
// 初始化时提取所有点的函数
const extractAllPoints = () => {
const points: any[] = [];
// console.log("Extracting points from nestedEncryption:", nestedEncryption);
// console.log("Extracting points from dynamicRouteGeneration:", dynamicRouteGeneration);
// 从嵌套加密数据中提取点
if (nestedEncryption && Array.isArray(nestedEncryption)) {
nestedEncryption.forEach((item: any) => {
@ -91,15 +230,18 @@ export const WorldGeo = memo(
// 添加起点到点集合
const fromCode = dataItem.country_code.toUpperCase();
const fromPoint = createCountryRipple(fromCode, item.color);
if (fromPoint && !points.some(p => p.country_code === fromCode)) {
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)) {
if (toPoint && !points.some((p) => p.country_code === toCode)) {
points.push(toPoint);
}
}
@ -107,7 +249,7 @@ export const WorldGeo = memo(
}
});
}
// 从动态路由数据中提取点
if (dynamicRouteGeneration && Array.isArray(dynamicRouteGeneration)) {
dynamicRouteGeneration.forEach((item: any) => {
@ -116,15 +258,18 @@ export const WorldGeo = memo(
// 添加起点到点集合
const fromCode = dataItem.country_code.toUpperCase();
const fromPoint = createCountryRipple(fromCode, item.color);
if (fromPoint && !points.some(p => p.country_code === fromCode)) {
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)) {
if (toPoint && !points.some((p) => p.country_code === toCode)) {
points.push(toPoint);
}
}
@ -132,11 +277,11 @@ export const WorldGeo = memo(
}
});
}
console.log("Extracted points:", points);
return points;
};
// 修改初始化逻辑,确保在数据变化时立即提取点
useEffect(() => {
// 提取所有点
@ -145,28 +290,33 @@ export const WorldGeo = memo(
setAllPoints(points);
}
}, [nestedEncryption, dynamicRouteGeneration]); // 监听数据变化
// 启动嵌套加密连线动画的函数
const startNestedEncryptionAnimation = (connections: {from: string, to: string, color?: string}[]) => {
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);
animationTimerRef.current = setTimeout(
animateNextLine,
LINE_ANIMATION_INTERVAL
);
}
};
// 开始动画
animateNextLine();
};
// 处理嵌套加密数据变化
useEffect(() => {
// 清除任何现有的动画定时器
@ -174,45 +324,52 @@ export const WorldGeo = memo(
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}[] = [];
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)) {
if (
fromPoint &&
!points.some((p) => p.country_code === fromCode)
) {
points.push(fromPoint);
if (!allExtractedPoints.some(p => p.country_code === fromCode)) {
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)) {
if (toPoint && !points.some((p) => p.country_code === toCode)) {
points.push(toPoint);
if (!allExtractedPoints.some(p => p.country_code === toCode)) {
if (
!allExtractedPoints.some((p) => p.country_code === toCode)
) {
allExtractedPoints.push(toPoint);
}
}
// 检查是否需要开始连线动画
if (item.isLine === true) {
connections.push({
from: fromCode,
to: toCode,
color: item.color
color: item.color,
});
shouldStartAnimation = true;
}
@ -220,19 +377,22 @@ export const WorldGeo = memo(
});
}
});
// 生成当前数据的唯一键
const currentKey = JSON.stringify(nestedEncryption);
// 检查数据是否变化
if (currentKey !== nestedEncryptionKeyRef.current || shouldStartAnimation) {
if (
currentKey !== nestedEncryptionKeyRef.current ||
shouldStartAnimation
) {
nestedEncryptionKeyRef.current = currentKey;
setNestedEncryptionLines(connections);
// 如果有连线数据且需要开始动画,重置索引并启动动画
if (connections.length > 0 && shouldStartAnimation) {
setNestedEncryptionLineIndex(-1); // 重置索引
// 启动连线动画
setTimeout(() => {
startNestedEncryptionAnimation(connections);
@ -243,17 +403,19 @@ export const WorldGeo = memo(
}
}
}
// 更新所有点
if (allExtractedPoints.length > 0) {
setAllPoints(prevPoints => {
setAllPoints((prevPoints) => {
const newPoints = [...prevPoints];
allExtractedPoints.forEach(point => {
if (!newPoints.some(p => p.country_code === point.country_code)) {
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);
const existingIndex = newPoints.findIndex(
(p) => p.country_code === point.country_code
);
if (existingIndex !== -1 && point.color) {
newPoints[existingIndex].color = point.color;
}
@ -263,7 +425,7 @@ export const WorldGeo = memo(
});
}
}, [nestedEncryption]);
// 处理动态路由数据变化
useEffect(() => {
// 清除任何现有的动画定时器
@ -271,45 +433,52 @@ export const WorldGeo = memo(
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}[] = [];
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)) {
if (
fromPoint &&
!points.some((p) => p.country_code === fromCode)
) {
points.push(fromPoint);
if (!allExtractedPoints.some(p => p.country_code === fromCode)) {
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)) {
if (toPoint && !points.some((p) => p.country_code === toCode)) {
points.push(toPoint);
if (!allExtractedPoints.some(p => p.country_code === toCode)) {
if (
!allExtractedPoints.some((p) => p.country_code === toCode)
) {
allExtractedPoints.push(toPoint);
}
}
// 检查是否需要开始连线动画
if (item.isLine === true) {
connections.push({
from: fromCode,
to: toCode,
color: item.color
color: item.color,
});
shouldStartAnimation = true;
}
@ -317,19 +486,19 @@ export const WorldGeo = memo(
});
}
});
// 生成当前数据的唯一键
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);
@ -340,17 +509,19 @@ export const WorldGeo = memo(
}
}
}
// 更新所有点
if (allExtractedPoints.length > 0) {
setAllPoints(prevPoints => {
setAllPoints((prevPoints) => {
const newPoints = [...prevPoints];
allExtractedPoints.forEach(point => {
if (!newPoints.some(p => p.country_code === point.country_code)) {
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);
const existingIndex = newPoints.findIndex(
(p) => p.country_code === point.country_code
);
if (existingIndex !== -1 && point.color) {
newPoints[existingIndex].color = point.color;
}
@ -360,28 +531,33 @@ export const WorldGeo = memo(
});
}
}, [dynamicRouteGeneration]);
// 启动动态路由连线动画的函数
const startDynamicRouteAnimation = (connections: {from: string, to: string, color?: string}[]) => {
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);
dynamicAnimationTimerRef.current = setTimeout(
animateNextLine,
LINE_ANIMATION_INTERVAL
);
}
};
// 开始动画
animateNextLine();
};
// 组件卸载时清除定时器
useEffect(() => {
return () => {
@ -395,7 +571,7 @@ export const WorldGeo = memo(
}
};
}, []);
const getLineItem = (
preCode: string,
nextCode: string,
@ -416,95 +592,57 @@ export const WorldGeo = memo(
},
];
};
const getLine = () => {
// 实现数据处理
const solidData: LinesType[] = []; // 不再使用单一数组,而是分开存储
// 处理嵌套加密连线 - 放入单独的数组
if (nestedEncryptionLineIndex >= 0 && nestedEncryptionLines.length > 0) {
const nestedLines: LinesDataType[] = [];
for (let i = 0; i <= nestedEncryptionLineIndex && i < nestedEncryptionLines.length; i++) {
for (
let i = 0;
i <= nestedEncryptionLineIndex && i < nestedEncryptionLines.length;
i++
) {
const connection = nestedEncryptionLines[i];
nestedLines.push(getLineItem(connection.from, connection.to, connection.color));
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++) {
for (
let i = 0;
i <= dynamicRouteLineIndex && i < dynamicRouteLines.length;
i++
) {
const connection = dynamicRouteLines[i];
dynamicLines.push(getLineItem(connection.from, connection.to, connection.color));
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 确保点始终显示
};
};
// 创建自定义提示框DOM元素
const createCustomTooltip = () => {
// 如果已经存在自定义提示框,则移除它
if (document.getElementById("custom-fixed-tooltip")) {
document.getElementById("custom-fixed-tooltip")?.remove();
}
// 创建自定义提示框
const tooltip = document.createElement("div");
tooltip.id = "custom-fixed-tooltip";
tooltip.style.position = "fixed";
tooltip.style.zIndex = "1000";
tooltip.style.pointerEvents = "auto";
tooltip.style.backgroundColor = "transparent";
tooltip.innerHTML = `
<div class="tooltip-content">
<img class="line-img" src="${getUrl(
"svg/anti-forensics-forwarding/Line.svg"
)}" alt="" />
<div class="fill"></div>
<div class="tip-box">
<div>
<div class="label" style="color: white; font-weight: bold;"></div>
<img class="close-icon" src="${getUrl(
"svg/Xwhite.svg"
)}" alt=""
style="cursor: pointer; " />
</div>
<img class="encryption-img"
}" src="${getUrl("image/nested-encryption.png")}" alt="" />
</div>
</div>
`;
// 添加到DOM
document.body.appendChild(tooltip);
customTooltipRef.current = tooltip;
// 添加关闭按钮事件
const closeButton = tooltip.querySelector(".close-icon");
if (closeButton) {
closeButton.addEventListener("click", () => {
setTooltipClosed(false);
tooltip.remove();
customTooltipRef.current = null;
});
}
// 定位提示框
positionCustomTooltip();
};
// 定位自定义提示框 - 优化版本
const positionCustomTooltip = () => {
if (!customTooltipRef.current || !proxyGeoRef.current) return;
@ -527,7 +665,13 @@ export const WorldGeo = memo(
console.error("Error positioning tooltip:", error);
}
};
// 处理关闭tooltip
const handleCloseTooltip = () => {
setTooltipClosed(false);
setTooltipClosed(false);
};
// 获取连线经纬度数据
const convertData = (data: LinesDataType[]) => {
const res = [];
@ -570,7 +714,7 @@ export const WorldGeo = memo(
// lineMidpointsRef.current = midpoints;
return res;
};
// 创建双层点效果 - 大点
const createDualLayerPoint = (
lastExit: LinesItemType,
@ -657,7 +801,7 @@ export const WorldGeo = memo(
} as echarts.SeriesOption,
];
};
// 添加新方法:根据经纬度数组创建蓝色涟漪小点(不包含白色内层点)
const createRipplePointsFromCoordinates = (
coordinates: [number, number][],
@ -689,7 +833,7 @@ export const WorldGeo = memo(
})),
} as echarts.SeriesOption);
};
// 创建路径点的双层效果
const createPathPoints = (
dataItems: LinesDataType[],
@ -772,7 +916,7 @@ export const WorldGeo = memo(
} as echarts.SeriesOption,
];
};
// 创建带自定义提示框的涟漪点
const createRipplePointsWithTooltip = (ripplePoints: any) => {
return {
@ -832,11 +976,11 @@ export const WorldGeo = memo(
})),
} as echarts.SeriesOption;
};
// 连线 series
const getLianData = (series: echarts.SeriesOption[]) => {
const { solidData, otherLineList, ripplePoints } = getLine();
// 如果有需要显示涟漪效果的点,添加它们
if (ripplePoints.length > 0) {
// 添加带自定义提示框的外层蓝色点
@ -862,17 +1006,17 @@ export const WorldGeo = memo(
})),
} as echarts.SeriesOption);
}
// 处理每个连线组
solidData.forEach((item) => {
// 如果没有连线数据,则跳过
if (item[1].length === 0) {
return;
}
// 为每条连线创建飞行线
const pathColor = item[0] === "nested" ? "#0ea5e9" : "#F0FFA2"; // 根据类型设置默认颜色
// 添加飞行线
series.push({
name: item[0],
@ -899,21 +1043,25 @@ export const WorldGeo = memo(
},
data: convertData(item[1]) as echarts.LinesSeriesOption["data"],
});
// 添加路径点的双层效果
const pathPoints = createPathPoints(item[1], true, pathColor);
series.push(...pathPoints);
// 添加出口节点的双层效果
item[1].forEach(lineData => {
item[1].forEach((lineData) => {
const lastExit = lineData[1];
if (lastExit) {
const exitNodes = createDualLayerPoint(lastExit, true, lastExit.color || pathColor);
const exitNodes = createDualLayerPoint(
lastExit,
true,
lastExit.color || pathColor
);
series.push(...exitNodes);
}
});
});
// 处理其他线(保持原有逻辑)
otherLineList.forEach((line: any) => {
line.forEach((item: any) => {
@ -948,10 +1096,10 @@ export const WorldGeo = memo(
}
});
});
return true;
};
// 创建A点和B点并添加飞线和标签
const createSpecialPoints = (series: echarts.SeriesOption[]) => {
// 定义点A和点B的坐标
@ -1094,7 +1242,7 @@ export const WorldGeo = memo(
});
return series;
};
const getOption = () => {
const series: echarts.SeriesOption[] = [];
getLianData(series);
@ -1184,7 +1332,7 @@ export const WorldGeo = memo(
};
return option;
};
// 创建DOM标签
const createDOMLabels = () => {
// 清除现有标签
@ -1250,7 +1398,7 @@ export const WorldGeo = memo(
// 更新标签位置
updateLabelPositions();
};
// 更新标签位置
const updateLabelPositions = () => {
if (!proxyGeoRef.current || !labelContainerRef.current) return;
@ -1267,19 +1415,21 @@ export const WorldGeo = memo(
}
});
};
const handleResize = () => {
proxyGeoRef.current?.resize();
updateLabelPositions();
positionCustomTooltip();
if (tooltipClosed) {
positionCustomTooltip();
}
};
// 更新图表
useEffect(() => {
const option = getOption();
proxyGeoRef.current?.setOption(option);
}, [nestedEncryptionLineIndex, dynamicRouteLineIndex, allPoints]); // 当连线索引或点变化时更新图表
useEffect(() => {
lineMidpointsRef.current = []; // 重置中点数据
const option = getOption();
@ -1287,7 +1437,7 @@ export const WorldGeo = memo(
// 创建DOM标签
setTimeout(createDOMLabels, 100);
}, [nestedEncryption, dynamicRouteGeneration, passAuthentication]);
useEffect(() => {
const chartDom = document.getElementById("screenGeo");
proxyGeoRef.current = echarts.init(chartDom);
@ -1295,13 +1445,13 @@ export const WorldGeo = memo(
"world",
worldGeoJson as unknown as Parameters<typeof echarts.registerMap>[1]
);
// 初始化时提取所有点
const initialPoints = extractAllPoints();
if (initialPoints.length > 0) {
setAllPoints(initialPoints);
}
const option = getOption();
option && proxyGeoRef.current?.setOption(option);
// 添加地图交互事件监听器
@ -1321,19 +1471,27 @@ export const WorldGeo = memo(
proxyGeoRef.current = null;
};
}, []);
// 在地图初始化后定位tooltip
useEffect(() => {
createCustomTooltip();
return () => {
customTooltipRef.current?.remove();
customTooltipRef.current = null;
};
}, []);
if (tooltipClosed) {
positionCustomTooltip();
}
}, [tooltipClosed, nestedEncryption]);
return (
<div className="flex-1 h-full flex flex-col">
<div id="screenGeo" className="flex-1"></div>
{tooltipClosed && (
<CustomTooltip
logs={
logs
}
onClose={handleCloseTooltip}
tooltipRef={customTooltipRef}
/>
)}
</div>
);
}
);
);

View File

@ -81,11 +81,24 @@
font-weight: 500;
// line-height: 24px;
}
.tip-box {
.tip-box-left{
position: relative;
width: 626px;
height: 281px;
width: 600px;
height: 400px;
padding: 20.85px 20.353px;
background: rgba(0, 11.82, 33.10, 0.10);
border-radius: 8px;
outline: 0.46px solid white;
outline-offset: -0.46px;
backdrop-filter: blur(5.50px);
}
.tip-box-hx,tip-box-left {
position: relative;
width: 600px;
height: 400px;
margin-left: 312.221px;
// min-height: 200px;
// max-height: 600px;
padding: 20.85px 20.353px;
background: rgba(0, 11.82, 33.10, 0.10);
border-radius: 8px;
@ -93,7 +106,8 @@
outline-offset: -0.46px;
backdrop-filter: blur(5.50px);
.close-icon , .close-icon2 {
.close-icon,
.close-icon2 {
width: 16px;
height: 16px;
position: absolute;
@ -116,7 +130,7 @@
margin-left: 16px;
}
.traffic-obfuscation-img{
.traffic-obfuscation-img {
width: 597px;
height: 241px;
margin-left: 16px;
@ -127,85 +141,18 @@
position: relative;
display: flex;
.line-img {
.line-img-hx {
width: 312.221px;
// margin-top: 80px;
top: 80px;
left: 0px;
position: absolute;
}
.line-img-left{
.line-img-left {
width: 216.86px;
margin-top: 30px;
}
.fill {
width: 9.165px;
height: 9.165px;
border-radius: 50%;
background-color: #18E4FF;
position: absolute;
left: 307.5px;
top: 77.5px;
z-index: 99;
}
.fill-left {
width: 9.165px;
height: 9.165px;
border-radius: 50%;
background-color: #18E4FF;
position: absolute;
right:210.5px;
top: 82.5px;
z-index: 99;
}
}
// // 轮播项目
// .carousel-item {
// flex: 0 0 auto;
// }
// // View Transitions 自定义样式
// @keyframes slide-from-right {
// from {
// transform: translateX(40px);
// opacity: 0;
// }
// }
// @keyframes slide-to-left {
// to {
// transform: translateX(-40px);
// opacity: 0;
// }
// }
// @keyframes slide-from-left {
// from {
// transform: translateX(-40px);
// opacity: 0;
// }
// }
// @keyframes slide-to-right {
// to {
// transform: translateX(40px);
// opacity: 0;
// }
// }
// // 自定义 View Transitions 动画
// ::view-transition-old(web3-item-1-4),
// ::view-transition-old(web3-item-2-4) {
// animation: 0.8s slide-to-left ease-in-out;
// }
// ::view-transition-new(web3-item-1-0),
// ::view-transition-new(web3-item-2-0) {
// animation: 0.8s slide-from-left ease-in-out;
// }
// // 确保过渡期间元素可见
// ::view-transition-group(*) {
// animation-duration: 0.8s;
// }

View File

@ -178,13 +178,7 @@ const DecentralizedElasticNetwork = () => {
let istrue = useRef(false);
const [nestedEncryption, setNestedEncryption] = useState<any>([]);
const [dynamicRouteGeneration, setDynamicRouteGeneration] = useState<any>([]);
// const [dataInfo, setDataInfo] = useState<any>({
// passAuthentication: {
// ...PASS_AUTHENTICATION,
// },
// nestedEncryption: [NESTED_ENCRYPTION],
// dynamicRouteGeneration: DYNAMIC_ROUTE_GENERATOR,
// });
const [logs, setLogs] = useState<any>([]);
const initData = async () => {
try {
@ -194,6 +188,7 @@ const DecentralizedElasticNetwork = () => {
nestedEncryption.data.isLine = false;
dynamicRouteGeneration.data[0].isLine = false;
setNestedEncryption([nestedEncryption.data]);
setLogs(nestedEncryption.logs);
setDynamicRouteGeneration(dynamicRouteGeneration.data);
setDataInfo({
nestedEncryption: [nestedEncryption.data],
@ -231,6 +226,7 @@ const DecentralizedElasticNetwork = () => {
<div className="decentralized w-full h-full flex flex-col relative">
<div className="mt-2 w-full h-full flex-1">
<WorldGeo
logs={logs}
nestedEncryption={nestedEncryption}
passAuthentication={dataInfo.passAuthentication}
dynamicRouteGeneration={dynamicRouteGeneration}

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, memo } from "react";
import { useEffect, useMemo, useRef, memo, useState } from "react";
import * as echarts from "echarts";
// import 'echarts-gl';
// import { useQueryClient } from "@tanstack/react-query";
@ -7,8 +7,7 @@ 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";
const planePathImg =
"image://data:image/svg+xml;charset=utf-8;base64,PHN2ZyB3aWR0aD0iNjciIGhlaWdodD0iMTAyIiB2aWV3Qm94PSIwIDAgNjcgMTAyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMF9mXzYxMTdfMjEyNDA3KSI+CjxwYXRoIGQ9Ik0zNC4yMTA5IDkxLjE4ODZMNTMuNjU3OCA0MC45NThDNTQuOTM4IDM3LjY1MTMgNTUuNzk4MyAzNC4xNTkyIDU1LjM1NjMgMzAuNjQxQzU0LjQzNTcgMjMuMzEyOCA1MC40Njg0IDExLjAyMDggMzQuMjExMiAxMS4wMjA4QzE5LjE5MDMgMTEuMDIwOCAxMy45MTEgMjEuNTE0NiAxMi4wNTU0IDI4Ljg5MTJDMTAuOTAxIDMzLjQ4MDYgMTEuOTkyNiAzOC4yMTg2IDEzLjgyMzEgNDIuNTgyN0wzNC4yMTA5IDkxLjE4ODZaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfNjExN18yMTI0MDcpIi8+CjwvZz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfZl82MTE3XzIxMjQwNyIgeD0iMC44OTE3NDQiIHk9IjAuMzMxOTkiIHdpZHRoPSI2NS4yNzA3IiBoZWlnaHQ9IjEwMS41NDUiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iNS4zNDQ0MSIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzYxMTdfMjEyNDA3Ii8+CjwvZmlsdGVyPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNjExN18yMTI0MDciIHgxPSIzNS4yODI2IiB5MT0iMTAuODU2NCIgeDI9IjM1LjI4MjYiIHkyPSI4Ni44NTY0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiMwMEYyRkYiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTUwMEZGIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==";
interface LinesItemType {
name: string;
country_code: string;
@ -17,6 +16,316 @@ interface LinesItemType {
}
type LinesDataType = [LinesItemType, LinesItemType];
type LinesType = [string, LinesDataType[]];
// 创建右侧自定义提示框组件
const CustomTooltip = ({
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("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-[450px] 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">
...
</div>
)}
{!isComplete && filteredLogs.length > 0 && (
<div className="loading-indicator mt-2 text-xs text-blue-300">
...
</div>
)}
</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-[450px] 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">
...
</div>
)}
{!isComplete && filteredLogs.length > 0 && (
<div className="loading-indicator mt-2 text-xs text-blue-300">
...
</div>
)}
</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];
@ -28,6 +337,7 @@ const createCountryRipple = (countryCode: string, color?: string) => {
color: color, // 添加颜色属性
};
};
export const WorldGeo = memo(
({
dataInfo,
@ -59,6 +369,40 @@ export const WorldGeo = memo(
>([]);
const labelContainerRef = useRef<HTMLDivElement | null>(null);
const labelsRef = useRef<HTMLDivElement[]>([]);
// 添加状态来控制是否显示tooltip
const [showTooltip1, setShowTooltip1] = useState(false);
const [showTooltip2, setShowTooltip2] = useState(false);
// 模拟日志数据
const [nestedEncryptionLogs] = useState<string[]>([
"初始化嵌套加密...",
"生成密钥对...",
"应用第一层加密...",
"应用第二层加密...",
"应用第三层加密...",
"加密完成,准备传输...",
]);
const [trafficObfuscationLogs] = useState<string[]>([
"初始化流量混淆...",
"分析流量特征...",
"应用随机填充...",
"调整数据包时间间隔...",
"模拟HTTP流量...",
"混淆完成,准备传输...",
]);
// 添加调试日志
useEffect(() => {
console.log("Tooltip state:", {
tooltipClosed,
tooltipType,
showTooltip1,
showTooltip2,
});
}, [tooltipClosed, tooltipType, showTooltip1, showTooltip2]);
const mainToData = useMemo(() => {
const newList = [
dataInfo.passAuthentication,
@ -71,7 +415,7 @@ export const WorldGeo = memo(
selectedApp && selectedApp ? [...newList, selectedApp] : newList ?? [];
// 初始化数据数组 - 不再包含 startCountry
const data: any = [];
console.log(proxiesList,'proxiesList')
console.log(proxiesList, "proxiesList");
// 遍历代理列表
proxiesList.forEach((proxyItem: any) => {
// 检查是否有数据数组
@ -107,165 +451,96 @@ export const WorldGeo = memo(
});
return data;
}, [dataInfo, selectedApp]);
// 创建自定义提示框DOM元素
const createCustomTooltip = () => {
console.log("createCustomTooltip")
// 如果已经存在自定义提示框,则移除它
if (document.getElementById("custom-fixed-tooltip")) {
document.getElementById("custom-fixed-tooltip")?.remove();
}
// 创建自定义提示框
const tooltip = document.createElement("div");
tooltip.id = "custom-fixed-tooltip";
tooltip.style.position = "fixed";
tooltip.style.zIndex = "1000";
tooltip.style.pointerEvents = "auto";
tooltip.style.backgroundColor = "transparent";
// 设置提示框内容
const currentTooltipType =
CONST_TOOLTIP_TYPE[tooltipType as keyof typeof CONST_TOOLTIP_TYPE] ||
CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION;
tooltip.innerHTML = `
<div class="tooltip-content">
<img class="line-img" src="${getUrl(
"svg/anti-forensics-forwarding/Line.svg"
)}" alt="" />
<div class="fill"></div>
<div class="tip-box">
<div>
<div class="label" style="color: white; font-weight: bold;">${
currentTooltipType.title
}</div>
<img class="close-icon" src="${getUrl(
"svg/Xwhite.svg"
)}" alt=""
style="cursor: pointer; " />
</div>
<img class="${
CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.type ===
currentTooltipType.type
? "encryption-img"
: "traffic-obfuscation-img"
}" src="${getUrl(
CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.type === currentTooltipType.type
? "image/nested-encryption.png"
: "image/traffic-obfuscation.png"
)}" alt="" />
</div>
</div>
`;
// 添加到DOM
document.body.appendChild(tooltip);
customTooltipRef.current = tooltip;
// 添加关闭按钮事件
const closeButton = tooltip.querySelector(".close-icon");
if (closeButton) {
closeButton.addEventListener("click", () => {
setTooltipClosed(false);
tooltip.remove();
customTooltipRef.current = null;
});
}
// 定位提示框
positionCustomTooltip();
};
// 定位自定义提示框 - 优化版本
const positionCustomTooltip = () => {
console.log("Positioning tooltip1", {
hasRef: !!customTooltipRef.current,
hasChart: !!proxyGeoRef.current,
});
if (!customTooltipRef.current || !proxyGeoRef.current) return;
// 找到US点
// 找到点
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
) {
// 设置提示框位置
customTooltipRef.current.style.left = `${screenCoord[0] + 232 + 7}px`;
customTooltipRef.current.style.top = `${screenCoord[1] + 40 + 15}px`;
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 tooltip:", error);
console.error("Error positioning tooltip1:", error);
}
};
// 创建自定义提示框DOM元素
const createCustomTooltip2 = () => {
// 如果已经存在自定义提示框,则移除它
if (document.getElementById("custom-fixed-tooltip2")) {
document.getElementById("custom-fixed-tooltip2")?.remove();
}
// 创建自定义提示框
const tooltip = document.createElement("div");
tooltip.id = "custom-fixed-tooltip2";
tooltip.style.position = "fixed";
tooltip.style.zIndex = "1000";
tooltip.style.pointerEvents = "auto";
tooltip.style.backgroundColor = "transparent";
tooltip.innerHTML = `
<div class="tooltip-content">
<div class="fill-left"></div>
<div class="tip-box">
<div>
<div class="label" style="color: white; font-weight: bold;"></div>
<img class="close-icon2" src="${getUrl(
"svg/Xwhite.svg"
)}" alt=""
style="cursor: pointer; " />
</div>
<img class="traffic-obfuscation-img" src="${getUrl(
"image/traffic-obfuscation.png"
)}" alt="" />
</div>
<img class="line-img-left" src="${getUrl(
"svg/anti-forensics-forwarding/LineLeft.svg"
)}" alt="" />
</div>
`;
// 添加到DOM
document.body.appendChild(tooltip);
customTooltip2Ref.current = tooltip;
// 添加关闭按钮事件
const closeButton = tooltip.querySelector(".close-icon2");
if (closeButton) {
closeButton.addEventListener("click", () => {
setTooltipClosed(false);
customTooltip2Ref.current?.remove();
customTooltip2Ref.current = null;
});
}
// 定位提示框
positionCustomTooltip2();
};
// 定位自定义提示框 - 优化版本
// 定位自定义提示框2 - 优化版本
const positionCustomTooltip2 = () => {
console.log("Positioning tooltip2", {
hasRef: !!customTooltip2Ref.current,
hasChart: !!proxyGeoRef.current,
});
if (!customTooltip2Ref.current || !proxyGeoRef.current) return;
// 找到US点
// 找到点
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
) {
// 设置提示框位置
customTooltip2Ref.current.style.left = `${
screenCoord[0] - 626 + 20
}px`;
customTooltip2Ref.current.style.top = `${
screenCoord[1] + 40 - 13
}px`;
const left = `${screenCoord[0] - 626 + 20}px`;
const top = `${screenCoord[1] + 40 - 13}px`;
console.log("Setting tooltip2 position:", { left, top });
customTooltip2Ref.current.style.left = left;
customTooltip2Ref.current.style.top = top;
}
} catch (error) {
console.error("Error positioning tooltip:", 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,
@ -286,6 +561,7 @@ export const WorldGeo = memo(
},
];
};
const getLine = () => {
// 实现数据处理
const solidData: LinesType[] = [["main", []]]; // 使用"main"替代startCountry.country_code
@ -345,7 +621,11 @@ export const WorldGeo = memo(
}
// 检查是否应该绘制连线
if (currentItem.isLine !== false) {
const lineItem = getLineItem(countryCode, nextCountryCode, lineColor);
const lineItem = getLineItem(
countryCode,
nextCountryCode,
lineColor
);
solidData[0][1].push(lineItem);
}
}
@ -359,6 +639,7 @@ export const WorldGeo = memo(
pointColors,
};
};
// 获取连线经纬度数据
const convertData = (data: LinesDataType[]) => {
const res = [];
@ -371,7 +652,7 @@ export const WorldGeo = memo(
const toCountry = dataIndex?.[1]?.country_code ?? "";
// 使用每条线自己的颜色
const lineColor = dataIndex?.[0]?.color || "#0ea5e9";
if (fromCoord && toCoord) {
res.push({
coords: [fromCoord, toCoord],
@ -379,7 +660,7 @@ export const WorldGeo = memo(
color: lineColor, // 使用自定义颜色
},
// 保存颜色信息用于飞行特效
color: lineColor
color: lineColor,
});
// 计算中点,考虑曲线的弧度
const curveness = -0.4; // 与飞线弧度相同
@ -405,6 +686,7 @@ export const WorldGeo = memo(
// lineMidpointsRef.current = midpoints;
return res;
};
// 创建双层点效果 - 大点
const createDualLayerPoint = (
lastExit: LinesItemType,
@ -412,7 +694,7 @@ export const WorldGeo = memo(
) => {
// 使用点自己的颜色
const pointColor = lastExit.color || "#0ea5e9";
// 创建数据数组,用于两个散点图层
const pointData = lastExit
? [lastExit].map((v) => {
@ -421,7 +703,7 @@ export const WorldGeo = memo(
value: v.value,
datas: {
country_code: v.country_code,
color: pointColor // 保存颜色信息
color: pointColor, // 保存颜色信息
},
};
})
@ -437,9 +719,9 @@ export const WorldGeo = memo(
coordinateSystem: "geo",
zlevel: 3,
itemStyle: {
color: function(params: any) {
color: function (params: any) {
return params.data.datas.color;
}
},
},
symbol: "circle",
symbolSize: outerSize,
@ -493,6 +775,7 @@ export const WorldGeo = memo(
} as echarts.SeriesOption,
];
};
// 添加新方法:根据经纬度数组创建蓝色涟漪小点(不包含白色内层点)
const createRipplePointsFromCoordinates = (
coordinates: [number, number][],
@ -500,7 +783,7 @@ export const WorldGeo = memo(
color: string = "#01FF5E"
) => {
if (!coordinates || coordinates.length === 0) return;
// 只创建外层带涟漪效果的点
series.push({
type: "effectScatter",
@ -524,6 +807,7 @@ export const WorldGeo = memo(
})),
} as echarts.SeriesOption);
};
// 创建路径点的双层效果
const createPathPoints = (
dataItems: LinesDataType[],
@ -533,13 +817,13 @@ export const WorldGeo = memo(
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 // 保存颜色信息
color: pointColor, // 保存颜色信息
},
};
});
@ -606,6 +890,7 @@ export const WorldGeo = memo(
} as echarts.SeriesOption,
];
};
// 创建带自定义提示框的涟漪点
const createRipplePointsWithTooltip = (ripplePoints: any) => {
return {
@ -662,6 +947,7 @@ export const WorldGeo = memo(
})),
} as echarts.SeriesOption;
};
// 连线 series
const getLianData = (series: echarts.SeriesOption[]) => {
const { solidData, otherLineList, ripplePoints } = getLine();
@ -694,13 +980,13 @@ export const WorldGeo = memo(
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}`,
@ -726,13 +1012,15 @@ export const WorldGeo = memo(
opacity: 0.1,
color: lineColor, // 使用线段自己的颜色
},
data: convertData([[fromPoint, toPoint]]) as echarts.LinesSeriesOption["data"],
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);
@ -740,7 +1028,7 @@ export const WorldGeo = memo(
}
});
});
otherLineList.forEach((line: any) => {
line.forEach((item: any) => {
// 处理每条虚线段
@ -748,7 +1036,7 @@ export const WorldGeo = memo(
const fromPoint = lineSegment[0];
const toPoint = lineSegment[1];
const lineColor = fromPoint.color || "#F0FFA2";
// 添加虚线
series.push({
name: `${item[0]}-dashed-${index}`,
@ -765,13 +1053,15 @@ export const WorldGeo = memo(
width: 0.5, // 飞线宽度
opacity: 0.6,
},
data: convertData([[fromPoint, toPoint]]) as echarts.LinesSeriesOption["data"],
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);
@ -780,9 +1070,10 @@ export const WorldGeo = memo(
});
});
});
return true;
};
// 创建A点和B点并添加飞线和标签
const createSpecialPoints = (series: echarts.SeriesOption[]) => {
// 定义点A和点B的坐标
@ -926,6 +1217,7 @@ export const WorldGeo = memo(
});
return series;
};
const getOption = () => {
const series: echarts.SeriesOption[] = [];
getLianData(series);
@ -1015,6 +1307,7 @@ export const WorldGeo = memo(
};
return option;
};
// 创建DOM标签
const createDOMLabels = () => {
// 清除现有标签
@ -1079,6 +1372,7 @@ export const WorldGeo = memo(
// 更新标签位置
updateLabelPositions();
};
// 更新标签位置
const updateLabelPositions = () => {
if (!proxyGeoRef.current || !labelContainerRef.current) return;
@ -1095,10 +1389,20 @@ export const WorldGeo = memo(
}
});
};
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
@ -1109,6 +1413,7 @@ export const WorldGeo = memo(
// 创建DOM标签
setTimeout(createDOMLabels, 100);
}, [dataInfo, mainToData]);
useEffect(() => {
const chartDom = document.getElementById("screenGeo");
proxyGeoRef.current = echarts.init(chartDom);
@ -1135,27 +1440,81 @@ export const WorldGeo = memo(
proxyGeoRef.current = null;
};
}, []);
// 修改处理tooltip的显示和隐藏的逻辑
useEffect(() => {
console.log("Tooltip effect triggered:", { tooltipClosed, tooltipType });
if (tooltipClosed) {
createCustomTooltip();
createCustomTooltip2();
if (tooltipType === "NESTED_ENCRYPTION") {
setShowTooltip1(true);
setShowTooltip2(false); // 确保另一个是关闭的
// 在下一个渲染周期后定位tooltip
setTimeout(() => {
positionCustomTooltip();
}, 0);
} else if (tooltipType === "TRAFFIC_OBFUSCATION") {
setShowTooltip1(false); // 确保另一个是关闭的
setShowTooltip2(true);
// 在下一个渲染周期后定位tooltip
setTimeout(() => {
positionCustomTooltip2();
}, 0);
}
} else {
setShowTooltip1(false);
setShowTooltip2(false);
}
return () => {
customTooltipRef.current?.remove();
customTooltip2Ref.current?.remove();
customTooltipRef.current = null;
customTooltip2Ref.current = null;
};
}, [
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>
{/* 嵌套加密提示框 */}
<CustomTooltip
logs={nestedEncryptionLogs}
onClose={handleCloseTooltip1}
tooltipRef={customTooltipRef}
title={CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.title}
imageSrc={getUrl("image/nested-encryption.png")}
/>
{/* 流量混淆提示框 - 确保条件正确 */}
<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;
// }

View File

@ -1,39 +1,5 @@
.proxies {
// .proxies-container {
// &_country {
// padding: 16px;
// border-radius: 8px;
// border: 1px solid #DCDFEA;
// background: #FFF;
// // box-shadow: 0px 1px 3px 0px rgba(16, 24, 40, 0.10), 0px 1px 2px 0px rgba(16, 24, 40, 0.06);
// }
// &::-webkit-scrollbar {
// width: 0px;
// height: 0px;
// /* background-color: red; */
// }
// &::-webkit-scrollbar-thumb {
// border-radius: 15px;
// background-color: rgba(144, 147, 153, 0.3);
// }
// &::-webkit-scrollbar-thumb:hover {
// background-color: rgba(144, 147, 153, 0.5);
// }
// & {
// /* Firefox */
// scrollbar-width: none;
// /* auto, thin, none */
// scrollbar-color: rgba(144, 147, 153, 0.5);
// }
// }
}
.proxies {}
.custom-font {
font-family: Arial, sans-serif;
}
}