feat: global map

This commit is contained in:
hamster1963 2024-12-04 10:03:37 +08:00
parent 535e9f6db1
commit 8228fab2fe
11 changed files with 465 additions and 4 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -22,10 +22,12 @@
"@tanstack/react-query": "^5.62.0", "@tanstack/react-query": "^5.62.0",
"@tanstack/react-query-devtools": "^5.62.0", "@tanstack/react-query-devtools": "^5.62.0",
"@tanstack/react-table": "^8.20.5", "@tanstack/react-table": "^8.20.5",
"@types/d3-geo": "^3.1.0",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"country-flag-icons": "^1.5.13", "country-flag-icons": "^1.5.13",
"d3-geo": "^3.1.1",
"framer-motion": "^11.12.0", "framer-motion": "^11.12.0",
"i18next": "^24.0.2", "i18next": "^24.0.2",
"lucide-react": "^0.460.0", "lucide-react": "^0.460.0",
@ -34,7 +36,6 @@
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-i18next": "^15.1.3", "react-i18next": "^15.1.3",
"react-router-dom": "^7.0.1", "react-router-dom": "^7.0.1",
"react-use-websocket": "^4.11.1",
"recharts": "^2.13.3", "recharts": "^2.13.3",
"sonner": "^1.7.0", "sonner": "^1.7.0",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",

View File

@ -0,0 +1,210 @@
import { geoJsonString } from "@/lib/geo-json-string";
import { NezhaServer } from "@/types/nezha-api";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { AnimatePresence, m } from "framer-motion";
import { geoEquirectangular, geoPath } from "d3-geo";
import { countryCoordinates } from "@/lib/geo-limit";
export default function GlobalMap({
serverList,
}: {
serverList: NezhaServer[];
}) {
const { t } = useTranslation();
const countryList: string[] = [];
const serverCounts: { [key: string]: number } = {};
console.log(serverList);
serverList.forEach((server) => {
if (server.country_code) {
const countryCode = server.country_code.toUpperCase();
if (!countryList.includes(countryCode)) {
countryList.push(countryCode);
}
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1;
}
});
const width = 900;
const height = 500;
const geoJson = JSON.parse(geoJsonString);
const filteredFeatures = geoJson.features.filter(
(feature: { properties: { iso_a3_eh: string } }) =>
feature.properties.iso_a3_eh !== "",
);
return (
<section className="flex flex-col gap-4 mt-8">
<p className="text-sm font-medium opacity-40">
{t("map.Distributions")} {countryList.length} {t("map.Regions")}
</p>
<div className="w-full overflow-x-auto">
<InteractiveMap
countries={countryList}
serverCounts={serverCounts}
width={width}
height={height}
filteredFeatures={filteredFeatures}
/>
</div>
</section>
);
}
interface InteractiveMapProps {
countries: string[];
serverCounts: { [key: string]: number };
width: number;
height: number;
filteredFeatures: {
type: "Feature";
properties: {
iso_a2_eh: string;
[key: string]: string;
};
geometry: never;
}[];
}
function InteractiveMap({
countries,
serverCounts,
width,
height,
filteredFeatures,
}: InteractiveMapProps) {
const { t } = useTranslation();
const [tooltipData, setTooltipData] = useState<{
centroid: [number, number];
country: string;
count: number;
} | null>(null);
const projection = geoEquirectangular()
.scale(140)
.translate([width / 2, height / 2])
.rotate([-12, 0, 0]);
const path = geoPath().projection(projection);
return (
<div className="relative w-full aspect-[2/1]">
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
xmlns="http://www.w3.org/2000/svg"
className="w-full h-auto"
>
<defs>
<pattern id="dots" width="2" height="2" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.5" fill="currentColor" />
</pattern>
</defs>
<g>
{filteredFeatures.map((feature, index) => {
const isHighlighted = countries.includes(
feature.properties.iso_a2_eh,
);
if (isHighlighted) {
console.log(feature.properties.iso_a2_eh);
}
const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0;
return (
<path
key={index}
d={path(feature) || ""}
className={
isHighlighted
? "fill-green-700 hover:fill-green-600 dark:fill-green-900 dark:hover:fill-green-700 transition-all cursor-pointer"
: "fill-neutral-200/50 dark:fill-neutral-800 stroke-neutral-300/40 dark:stroke-neutral-700 stroke-[0.5]"
}
onMouseEnter={() => {
if (isHighlighted && path.centroid(feature)) {
setTooltipData({
centroid: path.centroid(feature),
country: feature.properties.name,
count: serverCount,
});
}
}}
onMouseLeave={() => setTooltipData(null)}
/>
);
})}
{/* 渲染不在 filteredFeatures 中的国家标记点 */}
{countries.map((countryCode) => {
// 检查该国家是否已经在 filteredFeatures 中
const isInFilteredFeatures = filteredFeatures.some(
(feature) => feature.properties.iso_a2_eh === countryCode,
);
// 如果已经在 filteredFeatures 中,跳过
if (isInFilteredFeatures) return null;
// 获取国家的经纬度
const coords = countryCoordinates[countryCode];
if (!coords) return null;
// 使用投影函数将经纬度转换为 SVG 坐标
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0];
const serverCount = serverCounts[countryCode] || 0;
return (
<g
key={countryCode}
onMouseEnter={() => {
setTooltipData({
centroid: [x, y],
country: coords.name,
count: serverCount,
});
}}
onMouseLeave={() => setTooltipData(null)}
className="cursor-pointer"
>
<circle
cx={x}
cy={y}
r={4}
className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all"
/>
</g>
);
})}
</g>
</svg>
<AnimatePresence mode="wait">
{tooltipData && (
<m.div
initial={{ opacity: 0, filter: "blur(10px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
className="absolute hidden lg:block pointer-events-none bg-white dark:bg-neutral-800 px-2 py-1 rounded shadow-lg text-sm dark:border dark:border-neutral-700"
key={tooltipData.country}
style={{
left: tooltipData.centroid[0],
top: tooltipData.centroid[1],
transform: "translate(-50%, -50%)",
}}
>
<p className="font-medium">
{tooltipData.country === "China"
? "Mainland China"
: tooltipData.country}
</p>
<p className="text-neutral-600 dark:text-neutral-400">
{tooltipData.count} {t("map.Servers")}
</p>
</m.div>
)}
</AnimatePresence>
</div>
);
}

View File

@ -99,10 +99,10 @@ export default function ServerOverview({
{t("serverOverview.network")} {t("serverOverview.network")}
</p> </p>
<section className="flex flex-row z-[999] sm:items-center items-start pr-2 sm:pr-0 gap-1 ml-auto"> <section className="flex flex-row z-[999] sm:items-center items-start pr-2 sm:pr-0 gap-1 ml-auto">
<p className="sm:text-[12px] text-[10px] text-blue-800 text-nowrap font-medium"> <p className="sm:text-[12px] text-[10px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">
{formatBytes(up)} {formatBytes(up)}
</p> </p>
<p className="sm:text-[12px] text-[10px] text-purple-800 text-nowrap font-medium"> <p className="sm:text-[12px] text-[10px] text-purple-800 dark:text-purple-400 text-nowrap font-medium">
{formatBytes(down)} {formatBytes(down)}
</p> </p>
</section> </section>

File diff suppressed because one or more lines are too long

211
src/lib/geo-limit.ts Normal file
View File

@ -0,0 +1,211 @@
export const countryCoordinates: Record<
string,
{ lat: number; lng: number; name: string }
> = {
// 亚洲
AF: { lat: 33.0, lng: 65.0, name: "Afghanistan" }, // 阿富汗
AM: { lat: 40.0, lng: 45.0, name: "Armenia" }, // 亚美尼亚
AZ: { lat: 40.5, lng: 47.5, name: "Azerbaijan" }, // 阿塞拜疆
BD: { lat: 24.0, lng: 90.0, name: "Bangladesh" }, // 孟加拉国
BH: { lat: 26.0, lng: 50.55, name: "Bahrain" }, // 巴林
BT: { lat: 27.5, lng: 90.5, name: "Bhutan" }, // 不丹
BN: { lat: 4.5, lng: 114.6667, name: "Brunei" }, // 文莱
KH: { lat: 13.0, lng: 105.0, name: "Cambodia" }, // 柬埔寨
CN: { lat: 35.0, lng: 105.0, name: "China" }, // 中国
HK: { lat: 22.0, lng: 114.0, name: "Hong Kong" }, // 香港
CY: { lat: 35.0, lng: 33.0, name: "Cyprus" }, // 塞浦路斯
GE: { lat: 42.0, lng: 43.5, name: "Georgia" }, // 格鲁吉亚
IN: { lat: 20.0, lng: 77.0, name: "India" }, // 印度
ID: { lat: -5.0, lng: 120.0, name: "Indonesia" }, // 印度尼西亚
IR: { lat: 32.0, lng: 53.0, name: "Iran" }, // 伊朗
IQ: { lat: 33.0, lng: 44.0, name: "Iraq" }, // 伊拉克
IL: { lat: 31.5, lng: 34.75, name: "Israel" }, // 以色列
JP: { lat: 36.0, lng: 138.0, name: "Japan" }, // 日本
JO: { lat: 31.0, lng: 36.0, name: "Jordan" }, // 约旦
KZ: { lat: 48.0, lng: 68.0, name: "Kazakhstan" }, // 哈萨克斯坦
KW: { lat: 29.3375, lng: 47.6581, name: "Kuwait" }, // 科威特
KG: { lat: 41.0, lng: 75.0, name: "Kyrgyzstan" }, // 吉尔吉斯斯坦
LA: { lat: 18.0, lng: 105.0, name: "Laos" }, // 老挝
LB: { lat: 33.8333, lng: 35.8333, name: "Lebanon" }, // 黎巴嫩
MY: { lat: 2.5, lng: 112.5, name: "Malaysia" }, // 马来西亚
MV: { lat: 3.25, lng: 73.0, name: "Maldives" }, // 马尔代夫
MN: { lat: 46.0, lng: 105.0, name: "Mongolia" }, // 蒙古
MM: { lat: 22.0, lng: 98.0, name: "Myanmar" }, // 缅甸
NP: { lat: 28.0, lng: 84.0, name: "Nepal" }, // 尼泊尔
OM: { lat: 21.0, lng: 57.0, name: "Oman" }, // 阿曼
PK: { lat: 30.0, lng: 70.0, name: "Pakistan" }, // 巴基斯坦
PH: { lat: 13.0, lng: 122.0, name: "Philippines" }, // 菲律宾
QA: { lat: 25.5, lng: 51.25, name: "Qatar" }, // 卡塔尔
SA: { lat: 25.0, lng: 45.0, name: "Saudi Arabia" }, // 沙特阿拉伯
SG: { lat: 1.3667, lng: 103.8, name: "Singapore" }, // 新加坡
KR: { lat: 37.0, lng: 127.5, name: "South Korea" }, // 韩国
LK: { lat: 7.0, lng: 81.0, name: "Sri Lanka" }, // 斯里兰卡
SY: { lat: 35.0, lng: 38.0, name: "Syria" }, // 叙利亚
TW: { lat: 23.5, lng: 121.0, name: "Taiwan" }, // 台湾
TJ: { lat: 39.0, lng: 71.0, name: "Tajikistan" }, // 塔吉克斯坦
TH: { lat: 15.0, lng: 100.0, name: "Thailand" }, // 泰国
TR: { lat: 39.0, lng: 35.0, name: "Turkey" }, // 土耳其
TM: { lat: 40.0, lng: 60.0, name: "Turkmenistan" }, // 土库曼斯坦
AE: { lat: 24.0, lng: 54.0, name: "United Arab Emirates" }, // 阿联酋
UZ: { lat: 41.0, lng: 64.0, name: "Uzbekistan" }, // 乌兹别克斯坦
VN: { lat: 16.0, lng: 106.0, name: "Vietnam" }, // 越南
YE: { lat: 15.0, lng: 48.0, name: "Yemen" }, // 也门
PS: { lat: 32.0, lng: 35.25, name: "Palestine" }, // 巴勒斯坦
// 欧洲
AL: { lat: 41.0, lng: 20.0, name: "Albania" }, // 阿尔巴尼亚
AD: { lat: 42.5, lng: 1.6, name: "Andorra" }, // 安道尔
AT: { lat: 47.3333, lng: 13.3333, name: "Austria" }, // 奥地利
BY: { lat: 53.0, lng: 28.0, name: "Belarus" }, // 白俄罗斯
BE: { lat: 50.8333, lng: 4.0, name: "Belgium" }, // 比利时
BA: { lat: 44.0, lng: 18.0, name: "Bosnia and Herzegovina" }, // 波黑
BG: { lat: 43.0, lng: 25.0, name: "Bulgaria" }, // 保加利亚
HR: { lat: 45.1667, lng: 15.5, name: "Croatia" }, // 克罗地亚
CZ: { lat: 49.75, lng: 15.5, name: "Czech Republic" }, // 捷克
DK: { lat: 56.0, lng: 10.0, name: "Denmark" }, // 丹麦
EE: { lat: 59.0, lng: 26.0, name: "Estonia" }, // 爱沙尼亚
FI: { lat: 64.0, lng: 26.0, name: "Finland" }, // 芬兰
FR: { lat: 46.0, lng: 2.0, name: "France" }, // 法国
DE: { lat: 51.0, lng: 9.0, name: "Germany" }, // 德国
GR: { lat: 39.0, lng: 22.0, name: "Greece" }, // 希腊
HU: { lat: 47.0, lng: 20.0, name: "Hungary" }, // 匈牙利
IS: { lat: 65.0, lng: -18.0, name: "Iceland" }, // 冰岛
IE: { lat: 53.0, lng: -8.0, name: "Ireland" }, // 爱尔兰
IT: { lat: 42.8333, lng: 12.8333, name: "Italy" }, // 意大利
LV: { lat: 57.0, lng: 25.0, name: "Latvia" }, // 拉脱维亚
LI: { lat: 47.1667, lng: 9.5333, name: "Liechtenstein" }, // 列支敦士登
LT: { lat: 56.0, lng: 24.0, name: "Lithuania" }, // 立陶宛
LU: { lat: 49.75, lng: 6.1667, name: "Luxembourg" }, // 卢森堡
MT: { lat: 35.8333, lng: 14.5833, name: "Malta" }, // 马耳他
MD: { lat: 47.0, lng: 29.0, name: "Moldova" }, // 摩尔多瓦
MC: { lat: 43.7333, lng: 7.4, name: "Monaco" }, // 摩纳哥
ME: { lat: 42.0, lng: 19.0, name: "Montenegro" }, // 黑山
NL: { lat: 52.5, lng: 5.75, name: "Netherlands" }, // 荷兰
NO: { lat: 62.0, lng: 10.0, name: "Norway" }, // 挪威
PL: { lat: 52.0, lng: 20.0, name: "Poland" }, // 波兰
PT: { lat: 39.5, lng: -8.0, name: "Portugal" }, // 葡萄牙
RO: { lat: 46.0, lng: 25.0, name: "Romania" }, // 罗马尼亚
RU: { lat: 60.0, lng: 100.0, name: "Russia" }, // 俄罗斯
SM: { lat: 43.7667, lng: 12.4167, name: "San Marino" }, // 圣马力诺
RS: { lat: 44.0, lng: 21.0, name: "Serbia" }, // 塞尔维亚
SK: { lat: 48.6667, lng: 19.5, name: "Slovakia" }, // 斯洛伐克
SI: { lat: 46.0, lng: 15.0, name: "Slovenia" }, // 斯洛文尼亚
ES: { lat: 40.0, lng: -4.0, name: "Spain" }, // 西班牙
SE: { lat: 62.0, lng: 15.0, name: "Sweden" }, // 瑞典
CH: { lat: 47.0, lng: 8.0, name: "Switzerland" }, // 瑞士
UA: { lat: 49.0, lng: 32.0, name: "Ukraine" }, // 乌克兰
GB: { lat: 54.0, lng: -2.0, name: "United Kingdom" }, // 英国
VA: { lat: 41.9, lng: 12.45, name: "Vatican City" }, // 梵蒂冈
// 北美洲
AG: { lat: 17.05, lng: -61.8, name: "Antigua and Barbuda" }, // 安提瓜和巴布达
BS: { lat: 24.25, lng: -76.0, name: "Bahamas" }, // 巴哈马
BB: { lat: 13.1667, lng: -59.5333, name: "Barbados" }, // 巴巴多斯
BZ: { lat: 17.25, lng: -88.75, name: "Belize" }, // 伯利兹
CA: { lat: 60.0, lng: -95.0, name: "Canada" }, // 加拿大
CR: { lat: 10.0, lng: -84.0, name: "Costa Rica" }, // 哥斯达黎加
CU: { lat: 21.5, lng: -80.0, name: "Cuba" }, // 古巴
DM: { lat: 15.4167, lng: -61.3333, name: "Dominica" }, // 多米尼克
DO: { lat: 19.0, lng: -70.6667, name: "Dominican Republic" }, // 多米尼加共和国
SV: { lat: 13.8333, lng: -88.9167, name: "El Salvador" }, // 萨尔瓦多
GD: { lat: 12.1167, lng: -61.6667, name: "Grenada" }, // 格林纳达
GT: { lat: 15.5, lng: -90.25, name: "Guatemala" }, // 危地马拉
HT: { lat: 19.0, lng: -72.4167, name: "Haiti" }, // 海地
HN: { lat: 15.0, lng: -86.5, name: "Honduras" }, // 洪都拉斯
JM: { lat: 18.25, lng: -77.5, name: "Jamaica" }, // 牙买加
MX: { lat: 23.0, lng: -102.0, name: "Mexico" }, // 墨西哥
NI: { lat: 13.0, lng: -85.0, name: "Nicaragua" }, // 尼加拉瓜
PA: { lat: 9.0, lng: -80.0, name: "Panama" }, // 巴拿马
KN: { lat: 17.3333, lng: -62.75, name: "Saint Kitts and Nevis" }, // 圣基茨和尼维斯
LC: { lat: 13.8833, lng: -61.1333, name: "Saint Lucia" }, // 圣卢西亚
VC: { lat: 13.25, lng: -61.2, name: "Saint Vincent and the Grenadines" }, // 圣文森特和格林纳丁斯
TT: { lat: 11.0, lng: -61.0, name: "Trinidad and Tobago" }, // 特立尼达和多巴哥
US: { lat: 38.0, lng: -97.0, name: "United States" }, // 美国
// 南美洲
AR: { lat: -34.0, lng: -64.0, name: "Argentina" }, // 阿根廷
BO: { lat: -17.0, lng: -65.0, name: "Bolivia" }, // 玻利维亚
BR: { lat: -10.0, lng: -55.0, name: "Brazil" }, // 巴西
CL: { lat: -30.0, lng: -71.0, name: "Chile" }, // 智利
CO: { lat: 4.0, lng: -72.0, name: "Colombia" }, // 哥伦比亚
EC: { lat: -2.0, lng: -77.5, name: "Ecuador" }, // 厄瓜多尔
GY: { lat: 5.0, lng: -59.0, name: "Guyana" }, // 圭亚那
PY: { lat: -23.0, lng: -58.0, name: "Paraguay" }, // 巴拉圭
PE: { lat: -10.0, lng: -76.0, name: "Peru" }, // 秘鲁
SR: { lat: 4.0, lng: -56.0, name: "Suriname" }, // 苏里南
UY: { lat: -33.0, lng: -56.0, name: "Uruguay" }, // 乌拉圭
VE: { lat: 8.0, lng: -66.0, name: "Venezuela" }, // 委内瑞拉
// 大洋洲
AU: { lat: -27.0, lng: 133.0, name: "Australia" }, // 澳大利亚
FJ: { lat: -18.0, lng: 175.0, name: "Fiji" }, // 斐济
KI: { lat: 1.4167, lng: 173.0, name: "Kiribati" }, // 基里巴斯
MH: { lat: 9.0, lng: 168.0, name: "Marshall Islands" }, // 马绍尔群岛
FM: { lat: 6.9167, lng: 158.25, name: "Micronesia" }, // 密克罗尼西亚
NR: { lat: -0.5333, lng: 166.9167, name: "Nauru" }, // 瑙鲁
NZ: { lat: -41.0, lng: 174.0, name: "New Zealand" }, // 新西兰
PW: { lat: 7.5, lng: 134.5, name: "Palau" }, // 帕劳
PG: { lat: -6.0, lng: 147.0, name: "Papua New Guinea" }, // 巴布亚新几内亚
WS: { lat: -13.5833, lng: -172.3333, name: "Samoa" }, // 萨摩亚
SB: { lat: -8.0, lng: 159.0, name: "Solomon Islands" }, // 所罗门群岛
TO: { lat: -20.0, lng: -175.0, name: "Tonga" }, // 汤加
TV: { lat: -8.0, lng: 178.0, name: "Tuvalu" }, // 图瓦卢
VU: { lat: -16.0, lng: 167.0, name: "Vanuatu" }, // 瓦努阿图
// 非洲
DZ: { lat: 28.0, lng: 3.0, name: "Algeria" }, // 阿尔及利亚
AO: { lat: -12.5, lng: 18.5, name: "Angola" }, // 安哥拉
BJ: { lat: 9.5, lng: 2.25, name: "Benin" }, // 贝宁
BW: { lat: -22.0, lng: 24.0, name: "Botswana" }, // 博茨瓦纳
BF: { lat: 13.0, lng: -2.0, name: "Burkina Faso" }, // 布基纳法索
BI: { lat: -3.5, lng: 30.0, name: "Burundi" }, // 布隆迪
CM: { lat: 6.0, lng: 12.0, name: "Cameroon" }, // 喀麦隆
CV: { lat: 16.0, lng: -24.0, name: "Cape Verde" }, // 佛得角
CF: { lat: 7.0, lng: 21.0, name: "Central African Republic" }, // 中非共和国
TD: { lat: 15.0, lng: 19.0, name: "Chad" }, // 乍得
KM: { lat: -12.1667, lng: 44.25, name: "Comoros" }, // 科摩罗
CG: { lat: -1.0, lng: 15.0, name: "Congo" }, // 刚果
CD: { lat: 0.0, lng: 25.0, name: "Democratic Republic of the Congo" }, // 刚果民主共和国
CI: { lat: 8.0, lng: -5.0, name: "Côte d'Ivoire" }, // 科特迪瓦
DJ: { lat: 11.5, lng: 43.0, name: "Djibouti" }, // 吉布提
EG: { lat: 27.0, lng: 30.0, name: "Egypt" }, // 埃及
GQ: { lat: 2.0, lng: 10.0, name: "Equatorial Guinea" }, // 赤道几内亚
ER: { lat: 15.0, lng: 39.0, name: "Eritrea" }, // 厄立特里亚
ET: { lat: 8.0, lng: 38.0, name: "Ethiopia" }, // 埃塞俄比亚
GA: { lat: -1.0, lng: 11.75, name: "Gabon" }, // 加蓬
GM: { lat: 13.4667, lng: -16.5667, name: "Gambia" }, // 冈比亚
GH: { lat: 8.0, lng: -2.0, name: "Ghana" }, // 加纳
GN: { lat: 11.0, lng: -10.0, name: "Guinea" }, // 几内亚
GW: { lat: 12.0, lng: -15.0, name: "Guinea-Bissau" }, // 几内亚比绍
KE: { lat: 1.0, lng: 38.0, name: "Kenya" }, // 肯尼亚
LS: { lat: -29.5, lng: 28.5, name: "Lesotho" }, // 莱索托
LR: { lat: 6.5, lng: -9.5, name: "Liberia" }, // 利比里亚
LY: { lat: 25.0, lng: 17.0, name: "Libya" }, // 利比亚
MG: { lat: -20.0, lng: 47.0, name: "Madagascar" }, // 马达加斯加
MW: { lat: -13.5, lng: 34.0, name: "Malawi" }, // 马拉维
ML: { lat: 17.0, lng: -4.0, name: "Mali" }, // 马里
MR: { lat: 20.0, lng: -12.0, name: "Mauritania" }, // 毛里塔尼亚
MU: { lat: -20.2833, lng: 57.55, name: "Mauritius" }, // 毛里求斯
YT: { lat: -12.8333, lng: 45.1667, name: "Mayotte" }, // 马约特
MA: { lat: 32.0, lng: -5.0, name: "Morocco" }, // 摩洛哥
MZ: { lat: -18.25, lng: 35.0, name: "Mozambique" }, // 莫桑比克
NA: { lat: -22.0, lng: 17.0, name: "Namibia" }, // 纳米比亚
NE: { lat: 16.0, lng: 8.0, name: "Niger" }, // 尼日尔
NG: { lat: 10.0, lng: 8.0, name: "Nigeria" }, // 尼日利亚
RW: { lat: -2.0, lng: 30.0, name: "Rwanda" }, // 卢旺达
ST: { lat: 1.0, lng: 7.0, name: "São Tomé and Principe" }, // 圣多美和普林西比
SN: { lat: 14.0, lng: -14.0, name: "Senegal" }, // 塞内加尔
SC: { lat: -4.5833, lng: 55.6667, name: "Seychelles" }, // 塞舌尔
SL: { lat: 8.5, lng: -11.5, name: "Sierra Leone" }, // 塞拉利昂
SO: { lat: 10.0, lng: 49.0, name: "Somalia" }, // 索马里
ZA: { lat: -29.0, lng: 24.0, name: "South Africa" }, // 南非
SD: { lat: 15.0, lng: 30.0, name: "Sudan" }, // 苏丹
SZ: { lat: -26.5, lng: 31.5, name: "Swaziland" }, // 斯威士兰
TZ: { lat: -6.0, lng: 35.0, name: "Tanzania" }, // 坦桑尼亚
TG: { lat: 8.0, lng: 1.1667, name: "Togo" }, // 多哥
TN: { lat: 34.0, lng: 9.0, name: "Tunisia" }, // 突尼斯
UG: { lat: 1.0, lng: 32.0, name: "Uganda" }, // 乌干达
EH: { lat: 24.5, lng: -13.0, name: "Western Sahara" }, // 西撒哈拉
ZM: { lat: -15.0, lng: 30.0, name: "Zambia" }, // 赞比亚
ZW: { lat: -20.0, lng: 30.0, name: "Zimbabwe" }, // 津巴布韦
};

View File

@ -18,6 +18,11 @@
"speed": "Speed", "speed": "Speed",
"network": "Network" "network": "Network"
}, },
"map": {
"Distributions": "Servers are distributed in",
"Regions": "Regions",
"Servers": "servers"
},
"serverCard": { "serverCard": {
"mem": "MEM", "mem": "MEM",
"stg": "STG", "stg": "STG",

View File

@ -18,6 +18,11 @@
"speed": "速率", "speed": "速率",
"network": "网络" "network": "网络"
}, },
"map": {
"Distributions": "服务器分布在",
"Regions": "个区域",
"Servers": "个服务器"
},
"serverCard": { "serverCard": {
"mem": "内存", "mem": "内存",
"stg": "存储", "stg": "存储",

View File

@ -18,6 +18,11 @@
"speed": "速率", "speed": "速率",
"network": "網路" "network": "網路"
}, },
"map": {
"Distributions": "服務器分布在",
"Regions": "個區域",
"Servers": "個服務器"
},
"serverCard": { "serverCard": {
"mem": "內存", "mem": "內存",
"stg": "存儲", "stg": "存儲",

View File

@ -10,10 +10,15 @@ import GroupSwitch from "@/components/GroupSwitch";
import { ServerGroup } from "@/types/nezha-api"; import { ServerGroup } from "@/types/nezha-api";
import { useWebSocketContext } from "@/hooks/use-websocket-context"; import { useWebSocketContext } from "@/hooks/use-websocket-context";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ChartBarSquareIcon, ViewColumnsIcon } from "@heroicons/react/20/solid"; import {
ChartBarSquareIcon,
ViewColumnsIcon,
MapIcon,
} from "@heroicons/react/20/solid";
import { ServiceTracker } from "@/components/ServiceTracker"; import { ServiceTracker } from "@/components/ServiceTracker";
import ServerCardInline from "@/components/ServerCardInline"; import ServerCardInline from "@/components/ServerCardInline";
import { Loader } from "@/components/loading/Loader"; import { Loader } from "@/components/loading/Loader";
import GlobalMap from "@/components/GlobalMap";
export default function Servers() { export default function Servers() {
const { t } = useTranslation(); const { t } = useTranslation();
@ -24,6 +29,7 @@ export default function Servers() {
const { lastMessage, connected } = useWebSocketContext(); const { lastMessage, connected } = useWebSocketContext();
const [showServices, setShowServices] = useState<string>("0"); const [showServices, setShowServices] = useState<string>("0");
const [showMap, setShowMap] = useState<string>("0");
const [inline, setInline] = useState<string>("0"); const [inline, setInline] = useState<string>("0");
const [currentGroup, setCurrentGroup] = useState<string>("All"); const [currentGroup, setCurrentGroup] = useState<string>("All");
@ -144,6 +150,20 @@ export default function Servers() {
downSpeed={downSpeed} downSpeed={downSpeed}
/> />
<section className="flex mt-6 items-center gap-2 w-full overflow-hidden"> <section className="flex mt-6 items-center gap-2 w-full overflow-hidden">
<button
onClick={() => {
setShowMap(showMap === "0" ? "1" : "0");
}}
className={cn(
"rounded-[50px] text-white cursor-pointer [text-shadow:_0_1px_0_rgb(0_0_0_/_20%)] bg-blue-600 p-[10px] transition-all shadow-[inset_0_1px_0_rgba(255,255,255,0.2)] ",
{
"shadow-[inset_0_1px_0_rgba(0,0,0,0.2)] bg-blue-500":
showMap === "1",
},
)}
>
<MapIcon className="size-[13px]" />
</button>
<button <button
onClick={() => { onClick={() => {
setShowServices(showServices === "0" ? "1" : "0"); setShowServices(showServices === "0" ? "1" : "0");
@ -183,6 +203,7 @@ export default function Servers() {
setCurrentTab={setCurrentGroup} setCurrentTab={setCurrentGroup}
/> />
</section> </section>
{showMap === "1" && <GlobalMap serverList={nezhaWsData?.servers || []} />}
{showServices === "1" && <ServiceTracker />} {showServices === "1" && <ServiceTracker />}
{inline === "1" && ( {inline === "1" && (
<section className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden mt-6"> <section className="flex flex-col gap-2 overflow-x-scroll scrollbar-hidden mt-6">

View File

@ -86,5 +86,6 @@ export default defineConfig({
}, },
}, },
}, },
chunkSizeWarningLimit: 1500,
}, },
}); });