1532 lines
49 KiB
TypeScript
1532 lines
49 KiB
TypeScript
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; |