feat:新增一些日志功能
This commit is contained in:
parent
a9d30c05c8
commit
6357dfb24c
@ -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 */
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
@ -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;
|
||||
// }
|
||||
@ -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;
|
||||
// }
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
);
|
||||
|
||||
@ -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;
|
||||
// }
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
// }
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user