diff --git a/src/index.scss b/src/index.scss index 5bcfe37..584366b 100644 --- a/src/index.scss +++ b/src/index.scss @@ -7,6 +7,20 @@ body { } + +.scrollbar-visible { + scrollbar-width: auto; + /* For Firefox */ + -ms-overflow-style: scrollbar; + /* For Internet Explorer and Edge */ +} + +.scrollbar-visible::-webkit-scrollbar { + display: block; + /* For Chrome, Safari, and Opera */ + height: 10px; +} + ::selection { // background-color: #18181b; background-color: #1E3A8A; @@ -17,7 +31,7 @@ body { ::-webkit-scrollbar { width: 6px; height: 6px; - /* background-color: red; */ +// background-color: red; } /* ::-webkit-scrollbar-track { @@ -103,4 +117,4 @@ body { /* Internet Explorer/Edge (旧版) */ user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */ -} +} \ No newline at end of file diff --git a/src/layout/index.tsx b/src/layout/index.tsx index 6318eca..f1feac0 100644 --- a/src/layout/index.tsx +++ b/src/layout/index.tsx @@ -9,100 +9,102 @@ import TitleSvg from "@/assets/svg/layout/title.svg?react"; import ChevronDownSvg from "@/assets/svg/layout/chevron-down.svg?react"; import Decentralized from "@/assets/svg/layout/decentralized.svg?react"; -import PoolSvg from '@/assets/svg/layout/pool.svg?react' -import HomeSvg from '@/assets/svg/layout/home.svg?react' -import AntiDarkAnalysisNetworkSvg from '@/assets/svg/layout/anti-dark-analysis-network.svg?react' +import PoolSvg from "@/assets/svg/layout/pool.svg?react"; +import HomeSvg from "@/assets/svg/layout/home.svg?react"; +import AntiDarkAnalysisNetworkSvg from "@/assets/svg/layout/anti-dark-analysis-network.svg?react"; import "./index.scss"; import type { RootState } from "@/store"; export default function Layout() { - const [_, setActive] = useState(0); - const { coreVersion } = useSelector( - (state: RootState) => state.serviceReducer - ); + const [_, setActive] = useState(0); + const { coreVersion } = useSelector( + (state: RootState) => state.serviceReducer + ); - const navList = [ - { - id: "new-home", - title: "首页", - icon: , - }, - { - id: "home", - title: "去中心化的弹性网络", - icon: , - }, - { - id: "anti-forensics-forwarding", - title: "面向溯源对抗的数据转发", - icon: , - }, - { - id: "anti-dark-analysis-network", - title: "抗暗特征分析的隐匿网络应用", - icon: , - }, - // { - // id: 'proxies', - // title: '节点池', - // icon: , - // }, - ]; + const navList = [ + { + id: "new-home", + title: "首页", + icon: , + }, + { + id: "home", + title: "去中心化的弹性网络", + icon: , + }, + { + id: "anti-forensics-forwarding", + title: "面向溯源对抗的数据转发", + icon: , + }, + { + id: "anti-dark-analysis-network", + title: "抗暗特征分析的隐匿网络应用", + icon: , + }, + // { + // id: 'proxies', + // title: '节点池', + // icon: , + // }, + ]; - const handleClickMenu = (index: number) => { - setActive(index); - }; + const handleClickMenu = (index: number) => { + setActive(index); + }; - return ( -
-
-
- -
- - - {/* 匿名反溯源网络系统 */} - {/* Anonymous anti traceability network system */} -
-
- -
-
-
版本:{coreVersion || "v0.0.1"}
- {/*
环境:DEV
*/} -
- -
-
-
- -
+ return ( +
+
+
+ +
+ + + {/* 匿名反溯源网络系统 */} + {/* Anonymous anti traceability network system */} +
+
+ +
+
+
版本:{coreVersion || "v0.0.1"}
+ {/*
环境:DEV
*/} +
+ +
+
+
+
+
- ); +
+
+ ); } diff --git a/src/pages/anti-dark-analysis-network/components/world-geo copy 2.tsx b/src/pages/anti-dark-analysis-network/components/world-geo copy 2.tsx deleted file mode 100644 index 472bb7b..0000000 --- a/src/pages/anti-dark-analysis-network/components/world-geo copy 2.tsx +++ /dev/null @@ -1,949 +0,0 @@ -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(null); - const proxyGeoRef = useRef(null); - const preMainToData = useRef<{ country_code: string }[]>([]); - const lineMidpointsRef = useRef<{id: string, midpoint: number[], fromCountry: string, toCountry: string}[]>([]); - const labelContainerRef = useRef(null); - const labelsRef = useRef([]); - - 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 = ` -
- -
-
-
-
${ - currentTooltipType.title - }
- -
- - - -
-
- `; - // 添加到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] + 232 + 7 - }px`; - customTooltipRef.current.style.top = `${ - screenCoord[1] + 40 - 190 - }px`; - } - } catch (error) { - console.error("Error positioning tooltip:", error); - } - }; - // 主线每个节点tip竖线的经纬度,修改tip 竖线的高度也可以用这个 - const mianLineData = (data: typeof mainToData) => { - return ( - data - .map((item: any) => { - const countryCode = item.country_code.toUpperCase(); - 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 = []; - 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([fromCoord, toCoord]); - - // 计算中点,考虑曲线的弧度 - const curveness = -0.4; // 与飞线弧度相同 - const x1 = fromCoord[0]; - const y1 = fromCoord[1]; - const x2 = toCoord[0]; - const y2 = toCoord[1]; - - // 计算控制点 - const cpx = (x1 + x2) / 2 - (y2 - y1) * curveness; - const cpy = (y1 + y2) / 2 - (x1 - x2) * curveness; - - // 计算曲线上的中点 (t=0.5 时的贝塞尔曲线点) - const midX = x1 * 0.25 + cpx * 0.5 + x2 * 0.25; - const midY = y1 * 0.25 + cpy * 0.5 + y2 * 0.25; - - midpoints.push({ - id: `line-label-${index}`, - midpoint: [midX, midY], - fromCountry, - toCountry - }); - } - } - - // 更新中点引用 - lineMidpointsRef.current = midpoints; - - return res; - }; - // 创建双层点效果 - 大点 - const createDualLayerPoint = ( - lastExit: LinesItemType, - isMainPath: boolean = true - ) => { - // 创建数据数组,用于两个散点图层 - 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 ` -
- -
嵌套加密
- -
- `; - }, - 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 ` -
- -
嵌套加密
- -
- `; - }, - 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 ` -
- -
嵌套加密
- -
- `; - }, - 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(); - // 如果有需要显示涟漪效果的点,添加它们 - 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], - type: "lines", - zlevel: 1, - label: { - show: false, // 不使用内置标签 - }, - // 飞行线特效 - effect: { - show: true, // 是否显示 - period: 4, // 特效动画时间 - trailLength: 0.7, // 特效尾迹长度。取从 0 到 1 的值,数值越大尾迹越长 - symbol: planePathImg, // 特效图形标记 - symbolSize: [10, 20], - }, - // 线条样式 - lineStyle: { - curveness: -0.4, // 飞线弧度 - type: "solid", // 飞线类型 - 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: 0, // 尾迹线条宽度 - 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 getOption = () => { - const series: echarts.SeriesOption[] = []; - getLianData(series); - getMianLineTipData(series);// 添加主线tip 暂时隐藏 - - const option = { - backgroundColor: "transparent", - // 全局提示框配置 - tooltip: { - show: true, - trigger: "item", - enterable: true, - confine: true, // 保持提示框在图表范围内 - appendToBody: true, // 将提示框附加到body以获得更好的定位 - // position: function(pos:any, params, dom, rect, size) { - position: function (pos: any) { - // 自定义定位逻辑(如果需要) - return [pos[0] + 10, pos[1] - 50]; // 从光标偏移 - }, - }, - // 底图样式 - geo: { - map: "world", // 地图类型 - roam: true, // 是否开启缩放 - zoom: 1, // 初始缩放大小 - // center: [11.3316626, 19.5845024], // 地图中心点 - layoutCenter: ["50%", "50%"], //地图位置 - scaleLimit: { - // 缩放等级 - min: 1, - max: 3, - }, - label: { - show: false, - }, - nameMap: countryNameMap, // 自定义地区的名称映射 - // 三维地理坐标系样式 - itemStyle: { - areaColor: "#020617", // 修改为要求的填充颜色 - borderColor: "#cbd5e1", // 修改为要求的边框颜色 - borderWidth: 1, // 边框宽度 - borderType: "dashed", // 修改为点线边框 - }, - emphasis: { - itemStyle: { - areaColor: "#172554", // 修改为鼠标悬停时的填充颜色 - borderColor: "#0ea5e9", // 修改为鼠标悬停时的边框颜色 - borderWidth: 1.2, // 修改为鼠标悬停时的边框宽度 - borderType: "solid", // 修改为实线边框 - }, - label: false, - }, - tooltip: { - show: true, - trigger: "item", - triggerOn: "click", // 提示框触发的条件 - enterable: true, // 鼠标是否可进入提示框浮层中,默认为false,如需详情内交互,如添加链接,按钮,可设置为 true - backgroundColor: "rgba(0,0,0,0.8)", - borderColor: "rgba(0,0,0,0.2)", - textStyle: { - color: "#fff", - }, - formatter: (parameters: { - name: string; - data: - | { - name: string; - datas: { tradingCountry: string }; - } - | undefined; - }) => { - if (parameters.data?.name) - return parameters.data.name; - return parameters.name; - }, - }, - }, - series: series, - }; - return option; - }; - - // 创建DOM标签 - const createDOMLabels = () => { - // 清除现有标签 - if (labelContainerRef.current) { - labelContainerRef.current.innerHTML = ''; - labelsRef.current = []; - } else { - // 创建标签容器 - const container = document.createElement('div'); - container.className = 'line-labels-container'; - container.style.position = 'absolute'; - container.style.top = '0'; - container.style.left = '0'; - container.style.pointerEvents = 'none'; - container.style.zIndex = '1000'; - container.style.width = '100%'; - container.style.height = '100%'; - container.style.overflow = 'hidden'; - - // 添加到地图容器 - const chartDom = document.getElementById("screenGeo"); - if (chartDom) { - chartDom.style.position = 'relative'; - chartDom.appendChild(container); - labelContainerRef.current = container; - } - } - - // 创建新标签 - lineMidpointsRef.current.forEach((point, index) => { - const label = document.createElement('div'); - label.id = point.id; - label.className = 'line-label'; - label.style.position = 'absolute'; - label.style.backgroundColor = '#8B3700'; - label.style.color = '#FFB27A'; - label.style.padding = '5px 10px'; - label.style.borderRadius = '4px'; - label.style.fontSize = '18px'; - label.style.fontWeight = 'normal'; - label.style.textAlign = 'center'; - label.style.transform = 'translate(-50%, -50%)'; - label.style.whiteSpace = 'nowrap'; - label.style.pointerEvents = 'none'; - label.style.zIndex = '1001'; - label.textContent = 'SS签名'; - - // 添加到容器 - labelContainerRef.current?.appendChild(label); - labelsRef.current.push(label); - }); - - // 更新标签位置 - updateLabelPositions(); - }; - - // 更新标签位置 - const updateLabelPositions = () => { - if (!proxyGeoRef.current || !labelContainerRef.current) return; - - lineMidpointsRef.current.forEach((point, index) => { - const label = labelsRef.current[index]; - if (!label) return; - - const pixelPoint = proxyGeoRef.current?.convertToPixel('geo', point.midpoint); - if (pixelPoint && Array.isArray(pixelPoint)) { - label.style.left = `${pixelPoint[0]}px`; - label.style.top = `${pixelPoint[1]}px`; - } - }); - }; - - const handleResize = () => { - proxyGeoRef.current?.resize(); - updateLabelPositions(); - }; - - useEffect(() => { - preMainToData.current?.some( - (item, index) => - item.country_code !== mainToData[index]?.country_code - ) && proxyGeoRef.current?.clear(); - preMainToData.current = mainToData; - const option = getOption(); - proxyGeoRef.current?.setOption(option); - - // 创建DOM标签 - setTimeout(createDOMLabels, 100); - }, [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); - - // 添加地图交互事件监听器 - proxyGeoRef.current?.on('georoam', updateLabelPositions); - - // 页面resize时触发 - window.addEventListener("resize", handleResize); - return () => { - window.removeEventListener("resize", handleResize); - proxyGeoRef.current?.off('georoam', updateLabelPositions); - - // 清理DOM标签 - if (labelContainerRef.current) { - labelContainerRef.current.remove(); - labelContainerRef.current = null; - labelsRef.current = []; - } - - proxyGeoRef.current?.dispose(); - proxyGeoRef.current = null; - }; - }, []); - - useEffect(() => { - if (tooltipClosed) { - createCustomTooltip(); - } - }, [tooltipClosed, tooltipType]); - - return ( -
-
-
- ); - } -); \ No newline at end of file diff --git a/src/pages/anti-dark-analysis-network/components/world-geo copy.tsx b/src/pages/anti-dark-analysis-network/components/world-geo copy.tsx deleted file mode 100644 index 33e7b69..0000000 --- a/src/pages/anti-dark-analysis-network/components/world-geo copy.tsx +++ /dev/null @@ -1,876 +0,0 @@ -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(null); - const proxyGeoRef = useRef(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 = ` -
- -
-
-
-
${ - currentTooltipType.title - }
- -
- - - -
-
- `; - // 添加到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 ` -
- -
嵌套加密
- -
- `; - }, - 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 ` -
- -
嵌套加密
- -
- `; - }, - 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 ` -
- -
嵌套加密
- -
- `; - }, - 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 ( -
-
-
- ); - } -); diff --git a/src/pages/anti-dark-analysis-network/components/world-geo.tsx b/src/pages/anti-dark-analysis-network/components/world-geo.tsx index 3eb0745..116d7c4 100644 --- a/src/pages/anti-dark-analysis-network/components/world-geo.tsx +++ b/src/pages/anti-dark-analysis-network/components/world-geo.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, memo } from "react"; +import { useEffect, useMemo, useRef, memo, useState } from "react"; import * as echarts from "echarts"; // import 'echarts-gl'; // import { useQueryClient } from "@tanstack/react-query"; @@ -6,9 +6,8 @@ import type { EChartsType } from "echarts"; import worldGeoJson from "@/assets/echarts-map/json/world.json"; import { geoCoordMap, countryNameMap, countryCodeMap } from "@/data"; import { getUrl } from "@/lib/utils"; -import { CONST_TOOLTIP_TYPE } from "@/pages/anti-forensics-forwarding"; -const planePathImg = - "image://data:image/svg+xml;charset=utf-8;base64,PHN2ZyB3aWR0aD0iNjciIGhlaWdodD0iMTAyIiB2aWV3Qm94PSIwIDAgNjcgMTAyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8ZyBmaWx0ZXI9InVybCgjZmlsdGVyMF9mXzYxMTdfMjEyNDA3KSI+CjxwYXRoIGQ9Ik0zNC4yMTA5IDkxLjE4ODZMNTMuNjU3OCA0MC45NThDNTQuOTM4IDM3LjY1MTMgNTUuNzk4MyAzNC4xNTkyIDU1LjM1NjMgMzAuNjQxQzU0LjQzNTcgMjMuMzEyOCA1MC40Njg0IDExLjAyMDggMzQuMjExMiAxMS4wMjA4QzE5LjE5MDMgMTEuMDIwOCAxMy45MTEgMjEuNTE0NiAxMi4wNTU0IDI4Ljg5MTJDMTAuOTAxIDMzLjQ4MDYgMTEuOTkyNiAzOC4yMTg2IDEzLjgyMzEgNDIuNTgyN0wzNC4yMTA5IDkxLjE4ODZaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfNjExN18yMTI0MDcpIi8+CjwvZz4KPGRlZnM+CjxmaWx0ZXIgaWQ9ImZpbHRlcjBfZl82MTE3XzIxMjQwNyIgeD0iMC44OTE3NDQiIHk9IjAuMzMxOTkiIHdpZHRoPSI2NS4yNzA3IiBoZWlnaHQ9IjEwMS41NDUiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj4KPGZlRmxvb2QgZmxvb2Qtb3BhY2l0eT0iMCIgcmVzdWx0PSJCYWNrZ3JvdW5kSW1hZ2VGaXgiLz4KPGZlQmxlbmQgbW9kZT0ibm9ybWFsIiBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0ic2hhcGUiLz4KPGZlR2F1c3NpYW5CbHVyIHN0ZERldmlhdGlvbj0iNS4zNDQ0MSIgcmVzdWx0PSJlZmZlY3QxX2ZvcmVncm91bmRCbHVyXzYxMTdfMjEyNDA3Ii8+CjwvZmlsdGVyPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfNjExN18yMTI0MDciIHgxPSIzNS4yODI2IiB5MT0iMTAuODU2NCIgeDI9IjM1LjI4MjYiIHkyPSI4Ni44NTY0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiMwMEYyRkYiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMTUwMEZGIi8+CjwvbGluZWFyR3JhZGllbnQ+CjwvZGVmcz4KPC9zdmc+Cg=="; + + interface LinesItemType { name: string; country_code: string; @@ -17,6 +16,147 @@ interface LinesItemType { } type LinesDataType = [LinesItemType, LinesItemType]; type LinesType = [string, LinesDataType[]]; + + +// 创建左侧自定义提示框组件 +const CustomTooltipLeft = ({ + logs = [], + onClose, + tooltipRef, + title, +}: { + logs?: string[], + onClose: () => void, + tooltipRef: React.RefObject, + title: string, +}) => { + const [visibleLogs, setVisibleLogs] = useState([]); + const [isComplete, setIsComplete] = useState(false); + + // 过滤掉空日志 + const filteredLogs = useMemo(() => { + return logs.filter(log => log && log.trim() !== ''); + }, [logs]); + + // 使用useEffect实现逐条显示日志的效果 + useEffect(() => { + if (!filteredLogs || filteredLogs.length === 0) return; + + // 重置状态 + setVisibleLogs([]); + setIsComplete(false); + + // 先显示第一条日志 + setVisibleLogs([filteredLogs[0]]); + + // 如果只有一条日志,直接设置完成 + if (filteredLogs.length === 1) { + setIsComplete(true); + return; + } + + // 从第二条日志开始,每500毫秒显示一条 + let currentIndex = 1; + + const timer = setInterval(() => { + if (currentIndex < filteredLogs.length) { + setVisibleLogs(prev => [...prev, filteredLogs[currentIndex]]); + currentIndex++; + + // 如果已经是最后一条,设置完成状态 + if (currentIndex >= filteredLogs.length) { + clearInterval(timer); + setIsComplete(true); + } + } else { + clearInterval(timer); + setIsComplete(true); + } + }, 500); + + // 清理函数 + return () => { + clearInterval(timer); + }; + }, [filteredLogs]); // 当过滤后的日志变化时重新开始动画 + + // 自动滚动到最新的日志 + const logsContainerRef = useRef(null); + + useEffect(() => { + if (logsContainerRef.current && visibleLogs.length > 0) { + logsContainerRef.current.scrollTop = logsContainerRef.current.scrollHeight; + } + }, [visibleLogs]); + + return ( +
+
+
+
+
+
+ {title} +
+ +
+ + {filteredLogs.length > 0 && ( +
+ {visibleLogs.length > 0 ? ( +
    + {visibleLogs.map((log, index) => ( + log && log.trim() !== '' && ( +
  • + {log} +
  • + ) + ))} +
+ ) : ( +
+ 日志加载中... +
+ )} + + {/* {!isComplete && filteredLogs.length > 0 && ( +
+ 处理中... +
+ )} */} +
+ )} +
+ +
+
+ ); +}; // 创建单个国家的涟漪效果 const createCountryRipple = (countryCode: string, color?: string) => { const coords = geoCoordMap[countryCode]; @@ -28,6 +168,7 @@ const createCountryRipple = (countryCode: string, color?: string) => { color: color || "#0ea5e9", // 添加颜色属性,如果没有则使用默认颜色 }; }; + export const WorldGeo = memo( ({ currentValue, @@ -35,11 +176,13 @@ export const WorldGeo = memo( tooltipType, tooltipClosed, setTooltipClosed, + trafficObfuscationLogs, }: { currentValue: any; newHomeProxies: any; tooltipType: string; tooltipClosed: boolean; + trafficObfuscationLogs:any; setTooltipClosed: (value: boolean) => void; }) => { // const queryClient = useQueryClient() @@ -59,6 +202,13 @@ export const WorldGeo = memo( >([]); const labelContainerRef = useRef(null); const labelsRef = useRef([]); + + // 添加状态来控制是否显示tooltip + const [showTooltip1, setShowTooltip1] = useState(false); + const [showTooltip2, setShowTooltip2] = useState(false); + + + const mainToData = useMemo(() => { // 使用新的数据结构 const proxiesList = currentValue ?? []; @@ -99,69 +249,7 @@ export const WorldGeo = memo( }); return data; }, [currentValue]); - // 创建自定义提示框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 = ` -
- -
-
-
-
${ - currentTooltipType.title - }
- -
- - - -
-
- `; - // 添加到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; @@ -184,57 +272,8 @@ export const WorldGeo = memo( console.error("Error positioning tooltip:", error); } }; - // 创建自定义提示框DOM元素 - const createCustomTooltip2 = () => { - // 如果已经存在自定义提示框,则移除它 - if (document.getElementById("custom-fixed-tooltip2")) { - document.getElementById("custom-fixed-tooltip2")?.remove(); - } - // 创建自定义提示框 - const tooltip = document.createElement("div"); - tooltip.id = "custom-fixed-tooltip2"; - tooltip.style.position = "fixed"; - tooltip.style.zIndex = "1000"; - tooltip.style.pointerEvents = "auto"; - tooltip.style.backgroundColor = "transparent"; - tooltip.innerHTML = ` -
-
-
-
-
流量混淆
- -
- - - -
- -
- `; - // 添加到DOM - document.body.appendChild(tooltip); - customTooltip2Ref.current = tooltip; - // 添加关闭按钮事件 - const closeButton = tooltip.querySelector(".close-icon"); - if (closeButton) { - closeButton.addEventListener("click", () => { - setTooltipClosed(false); - tooltip.remove(); - customTooltip2Ref.current = null; - }); - } - // 定位提示框 - positionCustomTooltip2(); - }; - // 定位自定义提示框 - 优化版本 + + // 定位自定义提示框2 - 优化版本 const positionCustomTooltip2 = () => { if (!customTooltip2Ref.current || !proxyGeoRef.current) return; // 找到US点 @@ -250,16 +289,25 @@ export const WorldGeo = memo( ) { // 设置提示框位置 customTooltip2Ref.current.style.left = `${ - screenCoord[0] - 626 + 20 + screenCoord[0] - 626 + 53 }px`; customTooltip2Ref.current.style.top = `${ - screenCoord[1] + 40 - 218 + screenCoord[1] + 40 - 222 }px`; } } catch (error) { console.error("Error positioning tooltip:", error); } }; + + + + // 处理关闭tooltip2 + const handleCloseTooltip2 = () => { + setShowTooltip2(false); + setTooltipClosed(false); + }; + const getLineItem = ( preCode: string, nextCode: string @@ -277,28 +325,25 @@ export const WorldGeo = memo( }, ]; }; + 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(); const color = currentItem.color || "#0ea5e9"; // 获取颜色,如果没有则使用默认颜色 - // 如果当前项是起点,下一项是终点 if (currentItem.type === "start" && nextItem.type === "end") { const startCode = countryCode; const endCode = nextItem.country_code.toUpperCase(); - // 无论是否连线,都添加点的涟漪效果 const startPoint = createCountryRipple(startCode, color); const endPoint = createCountryRipple( @@ -307,7 +352,6 @@ export const WorldGeo = memo( ); if (startPoint) ripplePoints.push(startPoint); if (endPoint) ripplePoints.push(endPoint); - // 检查是否应该绘制连线 if (currentItem.isLine !== false) { const lineItem = getLineItem(startCode, endCode); @@ -316,14 +360,12 @@ export const WorldGeo = memo( lineItem[1].color = nextItem.color || color; solidData[0]?.[1].push(lineItem); } - // 跳过下一项,因为已经处理了 i++; } // 常规情况:当前项到下一项 else { const nextCountryCode = nextItem.country_code.toUpperCase(); - // 无论是否连线,都添加点的涟漪效果 const currentPoint = createCountryRipple(countryCode, color); const nextPoint = createCountryRipple( @@ -332,7 +374,6 @@ export const WorldGeo = memo( ); if (currentPoint) ripplePoints.push(currentPoint); if (nextPoint) ripplePoints.push(nextPoint); - // 检查是否应该绘制连线 if (currentItem.isLine !== false) { const lineItem = getLineItem(countryCode, nextCountryCode); @@ -343,7 +384,6 @@ export const WorldGeo = memo( } } } - // 虚线数据处理(保持原有逻辑) const otherLineList: any = []; return { @@ -352,6 +392,7 @@ export const WorldGeo = memo( ripplePoints, }; }; + // 获取连线经纬度数据 const convertData = (data: LinesDataType[]) => { const res = []; @@ -394,6 +435,7 @@ export const WorldGeo = memo( // lineMidpointsRef.current = midpoints; return res; }; + // 创建双层点效果 - 大点 const createDualLayerPoint = ( lastExit: LinesItemType, @@ -480,6 +522,7 @@ export const WorldGeo = memo( } as echarts.SeriesOption, ]; }; + // 添加新方法:根据经纬度数组创建蓝色涟漪小点(不包含白色内层点) const createRipplePointsFromCoordinates = ( coordinates: [number, number][], @@ -511,6 +554,7 @@ export const WorldGeo = memo( })), } as echarts.SeriesOption); }; + // 创建路径点的双层效果 const createPathPoints = ( dataItems: LinesDataType[], @@ -593,6 +637,7 @@ export const WorldGeo = memo( } as echarts.SeriesOption, ]; }; + // 创建带自定义提示框的涟漪点 const createRipplePointsWithTooltip = (ripplePoints: any) => { return { @@ -652,6 +697,7 @@ export const WorldGeo = memo( })), } as echarts.SeriesOption; }; + // 连线 series const getLianData = (series: echarts.SeriesOption[]) => { const { solidData, otherLineList, ripplePoints } = getLine(); @@ -688,7 +734,6 @@ export const WorldGeo = memo( const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null; // 获取当前路径的颜色 const pathColor = item[1]?.[0]?.[0]?.color || "#0ea5e9"; // 从第一个点获取颜色,如果没有则使用默认颜色 - // 添加飞行线 series.push({ name: item[0], @@ -716,11 +761,9 @@ export const WorldGeo = memo( }, data: convertData(item[1]) as echarts.LinesSeriesOption["data"], }); - // 添加路径点的双层效果 const pathPoints = createPathPoints(item[1], true, pathColor); series.push(...pathPoints); - // 添加出口节点的双层效果 if (lastExit) { const exitNodes = createDualLayerPoint(lastExit, true, pathColor); @@ -732,7 +775,6 @@ export const WorldGeo = memo( const lastExit = item[1]?.[item[1].length - 1]?.[1] ?? null; // 获取当前路径的颜色 const pathColor = item[1]?.[0]?.[0]?.color || "#F0FFA2"; // 从第一个点获取颜色,如果没有则使用默认颜色 - // 添加虚线 series.push({ name: item[0], @@ -906,6 +948,7 @@ export const WorldGeo = memo( }); return series; }; + const getOption = () => { const series: echarts.SeriesOption[] = []; getLianData(series); @@ -917,14 +960,12 @@ export const WorldGeo = memo( currentValue[0]?.authenticationPoint ) { console.log(currentValue, "values"); - createSpecialPoints(series); // 添加特殊点和飞线 createRipplePointsFromCoordinates( currentValue[0]?.authenticationPoint || [], series ); } - const option = { backgroundColor: "transparent", // 全局提示框配置 @@ -1000,6 +1041,7 @@ export const WorldGeo = memo( }; return option; }; + // 创建DOM标签 const createDOMLabels = () => { // 清除现有标签 @@ -1065,6 +1107,7 @@ export const WorldGeo = memo( // 更新标签位置 updateLabelPositions(); }; + // 更新标签位置 const updateLabelPositions = () => { if (!proxyGeoRef.current || !labelContainerRef.current) return; @@ -1081,10 +1124,20 @@ export const WorldGeo = memo( } }); }; + const handleResize = () => { proxyGeoRef.current?.resize(); updateLabelPositions(); + + // 重新定位tooltip + if (showTooltip1) { + positionCustomTooltip(); + } + if (showTooltip2) { + positionCustomTooltip2(); + } }; + useEffect(() => { preMainToData.current?.some( (item, index) => item.country_code !== mainToData[index]?.country_code @@ -1095,6 +1148,7 @@ export const WorldGeo = memo( // 创建DOM标签 setTimeout(createDOMLabels, 100); }, [newHomeProxies, mainToData]); + useEffect(() => { const chartDom = document.getElementById("screenGeo"); proxyGeoRef.current = echarts.init(chartDom); @@ -1121,34 +1175,68 @@ export const WorldGeo = memo( proxyGeoRef.current = null; }; }, []); + + // 处理tooltip的显示和隐藏 useEffect(() => { if (tooltipType !== "PASS_AUTHENTICATION") { lineMidpointsRef.current = []; } + if (tooltipClosed) { if (tooltipType === "NESTED_ENCRYPTION") { - createCustomTooltip(); + setShowTooltip1(true); + // 在下一个渲染周期后定位tooltip + setTimeout(() => { + positionCustomTooltip(); + }, 0); } if (tooltipType === "TRAFFIC_OBFUSCATION") { - createCustomTooltip2(); + setShowTooltip2(true); + // 在下一个渲染周期后定位tooltip + setTimeout(() => { + positionCustomTooltip2(); + }, 0); } } else { - customTooltipRef.current?.remove(); - customTooltip2Ref.current?.remove(); - customTooltipRef.current = null; - customTooltip2Ref.current = null; + setShowTooltip1(false); + setShowTooltip2(false); } - return () => { - customTooltipRef.current?.remove(); - customTooltip2Ref.current?.remove(); - customTooltipRef.current = null; - customTooltip2Ref.current = null; - }; }, [tooltipClosed, tooltipType, currentValue]); + + // 在地图初始化后定位tooltip + useEffect(() => { + if (showTooltip1) { + positionCustomTooltip(); + } + if (showTooltip2) { + positionCustomTooltip2(); + } + }, [showTooltip1, showTooltip2]); return (
+ + {/* 流量混淆提示框 */} + {showTooltip2 && ( + + )}
); } ); + +// 添加CSS样式 +// 可以放在你的全局CSS文件中 +// @keyframes fadeIn { +// from { opacity: 0; transform: translateY(5px); } +// to { opacity: 1; transform: translateY(0); } +// } +// +// .animate-fadeIn { +// animation: fadeIn 0.3s ease-out forwards; +// } \ No newline at end of file diff --git a/src/pages/anti-dark-analysis-network/index.scss b/src/pages/anti-dark-analysis-network/index.scss index ad5969c..1f1fe1a 100644 --- a/src/pages/anti-dark-analysis-network/index.scss +++ b/src/pages/anti-dark-analysis-network/index.scss @@ -81,11 +81,10 @@ font-weight: 500; // line-height: 24px; } - -.tip-box { +.tip-box-left{ position: relative; - width: 626px; - height: 281px; + width: 600px; + height: 400px; padding: 20.85px 20.353px; background: rgba(0, 11.82, 33.10, 0.10); border-radius: 8px; @@ -93,7 +92,30 @@ outline-offset: -0.46px; backdrop-filter: blur(5.50px); - .close-icon , .close-icon2 { +} +.line-img-left { + width: 216.86px; + // margin-top: 30px; + right: -216.86px; + top: 60px; + position: absolute; +} +.tip-box-hx { + position: relative; + width: 600px; + height: 400px; + margin-left: 312.221px; + // min-height: 200px; + // max-height: 600px; + padding: 20.85px 20.353px; + background: rgba(0, 11.82, 33.10, 0.10); + border-radius: 8px; + outline: 0.46px solid white; + outline-offset: -0.46px; + backdrop-filter: blur(5.50px); + + .close-icon, + .close-icon2 { width: 16px; height: 16px; position: absolute; @@ -116,7 +138,7 @@ margin-left: 16px; } - .traffic-obfuscation-img{ + .traffic-obfuscation-img { width: 597px; height: 241px; margin-left: 16px; @@ -127,85 +149,13 @@ position: relative; display: flex; - .line-img { + .line-img-hx { width: 312.221px; - } - - .line-img-left{ - width: 216.86px; - margin-top: 30px; - } - - .fill { - width: 9.165px; - height: 9.165px; - border-radius: 50%; - background-color: #18E4FF; + // margin-top: 80px; + top: 80px; + left: 0px; position: absolute; - left: 307.5px; - top: 77.5px; - z-index: 99; } - .fill-left { - width: 9.165px; - height: 9.165px; - border-radius: 50%; - background-color: #18E4FF; - position: absolute; - right:210.5px; - top: 82.5px; - z-index: 99; - } + } - - -// // 轮播项目 -// .carousel-item { -// flex: 0 0 auto; -// } - -// // View Transitions 自定义样式 -// @keyframes slide-from-right { -// from { -// transform: translateX(40px); -// opacity: 0; -// } -// } - -// @keyframes slide-to-left { -// to { -// transform: translateX(-40px); -// opacity: 0; -// } -// } - -// @keyframes slide-from-left { -// from { -// transform: translateX(-40px); -// opacity: 0; -// } -// } - -// @keyframes slide-to-right { -// to { -// transform: translateX(40px); -// opacity: 0; -// } -// } - -// // 自定义 View Transitions 动画 -// ::view-transition-old(web3-item-1-4), -// ::view-transition-old(web3-item-2-4) { -// animation: 0.8s slide-to-left ease-in-out; -// } - -// ::view-transition-new(web3-item-1-0), -// ::view-transition-new(web3-item-2-0) { -// animation: 0.8s slide-from-left ease-in-out; -// } - -// // 确保过渡期间元素可见 -// ::view-transition-group(*) { -// animation-duration: 0.8s; -// } \ No newline at end of file diff --git a/src/pages/anti-dark-analysis-network/index.tsx b/src/pages/anti-dark-analysis-network/index.tsx index 8290737..3fd99c8 100644 --- a/src/pages/anti-dark-analysis-network/index.tsx +++ b/src/pages/anti-dark-analysis-network/index.tsx @@ -30,8 +30,6 @@ import FacebookSvg from "@/assets/svg/anti-forensics-forwarding/Facebook.svg?rea import FacebookActiveSvg from "@/assets/svg/anti-forensics-forwarding/FacebookActive.svg?react"; import { RootState } from "@/store"; - - import "./index.scss"; import { getApplicationDiversion, @@ -172,10 +170,17 @@ const AntiDarkAnalysisNetwork = () => { const [selectedApp, setSelectedApp] = useState(null); const [dataInfo, setDataInfo] = useState(null); - + const [trafficObfuscationLogs, setTrafficObfuscationLogs] = useState([ + "初始化嵌套加密...", + "生成密钥对222...", + "应用第一层加密...", + "应用第二层加密...", + "应用第三层加密...", + "加密完成,准备传输...", + ]); const currentValue = useMemo(() => { let value = dataInfo; - + switch (tooltipType) { case CONST_TOOLTIP_TYPE.APP_DIVERSION.type: value = selectedApp ? [selectedApp] : []; @@ -184,7 +189,7 @@ const AntiDarkAnalysisNetwork = () => { break; } return value; - }, [tooltipType, selectedApp,dataInfo]); + }, [tooltipType, selectedApp, dataInfo]); const handleClickApp = (item: any) => { setSelectedApp(item); @@ -201,6 +206,7 @@ const AntiDarkAnalysisNetwork = () => { case CONST_TOOLTIP_TYPE.TRAFFIC_OBFUSCATION.type: const trafficObfuscation = await getTrafficObfuscation(); value = [trafficObfuscation.data]; + setTrafficObfuscationLogs(trafficObfuscation.logs); break; case CONST_TOOLTIP_TYPE.DYNAMIC_ROUTE_GENERATOR.type: const dynamicRouteGeneration = await getDynamicRouteGeneration(); @@ -219,7 +225,6 @@ const AntiDarkAnalysisNetwork = () => { default: break; } - console.log(value,'valuevalue') setDataInfo(value); }; @@ -227,7 +232,7 @@ const AntiDarkAnalysisNetwork = () => { const appDiversion = useMemo(() => { return Apps.map((item) => { const findApp = appData.find( - (appItem:any) => item.name === appItem.name + (appItem: any) => item.name === appItem.name ); return { ...item, @@ -245,7 +250,7 @@ const AntiDarkAnalysisNetwork = () => { useEffect(() => { getDataInfo(); - },[tooltipType]) + }, [tooltipType]); useEffect(() => { initData(); @@ -276,6 +281,7 @@ const AntiDarkAnalysisNetwork = () => {
void; + tooltipRef: React.RefObject; +}) => { + const [visibleLogs, setVisibleLogs] = useState([]); + 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) { + setVisibleLogs((prev) => [...prev, logs[currentIndex]]); + currentIndex++; + } else { + clearInterval(timer); + setIsComplete(true); + } + }, 500); + + // 清理函数 + return () => { + clearInterval(timer); + }; + }, [logs]); // 当logs变化时重新开始动画 + + // 自动滚动到最新的日志 + const logsContainerRef = useRef(null); + + useEffect(() => { + if (logsContainerRef.current && visibleLogs.length > 0) { + logsContainerRef.current.scrollTop = + logsContainerRef.current.scrollHeight; + } + }, [visibleLogs]); + + return ( +
+
+ +
+
+
+
+ 嵌套加密 +
+ +
+ +
+ {visibleLogs.length > 0 ? ( +
    + {visibleLogs.map((log, index) => ( +
  • + {log} +
  • + ))} +
+ ) : ( +
+ {logs.length > 0 ? "日志加载中..." : "暂无日志记录"} +
+ )} + + {/* {!isComplete && logs.length > 0 && ( +
+ 处理中... +
+ )} */} +
+
+
+
+ ); +}; + +// 添加一个淡入动画的CSS(可以放在你的全局CSS文件中) +// @keyframes fadeIn { +// from { opacity: 0; transform: translateY(5px); } +// to { opacity: 1; transform: translateY(0); } +// } +// +// .animate-fadeIn { +// animation: fadeIn 0.3s ease-out forwards; +// } + // 创建单个国家的涟漪效果 const createCountryRipple = (countryCode: string, color?: string) => { const coords = geoCoordMap[countryCode]; @@ -27,13 +157,17 @@ const createCountryRipple = (countryCode: string, color?: string) => { color: color || "#0ea5e9", // 添加颜色属性,如果没有则使用默认颜色 }; }; + export const WorldGeo = memo( ({ nestedEncryption, passAuthentication, dynamicRouteGeneration, + tooltipClosed, setTooltipClosed, + logs, }: { + logs:any[]; nestedEncryption: any; passAuthentication: any; dynamicRouteGeneration: any; @@ -56,33 +190,38 @@ export const WorldGeo = memo( >([]); const labelContainerRef = useRef(null); const labelsRef = useRef([]); - + // 添加状态来跟踪当前显示的连线索引 - const [nestedEncryptionLineIndex, setNestedEncryptionLineIndex] = useState(-1); + 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 [nestedEncryptionLines, setNestedEncryptionLines] = useState< + { from: string; to: string; color?: string }[] + >([]); + const [dynamicRouteLines, setDynamicRouteLines] = useState< + { from: string; to: string; color?: string }[] + >([]); + // 添加状态来存储所有点 const [allPoints, setAllPoints] = useState([]); - + // 使用ref来跟踪动画状态,避免重新渲染 const animationTimerRef = useRef(null); const dynamicAnimationTimerRef = useRef(null); - + // 添加状态来跟踪数据是否已经变化 const nestedEncryptionKeyRef = useRef(""); const dynamicRouteKeyRef = useRef(""); - + // 初始化时提取所有点的函数 const extractAllPoints = () => { const points: any[] = []; - + // console.log("Extracting points from nestedEncryption:", nestedEncryption); // console.log("Extracting points from dynamicRouteGeneration:", dynamicRouteGeneration); - + // 从嵌套加密数据中提取点 if (nestedEncryption && Array.isArray(nestedEncryption)) { nestedEncryption.forEach((item: any) => { @@ -91,15 +230,18 @@ export const WorldGeo = memo( // 添加起点到点集合 const fromCode = dataItem.country_code.toUpperCase(); const fromPoint = createCountryRipple(fromCode, item.color); - if (fromPoint && !points.some(p => p.country_code === fromCode)) { + 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)) { + if (toPoint && !points.some((p) => p.country_code === toCode)) { points.push(toPoint); } } @@ -107,7 +249,7 @@ export const WorldGeo = memo( } }); } - + // 从动态路由数据中提取点 if (dynamicRouteGeneration && Array.isArray(dynamicRouteGeneration)) { dynamicRouteGeneration.forEach((item: any) => { @@ -116,15 +258,18 @@ export const WorldGeo = memo( // 添加起点到点集合 const fromCode = dataItem.country_code.toUpperCase(); const fromPoint = createCountryRipple(fromCode, item.color); - if (fromPoint && !points.some(p => p.country_code === fromCode)) { + 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)) { + if (toPoint && !points.some((p) => p.country_code === toCode)) { points.push(toPoint); } } @@ -132,11 +277,11 @@ export const WorldGeo = memo( } }); } - + console.log("Extracted points:", points); return points; }; - + // 修改初始化逻辑,确保在数据变化时立即提取点 useEffect(() => { // 提取所有点 @@ -145,28 +290,33 @@ export const WorldGeo = memo( setAllPoints(points); } }, [nestedEncryption, dynamicRouteGeneration]); // 监听数据变化 - + // 启动嵌套加密连线动画的函数 - const startNestedEncryptionAnimation = (connections: {from: string, to: string, color?: string}[]) => { + 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); + animationTimerRef.current = setTimeout( + animateNextLine, + LINE_ANIMATION_INTERVAL + ); } }; - + // 开始动画 animateNextLine(); }; - + // 处理嵌套加密数据变化 useEffect(() => { // 清除任何现有的动画定时器 @@ -174,45 +324,52 @@ export const WorldGeo = memo( 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}[] = []; + 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)) { + if ( + fromPoint && + !points.some((p) => p.country_code === fromCode) + ) { points.push(fromPoint); - if (!allExtractedPoints.some(p => p.country_code === fromCode)) { + 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)) { + if (toPoint && !points.some((p) => p.country_code === toCode)) { points.push(toPoint); - if (!allExtractedPoints.some(p => p.country_code === toCode)) { + if ( + !allExtractedPoints.some((p) => p.country_code === toCode) + ) { allExtractedPoints.push(toPoint); } } - + // 检查是否需要开始连线动画 if (item.isLine === true) { connections.push({ from: fromCode, to: toCode, - color: item.color + color: item.color, }); shouldStartAnimation = true; } @@ -220,19 +377,22 @@ export const WorldGeo = memo( }); } }); - + // 生成当前数据的唯一键 const currentKey = JSON.stringify(nestedEncryption); - + // 检查数据是否变化 - if (currentKey !== nestedEncryptionKeyRef.current || shouldStartAnimation) { + if ( + currentKey !== nestedEncryptionKeyRef.current || + shouldStartAnimation + ) { nestedEncryptionKeyRef.current = currentKey; setNestedEncryptionLines(connections); - + // 如果有连线数据且需要开始动画,重置索引并启动动画 if (connections.length > 0 && shouldStartAnimation) { setNestedEncryptionLineIndex(-1); // 重置索引 - + // 启动连线动画 setTimeout(() => { startNestedEncryptionAnimation(connections); @@ -243,17 +403,19 @@ export const WorldGeo = memo( } } } - + // 更新所有点 if (allExtractedPoints.length > 0) { - setAllPoints(prevPoints => { + setAllPoints((prevPoints) => { const newPoints = [...prevPoints]; - allExtractedPoints.forEach(point => { - if (!newPoints.some(p => p.country_code === point.country_code)) { + 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); + const existingIndex = newPoints.findIndex( + (p) => p.country_code === point.country_code + ); if (existingIndex !== -1 && point.color) { newPoints[existingIndex].color = point.color; } @@ -263,7 +425,7 @@ export const WorldGeo = memo( }); } }, [nestedEncryption]); - + // 处理动态路由数据变化 useEffect(() => { // 清除任何现有的动画定时器 @@ -271,45 +433,52 @@ export const WorldGeo = memo( 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}[] = []; + 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)) { + if ( + fromPoint && + !points.some((p) => p.country_code === fromCode) + ) { points.push(fromPoint); - if (!allExtractedPoints.some(p => p.country_code === fromCode)) { + 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)) { + if (toPoint && !points.some((p) => p.country_code === toCode)) { points.push(toPoint); - if (!allExtractedPoints.some(p => p.country_code === toCode)) { + if ( + !allExtractedPoints.some((p) => p.country_code === toCode) + ) { allExtractedPoints.push(toPoint); } } - + // 检查是否需要开始连线动画 if (item.isLine === true) { connections.push({ from: fromCode, to: toCode, - color: item.color + color: item.color, }); shouldStartAnimation = true; } @@ -317,19 +486,19 @@ export const WorldGeo = memo( }); } }); - + // 生成当前数据的唯一键 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); @@ -340,17 +509,19 @@ export const WorldGeo = memo( } } } - + // 更新所有点 if (allExtractedPoints.length > 0) { - setAllPoints(prevPoints => { + setAllPoints((prevPoints) => { const newPoints = [...prevPoints]; - allExtractedPoints.forEach(point => { - if (!newPoints.some(p => p.country_code === point.country_code)) { + 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); + const existingIndex = newPoints.findIndex( + (p) => p.country_code === point.country_code + ); if (existingIndex !== -1 && point.color) { newPoints[existingIndex].color = point.color; } @@ -360,28 +531,33 @@ export const WorldGeo = memo( }); } }, [dynamicRouteGeneration]); - + // 启动动态路由连线动画的函数 - const startDynamicRouteAnimation = (connections: {from: string, to: string, color?: string}[]) => { + 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); + dynamicAnimationTimerRef.current = setTimeout( + animateNextLine, + LINE_ANIMATION_INTERVAL + ); } }; - + // 开始动画 animateNextLine(); }; - + // 组件卸载时清除定时器 useEffect(() => { return () => { @@ -395,7 +571,7 @@ export const WorldGeo = memo( } }; }, []); - + const getLineItem = ( preCode: string, nextCode: string, @@ -416,95 +592,57 @@ export const WorldGeo = memo( }, ]; }; - + const getLine = () => { // 实现数据处理 const solidData: LinesType[] = []; // 不再使用单一数组,而是分开存储 - + // 处理嵌套加密连线 - 放入单独的数组 if (nestedEncryptionLineIndex >= 0 && nestedEncryptionLines.length > 0) { const nestedLines: LinesDataType[] = []; - for (let i = 0; i <= nestedEncryptionLineIndex && i < nestedEncryptionLines.length; i++) { + for ( + let i = 0; + i <= nestedEncryptionLineIndex && i < nestedEncryptionLines.length; + i++ + ) { const connection = nestedEncryptionLines[i]; - nestedLines.push(getLineItem(connection.from, connection.to, connection.color)); + 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++) { + for ( + let i = 0; + i <= dynamicRouteLineIndex && i < dynamicRouteLines.length; + i++ + ) { const connection = dynamicRouteLines[i]; - dynamicLines.push(getLineItem(connection.from, connection.to, connection.color)); + 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 确保点始终显示 }; }; - - // 创建自定义提示框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"; - tooltip.innerHTML = ` -
- -
-
-
-
嵌套加密
- -
- - - -
-
- `; - // 添加到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; @@ -527,7 +665,13 @@ export const WorldGeo = memo( console.error("Error positioning tooltip:", error); } }; - + + // 处理关闭tooltip + const handleCloseTooltip = () => { + setTooltipClosed(false); + setTooltipClosed(false); + }; + // 获取连线经纬度数据 const convertData = (data: LinesDataType[]) => { const res = []; @@ -570,7 +714,7 @@ export const WorldGeo = memo( // lineMidpointsRef.current = midpoints; return res; }; - + // 创建双层点效果 - 大点 const createDualLayerPoint = ( lastExit: LinesItemType, @@ -657,7 +801,7 @@ export const WorldGeo = memo( } as echarts.SeriesOption, ]; }; - + // 添加新方法:根据经纬度数组创建蓝色涟漪小点(不包含白色内层点) const createRipplePointsFromCoordinates = ( coordinates: [number, number][], @@ -689,7 +833,7 @@ export const WorldGeo = memo( })), } as echarts.SeriesOption); }; - + // 创建路径点的双层效果 const createPathPoints = ( dataItems: LinesDataType[], @@ -772,7 +916,7 @@ export const WorldGeo = memo( } as echarts.SeriesOption, ]; }; - + // 创建带自定义提示框的涟漪点 const createRipplePointsWithTooltip = (ripplePoints: any) => { return { @@ -832,11 +976,11 @@ export const WorldGeo = memo( })), } as echarts.SeriesOption; }; - + // 连线 series const getLianData = (series: echarts.SeriesOption[]) => { const { solidData, otherLineList, ripplePoints } = getLine(); - + // 如果有需要显示涟漪效果的点,添加它们 if (ripplePoints.length > 0) { // 添加带自定义提示框的外层蓝色点 @@ -862,17 +1006,17 @@ export const WorldGeo = memo( })), } as echarts.SeriesOption); } - + // 处理每个连线组 solidData.forEach((item) => { // 如果没有连线数据,则跳过 if (item[1].length === 0) { return; } - + // 为每条连线创建飞行线 const pathColor = item[0] === "nested" ? "#0ea5e9" : "#F0FFA2"; // 根据类型设置默认颜色 - + // 添加飞行线 series.push({ name: item[0], @@ -899,21 +1043,25 @@ export const WorldGeo = memo( }, data: convertData(item[1]) as echarts.LinesSeriesOption["data"], }); - + // 添加路径点的双层效果 const pathPoints = createPathPoints(item[1], true, pathColor); series.push(...pathPoints); - + // 添加出口节点的双层效果 - item[1].forEach(lineData => { + item[1].forEach((lineData) => { const lastExit = lineData[1]; if (lastExit) { - const exitNodes = createDualLayerPoint(lastExit, true, lastExit.color || pathColor); + const exitNodes = createDualLayerPoint( + lastExit, + true, + lastExit.color || pathColor + ); series.push(...exitNodes); } }); }); - + // 处理其他线(保持原有逻辑) otherLineList.forEach((line: any) => { line.forEach((item: any) => { @@ -948,10 +1096,10 @@ export const WorldGeo = memo( } }); }); - + return true; }; - + // 创建A点和B点,并添加飞线和标签 const createSpecialPoints = (series: echarts.SeriesOption[]) => { // 定义点A和点B的坐标 @@ -1094,7 +1242,7 @@ export const WorldGeo = memo( }); return series; }; - + const getOption = () => { const series: echarts.SeriesOption[] = []; getLianData(series); @@ -1184,7 +1332,7 @@ export const WorldGeo = memo( }; return option; }; - + // 创建DOM标签 const createDOMLabels = () => { // 清除现有标签 @@ -1250,7 +1398,7 @@ export const WorldGeo = memo( // 更新标签位置 updateLabelPositions(); }; - + // 更新标签位置 const updateLabelPositions = () => { if (!proxyGeoRef.current || !labelContainerRef.current) return; @@ -1267,19 +1415,21 @@ export const WorldGeo = memo( } }); }; - + const handleResize = () => { proxyGeoRef.current?.resize(); updateLabelPositions(); - positionCustomTooltip(); + if (tooltipClosed) { + positionCustomTooltip(); + } }; - + // 更新图表 useEffect(() => { const option = getOption(); proxyGeoRef.current?.setOption(option); }, [nestedEncryptionLineIndex, dynamicRouteLineIndex, allPoints]); // 当连线索引或点变化时更新图表 - + useEffect(() => { lineMidpointsRef.current = []; // 重置中点数据 const option = getOption(); @@ -1287,7 +1437,7 @@ export const WorldGeo = memo( // 创建DOM标签 setTimeout(createDOMLabels, 100); }, [nestedEncryption, dynamicRouteGeneration, passAuthentication]); - + useEffect(() => { const chartDom = document.getElementById("screenGeo"); proxyGeoRef.current = echarts.init(chartDom); @@ -1295,13 +1445,13 @@ export const WorldGeo = memo( "world", worldGeoJson as unknown as Parameters[1] ); - + // 初始化时提取所有点 const initialPoints = extractAllPoints(); if (initialPoints.length > 0) { setAllPoints(initialPoints); } - + const option = getOption(); option && proxyGeoRef.current?.setOption(option); // 添加地图交互事件监听器 @@ -1321,19 +1471,27 @@ export const WorldGeo = memo( proxyGeoRef.current = null; }; }, []); - + + // 在地图初始化后定位tooltip useEffect(() => { - createCustomTooltip(); - return () => { - customTooltipRef.current?.remove(); - customTooltipRef.current = null; - }; - }, []); - + if (tooltipClosed) { + positionCustomTooltip(); + } + }, [tooltipClosed, nestedEncryption]); + return (
+ {tooltipClosed && ( + + )}
); } -); \ No newline at end of file +); diff --git a/src/pages/anti-forensics-forwarding/index.scss b/src/pages/anti-forensics-forwarding/index.scss index ad5969c..dcb5059 100644 --- a/src/pages/anti-forensics-forwarding/index.scss +++ b/src/pages/anti-forensics-forwarding/index.scss @@ -81,11 +81,24 @@ font-weight: 500; // line-height: 24px; } - -.tip-box { +.tip-box-left{ position: relative; - width: 626px; - height: 281px; + width: 600px; + height: 400px; + padding: 20.85px 20.353px; + background: rgba(0, 11.82, 33.10, 0.10); + border-radius: 8px; + outline: 0.46px solid white; + outline-offset: -0.46px; + backdrop-filter: blur(5.50px); +} +.tip-box-hx,tip-box-left { + position: relative; + width: 600px; + height: 400px; + margin-left: 312.221px; + // min-height: 200px; + // max-height: 600px; padding: 20.85px 20.353px; background: rgba(0, 11.82, 33.10, 0.10); border-radius: 8px; @@ -93,7 +106,8 @@ outline-offset: -0.46px; backdrop-filter: blur(5.50px); - .close-icon , .close-icon2 { + .close-icon, + .close-icon2 { width: 16px; height: 16px; position: absolute; @@ -116,7 +130,7 @@ margin-left: 16px; } - .traffic-obfuscation-img{ + .traffic-obfuscation-img { width: 597px; height: 241px; margin-left: 16px; @@ -127,85 +141,18 @@ position: relative; display: flex; - .line-img { + .line-img-hx { width: 312.221px; + // margin-top: 80px; + top: 80px; + left: 0px; + position: absolute; } - .line-img-left{ + .line-img-left { width: 216.86px; margin-top: 30px; } - .fill { - width: 9.165px; - height: 9.165px; - border-radius: 50%; - background-color: #18E4FF; - position: absolute; - left: 307.5px; - top: 77.5px; - z-index: 99; - } - - .fill-left { - width: 9.165px; - height: 9.165px; - border-radius: 50%; - background-color: #18E4FF; - position: absolute; - right:210.5px; - top: 82.5px; - z-index: 99; - } + } - - -// // 轮播项目 -// .carousel-item { -// flex: 0 0 auto; -// } - -// // View Transitions 自定义样式 -// @keyframes slide-from-right { -// from { -// transform: translateX(40px); -// opacity: 0; -// } -// } - -// @keyframes slide-to-left { -// to { -// transform: translateX(-40px); -// opacity: 0; -// } -// } - -// @keyframes slide-from-left { -// from { -// transform: translateX(-40px); -// opacity: 0; -// } -// } - -// @keyframes slide-to-right { -// to { -// transform: translateX(40px); -// opacity: 0; -// } -// } - -// // 自定义 View Transitions 动画 -// ::view-transition-old(web3-item-1-4), -// ::view-transition-old(web3-item-2-4) { -// animation: 0.8s slide-to-left ease-in-out; -// } - -// ::view-transition-new(web3-item-1-0), -// ::view-transition-new(web3-item-2-0) { -// animation: 0.8s slide-from-left ease-in-out; -// } - -// // 确保过渡期间元素可见 -// ::view-transition-group(*) { -// animation-duration: 0.8s; -// } \ No newline at end of file diff --git a/src/pages/anti-forensics-forwarding/index.tsx b/src/pages/anti-forensics-forwarding/index.tsx index ef7d510..8c0a3b9 100644 --- a/src/pages/anti-forensics-forwarding/index.tsx +++ b/src/pages/anti-forensics-forwarding/index.tsx @@ -178,13 +178,7 @@ const DecentralizedElasticNetwork = () => { let istrue = useRef(false); const [nestedEncryption, setNestedEncryption] = useState([]); const [dynamicRouteGeneration, setDynamicRouteGeneration] = useState([]); - // const [dataInfo, setDataInfo] = useState({ - // passAuthentication: { - // ...PASS_AUTHENTICATION, - // }, - // nestedEncryption: [NESTED_ENCRYPTION], - // dynamicRouteGeneration: DYNAMIC_ROUTE_GENERATOR, - // }); + const [logs, setLogs] = useState([]); const initData = async () => { try { @@ -194,6 +188,7 @@ const DecentralizedElasticNetwork = () => { nestedEncryption.data.isLine = false; dynamicRouteGeneration.data[0].isLine = false; setNestedEncryption([nestedEncryption.data]); + setLogs(nestedEncryption.logs); setDynamicRouteGeneration(dynamicRouteGeneration.data); setDataInfo({ nestedEncryption: [nestedEncryption.data], @@ -231,6 +226,7 @@ const DecentralizedElasticNetwork = () => {
void; + tooltipRef: React.RefObject; + title: string; + imageSrc: string; +}) => { + const [visibleLogs, setVisibleLogs] = useState([]); + const [isComplete, setIsComplete] = useState(false); + + // 过滤掉空日志 + const filteredLogs = useMemo(() => { + return logs.filter((log) => log && log.trim() !== ""); + }, [logs]); + + // 使用useEffect实现逐条显示日志的效果 + useEffect(() => { + if (!filteredLogs || filteredLogs.length === 0) return; + + // 重置状态 + setVisibleLogs([]); + setIsComplete(false); + + // 先显示第一条日志 + setVisibleLogs([filteredLogs[0]]); + + // 如果只有一条日志,直接设置完成 + if (filteredLogs.length === 1) { + setIsComplete(true); + return; + } + + // 从第二条日志开始,每500毫秒显示一条 + let currentIndex = 1; + + const timer = setInterval(() => { + if (currentIndex < filteredLogs.length) { + setVisibleLogs((prev) => [...prev, filteredLogs[currentIndex]]); + currentIndex++; + + // 如果已经是最后一条,设置完成状态 + if (currentIndex >= filteredLogs.length) { + clearInterval(timer); + setIsComplete(true); + } + } else { + clearInterval(timer); + setIsComplete(true); + } + }, 500); + + // 清理函数 + return () => { + clearInterval(timer); + }; + }, [filteredLogs]); // 当过滤后的日志变化时重新开始动画 + + // 自动滚动到最新的日志 + const logsContainerRef = useRef(null); + + useEffect(() => { + if (logsContainerRef.current && visibleLogs.length > 0) { + logsContainerRef.current.scrollTop = + logsContainerRef.current.scrollHeight; + } + }, [visibleLogs]); + + // 添加调试日志 + useEffect(() => { + console.log("CustomTooltip rendered", { title, logs: filteredLogs.length }); + }, []); + + return ( +
+
+ +
+
+
+
+ {title} +
+ +
+ + {filteredLogs.length > 0 && ( +
+ {visibleLogs.length > 0 ? ( +
    + {visibleLogs.map( + (log, index) => + log && + log.trim() !== "" && ( +
  • + {log} +
  • + ) + )} +
+ ) : ( +
+ 日志加载中... +
+ )} + + {!isComplete && filteredLogs.length > 0 && ( +
+ 处理中... +
+ )} +
+ )} +
+
+
+ ); +}; + +// 创建左侧自定义提示框组件 +const CustomTooltipLeft = ({ + logs = [], + onClose, + tooltipRef, + title, + imageSrc, +}: { + logs?: string[]; + onClose: () => void; + tooltipRef: React.RefObject; + title: string; + imageSrc: string; +}) => { + const [visibleLogs, setVisibleLogs] = useState([]); + const [isComplete, setIsComplete] = useState(false); + + // 过滤掉空日志 + const filteredLogs = useMemo(() => { + return logs.filter((log) => log && log.trim() !== ""); + }, [logs]); + + // 使用useEffect实现逐条显示日志的效果 + useEffect(() => { + if (!filteredLogs || filteredLogs.length === 0) return; + + // 重置状态 + setVisibleLogs([]); + setIsComplete(false); + + // 先显示第一条日志 + setVisibleLogs([filteredLogs[0]]); + + // 如果只有一条日志,直接设置完成 + if (filteredLogs.length === 1) { + setIsComplete(true); + return; + } + + // 从第二条日志开始,每500毫秒显示一条 + let currentIndex = 1; + + const timer = setInterval(() => { + if (currentIndex < filteredLogs.length) { + setVisibleLogs((prev) => [...prev, filteredLogs[currentIndex]]); + currentIndex++; + + // 如果已经是最后一条,设置完成状态 + if (currentIndex >= filteredLogs.length) { + clearInterval(timer); + setIsComplete(true); + } + } else { + clearInterval(timer); + setIsComplete(true); + } + }, 500); + + // 清理函数 + return () => { + clearInterval(timer); + }; + }, [filteredLogs]); // 当过滤后的日志变化时重新开始动画 + + // 自动滚动到最新的日志 + const logsContainerRef = useRef(null); + + useEffect(() => { + if (logsContainerRef.current && visibleLogs.length > 0) { + logsContainerRef.current.scrollTop = + logsContainerRef.current.scrollHeight; + } + }, [visibleLogs]); + + // 添加调试日志 + useEffect(() => { + console.log("CustomTooltipLeft rendered", { + title, + logs: filteredLogs.length, + }); + }, []); + + return ( +
+
+
+
+
+
+ {title} +
+ +
+ + {filteredLogs.length > 0 && ( +
+ {visibleLogs.length > 0 ? ( +
    + {visibleLogs.map( + (log, index) => + log && + log.trim() !== "" && ( +
  • + {log} +
  • + ) + )} +
+ ) : ( +
+ 日志加载中... +
+ )} + + {!isComplete && filteredLogs.length > 0 && ( +
+ 处理中... +
+ )} +
+ )} +
+ +
+
+ ); +}; + // 创建单个国家的涟漪效果 const createCountryRipple = (countryCode: string, color?: string) => { const coords = geoCoordMap[countryCode]; @@ -28,6 +337,7 @@ const createCountryRipple = (countryCode: string, color?: string) => { color: color, // 添加颜色属性 }; }; + export const WorldGeo = memo( ({ dataInfo, @@ -59,6 +369,40 @@ export const WorldGeo = memo( >([]); const labelContainerRef = useRef(null); const labelsRef = useRef([]); + + // 添加状态来控制是否显示tooltip + const [showTooltip1, setShowTooltip1] = useState(false); + const [showTooltip2, setShowTooltip2] = useState(false); + + // 模拟日志数据 + const [nestedEncryptionLogs] = useState([ + "初始化嵌套加密...", + "生成密钥对...", + "应用第一层加密...", + "应用第二层加密...", + "应用第三层加密...", + "加密完成,准备传输...", + ]); + + const [trafficObfuscationLogs] = useState([ + "初始化流量混淆...", + "分析流量特征...", + "应用随机填充...", + "调整数据包时间间隔...", + "模拟HTTP流量...", + "混淆完成,准备传输...", + ]); + + // 添加调试日志 + useEffect(() => { + console.log("Tooltip state:", { + tooltipClosed, + tooltipType, + showTooltip1, + showTooltip2, + }); + }, [tooltipClosed, tooltipType, showTooltip1, showTooltip2]); + const mainToData = useMemo(() => { const newList = [ dataInfo.passAuthentication, @@ -71,7 +415,7 @@ export const WorldGeo = memo( selectedApp && selectedApp ? [...newList, selectedApp] : newList ?? []; // 初始化数据数组 - 不再包含 startCountry const data: any = []; - console.log(proxiesList,'proxiesList') + console.log(proxiesList, "proxiesList"); // 遍历代理列表 proxiesList.forEach((proxyItem: any) => { // 检查是否有数据数组 @@ -107,165 +451,96 @@ export const WorldGeo = memo( }); return data; }, [dataInfo, selectedApp]); - // 创建自定义提示框DOM元素 - const createCustomTooltip = () => { - console.log("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 = ` -
- -
-
-
-
${ - currentTooltipType.title - }
- -
- -
-
- `; - // 添加到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 = () => { + console.log("Positioning tooltip1", { + hasRef: !!customTooltipRef.current, + hasChart: !!proxyGeoRef.current, + }); + if (!customTooltipRef.current || !proxyGeoRef.current) return; - // 找到US点 + + // 找到点 const coords = geoCoordMap[dataInfo.nestedEncryption?.[0]?.code ?? "GL"]; + console.log("Tooltip1 coords:", coords); + if (!coords) return; + try { // 将地理坐标转换为屏幕坐标 const screenCoord = proxyGeoRef.current.convertToPixel("geo", coords); + console.log("Tooltip1 screen coords:", screenCoord); + if ( screenCoord && Array.isArray(screenCoord) && screenCoord.length === 2 ) { // 设置提示框位置 - customTooltipRef.current.style.left = `${screenCoord[0] + 232 + 7}px`; - customTooltipRef.current.style.top = `${screenCoord[1] + 40 + 15}px`; + const left = `${screenCoord[0] + 232 + 7}px`; + const top = `${screenCoord[1] + 40 + 15}px`; + console.log("Setting tooltip1 position:", { left, top }); + + customTooltipRef.current.style.left = left; + customTooltipRef.current.style.top = top; } } catch (error) { - console.error("Error positioning tooltip:", error); + console.error("Error positioning tooltip1:", error); } }; - // 创建自定义提示框DOM元素 - const createCustomTooltip2 = () => { - // 如果已经存在自定义提示框,则移除它 - if (document.getElementById("custom-fixed-tooltip2")) { - document.getElementById("custom-fixed-tooltip2")?.remove(); - } - // 创建自定义提示框 - const tooltip = document.createElement("div"); - tooltip.id = "custom-fixed-tooltip2"; - tooltip.style.position = "fixed"; - tooltip.style.zIndex = "1000"; - tooltip.style.pointerEvents = "auto"; - tooltip.style.backgroundColor = "transparent"; - tooltip.innerHTML = ` -
-
-
-
-
流量混淆
- -
- -
- -
- `; - // 添加到DOM - document.body.appendChild(tooltip); - customTooltip2Ref.current = tooltip; - // 添加关闭按钮事件 - const closeButton = tooltip.querySelector(".close-icon2"); - if (closeButton) { - closeButton.addEventListener("click", () => { - setTooltipClosed(false); - customTooltip2Ref.current?.remove(); - customTooltip2Ref.current = null; - }); - } - // 定位提示框 - positionCustomTooltip2(); - }; - // 定位自定义提示框 - 优化版本 + + // 定位自定义提示框2 - 优化版本 const positionCustomTooltip2 = () => { + console.log("Positioning tooltip2", { + hasRef: !!customTooltip2Ref.current, + hasChart: !!proxyGeoRef.current, + }); + if (!customTooltip2Ref.current || !proxyGeoRef.current) return; - // 找到US点 + + // 找到点 const coords = geoCoordMap[dataInfo.trafficObfuscation?.[0]?.code ?? "ZA"]; + console.log("Tooltip2 coords:", coords); + if (!coords) return; + try { // 将地理坐标转换为屏幕坐标 const screenCoord = proxyGeoRef.current.convertToPixel("geo", coords); + console.log("Tooltip2 screen coords:", screenCoord); + if ( screenCoord && Array.isArray(screenCoord) && screenCoord.length === 2 ) { // 设置提示框位置 - customTooltip2Ref.current.style.left = `${ - screenCoord[0] - 626 + 20 - }px`; - customTooltip2Ref.current.style.top = `${ - screenCoord[1] + 40 - 13 - }px`; + const left = `${screenCoord[0] - 626 + 20}px`; + const top = `${screenCoord[1] + 40 - 13}px`; + console.log("Setting tooltip2 position:", { left, top }); + + customTooltip2Ref.current.style.left = left; + customTooltip2Ref.current.style.top = top; } } catch (error) { - console.error("Error positioning tooltip:", error); + console.error("Error positioning tooltip2:", error); } }; + + // 处理关闭tooltip + const handleCloseTooltip1 = () => { + setShowTooltip1(false); + setTooltipClosed(false); + }; + + // 处理关闭tooltip2 + const handleCloseTooltip2 = () => { + setShowTooltip2(false); + setTooltipClosed(false); + }; + const getLineItem = ( preCode: string, nextCode: string, @@ -286,6 +561,7 @@ export const WorldGeo = memo( }, ]; }; + const getLine = () => { // 实现数据处理 const solidData: LinesType[] = [["main", []]]; // 使用"main"替代startCountry.country_code @@ -345,7 +621,11 @@ export const WorldGeo = memo( } // 检查是否应该绘制连线 if (currentItem.isLine !== false) { - const lineItem = getLineItem(countryCode, nextCountryCode, lineColor); + const lineItem = getLineItem( + countryCode, + nextCountryCode, + lineColor + ); solidData[0][1].push(lineItem); } } @@ -359,6 +639,7 @@ export const WorldGeo = memo( pointColors, }; }; + // 获取连线经纬度数据 const convertData = (data: LinesDataType[]) => { const res = []; @@ -371,7 +652,7 @@ export const WorldGeo = memo( const toCountry = dataIndex?.[1]?.country_code ?? ""; // 使用每条线自己的颜色 const lineColor = dataIndex?.[0]?.color || "#0ea5e9"; - + if (fromCoord && toCoord) { res.push({ coords: [fromCoord, toCoord], @@ -379,7 +660,7 @@ export const WorldGeo = memo( color: lineColor, // 使用自定义颜色 }, // 保存颜色信息用于飞行特效 - color: lineColor + color: lineColor, }); // 计算中点,考虑曲线的弧度 const curveness = -0.4; // 与飞线弧度相同 @@ -405,6 +686,7 @@ export const WorldGeo = memo( // lineMidpointsRef.current = midpoints; return res; }; + // 创建双层点效果 - 大点 const createDualLayerPoint = ( lastExit: LinesItemType, @@ -412,7 +694,7 @@ export const WorldGeo = memo( ) => { // 使用点自己的颜色 const pointColor = lastExit.color || "#0ea5e9"; - + // 创建数据数组,用于两个散点图层 const pointData = lastExit ? [lastExit].map((v) => { @@ -421,7 +703,7 @@ export const WorldGeo = memo( value: v.value, datas: { country_code: v.country_code, - color: pointColor // 保存颜色信息 + color: pointColor, // 保存颜色信息 }, }; }) @@ -437,9 +719,9 @@ export const WorldGeo = memo( coordinateSystem: "geo", zlevel: 3, itemStyle: { - color: function(params: any) { + color: function (params: any) { return params.data.datas.color; - } + }, }, symbol: "circle", symbolSize: outerSize, @@ -493,6 +775,7 @@ export const WorldGeo = memo( } as echarts.SeriesOption, ]; }; + // 添加新方法:根据经纬度数组创建蓝色涟漪小点(不包含白色内层点) const createRipplePointsFromCoordinates = ( coordinates: [number, number][], @@ -500,7 +783,7 @@ export const WorldGeo = memo( color: string = "#01FF5E" ) => { if (!coordinates || coordinates.length === 0) return; - + // 只创建外层带涟漪效果的点 series.push({ type: "effectScatter", @@ -524,6 +807,7 @@ export const WorldGeo = memo( })), } as echarts.SeriesOption); }; + // 创建路径点的双层效果 const createPathPoints = ( dataItems: LinesDataType[], @@ -533,13 +817,13 @@ export const WorldGeo = memo( const pointData = dataItems.map((dataItem: LinesDataType) => { // 使用每个点自己的颜色 const pointColor = dataItem[0].color || "#0ea5e9"; - + return { name: dataItem[0].name, value: geoCoordMap[dataItem[0].country_code], datas: { country_code: dataItem[0].country_code, - color: pointColor // 保存颜色信息 + color: pointColor, // 保存颜色信息 }, }; }); @@ -606,6 +890,7 @@ export const WorldGeo = memo( } as echarts.SeriesOption, ]; }; + // 创建带自定义提示框的涟漪点 const createRipplePointsWithTooltip = (ripplePoints: any) => { return { @@ -662,6 +947,7 @@ export const WorldGeo = memo( })), } as echarts.SeriesOption; }; + // 连线 series const getLianData = (series: echarts.SeriesOption[]) => { const { solidData, otherLineList, ripplePoints } = getLine(); @@ -694,13 +980,13 @@ export const WorldGeo = memo( if (item[1].length === 0) { return; } - + // 处理每条线段 item[1].forEach((lineSegment, index) => { const fromPoint = lineSegment[0]; const toPoint = lineSegment[1]; const lineColor = fromPoint.color || "#0ea5e9"; - + // 添加单条飞行线 series.push({ name: `${item[0]}-${index}`, @@ -726,13 +1012,15 @@ export const WorldGeo = memo( opacity: 0.1, color: lineColor, // 使用线段自己的颜色 }, - data: convertData([[fromPoint, toPoint]]) as echarts.LinesSeriesOption["data"], + data: convertData([ + [fromPoint, toPoint], + ]) as echarts.LinesSeriesOption["data"], }); - + // 添加起点的双层效果 const startNodes = createDualLayerPoint(fromPoint, true); series.push(...startNodes); - + // 如果是最后一个线段,添加终点的双层效果 if (index === item[1].length - 1) { const endNodes = createDualLayerPoint(toPoint, true); @@ -740,7 +1028,7 @@ export const WorldGeo = memo( } }); }); - + otherLineList.forEach((line: any) => { line.forEach((item: any) => { // 处理每条虚线段 @@ -748,7 +1036,7 @@ export const WorldGeo = memo( const fromPoint = lineSegment[0]; const toPoint = lineSegment[1]; const lineColor = fromPoint.color || "#F0FFA2"; - + // 添加虚线 series.push({ name: `${item[0]}-dashed-${index}`, @@ -765,13 +1053,15 @@ export const WorldGeo = memo( width: 0.5, // 飞线宽度 opacity: 0.6, }, - data: convertData([[fromPoint, toPoint]]) as echarts.LinesSeriesOption["data"], + data: convertData([ + [fromPoint, toPoint], + ]) as echarts.LinesSeriesOption["data"], }); - + // 添加起点的双层效果 const startNodes = createDualLayerPoint(fromPoint, false); series.push(...startNodes); - + // 如果是最后一个线段,添加终点的双层效果 if (index === item[1].length - 1) { const endNodes = createDualLayerPoint(toPoint, false); @@ -780,9 +1070,10 @@ export const WorldGeo = memo( }); }); }); - + return true; }; + // 创建A点和B点,并添加飞线和标签 const createSpecialPoints = (series: echarts.SeriesOption[]) => { // 定义点A和点B的坐标 @@ -926,6 +1217,7 @@ export const WorldGeo = memo( }); return series; }; + const getOption = () => { const series: echarts.SeriesOption[] = []; getLianData(series); @@ -1015,6 +1307,7 @@ export const WorldGeo = memo( }; return option; }; + // 创建DOM标签 const createDOMLabels = () => { // 清除现有标签 @@ -1079,6 +1372,7 @@ export const WorldGeo = memo( // 更新标签位置 updateLabelPositions(); }; + // 更新标签位置 const updateLabelPositions = () => { if (!proxyGeoRef.current || !labelContainerRef.current) return; @@ -1095,10 +1389,20 @@ export const WorldGeo = memo( } }); }; + const handleResize = () => { proxyGeoRef.current?.resize(); updateLabelPositions(); + + // 重新定位tooltip + if (showTooltip1) { + positionCustomTooltip(); + } + if (showTooltip2) { + positionCustomTooltip2(); + } }; + useEffect(() => { preMainToData.current?.some( (item, index) => item.country_code !== mainToData[index]?.country_code @@ -1109,6 +1413,7 @@ export const WorldGeo = memo( // 创建DOM标签 setTimeout(createDOMLabels, 100); }, [dataInfo, mainToData]); + useEffect(() => { const chartDom = document.getElementById("screenGeo"); proxyGeoRef.current = echarts.init(chartDom); @@ -1135,27 +1440,81 @@ export const WorldGeo = memo( proxyGeoRef.current = null; }; }, []); + + // 修改处理tooltip的显示和隐藏的逻辑 useEffect(() => { + console.log("Tooltip effect triggered:", { tooltipClosed, tooltipType }); + if (tooltipClosed) { - createCustomTooltip(); - createCustomTooltip2(); + if (tooltipType === "NESTED_ENCRYPTION") { + setShowTooltip1(true); + setShowTooltip2(false); // 确保另一个是关闭的 + // 在下一个渲染周期后定位tooltip + setTimeout(() => { + positionCustomTooltip(); + }, 0); + } else if (tooltipType === "TRAFFIC_OBFUSCATION") { + setShowTooltip1(false); // 确保另一个是关闭的 + setShowTooltip2(true); + // 在下一个渲染周期后定位tooltip + setTimeout(() => { + positionCustomTooltip2(); + }, 0); + } + } else { + setShowTooltip1(false); + setShowTooltip2(false); } - return () => { - customTooltipRef.current?.remove(); - customTooltip2Ref.current?.remove(); - customTooltipRef.current = null; - customTooltip2Ref.current = null; - }; }, [ tooltipClosed, tooltipType, dataInfo.nestedEncryption, dataInfo.trafficObfuscation, ]); + + // 在地图初始化后定位tooltip + useEffect(() => { + positionCustomTooltip(); + positionCustomTooltip2(); + }, [showTooltip1, showTooltip2]); + return (
+ + {/* 嵌套加密提示框 */} + + + + {/* 流量混淆提示框 - 确保条件正确 */} + + + +
); } -); \ No newline at end of file +); + +// 添加CSS样式 +// 可以放在你的全局CSS文件中 +// @keyframes fadeIn { +// from { opacity: 0; transform: translateY(5px); } +// to { opacity: 1; transform: translateY(0); } +// } +// +// .animate-fadeIn { +// animation: fadeIn 0.3s ease-out forwards; +// } diff --git a/src/pages/proxies/index.scss b/src/pages/proxies/index.scss index f0f1892..46480e1 100644 --- a/src/pages/proxies/index.scss +++ b/src/pages/proxies/index.scss @@ -1,39 +1,5 @@ -.proxies { - // .proxies-container { - // &_country { - // padding: 16px; - // border-radius: 8px; - // border: 1px solid #DCDFEA; - // background: #FFF; - // // box-shadow: 0px 1px 3px 0px rgba(16, 24, 40, 0.10), 0px 1px 2px 0px rgba(16, 24, 40, 0.06); - // } - - // &::-webkit-scrollbar { - // width: 0px; - // height: 0px; - // /* background-color: red; */ - // } - - // &::-webkit-scrollbar-thumb { - // border-radius: 15px; - // background-color: rgba(144, 147, 153, 0.3); - // } - - // &::-webkit-scrollbar-thumb:hover { - // background-color: rgba(144, 147, 153, 0.5); - // } - - // & { - // /* Firefox */ - // scrollbar-width: none; - // /* auto, thin, none */ - // scrollbar-color: rgba(144, 147, 153, 0.5); - // } - // } - - -} +.proxies {} .custom-font { font-family: Arial, sans-serif; -} +} \ No newline at end of file