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 {
|
::selection {
|
||||||
// background-color: #18181b;
|
// background-color: #18181b;
|
||||||
background-color: #1E3A8A;
|
background-color: #1E3A8A;
|
||||||
@ -17,7 +31,7 @@ body {
|
|||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
/* background-color: red; */
|
// background-color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ::-webkit-scrollbar-track {
|
/* ::-webkit-scrollbar-track {
|
||||||
@ -103,4 +117,4 @@ body {
|
|||||||
/* Internet Explorer/Edge (旧版) */
|
/* Internet Explorer/Edge (旧版) */
|
||||||
user-select: none;
|
user-select: none;
|
||||||
/* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */
|
/* 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 ChevronDownSvg from "@/assets/svg/layout/chevron-down.svg?react";
|
||||||
import Decentralized from "@/assets/svg/layout/decentralized.svg?react";
|
import Decentralized from "@/assets/svg/layout/decentralized.svg?react";
|
||||||
import PoolSvg from '@/assets/svg/layout/pool.svg?react'
|
import PoolSvg from "@/assets/svg/layout/pool.svg?react";
|
||||||
import HomeSvg from '@/assets/svg/layout/home.svg?react'
|
import HomeSvg from "@/assets/svg/layout/home.svg?react";
|
||||||
import AntiDarkAnalysisNetworkSvg from '@/assets/svg/layout/anti-dark-analysis-network.svg?react'
|
import AntiDarkAnalysisNetworkSvg from "@/assets/svg/layout/anti-dark-analysis-network.svg?react";
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
|
|
||||||
import type { RootState } from "@/store";
|
import type { RootState } from "@/store";
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const [_, setActive] = useState(0);
|
const [_, setActive] = useState(0);
|
||||||
const { coreVersion } = useSelector(
|
const { coreVersion } = useSelector(
|
||||||
(state: RootState) => state.serviceReducer
|
(state: RootState) => state.serviceReducer
|
||||||
);
|
);
|
||||||
|
|
||||||
const navList = [
|
const navList = [
|
||||||
{
|
{
|
||||||
id: "new-home",
|
id: "new-home",
|
||||||
title: "首页",
|
title: "首页",
|
||||||
icon: <HomeSvg className="w-5 h-5" />,
|
icon: <HomeSvg className="w-5 h-5" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "home",
|
id: "home",
|
||||||
title: "去中心化的弹性网络",
|
title: "去中心化的弹性网络",
|
||||||
icon: <Decentralized className="w-5 h-5" />,
|
icon: <Decentralized className="w-5 h-5" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "anti-forensics-forwarding",
|
id: "anti-forensics-forwarding",
|
||||||
title: "面向溯源对抗的数据转发",
|
title: "面向溯源对抗的数据转发",
|
||||||
icon: <PoolSvg className="w-5 h-5" />,
|
icon: <PoolSvg className="w-5 h-5" />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "anti-dark-analysis-network",
|
id: "anti-dark-analysis-network",
|
||||||
title: "抗暗特征分析的隐匿网络应用",
|
title: "抗暗特征分析的隐匿网络应用",
|
||||||
icon: <AntiDarkAnalysisNetworkSvg className="w-5 h-5" />,
|
icon: <AntiDarkAnalysisNetworkSvg className="w-5 h-5" />,
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// id: 'proxies',
|
// id: 'proxies',
|
||||||
// title: '节点池',
|
// title: '节点池',
|
||||||
// icon: <PoolSvg className="w-5 h-5" />,
|
// icon: <PoolSvg className="w-5 h-5" />,
|
||||||
// },
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleClickMenu = (index: number) => {
|
const handleClickMenu = (index: number) => {
|
||||||
setActive(index);
|
setActive(index);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-tauri-drag-region className="layout flex">
|
<div data-tauri-drag-region className="layout flex">
|
||||||
<div
|
<div
|
||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className={cn(
|
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",
|
"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"
|
isMac && "mt-6"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<header className="flex items-center">
|
<header className="flex items-center">
|
||||||
<LogoSvg className="w-8 h-8" />
|
<LogoSvg className="w-8 h-8" />
|
||||||
<div className="ml-[9px] flex flex-col items-center justify-center">
|
<div className="ml-[9px] flex flex-col items-center justify-center">
|
||||||
<TitleSvg />
|
<TitleSvg />
|
||||||
<AnonymousSvg />
|
<AnonymousSvg />
|
||||||
{/* <span className="text-white text-[18px] font-bold tracking-wide">匿名反溯源网络系统</span> */}
|
{/* <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> */}
|
{/* <span className="text-white text-[8px] font-medium">Anonymous anti traceability network system</span> */}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<nav className="flex flex-col flex-1 gap-y-2">
|
<nav className="flex flex-col flex-1 gap-y-2">
|
||||||
{navList.map((item, index) => {
|
{navList.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={item.id}
|
key={item.id}
|
||||||
to={"/" + item.id}
|
to={"/" + item.id}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
cn(
|
cn(
|
||||||
"pl-[11px] py-2 flex items-center gap-2 rounded text-white text-sm",
|
"pl-[11px] py-2 flex items-center gap-2 rounded text-white text-sm",
|
||||||
isActive && "bg-[#213265] "
|
isActive && "bg-[#213265] "
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onClick={() => handleClickMenu(index)}
|
onClick={() => handleClickMenu(index)}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
<span>{item.title}</span>
|
<span>{item.title}</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
<footer className="h-[55px] p-4 items-center gap-2 inline-flex absolute left-4 bottom-[20px]">
|
<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 className="w-[156px] text-white/40 text-sm font-normal leading-tight">
|
||||||
<div>版本:{coreVersion || "v0.0.1"}</div>
|
<div>版本:{coreVersion || "v0.0.1"}</div>
|
||||||
{/* <div>环境:DEV</div> */}
|
{/* <div>环境:DEV</div> */}
|
||||||
</div>
|
</div>
|
||||||
<ChevronDownSvg fill="rgba(255, 255, 255, 0.4)" />
|
<ChevronDownSvg fill="rgba(255, 255, 255, 0.4)" />
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<main className="flex-1 bg-white mt-10 mb-1 mr-1 rounded-xl overflow-hidden min-w-[1693px]">
|
<main className="flex-1 bg-white mt-10 mb-1 mr-1 rounded-xl overflow-y-hidden overflow-x-auto w-full scrollbar-visible">
|
||||||
<Outlet />
|
<div className="min-w-[1693px] h-full">
|
||||||
</main>
|
<Outlet />
|
||||||
</div>
|
</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 * as echarts from "echarts";
|
||||||
// import 'echarts-gl';
|
// import 'echarts-gl';
|
||||||
// import { useQueryClient } from "@tanstack/react-query";
|
// 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 worldGeoJson from "@/assets/echarts-map/json/world.json";
|
||||||
import { geoCoordMap, countryNameMap, countryCodeMap } from "@/data";
|
import { geoCoordMap, countryNameMap, countryCodeMap } from "@/data";
|
||||||
import { getUrl } from "@/lib/utils";
|
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 {
|
interface LinesItemType {
|
||||||
name: string;
|
name: string;
|
||||||
country_code: string;
|
country_code: string;
|
||||||
@ -17,6 +16,147 @@ interface LinesItemType {
|
|||||||
}
|
}
|
||||||
type LinesDataType = [LinesItemType, LinesItemType];
|
type LinesDataType = [LinesItemType, LinesItemType];
|
||||||
type LinesType = [string, LinesDataType[]];
|
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 createCountryRipple = (countryCode: string, color?: string) => {
|
||||||
const coords = geoCoordMap[countryCode];
|
const coords = geoCoordMap[countryCode];
|
||||||
@ -28,6 +168,7 @@ const createCountryRipple = (countryCode: string, color?: string) => {
|
|||||||
color: color || "#0ea5e9", // 添加颜色属性,如果没有则使用默认颜色
|
color: color || "#0ea5e9", // 添加颜色属性,如果没有则使用默认颜色
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorldGeo = memo(
|
export const WorldGeo = memo(
|
||||||
({
|
({
|
||||||
currentValue,
|
currentValue,
|
||||||
@ -35,11 +176,13 @@ export const WorldGeo = memo(
|
|||||||
tooltipType,
|
tooltipType,
|
||||||
tooltipClosed,
|
tooltipClosed,
|
||||||
setTooltipClosed,
|
setTooltipClosed,
|
||||||
|
trafficObfuscationLogs,
|
||||||
}: {
|
}: {
|
||||||
currentValue: any;
|
currentValue: any;
|
||||||
newHomeProxies: any;
|
newHomeProxies: any;
|
||||||
tooltipType: string;
|
tooltipType: string;
|
||||||
tooltipClosed: boolean;
|
tooltipClosed: boolean;
|
||||||
|
trafficObfuscationLogs:any;
|
||||||
setTooltipClosed: (value: boolean) => void;
|
setTooltipClosed: (value: boolean) => void;
|
||||||
}) => {
|
}) => {
|
||||||
// const queryClient = useQueryClient()
|
// const queryClient = useQueryClient()
|
||||||
@ -59,6 +202,13 @@ export const WorldGeo = memo(
|
|||||||
>([]);
|
>([]);
|
||||||
const labelContainerRef = useRef<HTMLDivElement | null>(null);
|
const labelContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const labelsRef = useRef<HTMLDivElement[]>([]);
|
const labelsRef = useRef<HTMLDivElement[]>([]);
|
||||||
|
|
||||||
|
// 添加状态来控制是否显示tooltip
|
||||||
|
const [showTooltip1, setShowTooltip1] = useState(false);
|
||||||
|
const [showTooltip2, setShowTooltip2] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const mainToData = useMemo(() => {
|
const mainToData = useMemo(() => {
|
||||||
// 使用新的数据结构
|
// 使用新的数据结构
|
||||||
const proxiesList = currentValue ?? [];
|
const proxiesList = currentValue ?? [];
|
||||||
@ -99,69 +249,7 @@ export const WorldGeo = memo(
|
|||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
}, [currentValue]);
|
}, [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 = () => {
|
const positionCustomTooltip = () => {
|
||||||
if (!customTooltipRef.current || !proxyGeoRef.current) return;
|
if (!customTooltipRef.current || !proxyGeoRef.current) return;
|
||||||
@ -184,57 +272,8 @@ export const WorldGeo = memo(
|
|||||||
console.error("Error positioning tooltip:", error);
|
console.error("Error positioning tooltip:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// 创建自定义提示框DOM元素
|
|
||||||
const createCustomTooltip2 = () => {
|
// 定位自定义提示框2 - 优化版本
|
||||||
// 如果已经存在自定义提示框,则移除它
|
|
||||||
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();
|
|
||||||
};
|
|
||||||
// 定位自定义提示框 - 优化版本
|
|
||||||
const positionCustomTooltip2 = () => {
|
const positionCustomTooltip2 = () => {
|
||||||
if (!customTooltip2Ref.current || !proxyGeoRef.current) return;
|
if (!customTooltip2Ref.current || !proxyGeoRef.current) return;
|
||||||
// 找到US点
|
// 找到US点
|
||||||
@ -250,16 +289,25 @@ export const WorldGeo = memo(
|
|||||||
) {
|
) {
|
||||||
// 设置提示框位置
|
// 设置提示框位置
|
||||||
customTooltip2Ref.current.style.left = `${
|
customTooltip2Ref.current.style.left = `${
|
||||||
screenCoord[0] - 626 + 20
|
screenCoord[0] - 626 + 53
|
||||||
}px`;
|
}px`;
|
||||||
customTooltip2Ref.current.style.top = `${
|
customTooltip2Ref.current.style.top = `${
|
||||||
screenCoord[1] + 40 - 218
|
screenCoord[1] + 40 - 222
|
||||||
}px`;
|
}px`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error positioning tooltip:", error);
|
console.error("Error positioning tooltip:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 处理关闭tooltip2
|
||||||
|
const handleCloseTooltip2 = () => {
|
||||||
|
setShowTooltip2(false);
|
||||||
|
setTooltipClosed(false);
|
||||||
|
};
|
||||||
|
|
||||||
const getLineItem = (
|
const getLineItem = (
|
||||||
preCode: string,
|
preCode: string,
|
||||||
nextCode: string
|
nextCode: string
|
||||||
@ -277,28 +325,25 @@ export const WorldGeo = memo(
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLine = () => {
|
const getLine = () => {
|
||||||
// 实现数据处理
|
// 实现数据处理
|
||||||
const solidData: LinesType[] = [["main", []]]; // 使用"main"替代startCountry.country_code
|
const solidData: LinesType[] = [["main", []]]; // 使用"main"替代startCountry.country_code
|
||||||
// 收集需要显示涟漪效果的所有点(包括连线和不连线的)
|
// 收集需要显示涟漪效果的所有点(包括连线和不连线的)
|
||||||
const ripplePoints: any[] = [];
|
const ripplePoints: any[] = [];
|
||||||
|
|
||||||
// 处理主路径数据
|
// 处理主路径数据
|
||||||
for (let i = 0; i < mainToData.length; i++) {
|
for (let i = 0; i < mainToData.length; i++) {
|
||||||
// 如果是最后一个元素,则跳过(因为没有下一个元素作为终点)
|
// 如果是最后一个元素,则跳过(因为没有下一个元素作为终点)
|
||||||
if (i === mainToData.length - 1) continue;
|
if (i === mainToData.length - 1) continue;
|
||||||
const currentItem = mainToData[i];
|
const currentItem = mainToData[i];
|
||||||
const nextItem = mainToData[i + 1];
|
const nextItem = mainToData[i + 1];
|
||||||
|
|
||||||
// 获取当前国家代码和颜色
|
// 获取当前国家代码和颜色
|
||||||
const countryCode = currentItem.country_code.toUpperCase();
|
const countryCode = currentItem.country_code.toUpperCase();
|
||||||
const color = currentItem.color || "#0ea5e9"; // 获取颜色,如果没有则使用默认颜色
|
const color = currentItem.color || "#0ea5e9"; // 获取颜色,如果没有则使用默认颜色
|
||||||
|
|
||||||
// 如果当前项是起点,下一项是终点
|
// 如果当前项是起点,下一项是终点
|
||||||
if (currentItem.type === "start" && nextItem.type === "end") {
|
if (currentItem.type === "start" && nextItem.type === "end") {
|
||||||
const startCode = countryCode;
|
const startCode = countryCode;
|
||||||
const endCode = nextItem.country_code.toUpperCase();
|
const endCode = nextItem.country_code.toUpperCase();
|
||||||
|
|
||||||
// 无论是否连线,都添加点的涟漪效果
|
// 无论是否连线,都添加点的涟漪效果
|
||||||
const startPoint = createCountryRipple(startCode, color);
|
const startPoint = createCountryRipple(startCode, color);
|
||||||
const endPoint = createCountryRipple(
|
const endPoint = createCountryRipple(
|
||||||
@ -307,7 +352,6 @@ export const WorldGeo = memo(
|
|||||||
);
|
);
|
||||||
if (startPoint) ripplePoints.push(startPoint);
|
if (startPoint) ripplePoints.push(startPoint);
|
||||||
if (endPoint) ripplePoints.push(endPoint);
|
if (endPoint) ripplePoints.push(endPoint);
|
||||||
|
|
||||||
// 检查是否应该绘制连线
|
// 检查是否应该绘制连线
|
||||||
if (currentItem.isLine !== false) {
|
if (currentItem.isLine !== false) {
|
||||||
const lineItem = getLineItem(startCode, endCode);
|
const lineItem = getLineItem(startCode, endCode);
|
||||||
@ -316,14 +360,12 @@ export const WorldGeo = memo(
|
|||||||
lineItem[1].color = nextItem.color || color;
|
lineItem[1].color = nextItem.color || color;
|
||||||
solidData[0]?.[1].push(lineItem);
|
solidData[0]?.[1].push(lineItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳过下一项,因为已经处理了
|
// 跳过下一项,因为已经处理了
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
// 常规情况:当前项到下一项
|
// 常规情况:当前项到下一项
|
||||||
else {
|
else {
|
||||||
const nextCountryCode = nextItem.country_code.toUpperCase();
|
const nextCountryCode = nextItem.country_code.toUpperCase();
|
||||||
|
|
||||||
// 无论是否连线,都添加点的涟漪效果
|
// 无论是否连线,都添加点的涟漪效果
|
||||||
const currentPoint = createCountryRipple(countryCode, color);
|
const currentPoint = createCountryRipple(countryCode, color);
|
||||||
const nextPoint = createCountryRipple(
|
const nextPoint = createCountryRipple(
|
||||||
@ -332,7 +374,6 @@ export const WorldGeo = memo(
|
|||||||
);
|
);
|
||||||
if (currentPoint) ripplePoints.push(currentPoint);
|
if (currentPoint) ripplePoints.push(currentPoint);
|
||||||
if (nextPoint) ripplePoints.push(nextPoint);
|
if (nextPoint) ripplePoints.push(nextPoint);
|
||||||
|
|
||||||
// 检查是否应该绘制连线
|
// 检查是否应该绘制连线
|
||||||
if (currentItem.isLine !== false) {
|
if (currentItem.isLine !== false) {
|
||||||
const lineItem = getLineItem(countryCode, nextCountryCode);
|
const lineItem = getLineItem(countryCode, nextCountryCode);
|
||||||
@ -343,7 +384,6 @@ export const WorldGeo = memo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 虚线数据处理(保持原有逻辑)
|
// 虚线数据处理(保持原有逻辑)
|
||||||
const otherLineList: any = [];
|
const otherLineList: any = [];
|
||||||
return {
|
return {
|
||||||
@ -352,6 +392,7 @@ export const WorldGeo = memo(
|
|||||||
ripplePoints,
|
ripplePoints,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取连线经纬度数据
|
// 获取连线经纬度数据
|
||||||
const convertData = (data: LinesDataType[]) => {
|
const convertData = (data: LinesDataType[]) => {
|
||||||
const res = [];
|
const res = [];
|
||||||
@ -394,6 +435,7 @@ export const WorldGeo = memo(
|
|||||||
// lineMidpointsRef.current = midpoints;
|
// lineMidpointsRef.current = midpoints;
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建双层点效果 - 大点
|
// 创建双层点效果 - 大点
|
||||||
const createDualLayerPoint = (
|
const createDualLayerPoint = (
|
||||||
lastExit: LinesItemType,
|
lastExit: LinesItemType,
|
||||||
@ -480,6 +522,7 @@ export const WorldGeo = memo(
|
|||||||
} as echarts.SeriesOption,
|
} as echarts.SeriesOption,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加新方法:根据经纬度数组创建蓝色涟漪小点(不包含白色内层点)
|
// 添加新方法:根据经纬度数组创建蓝色涟漪小点(不包含白色内层点)
|
||||||
const createRipplePointsFromCoordinates = (
|
const createRipplePointsFromCoordinates = (
|
||||||
coordinates: [number, number][],
|
coordinates: [number, number][],
|
||||||
@ -511,6 +554,7 @@ export const WorldGeo = memo(
|
|||||||
})),
|
})),
|
||||||
} as echarts.SeriesOption);
|
} as echarts.SeriesOption);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建路径点的双层效果
|
// 创建路径点的双层效果
|
||||||
const createPathPoints = (
|
const createPathPoints = (
|
||||||
dataItems: LinesDataType[],
|
dataItems: LinesDataType[],
|
||||||
@ -593,6 +637,7 @@ export const WorldGeo = memo(
|
|||||||
} as echarts.SeriesOption,
|
} as echarts.SeriesOption,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建带自定义提示框的涟漪点
|
// 创建带自定义提示框的涟漪点
|
||||||
const createRipplePointsWithTooltip = (ripplePoints: any) => {
|
const createRipplePointsWithTooltip = (ripplePoints: any) => {
|
||||||
return {
|
return {
|
||||||
@ -652,6 +697,7 @@ export const WorldGeo = memo(
|
|||||||
})),
|
})),
|
||||||
} as echarts.SeriesOption;
|
} as echarts.SeriesOption;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 连线 series
|
// 连线 series
|
||||||
const getLianData = (series: echarts.SeriesOption[]) => {
|
const getLianData = (series: echarts.SeriesOption[]) => {
|
||||||
const { solidData, otherLineList, ripplePoints } = getLine();
|
const { solidData, otherLineList, ripplePoints } = getLine();
|
||||||
@ -688,7 +734,6 @@ export const WorldGeo = memo(
|
|||||||
const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
|
const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
|
||||||
// 获取当前路径的颜色
|
// 获取当前路径的颜色
|
||||||
const pathColor = item[1]?.[0]?.[0]?.color || "#0ea5e9"; // 从第一个点获取颜色,如果没有则使用默认颜色
|
const pathColor = item[1]?.[0]?.[0]?.color || "#0ea5e9"; // 从第一个点获取颜色,如果没有则使用默认颜色
|
||||||
|
|
||||||
// 添加飞行线
|
// 添加飞行线
|
||||||
series.push({
|
series.push({
|
||||||
name: item[0],
|
name: item[0],
|
||||||
@ -716,11 +761,9 @@ export const WorldGeo = memo(
|
|||||||
},
|
},
|
||||||
data: convertData(item[1]) as echarts.LinesSeriesOption["data"],
|
data: convertData(item[1]) as echarts.LinesSeriesOption["data"],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加路径点的双层效果
|
// 添加路径点的双层效果
|
||||||
const pathPoints = createPathPoints(item[1], true, pathColor);
|
const pathPoints = createPathPoints(item[1], true, pathColor);
|
||||||
series.push(...pathPoints);
|
series.push(...pathPoints);
|
||||||
|
|
||||||
// 添加出口节点的双层效果
|
// 添加出口节点的双层效果
|
||||||
if (lastExit) {
|
if (lastExit) {
|
||||||
const exitNodes = createDualLayerPoint(lastExit, true, pathColor);
|
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 lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
|
||||||
// 获取当前路径的颜色
|
// 获取当前路径的颜色
|
||||||
const pathColor = item[1]?.[0]?.[0]?.color || "#F0FFA2"; // 从第一个点获取颜色,如果没有则使用默认颜色
|
const pathColor = item[1]?.[0]?.[0]?.color || "#F0FFA2"; // 从第一个点获取颜色,如果没有则使用默认颜色
|
||||||
|
|
||||||
// 添加虚线
|
// 添加虚线
|
||||||
series.push({
|
series.push({
|
||||||
name: item[0],
|
name: item[0],
|
||||||
@ -906,6 +948,7 @@ export const WorldGeo = memo(
|
|||||||
});
|
});
|
||||||
return series;
|
return series;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOption = () => {
|
const getOption = () => {
|
||||||
const series: echarts.SeriesOption[] = [];
|
const series: echarts.SeriesOption[] = [];
|
||||||
getLianData(series);
|
getLianData(series);
|
||||||
@ -917,14 +960,12 @@ export const WorldGeo = memo(
|
|||||||
currentValue[0]?.authenticationPoint
|
currentValue[0]?.authenticationPoint
|
||||||
) {
|
) {
|
||||||
console.log(currentValue, "values");
|
console.log(currentValue, "values");
|
||||||
|
|
||||||
createSpecialPoints(series); // 添加特殊点和飞线
|
createSpecialPoints(series); // 添加特殊点和飞线
|
||||||
createRipplePointsFromCoordinates(
|
createRipplePointsFromCoordinates(
|
||||||
currentValue[0]?.authenticationPoint || [],
|
currentValue[0]?.authenticationPoint || [],
|
||||||
series
|
series
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const option = {
|
const option = {
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
// 全局提示框配置
|
// 全局提示框配置
|
||||||
@ -1000,6 +1041,7 @@ export const WorldGeo = memo(
|
|||||||
};
|
};
|
||||||
return option;
|
return option;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建DOM标签
|
// 创建DOM标签
|
||||||
const createDOMLabels = () => {
|
const createDOMLabels = () => {
|
||||||
// 清除现有标签
|
// 清除现有标签
|
||||||
@ -1065,6 +1107,7 @@ export const WorldGeo = memo(
|
|||||||
// 更新标签位置
|
// 更新标签位置
|
||||||
updateLabelPositions();
|
updateLabelPositions();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新标签位置
|
// 更新标签位置
|
||||||
const updateLabelPositions = () => {
|
const updateLabelPositions = () => {
|
||||||
if (!proxyGeoRef.current || !labelContainerRef.current) return;
|
if (!proxyGeoRef.current || !labelContainerRef.current) return;
|
||||||
@ -1081,10 +1124,20 @@ export const WorldGeo = memo(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
proxyGeoRef.current?.resize();
|
proxyGeoRef.current?.resize();
|
||||||
updateLabelPositions();
|
updateLabelPositions();
|
||||||
|
|
||||||
|
// 重新定位tooltip
|
||||||
|
if (showTooltip1) {
|
||||||
|
positionCustomTooltip();
|
||||||
|
}
|
||||||
|
if (showTooltip2) {
|
||||||
|
positionCustomTooltip2();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
preMainToData.current?.some(
|
preMainToData.current?.some(
|
||||||
(item, index) => item.country_code !== mainToData[index]?.country_code
|
(item, index) => item.country_code !== mainToData[index]?.country_code
|
||||||
@ -1095,6 +1148,7 @@ export const WorldGeo = memo(
|
|||||||
// 创建DOM标签
|
// 创建DOM标签
|
||||||
setTimeout(createDOMLabels, 100);
|
setTimeout(createDOMLabels, 100);
|
||||||
}, [newHomeProxies, mainToData]);
|
}, [newHomeProxies, mainToData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const chartDom = document.getElementById("screenGeo");
|
const chartDom = document.getElementById("screenGeo");
|
||||||
proxyGeoRef.current = echarts.init(chartDom);
|
proxyGeoRef.current = echarts.init(chartDom);
|
||||||
@ -1121,34 +1175,68 @@ export const WorldGeo = memo(
|
|||||||
proxyGeoRef.current = null;
|
proxyGeoRef.current = null;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 处理tooltip的显示和隐藏
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tooltipType !== "PASS_AUTHENTICATION") {
|
if (tooltipType !== "PASS_AUTHENTICATION") {
|
||||||
lineMidpointsRef.current = [];
|
lineMidpointsRef.current = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tooltipClosed) {
|
if (tooltipClosed) {
|
||||||
if (tooltipType === "NESTED_ENCRYPTION") {
|
if (tooltipType === "NESTED_ENCRYPTION") {
|
||||||
createCustomTooltip();
|
setShowTooltip1(true);
|
||||||
|
// 在下一个渲染周期后定位tooltip
|
||||||
|
setTimeout(() => {
|
||||||
|
positionCustomTooltip();
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
if (tooltipType === "TRAFFIC_OBFUSCATION") {
|
if (tooltipType === "TRAFFIC_OBFUSCATION") {
|
||||||
createCustomTooltip2();
|
setShowTooltip2(true);
|
||||||
|
// 在下一个渲染周期后定位tooltip
|
||||||
|
setTimeout(() => {
|
||||||
|
positionCustomTooltip2();
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
customTooltipRef.current?.remove();
|
setShowTooltip1(false);
|
||||||
customTooltip2Ref.current?.remove();
|
setShowTooltip2(false);
|
||||||
customTooltipRef.current = null;
|
|
||||||
customTooltip2Ref.current = null;
|
|
||||||
}
|
}
|
||||||
return () => {
|
|
||||||
customTooltipRef.current?.remove();
|
|
||||||
customTooltip2Ref.current?.remove();
|
|
||||||
customTooltipRef.current = null;
|
|
||||||
customTooltip2Ref.current = null;
|
|
||||||
};
|
|
||||||
}, [tooltipClosed, tooltipType, currentValue]);
|
}, [tooltipClosed, tooltipType, currentValue]);
|
||||||
|
|
||||||
|
// 在地图初始化后定位tooltip
|
||||||
|
useEffect(() => {
|
||||||
|
if (showTooltip1) {
|
||||||
|
positionCustomTooltip();
|
||||||
|
}
|
||||||
|
if (showTooltip2) {
|
||||||
|
positionCustomTooltip2();
|
||||||
|
}
|
||||||
|
}, [showTooltip1, showTooltip2]);
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 h-full flex flex-col">
|
<div className="flex-1 h-full flex flex-col">
|
||||||
<div id="screenGeo" className="flex-1"></div>
|
<div id="screenGeo" className="flex-1"></div>
|
||||||
|
|
||||||
|
{/* 流量混淆提示框 */}
|
||||||
|
{showTooltip2 && (
|
||||||
|
<CustomTooltipLeft
|
||||||
|
logs={trafficObfuscationLogs}
|
||||||
|
onClose={handleCloseTooltip2}
|
||||||
|
tooltipRef={customTooltip2Ref}
|
||||||
|
title="流量混淆"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</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;
|
||||||
|
// }
|
||||||
@ -81,11 +81,10 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
// line-height: 24px;
|
// line-height: 24px;
|
||||||
}
|
}
|
||||||
|
.tip-box-left{
|
||||||
.tip-box {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 626px;
|
width: 600px;
|
||||||
height: 281px;
|
height: 400px;
|
||||||
padding: 20.85px 20.353px;
|
padding: 20.85px 20.353px;
|
||||||
background: rgba(0, 11.82, 33.10, 0.10);
|
background: rgba(0, 11.82, 33.10, 0.10);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -93,7 +92,30 @@
|
|||||||
outline-offset: -0.46px;
|
outline-offset: -0.46px;
|
||||||
backdrop-filter: blur(5.50px);
|
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;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -116,7 +138,7 @@
|
|||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.traffic-obfuscation-img{
|
.traffic-obfuscation-img {
|
||||||
width: 597px;
|
width: 597px;
|
||||||
height: 241px;
|
height: 241px;
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
@ -127,85 +149,13 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.line-img {
|
.line-img-hx {
|
||||||
width: 312.221px;
|
width: 312.221px;
|
||||||
}
|
// margin-top: 80px;
|
||||||
|
top: 80px;
|
||||||
.line-img-left{
|
left: 0px;
|
||||||
width: 216.86px;
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fill {
|
|
||||||
width: 9.165px;
|
|
||||||
height: 9.165px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: #18E4FF;
|
|
||||||
position: absolute;
|
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 FacebookActiveSvg from "@/assets/svg/anti-forensics-forwarding/FacebookActive.svg?react";
|
||||||
import { RootState } from "@/store";
|
import { RootState } from "@/store";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
import {
|
import {
|
||||||
getApplicationDiversion,
|
getApplicationDiversion,
|
||||||
@ -172,10 +170,17 @@ const AntiDarkAnalysisNetwork = () => {
|
|||||||
|
|
||||||
const [selectedApp, setSelectedApp] = useState<any>(null);
|
const [selectedApp, setSelectedApp] = useState<any>(null);
|
||||||
const [dataInfo, setDataInfo] = useState<any>(null);
|
const [dataInfo, setDataInfo] = useState<any>(null);
|
||||||
|
const [trafficObfuscationLogs, setTrafficObfuscationLogs] = useState<any>([
|
||||||
|
"初始化嵌套加密...",
|
||||||
|
"生成密钥对222...",
|
||||||
|
"应用第一层加密...",
|
||||||
|
"应用第二层加密...",
|
||||||
|
"应用第三层加密...",
|
||||||
|
"加密完成,准备传输...",
|
||||||
|
]);
|
||||||
const currentValue = useMemo(() => {
|
const currentValue = useMemo(() => {
|
||||||
let value = dataInfo;
|
let value = dataInfo;
|
||||||
|
|
||||||
switch (tooltipType) {
|
switch (tooltipType) {
|
||||||
case CONST_TOOLTIP_TYPE.APP_DIVERSION.type:
|
case CONST_TOOLTIP_TYPE.APP_DIVERSION.type:
|
||||||
value = selectedApp ? [selectedApp] : [];
|
value = selectedApp ? [selectedApp] : [];
|
||||||
@ -184,7 +189,7 @@ const AntiDarkAnalysisNetwork = () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}, [tooltipType, selectedApp,dataInfo]);
|
}, [tooltipType, selectedApp, dataInfo]);
|
||||||
|
|
||||||
const handleClickApp = (item: any) => {
|
const handleClickApp = (item: any) => {
|
||||||
setSelectedApp(item);
|
setSelectedApp(item);
|
||||||
@ -201,6 +206,7 @@ const AntiDarkAnalysisNetwork = () => {
|
|||||||
case CONST_TOOLTIP_TYPE.TRAFFIC_OBFUSCATION.type:
|
case CONST_TOOLTIP_TYPE.TRAFFIC_OBFUSCATION.type:
|
||||||
const trafficObfuscation = await getTrafficObfuscation();
|
const trafficObfuscation = await getTrafficObfuscation();
|
||||||
value = [trafficObfuscation.data];
|
value = [trafficObfuscation.data];
|
||||||
|
setTrafficObfuscationLogs(trafficObfuscation.logs);
|
||||||
break;
|
break;
|
||||||
case CONST_TOOLTIP_TYPE.DYNAMIC_ROUTE_GENERATOR.type:
|
case CONST_TOOLTIP_TYPE.DYNAMIC_ROUTE_GENERATOR.type:
|
||||||
const dynamicRouteGeneration = await getDynamicRouteGeneration();
|
const dynamicRouteGeneration = await getDynamicRouteGeneration();
|
||||||
@ -219,7 +225,6 @@ const AntiDarkAnalysisNetwork = () => {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
console.log(value,'valuevalue')
|
|
||||||
setDataInfo(value);
|
setDataInfo(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -227,7 +232,7 @@ const AntiDarkAnalysisNetwork = () => {
|
|||||||
const appDiversion = useMemo(() => {
|
const appDiversion = useMemo(() => {
|
||||||
return Apps.map((item) => {
|
return Apps.map((item) => {
|
||||||
const findApp = appData.find(
|
const findApp = appData.find(
|
||||||
(appItem:any) => item.name === appItem.name
|
(appItem: any) => item.name === appItem.name
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
@ -245,7 +250,7 @@ const AntiDarkAnalysisNetwork = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getDataInfo();
|
getDataInfo();
|
||||||
},[tooltipType])
|
}, [tooltipType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initData();
|
initData();
|
||||||
@ -276,6 +281,7 @@ const AntiDarkAnalysisNetwork = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-2 w-full h-full flex-1">
|
<div className="mt-2 w-full h-full flex-1">
|
||||||
<WorldGeo
|
<WorldGeo
|
||||||
|
trafficObfuscationLogs={trafficObfuscationLogs}
|
||||||
currentValue={currentValue}
|
currentValue={currentValue}
|
||||||
newHomeProxies={newHomeProxies}
|
newHomeProxies={newHomeProxies}
|
||||||
tooltipType={tooltipType}
|
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 * as echarts from "echarts";
|
||||||
// import 'echarts-gl';
|
// import 'echarts-gl';
|
||||||
// import { useQueryClient } from "@tanstack/react-query";
|
// import { useQueryClient } from "@tanstack/react-query";
|
||||||
@ -16,6 +16,136 @@ interface LinesItemType {
|
|||||||
}
|
}
|
||||||
type LinesDataType = [LinesItemType, LinesItemType];
|
type LinesDataType = [LinesItemType, LinesItemType];
|
||||||
type LinesType = [string, LinesDataType[]];
|
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 createCountryRipple = (countryCode: string, color?: string) => {
|
||||||
const coords = geoCoordMap[countryCode];
|
const coords = geoCoordMap[countryCode];
|
||||||
@ -27,13 +157,17 @@ const createCountryRipple = (countryCode: string, color?: string) => {
|
|||||||
color: color || "#0ea5e9", // 添加颜色属性,如果没有则使用默认颜色
|
color: color || "#0ea5e9", // 添加颜色属性,如果没有则使用默认颜色
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorldGeo = memo(
|
export const WorldGeo = memo(
|
||||||
({
|
({
|
||||||
nestedEncryption,
|
nestedEncryption,
|
||||||
passAuthentication,
|
passAuthentication,
|
||||||
dynamicRouteGeneration,
|
dynamicRouteGeneration,
|
||||||
|
tooltipClosed,
|
||||||
setTooltipClosed,
|
setTooltipClosed,
|
||||||
|
logs,
|
||||||
}: {
|
}: {
|
||||||
|
logs:any[];
|
||||||
nestedEncryption: any;
|
nestedEncryption: any;
|
||||||
passAuthentication: any;
|
passAuthentication: any;
|
||||||
dynamicRouteGeneration: any;
|
dynamicRouteGeneration: any;
|
||||||
@ -56,33 +190,38 @@ export const WorldGeo = memo(
|
|||||||
>([]);
|
>([]);
|
||||||
const labelContainerRef = useRef<HTMLDivElement | null>(null);
|
const labelContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const labelsRef = useRef<HTMLDivElement[]>([]);
|
const labelsRef = useRef<HTMLDivElement[]>([]);
|
||||||
|
|
||||||
// 添加状态来跟踪当前显示的连线索引
|
// 添加状态来跟踪当前显示的连线索引
|
||||||
const [nestedEncryptionLineIndex, setNestedEncryptionLineIndex] = useState(-1);
|
const [nestedEncryptionLineIndex, setNestedEncryptionLineIndex] =
|
||||||
|
useState(-1);
|
||||||
const [dynamicRouteLineIndex, setDynamicRouteLineIndex] = useState(-1);
|
const [dynamicRouteLineIndex, setDynamicRouteLineIndex] = useState(-1);
|
||||||
|
|
||||||
// 添加状态来存储所有连线数据
|
// 添加状态来存储所有连线数据
|
||||||
const [nestedEncryptionLines, setNestedEncryptionLines] = useState<{from: string, to: string, color?: string}[]>([]);
|
const [nestedEncryptionLines, setNestedEncryptionLines] = useState<
|
||||||
const [dynamicRouteLines, setDynamicRouteLines] = useState<{from: string, to: string, color?: string}[]>([]);
|
{ from: string; to: string; color?: string }[]
|
||||||
|
>([]);
|
||||||
|
const [dynamicRouteLines, setDynamicRouteLines] = useState<
|
||||||
|
{ from: string; to: string; color?: string }[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
// 添加状态来存储所有点
|
// 添加状态来存储所有点
|
||||||
const [allPoints, setAllPoints] = useState<any[]>([]);
|
const [allPoints, setAllPoints] = useState<any[]>([]);
|
||||||
|
|
||||||
// 使用ref来跟踪动画状态,避免重新渲染
|
// 使用ref来跟踪动画状态,避免重新渲染
|
||||||
const animationTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const animationTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const dynamicAnimationTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const dynamicAnimationTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
// 添加状态来跟踪数据是否已经变化
|
// 添加状态来跟踪数据是否已经变化
|
||||||
const nestedEncryptionKeyRef = useRef<string>("");
|
const nestedEncryptionKeyRef = useRef<string>("");
|
||||||
const dynamicRouteKeyRef = useRef<string>("");
|
const dynamicRouteKeyRef = useRef<string>("");
|
||||||
|
|
||||||
// 初始化时提取所有点的函数
|
// 初始化时提取所有点的函数
|
||||||
const extractAllPoints = () => {
|
const extractAllPoints = () => {
|
||||||
const points: any[] = [];
|
const points: any[] = [];
|
||||||
|
|
||||||
// console.log("Extracting points from nestedEncryption:", nestedEncryption);
|
// console.log("Extracting points from nestedEncryption:", nestedEncryption);
|
||||||
// console.log("Extracting points from dynamicRouteGeneration:", dynamicRouteGeneration);
|
// console.log("Extracting points from dynamicRouteGeneration:", dynamicRouteGeneration);
|
||||||
|
|
||||||
// 从嵌套加密数据中提取点
|
// 从嵌套加密数据中提取点
|
||||||
if (nestedEncryption && Array.isArray(nestedEncryption)) {
|
if (nestedEncryption && Array.isArray(nestedEncryption)) {
|
||||||
nestedEncryption.forEach((item: any) => {
|
nestedEncryption.forEach((item: any) => {
|
||||||
@ -91,15 +230,18 @@ export const WorldGeo = memo(
|
|||||||
// 添加起点到点集合
|
// 添加起点到点集合
|
||||||
const fromCode = dataItem.country_code.toUpperCase();
|
const fromCode = dataItem.country_code.toUpperCase();
|
||||||
const fromPoint = createCountryRipple(fromCode, item.color);
|
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);
|
points.push(fromPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有终点,也添加到点集合
|
// 如果有终点,也添加到点集合
|
||||||
if (dataItem.ingress_country_code) {
|
if (dataItem.ingress_country_code) {
|
||||||
const toCode = dataItem.ingress_country_code.toUpperCase();
|
const toCode = dataItem.ingress_country_code.toUpperCase();
|
||||||
const toPoint = createCountryRipple(toCode, item.color);
|
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);
|
points.push(toPoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,7 +249,7 @@ export const WorldGeo = memo(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从动态路由数据中提取点
|
// 从动态路由数据中提取点
|
||||||
if (dynamicRouteGeneration && Array.isArray(dynamicRouteGeneration)) {
|
if (dynamicRouteGeneration && Array.isArray(dynamicRouteGeneration)) {
|
||||||
dynamicRouteGeneration.forEach((item: any) => {
|
dynamicRouteGeneration.forEach((item: any) => {
|
||||||
@ -116,15 +258,18 @@ export const WorldGeo = memo(
|
|||||||
// 添加起点到点集合
|
// 添加起点到点集合
|
||||||
const fromCode = dataItem.country_code.toUpperCase();
|
const fromCode = dataItem.country_code.toUpperCase();
|
||||||
const fromPoint = createCountryRipple(fromCode, item.color);
|
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);
|
points.push(fromPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有终点,也添加到点集合
|
// 如果有终点,也添加到点集合
|
||||||
if (dataItem.ingress_country_code) {
|
if (dataItem.ingress_country_code) {
|
||||||
const toCode = dataItem.ingress_country_code.toUpperCase();
|
const toCode = dataItem.ingress_country_code.toUpperCase();
|
||||||
const toPoint = createCountryRipple(toCode, item.color);
|
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);
|
points.push(toPoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,11 +277,11 @@ export const WorldGeo = memo(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Extracted points:", points);
|
console.log("Extracted points:", points);
|
||||||
return points;
|
return points;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 修改初始化逻辑,确保在数据变化时立即提取点
|
// 修改初始化逻辑,确保在数据变化时立即提取点
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 提取所有点
|
// 提取所有点
|
||||||
@ -145,28 +290,33 @@ export const WorldGeo = memo(
|
|||||||
setAllPoints(points);
|
setAllPoints(points);
|
||||||
}
|
}
|
||||||
}, [nestedEncryption, dynamicRouteGeneration]); // 监听数据变化
|
}, [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;
|
if (connections.length === 0) return;
|
||||||
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
|
||||||
// 递归函数,用于按顺序显示连线
|
// 递归函数,用于按顺序显示连线
|
||||||
const animateNextLine = () => {
|
const animateNextLine = () => {
|
||||||
setNestedEncryptionLineIndex(index);
|
setNestedEncryptionLineIndex(index);
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
|
|
||||||
if (index < connections.length) {
|
if (index < connections.length) {
|
||||||
animationTimerRef.current = setTimeout(animateNextLine, LINE_ANIMATION_INTERVAL);
|
animationTimerRef.current = setTimeout(
|
||||||
|
animateNextLine,
|
||||||
|
LINE_ANIMATION_INTERVAL
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 开始动画
|
// 开始动画
|
||||||
animateNextLine();
|
animateNextLine();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理嵌套加密数据变化
|
// 处理嵌套加密数据变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 清除任何现有的动画定时器
|
// 清除任何现有的动画定时器
|
||||||
@ -174,45 +324,52 @@ export const WorldGeo = memo(
|
|||||||
clearTimeout(animationTimerRef.current);
|
clearTimeout(animationTimerRef.current);
|
||||||
animationTimerRef.current = null;
|
animationTimerRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allExtractedPoints: any[] = [];
|
const allExtractedPoints: any[] = [];
|
||||||
|
|
||||||
// 处理嵌套加密数据
|
// 处理嵌套加密数据
|
||||||
if (nestedEncryption && Array.isArray(nestedEncryption)) {
|
if (nestedEncryption && Array.isArray(nestedEncryption)) {
|
||||||
const points: any[] = [];
|
const points: any[] = [];
|
||||||
const connections: {from: string, to: string, color?: string}[] = [];
|
const connections: { from: string; to: string; color?: string }[] = [];
|
||||||
let shouldStartAnimation = false;
|
let shouldStartAnimation = false;
|
||||||
|
|
||||||
nestedEncryption.forEach((item: any) => {
|
nestedEncryption.forEach((item: any) => {
|
||||||
if (item.data && Array.isArray(item.data)) {
|
if (item.data && Array.isArray(item.data)) {
|
||||||
item.data.forEach((dataItem: any) => {
|
item.data.forEach((dataItem: any) => {
|
||||||
// 添加起点到点集合
|
// 添加起点到点集合
|
||||||
const fromCode = dataItem.country_code.toUpperCase();
|
const fromCode = dataItem.country_code.toUpperCase();
|
||||||
const fromPoint = createCountryRipple(fromCode, item.color);
|
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);
|
points.push(fromPoint);
|
||||||
if (!allExtractedPoints.some(p => p.country_code === fromCode)) {
|
if (
|
||||||
|
!allExtractedPoints.some((p) => p.country_code === fromCode)
|
||||||
|
) {
|
||||||
allExtractedPoints.push(fromPoint);
|
allExtractedPoints.push(fromPoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有终点,也添加到点集合
|
// 如果有终点,也添加到点集合
|
||||||
if (dataItem.ingress_country_code) {
|
if (dataItem.ingress_country_code) {
|
||||||
const toCode = dataItem.ingress_country_code.toUpperCase();
|
const toCode = dataItem.ingress_country_code.toUpperCase();
|
||||||
const toPoint = createCountryRipple(toCode, item.color);
|
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);
|
points.push(toPoint);
|
||||||
if (!allExtractedPoints.some(p => p.country_code === toCode)) {
|
if (
|
||||||
|
!allExtractedPoints.some((p) => p.country_code === toCode)
|
||||||
|
) {
|
||||||
allExtractedPoints.push(toPoint);
|
allExtractedPoints.push(toPoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否需要开始连线动画
|
// 检查是否需要开始连线动画
|
||||||
if (item.isLine === true) {
|
if (item.isLine === true) {
|
||||||
connections.push({
|
connections.push({
|
||||||
from: fromCode,
|
from: fromCode,
|
||||||
to: toCode,
|
to: toCode,
|
||||||
color: item.color
|
color: item.color,
|
||||||
});
|
});
|
||||||
shouldStartAnimation = true;
|
shouldStartAnimation = true;
|
||||||
}
|
}
|
||||||
@ -220,19 +377,22 @@ export const WorldGeo = memo(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 生成当前数据的唯一键
|
// 生成当前数据的唯一键
|
||||||
const currentKey = JSON.stringify(nestedEncryption);
|
const currentKey = JSON.stringify(nestedEncryption);
|
||||||
|
|
||||||
// 检查数据是否变化
|
// 检查数据是否变化
|
||||||
if (currentKey !== nestedEncryptionKeyRef.current || shouldStartAnimation) {
|
if (
|
||||||
|
currentKey !== nestedEncryptionKeyRef.current ||
|
||||||
|
shouldStartAnimation
|
||||||
|
) {
|
||||||
nestedEncryptionKeyRef.current = currentKey;
|
nestedEncryptionKeyRef.current = currentKey;
|
||||||
setNestedEncryptionLines(connections);
|
setNestedEncryptionLines(connections);
|
||||||
|
|
||||||
// 如果有连线数据且需要开始动画,重置索引并启动动画
|
// 如果有连线数据且需要开始动画,重置索引并启动动画
|
||||||
if (connections.length > 0 && shouldStartAnimation) {
|
if (connections.length > 0 && shouldStartAnimation) {
|
||||||
setNestedEncryptionLineIndex(-1); // 重置索引
|
setNestedEncryptionLineIndex(-1); // 重置索引
|
||||||
|
|
||||||
// 启动连线动画
|
// 启动连线动画
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
startNestedEncryptionAnimation(connections);
|
startNestedEncryptionAnimation(connections);
|
||||||
@ -243,17 +403,19 @@ export const WorldGeo = memo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新所有点
|
// 更新所有点
|
||||||
if (allExtractedPoints.length > 0) {
|
if (allExtractedPoints.length > 0) {
|
||||||
setAllPoints(prevPoints => {
|
setAllPoints((prevPoints) => {
|
||||||
const newPoints = [...prevPoints];
|
const newPoints = [...prevPoints];
|
||||||
allExtractedPoints.forEach(point => {
|
allExtractedPoints.forEach((point) => {
|
||||||
if (!newPoints.some(p => p.country_code === point.country_code)) {
|
if (!newPoints.some((p) => p.country_code === point.country_code)) {
|
||||||
newPoints.push(point);
|
newPoints.push(point);
|
||||||
} else {
|
} 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) {
|
if (existingIndex !== -1 && point.color) {
|
||||||
newPoints[existingIndex].color = point.color;
|
newPoints[existingIndex].color = point.color;
|
||||||
}
|
}
|
||||||
@ -263,7 +425,7 @@ export const WorldGeo = memo(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [nestedEncryption]);
|
}, [nestedEncryption]);
|
||||||
|
|
||||||
// 处理动态路由数据变化
|
// 处理动态路由数据变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 清除任何现有的动画定时器
|
// 清除任何现有的动画定时器
|
||||||
@ -271,45 +433,52 @@ export const WorldGeo = memo(
|
|||||||
clearTimeout(dynamicAnimationTimerRef.current);
|
clearTimeout(dynamicAnimationTimerRef.current);
|
||||||
dynamicAnimationTimerRef.current = null;
|
dynamicAnimationTimerRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const allExtractedPoints: any[] = [];
|
const allExtractedPoints: any[] = [];
|
||||||
|
|
||||||
// 处理动态路由数据
|
// 处理动态路由数据
|
||||||
if (dynamicRouteGeneration && Array.isArray(dynamicRouteGeneration)) {
|
if (dynamicRouteGeneration && Array.isArray(dynamicRouteGeneration)) {
|
||||||
const points: any[] = [];
|
const points: any[] = [];
|
||||||
const connections: {from: string, to: string, color?: string}[] = [];
|
const connections: { from: string; to: string; color?: string }[] = [];
|
||||||
let shouldStartAnimation = false;
|
let shouldStartAnimation = false;
|
||||||
|
|
||||||
dynamicRouteGeneration.forEach((item: any) => {
|
dynamicRouteGeneration.forEach((item: any) => {
|
||||||
if (item.data && Array.isArray(item.data)) {
|
if (item.data && Array.isArray(item.data)) {
|
||||||
item.data.forEach((dataItem: any) => {
|
item.data.forEach((dataItem: any) => {
|
||||||
// 添加起点到点集合
|
// 添加起点到点集合
|
||||||
const fromCode = dataItem.country_code.toUpperCase();
|
const fromCode = dataItem.country_code.toUpperCase();
|
||||||
const fromPoint = createCountryRipple(fromCode, item.color);
|
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);
|
points.push(fromPoint);
|
||||||
if (!allExtractedPoints.some(p => p.country_code === fromCode)) {
|
if (
|
||||||
|
!allExtractedPoints.some((p) => p.country_code === fromCode)
|
||||||
|
) {
|
||||||
allExtractedPoints.push(fromPoint);
|
allExtractedPoints.push(fromPoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有终点,也添加到点集合
|
// 如果有终点,也添加到点集合
|
||||||
if (dataItem.ingress_country_code) {
|
if (dataItem.ingress_country_code) {
|
||||||
const toCode = dataItem.ingress_country_code.toUpperCase();
|
const toCode = dataItem.ingress_country_code.toUpperCase();
|
||||||
const toPoint = createCountryRipple(toCode, item.color);
|
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);
|
points.push(toPoint);
|
||||||
if (!allExtractedPoints.some(p => p.country_code === toCode)) {
|
if (
|
||||||
|
!allExtractedPoints.some((p) => p.country_code === toCode)
|
||||||
|
) {
|
||||||
allExtractedPoints.push(toPoint);
|
allExtractedPoints.push(toPoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否需要开始连线动画
|
// 检查是否需要开始连线动画
|
||||||
if (item.isLine === true) {
|
if (item.isLine === true) {
|
||||||
connections.push({
|
connections.push({
|
||||||
from: fromCode,
|
from: fromCode,
|
||||||
to: toCode,
|
to: toCode,
|
||||||
color: item.color
|
color: item.color,
|
||||||
});
|
});
|
||||||
shouldStartAnimation = true;
|
shouldStartAnimation = true;
|
||||||
}
|
}
|
||||||
@ -317,19 +486,19 @@ export const WorldGeo = memo(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 生成当前数据的唯一键
|
// 生成当前数据的唯一键
|
||||||
const currentKey = JSON.stringify(dynamicRouteGeneration);
|
const currentKey = JSON.stringify(dynamicRouteGeneration);
|
||||||
|
|
||||||
// 检查数据是否变化
|
// 检查数据是否变化
|
||||||
if (currentKey !== dynamicRouteKeyRef.current || shouldStartAnimation) {
|
if (currentKey !== dynamicRouteKeyRef.current || shouldStartAnimation) {
|
||||||
dynamicRouteKeyRef.current = currentKey;
|
dynamicRouteKeyRef.current = currentKey;
|
||||||
setDynamicRouteLines(connections);
|
setDynamicRouteLines(connections);
|
||||||
|
|
||||||
// 如果有连线数据且需要开始动画,重置索引并启动动画
|
// 如果有连线数据且需要开始动画,重置索引并启动动画
|
||||||
if (connections.length > 0 && shouldStartAnimation) {
|
if (connections.length > 0 && shouldStartAnimation) {
|
||||||
setDynamicRouteLineIndex(-1); // 重置索引
|
setDynamicRouteLineIndex(-1); // 重置索引
|
||||||
|
|
||||||
// 启动连线动画
|
// 启动连线动画
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
startDynamicRouteAnimation(connections);
|
startDynamicRouteAnimation(connections);
|
||||||
@ -340,17 +509,19 @@ export const WorldGeo = memo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新所有点
|
// 更新所有点
|
||||||
if (allExtractedPoints.length > 0) {
|
if (allExtractedPoints.length > 0) {
|
||||||
setAllPoints(prevPoints => {
|
setAllPoints((prevPoints) => {
|
||||||
const newPoints = [...prevPoints];
|
const newPoints = [...prevPoints];
|
||||||
allExtractedPoints.forEach(point => {
|
allExtractedPoints.forEach((point) => {
|
||||||
if (!newPoints.some(p => p.country_code === point.country_code)) {
|
if (!newPoints.some((p) => p.country_code === point.country_code)) {
|
||||||
newPoints.push(point);
|
newPoints.push(point);
|
||||||
} else {
|
} 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) {
|
if (existingIndex !== -1 && point.color) {
|
||||||
newPoints[existingIndex].color = point.color;
|
newPoints[existingIndex].color = point.color;
|
||||||
}
|
}
|
||||||
@ -360,28 +531,33 @@ export const WorldGeo = memo(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [dynamicRouteGeneration]);
|
}, [dynamicRouteGeneration]);
|
||||||
|
|
||||||
// 启动动态路由连线动画的函数
|
// 启动动态路由连线动画的函数
|
||||||
const startDynamicRouteAnimation = (connections: {from: string, to: string, color?: string}[]) => {
|
const startDynamicRouteAnimation = (
|
||||||
|
connections: { from: string; to: string; color?: string }[]
|
||||||
|
) => {
|
||||||
if (connections.length === 0) return;
|
if (connections.length === 0) return;
|
||||||
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
|
||||||
// 递归函数,用于按顺序显示连线
|
// 递归函数,用于按顺序显示连线
|
||||||
const animateNextLine = () => {
|
const animateNextLine = () => {
|
||||||
setDynamicRouteLineIndex(index);
|
setDynamicRouteLineIndex(index);
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
|
|
||||||
if (index < connections.length) {
|
if (index < connections.length) {
|
||||||
dynamicAnimationTimerRef.current = setTimeout(animateNextLine, LINE_ANIMATION_INTERVAL);
|
dynamicAnimationTimerRef.current = setTimeout(
|
||||||
|
animateNextLine,
|
||||||
|
LINE_ANIMATION_INTERVAL
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 开始动画
|
// 开始动画
|
||||||
animateNextLine();
|
animateNextLine();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 组件卸载时清除定时器
|
// 组件卸载时清除定时器
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@ -395,7 +571,7 @@ export const WorldGeo = memo(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getLineItem = (
|
const getLineItem = (
|
||||||
preCode: string,
|
preCode: string,
|
||||||
nextCode: string,
|
nextCode: string,
|
||||||
@ -416,95 +592,57 @@ export const WorldGeo = memo(
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLine = () => {
|
const getLine = () => {
|
||||||
// 实现数据处理
|
// 实现数据处理
|
||||||
const solidData: LinesType[] = []; // 不再使用单一数组,而是分开存储
|
const solidData: LinesType[] = []; // 不再使用单一数组,而是分开存储
|
||||||
|
|
||||||
// 处理嵌套加密连线 - 放入单独的数组
|
// 处理嵌套加密连线 - 放入单独的数组
|
||||||
if (nestedEncryptionLineIndex >= 0 && nestedEncryptionLines.length > 0) {
|
if (nestedEncryptionLineIndex >= 0 && nestedEncryptionLines.length > 0) {
|
||||||
const nestedLines: LinesDataType[] = [];
|
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];
|
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) {
|
if (nestedLines.length > 0) {
|
||||||
solidData.push(["nested", nestedLines]);
|
solidData.push(["nested", nestedLines]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理动态路由连线 - 放入单独的数组
|
// 处理动态路由连线 - 放入单独的数组
|
||||||
if (dynamicRouteLineIndex >= 0 && dynamicRouteLines.length > 0) {
|
if (dynamicRouteLineIndex >= 0 && dynamicRouteLines.length > 0) {
|
||||||
const dynamicLines: LinesDataType[] = [];
|
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];
|
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) {
|
if (dynamicLines.length > 0) {
|
||||||
solidData.push(["dynamic", dynamicLines]);
|
solidData.push(["dynamic", dynamicLines]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 虚线数据处理(保持原有逻辑)
|
// 虚线数据处理(保持原有逻辑)
|
||||||
const otherLineList: any = [];
|
const otherLineList: any = [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
solidData,
|
solidData,
|
||||||
otherLineList,
|
otherLineList,
|
||||||
ripplePoints: allPoints, // 使用 allPoints 确保点始终显示
|
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 = () => {
|
const positionCustomTooltip = () => {
|
||||||
if (!customTooltipRef.current || !proxyGeoRef.current) return;
|
if (!customTooltipRef.current || !proxyGeoRef.current) return;
|
||||||
@ -527,7 +665,13 @@ export const WorldGeo = memo(
|
|||||||
console.error("Error positioning tooltip:", error);
|
console.error("Error positioning tooltip:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理关闭tooltip
|
||||||
|
const handleCloseTooltip = () => {
|
||||||
|
setTooltipClosed(false);
|
||||||
|
setTooltipClosed(false);
|
||||||
|
};
|
||||||
|
|
||||||
// 获取连线经纬度数据
|
// 获取连线经纬度数据
|
||||||
const convertData = (data: LinesDataType[]) => {
|
const convertData = (data: LinesDataType[]) => {
|
||||||
const res = [];
|
const res = [];
|
||||||
@ -570,7 +714,7 @@ export const WorldGeo = memo(
|
|||||||
// lineMidpointsRef.current = midpoints;
|
// lineMidpointsRef.current = midpoints;
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建双层点效果 - 大点
|
// 创建双层点效果 - 大点
|
||||||
const createDualLayerPoint = (
|
const createDualLayerPoint = (
|
||||||
lastExit: LinesItemType,
|
lastExit: LinesItemType,
|
||||||
@ -657,7 +801,7 @@ export const WorldGeo = memo(
|
|||||||
} as echarts.SeriesOption,
|
} as echarts.SeriesOption,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加新方法:根据经纬度数组创建蓝色涟漪小点(不包含白色内层点)
|
// 添加新方法:根据经纬度数组创建蓝色涟漪小点(不包含白色内层点)
|
||||||
const createRipplePointsFromCoordinates = (
|
const createRipplePointsFromCoordinates = (
|
||||||
coordinates: [number, number][],
|
coordinates: [number, number][],
|
||||||
@ -689,7 +833,7 @@ export const WorldGeo = memo(
|
|||||||
})),
|
})),
|
||||||
} as echarts.SeriesOption);
|
} as echarts.SeriesOption);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建路径点的双层效果
|
// 创建路径点的双层效果
|
||||||
const createPathPoints = (
|
const createPathPoints = (
|
||||||
dataItems: LinesDataType[],
|
dataItems: LinesDataType[],
|
||||||
@ -772,7 +916,7 @@ export const WorldGeo = memo(
|
|||||||
} as echarts.SeriesOption,
|
} as echarts.SeriesOption,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建带自定义提示框的涟漪点
|
// 创建带自定义提示框的涟漪点
|
||||||
const createRipplePointsWithTooltip = (ripplePoints: any) => {
|
const createRipplePointsWithTooltip = (ripplePoints: any) => {
|
||||||
return {
|
return {
|
||||||
@ -832,11 +976,11 @@ export const WorldGeo = memo(
|
|||||||
})),
|
})),
|
||||||
} as echarts.SeriesOption;
|
} as echarts.SeriesOption;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 连线 series
|
// 连线 series
|
||||||
const getLianData = (series: echarts.SeriesOption[]) => {
|
const getLianData = (series: echarts.SeriesOption[]) => {
|
||||||
const { solidData, otherLineList, ripplePoints } = getLine();
|
const { solidData, otherLineList, ripplePoints } = getLine();
|
||||||
|
|
||||||
// 如果有需要显示涟漪效果的点,添加它们
|
// 如果有需要显示涟漪效果的点,添加它们
|
||||||
if (ripplePoints.length > 0) {
|
if (ripplePoints.length > 0) {
|
||||||
// 添加带自定义提示框的外层蓝色点
|
// 添加带自定义提示框的外层蓝色点
|
||||||
@ -862,17 +1006,17 @@ export const WorldGeo = memo(
|
|||||||
})),
|
})),
|
||||||
} as echarts.SeriesOption);
|
} as echarts.SeriesOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理每个连线组
|
// 处理每个连线组
|
||||||
solidData.forEach((item) => {
|
solidData.forEach((item) => {
|
||||||
// 如果没有连线数据,则跳过
|
// 如果没有连线数据,则跳过
|
||||||
if (item[1].length === 0) {
|
if (item[1].length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 为每条连线创建飞行线
|
// 为每条连线创建飞行线
|
||||||
const pathColor = item[0] === "nested" ? "#0ea5e9" : "#F0FFA2"; // 根据类型设置默认颜色
|
const pathColor = item[0] === "nested" ? "#0ea5e9" : "#F0FFA2"; // 根据类型设置默认颜色
|
||||||
|
|
||||||
// 添加飞行线
|
// 添加飞行线
|
||||||
series.push({
|
series.push({
|
||||||
name: item[0],
|
name: item[0],
|
||||||
@ -899,21 +1043,25 @@ export const WorldGeo = memo(
|
|||||||
},
|
},
|
||||||
data: convertData(item[1]) as echarts.LinesSeriesOption["data"],
|
data: convertData(item[1]) as echarts.LinesSeriesOption["data"],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加路径点的双层效果
|
// 添加路径点的双层效果
|
||||||
const pathPoints = createPathPoints(item[1], true, pathColor);
|
const pathPoints = createPathPoints(item[1], true, pathColor);
|
||||||
series.push(...pathPoints);
|
series.push(...pathPoints);
|
||||||
|
|
||||||
// 添加出口节点的双层效果
|
// 添加出口节点的双层效果
|
||||||
item[1].forEach(lineData => {
|
item[1].forEach((lineData) => {
|
||||||
const lastExit = lineData[1];
|
const lastExit = lineData[1];
|
||||||
if (lastExit) {
|
if (lastExit) {
|
||||||
const exitNodes = createDualLayerPoint(lastExit, true, lastExit.color || pathColor);
|
const exitNodes = createDualLayerPoint(
|
||||||
|
lastExit,
|
||||||
|
true,
|
||||||
|
lastExit.color || pathColor
|
||||||
|
);
|
||||||
series.push(...exitNodes);
|
series.push(...exitNodes);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理其他线(保持原有逻辑)
|
// 处理其他线(保持原有逻辑)
|
||||||
otherLineList.forEach((line: any) => {
|
otherLineList.forEach((line: any) => {
|
||||||
line.forEach((item: any) => {
|
line.forEach((item: any) => {
|
||||||
@ -948,10 +1096,10 @@ export const WorldGeo = memo(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建A点和B点,并添加飞线和标签
|
// 创建A点和B点,并添加飞线和标签
|
||||||
const createSpecialPoints = (series: echarts.SeriesOption[]) => {
|
const createSpecialPoints = (series: echarts.SeriesOption[]) => {
|
||||||
// 定义点A和点B的坐标
|
// 定义点A和点B的坐标
|
||||||
@ -1094,7 +1242,7 @@ export const WorldGeo = memo(
|
|||||||
});
|
});
|
||||||
return series;
|
return series;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOption = () => {
|
const getOption = () => {
|
||||||
const series: echarts.SeriesOption[] = [];
|
const series: echarts.SeriesOption[] = [];
|
||||||
getLianData(series);
|
getLianData(series);
|
||||||
@ -1184,7 +1332,7 @@ export const WorldGeo = memo(
|
|||||||
};
|
};
|
||||||
return option;
|
return option;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建DOM标签
|
// 创建DOM标签
|
||||||
const createDOMLabels = () => {
|
const createDOMLabels = () => {
|
||||||
// 清除现有标签
|
// 清除现有标签
|
||||||
@ -1250,7 +1398,7 @@ export const WorldGeo = memo(
|
|||||||
// 更新标签位置
|
// 更新标签位置
|
||||||
updateLabelPositions();
|
updateLabelPositions();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新标签位置
|
// 更新标签位置
|
||||||
const updateLabelPositions = () => {
|
const updateLabelPositions = () => {
|
||||||
if (!proxyGeoRef.current || !labelContainerRef.current) return;
|
if (!proxyGeoRef.current || !labelContainerRef.current) return;
|
||||||
@ -1267,19 +1415,21 @@ export const WorldGeo = memo(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
proxyGeoRef.current?.resize();
|
proxyGeoRef.current?.resize();
|
||||||
updateLabelPositions();
|
updateLabelPositions();
|
||||||
positionCustomTooltip();
|
if (tooltipClosed) {
|
||||||
|
positionCustomTooltip();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新图表
|
// 更新图表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const option = getOption();
|
const option = getOption();
|
||||||
proxyGeoRef.current?.setOption(option);
|
proxyGeoRef.current?.setOption(option);
|
||||||
}, [nestedEncryptionLineIndex, dynamicRouteLineIndex, allPoints]); // 当连线索引或点变化时更新图表
|
}, [nestedEncryptionLineIndex, dynamicRouteLineIndex, allPoints]); // 当连线索引或点变化时更新图表
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
lineMidpointsRef.current = []; // 重置中点数据
|
lineMidpointsRef.current = []; // 重置中点数据
|
||||||
const option = getOption();
|
const option = getOption();
|
||||||
@ -1287,7 +1437,7 @@ export const WorldGeo = memo(
|
|||||||
// 创建DOM标签
|
// 创建DOM标签
|
||||||
setTimeout(createDOMLabels, 100);
|
setTimeout(createDOMLabels, 100);
|
||||||
}, [nestedEncryption, dynamicRouteGeneration, passAuthentication]);
|
}, [nestedEncryption, dynamicRouteGeneration, passAuthentication]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const chartDom = document.getElementById("screenGeo");
|
const chartDom = document.getElementById("screenGeo");
|
||||||
proxyGeoRef.current = echarts.init(chartDom);
|
proxyGeoRef.current = echarts.init(chartDom);
|
||||||
@ -1295,13 +1445,13 @@ export const WorldGeo = memo(
|
|||||||
"world",
|
"world",
|
||||||
worldGeoJson as unknown as Parameters<typeof echarts.registerMap>[1]
|
worldGeoJson as unknown as Parameters<typeof echarts.registerMap>[1]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 初始化时提取所有点
|
// 初始化时提取所有点
|
||||||
const initialPoints = extractAllPoints();
|
const initialPoints = extractAllPoints();
|
||||||
if (initialPoints.length > 0) {
|
if (initialPoints.length > 0) {
|
||||||
setAllPoints(initialPoints);
|
setAllPoints(initialPoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
const option = getOption();
|
const option = getOption();
|
||||||
option && proxyGeoRef.current?.setOption(option);
|
option && proxyGeoRef.current?.setOption(option);
|
||||||
// 添加地图交互事件监听器
|
// 添加地图交互事件监听器
|
||||||
@ -1321,19 +1471,27 @@ export const WorldGeo = memo(
|
|||||||
proxyGeoRef.current = null;
|
proxyGeoRef.current = null;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 在地图初始化后定位tooltip
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
createCustomTooltip();
|
if (tooltipClosed) {
|
||||||
return () => {
|
positionCustomTooltip();
|
||||||
customTooltipRef.current?.remove();
|
}
|
||||||
customTooltipRef.current = null;
|
}, [tooltipClosed, nestedEncryption]);
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 h-full flex flex-col">
|
<div className="flex-1 h-full flex flex-col">
|
||||||
<div id="screenGeo" className="flex-1"></div>
|
<div id="screenGeo" className="flex-1"></div>
|
||||||
|
{tooltipClosed && (
|
||||||
|
<CustomTooltip
|
||||||
|
logs={
|
||||||
|
logs
|
||||||
|
}
|
||||||
|
onClose={handleCloseTooltip}
|
||||||
|
tooltipRef={customTooltipRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@ -81,11 +81,24 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
// line-height: 24px;
|
// line-height: 24px;
|
||||||
}
|
}
|
||||||
|
.tip-box-left{
|
||||||
.tip-box {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 626px;
|
width: 600px;
|
||||||
height: 281px;
|
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;
|
padding: 20.85px 20.353px;
|
||||||
background: rgba(0, 11.82, 33.10, 0.10);
|
background: rgba(0, 11.82, 33.10, 0.10);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -93,7 +106,8 @@
|
|||||||
outline-offset: -0.46px;
|
outline-offset: -0.46px;
|
||||||
backdrop-filter: blur(5.50px);
|
backdrop-filter: blur(5.50px);
|
||||||
|
|
||||||
.close-icon , .close-icon2 {
|
.close-icon,
|
||||||
|
.close-icon2 {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -116,7 +130,7 @@
|
|||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.traffic-obfuscation-img{
|
.traffic-obfuscation-img {
|
||||||
width: 597px;
|
width: 597px;
|
||||||
height: 241px;
|
height: 241px;
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
@ -127,85 +141,18 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.line-img {
|
.line-img-hx {
|
||||||
width: 312.221px;
|
width: 312.221px;
|
||||||
|
// margin-top: 80px;
|
||||||
|
top: 80px;
|
||||||
|
left: 0px;
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-img-left{
|
.line-img-left {
|
||||||
width: 216.86px;
|
width: 216.86px;
|
||||||
margin-top: 30px;
|
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);
|
let istrue = useRef(false);
|
||||||
const [nestedEncryption, setNestedEncryption] = useState<any>([]);
|
const [nestedEncryption, setNestedEncryption] = useState<any>([]);
|
||||||
const [dynamicRouteGeneration, setDynamicRouteGeneration] = useState<any>([]);
|
const [dynamicRouteGeneration, setDynamicRouteGeneration] = useState<any>([]);
|
||||||
// const [dataInfo, setDataInfo] = useState<any>({
|
const [logs, setLogs] = useState<any>([]);
|
||||||
// passAuthentication: {
|
|
||||||
// ...PASS_AUTHENTICATION,
|
|
||||||
// },
|
|
||||||
// nestedEncryption: [NESTED_ENCRYPTION],
|
|
||||||
// dynamicRouteGeneration: DYNAMIC_ROUTE_GENERATOR,
|
|
||||||
// });
|
|
||||||
|
|
||||||
const initData = async () => {
|
const initData = async () => {
|
||||||
try {
|
try {
|
||||||
@ -194,6 +188,7 @@ const DecentralizedElasticNetwork = () => {
|
|||||||
nestedEncryption.data.isLine = false;
|
nestedEncryption.data.isLine = false;
|
||||||
dynamicRouteGeneration.data[0].isLine = false;
|
dynamicRouteGeneration.data[0].isLine = false;
|
||||||
setNestedEncryption([nestedEncryption.data]);
|
setNestedEncryption([nestedEncryption.data]);
|
||||||
|
setLogs(nestedEncryption.logs);
|
||||||
setDynamicRouteGeneration(dynamicRouteGeneration.data);
|
setDynamicRouteGeneration(dynamicRouteGeneration.data);
|
||||||
setDataInfo({
|
setDataInfo({
|
||||||
nestedEncryption: [nestedEncryption.data],
|
nestedEncryption: [nestedEncryption.data],
|
||||||
@ -231,6 +226,7 @@ const DecentralizedElasticNetwork = () => {
|
|||||||
<div className="decentralized w-full h-full flex flex-col relative">
|
<div className="decentralized w-full h-full flex flex-col relative">
|
||||||
<div className="mt-2 w-full h-full flex-1">
|
<div className="mt-2 w-full h-full flex-1">
|
||||||
<WorldGeo
|
<WorldGeo
|
||||||
|
logs={logs}
|
||||||
nestedEncryption={nestedEncryption}
|
nestedEncryption={nestedEncryption}
|
||||||
passAuthentication={dataInfo.passAuthentication}
|
passAuthentication={dataInfo.passAuthentication}
|
||||||
dynamicRouteGeneration={dynamicRouteGeneration}
|
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 * as echarts from "echarts";
|
||||||
// import 'echarts-gl';
|
// import 'echarts-gl';
|
||||||
// import { useQueryClient } from "@tanstack/react-query";
|
// 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 { geoCoordMap, countryNameMap, countryCodeMap } from "@/data";
|
||||||
import { getUrl } from "@/lib/utils";
|
import { getUrl } from "@/lib/utils";
|
||||||
import { CONST_TOOLTIP_TYPE } from "@/pages/anti-forensics-forwarding";
|
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 {
|
interface LinesItemType {
|
||||||
name: string;
|
name: string;
|
||||||
country_code: string;
|
country_code: string;
|
||||||
@ -17,6 +16,316 @@ interface LinesItemType {
|
|||||||
}
|
}
|
||||||
type LinesDataType = [LinesItemType, LinesItemType];
|
type LinesDataType = [LinesItemType, LinesItemType];
|
||||||
type LinesType = [string, LinesDataType[]];
|
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 createCountryRipple = (countryCode: string, color?: string) => {
|
||||||
const coords = geoCoordMap[countryCode];
|
const coords = geoCoordMap[countryCode];
|
||||||
@ -28,6 +337,7 @@ const createCountryRipple = (countryCode: string, color?: string) => {
|
|||||||
color: color, // 添加颜色属性
|
color: color, // 添加颜色属性
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorldGeo = memo(
|
export const WorldGeo = memo(
|
||||||
({
|
({
|
||||||
dataInfo,
|
dataInfo,
|
||||||
@ -59,6 +369,40 @@ export const WorldGeo = memo(
|
|||||||
>([]);
|
>([]);
|
||||||
const labelContainerRef = useRef<HTMLDivElement | null>(null);
|
const labelContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const labelsRef = useRef<HTMLDivElement[]>([]);
|
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 mainToData = useMemo(() => {
|
||||||
const newList = [
|
const newList = [
|
||||||
dataInfo.passAuthentication,
|
dataInfo.passAuthentication,
|
||||||
@ -71,7 +415,7 @@ export const WorldGeo = memo(
|
|||||||
selectedApp && selectedApp ? [...newList, selectedApp] : newList ?? [];
|
selectedApp && selectedApp ? [...newList, selectedApp] : newList ?? [];
|
||||||
// 初始化数据数组 - 不再包含 startCountry
|
// 初始化数据数组 - 不再包含 startCountry
|
||||||
const data: any = [];
|
const data: any = [];
|
||||||
console.log(proxiesList,'proxiesList')
|
console.log(proxiesList, "proxiesList");
|
||||||
// 遍历代理列表
|
// 遍历代理列表
|
||||||
proxiesList.forEach((proxyItem: any) => {
|
proxiesList.forEach((proxyItem: any) => {
|
||||||
// 检查是否有数据数组
|
// 检查是否有数据数组
|
||||||
@ -107,165 +451,96 @@ export const WorldGeo = memo(
|
|||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
}, [dataInfo, selectedApp]);
|
}, [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 = () => {
|
const positionCustomTooltip = () => {
|
||||||
|
console.log("Positioning tooltip1", {
|
||||||
|
hasRef: !!customTooltipRef.current,
|
||||||
|
hasChart: !!proxyGeoRef.current,
|
||||||
|
});
|
||||||
|
|
||||||
if (!customTooltipRef.current || !proxyGeoRef.current) return;
|
if (!customTooltipRef.current || !proxyGeoRef.current) return;
|
||||||
// 找到US点
|
|
||||||
|
// 找到点
|
||||||
const coords = geoCoordMap[dataInfo.nestedEncryption?.[0]?.code ?? "GL"];
|
const coords = geoCoordMap[dataInfo.nestedEncryption?.[0]?.code ?? "GL"];
|
||||||
|
console.log("Tooltip1 coords:", coords);
|
||||||
|
|
||||||
if (!coords) return;
|
if (!coords) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 将地理坐标转换为屏幕坐标
|
// 将地理坐标转换为屏幕坐标
|
||||||
const screenCoord = proxyGeoRef.current.convertToPixel("geo", coords);
|
const screenCoord = proxyGeoRef.current.convertToPixel("geo", coords);
|
||||||
|
console.log("Tooltip1 screen coords:", screenCoord);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
screenCoord &&
|
screenCoord &&
|
||||||
Array.isArray(screenCoord) &&
|
Array.isArray(screenCoord) &&
|
||||||
screenCoord.length === 2
|
screenCoord.length === 2
|
||||||
) {
|
) {
|
||||||
// 设置提示框位置
|
// 设置提示框位置
|
||||||
customTooltipRef.current.style.left = `${screenCoord[0] + 232 + 7}px`;
|
const left = `${screenCoord[0] + 232 + 7}px`;
|
||||||
customTooltipRef.current.style.top = `${screenCoord[1] + 40 + 15}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) {
|
} catch (error) {
|
||||||
console.error("Error positioning tooltip:", error);
|
console.error("Error positioning tooltip1:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// 创建自定义提示框DOM元素
|
|
||||||
const createCustomTooltip2 = () => {
|
// 定位自定义提示框2 - 优化版本
|
||||||
// 如果已经存在自定义提示框,则移除它
|
|
||||||
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();
|
|
||||||
};
|
|
||||||
// 定位自定义提示框 - 优化版本
|
|
||||||
const positionCustomTooltip2 = () => {
|
const positionCustomTooltip2 = () => {
|
||||||
|
console.log("Positioning tooltip2", {
|
||||||
|
hasRef: !!customTooltip2Ref.current,
|
||||||
|
hasChart: !!proxyGeoRef.current,
|
||||||
|
});
|
||||||
|
|
||||||
if (!customTooltip2Ref.current || !proxyGeoRef.current) return;
|
if (!customTooltip2Ref.current || !proxyGeoRef.current) return;
|
||||||
// 找到US点
|
|
||||||
|
// 找到点
|
||||||
const coords =
|
const coords =
|
||||||
geoCoordMap[dataInfo.trafficObfuscation?.[0]?.code ?? "ZA"];
|
geoCoordMap[dataInfo.trafficObfuscation?.[0]?.code ?? "ZA"];
|
||||||
|
console.log("Tooltip2 coords:", coords);
|
||||||
|
|
||||||
if (!coords) return;
|
if (!coords) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 将地理坐标转换为屏幕坐标
|
// 将地理坐标转换为屏幕坐标
|
||||||
const screenCoord = proxyGeoRef.current.convertToPixel("geo", coords);
|
const screenCoord = proxyGeoRef.current.convertToPixel("geo", coords);
|
||||||
|
console.log("Tooltip2 screen coords:", screenCoord);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
screenCoord &&
|
screenCoord &&
|
||||||
Array.isArray(screenCoord) &&
|
Array.isArray(screenCoord) &&
|
||||||
screenCoord.length === 2
|
screenCoord.length === 2
|
||||||
) {
|
) {
|
||||||
// 设置提示框位置
|
// 设置提示框位置
|
||||||
customTooltip2Ref.current.style.left = `${
|
const left = `${screenCoord[0] - 626 + 20}px`;
|
||||||
screenCoord[0] - 626 + 20
|
const top = `${screenCoord[1] + 40 - 13}px`;
|
||||||
}px`;
|
console.log("Setting tooltip2 position:", { left, top });
|
||||||
customTooltip2Ref.current.style.top = `${
|
|
||||||
screenCoord[1] + 40 - 13
|
customTooltip2Ref.current.style.left = left;
|
||||||
}px`;
|
customTooltip2Ref.current.style.top = top;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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 = (
|
const getLineItem = (
|
||||||
preCode: string,
|
preCode: string,
|
||||||
nextCode: string,
|
nextCode: string,
|
||||||
@ -286,6 +561,7 @@ export const WorldGeo = memo(
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLine = () => {
|
const getLine = () => {
|
||||||
// 实现数据处理
|
// 实现数据处理
|
||||||
const solidData: LinesType[] = [["main", []]]; // 使用"main"替代startCountry.country_code
|
const solidData: LinesType[] = [["main", []]]; // 使用"main"替代startCountry.country_code
|
||||||
@ -345,7 +621,11 @@ export const WorldGeo = memo(
|
|||||||
}
|
}
|
||||||
// 检查是否应该绘制连线
|
// 检查是否应该绘制连线
|
||||||
if (currentItem.isLine !== false) {
|
if (currentItem.isLine !== false) {
|
||||||
const lineItem = getLineItem(countryCode, nextCountryCode, lineColor);
|
const lineItem = getLineItem(
|
||||||
|
countryCode,
|
||||||
|
nextCountryCode,
|
||||||
|
lineColor
|
||||||
|
);
|
||||||
solidData[0][1].push(lineItem);
|
solidData[0][1].push(lineItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -359,6 +639,7 @@ export const WorldGeo = memo(
|
|||||||
pointColors,
|
pointColors,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取连线经纬度数据
|
// 获取连线经纬度数据
|
||||||
const convertData = (data: LinesDataType[]) => {
|
const convertData = (data: LinesDataType[]) => {
|
||||||
const res = [];
|
const res = [];
|
||||||
@ -371,7 +652,7 @@ export const WorldGeo = memo(
|
|||||||
const toCountry = dataIndex?.[1]?.country_code ?? "";
|
const toCountry = dataIndex?.[1]?.country_code ?? "";
|
||||||
// 使用每条线自己的颜色
|
// 使用每条线自己的颜色
|
||||||
const lineColor = dataIndex?.[0]?.color || "#0ea5e9";
|
const lineColor = dataIndex?.[0]?.color || "#0ea5e9";
|
||||||
|
|
||||||
if (fromCoord && toCoord) {
|
if (fromCoord && toCoord) {
|
||||||
res.push({
|
res.push({
|
||||||
coords: [fromCoord, toCoord],
|
coords: [fromCoord, toCoord],
|
||||||
@ -379,7 +660,7 @@ export const WorldGeo = memo(
|
|||||||
color: lineColor, // 使用自定义颜色
|
color: lineColor, // 使用自定义颜色
|
||||||
},
|
},
|
||||||
// 保存颜色信息用于飞行特效
|
// 保存颜色信息用于飞行特效
|
||||||
color: lineColor
|
color: lineColor,
|
||||||
});
|
});
|
||||||
// 计算中点,考虑曲线的弧度
|
// 计算中点,考虑曲线的弧度
|
||||||
const curveness = -0.4; // 与飞线弧度相同
|
const curveness = -0.4; // 与飞线弧度相同
|
||||||
@ -405,6 +686,7 @@ export const WorldGeo = memo(
|
|||||||
// lineMidpointsRef.current = midpoints;
|
// lineMidpointsRef.current = midpoints;
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建双层点效果 - 大点
|
// 创建双层点效果 - 大点
|
||||||
const createDualLayerPoint = (
|
const createDualLayerPoint = (
|
||||||
lastExit: LinesItemType,
|
lastExit: LinesItemType,
|
||||||
@ -412,7 +694,7 @@ export const WorldGeo = memo(
|
|||||||
) => {
|
) => {
|
||||||
// 使用点自己的颜色
|
// 使用点自己的颜色
|
||||||
const pointColor = lastExit.color || "#0ea5e9";
|
const pointColor = lastExit.color || "#0ea5e9";
|
||||||
|
|
||||||
// 创建数据数组,用于两个散点图层
|
// 创建数据数组,用于两个散点图层
|
||||||
const pointData = lastExit
|
const pointData = lastExit
|
||||||
? [lastExit].map((v) => {
|
? [lastExit].map((v) => {
|
||||||
@ -421,7 +703,7 @@ export const WorldGeo = memo(
|
|||||||
value: v.value,
|
value: v.value,
|
||||||
datas: {
|
datas: {
|
||||||
country_code: v.country_code,
|
country_code: v.country_code,
|
||||||
color: pointColor // 保存颜色信息
|
color: pointColor, // 保存颜色信息
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@ -437,9 +719,9 @@ export const WorldGeo = memo(
|
|||||||
coordinateSystem: "geo",
|
coordinateSystem: "geo",
|
||||||
zlevel: 3,
|
zlevel: 3,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: function(params: any) {
|
color: function (params: any) {
|
||||||
return params.data.datas.color;
|
return params.data.datas.color;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
symbol: "circle",
|
symbol: "circle",
|
||||||
symbolSize: outerSize,
|
symbolSize: outerSize,
|
||||||
@ -493,6 +775,7 @@ export const WorldGeo = memo(
|
|||||||
} as echarts.SeriesOption,
|
} as echarts.SeriesOption,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加新方法:根据经纬度数组创建蓝色涟漪小点(不包含白色内层点)
|
// 添加新方法:根据经纬度数组创建蓝色涟漪小点(不包含白色内层点)
|
||||||
const createRipplePointsFromCoordinates = (
|
const createRipplePointsFromCoordinates = (
|
||||||
coordinates: [number, number][],
|
coordinates: [number, number][],
|
||||||
@ -500,7 +783,7 @@ export const WorldGeo = memo(
|
|||||||
color: string = "#01FF5E"
|
color: string = "#01FF5E"
|
||||||
) => {
|
) => {
|
||||||
if (!coordinates || coordinates.length === 0) return;
|
if (!coordinates || coordinates.length === 0) return;
|
||||||
|
|
||||||
// 只创建外层带涟漪效果的点
|
// 只创建外层带涟漪效果的点
|
||||||
series.push({
|
series.push({
|
||||||
type: "effectScatter",
|
type: "effectScatter",
|
||||||
@ -524,6 +807,7 @@ export const WorldGeo = memo(
|
|||||||
})),
|
})),
|
||||||
} as echarts.SeriesOption);
|
} as echarts.SeriesOption);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建路径点的双层效果
|
// 创建路径点的双层效果
|
||||||
const createPathPoints = (
|
const createPathPoints = (
|
||||||
dataItems: LinesDataType[],
|
dataItems: LinesDataType[],
|
||||||
@ -533,13 +817,13 @@ export const WorldGeo = memo(
|
|||||||
const pointData = dataItems.map((dataItem: LinesDataType) => {
|
const pointData = dataItems.map((dataItem: LinesDataType) => {
|
||||||
// 使用每个点自己的颜色
|
// 使用每个点自己的颜色
|
||||||
const pointColor = dataItem[0].color || "#0ea5e9";
|
const pointColor = dataItem[0].color || "#0ea5e9";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: dataItem[0].name,
|
name: dataItem[0].name,
|
||||||
value: geoCoordMap[dataItem[0].country_code],
|
value: geoCoordMap[dataItem[0].country_code],
|
||||||
datas: {
|
datas: {
|
||||||
country_code: dataItem[0].country_code,
|
country_code: dataItem[0].country_code,
|
||||||
color: pointColor // 保存颜色信息
|
color: pointColor, // 保存颜色信息
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -606,6 +890,7 @@ export const WorldGeo = memo(
|
|||||||
} as echarts.SeriesOption,
|
} as echarts.SeriesOption,
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建带自定义提示框的涟漪点
|
// 创建带自定义提示框的涟漪点
|
||||||
const createRipplePointsWithTooltip = (ripplePoints: any) => {
|
const createRipplePointsWithTooltip = (ripplePoints: any) => {
|
||||||
return {
|
return {
|
||||||
@ -662,6 +947,7 @@ export const WorldGeo = memo(
|
|||||||
})),
|
})),
|
||||||
} as echarts.SeriesOption;
|
} as echarts.SeriesOption;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 连线 series
|
// 连线 series
|
||||||
const getLianData = (series: echarts.SeriesOption[]) => {
|
const getLianData = (series: echarts.SeriesOption[]) => {
|
||||||
const { solidData, otherLineList, ripplePoints } = getLine();
|
const { solidData, otherLineList, ripplePoints } = getLine();
|
||||||
@ -694,13 +980,13 @@ export const WorldGeo = memo(
|
|||||||
if (item[1].length === 0) {
|
if (item[1].length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理每条线段
|
// 处理每条线段
|
||||||
item[1].forEach((lineSegment, index) => {
|
item[1].forEach((lineSegment, index) => {
|
||||||
const fromPoint = lineSegment[0];
|
const fromPoint = lineSegment[0];
|
||||||
const toPoint = lineSegment[1];
|
const toPoint = lineSegment[1];
|
||||||
const lineColor = fromPoint.color || "#0ea5e9";
|
const lineColor = fromPoint.color || "#0ea5e9";
|
||||||
|
|
||||||
// 添加单条飞行线
|
// 添加单条飞行线
|
||||||
series.push({
|
series.push({
|
||||||
name: `${item[0]}-${index}`,
|
name: `${item[0]}-${index}`,
|
||||||
@ -726,13 +1012,15 @@ export const WorldGeo = memo(
|
|||||||
opacity: 0.1,
|
opacity: 0.1,
|
||||||
color: lineColor, // 使用线段自己的颜色
|
color: lineColor, // 使用线段自己的颜色
|
||||||
},
|
},
|
||||||
data: convertData([[fromPoint, toPoint]]) as echarts.LinesSeriesOption["data"],
|
data: convertData([
|
||||||
|
[fromPoint, toPoint],
|
||||||
|
]) as echarts.LinesSeriesOption["data"],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加起点的双层效果
|
// 添加起点的双层效果
|
||||||
const startNodes = createDualLayerPoint(fromPoint, true);
|
const startNodes = createDualLayerPoint(fromPoint, true);
|
||||||
series.push(...startNodes);
|
series.push(...startNodes);
|
||||||
|
|
||||||
// 如果是最后一个线段,添加终点的双层效果
|
// 如果是最后一个线段,添加终点的双层效果
|
||||||
if (index === item[1].length - 1) {
|
if (index === item[1].length - 1) {
|
||||||
const endNodes = createDualLayerPoint(toPoint, true);
|
const endNodes = createDualLayerPoint(toPoint, true);
|
||||||
@ -740,7 +1028,7 @@ export const WorldGeo = memo(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
otherLineList.forEach((line: any) => {
|
otherLineList.forEach((line: any) => {
|
||||||
line.forEach((item: any) => {
|
line.forEach((item: any) => {
|
||||||
// 处理每条虚线段
|
// 处理每条虚线段
|
||||||
@ -748,7 +1036,7 @@ export const WorldGeo = memo(
|
|||||||
const fromPoint = lineSegment[0];
|
const fromPoint = lineSegment[0];
|
||||||
const toPoint = lineSegment[1];
|
const toPoint = lineSegment[1];
|
||||||
const lineColor = fromPoint.color || "#F0FFA2";
|
const lineColor = fromPoint.color || "#F0FFA2";
|
||||||
|
|
||||||
// 添加虚线
|
// 添加虚线
|
||||||
series.push({
|
series.push({
|
||||||
name: `${item[0]}-dashed-${index}`,
|
name: `${item[0]}-dashed-${index}`,
|
||||||
@ -765,13 +1053,15 @@ export const WorldGeo = memo(
|
|||||||
width: 0.5, // 飞线宽度
|
width: 0.5, // 飞线宽度
|
||||||
opacity: 0.6,
|
opacity: 0.6,
|
||||||
},
|
},
|
||||||
data: convertData([[fromPoint, toPoint]]) as echarts.LinesSeriesOption["data"],
|
data: convertData([
|
||||||
|
[fromPoint, toPoint],
|
||||||
|
]) as echarts.LinesSeriesOption["data"],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加起点的双层效果
|
// 添加起点的双层效果
|
||||||
const startNodes = createDualLayerPoint(fromPoint, false);
|
const startNodes = createDualLayerPoint(fromPoint, false);
|
||||||
series.push(...startNodes);
|
series.push(...startNodes);
|
||||||
|
|
||||||
// 如果是最后一个线段,添加终点的双层效果
|
// 如果是最后一个线段,添加终点的双层效果
|
||||||
if (index === item[1].length - 1) {
|
if (index === item[1].length - 1) {
|
||||||
const endNodes = createDualLayerPoint(toPoint, false);
|
const endNodes = createDualLayerPoint(toPoint, false);
|
||||||
@ -780,9 +1070,10 @@ export const WorldGeo = memo(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建A点和B点,并添加飞线和标签
|
// 创建A点和B点,并添加飞线和标签
|
||||||
const createSpecialPoints = (series: echarts.SeriesOption[]) => {
|
const createSpecialPoints = (series: echarts.SeriesOption[]) => {
|
||||||
// 定义点A和点B的坐标
|
// 定义点A和点B的坐标
|
||||||
@ -926,6 +1217,7 @@ export const WorldGeo = memo(
|
|||||||
});
|
});
|
||||||
return series;
|
return series;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOption = () => {
|
const getOption = () => {
|
||||||
const series: echarts.SeriesOption[] = [];
|
const series: echarts.SeriesOption[] = [];
|
||||||
getLianData(series);
|
getLianData(series);
|
||||||
@ -1015,6 +1307,7 @@ export const WorldGeo = memo(
|
|||||||
};
|
};
|
||||||
return option;
|
return option;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建DOM标签
|
// 创建DOM标签
|
||||||
const createDOMLabels = () => {
|
const createDOMLabels = () => {
|
||||||
// 清除现有标签
|
// 清除现有标签
|
||||||
@ -1079,6 +1372,7 @@ export const WorldGeo = memo(
|
|||||||
// 更新标签位置
|
// 更新标签位置
|
||||||
updateLabelPositions();
|
updateLabelPositions();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新标签位置
|
// 更新标签位置
|
||||||
const updateLabelPositions = () => {
|
const updateLabelPositions = () => {
|
||||||
if (!proxyGeoRef.current || !labelContainerRef.current) return;
|
if (!proxyGeoRef.current || !labelContainerRef.current) return;
|
||||||
@ -1095,10 +1389,20 @@ export const WorldGeo = memo(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
proxyGeoRef.current?.resize();
|
proxyGeoRef.current?.resize();
|
||||||
updateLabelPositions();
|
updateLabelPositions();
|
||||||
|
|
||||||
|
// 重新定位tooltip
|
||||||
|
if (showTooltip1) {
|
||||||
|
positionCustomTooltip();
|
||||||
|
}
|
||||||
|
if (showTooltip2) {
|
||||||
|
positionCustomTooltip2();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
preMainToData.current?.some(
|
preMainToData.current?.some(
|
||||||
(item, index) => item.country_code !== mainToData[index]?.country_code
|
(item, index) => item.country_code !== mainToData[index]?.country_code
|
||||||
@ -1109,6 +1413,7 @@ export const WorldGeo = memo(
|
|||||||
// 创建DOM标签
|
// 创建DOM标签
|
||||||
setTimeout(createDOMLabels, 100);
|
setTimeout(createDOMLabels, 100);
|
||||||
}, [dataInfo, mainToData]);
|
}, [dataInfo, mainToData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const chartDom = document.getElementById("screenGeo");
|
const chartDom = document.getElementById("screenGeo");
|
||||||
proxyGeoRef.current = echarts.init(chartDom);
|
proxyGeoRef.current = echarts.init(chartDom);
|
||||||
@ -1135,27 +1440,81 @@ export const WorldGeo = memo(
|
|||||||
proxyGeoRef.current = null;
|
proxyGeoRef.current = null;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 修改处理tooltip的显示和隐藏的逻辑
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log("Tooltip effect triggered:", { tooltipClosed, tooltipType });
|
||||||
|
|
||||||
if (tooltipClosed) {
|
if (tooltipClosed) {
|
||||||
createCustomTooltip();
|
if (tooltipType === "NESTED_ENCRYPTION") {
|
||||||
createCustomTooltip2();
|
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,
|
tooltipClosed,
|
||||||
tooltipType,
|
tooltipType,
|
||||||
dataInfo.nestedEncryption,
|
dataInfo.nestedEncryption,
|
||||||
dataInfo.trafficObfuscation,
|
dataInfo.trafficObfuscation,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 在地图初始化后定位tooltip
|
||||||
|
useEffect(() => {
|
||||||
|
positionCustomTooltip();
|
||||||
|
positionCustomTooltip2();
|
||||||
|
}, [showTooltip1, showTooltip2]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 h-full flex flex-col">
|
<div className="flex-1 h-full flex flex-col">
|
||||||
<div id="screenGeo" className="flex-1"></div>
|
<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>
|
</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 {}
|
||||||
// .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);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-font {
|
.custom-font {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user