[优化] 新增 GeoIP 测试功能,更新相关 API 路由和前端组件

This commit is contained in:
ryan
2026-03-15 14:43:28 +08:00
parent 5bb25d2203
commit 640dd6c82c
9 changed files with 327 additions and 8 deletions
@@ -2,6 +2,7 @@ import { apiRequest } from '@/lib/api/client';
import type {
BootstrapTokenPayload,
GeoIPLookupResult,
OptionItem,
SettingsProfile,
UpdateSelfPayload,
@@ -18,6 +19,13 @@ export function updateOption(key: string, value: string) {
});
}
export function lookupGeoIP(provider: string, ip: string) {
return apiRequest<GeoIPLookupResult>('/option/geoip/lookup', {
method: 'POST',
body: JSON.stringify({ provider, ip }),
});
}
export function getBootstrapToken() {
return apiRequest<BootstrapTokenPayload>('/nodes/bootstrap-token');
}
@@ -21,12 +21,14 @@ import {
getBootstrapToken,
getOptions,
getSettingsProfile,
lookupGeoIP,
rotateBootstrapToken,
updateOption,
updateSelf,
} from '@/features/settings/api/settings';
import type {
BootstrapTokenPayload,
GeoIPLookupResult,
OptionItem,
UpdateSelfPayload,
} from '@/features/settings/types';
@@ -230,6 +232,7 @@ export function SettingsPage() {
const [emailAddress, setEmailAddress] = useState('');
const [emailCode, setEmailCode] = useState('');
const [emailTurnstileToken, setEmailTurnstileToken] = useState('');
const [geoIPTestIP, setGeoIPTestIP] = useState('8.8.8.8');
const isRoot = (user?.role ?? 0) >= 100;
@@ -443,6 +446,11 @@ export function SettingsPage() {
},
});
const geoIPLookupMutation = useMutation({
mutationFn: ({ provider, ip }: { provider: string; ip: string }) =>
lookupGeoIP(provider, ip),
});
const discoveryToken = bootstrapQuery.data?.discovery_token ?? '';
const discoveryCommand =
isRoot && operationFields.ServerAddress && discoveryToken
@@ -505,6 +513,17 @@ export function SettingsPage() {
setFeedback({ tone: 'success', message: successMessage });
};
const handleGeoIPLookup = () => {
geoIPLookupMutation.reset();
geoIPLookupMutation.mutate({
provider: operationFields.GeoIPProvider,
ip: geoIPTestIP.trim(),
});
};
const geoIPLookupResult: GeoIPLookupResult | undefined =
geoIPLookupMutation.data;
const handleProfileSave = () => {
void runBusyAction('profile', async () => {
await updateSelf({
@@ -946,7 +965,10 @@ export function SettingsPage() {
[
['AgentHeartbeatInterval', String(heartbeat)],
['NodeOfflineThreshold', String(offline)],
['AgentUpdateRepo', operationFields.AgentUpdateRepo.trim()],
[
'AgentUpdateRepo',
operationFields.AgentUpdateRepo.trim(),
],
['GeoIPProvider', operationFields.GeoIPProvider],
],
'运维设置已保存。',
@@ -1006,7 +1028,8 @@ export function SettingsPage() {
Agent 更新仓库
</p>
<p className="text-sm text-[var(--foreground-muted)]">
自动更新和手动更新动作在节点页触发,这里维护 Agent 自更新使用的仓库地址。
自动更新和手动更新动作在节点页触发,这里维护 Agent
自更新使用的仓库地址。
</p>
</div>
<div className="mt-4">
@@ -1031,7 +1054,8 @@ export function SettingsPage() {
IP 归属方式
</p>
<p className="text-sm text-[var(--foreground-muted)]">
控制世界地图等场景使用的 IP 归属解析来源。选择 MaxMind 时会按需下载本地 mmdb 数据库。
控制世界地图等场景使用的 IP 归属解析来源。选择 MaxMind
时会按需下载本地 mmdb 数据库。
</p>
</div>
<div className="mt-4">
@@ -1056,6 +1080,84 @@ export function SettingsPage() {
</ResourceSelect>
</ResourceField>
</div>
<div className="mt-5 rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] p-5">
<div className="flex flex-col gap-3 lg:flex-row lg:items-end">
<ResourceField
label="测试 IP"
>
<ResourceInput
value={geoIPTestIP}
onChange={(event) =>
setGeoIPTestIP(event.target.value)
}
placeholder="例如 8.8.8.8"
/>
</ResourceField>
<PrimaryButton
type="button"
onClick={handleGeoIPLookup}
disabled={geoIPLookupMutation.isPending}
>
{geoIPLookupMutation.isPending
? '查询中...'
: '查询归属'}
</PrimaryButton>
</div>
<div className="mt-4 space-y-3">
{geoIPLookupMutation.isError ? (
<InlineMessage
tone="danger"
message={getErrorMessage(geoIPLookupMutation.error)}
/>
) : null}
{geoIPLookupResult ? (
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-base)] px-4 py-4">
<p className="text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">
查询 IP
</p>
<p className="mt-2 text-sm font-semibold text-[var(--foreground-primary)]">
{geoIPLookupResult.ip}
</p>
</div>
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-base)] px-4 py-4">
<p className="text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">
国家 / 地区
</p>
<p className="mt-2 text-sm font-semibold text-[var(--foreground-primary)]">
{geoIPLookupResult.name || '—'}
</p>
</div>
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-base)] px-4 py-4">
<p className="text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">
ISO Code
</p>
<div className="mt-2">
<StatusBadge
label={geoIPLookupResult.iso_code || '—'}
variant="info"
/>
</div>
</div>
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-base)] px-4 py-4">
<p className="text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">
经纬度
</p>
<p className="mt-2 text-sm font-semibold text-[var(--foreground-primary)]">
{geoIPLookupResult.latitude !== undefined &&
geoIPLookupResult.latitude !== null &&
geoIPLookupResult.longitude !== undefined &&
geoIPLookupResult.longitude !== null
? `${geoIPLookupResult.latitude.toFixed(4)}, ${geoIPLookupResult.longitude.toFixed(4)}`
: '—'}
</p>
</div>
</div>
) : null}
</div>
</div>
</div>
</div>
</AppCard>
@@ -1281,7 +1383,9 @@ export function SettingsPage() {
}
disabled={busyKey === 'system-general'}
>
{busyKey === 'system-general' ? '保存中...' : '保存通用设置'}
{busyKey === 'system-general'
? '保存中...'
: '保存通用设置'}
</PrimaryButton>
</div>
}
@@ -1904,9 +2008,7 @@ export function SettingsPage() {
return (
<div className="space-y-6">
<PageHeader
title="设置"
/>
<PageHeader title="设置" />
{feedback ? (
<InlineMessage tone={feedback.tone} message={feedback.message} />
@@ -9,6 +9,15 @@ export interface BootstrapTokenPayload {
discovery_token: string;
}
export interface GeoIPLookupResult {
provider: string;
ip: string;
iso_code: string;
name: string;
latitude?: number | null;
longitude?: number | null;
}
export interface UpdateSelfPayload {
username: string;
display_name: string;