feat: 实现 Builder API Key 系统级配置和 Gasless 交易支持

- 新增系统配置表(SystemConfig)用于存储 Builder API Key
- 实现 Builder API Key 的前端配置页面
- 集成 Builder Relayer API 支持 Gasless 交易
- 在 API 健康检查中添加 Builder Relayer API 检测
- 移除前端 API 测试功能,简化配置页面
- 修复赎回仓位时的 Builder API Key 检查逻辑
- 更新错误处理,引导用户配置 Builder API Key
This commit is contained in:
WrBug
2025-12-06 23:10:26 +08:00
parent 3369dbb248
commit bfbbbdd1de
27 changed files with 1971 additions and 233 deletions
@@ -0,0 +1,165 @@
import { useEffect, useState } from 'react'
import { Card, Form, Button, Input, message, Typography, Space, Alert } from 'antd'
import { SaveOutlined, LinkOutlined } from '@ant-design/icons'
import { apiService } from '../services/api'
import { useMediaQuery } from 'react-responsive'
import { useTranslation } from 'react-i18next'
import type { SystemConfig, BuilderApiKeyUpdateRequest } from '../types'
const { Title, Paragraph, Text } = Typography
const BuilderApiKeySettings: React.FC = () => {
const { t } = useTranslation()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [builderApiKeyForm] = Form.useForm()
const [builderApiKeyConfig, setBuilderApiKeyConfig] = useState<SystemConfig | null>(null)
const [builderApiKeyLoading, setBuilderApiKeyLoading] = useState(false)
useEffect(() => {
fetchBuilderApiKeyConfig()
}, [])
const fetchBuilderApiKeyConfig = async () => {
try {
const response = await apiService.systemConfig.get()
if (response.data.code === 0 && response.data.data) {
const config = response.data.data
setBuilderApiKeyConfig(config)
// 预填充字段(如果已配置,显示占位符)
builderApiKeyForm.setFieldsValue({
builderApiKey: config.builderApiKeyConfigured ? '***' : '',
builderSecret: config.builderSecretConfigured ? '***' : '',
builderPassphrase: config.builderPassphraseConfigured ? '***' : '',
})
} else {
message.error(response.data.msg || t('builderApiKey.getFailed'))
}
} catch (error: any) {
message.error(error.message || t('builderApiKey.getFailed'))
}
}
const handleBuilderApiKeySubmit = async (values: BuilderApiKeyUpdateRequest) => {
setBuilderApiKeyLoading(true)
try {
// 如果值是 '***',表示已配置但未修改,不发送
const updateData: BuilderApiKeyUpdateRequest = {}
if (values.builderApiKey && values.builderApiKey !== '***') {
updateData.builderApiKey = values.builderApiKey
}
if (values.builderSecret && values.builderSecret !== '***') {
updateData.builderSecret = values.builderSecret
}
if (values.builderPassphrase && values.builderPassphrase !== '***') {
updateData.builderPassphrase = values.builderPassphrase
}
const response = await apiService.systemConfig.updateBuilderApiKey(updateData)
if (response.data.code === 0) {
message.success(t('builderApiKey.saveSuccess'))
fetchBuilderApiKeyConfig()
} else {
message.error(response.data.msg || t('builderApiKey.saveFailed'))
}
} catch (error: any) {
message.error(error.message || t('builderApiKey.saveFailed'))
} finally {
setBuilderApiKeyLoading(false)
}
}
return (
<div>
<div style={{ marginBottom: '16px' }}>
<Title level={2} style={{ margin: 0 }}>{t('builderApiKey.title')}</Title>
</div>
<Card>
<Alert
message={t('builderApiKey.alertTitle')}
description={
<div>
<Paragraph style={{ marginBottom: '8px' }}>
{t('builderApiKey.description')}
</Paragraph>
<Paragraph style={{ marginBottom: '8px' }}>
<Text strong>{t('builderApiKey.purposeTitle')}</Text>
<ul style={{ marginTop: '8px', marginBottom: 0, paddingLeft: '20px' }}>
<li>{t('builderApiKey.purpose1')}</li>
<li>{t('builderApiKey.purpose2')}</li>
<li>{t('builderApiKey.purpose3')}</li>
</ul>
</Paragraph>
<Paragraph style={{ marginBottom: 0 }}>
<Text strong>{t('builderApiKey.getApiKey')}</Text>
<Space style={{ marginLeft: '8px' }}>
<a
href="https://polymarket.com/settings?tab=builder"
target="_blank"
rel="noopener noreferrer"
>
<LinkOutlined /> {t('builderApiKey.openSettings')}
</a>
</Space>
</Paragraph>
</div>
}
type="info"
showIcon
style={{ marginBottom: '24px' }}
/>
<Form
form={builderApiKeyForm}
layout="vertical"
onFinish={handleBuilderApiKeySubmit}
size={isMobile ? 'middle' : 'large'}
>
<Form.Item
label={t('builderApiKey.apiKey')}
name="builderApiKey"
help={builderApiKeyConfig?.builderApiKeyConfigured ? t('builderApiKey.apiKeyHelp') : t('builderApiKey.apiKeyPlaceholder')}
>
<Input
placeholder={builderApiKeyConfig?.builderApiKeyConfigured ? t('builderApiKey.apiKeyHelp') : t('builderApiKey.apiKeyPlaceholder')}
/>
</Form.Item>
<Form.Item
label={t('builderApiKey.secret')}
name="builderSecret"
help={builderApiKeyConfig?.builderSecretConfigured ? t('builderApiKey.secretHelp') : t('builderApiKey.secretPlaceholder')}
>
<Input
placeholder={builderApiKeyConfig?.builderSecretConfigured ? t('builderApiKey.secretHelp') : t('builderApiKey.secretPlaceholder')}
/>
</Form.Item>
<Form.Item
label={t('builderApiKey.passphrase')}
name="builderPassphrase"
help={builderApiKeyConfig?.builderPassphraseConfigured ? t('builderApiKey.passphraseHelp') : t('builderApiKey.passphrasePlaceholder')}
>
<Input
placeholder={builderApiKeyConfig?.builderPassphraseConfigured ? t('builderApiKey.passphraseHelp') : t('builderApiKey.passphrasePlaceholder')}
/>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
icon={<SaveOutlined />}
loading={builderApiKeyLoading}
>
{t('common.save') || '保存配置'}
</Button>
</Form.Item>
</Form>
</Card>
</div>
)
}
export default BuilderApiKeySettings
+28 -2
View File
@@ -1,6 +1,7 @@
import { useEffect, useState, useMemo } from 'react'
import { Card, Table, Tag, message, Space, Input, Radio, Select, Button, Row, Col, Empty, Modal, Form, Descriptions } from 'antd'
import { SearchOutlined, AppstoreOutlined, UnorderedListOutlined, UpOutlined, DownOutlined } from '@ant-design/icons'
import { useNavigate } from 'react-router-dom'
import { apiService } from '../services/api'
import type { AccountPosition, Account, PositionPushMessage, PositionSellRequest, MarketPriceResponse, RedeemablePositionsSummary, PositionRedeemRequest } from '../types'
import { getPositionKey } from '../types'
@@ -13,6 +14,7 @@ type PositionFilter = 'current' | 'historical'
type ViewMode = 'card' | 'list'
const PositionList: React.FC = () => {
const navigate = useNavigate()
const isMobile = useMediaQuery({ maxWidth: 768 })
const [currentPositions, setCurrentPositions] = useState<AccountPosition[]>([])
const [historyPositions, setHistoryPositions] = useState<AccountPosition[]>([])
@@ -113,10 +115,34 @@ const PositionList: React.FC = () => {
// 刷新可赎回统计
await fetchRedeemableSummary()
} else {
message.error(response.data.msg || '赎回失败')
// 检查是否是 Builder API Key 未配置的错误
if (response.data.code === 2014 || response.data.msg?.includes('Builder API Key 未配置')) {
message.error({
content: response.data.msg || 'Builder API Key 未配置',
duration: 5,
})
// 延迟跳转,让用户看到错误消息
setTimeout(() => {
navigate('/system-settings/builder-api-key')
}, 1500)
} else {
message.error(response.data.msg || '赎回失败')
}
}
} catch (error: any) {
message.error('赎回失败: ' + (error.message || '未知错误'))
// 检查是否是 Builder API Key 未配置的错误
if (error.response?.data?.code === 2014 || error.message?.includes('Builder API Key 未配置')) {
message.error({
content: error.response?.data?.msg || error.message || 'Builder API Key 未配置,请前往系统设置页面配置',
duration: 5,
})
// 延迟跳转,让用户看到错误消息
setTimeout(() => {
navigate('/system-settings/builder-api-key')
}, 1500)
} else {
message.error('赎回失败: ' + (error.message || '未知错误'))
}
} finally {
setRedeeming(false)
}