2025-05-12 15:28:00 +08:00

1532 lines
49 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, useRef, memo, useState } from "react";
import * as echarts from "echarts";
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 dayjs from "dayjs";
// 连线动画的间隔时间(毫秒)
const LINE_ANIMATION_INTERVAL = 500;
interface LinesItemType {
name: string;
country_code: string;
value: number[];
color?: string;
}
type LinesDataType = [LinesItemType, LinesItemType];
type LinesType = [string, LinesDataType[]];
// 创建自定义提示框组件
const CustomTooltip = ({
logs,
onClose,
tooltipRef,
}: {
logs: string[];
onClose: () => void;
tooltipRef: React.RefObject<HTMLDivElement>;
}) => {
const [visibleLogs, setVisibleLogs] = useState<string[]>([]);
const [isComplete, setIsComplete] = useState(false);
// 使用useEffect实现逐条显示日志的效果
useEffect(() => {
console.log("logs-------", logs.length);
if (!logs || logs.length === 0) return;
// 重置状态
setVisibleLogs([]);
setIsComplete(false);
let currentIndex = 0;
// 创建一个定时器每500毫秒显示一条新日志
const timer = setInterval(() => {
if (currentIndex < logs.length) {
const curLog = `[${dayjs().format("YYYY-MM-DD HH:mm:ss")}] ${logs[currentIndex]}`;
setVisibleLogs((prev) => [...prev, curLog]);
currentIndex++;
} else {
clearInterval(timer);
setIsComplete(true);
}
}, 1000);
// 清理函数
return () => {
clearInterval(timer);
};
}, [logs]); // 当logs变化时重新开始动画
// 自动滚动到最新的日志
const logsContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (logsContainerRef.current && visibleLogs.length > 0) {
logsContainerRef.current.scrollTop =
logsContainerRef.current.scrollHeight;
}
}, [visibleLogs]);
return (
<div
id="custom-fixed-tooltip"
ref={tooltipRef}
style={{
position: "fixed",
zIndex: 1000,
pointerEvents: "auto",
backgroundColor: "transparent",
}}
>
<div className="tooltip-content">
<img
className="line-img-hx"
src={getUrl("svg/anti-forensics-forwarding/Line.svg")}
alt=""
/>
<div className="fill"></div>
<div className="tip-box-hx">
<div className="flex justify-between items-center mb-2">
<div
className="label"
style={{ color: "white", fontWeight: "bold" }}
>
</div>
<img
className="close-icon"
src={getUrl("svg/Xwhite.svg")}
alt=""
style={{ cursor: "pointer" }}
onClick={onClose}
/>
</div>
<div
ref={logsContainerRef}
className="logs-container mt-3 max-h-[335px] overflow-y-auto"
>
{visibleLogs.length > 0 ? (
<ul className="logs-list space-y-1.5">
{visibleLogs.map((log, index) => (
<li
key={index}
className="log-item text-sm text-white py-1 px-2 bg-black/20 rounded animate-fadeIn"
>
{log}
</li>
))}
</ul>
) : (
<div className="text-sm text-gray-400 italic">
{logs.length > 0 ? "日志加载中..." : "暂无日志记录"}
</div>
)}
</div>
</div>
</div>
</div>
);
};
// 创建单个国家的涟漪效果
const createCountryRipple = (countryCode: string, color?: string) => {
const coords = geoCoordMap[countryCode];
if (!coords) return null;
return {
name: countryCodeMap[countryCode] ?? "",
value: coords,
country_code: countryCode,
color: color || "#0ea5e9",
};
};
export const WorldGeo = memo(
({
nestedEncryption,
passAuthentication,
dynamicRouteGeneration,
tooltipClosed,
setTooltipClosed,
logs,
}: {
logs: any[];
nestedEncryption: any;
passAuthentication: any;
dynamicRouteGeneration: any;
tooltipType: string;
tooltipClosed: boolean;
setTooltipClosed: (value: boolean) => void;
}) => {
// 嵌套加密ref
const customTooltipRef = useRef<HTMLDivElement | null>(null);
// 流量混淆ref
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 [nestedEncryptionLineIndex, setNestedEncryptionLineIndex] = useState(-1);
const [dynamicRouteLineIndex, setDynamicRouteLineIndex] = useState(-1);
// 添加状态来存储所有连线数据
const [nestedEncryptionLines, setNestedEncryptionLines] = useState<
{ from: string; to: string; color?: string }[]
>([]);
const [dynamicRouteLines, setDynamicRouteLines] = useState<
{ from: string; to: string; color?: string }[]
>([]);
// 添加状态来存储所有点
const [allPoints, setAllPoints] = useState<any[]>([]);
// 使用ref来跟踪动画状态避免重新渲染
const animationTimerRef = useRef<NodeJS.Timeout | null>(null);
const dynamicAnimationTimerRef = useRef<NodeJS.Timeout | null>(null);
// 添加状态来跟踪数据是否已经变化
const nestedEncryptionKeyRef = useRef<string>("");
const dynamicRouteKeyRef = useRef<string>("");
// 添加一个ref来跟踪图表是否已初始化
const chartInitializedRef = useRef(false);
// 初始化时提取所有点的函数
const extractAllPoints = () => {
const points: any[] = [];
// 从嵌套加密数据中提取点
if (nestedEncryption && Array.isArray(nestedEncryption)) {
nestedEncryption.forEach((item: any) => {
if (item.data && Array.isArray(item.data)) {
item.data.forEach((dataItem: any) => {
// 添加起点到点集合
const fromCode = dataItem.country_code.toUpperCase();
const fromPoint = createCountryRipple(fromCode, item.color);
if (
fromPoint &&
!points.some((p) => p.country_code === fromCode)
) {
points.push(fromPoint);
}
// 如果有终点,也添加到点集合
if (dataItem.ingress_country_code) {
const toCode = dataItem.ingress_country_code.toUpperCase();
const toPoint = createCountryRipple(toCode, item.color);
if (toPoint && !points.some((p) => p.country_code === toCode)) {
points.push(toPoint);
}
}
});
}
});
}
// 从动态路由数据中提取点
if (dynamicRouteGeneration && Array.isArray(dynamicRouteGeneration)) {
dynamicRouteGeneration.forEach((item: any) => {
if (item.data && Array.isArray(item.data)) {
item.data.forEach((dataItem: any) => {
// 添加起点到点集合
const fromCode = dataItem.country_code.toUpperCase();
const fromPoint = createCountryRipple(fromCode, item.color);
if (
fromPoint &&
!points.some((p) => p.country_code === fromCode)
) {
points.push(fromPoint);
}
// 如果有终点,也添加到点集合
if (dataItem.ingress_country_code) {
const toCode = dataItem.ingress_country_code.toUpperCase();
const toPoint = createCountryRipple(toCode, item.color);
if (toPoint && !points.some((p) => p.country_code === toCode)) {
points.push(toPoint);
}
}
});
}
});
}
return points;
};
// 修改初始化逻辑,确保在数据变化时立即提取点
useEffect(() => {
// 提取所有点
const points = extractAllPoints();
if (points.length > 0) {
setAllPoints(points);
}
}, [nestedEncryption, dynamicRouteGeneration]); // 监听数据变化
// 启动嵌套加密连线动画的函数
const startNestedEncryptionAnimation = (
connections: { from: string; to: string; color?: string }[]
) => {
if (connections.length === 0) return;
let index = 0;
// 递归函数,用于按顺序显示连线
const animateNextLine = () => {
setNestedEncryptionLineIndex(index);
index++;
if (index < connections.length) {
animationTimerRef.current = setTimeout(
animateNextLine,
LINE_ANIMATION_INTERVAL
);
}
};
// 开始动画
animateNextLine();
};
// 处理嵌套加密数据变化
useEffect(() => {
// 清除任何现有的动画定时器
if (animationTimerRef.current) {
clearTimeout(animationTimerRef.current);
animationTimerRef.current = null;
}
const allExtractedPoints: any[] = [];
// 处理嵌套加密数据
if (nestedEncryption && Array.isArray(nestedEncryption)) {
const points: any[] = [];
const connections: { from: string; to: string; color?: string }[] = [];
let shouldStartAnimation = false;
nestedEncryption.forEach((item: any) => {
if (item.data && Array.isArray(item.data)) {
item.data.forEach((dataItem: any) => {
// 添加起点到点集合
const fromCode = dataItem.country_code.toUpperCase();
const fromPoint = createCountryRipple(fromCode, item.color);
if (
fromPoint &&
!points.some((p) => p.country_code === fromCode)
) {
points.push(fromPoint);
if (
!allExtractedPoints.some((p) => p.country_code === fromCode)
) {
allExtractedPoints.push(fromPoint);
}
}
// 如果有终点,也添加到点集合
if (dataItem.ingress_country_code) {
const toCode = dataItem.ingress_country_code.toUpperCase();
const toPoint = createCountryRipple(toCode, item.color);
if (toPoint && !points.some((p) => p.country_code === toCode)) {
points.push(toPoint);
if (
!allExtractedPoints.some((p) => p.country_code === toCode)
) {
allExtractedPoints.push(toPoint);
}
}
// 检查是否需要开始连线动画
if (item.isLine === true) {
connections.push({
from: fromCode,
to: toCode,
color: item.color,
});
shouldStartAnimation = true;
}
}
});
}
});
// 生成当前数据的唯一键
const currentKey = JSON.stringify(nestedEncryption);
// 检查数据是否变化
if (
currentKey !== nestedEncryptionKeyRef.current ||
shouldStartAnimation
) {
nestedEncryptionKeyRef.current = currentKey;
setNestedEncryptionLines(connections);
// 如果有连线数据且需要开始动画,重置索引并启动动画
if (connections.length > 0 && shouldStartAnimation) {
setNestedEncryptionLineIndex(-1); // 重置索引
// 启动连线动画
setTimeout(() => {
startNestedEncryptionAnimation(connections);
}, 500);
} else if (!shouldStartAnimation) {
// 如果不需要连线,设置索引为-1
setNestedEncryptionLineIndex(-1);
}
}
}
// 更新所有点
if (allExtractedPoints.length > 0) {
setAllPoints((prevPoints) => {
const newPoints = [...prevPoints];
allExtractedPoints.forEach((point) => {
if (!newPoints.some((p) => p.country_code === point.country_code)) {
newPoints.push(point);
} else {
// 更新已存在点的颜色
const existingIndex = newPoints.findIndex(
(p) => p.country_code === point.country_code
);
if (existingIndex !== -1 && point.color) {
newPoints[existingIndex].color = point.color;
}
}
});
return newPoints;
});
}
}, [nestedEncryption]);
// 启动动态路由连线动画的函数
const startDynamicRouteAnimation = (
connections: { from: string; to: string; color?: string }[]
) => {
if (connections.length === 0) return;
let index = 0;
// 递归函数,用于按顺序显示连线
const animateNextLine = () => {
setDynamicRouteLineIndex(index);
index++;
if (index < connections.length) {
dynamicAnimationTimerRef.current = setTimeout(
animateNextLine,
LINE_ANIMATION_INTERVAL
);
}
};
// 开始动画
animateNextLine();
};
// 处理动态路由数据变化
useEffect(() => {
// 清除任何现有的动画定时器
if (dynamicAnimationTimerRef.current) {
clearTimeout(dynamicAnimationTimerRef.current);
dynamicAnimationTimerRef.current = null;
}
const allExtractedPoints: any[] = [];
// 处理动态路由数据
if (dynamicRouteGeneration && Array.isArray(dynamicRouteGeneration)) {
const points: any[] = [];
const connections: { from: string; to: string; color?: string }[] = [];
let shouldStartAnimation = false;
dynamicRouteGeneration.forEach((item: any) => {
if (item.data && Array.isArray(item.data)) {
item.data.forEach((dataItem: any) => {
// 添加起点到点集合
const fromCode = dataItem.country_code.toUpperCase();
const fromPoint = createCountryRipple(fromCode, item.color);
if (
fromPoint &&
!points.some((p) => p.country_code === fromCode)
) {
points.push(fromPoint);
if (
!allExtractedPoints.some((p) => p.country_code === fromCode)
) {
allExtractedPoints.push(fromPoint);
}
}
// 如果有终点,也添加到点集合
if (dataItem.ingress_country_code) {
const toCode = dataItem.ingress_country_code.toUpperCase();
const toPoint = createCountryRipple(toCode, item.color);
if (toPoint && !points.some((p) => p.country_code === toCode)) {
points.push(toPoint);
if (
!allExtractedPoints.some((p) => p.country_code === toCode)
) {
allExtractedPoints.push(toPoint);
}
}
// 检查是否需要开始连线动画
if (item.isLine === true) {
connections.push({
from: fromCode,
to: toCode,
color: item.color,
});
shouldStartAnimation = true;
}
}
});
}
});
// 生成当前数据的唯一键
const currentKey = JSON.stringify(dynamicRouteGeneration);
// 检查数据是否变化
if (currentKey !== dynamicRouteKeyRef.current || shouldStartAnimation) {
dynamicRouteKeyRef.current = currentKey;
setDynamicRouteLines(connections);
// 如果有连线数据且需要开始动画,重置索引并启动动画
if (connections.length > 0 && shouldStartAnimation) {
setDynamicRouteLineIndex(-1); // 重置索引
// 启动连线动画
setTimeout(() => {
startDynamicRouteAnimation(connections);
}, 500);
} else if (!shouldStartAnimation) {
// 如果不需要连线,设置索引为-1
setDynamicRouteLineIndex(-1);
}
}
}
// 更新所有点
if (allExtractedPoints.length > 0) {
setAllPoints((prevPoints) => {
const newPoints = [...prevPoints];
allExtractedPoints.forEach((point) => {
if (!newPoints.some((p) => p.country_code === point.country_code)) {
newPoints.push(point);
} else {
// 更新已存在点的颜色
const existingIndex = newPoints.findIndex(
(p) => p.country_code === point.country_code
);
if (existingIndex !== -1 && point.color) {
newPoints[existingIndex].color = point.color;
}
}
});
return newPoints;
});
}
}, [dynamicRouteGeneration]);
// 组件卸载时清除定时器
useEffect(() => {
return () => {
if (animationTimerRef.current) {
clearTimeout(animationTimerRef.current);
animationTimerRef.current = null;
}
if (dynamicAnimationTimerRef.current) {
clearTimeout(dynamicAnimationTimerRef.current);
dynamicAnimationTimerRef.current = null;
}
};
}, []);
const getLineItem = (
preCode: string,
nextCode: string,
color?: string
): [LinesItemType, LinesItemType] => {
return [
{
name: countryCodeMap[preCode] ?? "",
value: geoCoordMap[preCode] ?? [],
country_code: preCode,
color: color,
},
{
name: countryCodeMap[nextCode] ?? "",
value: geoCoordMap[nextCode] ?? [],
country_code: nextCode,
color: color,
},
];
};
const getLine = () => {
// 实现数据处理
const solidData: LinesType[] = []; // 不再使用单一数组,而是分开存储
// 处理嵌套加密连线 - 放入单独的数组
if (nestedEncryptionLineIndex >= 0 && nestedEncryptionLines.length > 0) {
const nestedLines: LinesDataType[] = [];
for (
let i = 0;
i <= nestedEncryptionLineIndex && i < nestedEncryptionLines.length;
i++
) {
const connection = nestedEncryptionLines[i];
nestedLines.push(
getLineItem(connection.from, connection.to, connection.color)
);
}
if (nestedLines.length > 0) {
solidData.push(["nested", nestedLines]);
}
}
// 处理动态路由连线 - 放入单独的数组
if (dynamicRouteLineIndex >= 0 && dynamicRouteLines.length > 0) {
const dynamicLines: LinesDataType[] = [];
for (
let i = 0;
i <= dynamicRouteLineIndex && i < dynamicRouteLines.length;
i++
) {
const connection = dynamicRouteLines[i];
dynamicLines.push(
getLineItem(connection.from, connection.to, connection.color)
);
}
if (dynamicLines.length > 0) {
solidData.push(["dynamic", dynamicLines]);
}
}
// 虚线数据处理(保持原有逻辑)
const otherLineList: any = [];
return {
solidData,
otherLineList,
ripplePoints: allPoints, // 使用 allPoints 确保点始终显示
};
};
// 定位自定义提示框 - 优化版本
const positionCustomTooltip = () => {
if (!customTooltipRef.current || !proxyGeoRef.current) return;
// 找到US点
const coords = geoCoordMap[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);
}
};
// 处理关闭tooltip
const handleCloseTooltip = () => {
setTooltipClosed(false);
};
// 获取连线经纬度数据
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({
coords: [fromCoord, toCoord],
// 添加颜色属性
lineStyle: {
color: dataIndex?.[0]?.color || "#0ea5e9",
},
});
// 计算中点,考虑曲线的弧度
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,
});
}
}
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,
color: v.color, // 添加颜色属性
},
};
})
: [];
// 根据是否是主路径设置不同的大小和颜色
const outerSize = isMainPath ? 8 : 4;
const innerSize = isMainPath ? 4 : 2;
// 使用传入的颜色或从数据中获取颜色,如果都没有则使用默认颜色
const outerColor = color || lastExit?.color || "#0ea5e9";
const innerColor = "#FFFFFF"; // 白色内层
return [
{
// 外层蓝色点,带涟漪效果
type: "effectScatter",
coordinateSystem: "geo",
zlevel: 3,
itemStyle: {
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) => {
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,
color?: string
) => {
// 创建数据数组
const pointData = dataItems.map((dataItem: LinesDataType) => {
return {
name: dataItem[0].name,
value: geoCoordMap[dataItem[0].country_code],
datas: {
country_code: dataItem[0].country_code,
color: dataItem[0].color, // 添加颜色属性
},
};
});
// 根据是否是主路径设置不同的大小和颜色
const outerSize = isMainPath ? 8 : 4;
const innerSize = isMainPath ? 4 : 2;
// 使用传入的颜色或从数据中获取颜色,如果都没有则使用默认颜色
const outerColor = color || dataItems[0]?.[0]?.color || "#0ea5e9";
const innerColor = "#FFFFFF"; // 白色内层
return [
{
// 外层蓝色点,带涟漪效果
type: "effectScatter",
coordinateSystem: "geo",
zlevel: 3,
itemStyle: {
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) => {
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: (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, // 添加颜色属性
},
})),
} 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,
color: point.color, // 添加颜色属性
},
})),
} as echarts.SeriesOption);
}
// 处理每个连线组
solidData.forEach((item) => {
// 如果没有连线数据,则跳过
if (item[1].length === 0) {
return;
}
// 为每条连线创建飞行线
const pathColor = item[0] === "nested" ? "#0ea5e9" : "#F0FFA2"; // 根据类型设置默认颜色
// 添加飞行线
series.push({
name: item[0],
type: "lines",
zlevel: 1,
label: {
show: false, // 不使用内置标签
},
// 飞行线特效
effect: {
show: true, // 是否显示
period: 4, // 特效动画时间
trailLength: 0.7, // 特效尾迹长度。取从 0 到 1 的值,数值越大尾迹越长
color: pathColor, // 特效颜色
symbolSize: [10, 20],
},
// 线条样式
lineStyle: {
curveness: -0.4, // 飞线弧度
type: "solid", // 飞线类型
color: pathColor, // 使用从数据中获取的颜色
width: 1.5, // 飞线宽度
opacity: 0.1,
},
data: convertData(item[1]) as echarts.LinesSeriesOption["data"],
});
// 添加路径点的双层效果
const pathPoints = createPathPoints(item[1], true, pathColor);
series.push(...pathPoints);
// 添加出口节点的双层效果
item[1].forEach((lineData) => {
const lastExit = lineData[1];
if (lastExit) {
const exitNodes = createDualLayerPoint(
lastExit,
true,
lastExit.color || pathColor
);
series.push(...exitNodes);
}
});
});
// 处理其他线(保持原有逻辑)
otherLineList.forEach((line: any) => {
line.forEach((item: any) => {
const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
// 获取当前路径的颜色
const pathColor = item[1]?.[0]?.[0]?.color || "#F0FFA2"; // 从第一个点获取颜色,如果没有则使用默认颜色
// 添加虚线
series.push({
name: item[0],
type: "lines",
zlevel: 1,
label: {
show: false,
},
// 线条样式
lineStyle: {
curveness: -0.4, // 飞线弧度
type: [5, 5], // 飞线类型
color: pathColor, // 使用从数据中获取的颜色
width: 0.5, // 飞线宽度
opacity: 0.6,
},
data: convertData(item[1]) as echarts.LinesSeriesOption["data"],
});
// 添加路径点的双层效果(次要路径)
const pathPoints = createPathPoints(item[1], false, pathColor);
series.push(...pathPoints);
// 添加出口节点的双层效果(次要路径)
if (lastExit) {
const exitNodes = createDualLayerPoint(lastExit, false, pathColor);
series.push(...exitNodes);
}
});
});
return true;
};
// 创建A点和B点并添加飞线和标签
const createSpecialPoints = (series: echarts.SeriesOption[]) => {
// 定义点A和点B的坐标
const pointA = geoCoordMap[passAuthentication[0]?.startPoint ?? "GL"];
const pointB = geoCoordMap[passAuthentication[0]?.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);
if (
passAuthentication.length &&
passAuthentication[0]?.authenticationPoint
) {
createSpecialPoints(series); // 添加特殊点和飞线
createRipplePointsFromCoordinates(
passAuthentication[0]?.authenticationPoint || [],
series
);
}
const option = {
backgroundColor: "transparent",
// 全局提示框配置
tooltip: {
show: true,
trigger: "item",
enterable: true,
confine: true, // 保持提示框在图表范围内
appendToBody: true, // 将提示框附加到body以获得更好的定位
position: function (pos: any) {
// 自定义定位逻辑(如果需要)
return [pos[0] + 10, pos[1] - 50]; // 从光标偏移
},
},
// 底图样式
geo: {
map: "world", // 地图类型
roam: true, // 是否开启缩放
zoom: 1, // 初始缩放大小
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?.forEach((item) => item?.remove());
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();
if (tooltipClosed) {
positionCustomTooltip();
}
};
// 更新图表 - 关键修改:使用 silent 参数避免重新渲染动画
useEffect(() => {
if (!proxyGeoRef.current || !chartInitializedRef.current) return;
// 获取当前系列
const series: echarts.SeriesOption[] = [];
getLianData(series);
// 使用 setOption 更新图表,但保持动画状态
proxyGeoRef.current.setOption(
{ series: series },
{
notMerge: false, // 不合并会导致闪烁
silent: true, // 静默更新,不触发动画重新开始
lazyUpdate: true // 延迟更新
}
);
}, [nestedEncryptionLineIndex, dynamicRouteLineIndex]);
// 处理数据变化 - 关键修改:使用 silent 参数避免重新渲染动画
useEffect(() => {
if (!proxyGeoRef.current || !chartInitializedRef.current) return;
lineMidpointsRef.current = []; // 重置中点数据
// 获取当前系列
const series: echarts.SeriesOption[] = [];
getLianData(series);
if (
passAuthentication.length &&
passAuthentication[0]?.authenticationPoint
) {
createSpecialPoints(series);
createRipplePointsFromCoordinates(
passAuthentication[0]?.authenticationPoint || [],
series
);
}
// 使用 setOption 更新图表,但保持动画状态
proxyGeoRef.current.setOption(
{ series: series },
{
notMerge: false, // 不合并会导致闪烁
silent: true, // 静默更新,不触发动画重新开始
lazyUpdate: true // 延迟更新
}
);
// 创建DOM标签
setTimeout(createDOMLabels, 100);
}, [nestedEncryption, dynamicRouteGeneration, passAuthentication]);
// 初始化图表
useEffect(() => {
const chartDom = document.getElementById("screenGeo");
proxyGeoRef.current = echarts.init(chartDom);
echarts.registerMap(
"world",
worldGeoJson as unknown as Parameters<typeof echarts.registerMap>[1]
);
// 初始化时提取所有点
const initialPoints = extractAllPoints();
if (initialPoints.length > 0) {
setAllPoints(initialPoints);
}
const option = getOption();
option && proxyGeoRef.current?.setOption(option);
// 标记图表已初始化
chartInitializedRef.current = true;
// 添加地图交互事件监听器
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;
};
}, []);
// 在地图初始化后定位tooltip
useEffect(() => {
if (tooltipClosed) {
positionCustomTooltip();
}
}, [tooltipClosed, nestedEncryption]);
return (
<div className="flex-1 h-full flex flex-col">
<div id="screenGeo" className="flex-1"></div>
{tooltipClosed && (
<CustomTooltip
logs={logs}
onClose={handleCloseTooltip}
tooltipRef={customTooltipRef}
/>
)}
</div>
);
}
);
export default WorldGeo;