2025-04-16 19:00:51 +08:00

877 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import { useEffect, useMemo, useRef, memo } from "react";
import * as echarts from "echarts";
// import 'echarts-gl';
// import { useQueryClient } from "@tanstack/react-query";
import type { EChartsType } from "echarts";
import worldGeoJson from "@/assets/echarts-map/json/world.json";
import { geoCoordMap, countryNameMap, countryCodeMap } from "@/data";
import { getUrl } from "@/lib/utils";
import { CONST_TOOLTIP_TYPE } from "..";
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>
);
}
);