feat(settings): add GeoIP provider selection to settings page

- Introduced a new ResourceSelect component for selecting the GeoIP provider.
- Added options for disabling GeoIP, using MaxMind mmdb, and various external GeoIP services.
- Implemented saving functionality for the selected GeoIP provider with a corresponding button.
- Updated default operation fields to include GeoIPProvider with a default value of 'disabled'.
This commit is contained in:
ryan
2026-03-14 15:19:46 +08:00
parent f6dd7df55c
commit 4a50762092
3 changed files with 99 additions and 27 deletions
@@ -9,6 +9,7 @@ import { useEffect, useMemo, useState } from 'react';
import { EmptyState } from '@/components/feedback/empty-state';
import { useTheme } from '@/components/providers/theme-provider';
import worldGeoJson from '@/features/dashboard/data/world-geo.json';
import { StatusBadge } from '@/components/ui/status-badge';
import type {
DashboardConfig,
@@ -71,6 +72,56 @@ type MapNodeDatum = {
};
};
type WorldGeoJson = {
type: string;
features?: unknown[];
};
let worldMapRegistrationAttempted = false;
let worldMapRegistrationSucceeded = false;
let worldMapRegistrationError: Error | null = null;
function ensureWorldMapRegistered() {
if (worldMapRegistrationSucceeded || echarts.getMap('world')) {
worldMapRegistrationSucceeded = true;
worldMapRegistrationError = null;
return true;
}
if (worldMapRegistrationAttempted) {
return false;
}
worldMapRegistrationAttempted = true;
try {
const geoJson = worldGeoJson as WorldGeoJson;
if (geoJson.type !== 'FeatureCollection' || !Array.isArray(geoJson.features)) {
throw new Error('invalid world geojson payload');
}
echarts.registerMap(
'world',
geoJson as unknown as Parameters<typeof echarts.registerMap>[1],
);
if (!echarts.getMap('world')) {
throw new Error('world map registration failed');
}
worldMapRegistrationSucceeded = true;
worldMapRegistrationError = null;
return true;
} catch (error) {
worldMapRegistrationSucceeded = false;
worldMapRegistrationError =
error instanceof Error ? error : new Error('unknown world map registration error');
console.error('Failed to register ECharts world map', worldMapRegistrationError);
return false;
}
}
function formatPercent(value: number) {
if (!Number.isFinite(value)) {
return '0%';
@@ -271,32 +322,9 @@ export function WorldStage({
const [mapFailed, setMapFailed] = useState(false);
useEffect(() => {
let disposed = false;
import('@/features/dashboard/data/world-geo.json')
.then(({ default: worldGeoJson }) => {
if (!echarts.getMap('world')) {
echarts.registerMap('world', worldGeoJson);
}
if (!echarts.getMap('world')) {
throw new Error('world map registration failed');
}
if (!disposed) {
setMapReady(true);
setMapFailed(false);
}
})
.catch((error) => {
console.error('Failed to register ECharts world map', error);
if (!disposed) {
setMapReady(false);
setMapFailed(true);
}
});
return () => {
disposed = true;
};
const ready = ensureWorldMapRegistered();
setMapReady(ready);
setMapFailed(!ready);
}, []);
const onlineRate =
File diff suppressed because one or more lines are too long
@@ -35,6 +35,7 @@ import {
PrimaryButton,
ResourceField,
ResourceInput,
ResourceSelect,
ResourceTextarea,
SecondaryButton,
ToggleField,
@@ -71,6 +72,7 @@ const defaultOperationFields = {
AgentHeartbeatInterval: '10000',
NodeOfflineThreshold: '120000',
AgentUpdateRepo: 'Rain-kl/ATSFlare',
GeoIPProvider: 'disabled',
OpenRestyWorkerProcesses: 'auto',
OpenRestyWorkerConnections: '4096',
OpenRestyWorkerRlimitNofile: '65535',
@@ -337,6 +339,7 @@ export function SettingsPage() {
AgentHeartbeatInterval: optionMap.AgentHeartbeatInterval ?? '10000',
NodeOfflineThreshold: optionMap.NodeOfflineThreshold ?? '120000',
AgentUpdateRepo: optionMap.AgentUpdateRepo ?? 'Rain-kl/ATSFlare',
GeoIPProvider: optionMap.GeoIPProvider ?? 'disabled',
OpenRestyWorkerProcesses: optionMap.OpenRestyWorkerProcesses ?? 'auto',
OpenRestyWorkerConnections:
optionMap.OpenRestyWorkerConnections ?? '4096',
@@ -1040,6 +1043,47 @@ export function SettingsPage() {
/>
</ResourceField>
</AppCard>
<AppCard
title="IP 归属方式"
description="控制世界地图等场景使用的 IP 归属解析来源。选择 MaxMind 时会按需下载本地 mmdb 数据库。"
action={
<PrimaryButton
type="button"
onClick={() =>
void runBusyAction('operation-geoip', async () => {
await saveOptionEntries(
[['GeoIPProvider', operationFields.GeoIPProvider]],
'IP 归属方式已保存。',
);
})
}
disabled={busyKey === 'operation-geoip'}
>
{busyKey === 'operation-geoip' ? '保存中...' : '保存归属方式'}
</PrimaryButton>
}
>
<ResourceField
label="归属方式"
hint="disabled 关闭解析;mmdb 使用本地数据库;其余选项调用外部 GeoIP 服务。"
>
<ResourceSelect
value={operationFields.GeoIPProvider}
onChange={(event) =>
setOperationFields((previous) => ({
...previous,
GeoIPProvider: event.target.value,
}))
}
>
<option value="disabled">关闭</option>
<option value="mmdb">MaxMind mmdb</option>
<option value="ip-api">ip-api.com</option>
<option value="geojs">geojs.io</option>
<option value="ipinfo">ipinfo.io</option>
</ResourceSelect>
</ResourceField>
</AppCard>
<AppCard
title="Discovery Token 与部署命令"
description="适用于新节点首次接入。可直接复制一键安装命令。"