fix:bug
This commit is contained in:
parent
e80e4035a0
commit
2efe64862d
5
src/assets/svg/layout/anti-dark-analysis-network.svg
Normal file
5
src/assets/svg/layout/anti-dark-analysis-network.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon / 打击节点">
|
||||
<path id="Icon " fill-rule="evenodd" clip-rule="evenodd" d="M11.1652 1.11022C11.5045 1.25753 11.7061 1.61092 11.6602 1.978L10.944 7.70797L16.1065 7.70797C16.3073 7.70793 16.5107 7.70789 16.6769 7.72294C16.8341 7.73717 17.135 7.77482 17.3961 7.98574C17.6949 8.22721 17.8661 8.59267 17.8603 8.97682C17.8552 9.31237 17.6914 9.56768 17.6018 9.69753C17.5069 9.83483 17.3767 9.99106 17.2481 10.1453L9.80685 19.0748C9.57002 19.359 9.17419 19.453 8.83484 19.3057C8.49549 19.1584 8.29388 18.805 8.33977 18.4379L9.05602 12.708L3.89353 12.708C3.6927 12.708 3.48929 12.7081 3.3231 12.693C3.16593 12.6788 2.86495 12.6411 2.60394 12.4302C2.30513 12.1887 2.13395 11.8233 2.13975 11.4391C2.1448 11.1036 2.30856 10.8483 2.39825 10.7184C2.49309 10.5811 2.62334 10.4249 2.75194 10.2706C2.75831 10.263 2.76468 10.2553 2.77103 10.2477L10.1932 1.34115C10.43 1.05696 10.8258 0.962915 11.1652 1.11022ZM4.2792 11.0413H10C10.239 11.0413 10.4665 11.1439 10.6247 11.3231C10.7829 11.5023 10.8565 11.7408 10.8269 11.978L10.3461 15.8243L15.7208 9.37464H10C9.76098 9.37464 9.53346 9.272 9.37527 9.09281C9.21708 8.91362 9.14346 8.67513 9.1731 8.43794L9.65388 4.59169L4.2792 11.0413Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@ -11,6 +11,7 @@ import ChevronDownSvg from "@/assets/svg/layout/chevron-down.svg?react";
|
||||
import Decentralized from "@/assets/svg/layout/decentralized.svg?react";
|
||||
import PoolSvg from '@/assets/svg/layout/pool.svg?react'
|
||||
import HomeSvg from '@/assets/svg/layout/home.svg?react'
|
||||
import AntiDarkAnalysisNetworkSvg from '@/assets/svg/layout/anti-dark-analysis-network.svg?react'
|
||||
import "./index.scss";
|
||||
|
||||
import type { RootState } from "@/store";
|
||||
@ -37,6 +38,11 @@ export default function Layout() {
|
||||
title: "面向溯源对抗的数据转发",
|
||||
icon: <PoolSvg className="w-5 h-5" />,
|
||||
},
|
||||
{
|
||||
id: "anti-dark-analysis-network",
|
||||
title: "抗暗特征分析的隐匿网络应用",
|
||||
icon: <AntiDarkAnalysisNetworkSvg className="w-5 h-5" />,
|
||||
},
|
||||
// {
|
||||
// id: 'proxies',
|
||||
// title: '节点池',
|
||||
@ -74,7 +80,7 @@ export default function Layout() {
|
||||
to={"/" + item.id}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"px-[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] "
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,949 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
);
|
||||
@ -0,0 +1,876 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
);
|
||||
1154
src/pages/anti-dark-analysis-network/components/world-geo.tsx
Normal file
1154
src/pages/anti-dark-analysis-network/components/world-geo.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1175
src/pages/anti-dark-analysis-network/data/index.ts
Normal file
1175
src/pages/anti-dark-analysis-network/data/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
449
src/pages/anti-dark-analysis-network/data/mockData.ts
Normal file
449
src/pages/anti-dark-analysis-network/data/mockData.ts
Normal file
@ -0,0 +1,449 @@
|
||||
|
||||
export const TRAFFIC_OBFUSCATION = {
|
||||
type: "NESTED_ENCRYPTION",
|
||||
name: "流量混淆",
|
||||
code: "ZA",
|
||||
data: [
|
||||
{
|
||||
country_code: "gl",
|
||||
ingress_country_code: "za",
|
||||
},
|
||||
{
|
||||
country_code: "za",
|
||||
ingress_country_code: "dz",
|
||||
},
|
||||
{
|
||||
country_code: "dz",
|
||||
ingress_country_code: "ru",
|
||||
},
|
||||
{
|
||||
country_code: "dz",
|
||||
ingress_country_code: "cn",
|
||||
},
|
||||
],
|
||||
isLine: true,
|
||||
};
|
||||
export const NESTED_ENCRYPTION = {
|
||||
type: "NESTED_ENCRYPTION",
|
||||
name: "嵌套加密",
|
||||
code: "GL",
|
||||
data: [
|
||||
{
|
||||
country_code: "gl",
|
||||
ingress_country_code: "br",
|
||||
},
|
||||
{
|
||||
country_code: "br",
|
||||
ingress_country_code: "dz",
|
||||
},
|
||||
{
|
||||
country_code: "dz",
|
||||
ingress_country_code: "ru",
|
||||
},
|
||||
{
|
||||
country_code: "dz",
|
||||
ingress_country_code: "cn",
|
||||
},
|
||||
],
|
||||
isLine: true,
|
||||
};
|
||||
|
||||
export const DYNAMIC_ROUTE_GENERATOR = [
|
||||
{
|
||||
type: "DYNAMIC_ROUTE_GENERATOR",
|
||||
name: "动态路由生成",
|
||||
data: [
|
||||
{
|
||||
country_code: "us",
|
||||
ingress_country_code: "ca",
|
||||
},
|
||||
{
|
||||
country_code: "ca",
|
||||
ingress_country_code: "gl",
|
||||
},
|
||||
{
|
||||
country_code: "gl",
|
||||
ingress_country_code: "by",
|
||||
},
|
||||
{
|
||||
country_code: "dz",
|
||||
ingress_country_code: "cn",
|
||||
},
|
||||
],
|
||||
color: "#48D3D5",
|
||||
isLine: true,
|
||||
},
|
||||
{
|
||||
type: "DYNAMIC_ROUTE_GENERATOR",
|
||||
name: "动态路由生成2",
|
||||
data: [
|
||||
{
|
||||
country_code: "br",
|
||||
ingress_country_code: "ml",
|
||||
},
|
||||
{
|
||||
country_code: "ml",
|
||||
ingress_country_code: "ly",
|
||||
},
|
||||
{
|
||||
country_code: "ly",
|
||||
ingress_country_code: "cn",
|
||||
},
|
||||
{
|
||||
country_code: "cn",
|
||||
ingress_country_code: "ru",
|
||||
},
|
||||
],
|
||||
color: "#50FE35",
|
||||
isLine: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const APP_DIVERSION = [
|
||||
{
|
||||
name: "Netflix",
|
||||
color: "#DC2626",
|
||||
data: [
|
||||
{
|
||||
country_code: "mg",
|
||||
ingress_country_code: "ru",
|
||||
},
|
||||
{
|
||||
country_code: "ru",
|
||||
ingress_country_code: "fr",
|
||||
},
|
||||
{
|
||||
country_code: "fr",
|
||||
ingress_country_code: "br",
|
||||
},
|
||||
{
|
||||
country_code: "br",
|
||||
ingress_country_code: "us",
|
||||
},
|
||||
],
|
||||
isLine: true,
|
||||
},
|
||||
{
|
||||
name: "Spotify",
|
||||
color: "#22C55E",
|
||||
data: [
|
||||
{
|
||||
country_code: "jp",
|
||||
ingress_country_code: "au",
|
||||
},
|
||||
{
|
||||
country_code: "au",
|
||||
ingress_country_code: "za",
|
||||
},
|
||||
{
|
||||
country_code: "za",
|
||||
ingress_country_code: "de",
|
||||
},
|
||||
{
|
||||
country_code: "de",
|
||||
ingress_country_code: "ca",
|
||||
},
|
||||
],
|
||||
isLine: true,
|
||||
},
|
||||
{
|
||||
name: "Instagram",
|
||||
color: "#8B5CF6",
|
||||
data: [
|
||||
{
|
||||
country_code: "it",
|
||||
ingress_country_code: "in",
|
||||
},
|
||||
{
|
||||
country_code: "in",
|
||||
ingress_country_code: "mx",
|
||||
},
|
||||
{
|
||||
country_code: "mx",
|
||||
ingress_country_code: "se",
|
||||
},
|
||||
{
|
||||
country_code: "se",
|
||||
ingress_country_code: "sg",
|
||||
},
|
||||
],
|
||||
isLine: true,
|
||||
},
|
||||
{
|
||||
name: "Telegram",
|
||||
color: "#2563EB",
|
||||
data: [
|
||||
{
|
||||
country_code: "ar",
|
||||
ingress_country_code: "nl",
|
||||
},
|
||||
{
|
||||
country_code: "nl",
|
||||
ingress_country_code: "kr",
|
||||
},
|
||||
{
|
||||
country_code: "kr",
|
||||
ingress_country_code: "eg",
|
||||
},
|
||||
{
|
||||
country_code: "eg",
|
||||
ingress_country_code: "nz",
|
||||
},
|
||||
],
|
||||
isLine: true,
|
||||
},
|
||||
{
|
||||
name: "Google",
|
||||
color: "#3B82F6",
|
||||
data: [
|
||||
{
|
||||
country_code: "ch",
|
||||
ingress_country_code: "br",
|
||||
},
|
||||
{
|
||||
country_code: "br",
|
||||
ingress_country_code: "hk",
|
||||
},
|
||||
{
|
||||
country_code: "hk",
|
||||
ingress_country_code: "no",
|
||||
},
|
||||
{
|
||||
country_code: "no",
|
||||
ingress_country_code: "ae",
|
||||
},
|
||||
],
|
||||
isLine: true,
|
||||
},
|
||||
{
|
||||
name: "Gmail",
|
||||
color: "#22C55E",
|
||||
data: [
|
||||
{
|
||||
country_code: "es",
|
||||
ingress_country_code: "cn",
|
||||
},
|
||||
{
|
||||
country_code: "cn",
|
||||
ingress_country_code: "co",
|
||||
},
|
||||
{
|
||||
country_code: "co",
|
||||
ingress_country_code: "fi",
|
||||
},
|
||||
{
|
||||
country_code: "fi",
|
||||
ingress_country_code: "id",
|
||||
},
|
||||
],
|
||||
isLine: true,
|
||||
},
|
||||
{
|
||||
name: "Amazon",
|
||||
color: "#EAB308",
|
||||
data: [
|
||||
{
|
||||
country_code: "gb",
|
||||
ingress_country_code: "th",
|
||||
},
|
||||
{
|
||||
country_code: "th",
|
||||
ingress_country_code: "cl",
|
||||
},
|
||||
{
|
||||
country_code: "cl",
|
||||
ingress_country_code: "be",
|
||||
},
|
||||
{
|
||||
country_code: "be",
|
||||
ingress_country_code: "ph",
|
||||
},
|
||||
],
|
||||
isLine: true,
|
||||
},
|
||||
{
|
||||
name: "Ebay",
|
||||
color: "#3B82F6",
|
||||
data: [
|
||||
{
|
||||
country_code: "pl",
|
||||
ingress_country_code: "my",
|
||||
},
|
||||
{
|
||||
country_code: "my",
|
||||
ingress_country_code: "pe",
|
||||
},
|
||||
{
|
||||
country_code: "pe",
|
||||
ingress_country_code: "dk",
|
||||
},
|
||||
{
|
||||
country_code: "dk",
|
||||
ingress_country_code: "ng",
|
||||
},
|
||||
],
|
||||
isLine: true,
|
||||
},
|
||||
{
|
||||
name: "AppleNews",
|
||||
color: "#EF4444",
|
||||
data: [
|
||||
{
|
||||
country_code: "ie",
|
||||
ingress_country_code: "vn",
|
||||
},
|
||||
{
|
||||
country_code: "vn",
|
||||
ingress_country_code: "ma",
|
||||
},
|
||||
{
|
||||
country_code: "ma",
|
||||
ingress_country_code: "at",
|
||||
},
|
||||
{
|
||||
country_code: "at",
|
||||
ingress_country_code: "tw",
|
||||
},
|
||||
],
|
||||
isLine: true,
|
||||
},
|
||||
{
|
||||
name: "CNN",
|
||||
color: "#EF4444",
|
||||
data: [
|
||||
{
|
||||
country_code: "ua",
|
||||
ingress_country_code: "sa",
|
||||
},
|
||||
{
|
||||
country_code: "sa",
|
||||
ingress_country_code: "gr",
|
||||
},
|
||||
{
|
||||
country_code: "gr",
|
||||
ingress_country_code: "pk",
|
||||
},
|
||||
{
|
||||
country_code: "pk",
|
||||
ingress_country_code: "pt",
|
||||
},
|
||||
],
|
||||
isLine: true,
|
||||
},
|
||||
{
|
||||
name: "Browser",
|
||||
color: "#8B5CF6",
|
||||
data: [
|
||||
{
|
||||
country_code: "il",
|
||||
ingress_country_code: "ro",
|
||||
},
|
||||
{
|
||||
country_code: "ro",
|
||||
ingress_country_code: "nz",
|
||||
},
|
||||
{
|
||||
country_code: "nz",
|
||||
ingress_country_code: "tr",
|
||||
},
|
||||
{
|
||||
country_code: "tr",
|
||||
ingress_country_code: "ca",
|
||||
},
|
||||
],
|
||||
isLine: true,
|
||||
},
|
||||
{
|
||||
name: "YouTube",
|
||||
color: "#EF4444",
|
||||
data: [
|
||||
{
|
||||
country_code: "cz",
|
||||
ingress_country_code: "sg",
|
||||
},
|
||||
{
|
||||
country_code: "sg",
|
||||
ingress_country_code: "ar",
|
||||
},
|
||||
{
|
||||
country_code: "ar",
|
||||
ingress_country_code: "hu",
|
||||
},
|
||||
{
|
||||
country_code: "hu",
|
||||
ingress_country_code: "jp",
|
||||
},
|
||||
],
|
||||
isLine: true,
|
||||
},
|
||||
{
|
||||
name: "Facebook",
|
||||
color: "#3B82F6",
|
||||
data: [
|
||||
{
|
||||
country_code: "is",
|
||||
ingress_country_code: "za",
|
||||
},
|
||||
{
|
||||
country_code: "za",
|
||||
ingress_country_code: "mx",
|
||||
},
|
||||
{
|
||||
country_code: "mx",
|
||||
ingress_country_code: "it",
|
||||
},
|
||||
{
|
||||
country_code: "it",
|
||||
ingress_country_code: "kr",
|
||||
},
|
||||
],
|
||||
isLine: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const PASS_AUTHENTICATION = {
|
||||
type: "PASS_AUTHENTICATION",
|
||||
name: "通信认证",
|
||||
startPoint: "GL",
|
||||
endPoint: "CA",
|
||||
authenticationPoint: [
|
||||
[-103.346771, 54.130366],
|
||||
[-120.346771, 52.130366],
|
||||
[-108.346771, 48.130366],
|
||||
[-98.346771, 46.130366],
|
||||
[-106.346771, 48.450366],
|
||||
[-101.346771, 53.130366],
|
||||
[-123.346771, 58.130366],
|
||||
[-111.346771, 65.443366],
|
||||
[-108.346771, 54.130366],
|
||||
[-116.346771, 59.130366],
|
||||
[-97.346771, 61.130366],
|
||||
[-95.346771, 63.130366],
|
||||
[-113.346771, 58.840366],
|
||||
[-99.346771, 59.130366],
|
||||
[-102.346771, 68.130366],
|
||||
],
|
||||
data: [
|
||||
{
|
||||
country_code: "gl",
|
||||
ingress_country_code: "dz",
|
||||
},
|
||||
{
|
||||
country_code: "br",
|
||||
ingress_country_code: "dz",
|
||||
},
|
||||
{
|
||||
country_code: "dz",
|
||||
ingress_country_code: "ru",
|
||||
},
|
||||
{
|
||||
country_code: "dz",
|
||||
ingress_country_code: "cn",
|
||||
},
|
||||
{
|
||||
country_code: "ru",
|
||||
ingress_country_code: "za",
|
||||
},
|
||||
],
|
||||
isLine: true,
|
||||
}
|
||||
211
src/pages/anti-dark-analysis-network/index.scss
Normal file
211
src/pages/anti-dark-analysis-network/index.scss
Normal file
@ -0,0 +1,211 @@
|
||||
// // 添加到 index.scss
|
||||
.decentralized {
|
||||
background-color: #0f172a;
|
||||
|
||||
// background-image: linear-gradient(180deg, #172554 0%, #0A0F2A 100%);
|
||||
.box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url("@/assets/image/line-bg.png");
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
mix-blend-mode: lighten;
|
||||
}
|
||||
|
||||
.web3-line::after {
|
||||
content: "";
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background-color: #7D82FF;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0px;
|
||||
z-index: 999;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// // 轮播容器样式
|
||||
.carousel-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3rem; // 对应原来的gap-12
|
||||
// width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.bt1 {
|
||||
display: flex;
|
||||
padding: var(--8-spacing-04, 8px) var(--16-spacing-08, 16px);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--8-spacing-04, 8px);
|
||||
border-radius: var(--radius-6, 6px);
|
||||
border: 1px solid var(--Colors-Bluepurple-600, #4136F5);
|
||||
background: var(--button-wireframe-button-wireframe, rgba(9, 9, 11, 0.00));
|
||||
box-shadow: 0px 0px 4px 0px var(--Colors-Bluepurple-500, #5457FF), 0px 0px 10px 0px var(--Colors-Bluepurple-600, #4136F5);
|
||||
color: var(--text-text-primary-900, #FFF);
|
||||
/* Text/Medium/T5文本1 */
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
// line-height: 24px;
|
||||
/* 171.429% */
|
||||
}
|
||||
|
||||
|
||||
|
||||
.bt2 {
|
||||
|
||||
display: flex;
|
||||
padding: var(--8-spacing-04, 8px) var(--16-spacing-08, 16px);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--8-spacing-04, 8px);
|
||||
border-radius: var(--radius-6, 6px);
|
||||
border: 1px solid var(--Colors-Rose-600, #E11D48);
|
||||
background: var(--button-wireframe-button-wireframe, rgba(255, 255, 255, 0.00));
|
||||
box-shadow: 0px 0px 4px 0px var(--Colors-Rose-600, #E11D48), 0px 0px 10px 0px var(--Colors-Rose-600, #E11D48);
|
||||
color: var(--text-text-primary-900, #FFF);
|
||||
|
||||
font-family: "PingFang SC";
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
// line-height: 24px;
|
||||
}
|
||||
|
||||
.tip-box {
|
||||
position: relative;
|
||||
width: 626px;
|
||||
height: 281px;
|
||||
padding: 20.85px 20.353px;
|
||||
background: rgba(0, 11.82, 33.10, 0.10);
|
||||
border-radius: 8px;
|
||||
outline: 0.46px solid white;
|
||||
outline-offset: -0.46px;
|
||||
backdrop-filter: blur(5.50px);
|
||||
|
||||
.close-icon , .close-icon2 {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 100%;
|
||||
color: #FFF;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.encryption-img {
|
||||
width: 526px;
|
||||
height: 241px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.traffic-obfuscation-img{
|
||||
width: 597px;
|
||||
height: 241px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
||||
.line-img {
|
||||
width: 312.221px;
|
||||
}
|
||||
|
||||
.line-img-left{
|
||||
width: 216.86px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.fill {
|
||||
width: 9.165px;
|
||||
height: 9.165px;
|
||||
border-radius: 50%;
|
||||
background-color: #18E4FF;
|
||||
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;
|
||||
// }
|
||||
309
src/pages/anti-dark-analysis-network/index.tsx
Normal file
309
src/pages/anti-dark-analysis-network/index.tsx
Normal file
@ -0,0 +1,309 @@
|
||||
import { useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { WorldGeo } from "./components/world-geo";
|
||||
|
||||
import NetflixSvg from "@/assets/svg/anti-forensics-forwarding/Netflix.svg?react";
|
||||
import NetflixActiveSvg from "@/assets/svg/anti-forensics-forwarding/NetflixActive.svg?react";
|
||||
import SpotifySvg from "@/assets/svg/anti-forensics-forwarding/Spotify.svg?react";
|
||||
import SpotifyActiveSvg from "@/assets/svg/anti-forensics-forwarding/SpotifyActive.svg?react";
|
||||
import InstagramSvg from "@/assets/svg/anti-forensics-forwarding/Instagram.svg?react";
|
||||
import InstagramActiveSvg from "@/assets/svg/anti-forensics-forwarding/InstagramActive.svg?react";
|
||||
import TelegramSvg from "@/assets/svg/anti-forensics-forwarding/Telegram.svg?react";
|
||||
import TelegramActiveSvg from "@/assets/svg/anti-forensics-forwarding/TelegramActive.svg?react";
|
||||
import GoogleSvg from "@/assets/svg/anti-forensics-forwarding/Google.svg?react";
|
||||
import GoogleActiveSvg from "@/assets/svg/anti-forensics-forwarding/GoogleActive.svg?react";
|
||||
import GmailSvg from "@/assets/svg/anti-forensics-forwarding/Gmail.svg?react";
|
||||
import GmailActiveSvg from "@/assets/svg/anti-forensics-forwarding/GmailActive.svg?react";
|
||||
import AmazonSvg from "@/assets/svg/anti-forensics-forwarding/Amazon.svg?react";
|
||||
import AmazonActiveSvg from "@/assets/svg/anti-forensics-forwarding/AmazonActive.svg?react";
|
||||
import EbaySvg from "@/assets/svg/anti-forensics-forwarding/Ebay.svg?react";
|
||||
import EbayActiveSvg from "@/assets/svg/anti-forensics-forwarding/EbayActive.svg?react";
|
||||
import AppleNewsSvg from "@/assets/svg/anti-forensics-forwarding/AppleNews.svg?react";
|
||||
import AppleNewsActiveSvg from "@/assets/svg/anti-forensics-forwarding/AppleNewsActive.svg?react";
|
||||
import CNNSvg from "@/assets/svg/anti-forensics-forwarding/CNN.svg?react";
|
||||
import CNNActiveSvg from "@/assets/svg/anti-forensics-forwarding/CNNActive.svg?react";
|
||||
import BrowserSvg from "@/assets/svg/anti-forensics-forwarding/Browser.svg?react";
|
||||
import BrowserActiveSvg from "@/assets/svg/anti-forensics-forwarding/BrowserActive.svg?react";
|
||||
import YouTubeSvg from "@/assets/svg/anti-forensics-forwarding/YouTube.svg?react";
|
||||
import YouTubeActiveSvg from "@/assets/svg/anti-forensics-forwarding/YouTubeActive.svg?react";
|
||||
import FacebookSvg from "@/assets/svg/anti-forensics-forwarding/Facebook.svg?react";
|
||||
import FacebookActiveSvg from "@/assets/svg/anti-forensics-forwarding/FacebookActive.svg?react";
|
||||
import { RootState } from "@/store";
|
||||
|
||||
|
||||
|
||||
import "./index.scss";
|
||||
import {
|
||||
getApplicationDiversion,
|
||||
getDynamicRouteGeneration,
|
||||
getNestedEncryption,
|
||||
getPassAuthentication,
|
||||
getTrafficObfuscation,
|
||||
} from "@/api/flying-line";
|
||||
|
||||
export const DIALOGTYPE = {
|
||||
ADDNode: {
|
||||
title: "添加节点",
|
||||
desc: "",
|
||||
successText: "添加",
|
||||
},
|
||||
AddNetwork: {
|
||||
title: "构建网络",
|
||||
desc: "",
|
||||
successText: "构建",
|
||||
},
|
||||
};
|
||||
|
||||
export const NODEDIALOGTYPE = {
|
||||
ClearFailNode: {
|
||||
title: "清除掉线节点",
|
||||
desc: "",
|
||||
successText: "清除",
|
||||
},
|
||||
ClearWargingNode: {
|
||||
title: "恶意节点",
|
||||
desc: "",
|
||||
successText: "清除",
|
||||
},
|
||||
};
|
||||
|
||||
export const Apps = [
|
||||
{
|
||||
name: "Netflix",
|
||||
icon: NetflixSvg,
|
||||
activeIcon: NetflixActiveSvg,
|
||||
},
|
||||
{
|
||||
name: "Spotify",
|
||||
icon: SpotifySvg,
|
||||
activeIcon: SpotifyActiveSvg,
|
||||
},
|
||||
{
|
||||
name: "Instagram",
|
||||
icon: InstagramSvg,
|
||||
activeIcon: InstagramActiveSvg,
|
||||
},
|
||||
{
|
||||
name: "Telegram",
|
||||
icon: TelegramSvg,
|
||||
activeIcon: TelegramActiveSvg,
|
||||
},
|
||||
{
|
||||
name: "Google",
|
||||
icon: GoogleSvg,
|
||||
activeIcon: GoogleActiveSvg,
|
||||
},
|
||||
{
|
||||
name: "Gmail",
|
||||
icon: GmailSvg,
|
||||
activeIcon: GmailActiveSvg,
|
||||
},
|
||||
{
|
||||
name: "Amazon",
|
||||
icon: AmazonSvg,
|
||||
activeIcon: AmazonActiveSvg,
|
||||
},
|
||||
{
|
||||
name: "Ebay",
|
||||
icon: EbaySvg,
|
||||
activeIcon: EbayActiveSvg,
|
||||
},
|
||||
{
|
||||
name: "AppleNews",
|
||||
icon: AppleNewsSvg,
|
||||
activeIcon: AppleNewsActiveSvg,
|
||||
},
|
||||
{
|
||||
name: "CNN",
|
||||
icon: CNNSvg,
|
||||
activeIcon: CNNActiveSvg,
|
||||
},
|
||||
{
|
||||
name: "Browser",
|
||||
icon: BrowserSvg,
|
||||
activeIcon: BrowserActiveSvg,
|
||||
},
|
||||
{
|
||||
name: "YouTube",
|
||||
icon: YouTubeSvg,
|
||||
activeIcon: YouTubeActiveSvg,
|
||||
},
|
||||
{
|
||||
name: "Facebook",
|
||||
icon: FacebookSvg,
|
||||
activeIcon: FacebookActiveSvg,
|
||||
},
|
||||
];
|
||||
|
||||
export const CONST_TOOLTIP_TYPE = {
|
||||
NESTED_ENCRYPTION: {
|
||||
type: "NESTED_ENCRYPTION",
|
||||
title: "嵌套加密",
|
||||
},
|
||||
TRAFFIC_OBFUSCATION: {
|
||||
type: "TRAFFIC_OBFUSCATION",
|
||||
title: "流量混淆",
|
||||
},
|
||||
DYNAMIC_ROUTE_GENERATOR: {
|
||||
type: "DYNAMIC_ROUTE_GENERATOR",
|
||||
title: "动态路由生成",
|
||||
},
|
||||
// 应用分流
|
||||
APP_DIVERSION: {
|
||||
type: "APP_DIVERSION",
|
||||
title: "应用分流",
|
||||
},
|
||||
// 通信认证
|
||||
PASS_AUTHENTICATION: {
|
||||
type: "PASS_AUTHENTICATION",
|
||||
title: "通信认证",
|
||||
},
|
||||
};
|
||||
|
||||
const AntiDarkAnalysisNetwork = () => {
|
||||
const { newHomeProxies } = useSelector(
|
||||
(state: RootState) => state.web3Reducer
|
||||
);
|
||||
|
||||
const [tooltipType, setTooltipType] = useState(
|
||||
CONST_TOOLTIP_TYPE.PASS_AUTHENTICATION.type
|
||||
);
|
||||
const [tooltipClosed, setTooltipClosed] = useState(false);
|
||||
|
||||
const [selectedApp, setSelectedApp] = useState<any>(null);
|
||||
const [dataInfo, setDataInfo] = useState<any>(null);
|
||||
|
||||
const currentValue = useMemo(() => {
|
||||
let value = dataInfo;
|
||||
|
||||
switch (tooltipType) {
|
||||
case CONST_TOOLTIP_TYPE.APP_DIVERSION.type:
|
||||
value = selectedApp ? [selectedApp] : [];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
}, [tooltipType, selectedApp,dataInfo]);
|
||||
|
||||
const handleClickApp = (item: any) => {
|
||||
setSelectedApp(item);
|
||||
};
|
||||
|
||||
const getDataInfo = async () => {
|
||||
let value = [];
|
||||
|
||||
switch (tooltipType) {
|
||||
case CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.type:
|
||||
const nestedEncryption = await getNestedEncryption();
|
||||
value = [nestedEncryption.data];
|
||||
break;
|
||||
case CONST_TOOLTIP_TYPE.TRAFFIC_OBFUSCATION.type:
|
||||
const trafficObfuscation = await getTrafficObfuscation();
|
||||
value = [trafficObfuscation.data];
|
||||
break;
|
||||
case CONST_TOOLTIP_TYPE.DYNAMIC_ROUTE_GENERATOR.type:
|
||||
const dynamicRouteGeneration = await getDynamicRouteGeneration();
|
||||
value = dynamicRouteGeneration.data;
|
||||
break;
|
||||
// case CONST_TOOLTIP_TYPE.APP_DIVERSION.type:
|
||||
// const applicationDiversion = await getApplicationDiversion();
|
||||
|
||||
// value = selectedApp ? [selectedApp] : [];
|
||||
// break;
|
||||
case CONST_TOOLTIP_TYPE.PASS_AUTHENTICATION.type:
|
||||
const passAuthentication = await getPassAuthentication();
|
||||
value = [passAuthentication.data];
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
console.log(value,'valuevalue')
|
||||
setDataInfo(value);
|
||||
};
|
||||
|
||||
const [appData, setAppData] = useState<any>([]);
|
||||
const appDiversion = useMemo(() => {
|
||||
return Apps.map((item) => {
|
||||
const findApp = appData.find(
|
||||
(appItem:any) => item.name === appItem.name
|
||||
);
|
||||
return {
|
||||
...item,
|
||||
...findApp,
|
||||
};
|
||||
});
|
||||
}, [appData]);
|
||||
const initData = async () => {
|
||||
const applicationDiversion = await getApplicationDiversion();
|
||||
|
||||
setAppData(applicationDiversion.data);
|
||||
setTooltipType(CONST_TOOLTIP_TYPE.TRAFFIC_OBFUSCATION.type);
|
||||
setTooltipClosed(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getDataInfo();
|
||||
},[tooltipType])
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
() => {
|
||||
setTooltipClosed(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="decentralized w-full h-full flex flex-col relative">
|
||||
<div className="flex items-center gap-[60px] absolute top-12 left-12 z-10">
|
||||
{tooltipType === CONST_TOOLTIP_TYPE.APP_DIVERSION.type &&
|
||||
appDiversion.map((item) => {
|
||||
return (
|
||||
<div
|
||||
key={item.name}
|
||||
className="flex items-center justify-center w-16 h-16 relative rounded-[4.95px] shadow-[0px_0px_3.299999952316284px_0px_rgba(84,87,255,1.00)] outline outline-[0.50px] outline-offset-[-0.50px] outline-indigo-50/60 overflow-hidden"
|
||||
onClick={() => handleClickApp(item)}
|
||||
>
|
||||
{selectedApp?.name === item?.name ? (
|
||||
<item.activeIcon />
|
||||
) : (
|
||||
<item.icon />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-2 w-full h-full flex-1">
|
||||
<WorldGeo
|
||||
currentValue={currentValue}
|
||||
newHomeProxies={newHomeProxies}
|
||||
tooltipType={tooltipType}
|
||||
tooltipClosed={tooltipClosed}
|
||||
setTooltipClosed={setTooltipClosed}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-6 left-[50%] translate-x-[-50%] w-[calc(100%-51px)] p-6 bg-indigo-950 bg-opacity-10 rounded-md outline outline-1 outline-zinc-200 outline-opacity-40 backdrop-blur-lg inline-flex justify-start items-center gap-4">
|
||||
<div
|
||||
className="bt1 cursor-pointer"
|
||||
onClick={() => {
|
||||
setTooltipType(CONST_TOOLTIP_TYPE.TRAFFIC_OBFUSCATION.type);
|
||||
setTooltipClosed(true);
|
||||
}}
|
||||
>
|
||||
流量混淆
|
||||
</div>
|
||||
<div
|
||||
className="bt1 cursor-pointer"
|
||||
onClick={() => {
|
||||
setTooltipType(CONST_TOOLTIP_TYPE.APP_DIVERSION.type);
|
||||
}}
|
||||
>
|
||||
应用分流
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AntiDarkAnalysisNetwork;
|
||||
@ -314,7 +314,7 @@ const DecentralizedElasticNetwork = () => {
|
||||
>
|
||||
动态路由生成
|
||||
</div>
|
||||
<div
|
||||
{/* <div
|
||||
className="bt1 cursor-pointer"
|
||||
onClick={() => {
|
||||
setTooltipType(CONST_TOOLTIP_TYPE.TRAFFIC_OBFUSCATION.type);
|
||||
@ -330,7 +330,7 @@ const DecentralizedElasticNetwork = () => {
|
||||
}}
|
||||
>
|
||||
应用分流
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -3,6 +3,7 @@ import HomePage from '@/pages/home'
|
||||
import NewHomePage from '@/pages/new-home'
|
||||
import DecentralizedElasticNetworkPage from '@/pages/decentralized-lastic-network'
|
||||
import AntiForensicsForwardingPage from '@/pages/anti-forensics-forwarding'
|
||||
import AntiDarkAnalysisNetwork from '@/pages/anti-dark-analysis-network'
|
||||
import LazyLoader from '@/layout/LazyLoader'
|
||||
import App from '@/App'
|
||||
|
||||
@ -29,6 +30,10 @@ export const router = createBrowserRouter([
|
||||
path: '/anti-forensics-forwarding',
|
||||
element: <LazyLoader component={AntiForensicsForwardingPage} />,
|
||||
},
|
||||
{
|
||||
path: '/anti-dark-analysis-network',
|
||||
element: <LazyLoader component={AntiDarkAnalysisNetwork} />,
|
||||
},
|
||||
// {
|
||||
// path: '/home',
|
||||
// element: <LazyLoader component={HomePage} />,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user