2025-04-17 14:12:49 +08:00

1189 lines
42 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import { useEffect, useMemo, useRef, memo } from "react";
import * as echarts from "echarts";
// import 'echarts-gl';
// import { useQueryClient } from "@tanstack/react-query";
import type { EChartsType } from "echarts";
import worldGeoJson from "@/assets/echarts-map/json/world.json";
import { geoCoordMap, countryNameMap, countryCodeMap } from "@/data";
import { getUrl } from "@/lib/utils";
import { CONST_TOOLTIP_TYPE } from "@/pages/anti-forensics-forwarding";
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(
({
dataInfo,
selectedApp,
tooltipType,
tooltipClosed,
setTooltipClosed,
}: {
dataInfo: any;
selectedApp: any;
tooltipType: string;
tooltipClosed: boolean;
setTooltipClosed: (value: boolean) => void;
}) => {
// const queryClient = useQueryClient()
// 嵌套加密ref
const customTooltipRef = useRef<HTMLDivElement | null>(null);
// 流量混淆ref
const customTooltip2Ref = useRef<HTMLDivElement | null>(null);
const proxyGeoRef = useRef<EChartsType | null>(null);
const preMainToData = useRef<{ country_code: string }[]>([]);
const lineMidpointsRef = useRef<
{
id: string;
midpoint: number[];
fromCountry: string;
toCountry: string;
}[]
>([]);
const labelContainerRef = useRef<HTMLDivElement | null>(null);
const labelsRef = useRef<HTMLDivElement[]>([]);
const mainToData = useMemo(() => {
const newList = [
...dataInfo.passAuthentication.data,
...dataInfo.trafficObfuscation,
...dataInfo.nestedEncryption,
...dataInfo.dynamicRouteGeneration,
];
// 使用新的数据结构
const proxiesList =
selectedApp && selectedApp ? [...newList, selectedApp] : newList ?? [];
// 初始化数据数组 - 不再包含 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, // 保存连线标志
color: proxyItem.color, // 保存颜色信息
});
// 添加终点ingress_country_code
data.push({
country_code: item.ingress_country_code,
type: "end",
isLine: proxyItem.isLine, // 保存连线标志
color: proxyItem.color, // 保存颜色信息
});
} else {
// 如果没有 ingress_country_code只添加 country_code
data.push({
country_code: item.country_code,
isLine: proxyItem.isLine, // 保存连线标志
color: proxyItem.color, // 保存颜色信息
});
}
});
}
});
return data;
}, [dataInfo, selectedApp]);
// 创建自定义提示框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[dataInfo.nestedEncryption?.[0]?.code ?? "GL"];
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);
}
};
// 创建自定义提示框DOM元素
const createCustomTooltip2 = () => {
// 如果已经存在自定义提示框,则移除它
if (document.getElementById("custom-fixed-tooltip2")) {
document.getElementById("custom-fixed-tooltip2")?.remove();
}
// 创建自定义提示框
const tooltip = document.createElement("div");
tooltip.id = "custom-fixed-tooltip2";
tooltip.style.position = "fixed";
tooltip.style.zIndex = "1000";
tooltip.style.pointerEvents = "auto";
tooltip.style.backgroundColor = "transparent";
tooltip.innerHTML = `
<div class="tooltip-content">
<div class="fill-left"></div>
<div class="tip-box">
<div>
<div class="label" style="color: white; font-weight: bold;">流量混淆</div>
<img class="close-icon" src="${getUrl(
"svg/Xwhite.svg"
)}" alt=""
style="cursor: pointer; " />
</div>
<img class="traffic-obfuscation-img" src="${getUrl(
"image/traffic-obfuscation.png"
)}" alt="" />
</div>
<img class="line-img-left" src="${getUrl(
"svg/anti-forensics-forwarding/LineLeft.svg"
)}" alt="" />
</div>
`;
// 添加到DOM
document.body.appendChild(tooltip);
customTooltip2Ref.current = tooltip;
// 添加关闭按钮事件
const closeButton = tooltip.querySelector(".close-icon");
if (closeButton) {
closeButton.addEventListener("click", () => {
setTooltipClosed(false);
tooltip.remove();
customTooltip2Ref.current = null;
});
}
// 定位提示框
positionCustomTooltip2();
};
// 定位自定义提示框 - 优化版本
const positionCustomTooltip2 = () => {
if (!customTooltip2Ref.current || !proxyGeoRef.current) return;
// 找到US点
const coords = geoCoordMap[dataInfo.trafficObfuscation?.[0]?.code ?? "ZA"];
if (!coords) return;
try {
// 将地理坐标转换为屏幕坐标
const screenCoord = proxyGeoRef.current.convertToPixel("geo", coords);
if (
screenCoord &&
Array.isArray(screenCoord) &&
screenCoord.length === 2
) {
// 设置提示框位置
customTooltip2Ref.current.style.left = `${
screenCoord[0] - 626 + 20
}px`;
customTooltip2Ref.current.style.top = `${
screenCoord[1] + 40 - 218
}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();
if (!(["RU", "FR"].includes(countryCode) && item.type === "start"))
return null;
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[] = [];
// 收集每个点的颜色信息
const pointColors: Record<string, string> = {};
// 处理主路径数据
for (let i = 0; i < mainToData.length; i++) {
// 如果是最后一个元素,则跳过(因为没有下一个元素作为终点)
if (i === mainToData.length - 1) continue;
const currentItem = mainToData[i];
const nextItem = mainToData[i + 1];
// 获取当前国家代码
const countryCode = currentItem.country_code.toUpperCase();
// 获取颜色信息
const lineColor = currentItem.color || "#0ea5e9"; // 默认颜色
// 保存点的颜色信息
pointColors[countryCode] = lineColor;
// 如果当前项是起点,下一项是终点
if (currentItem.type === "start" && nextItem.type === "end") {
const startCode = countryCode;
const endCode = nextItem.country_code.toUpperCase();
// 保存终点的颜色信息
pointColors[endCode] = lineColor;
// 无论是否连线,都添加点的涟漪效果
const startPoint = createCountryRipple(startCode);
const endPoint = createCountryRipple(endCode);
if (startPoint) {
ripplePoints.push({
...startPoint,
color: lineColor, // 添加颜色信息
});
}
if (endPoint) {
ripplePoints.push({
...endPoint,
color: lineColor, // 添加颜色信息
});
}
// 检查是否应该绘制连线
if (currentItem.isLine !== false) {
const lineItem = getLineItem(startCode, endCode);
// 添加颜色信息到连线数据
solidData[0][1].push({
...lineItem,
color: lineColor, // 添加颜色信息
} as any);
}
// 跳过下一项,因为已经处理了
i++;
}
// 常规情况:当前项到下一项
else {
const nextCountryCode = nextItem.country_code.toUpperCase();
// 保存下一个点的颜色信息
pointColors[nextCountryCode] = nextItem.color || lineColor;
// 无论是否连线,都添加点的涟漪效果
const currentPoint = createCountryRipple(countryCode);
const nextPoint = createCountryRipple(nextCountryCode);
if (currentPoint) {
ripplePoints.push({
...currentPoint,
color: lineColor, // 添加颜色信息
});
}
if (nextPoint) {
ripplePoints.push({
...nextPoint,
color: nextItem.color || lineColor, // 添加颜色信息
});
}
// 检查是否应该绘制连线
if (currentItem.isLine !== false) {
const lineItem = getLineItem(countryCode, nextCountryCode);
// 添加颜色信息到连线数据
solidData[0][1].push({
...lineItem,
color: lineColor, // 添加颜色信息
} as any);
}
}
}
// 虚线数据处理(保持原有逻辑)
const otherLineList: any = [];
return {
solidData,
otherLineList,
ripplePoints,
pointColors,
};
};
// 获取连线经纬度数据
const convertData = (data: LinesDataType[]) => {
const res = [];
const midpoints = [];
for (let index = 0; index < data.length; index++) {
const dataIndex = data[index];
const fromCoord = geoCoordMap[dataIndex?.[0]?.country_code ?? ""];
const toCoord = geoCoordMap[dataIndex?.[1]?.country_code ?? ""];
const fromCountry = dataIndex?.[0]?.country_code ?? "";
const toCountry = dataIndex?.[1]?.country_code ?? "";
const lineColor = (dataIndex as any)?.color || "#0ea5e9"; // 获取线条颜色
if (fromCoord && toCoord) {
res.push({
coords: [fromCoord, toCoord],
lineStyle: {
color: lineColor, // 使用自定义颜色
},
});
// 计算中点,考虑曲线的弧度
const curveness = -0.4; // 与飞线弧度相同
const x1 = fromCoord[0];
const y1 = fromCoord[1];
const x2 = toCoord[0];
const y2 = toCoord[1];
// 计算控制点
const cpx = (x1 + x2) / 2 - (y2 - y1) * curveness;
const cpy = (y1 + y2) / 2 - (x1 - x2) * curveness;
// 计算曲线上的中点 (t=0.5 时的贝塞尔曲线点)
const midX = x1 * 0.25 + cpx * 0.5 + x2 * 0.25;
const midY = y1 * 0.25 + cpy * 0.5 + y2 * 0.25;
midpoints.push({
id: `line-label-${index}`,
midpoint: [midX, midY],
fromCountry,
toCountry,
});
}
}
// 更新中点引用
// lineMidpointsRef.current = midpoints;
return res;
};
// 创建双层点效果 - 大点
const createDualLayerPoint = (
lastExit: LinesItemType,
isMainPath: boolean = true,
color?: string
) => {
// 创建数据数组,用于两个散点图层
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;
// 使用传入的颜色或默认颜色
const outerColor = color || "#0ea5e9";
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 createRipplePointsFromCoordinates = (
coordinates: [number, number][],
series: echarts.SeriesOption[]
) => {
if (!coordinates || coordinates.length === 0) return;
// 使用selectedApp.color或默认蓝色
const outerColor = "#01FF5E";
// 只创建外层带涟漪效果的点
series.push({
type: "effectScatter",
coordinateSystem: "geo",
zlevel: 3,
color: outerColor,
symbol: "circle",
symbolSize: 6,
rippleEffect: {
period: 8, // 动画时间
brushType: "stroke", // 波纹绘制方式
scale: 6, // 波纹圆环最大限制
brushWidth: 2,
},
label: {
show: false,
},
data: coordinates.map((coord) => ({
name: "", // 可以根据需要添加名称
value: coord,
})),
} as echarts.SeriesOption);
};
// 创建路径点的双层效果
const createPathPoints = (
dataItems: LinesDataType[],
isMainPath: boolean = true
) => {
// 创建数据数组
const pointData = dataItems.map((dataItem: LinesDataType) => {
const color = (dataItem as any).color || "#0ea5e9"; // 获取颜色信息
return {
name: dataItem[0].name,
value: geoCoordMap[dataItem[0].country_code],
datas: {
country_code: dataItem[0].country_code,
color: color, // 保存颜色信息
},
};
});
// 根据是否是主路径设置不同的大小和颜色
const outerSize = isMainPath ? 8 : 4;
const innerSize = isMainPath ? 4 : 2;
const innerColor = "#FFFFFF"; // 白色内层
return [
{
// 外层彩色点,带涟漪效果
type: "effectScatter",
coordinateSystem: "geo",
zlevel: 3,
// 使用回调函数根据数据项设置颜色
itemStyle: {
color: function (params: any) {
return params.data.datas.color || "#0ea5e9";
},
},
symbol: "circle",
symbolSize: outerSize,
rippleEffect: {
period: 8, // 动画时间,值越小速度越快
brushType: "stroke", // 波纹绘制方式 stroke
scale: 6, // 波纹圆环最大限制,值越大波纹越大
brushWidth: 2,
},
label: {
show: false,
},
tooltip: {
show: false,
trigger: "item",
formatter: (params: any) => {
return `
<div class="tip-box">
<img class="close-icon" src="${getUrl(
"svg/Xwhite.svg"
)}" alt="" />
<div class="label">嵌套加密</div>
<img class="encryption-img" width="100%" src="${getUrl(
"image/nested-encryption.png"
)}" alt="" />
</div>
`;
},
backgroundColor: "transparent",
borderWidth: 0,
},
data: pointData,
} as echarts.SeriesOption,
{
// 内层白色点,不带涟漪效果
type: "scatter", // 使用普通scatter不带特效
coordinateSystem: "geo",
zlevel: 4, // 确保在蓝色点上方
color: innerColor,
symbol: "circle",
symbolSize: innerSize,
label: {
show: false,
},
data: pointData,
} as echarts.SeriesOption,
];
};
// 创建带自定义提示框的涟漪点
const createRipplePointsWithTooltip = (ripplePoints: any) => {
return {
type: "effectScatter",
coordinateSystem: "geo",
zlevel: 3,
// 使用回调函数根据数据项设置颜色
itemStyle: {
color: function (params: any) {
return params.data.datas.color || "#0ea5e9";
},
},
symbol: "circle",
symbolSize: 8,
rippleEffect: {
period: 8,
brushType: "stroke",
scale: 6,
brushWidth: 2,
},
label: {
show: false,
formatter: (params: any) => {
return `{${params.data.datas.country_code}|}`;
},
},
// 添加提示框配置
tooltip: {
show: false,
trigger: "item",
formatter: (params: any) => {
return `
<div class="tip-box">
<img class="close-icon" src="${getUrl(
"svg/Xwhite.svg"
)}" alt="" />
<div class="label">嵌套加密</div>
<img class="encryption-img" width="100%" src="${getUrl(
"image/nested-encryption.png"
)}" alt="" />
</div>
`;
},
backgroundColor: "transparent",
borderWidth: 0,
},
data: ripplePoints.map((point: any) => ({
name: point.name,
value: point.value,
datas: {
country_code: point.country_code,
color: point.color || "#0ea5e9", // 保存颜色信息
},
})),
} as echarts.SeriesOption;
};
// 连线 series
const getLianData = (series: echarts.SeriesOption[]) => {
const { solidData, otherLineList, ripplePoints } = getLine();
// 如果有需要显示涟漪效果的点,添加它们
if (ripplePoints.length > 0) {
// 添加带自定义提示框的外层彩色点
series.push(createRipplePointsWithTooltip(ripplePoints));
// 添加内层白色点,不带涟漪效果
series.push({
type: "scatter", // 使用普通scatter不带特效
coordinateSystem: "geo",
zlevel: 4, // 确保在彩色点上方
color: "#FFFFFF", // 白色内层
symbol: "circle",
symbolSize: 4,
label: {
show: false,
},
data: ripplePoints.map((point) => ({
name: point.name,
value: point.value,
datas: {
country_code: point.country_code,
},
})),
} as echarts.SeriesOption);
}
solidData.forEach((item) => {
// 如果没有连线数据,则跳过
if (item[1].length === 0) {
return;
}
const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
const lastExitColor =
(item[1]?.[item[1].length - 1] as any)?.color || "#0ea5e9";
// 添加飞行线
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", // 飞线类型
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, lastExitColor);
series.push(...exitNodes);
}
});
otherLineList.forEach((line: any) => {
line.forEach((item: any) => {
const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
const lastExitColor =
(item[1]?.[item[1].length - 1] as any)?.color || "#0ea5e9";
// 添加虚线
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,
lastExitColor
);
series.push(...exitNodes);
}
});
});
return true;
};
// 创建A点和B点并添加飞线和标签
const createSpecialPoints = (series: echarts.SeriesOption[]) => {
// 定义点A和点B的坐标
const pointA =
geoCoordMap[dataInfo.passAuthentication.startPoint ?? "GL"];
const pointB = geoCoordMap[dataInfo.passAuthentication.endPoint ?? "CA"];
const newPointB = [pointB[0] + 14, pointB[1] + 10];
// 添加A点 - 带涟漪效果的双层点
series.push(
// 外层带涟漪效果的点
{
type: "effectScatter",
coordinateSystem: "geo",
zlevel: 3,
color: "#FF6B01", // 橙色外层
symbol: "circle",
symbolSize: 8,
rippleEffect: {
period: 8, // 动画时间
brushType: "stroke", // 波纹绘制方式
scale: 6, // 波纹圆环最大限制
brushWidth: 2,
},
label: {
show: true,
position: [10, -50],
formatter: () => {
return "{name1|待认证节点}";
},
rich: {
name1: {
color: "#FF6B01",
align: "center",
lineHeight: 35,
fontSize: 18,
fontWeight: 600,
padding: [11, 16.52, 11, 16.52],
backgroundColor: "rgba(63, 6, 3, 0.5)",
},
},
backgroundColor: "transparent",
},
data: [
{
name: "格陵兰",
value: pointA,
},
],
} as echarts.SeriesOption,
// 内层白色点
{
type: "scatter", // 普通scatter不带特效
coordinateSystem: "geo",
zlevel: 4, // 确保在外层点上方
color: "#FFFFFF", // 白色内层
symbol: "circle",
symbolSize: 4,
label: {
show: false,
},
data: [
{
name: "格陵兰",
value: pointA,
},
],
} as echarts.SeriesOption
);
// 添加B点 - 大型圆形区域
series.push({
type: "scatter",
coordinateSystem: "geo",
zlevel: 2,
color: "rgba(55, 255, 0, 0.50)", // 半透明绿色
symbol: "circle",
symbolSize: 150, // 大尺寸圆形
label: {
show: true,
position: [-70, -30],
formatter: () => {
return "{name2|权威节点团}";
},
rich: {
name2: {
color: "#37FF00",
align: "center",
lineHeight: 35,
fontSize: 18,
fontWeight: 600,
padding: [11, 16.52, 11, 16.52],
backgroundColor: "rgba(4, 59, 27, 0.5)",
},
},
backgroundColor: "transparent",
},
data: [
{
name: "加拿大",
value: pointB,
},
],
} as echarts.SeriesOption);
// 添加A到B的飞线无特效
series.push({
type: "lines",
zlevel: 1,
effect: {
show: false, // 关闭特效
},
lineStyle: {
curveness: -0.4, // 飞线弧度
type: "solid",
color: "#FEAA18", // 飞线颜色
width: 1.5,
opacity: 0.8,
},
data: [
{
coords: [pointA, newPointB], // 从A点到B点
},
],
} as echarts.SeriesOption);
// 计算飞线中点坐标(考虑曲率)
const x1 = pointA[0];
const y1 = pointA[1];
const x2 = newPointB[0];
const y2 = newPointB[1];
const curveness = -0.4;
// 计算控制点
const cpx = (x1 + x2) / 2 - (y2 - y1) * curveness;
const cpy = (y1 + y2) / 2 - (x1 - x2) * curveness;
// 计算曲线上的中点 (t=0.5 时的贝塞尔曲线点)
const midX = x1 * 0.25 + cpx * 0.5 + x2 * 0.25;
const midY = y1 * 0.25 + cpy * 0.5 + y2 * 0.25;
// 将中点添加到 lineMidpointsRef 中,以便使用 DOM 方式创建标签
lineMidpointsRef.current.push({
id: "special-line-label",
midpoint: [midX, midY],
fromCountry: "A",
toCountry: "B",
});
return series;
};
const getOption = () => {
const series: echarts.SeriesOption[] = [];
getLianData(series);
// getMianLineTipData(series); // 添加主线tip 暂时隐藏
createSpecialPoints(series); // 添加特殊点和飞线
if (dataInfo.passAuthentication?.authenticationPoint) {
createRipplePointsFromCoordinates(
dataInfo.passAuthentication?.authenticationPoint || [],
series
);
}
const option = {
backgroundColor: "transparent",
// 全局提示框配置
tooltip: {
show: true,
trigger: "item",
enterable: true,
confine: true, // 保持提示框在图表范围内
appendToBody: true, // 将提示框附加到body以获得更好的定位
// position: function(pos:any, params, dom, rect, size) {
position: function (pos: any) {
// 自定义定位逻辑(如果需要)
return [pos[0] + 10, pos[1] - 50]; // 从光标偏移
},
},
// 底图样式
geo: {
map: "world", // 地图类型
roam: true, // 是否开启缩放
zoom: 1, // 初始缩放大小
// center: [11.3316626, 19.5845024], // 地图中心点
layoutCenter: ["50%", "50%"], //地图位置
scaleLimit: {
// 缩放等级
min: 1,
max: 3,
},
label: {
show: false,
},
nameMap: countryNameMap, // 自定义地区的名称映射
// 三维地理坐标系样式
itemStyle: {
areaColor: "#020617", // 修改为要求的填充颜色
borderColor: "#cbd5e1", // 修改为要求的边框颜色
borderWidth: 1, // 边框宽度
borderType: "dashed", // 修改为点线边框
},
emphasis: {
itemStyle: {
areaColor: "#172554", // 修改为鼠标悬停时的填充颜色
borderColor: "#0ea5e9", // 修改为鼠标悬停时的边框颜色
borderWidth: 1.2, // 修改为鼠标悬停时的边框宽度
borderType: "solid", // 修改为实线边框
},
label: false,
},
tooltip: {
show: true,
trigger: "item",
triggerOn: "click", // 提示框触发的条件
enterable: true, // 鼠标是否可进入提示框浮层中默认为false如需详情内交互如添加链接按钮可设置为 true
backgroundColor: "rgba(0,0,0,0.8)",
borderColor: "rgba(0,0,0,0.2)",
textStyle: {
color: "#fff",
},
formatter: (parameters: {
name: string;
data:
| {
name: string;
datas: { tradingCountry: string };
}
| undefined;
}) => {
if (parameters.data?.name) return parameters.data.name;
return parameters.name;
},
},
},
series: series,
};
return option;
};
// 创建DOM标签
const createDOMLabels = () => {
// 清除现有标签
if (labelContainerRef.current) {
labelContainerRef.current.innerHTML = "";
labelsRef.current = [];
} else {
// 创建标签容器
const container = document.createElement("div");
container.className = "line-labels-container";
container.style.position = "absolute";
container.style.top = "0";
container.style.left = "0";
container.style.pointerEvents = "none";
container.style.zIndex = "1000";
container.style.width = "100%";
container.style.height = "100%";
container.style.overflow = "hidden";
// 添加到地图容器
const chartDom = document.getElementById("screenGeo");
if (chartDom) {
chartDom.style.position = "relative";
chartDom.appendChild(container);
labelContainerRef.current = container;
}
}
// 创建新标签
lineMidpointsRef.current.forEach((point, index) => {
const label = document.createElement("div");
label.id = point.id;
label.className = "line-label";
label.style.position = "absolute";
label.style.textAlign = "center";
label.style.transform = "translate(-50%, -50%)";
label.style.whiteSpace = "nowrap";
label.style.pointerEvents = "none";
label.style.zIndex = "1001";
// 特殊线标签A到B的线
if (point.id === "special-line-label") {
label.style.backgroundColor = "#8B3700";
label.style.color = "#FFB27A";
label.style.padding = "5px 10px";
label.style.borderRadius = "4px";
label.style.fontSize = "18px";
label.style.fontWeight = "normal";
label.textContent = "SS签名";
}
// 其他线标签
else {
label.style.backgroundColor = "#8B3700";
label.style.color = "#FFB27A";
label.style.padding = "5px 10px";
label.style.borderRadius = "4px";
label.style.fontSize = "18px";
label.style.fontWeight = "normal";
label.textContent = "SS签名";
}
// 添加到容器
labelContainerRef.current?.appendChild(label);
labelsRef.current.push(label);
});
// 更新标签位置
updateLabelPositions();
};
// 更新标签位置
const updateLabelPositions = () => {
if (!proxyGeoRef.current || !labelContainerRef.current) return;
lineMidpointsRef.current.forEach((point, index) => {
const label = labelsRef.current[index];
if (!label) return;
const pixelPoint = proxyGeoRef.current?.convertToPixel(
"geo",
point.midpoint
);
if (pixelPoint && Array.isArray(pixelPoint)) {
label.style.left = `${pixelPoint[0]}px`;
label.style.top = `${pixelPoint[1]}px`;
}
});
};
const handleResize = () => {
proxyGeoRef.current?.resize();
updateLabelPositions();
};
useEffect(() => {
preMainToData.current?.some(
(item, index) => item.country_code !== mainToData[index]?.country_code
) && proxyGeoRef.current?.clear();
preMainToData.current = mainToData;
const option = getOption();
proxyGeoRef.current?.setOption(option);
// 创建DOM标签
setTimeout(createDOMLabels, 100);
}, [dataInfo, mainToData]);
useEffect(() => {
const chartDom = document.getElementById("screenGeo");
proxyGeoRef.current = echarts.init(chartDom);
echarts.registerMap(
"world",
worldGeoJson as unknown as Parameters<typeof echarts.registerMap>[1]
);
const option = getOption();
option && proxyGeoRef.current?.setOption(option);
// 添加地图交互事件监听器
proxyGeoRef.current?.on("georoam", updateLabelPositions);
// 页面resize时触发
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
proxyGeoRef.current?.off("georoam", updateLabelPositions);
// 清理DOM标签
if (labelContainerRef.current) {
labelContainerRef.current.remove();
labelContainerRef.current = null;
labelsRef.current = [];
}
proxyGeoRef.current?.dispose();
proxyGeoRef.current = null;
};
}, []);
useEffect(() => {
if (tooltipClosed) {
createCustomTooltip();
createCustomTooltip2();
} else {
customTooltipRef.current?.remove();
customTooltip2Ref.current?.remove();
customTooltipRef.current = null;
customTooltip2Ref.current = null;
}
return () => {
customTooltipRef.current?.remove();
customTooltip2Ref.current?.remove();
customTooltipRef.current = null;
customTooltip2Ref.current = null;
};
}, [
tooltipClosed,
tooltipType,
dataInfo.nestedEncryption,
dataInfo.trafficObfuscation,
]);
return (
<div className="flex-1 h-full flex flex-col">
<div id="screenGeo" className="flex-1"></div>
</div>
);
}
);