877 lines
40 KiB
TypeScript
877 lines
40 KiB
TypeScript
import { useEffect, useMemo, useRef, memo } from "react";
|
||
import * as echarts from "echarts";
|
||
// import 'echarts-gl';
|
||
// import { useQueryClient } from "@tanstack/react-query";
|
||
import type { EChartsType } from "echarts";
|
||
import worldGeoJson from "@/assets/echarts-map/json/world.json";
|
||
import { geoCoordMap, countryNameMap, countryCodeMap } from "@/data";
|
||
import { getUrl } from "@/lib/utils";
|
||
import { CONST_TOOLTIP_TYPE } from "..";
|
||
const planePathImg =
|
||
"image://data:image/svg+xml;charset=utf-8;base64,PHN2ZyB3aWR0aD0iNjciIGhlaWdodD0iMTAyIiB2aWV3Qm94PSIwIDAgNjcgMTAyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMF9mXzYxMTdfMjEyNDA3KSI+CjxwYXRoIGQ9Ik0zNC4yMTA5IDkxLjE4ODZMNTMuNjU3OCA0MC45NThDNTQuOTM4IDM3LjY1MTMgNTUuNzk4MyAzNC4xNTkyIDU1LjM1NjMgMzAuNjQxQzU0LjQzNTcgMjMuMzEyOCA1MC40Njg0IDExLjAyMDggMzQuMjExMiAxMS4wMjA4QzE5LjE5MDMgMTEuMDIwOCAxMy45MTEgMjEuNTE0NiAxMi4wNTU0IDI4Ljg5MTJDMTAuOTAxIDMzLjQ4MDYgMTEuOTkyNiAzOC4yMTg2IDEzLjgyMzEgNDIuNTgyN0wzNC4yMTA5IDkxLjE4ODZaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfNjExN18yMTI0MDcpIi8+CjwvZz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfZl82MTE3XzIxMjQwNyIgeD0iMC44OTE3NDQiIHk9IjAuMzMxOTkiIHdpZHRoPSI2NS4yNzA3IiBoZWlnaHQ9IjEwMS41NDUiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iNS4zNDQ0MSIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzYxMTdfMjEyNDA3Ii8+CjwvZmlsdGVyPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNjExN18yMTI0MDciIHgxPSIzNS4yODI2IiB5MT0iMTAuODU2NCIgeDI9IjM1LjI4MjYiIHkyPSI4Ni44NTY0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiMwMEYyRkYiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTUwMEZGIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg==";
|
||
interface LinesItemType {
|
||
name: string;
|
||
country_code: string;
|
||
value: number[];
|
||
}
|
||
type LinesDataType = [LinesItemType, LinesItemType];
|
||
type LinesType = [string, LinesDataType[]];
|
||
// 创建单个国家的涟漪效果
|
||
const createCountryRipple = (countryCode: string) => {
|
||
const coords = geoCoordMap[countryCode];
|
||
if (!coords) return null;
|
||
return {
|
||
name: countryCodeMap[countryCode] ?? "",
|
||
value: coords,
|
||
country_code: countryCode,
|
||
};
|
||
};
|
||
export const WorldGeo = memo(
|
||
({
|
||
screenData,
|
||
selectedApp,
|
||
tooltipType,
|
||
tooltipClosed,
|
||
setTooltipClosed,
|
||
}: {
|
||
screenData: any;
|
||
selectedApp: any;
|
||
tooltipType: string;
|
||
tooltipClosed: boolean;
|
||
setTooltipClosed: (value: boolean) => void;
|
||
}) => {
|
||
// const queryClient = useQueryClient()
|
||
const customTooltipRef = useRef<HTMLDivElement | null>(null);
|
||
const proxyGeoRef = useRef<EChartsType | null>(null);
|
||
const preMainToData = useRef<{ country_code: string }[]>([]);
|
||
const mainToData = useMemo(() => {
|
||
// 使用新的数据结构
|
||
const proxiesList =
|
||
selectedApp && selectedApp?.jumpList
|
||
? [selectedApp.jumpList]
|
||
: screenData?.proxy_info?.proxies ?? [];
|
||
// 初始化数据数组 - 不再包含 startCountry
|
||
const data: any = [];
|
||
// 遍历代理列表
|
||
proxiesList.forEach((proxyItem: any) => {
|
||
// 检查是否有数据数组
|
||
if (proxyItem.data && Array.isArray(proxyItem.data)) {
|
||
// 遍历数据数组中的每个项目
|
||
proxyItem.data.forEach((item: any) => {
|
||
// 如果有 ingress_country_code,则添加一对起点和终点
|
||
if (item.ingress_country_code) {
|
||
// 添加起点(country_code)
|
||
data.push({
|
||
country_code: item.country_code,
|
||
type: "start",
|
||
isLine: proxyItem.isLine, // 保存连线标志
|
||
});
|
||
// 添加终点(ingress_country_code)
|
||
data.push({
|
||
country_code: item.ingress_country_code,
|
||
type: "end",
|
||
isLine: proxyItem.isLine, // 保存连线标志
|
||
});
|
||
} else {
|
||
// 如果没有 ingress_country_code,只添加 country_code
|
||
data.push({
|
||
country_code: item.country_code,
|
||
isLine: proxyItem.isLine, // 保存连线标志
|
||
});
|
||
}
|
||
});
|
||
}
|
||
});
|
||
return data;
|
||
}, [screenData, selectedApp]);
|
||
// 创建自定义提示框DOM元素
|
||
const createCustomTooltip = () => {
|
||
// 如果已经存在自定义提示框,则移除它
|
||
if (document.getElementById("custom-fixed-tooltip")) {
|
||
document.getElementById("custom-fixed-tooltip")?.remove();
|
||
}
|
||
// 创建自定义提示框
|
||
const tooltip = document.createElement("div");
|
||
tooltip.id = "custom-fixed-tooltip";
|
||
tooltip.style.position = "fixed";
|
||
tooltip.style.zIndex = "1000";
|
||
tooltip.style.pointerEvents = "auto";
|
||
tooltip.style.backgroundColor = "transparent";
|
||
// 设置提示框内容
|
||
const currentTooltipType =
|
||
CONST_TOOLTIP_TYPE[
|
||
tooltipType as keyof typeof CONST_TOOLTIP_TYPE
|
||
] || CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION;
|
||
tooltip.innerHTML = `
|
||
<div class="tooltip-content">
|
||
<img class="line-img" src="${getUrl(
|
||
"svg/anti-forensics-forwarding/Line.svg"
|
||
)}" alt="" />
|
||
<div class="fill"></div>
|
||
<div class="tip-box">
|
||
<div>
|
||
<div class="label" style="color: white; font-weight: bold;">${
|
||
currentTooltipType.title
|
||
}</div>
|
||
<img class="close-icon" src="${getUrl(
|
||
"svg/Xwhite.svg"
|
||
)}" alt=""
|
||
style="cursor: pointer; " />
|
||
</div>
|
||
|
||
<img class="${
|
||
CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.type ===
|
||
currentTooltipType.type
|
||
? "encryption-img"
|
||
: "traffic-obfuscation-img"
|
||
}" src="${getUrl(
|
||
CONST_TOOLTIP_TYPE.NESTED_ENCRYPTION.type ===
|
||
currentTooltipType.type
|
||
? "image/nested-encryption.png"
|
||
: "image/traffic-obfuscation.png"
|
||
)}" alt="" />
|
||
|
||
</div>
|
||
</div>
|
||
`;
|
||
// 添加到DOM
|
||
document.body.appendChild(tooltip);
|
||
customTooltipRef.current = tooltip;
|
||
// 添加关闭按钮事件
|
||
const closeButton = tooltip.querySelector(".close-icon");
|
||
if (closeButton) {
|
||
closeButton.addEventListener("click", () => {
|
||
setTooltipClosed(false);
|
||
tooltip.remove();
|
||
customTooltipRef.current = null;
|
||
});
|
||
}
|
||
// 定位提示框
|
||
positionCustomTooltip();
|
||
};
|
||
// 定位自定义提示框 - 优化版本
|
||
const positionCustomTooltip = () => {
|
||
if (!customTooltipRef.current || !proxyGeoRef.current) return;
|
||
// 找到US点
|
||
const coords = geoCoordMap["CA"];
|
||
if (!coords) return;
|
||
try {
|
||
// 将地理坐标转换为屏幕坐标
|
||
const screenCoord = proxyGeoRef.current.convertToPixel(
|
||
"geo",
|
||
coords
|
||
);
|
||
if (
|
||
screenCoord &&
|
||
Array.isArray(screenCoord) &&
|
||
screenCoord.length === 2
|
||
) {
|
||
// 设置提示框位置
|
||
// customTooltipRef.current.style.left = `${
|
||
// screenCoord[0] + screenCoord[0] / 2 + 18
|
||
// }px`;
|
||
// customTooltipRef.current.style.top = `${
|
||
// screenCoord[1] - screenCoord[1] / 2 + 7
|
||
// }px`;
|
||
// 设置提示框位置
|
||
customTooltipRef.current.style.left = `${
|
||
screenCoord[0] + 232 + 7
|
||
}px`;
|
||
customTooltipRef.current.style.top = `${
|
||
screenCoord[1] + 40 - 190
|
||
}px`;
|
||
}
|
||
} catch (error) {
|
||
console.error("Error positioning tooltip:", error);
|
||
}
|
||
};
|
||
// 主线每个节点tip竖线的经纬度
|
||
const mianLineData = (data: typeof mainToData) => {
|
||
return (
|
||
data
|
||
.map((item: any) => {
|
||
const countryCode = item.country_code.toUpperCase();
|
||
const coords = geoCoordMap[countryCode] as
|
||
| [number, number]
|
||
| undefined;
|
||
if (!coords) return null;
|
||
return {
|
||
name: countryCodeMap[countryCode],
|
||
coords: [coords, [coords[0], coords[1] + 4]],
|
||
value: countryCode,
|
||
};
|
||
})
|
||
.filter((v: any) => !!v) ?? []
|
||
);
|
||
};
|
||
const getLineItem = (
|
||
preCode: string,
|
||
nextCode: string
|
||
): [LinesItemType, LinesItemType] => {
|
||
return [
|
||
{
|
||
name: countryCodeMap[preCode] ?? "",
|
||
value: geoCoordMap[preCode] ?? [],
|
||
country_code: preCode,
|
||
},
|
||
{
|
||
name: countryCodeMap[nextCode] ?? "",
|
||
value: geoCoordMap[nextCode] ?? [],
|
||
country_code: nextCode,
|
||
},
|
||
];
|
||
};
|
||
const getLine = () => {
|
||
// 实现数据处理
|
||
const solidData: LinesType[] = [["main", []]]; // 使用"main"替代startCountry.country_code
|
||
// 收集需要显示涟漪效果的所有点(包括连线和不连线的)
|
||
const ripplePoints: any[] = [];
|
||
// 处理主路径数据
|
||
for (let i = 0; i < mainToData.length; i++) {
|
||
// 如果是最后一个元素,则跳过(因为没有下一个元素作为终点)
|
||
if (i === mainToData.length - 1) continue;
|
||
const currentItem = mainToData[i];
|
||
const nextItem = mainToData[i + 1];
|
||
// 获取当前国家代码
|
||
const countryCode = currentItem.country_code.toUpperCase();
|
||
// 如果当前项是起点,下一项是终点
|
||
if (currentItem.type === "start" && nextItem.type === "end") {
|
||
const startCode = countryCode;
|
||
const endCode = nextItem.country_code.toUpperCase();
|
||
// 无论是否连线,都添加点的涟漪效果
|
||
const startPoint = createCountryRipple(startCode);
|
||
const endPoint = createCountryRipple(endCode);
|
||
if (startPoint) ripplePoints.push(startPoint);
|
||
if (endPoint) ripplePoints.push(endPoint);
|
||
// 检查是否应该绘制连线
|
||
if (currentItem.isLine !== false) {
|
||
solidData[0]?.[1].push(getLineItem(startCode, endCode));
|
||
}
|
||
// 跳过下一项,因为已经处理了
|
||
i++;
|
||
}
|
||
// 常规情况:当前项到下一项
|
||
else {
|
||
const nextCountryCode = nextItem.country_code.toUpperCase();
|
||
// 无论是否连线,都添加点的涟漪效果
|
||
const currentPoint = createCountryRipple(countryCode);
|
||
const nextPoint = createCountryRipple(nextCountryCode);
|
||
if (currentPoint) ripplePoints.push(currentPoint);
|
||
if (nextPoint) ripplePoints.push(nextPoint);
|
||
// 检查是否应该绘制连线
|
||
if (currentItem.isLine !== false) {
|
||
solidData[0]?.[1].push(
|
||
getLineItem(countryCode, nextCountryCode)
|
||
);
|
||
}
|
||
}
|
||
}
|
||
// 虚线数据处理(保持原有逻辑)
|
||
const pathList =
|
||
screenData?.path_list?.filter(
|
||
(v: any) => v.name !== screenData?.proxy_info?.name
|
||
) ?? [];
|
||
const otherLineList = pathList.map(() => {});
|
||
return {
|
||
solidData,
|
||
otherLineList,
|
||
ripplePoints,
|
||
};
|
||
};
|
||
// 获取连线经纬度数据
|
||
const convertData = (data: LinesDataType[]) => {
|
||
const res = [];
|
||
for (let index = 0; index < data.length; index++) {
|
||
const dataIndex = data[index];
|
||
const fromCoord =
|
||
geoCoordMap[dataIndex?.[0]?.country_code ?? ""];
|
||
const toCoord = geoCoordMap[dataIndex?.[1]?.country_code ?? ""];
|
||
if (fromCoord && toCoord) {
|
||
res.push([fromCoord, toCoord]);
|
||
}
|
||
}
|
||
return res;
|
||
};
|
||
// 创建双层点效果 - 大点
|
||
const createDualLayerPoint = (
|
||
lastExit: LinesItemType,
|
||
isMainPath: boolean = true
|
||
) => {
|
||
// 创建数据数组,用于两个散点图层
|
||
const pointData = lastExit
|
||
? [lastExit].map((v) => {
|
||
return {
|
||
name: v.name,
|
||
value: v.value,
|
||
datas: {
|
||
country_code: v.country_code,
|
||
},
|
||
};
|
||
})
|
||
: [];
|
||
// 根据是否是主路径设置不同的大小和颜色
|
||
const outerSize = isMainPath ? 8 : 4;
|
||
const innerSize = isMainPath ? 4 : 2;
|
||
// Use selectedApp.color if available, otherwise default to blue
|
||
const outerColor = selectedApp?.color || "#0ea5e9"; // Use selectedApp.color with fallback
|
||
const innerColor = "#FFFFFF"; // 白色内层
|
||
return [
|
||
{
|
||
// 外层蓝色点,带涟漪效果
|
||
type: "effectScatter",
|
||
coordinateSystem: "geo",
|
||
zlevel: 3,
|
||
color: outerColor,
|
||
symbol: "circle",
|
||
symbolSize: outerSize,
|
||
rippleEffect: {
|
||
period: 8, // 动画时间,值越小速度越快
|
||
brushType: "stroke", // 波纹绘制方式 stroke
|
||
scale: 6, // 波纹圆环最大限制,值越大波纹越大
|
||
brushWidth: 2,
|
||
},
|
||
label: {
|
||
show: false,
|
||
},
|
||
tooltip: {
|
||
show: false,
|
||
trigger: "item",
|
||
showContent: true,
|
||
alwaysShowContent: true,
|
||
formatter: (params: any) => {
|
||
// const countryCode = params.data.datas.country_code;
|
||
// const countryName = params.data.name;
|
||
// 创建自定义HTML提示框
|
||
return `
|
||
<div class="tip-box">
|
||
<img class="close-icon" src="${getUrl(
|
||
"svg/Xwhite.svg"
|
||
)}" alt="" />
|
||
<div class="label">嵌套加密</div>
|
||
<img class="encryption-img" width="100%" src="${getUrl(
|
||
"image/nested-encryption.png"
|
||
)}" alt="" />
|
||
</div>
|
||
`;
|
||
},
|
||
backgroundColor: "transparent",
|
||
borderWidth: 0,
|
||
},
|
||
data: pointData,
|
||
} as echarts.SeriesOption,
|
||
{
|
||
// 内层白色点,不带涟漪效果
|
||
type: "scatter", // 使用普通scatter,不带特效
|
||
coordinateSystem: "geo",
|
||
zlevel: 4, // 确保在蓝色点上方
|
||
color: innerColor,
|
||
symbol: "circle",
|
||
symbolSize: innerSize,
|
||
label: {
|
||
show: false,
|
||
},
|
||
data: pointData,
|
||
} as echarts.SeriesOption,
|
||
];
|
||
};
|
||
// 创建路径点的双层效果
|
||
const createPathPoints = (
|
||
dataItems: LinesDataType[],
|
||
isMainPath: boolean = true
|
||
) => {
|
||
// 创建数据数组
|
||
const pointData = dataItems.map((dataItem: LinesDataType) => {
|
||
return {
|
||
name: dataItem[0].name,
|
||
value: geoCoordMap[dataItem[0].country_code],
|
||
datas: {
|
||
country_code: dataItem[0].country_code,
|
||
},
|
||
};
|
||
});
|
||
// 根据是否是主路径设置不同的大小和颜色
|
||
const outerSize = isMainPath ? 8 : 4;
|
||
const innerSize = isMainPath ? 4 : 2;
|
||
// Use selectedApp.color if available, otherwise default to blue
|
||
const outerColor = selectedApp?.color || "#0ea5e9"; // Use selectedApp.color with fallback
|
||
const innerColor = "#FFFFFF"; // 白色内层
|
||
return [
|
||
{
|
||
// 外层蓝色点,带涟漪效果
|
||
type: "effectScatter",
|
||
coordinateSystem: "geo",
|
||
zlevel: 3,
|
||
color: outerColor,
|
||
symbol: "circle",
|
||
symbolSize: outerSize,
|
||
rippleEffect: {
|
||
period: 8, // 动画时间,值越小速度越快
|
||
brushType: "stroke", // 波纹绘制方式 stroke
|
||
scale: 6, // 波纹圆环最大限制,值越大波纹越大
|
||
brushWidth: 2,
|
||
},
|
||
label: {
|
||
show: false,
|
||
},
|
||
tooltip: {
|
||
show: false,
|
||
trigger: "item",
|
||
formatter: (params: any) => {
|
||
// const countryCode = params.data.datas.country_code;
|
||
// const countryName = params.data.name;
|
||
// 创建自定义HTML提示框
|
||
return `
|
||
<div class="tip-box">
|
||
<img class="close-icon" src="${getUrl(
|
||
"svg/Xwhite.svg"
|
||
)}" alt="" />
|
||
<div class="label">嵌套加密</div>
|
||
<img class="encryption-img" width="100%" src="${getUrl(
|
||
"image/nested-encryption.png"
|
||
)}" alt="" />
|
||
</div>
|
||
`;
|
||
},
|
||
backgroundColor: "transparent",
|
||
borderWidth: 0,
|
||
},
|
||
data: pointData,
|
||
} as echarts.SeriesOption,
|
||
{
|
||
// 内层白色点,不带涟漪效果
|
||
type: "scatter", // 使用普通scatter,不带特效
|
||
coordinateSystem: "geo",
|
||
zlevel: 4, // 确保在蓝色点上方
|
||
color: innerColor,
|
||
symbol: "circle",
|
||
symbolSize: innerSize,
|
||
label: {
|
||
show: false,
|
||
},
|
||
data: pointData,
|
||
} as echarts.SeriesOption,
|
||
];
|
||
};
|
||
// 创建带自定义提示框的涟漪点
|
||
const createRipplePointsWithTooltip = (ripplePoints: any) => {
|
||
// Use selectedApp.color if available, otherwise default to blue
|
||
const outerColor = selectedApp?.color || "#0ea5e9"; // Use selectedApp.color with fallback
|
||
|
||
return {
|
||
type: "effectScatter",
|
||
coordinateSystem: "geo",
|
||
zlevel: 3,
|
||
color: outerColor,
|
||
symbol: "circle",
|
||
symbolSize: 8,
|
||
rippleEffect: {
|
||
period: 8,
|
||
brushType: "stroke",
|
||
scale: 6,
|
||
brushWidth: 2,
|
||
},
|
||
label: {
|
||
show: false,
|
||
formatter: (params: any) => {
|
||
return `{${params.data.datas.country_code}|}`;
|
||
},
|
||
},
|
||
// 添加提示框配置
|
||
tooltip: {
|
||
show: false,
|
||
trigger: "item",
|
||
formatter: (params: any) => {
|
||
// const countryCode = params.data.datas.country_code;
|
||
// const countryName = params.data.name;
|
||
// 创建自定义HTML提示框
|
||
return `
|
||
<div class="tip-box">
|
||
<img class="close-icon" src="${getUrl(
|
||
"svg/Xwhite.svg"
|
||
)}" alt="" />
|
||
<div class="label">嵌套加密</div>
|
||
<img class="encryption-img" width="100%" src="${getUrl(
|
||
"image/nested-encryption.png"
|
||
)}" alt="" />
|
||
</div>
|
||
`;
|
||
},
|
||
backgroundColor: "transparent",
|
||
borderWidth: 0,
|
||
},
|
||
data: ripplePoints.map((point: any) => ({
|
||
name: point.name,
|
||
value: point.value,
|
||
datas: {
|
||
country_code: point.country_code,
|
||
},
|
||
})),
|
||
} as echarts.SeriesOption;
|
||
};
|
||
// 连线 series
|
||
const getLianData = (series: echarts.SeriesOption[]) => {
|
||
const { solidData, otherLineList, ripplePoints } = getLine();
|
||
console.log(solidData, "solidData");
|
||
console.log(otherLineList, "otherLineList");
|
||
console.log(ripplePoints, "ripplePoints");
|
||
// 如果有需要显示涟漪效果的点,添加它们
|
||
if (ripplePoints.length > 0) {
|
||
// 添加带自定义提示框的外层蓝色点
|
||
series.push(createRipplePointsWithTooltip(ripplePoints));
|
||
// 添加内层白色点,不带涟漪效果
|
||
series.push({
|
||
type: "scatter", // 使用普通scatter,不带特效
|
||
coordinateSystem: "geo",
|
||
zlevel: 4, // 确保在蓝色点上方
|
||
color: "#FFFFFF", // 白色内层
|
||
symbol: "circle",
|
||
symbolSize: 4,
|
||
label: {
|
||
show: false,
|
||
},
|
||
data: ripplePoints.map((point) => ({
|
||
name: point.name,
|
||
value: point.value,
|
||
datas: {
|
||
country_code: point.country_code,
|
||
},
|
||
})),
|
||
} as echarts.SeriesOption);
|
||
}
|
||
solidData.forEach((item) => {
|
||
// 如果没有连线数据,则跳过
|
||
if (item[1].length === 0) {
|
||
return;
|
||
}
|
||
const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
|
||
// 添加飞行线
|
||
series.push({
|
||
name: item[0], // todo ! 需要在飞线中间添加label
|
||
type: "lines",
|
||
zlevel: 1,
|
||
label: {
|
||
show: false,
|
||
position: "middle",
|
||
formatter: (params: any) => {
|
||
|
||
// 使用自定义样式的文本
|
||
return (
|
||
"{text|SS签名}"
|
||
);
|
||
},
|
||
rich: {
|
||
text: {
|
||
color: "#FFB27A", // 字体颜色为 #FFB27A
|
||
fontSize: 18, // 字体大小为 18px
|
||
padding: [10,10,10,10], // padding 上下为 4,左右为 8
|
||
backgroundColor: "#8B3700", // 背景颜色为 #8B3700
|
||
borderRadius: 4, // 可选:添加圆角效果
|
||
},
|
||
},
|
||
// 不需要额外的背景色,因为已经在 rich 中设置了
|
||
backgroundColor: "transparent",
|
||
padding: [0, 0, 0, 0],
|
||
},
|
||
// 飞行线特效
|
||
effect: {
|
||
show: true, // 是否显示
|
||
period: 4, // 特效动画时间
|
||
trailLength: 0.7, // 特效尾迹长度。取从 0 到 1 的值,数值越大尾迹越长
|
||
symbol: planePathImg, // 特效图形标记
|
||
symbolSize: [10, 20],
|
||
},
|
||
// 线条样式
|
||
lineStyle: {
|
||
curveness: -0.4, // 飞线弧度
|
||
type: "solid", // 飞线类型
|
||
color: selectedApp?.color || "#0ea5e9", // Use selectedApp.color with fallback
|
||
width: 1.5, // 飞线宽度
|
||
opacity: 0.1,
|
||
},
|
||
data: convertData(
|
||
item[1]
|
||
) as echarts.LinesSeriesOption["data"],
|
||
});
|
||
// 添加路径点的双层效果
|
||
const pathPoints = createPathPoints(item[1], true);
|
||
series.push(...pathPoints);
|
||
// 添加出口节点的双层效果
|
||
if (lastExit) {
|
||
const exitNodes = createDualLayerPoint(lastExit, true);
|
||
series.push(...exitNodes);
|
||
}
|
||
});
|
||
otherLineList.forEach((line: any) => {
|
||
line.forEach((item: any) => {
|
||
const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null;
|
||
// 添加虚线
|
||
series.push({
|
||
name: item[0],
|
||
type: "lines",
|
||
zlevel: 1,
|
||
label: {
|
||
show: false,
|
||
},
|
||
// 线条样式
|
||
lineStyle: {
|
||
curveness: -0.4, // 飞线弧度
|
||
type: [5, 5], // 飞线类型
|
||
color: "#F0FFA2", // 飞线颜色
|
||
width: 0.5, // 飞线宽度
|
||
opacity: 0.6,
|
||
},
|
||
data: convertData(
|
||
item[1]
|
||
) as echarts.LinesSeriesOption["data"],
|
||
});
|
||
// 添加路径点的双层效果(次要路径)
|
||
const pathPoints = createPathPoints(item[1], false);
|
||
series.push(...pathPoints);
|
||
// 添加出口节点的双层效果(次要路径)
|
||
if (lastExit) {
|
||
const exitNodes = createDualLayerPoint(lastExit, false);
|
||
series.push(...exitNodes);
|
||
}
|
||
});
|
||
});
|
||
return true;
|
||
};
|
||
// 主线tip series
|
||
const getMianLineTipData = (series: echarts.SeriesOption[] = []) => {
|
||
const rich = Object.keys(countryCodeMap).reduce((object, code) => {
|
||
object[code] = {
|
||
color: "transparent",
|
||
height: 20,
|
||
width: 20,
|
||
align: "left",
|
||
backgroundColor: {
|
||
image: getUrl(
|
||
`image/res/flag/${code.toUpperCase()}.png`
|
||
), // 动态生成国旗图标
|
||
},
|
||
};
|
||
return object;
|
||
}, {} as { [key: string]: { [key: string]: number | string | unknown } });
|
||
series.push(
|
||
// 柱状体的主干
|
||
{
|
||
name: "solidTip",
|
||
type: "lines",
|
||
zlevel: 5,
|
||
effect: {
|
||
show: false,
|
||
symbolSize: 5, // 图标大小
|
||
},
|
||
lineStyle: {
|
||
width: 2, // 尾迹线条宽度
|
||
color: "#F0FFA2",
|
||
opacity: 1, // 尾迹线条透明度
|
||
curveness: 0, // 尾迹线条曲直度
|
||
},
|
||
label: {
|
||
show: true,
|
||
position: "end",
|
||
color: "#fff",
|
||
formatter: (parameters) => {
|
||
return `{left|} {gap1|}{${parameters.value}|}{gap2|}{name|${parameters.name}}{gap3|}{right|}`;
|
||
},
|
||
rich: {
|
||
left: {
|
||
color: "transparent",
|
||
height: 35,
|
||
width: 8,
|
||
align: "center",
|
||
backgroundColor: {
|
||
image: `data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMSIgaGVpZ2h0PSI1OCIgdmlld0JveD0iMCAwIDExIDU4IiBmaWxsPSJub25lIj4KPHBhdGggZD0iTTExIDU2LjA4ODRIMVY0Ni4wODg0IiBzdHJva2U9IiNGRkZERDMiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIi8+CjxwYXRoIGQ9Ik0xIDExVjFIMTEiIHN0cm9rZT0iI0ZGRkREMyIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiLz4KPHBhdGggZD0iTTguNzI5NDkgMTlWMzkiIHN0cm9rZT0iI0ZGRkREMyIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiLz4KPC9zdmc+`, // 动态生成国旗图标
|
||
},
|
||
},
|
||
gap1: {
|
||
height: 35,
|
||
width: 8,
|
||
},
|
||
...rich,
|
||
gap2: {
|
||
height: 35,
|
||
width: 6,
|
||
},
|
||
name: {
|
||
color: "#fff",
|
||
align: "center",
|
||
lineHeight: 35,
|
||
fontSize: 14,
|
||
padding: [2, 0, 0, 0],
|
||
},
|
||
gap3: {
|
||
height: 35,
|
||
width: 8,
|
||
},
|
||
right: {
|
||
color: "transparent",
|
||
height: 35,
|
||
width: 8,
|
||
align: "center",
|
||
backgroundColor: {
|
||
image: `data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNCIgaGVpZ2h0PSI1OCIgdmlld0JveD0iMCAwIDE0IDU4IiBmaWxsPSJub25lIj4KPHBhdGggZD0iTTEyLjczMDIgNDYuMDQzOVY1Ni4wNDM5SDIuNzMwMjIiIHN0cm9rZT0iI0ZGRkREMyIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiLz4KPHBhdGggZD0iTTIuNzMwMjIgMS4wNDM5NUgxMi43MzAyVjExLjA0MzkiIHN0cm9rZT0iI0ZGRkREMyIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiLz4KPHBhdGggZD0iTTEgMTkuMDQzOVYzOS4wNDM5IiBzdHJva2U9IiNGRkZERDMiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIi8+Cjwvc3ZnPg==`, // 动态生成国旗图标
|
||
},
|
||
},
|
||
},
|
||
backgroundColor: "#080A00",
|
||
},
|
||
silent: true,
|
||
data: mianLineData(mainToData),
|
||
}
|
||
);
|
||
};
|
||
const isCN = (code: string) => {
|
||
return ["HK", "MO", "TW", "CN"].includes(code.toUpperCase());
|
||
};
|
||
const getRegions = () => {
|
||
const codeList: string[] = [];
|
||
const regionsData = mainToData;
|
||
regionsData.forEach((item: any) =>
|
||
codeList.push(
|
||
isCN(item.country_code)
|
||
? "CN"
|
||
: item.country_code.toUpperCase()
|
||
)
|
||
);
|
||
const regions = codeList.map((item) => {
|
||
return {
|
||
name: countryCodeMap[item], // 中国
|
||
itemStyle: {
|
||
color: "#172554",
|
||
areaColor: "#172554",
|
||
borderColor: "#0ea5e9", // 边框颜色
|
||
borderWidth: 1.2, // 边框宽度
|
||
borderType: "solid", // 修改为实线边框
|
||
},
|
||
};
|
||
});
|
||
return regions;
|
||
};
|
||
const getOption = () => {
|
||
const series: echarts.SeriesOption[] = [];
|
||
getLianData(series);
|
||
// getMianLineTipData(series);// 添加主线tip 暂时隐藏
|
||
const regions = getRegions();
|
||
const option = {
|
||
backgroundColor: "transparent",
|
||
// 全局提示框配置
|
||
tooltip: {
|
||
show: true,
|
||
trigger: "item",
|
||
enterable: true,
|
||
confine: true, // 保持提示框在图表范围内
|
||
appendToBody: true, // 将提示框附加到body以获得更好的定位
|
||
// position: function(pos:any, params, dom, rect, size) {
|
||
position: function (pos: any) {
|
||
// 自定义定位逻辑(如果需要)
|
||
return [pos[0] + 10, pos[1] - 50]; // 从光标偏移
|
||
},
|
||
},
|
||
// 底图样式
|
||
geo: {
|
||
map: "world", // 地图类型
|
||
roam: true, // 是否开启缩放
|
||
zoom: 1, // 初始缩放大小
|
||
// center: [11.3316626, 19.5845024], // 地图中心点
|
||
layoutCenter: ["50%", "50%"], //地图位置
|
||
scaleLimit: {
|
||
// 缩放等级
|
||
min: 1,
|
||
max: 3,
|
||
},
|
||
label: {
|
||
show: false,
|
||
},
|
||
nameMap: countryNameMap, // 自定义地区的名称映射
|
||
// 三维地理坐标系样式
|
||
itemStyle: {
|
||
areaColor: "#020617", // 修改为要求的填充颜色
|
||
borderColor: "#cbd5e1", // 修改为要求的边框颜色
|
||
borderWidth: 1, // 边框宽度
|
||
borderType: "dashed", // 修改为点线边框
|
||
},
|
||
emphasis: {
|
||
itemStyle: {
|
||
areaColor: "#172554", // 修改为鼠标悬停时的填充颜色
|
||
borderColor: "#0ea5e9", // 修改为鼠标悬停时的边框颜色
|
||
borderWidth: 1.2, // 修改为鼠标悬停时的边框宽度
|
||
borderType: "solid", // 修改为实线边框
|
||
},
|
||
label: false,
|
||
},
|
||
tooltip: {
|
||
show: true,
|
||
trigger: "item",
|
||
triggerOn: "click", // 提示框触发的条件
|
||
enterable: true, // 鼠标是否可进入提示框浮层中,默认为false,如需详情内交互,如添加链接,按钮,可设置为 true
|
||
backgroundColor: "rgba(0,0,0,0.8)",
|
||
borderColor: "rgba(0,0,0,0.2)",
|
||
textStyle: {
|
||
color: "#fff",
|
||
},
|
||
formatter: (parameters: {
|
||
name: string;
|
||
data:
|
||
| {
|
||
name: string;
|
||
datas: { tradingCountry: string };
|
||
}
|
||
| undefined;
|
||
}) => {
|
||
if (parameters.data?.name)
|
||
return parameters.data.name;
|
||
return parameters.name;
|
||
},
|
||
},
|
||
regions,
|
||
},
|
||
series: series,
|
||
};
|
||
return option;
|
||
};
|
||
const handleResize = () => {
|
||
proxyGeoRef.current?.resize();
|
||
};
|
||
useEffect(() => {
|
||
preMainToData.current?.some(
|
||
(item, index) =>
|
||
item.country_code !== mainToData[index]?.country_code
|
||
) && proxyGeoRef.current?.clear();
|
||
preMainToData.current = mainToData;
|
||
const option = getOption();
|
||
proxyGeoRef.current?.setOption(option);
|
||
}, [screenData, mainToData]);
|
||
useEffect(() => {
|
||
const chartDom = document.getElementById("screenGeo");
|
||
proxyGeoRef.current = echarts.init(chartDom);
|
||
echarts.registerMap(
|
||
"world",
|
||
worldGeoJson as unknown as Parameters<
|
||
typeof echarts.registerMap
|
||
>[1]
|
||
);
|
||
const option = getOption();
|
||
option && proxyGeoRef.current?.setOption(option);
|
||
// 页面resize时触发
|
||
window.addEventListener("resize", handleResize);
|
||
return () => {
|
||
window.removeEventListener("resize", handleResize);
|
||
proxyGeoRef.current?.dispose();
|
||
proxyGeoRef.current = null;
|
||
};
|
||
}, []);
|
||
useEffect(() => {
|
||
if (tooltipClosed) {
|
||
createCustomTooltip();
|
||
}
|
||
}, [tooltipClosed, tooltipType]);
|
||
return (
|
||
<div className="flex-1 h-full flex flex-col">
|
||
<div id="screenGeo" className="flex-1"></div>
|
||
</div>
|
||
);
|
||
}
|
||
);
|