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 (
-
-
+
+
+
+
);
}
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 (
+
+
+
+ {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"
+ />
+
{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() {