feat: SOCKS5代理支持、落地IP国家查询、设置页优化

- 代理支持SOCKS5和HTTP两种类型切换
- 落地IP查询显示国家、城市、ISP信息
- 设置页面不再隐藏已配置的值
- Airwallex API异常统一返回400+详细错误信息
This commit is contained in:
zqq61
2026-03-16 00:02:59 +08:00
parent c28090e75d
commit 01773500af
5 changed files with 107 additions and 45 deletions

View File

@@ -1,15 +1,23 @@
import { useState, useEffect } from 'react'
import { Card, Form, Input, InputNumber, Button, Divider, message, Space, Descriptions, Tag } from 'antd'
import { Card, Form, Input, InputNumber, Button, Divider, message, Space, Descriptions, Tag, Select } from 'antd'
import { SaveOutlined, ApiOutlined, GlobalOutlined } from '@ant-design/icons'
import { settingsApi } from '@/services/api'
import { settingsApi, getErrorMsg } from '@/services/api'
interface ProxyTestResult {
success: boolean
proxy_configured: boolean
proxy_url: string | null
proxy_ip: string | null
proxy_country: string | null
proxy_country_code: string | null
proxy_city: string | null
proxy_isp: string | null
proxy_status: string | null
direct_ip: string | null
direct_country: string | null
direct_country_code: string | null
direct_city: string | null
direct_isp: string | null
direct_status: string | null
}
@@ -28,14 +36,12 @@ export default function Settings() {
const data: Record<string, string> = {}
if (Array.isArray(res.data)) {
for (const item of res.data as { key: string; value: string }[]) {
// Don't fill masked values into password fields
if (item.value === '********') continue
data[item.key] = item.value
}
}
form.setFieldsValue(data)
} catch {
message.error('获取设置失败')
} catch (err) {
message.error(getErrorMsg(err, '获取设置失败'))
} finally {
setLoading(false)
}
@@ -55,8 +61,8 @@ export default function Settings() {
.map(([key, value]) => ({ key, value: String(value) }))
await settingsApi.updateSettings(updates)
message.success('设置已保存')
} catch {
message.error('保存设置失败')
} catch (err) {
message.error(getErrorMsg(err, '保存设置失败'))
} finally {
setSaving(false)
}
@@ -71,8 +77,8 @@ export default function Settings() {
} else {
message.error(res.data.message || '连接测试失败')
}
} catch {
message.error('连接测试失败')
} catch (err) {
message.error(getErrorMsg(err, '连接测试失败'))
} finally {
setTesting(false)
}
@@ -89,13 +95,42 @@ export default function Settings() {
} else {
message.warning('代理测试失败,请检查配置')
}
} catch {
message.error('代理测试请求失败')
} catch (err) {
message.error(getErrorMsg(err, '代理测试请求失败'))
} finally {
setTestingProxy(false)
}
}
const renderIpInfo = (label: string, prefix: string) => {
if (!proxyResult) return null
const ip = proxyResult[`${prefix}_ip` as keyof ProxyTestResult] as string | null
const country = proxyResult[`${prefix}_country` as keyof ProxyTestResult] as string | null
const countryCode = proxyResult[`${prefix}_country_code` as keyof ProxyTestResult] as string | null
const city = proxyResult[`${prefix}_city` as keyof ProxyTestResult] as string | null
const isp = proxyResult[`${prefix}_isp` as keyof ProxyTestResult] as string | null
const status = proxyResult[`${prefix}_status` as keyof ProxyTestResult] as string | null
if (status !== 'ok') {
return (
<Descriptions.Item label={label}>
<Tag color="red">{status}</Tag>
</Descriptions.Item>
)
}
return (
<Descriptions.Item label={label}>
<Space direction="vertical" size={2}>
<span><strong>IP:</strong> <Tag color="green">{ip}</Tag></span>
<span><strong>:</strong> {country} ({countryCode})</span>
{city && <span><strong>:</strong> {city}</span>}
{isp && <span><strong>ISP:</strong> {isp}</span>}
</Space>
</Descriptions.Item>
)
}
return (
<div>
<div className="page-header">
@@ -115,7 +150,7 @@ export default function Settings() {
name="airwallex_api_key"
rules={[{ required: true, message: '请输入 API Key' }]}
>
<Input.Password placeholder="请输入 API Key(留空则保持不变)" />
<Input.Password placeholder="请输入 API Key" />
</Form.Item>
<Form.Item label="Base URL" name="airwallex_base_url">
<Input placeholder="https://api-demo.airwallex.com/" />
@@ -130,6 +165,12 @@ export default function Settings() {
</Card>
<Card title="代理设置" style={{ marginBottom: 16 }}>
<Form.Item label="代理类型" name="proxy_type" initialValue="socks5">
<Select>
<Select.Option value="socks5">SOCKS5</Select.Option>
<Select.Option value="http">HTTP</Select.Option>
</Select>
</Form.Item>
<Form.Item label="代理 IP" name="proxy_ip">
<Input placeholder="例如: 127.0.0.1" />
</Form.Item>
@@ -140,7 +181,7 @@ export default function Settings() {
<Input placeholder="可选" />
</Form.Item>
<Form.Item label="代理密码" name="proxy_password">
<Input.Password placeholder="可选(留空则保持不变)" />
<Input.Password placeholder="可选" />
</Form.Item>
<Space>
<Button
@@ -158,18 +199,8 @@ export default function Settings() {
? <Tag color="blue">{proxyResult.proxy_url}</Tag>
: <Tag color="default"></Tag>}
</Descriptions.Item>
{proxyResult.proxy_ip !== undefined && (
<Descriptions.Item label="代理落地 IP">
{proxyResult.proxy_status === 'ok'
? <Tag color="green">{proxyResult.proxy_ip}</Tag>
: <Tag color="red">{proxyResult.proxy_status}</Tag>}
</Descriptions.Item>
)}
<Descriptions.Item label="服务器直连 IP">
{proxyResult.direct_status === 'ok'
? <Tag color="green">{proxyResult.direct_ip}</Tag>
: <Tag color="red">{proxyResult.direct_status}</Tag>}
</Descriptions.Item>
{proxyResult.proxy_ip !== undefined && renderIpInfo('代理落地', 'proxy')}
{renderIpInfo('服务器直连', 'direct')}
</Descriptions>
)}
</Card>