feat: 添加系统管理功能(代理配置和API健康检查)
- 新增代理配置管理功能,支持HTTP代理配置(host、port、用户名、密码) - 代理配置存储在数据库中,支持实时生效,无需重启服务 - 代理配置变更时自动重连WebSocket连接(订单推送和跟单服务) - 新增API健康状态检查功能,监控Polymarket各API和Polygon RPC的可用性 - 优化API健康状态UI,支持响应式设计(移动端和桌面端) - 将Ethereum RPC相关配置和代码统一改为Polygon RPC - 修复代理检查时的SSL证书验证问题 - 优化代理配置:关闭代理时保留配置信息,避免重新输入
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Form, Button, Switch, Input, InputNumber, message, Typography, Space, Alert, Badge, Spin, Row, Col } from 'antd'
|
||||
import { SaveOutlined, CheckCircleOutlined, ReloadOutlined, GlobalOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
interface ProxyConfig {
|
||||
id?: number
|
||||
type: string
|
||||
enabled: boolean
|
||||
host?: string
|
||||
port?: number
|
||||
username?: string
|
||||
subscriptionUrl?: string
|
||||
lastSubscriptionUpdate?: number
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
interface ProxyCheckResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
responseTime?: number
|
||||
latency?: number // 延迟(毫秒)
|
||||
}
|
||||
|
||||
interface ApiHealthStatus {
|
||||
name: string
|
||||
url: string
|
||||
status: string
|
||||
message: string
|
||||
responseTime?: number
|
||||
}
|
||||
|
||||
const SystemSettings: React.FC = () => {
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [checking, setChecking] = useState(false)
|
||||
const [checkResult, setCheckResult] = useState<ProxyCheckResponse | null>(null)
|
||||
const [currentConfig, setCurrentConfig] = useState<ProxyConfig | null>(null)
|
||||
const [apiHealthStatus, setApiHealthStatus] = useState<ApiHealthStatus[]>([])
|
||||
const [checkingApiHealth, setCheckingApiHealth] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig()
|
||||
checkApiHealth()
|
||||
}, [])
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const response = await apiService.proxyConfig.get()
|
||||
if (response.data.code === 0) {
|
||||
const data = response.data.data
|
||||
setCurrentConfig(data)
|
||||
if (data) {
|
||||
form.setFieldsValue({
|
||||
enabled: data.enabled,
|
||||
host: data.host || '',
|
||||
port: data.port || undefined,
|
||||
username: data.username || '',
|
||||
password: '', // 密码不预填充
|
||||
})
|
||||
} else {
|
||||
form.resetFields()
|
||||
}
|
||||
} else {
|
||||
message.error(response.data.msg || '获取代理配置失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取代理配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await apiService.proxyConfig.saveHttp({
|
||||
enabled: values.enabled || false,
|
||||
host: values.host,
|
||||
port: values.port,
|
||||
username: values.username || undefined,
|
||||
password: values.password || undefined, // 如果密码为空,则不更新密码
|
||||
})
|
||||
if (response.data.code === 0) {
|
||||
message.success('保存代理配置成功。新配置将立即生效,已建立的 WebSocket 连接需要重新连接才能使用新代理。')
|
||||
fetchConfig()
|
||||
setCheckResult(null) // 清除检查结果
|
||||
// 自动刷新 API 健康状态,验证新配置是否生效
|
||||
setTimeout(() => {
|
||||
checkApiHealth()
|
||||
}, 1000)
|
||||
} else {
|
||||
message.error(response.data.msg || '保存代理配置失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '保存代理配置失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCheck = async () => {
|
||||
setChecking(true)
|
||||
setCheckResult(null)
|
||||
try {
|
||||
const response = await apiService.proxyConfig.check()
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const result = response.data.data
|
||||
setCheckResult(result)
|
||||
if (result.success) {
|
||||
message.success(`代理检查成功:${result.message}${result.responseTime ? ` (响应时间: ${result.responseTime}ms)` : ''}`)
|
||||
} else {
|
||||
message.warning(`代理检查失败:${result.message}`)
|
||||
}
|
||||
} else {
|
||||
message.error(response.data.msg || '代理检查失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '代理检查失败')
|
||||
} finally {
|
||||
setChecking(false)
|
||||
}
|
||||
}
|
||||
|
||||
const checkApiHealth = async () => {
|
||||
setCheckingApiHealth(true)
|
||||
try {
|
||||
const response = await apiService.proxyConfig.checkApiHealth()
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setApiHealthStatus(response.data.data.apis)
|
||||
} else {
|
||||
message.error(response.data.msg || 'API 健康检查失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || 'API 健康检查失败')
|
||||
} finally {
|
||||
setCheckingApiHealth(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
if (status === 'success') {
|
||||
return '#52c41a'
|
||||
} else if (status === 'skipped') {
|
||||
return '#999'
|
||||
} else {
|
||||
return '#ff4d4f'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
if (status === 'success') {
|
||||
return '正常'
|
||||
} else if (status === 'skipped') {
|
||||
return '未配置'
|
||||
} else {
|
||||
return '异常'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Title level={2} style={{ margin: 0 }}>系统管理</Title>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<GlobalOutlined />
|
||||
<span>API 健康状态</span>
|
||||
</Space>
|
||||
}
|
||||
style={{ marginBottom: '16px' }}
|
||||
extra={
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={checkApiHealth}
|
||||
loading={checkingApiHealth}
|
||||
size="small"
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Spin spinning={checkingApiHealth}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{apiHealthStatus.map((item, index) => (
|
||||
<Col
|
||||
key={index}
|
||||
xs={24}
|
||||
sm={12}
|
||||
md={12}
|
||||
lg={8}
|
||||
xl={6}
|
||||
>
|
||||
{isMobile ? (
|
||||
// 移动端:一行显示,只保留名称、延迟、状态点(不显示文字)
|
||||
<Card
|
||||
size="small"
|
||||
style={{
|
||||
borderLeft: `4px solid ${getStatusColor(item.status)}`,
|
||||
}}
|
||||
bodyStyle={{ padding: '12px' }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '8px' }}>
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Space>
|
||||
{item.responseTime !== undefined && item.responseTime !== null && (
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
<Text strong style={{ color: '#1890ff' }}>{item.responseTime}ms</Text>
|
||||
</Text>
|
||||
)}
|
||||
<Badge
|
||||
status={item.status === 'success' ? 'success' : item.status === 'skipped' ? 'default' : 'error'}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
// 桌面端:保持原有卡片布局
|
||||
<Card
|
||||
size="small"
|
||||
style={{
|
||||
borderLeft: `4px solid ${getStatusColor(item.status)}`,
|
||||
height: '100%'
|
||||
}}
|
||||
bodyStyle={{ padding: '16px' }}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Text strong style={{ fontSize: '14px' }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Badge
|
||||
status={item.status === 'success' ? 'success' : item.status === 'skipped' ? 'default' : 'error'}
|
||||
text={getStatusText(item.status)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text type="secondary" style={{ fontSize: '12px', wordBreak: 'break-all' }}>
|
||||
{item.url}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{item.message && item.message !== '连接成功' && (
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text
|
||||
type={item.status === 'success' ? 'success' : item.status === 'skipped' ? 'secondary' : 'danger'}
|
||||
style={{ fontSize: '13px' }}
|
||||
>
|
||||
{item.message}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.responseTime !== undefined && item.responseTime !== null && (
|
||||
<div style={{ marginTop: '8px', display: 'flex', alignItems: 'center' }}>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
延迟: <Text strong style={{ color: '#1890ff' }}>{item.responseTime}ms</Text>
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{apiHealthStatus.length === 0 && !checkingApiHealth && (
|
||||
<div style={{ textAlign: 'center', padding: '40px 0', color: '#999' }}>
|
||||
<Text type="secondary">暂无 API 状态信息</Text>
|
||||
</div>
|
||||
)}
|
||||
</Spin>
|
||||
</Card>
|
||||
|
||||
<Card title="代理设置" style={{ marginBottom: '16px' }}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
>
|
||||
<Form.Item
|
||||
label="启用代理"
|
||||
name="enabled"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="代理主机"
|
||||
name="host"
|
||||
rules={[
|
||||
{ required: true, message: '请输入代理主机地址' },
|
||||
{ pattern: /^[\w\.-]+$/, message: '请输入有效的主机地址' }
|
||||
]}
|
||||
>
|
||||
<Input placeholder="例如:127.0.0.1 或 proxy.example.com" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="代理端口"
|
||||
name="port"
|
||||
rules={[
|
||||
{ required: true, message: '请输入代理端口' },
|
||||
{ type: 'number', min: 1, max: 65535, message: '端口必须在 1-65535 之间' }
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={65535}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="例如:8888"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="代理用户名(可选)"
|
||||
name="username"
|
||||
>
|
||||
<Input placeholder="如果代理需要认证,请输入用户名" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="代理密码(可选)"
|
||||
name="password"
|
||||
help={currentConfig ? "留空则不更新密码,输入新密码则更新" : "如果代理需要认证,请输入密码"}
|
||||
>
|
||||
<Input.Password placeholder={currentConfig ? "留空则不更新密码" : "如果代理需要认证,请输入密码"} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={<SaveOutlined />}
|
||||
loading={loading}
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
<Button
|
||||
icon={<CheckCircleOutlined />}
|
||||
onClick={handleCheck}
|
||||
loading={checking}
|
||||
>
|
||||
检查代理
|
||||
</Button>
|
||||
{checkResult && (
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={fetchConfig}
|
||||
>
|
||||
刷新配置
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{checkResult && (
|
||||
<Alert
|
||||
type={checkResult.success ? 'success' : 'error'}
|
||||
message={checkResult.success ? '代理检查成功' : '代理检查失败'}
|
||||
description={
|
||||
<div>
|
||||
<Text>{checkResult.message}</Text>
|
||||
{(checkResult.responseTime !== undefined || checkResult.latency !== undefined) && (
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text type="secondary">
|
||||
延迟: {(checkResult.latency ?? checkResult.responseTime) ?? 0}ms
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
style={{ marginTop: '16px' }}
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SystemSettings
|
||||
|
||||
Reference in New Issue
Block a user