feat: 完善 Telegram 推送通知功能

- 推送模板优化:
  - 优先使用账户名称而不是钱包地址
  - 使用市场标题而不是16进制ID
  - 添加可点击的市场链接(支持 slug 和 conditionId)
  - 添加市场方向(outcome)显示
  - 数量和价格从订单详情API获取实际值
  - 失败通知只显示后端返回的msg,不显示完整堆栈

- 多语言支持:
  - 后端推送消息支持多语言(使用前端最后请求的语言)
  - 添加所有 ErrorCode 的多语言资源文件(中文简体、繁体、英文)
  - 通知消息文本全部使用多语言资源

- 功能改进:
  - 从订单详情获取实际的 side、price、size、outcome
  - 支持买入/卖出方向的多语言显示
  - 错误信息优化,只显示后端返回的错误消息
This commit is contained in:
WrBug
2025-12-05 02:20:46 +08:00
parent 41596887c9
commit 777710c2ed
32 changed files with 4059 additions and 21 deletions
@@ -0,0 +1,49 @@
import { Form, Input, Alert } from 'antd'
import { useTranslation } from 'react-i18next'
interface DiscordConfigFormProps {
form: any
}
/**
* Discord 配置表单组件
*/
const DiscordConfigForm: React.FC<DiscordConfigFormProps> = ({ form }) => {
const { t } = useTranslation()
return (
<>
<Alert
message={t('discordConfig.title')}
description={
<div style={{ fontSize: '13px', lineHeight: '1.8' }}>
<p style={{ margin: '4px 0' }}>{t('discordConfig.step1')}</p>
<p style={{ margin: '4px 0' }}>{t('discordConfig.step2')}</p>
<p style={{ margin: '4px 0' }}>{t('discordConfig.step3')}</p>
</div>
}
type="info"
showIcon
style={{ fontSize: '12px', marginBottom: 16 }}
/>
<Form.Item
label={t('notificationSettings.chatIds')}
required
>
<Form.Item
name={['config', 'webhookUrl']}
rules={[{ required: true, message: t('discordConfig.webhookUrlRequired') }]}
>
<Input
placeholder={t('discordConfig.webhookUrlPlaceholder')}
addonBefore={t('discordConfig.webhookUrl')}
/>
</Form.Item>
</Form.Item>
</>
)
}
export default DiscordConfigForm
@@ -0,0 +1,49 @@
import { Form, Input, Alert } from 'antd'
import { useTranslation } from 'react-i18next'
interface SlackConfigFormProps {
form: any
}
/**
* Slack 配置表单组件
*/
const SlackConfigForm: React.FC<SlackConfigFormProps> = ({ form }) => {
const { t } = useTranslation()
return (
<>
<Alert
message={t('slackConfig.title')}
description={
<div style={{ fontSize: '13px', lineHeight: '1.8' }}>
<p style={{ margin: '4px 0' }}>{t('slackConfig.step1')}</p>
<p style={{ margin: '4px 0' }}>{t('slackConfig.step2')}</p>
<p style={{ margin: '4px 0' }}>{t('slackConfig.step3')}</p>
</div>
}
type="info"
showIcon
style={{ fontSize: '12px', marginBottom: 16 }}
/>
<Form.Item
label={t('notificationSettings.chatIds')}
required
>
<Form.Item
name={['config', 'webhookUrl']}
rules={[{ required: true, message: t('slackConfig.webhookUrlRequired') }]}
>
<Input
placeholder={t('slackConfig.webhookUrlPlaceholder')}
addonBefore={t('slackConfig.webhookUrl')}
/>
</Form.Item>
</Form.Item>
</>
)
}
export default SlackConfigForm
@@ -0,0 +1,123 @@
import { useState } from 'react'
import { Form, Input, Alert, Button, Space, message } from 'antd'
import { ReloadOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { apiService } from '../../services/api'
interface TelegramConfigFormProps {
form: any
}
/**
* Telegram 配置表单组件
*/
const TelegramConfigForm: React.FC<TelegramConfigFormProps> = ({ form }) => {
const { t } = useTranslation()
const [loading, setLoading] = useState(false)
/**
* 自动获取 Chat IDs
*/
const handleGetChatIds = async () => {
const botToken = form.getFieldValue(['config', 'botToken'])
if (!botToken || botToken.trim() === '') {
message.warning(t('notificationSettings.getChatIdsNoToken'))
return
}
setLoading(true)
try {
const response = await apiService.notifications.getTelegramChatIds({ botToken: botToken.trim() })
if (response.data.code === 0 && response.data.data) {
const chatIds = response.data.data
if (chatIds.length > 0) {
// 获取现有的 Chat IDs
const existingChatIds = form.getFieldValue(['config', 'chatIds']) || ''
const existingArray = typeof existingChatIds === 'string'
? existingChatIds.split(',').map((id: string) => id.trim()).filter((id: string) => id)
: Array.isArray(existingChatIds) ? existingChatIds : []
// 合并并去重
const allChatIds = [...new Set([...existingArray, ...chatIds])]
form.setFieldsValue({
config: {
...form.getFieldValue('config'),
chatIds: allChatIds.join(',')
}
})
message.success(t('notificationSettings.getChatIdsSuccess', { count: chatIds.length }))
} else {
message.warning(t('notificationSettings.getChatIdsNoMessage'))
}
} else {
message.error(response.data.msg || t('notificationSettings.getChatIdsFailed'))
}
} catch (error: any) {
message.error(error.message || t('notificationSettings.getChatIdsFailed'))
} finally {
setLoading(false)
}
}
return (
<>
<Alert
message={t('telegramConfig.title')}
description={
<div style={{ fontSize: '13px', lineHeight: '1.8' }}>
<p style={{ margin: '4px 0' }} dangerouslySetInnerHTML={{ __html: `1. ${t('telegramConfig.step1')}` }} />
<p style={{ margin: '4px 0' }} dangerouslySetInnerHTML={{ __html: `2. ${t('telegramConfig.step2')}` }} />
<p style={{ margin: '4px 0' }} dangerouslySetInnerHTML={{ __html: `3. ${t('telegramConfig.step3')}` }} />
<p style={{ margin: '4px 0' }}>{t('telegramConfig.step4')}</p>
<p style={{ margin: '4px 0' }}>{t('telegramConfig.step5')}</p>
</div>
}
type="info"
showIcon
style={{ fontSize: '12px', marginBottom: 16 }}
/>
<Form.Item
label={t('notificationSettings.chatIds')}
required
>
<Form.Item
name={['config', 'botToken']}
rules={[{ required: true, message: t('telegramConfig.botTokenRequired') }]}
style={{ marginBottom: 16 }}
>
<Input.Password
placeholder={t('telegramConfig.botTokenPlaceholder')}
addonBefore={t('telegramConfig.botToken')}
addonAfter={
<Button
type="link"
size="small"
icon={<ReloadOutlined />}
loading={loading}
onClick={handleGetChatIds}
style={{ padding: 0, height: 'auto' }}
>
{t('notificationSettings.getChatIdsButton')}
</Button>
}
/>
</Form.Item>
<Form.Item
name={['config', 'chatIds']}
rules={[{ required: true, message: t('notificationSettings.chatIdsRequired') }]}
extra={t('notificationSettings.chatIdsExtra')}
>
<Input.TextArea
placeholder={t('notificationSettings.chatIdsPlaceholder')}
rows={3}
/>
</Form.Item>
</Form.Item>
</>
)
}
export default TelegramConfigForm
@@ -0,0 +1,4 @@
export { default as TelegramConfigForm } from './TelegramConfigForm'
export { default as DiscordConfigForm } from './DiscordConfigForm'
export { default as SlackConfigForm } from './SlackConfigForm'