diff --git a/.env.example b/.env.example index 7b86098..2dcb8ba 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,6 @@ NEXT_PUBLIC_FixedTopServerName=false NEXT_PUBLIC_CustomLogo=https://nezha-cf.buycoffee.top/apple-touch-icon.png NEXT_PUBLIC_CustomTitle=NezhaDash NEXT_PUBLIC_CustomDescription=NezhaDash is a dashboard for Nezha. -NEXT_PUBLIC_Links="[{"link":"https://github.com/hamster1963/nezha-dash","name":"GitHub"},{"link":"https://buycoffee.top/coffee","name":"Buycoffee☕️"}]" +NEXT_PUBLIC_Links='[{"link":"https://github.com/hamster1963/nezha-dash","name":"GitHub"},{"link":"https://buycoffee.top/coffee","name":"Buycoffee☕️"}]' NEXT_PUBLIC_DisableIndex=false NEXT_PUBLIC_ShowTagCount=false diff --git a/app/(main)/ClientComponents/Global.tsx b/app/(main)/ClientComponents/Global.tsx index a375488..e257f4b 100644 --- a/app/(main)/ClientComponents/Global.tsx +++ b/app/(main)/ClientComponents/Global.tsx @@ -1,141 +1,49 @@ -import { countryCodeMapping, reverseCountryCodeMapping } from "@/lib/geo"; -import { countryCoordinates } from "@/lib/geo-limit"; import { GetNezhaData } from "@/lib/serverFetch"; -import * as turf from "@turf/turf"; -import DottedMap from "dotted-map/without-countries"; import { geoJsonString } from "../../../lib/geo-json-string"; -import { mapJsonString } from "../../../lib/map-string"; import GlobalInfo from "./GlobalInfo"; - -interface GlobalProps { - countries?: string[]; -} +import { InteractiveMap } from "./InteractiveMap"; export default async function ServerGlobal() { const nezhaServerList = await GetNezhaData(); const countrytList: string[] = []; + const serverCounts: { [key: string]: number } = {}; + nezhaServerList.result.forEach((server) => { if (server.host.CountryCode) { - server.host.CountryCode = server.host.CountryCode.toUpperCase(); - if (!countrytList.includes(server.host.CountryCode)) { - countrytList.push(server.host.CountryCode); + const countryCode = server.host.CountryCode.toUpperCase(); + if (!countrytList.includes(countryCode)) { + countrytList.push(countryCode); } + serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1; } }); - return ; -} + countrytList.push("TW"); + countrytList.push("SG"); + countrytList.push("RU"); -export async function Global({ countries = [] }: GlobalProps) { - const map = new DottedMap({ map: JSON.parse(mapJsonString) }); - - const countries_alpha3 = countries - .map((code) => countryCodeMapping[code]) - .filter((code) => code !== undefined); + const width = 900; + const height = 500; const geoJson = JSON.parse(geoJsonString); - - countries_alpha3.forEach((countryCode) => { - const feature = geoJson.features.find( - (f: any) => f.properties.iso_a3 === countryCode, - ); - - if (feature) { - if (countryCode === "RUS") { - // 获取俄罗斯的多个边界 - const bboxList = feature.geometry.coordinates.map((polygon: any) => - turf.bbox({ type: "Polygon", coordinates: polygon }), - ); - - const spacing = 20; // 单位为千米 - const options = { units: "kilometers" }; - - bboxList.forEach((bbox: any) => { - // @ts-expect-error ignore - const pointGrid = turf.pointGrid(bbox, spacing, options); - - // 过滤出位于当前多边形内部的点 - const pointsWithin = turf.pointsWithinPolygon(pointGrid, feature); - - if (pointsWithin.features.length === 0) { - const centroid = turf.centroid(feature); - const [lng, lat] = centroid.geometry.coordinates; - map.addPin({ - lat, - lng, - svgOptions: { color: "#FF4500", radius: 0.3 }, - }); - } else { - pointsWithin.features.forEach((point: any) => { - const [lng, lat] = point.geometry.coordinates; - map.addPin({ - lat, - lng, - svgOptions: { color: "#FF4500", radius: 0.3 }, - }); - }); - } - }); - } - // 获取国家的边界框 - const bbox = turf.bbox(feature); - - const spacing = 40; // 单位为千米,值越小点越密集 - const options = { units: "kilometers" }; - // @ts-expect-error ignore - const pointGrid = turf.pointGrid(bbox, spacing, options); - - // 过滤出位于国家多边形内部的点 - const pointsWithin = turf.pointsWithinPolygon(pointGrid, feature); - - // 如果没有点在多边形内部,则使用国家的中心点 - if (pointsWithin.features.length === 0) { - const centroid = turf.centroid(feature); - const [lng, lat] = centroid.geometry.coordinates; - map.addPin({ - lat, - lng, - svgOptions: { color: "#FF4500", radius: 0.3 }, - }); - } else { - pointsWithin.features.forEach((point: any) => { - const [lng, lat] = point.geometry.coordinates; - map.addPin({ - lat, - lng, - svgOptions: { color: "#FF4500", radius: 0.3 }, - }); - }); - } - } else { - // 如果找不到feature,使用countryCoordinates中的坐标 - const alpha2Code = reverseCountryCodeMapping[countryCode]; - if (alpha2Code && countryCoordinates[alpha2Code]) { - const coordinates = countryCoordinates[alpha2Code]; - map.addPin({ - lat: coordinates.lat, - lng: coordinates.lng, - svgOptions: { color: "#FF4500", radius: 0.3 }, - }); - } - } - }); - - const finalMap = map.getSVG({ - radius: 0.35, - color: "#D1D5DA", - shape: "circle", - }); + const filteredFeatures = geoJson.features.filter( + (feature: any) => feature.properties.iso_a3 !== "", + ); return (
- - World Map with Highlighted Countries + +
+ +
); } diff --git a/app/(main)/ClientComponents/InteractiveMap.tsx b/app/(main)/ClientComponents/InteractiveMap.tsx new file mode 100644 index 0000000..3f2f096 --- /dev/null +++ b/app/(main)/ClientComponents/InteractiveMap.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { countryCodeMapping } from "@/lib/geo"; +import { geoEqualEarth, geoPath } from "d3-geo"; +import { AnimatePresence, m } from "framer-motion"; +import { useTranslations } from "next-intl"; +import { useState } from "react"; + +interface InteractiveMapProps { + countries: string[]; + serverCounts: { [key: string]: number }; + width: number; + height: number; + filteredFeatures: any[]; +} + +export function InteractiveMap({ + countries, + serverCounts, + width, + height, + filteredFeatures, +}: InteractiveMapProps) { + const t = useTranslations("Global"); + + const [tooltipData, setTooltipData] = useState<{ + centroid: [number, number]; + country: string; + count: number; + } | null>(null); + + const countries_alpha3 = countries + .map((code) => countryCodeMapping[code]) + .filter((code) => code !== undefined); + + const projection = geoEqualEarth() + .scale(180) + .translate([width / 2, height / 2]); + + const path = geoPath().projection(projection); + + return ( +
+ + + + + + + + {filteredFeatures.map((feature, index) => { + const isHighlighted = countries_alpha3.includes( + feature.properties.iso_a3, + ); + const countryCode = Object.entries(countryCodeMapping).find( + ([,alpha3]) => alpha3 === feature.properties.iso_a3, + )?.[0]; + const serverCount = countryCode + ? serverCounts[countryCode] || 0 + : 0; + + return ( + { + if (isHighlighted && path.centroid(feature)) { + setTooltipData({ + centroid: path.centroid(feature), + country: feature.properties.name, + count: serverCount, + }); + } + }} + onMouseLeave={() => setTooltipData(null)} + /> + ); + })} + + + + {tooltipData && ( + +

{tooltipData.country}

+

+ {tooltipData.count} {t("Servers")} +

+
+ )} +
+
+ ); +} diff --git a/app/(main)/header.tsx b/app/(main)/header.tsx index ede7663..ff0fe68 100644 --- a/app/(main)/header.tsx +++ b/app/(main)/header.tsx @@ -7,13 +7,11 @@ import { Skeleton } from "@/components/ui/skeleton"; import getEnv from "@/lib/env-entry"; import { DateTime } from "luxon"; import { useTranslations } from "next-intl"; -import { useTheme } from "next-themes"; import { useRouter } from "next/navigation"; import React, { useEffect, useRef, useState } from "react"; function Header() { const t = useTranslations("Header"); - const { resolvedTheme } = useTheme(); const customLogo = getEnv("NEXT_PUBLIC_CustomLogo"); const customTitle = getEnv("NEXT_PUBLIC_CustomTitle"); const customDescription = getEnv("NEXT_PUBLIC_CustomDescription"); @@ -35,14 +33,15 @@ function Header() { width={40} height={40} alt="apple-touch-icon" - src={ - customLogo - ? customLogo - : resolvedTheme === "light" - ? "/apple-touch-icon.png" - : "/apple-touch-icon-dark.png" - } - className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0!" + src={customLogo ? customLogo : "/apple-touch-icon.png"} + className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0! dark:hidden" + /> + apple-touch-icon {customTitle ? customTitle : "NezhaDash"} diff --git a/bun.lockb b/bun.lockb index 3c5409c..0406a32 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/LanguageSwitcher.tsx b/components/LanguageSwitcher.tsx index 16ffb64..a336d62 100644 --- a/components/LanguageSwitcher.tsx +++ b/components/LanguageSwitcher.tsx @@ -27,7 +27,7 @@ export function LanguageSwitcher() {