feat: 完善 Telegram 推送通知功能
- 推送模板优化: - 优先使用账户名称而不是钱包地址 - 使用市场标题而不是16进制ID - 添加可点击的市场链接(支持 slug 和 conditionId) - 添加市场方向(outcome)显示 - 数量和价格从订单详情API获取实际值 - 失败通知只显示后端返回的msg,不显示完整堆栈 - 多语言支持: - 后端推送消息支持多语言(使用前端最后请求的语言) - 添加所有 ErrorCode 的多语言资源文件(中文简体、繁体、英文) - 通知消息文本全部使用多语言资源 - 功能改进: - 从订单详情获取实际的 side、price、size、outcome - 支持买入/卖出方向的多语言显示 - 错误信息优化,只显示后端返回的错误消息
This commit is contained in:
@@ -32,6 +32,7 @@ import SystemSettings from './pages/SystemSettings'
|
||||
import LanguageSettings from './pages/LanguageSettings'
|
||||
import ApiHealthStatus from './pages/ApiHealthStatus'
|
||||
import ProxySettings from './pages/ProxySettings'
|
||||
import NotificationSettings from './pages/NotificationSettings'
|
||||
import { wsManager } from './services/websocket'
|
||||
import type { OrderPushMessage } from './types'
|
||||
import { apiService } from './services/api'
|
||||
@@ -247,6 +248,7 @@ function App() {
|
||||
<Route path="/system-settings/language" element={<ProtectedRoute><LanguageSettings /></ProtectedRoute>} />
|
||||
<Route path="/system-settings/api-health" element={<ProtectedRoute><ApiHealthStatus /></ProtectedRoute>} />
|
||||
<Route path="/system-settings/proxy" element={<ProtectedRoute><ProxySettings /></ProtectedRoute>} />
|
||||
<Route path="/system-settings/notifications" element={<ProtectedRoute><NotificationSettings /></ProtectedRoute>} />
|
||||
|
||||
{/* 默认重定向到登录页 */}
|
||||
<Route path="*" element={<Navigate to="/login" replace />} />
|
||||
|
||||
@@ -18,12 +18,14 @@ import {
|
||||
GithubOutlined,
|
||||
TwitterOutlined,
|
||||
GlobalOutlined,
|
||||
CheckCircleOutlined
|
||||
CheckCircleOutlined,
|
||||
NotificationOutlined
|
||||
} from '@ant-design/icons'
|
||||
import type { MenuProps } from 'antd'
|
||||
import type { ReactNode } from 'react'
|
||||
import { removeToken } from '../utils'
|
||||
import { wsManager } from '../services/websocket'
|
||||
import Logo from './Logo'
|
||||
|
||||
const { Header, Content, Sider } = AntLayout
|
||||
|
||||
@@ -133,6 +135,11 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
key: '/system-settings/proxy',
|
||||
icon: <LinkOutlined />,
|
||||
label: t('menu.proxy')
|
||||
},
|
||||
{
|
||||
key: '/system-settings/notifications',
|
||||
icon: <NotificationOutlined />,
|
||||
label: t('menu.notifications')
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -198,9 +205,10 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<div style={{ color: '#fff', fontSize: '18px', fontWeight: 'bold' }}>
|
||||
PolyHermes
|
||||
</div>
|
||||
<Logo
|
||||
size="normal"
|
||||
darkMode={true}
|
||||
/>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<a
|
||||
href="https://github.com/WrBug/PolyHermes"
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import React from 'react'
|
||||
|
||||
interface LogoProps {
|
||||
/**
|
||||
* Logo 尺寸
|
||||
* @default 'normal'
|
||||
*/
|
||||
size?: 'small' | 'normal' | 'large'
|
||||
/**
|
||||
* 是否只显示图标(不显示文字)
|
||||
* @default false
|
||||
*/
|
||||
iconOnly?: boolean
|
||||
/**
|
||||
* 是否使用深色模式(用于深色背景)
|
||||
* @default false
|
||||
*/
|
||||
darkMode?: boolean
|
||||
/**
|
||||
* 自定义样式类名
|
||||
*/
|
||||
className?: string
|
||||
/**
|
||||
* 自定义样式
|
||||
*/
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
/**
|
||||
* PolyHermes Logo 组件
|
||||
*
|
||||
* 设计理念:
|
||||
* - Hermes(赫尔墨斯)是希腊神话中的信使神,代表快速传递和连接
|
||||
* - 结合交易元素(箭头、连接线)体现跟单交易的核心功能
|
||||
* - 现代简洁的设计风格,适配深色和浅色背景
|
||||
*/
|
||||
const Logo: React.FC<LogoProps> = ({
|
||||
size = 'normal',
|
||||
iconOnly = false,
|
||||
darkMode = false,
|
||||
className = '',
|
||||
style = {}
|
||||
}) => {
|
||||
// 根据尺寸确定图标大小
|
||||
const iconSizes = {
|
||||
small: 24,
|
||||
normal: 32,
|
||||
large: 48
|
||||
}
|
||||
|
||||
// 根据尺寸确定文字大小
|
||||
const textSizes = {
|
||||
small: 14,
|
||||
normal: 18,
|
||||
large: 24
|
||||
}
|
||||
|
||||
const iconSize = iconSizes[size]
|
||||
const textSize = textSizes[size]
|
||||
|
||||
// 根据深色模式选择颜色
|
||||
const gradientColors = darkMode
|
||||
? { start: '#69c0ff', end: '#b37feb' } // 深色背景使用较亮的颜色
|
||||
: { start: '#1890ff', end: '#722ed1' } // 浅色背景使用标准颜色
|
||||
|
||||
const textColor = darkMode ? '#fff' : 'inherit'
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`polyhermes-logo ${className}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
...style
|
||||
}}
|
||||
>
|
||||
{/* Logo 图标 */}
|
||||
<svg
|
||||
width={iconSize}
|
||||
height={iconSize}
|
||||
viewBox="0 0 64 64"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{/* 渐变定义 */}
|
||||
<defs>
|
||||
<linearGradient id={`logoGradient-${darkMode ? 'dark' : 'light'}`} x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stopColor={gradientColors.start} />
|
||||
<stop offset="100%" stopColor={gradientColors.end} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
{/* 主图标设计:双箭头连接,代表跟单交易 */}
|
||||
{/* 左侧箭头(指向中心) */}
|
||||
<path
|
||||
d="M 16 32 L 8 24 L 8 40 Z"
|
||||
fill={`url(#logoGradient-${darkMode ? 'dark' : 'light'})`}
|
||||
/>
|
||||
|
||||
{/* 中心连接线(代表跟单连接) */}
|
||||
<line
|
||||
x1="20"
|
||||
y1="32"
|
||||
x2="44"
|
||||
y2="32"
|
||||
stroke={`url(#logoGradient-${darkMode ? 'dark' : 'light'})`}
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
|
||||
{/* 右侧箭头(指向中心) */}
|
||||
<path
|
||||
d="M 48 32 L 56 24 L 56 40 Z"
|
||||
fill={`url(#logoGradient-${darkMode ? 'dark' : 'light'})`}
|
||||
/>
|
||||
|
||||
{/* 中心圆点(代表交易节点/数据同步点) */}
|
||||
<circle
|
||||
cx="32"
|
||||
cy="32"
|
||||
r="5"
|
||||
fill={`url(#logoGradient-${darkMode ? 'dark' : 'light'})`}
|
||||
/>
|
||||
|
||||
{/* 装饰性数据流弧线(代表实时数据同步) */}
|
||||
<path
|
||||
d="M 20 20 Q 32 14 44 20"
|
||||
stroke={`url(#logoGradient-${darkMode ? 'dark' : 'light'})`}
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
opacity="0.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M 20 44 Q 32 50 44 44"
|
||||
stroke={`url(#logoGradient-${darkMode ? 'dark' : 'light'})`}
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
opacity="0.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{/* Logo 文字 */}
|
||||
{!iconOnly && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: `${textSize}px`,
|
||||
fontWeight: 'bold',
|
||||
background: darkMode
|
||||
? 'linear-gradient(135deg, #69c0ff 0%, #b37feb 100%)'
|
||||
: 'linear-gradient(135deg, #1890ff 0%, #722ed1 100%)',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
backgroundClip: 'text',
|
||||
letterSpacing: '0.5px',
|
||||
color: textColor
|
||||
}}
|
||||
>
|
||||
PolyHermes
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Logo
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -203,6 +203,7 @@
|
||||
"language": "Language",
|
||||
"apiHealth": "API Health",
|
||||
"proxy": "Proxy",
|
||||
"notifications": "Notifications",
|
||||
"logout": "Logout",
|
||||
"logoutConfirm": "Confirm Logout",
|
||||
"logoutConfirmDesc": "Are you sure you want to logout?",
|
||||
@@ -428,5 +429,75 @@
|
||||
"deleteSuccess": "Copy trading deleted successfully",
|
||||
"deleteFailed": "Failed to delete copy trading",
|
||||
"deleteConfirm": "Are you sure you want to delete this copy trading relationship?"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"title": "Notification Settings",
|
||||
"addConfig": "Add Configuration",
|
||||
"editConfig": "Edit Configuration",
|
||||
"configName": "Configuration Name",
|
||||
"configNamePlaceholder": "e.g., My Telegram Bot",
|
||||
"configNameRequired": "Please enter configuration name",
|
||||
"type": "Notification Type",
|
||||
"typeRequired": "Please select notification type",
|
||||
"enabled": "Enabled Status",
|
||||
"status": "Status",
|
||||
"enabledStatus": "Enabled",
|
||||
"disabledStatus": "Disabled",
|
||||
"chatIds": "Chat IDs",
|
||||
"chatIdsPlaceholder": "Chat IDs (click button above to auto-fetch, or enter manually, separate multiple with commas)",
|
||||
"chatIdsRequired": "Please enter at least one Chat ID",
|
||||
"chatIdsExtra": "Multiple Chat IDs separated by commas, e.g., 123456789,987654321. Click button above to auto-fetch (need to send message to bot first)",
|
||||
"chatIdsCount": " recipients",
|
||||
"chatIdsNotConfigured": "Not Configured",
|
||||
"edit": "Edit",
|
||||
"test": "Test",
|
||||
"delete": "Delete",
|
||||
"deleteConfirm": "Are you sure you want to delete this configuration?",
|
||||
"enableSuccess": "Enabled",
|
||||
"disableSuccess": "Disabled",
|
||||
"updateStatusFailed": "Failed to update",
|
||||
"deleteSuccess": "Deleted successfully",
|
||||
"deleteFailed": "Failed to delete",
|
||||
"createSuccess": "Created successfully",
|
||||
"createFailed": "Failed to create",
|
||||
"updateSuccess": "Updated successfully",
|
||||
"updateFailed": "Failed to update",
|
||||
"fetchFailed": "Failed to get configuration list",
|
||||
"testSuccess": "Test message sent successfully, please check Telegram",
|
||||
"testFailed": "Failed to send test message",
|
||||
"getChatIdsSuccess": "Successfully fetched {count} Chat ID(s)",
|
||||
"getChatIdsFailed": "Failed to get Chat IDs",
|
||||
"getChatIdsNoToken": "Please enter Bot Token first",
|
||||
"getChatIdsNoMessage": "Chat ID not found, please send a message to the bot first (e.g., /start), then retry",
|
||||
"getChatIdsButton": "Get Chat ID"
|
||||
},
|
||||
"telegramConfig": {
|
||||
"title": "Telegram Configuration Guide",
|
||||
"step1": "Create a Telegram bot via <strong>@BotFather</strong> and get Bot Token",
|
||||
"step2": "After entering Bot Token, click 'Get Chat ID' button to auto-fetch (need to send message to bot first)",
|
||||
"step3": "Or manually get Chat ID via <strong>@userinfobot</strong>",
|
||||
"step4": "Support multiple Chat IDs (separated by commas), all configured users will receive notifications",
|
||||
"step5": "Telegram messages will be sent automatically when orders succeed or fail",
|
||||
"botToken": "Bot Token",
|
||||
"botTokenPlaceholder": "Bot Token (from @BotFather)",
|
||||
"botTokenRequired": "Please enter Bot Token"
|
||||
},
|
||||
"discordConfig": {
|
||||
"title": "Discord Configuration Guide",
|
||||
"step1": "Create a Webhook in Discord server",
|
||||
"step2": "Copy Webhook URL and enter in configuration",
|
||||
"step3": "Discord messages will be sent automatically when orders succeed or fail",
|
||||
"webhookUrl": "Webhook URL",
|
||||
"webhookUrlPlaceholder": "Webhook URL (from Discord server settings)",
|
||||
"webhookUrlRequired": "Please enter Webhook URL"
|
||||
},
|
||||
"slackConfig": {
|
||||
"title": "Slack Configuration Guide",
|
||||
"step1": "Create an Incoming Webhook in Slack workspace",
|
||||
"step2": "Copy Webhook URL and enter in configuration",
|
||||
"step3": "Slack messages will be sent automatically when orders succeed or fail",
|
||||
"webhookUrl": "Webhook URL",
|
||||
"webhookUrlPlaceholder": "Webhook URL (from Slack App settings)",
|
||||
"webhookUrlRequired": "Please enter Webhook URL"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,14 +56,58 @@
|
||||
"importAccount": "导入账户",
|
||||
"accountName": "账户名称",
|
||||
"walletAddress": "钱包地址",
|
||||
"proxyAddress": "代理钱包地址",
|
||||
"apiCredentials": "API 凭证",
|
||||
"balance": "余额",
|
||||
"activeOrders": "活跃订单",
|
||||
"action": "操作",
|
||||
"actions": "操作",
|
||||
"detail": "详情",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"viewDetail": "查看详情",
|
||||
"deleteConfirm": "确定要删除这个账户吗?",
|
||||
"deleteConfirmDesc": "删除账户前,请确保已取消所有活跃订单。删除后无法恢复,请谨慎操作!",
|
||||
"deleteConfirmDescSimple": "删除后无法恢复,请谨慎操作!",
|
||||
"deleteConfirmOk": "确定删除",
|
||||
"deleteSuccess": "删除账户成功",
|
||||
"deleteFailed": "删除账户失败",
|
||||
"copySuccess": "已复制到剪贴板",
|
||||
"copyFailed": "复制失败",
|
||||
"fullConfig": "完整配置",
|
||||
"partialConfig": "部分配置",
|
||||
"notConfigured": "未配置",
|
||||
"totalBalance": "总余额",
|
||||
"available": "可用",
|
||||
"position": "仓位",
|
||||
"refreshBalance": "刷新余额",
|
||||
"refreshBalanceSuccess": "余额刷新成功",
|
||||
"refreshBalanceFailed": "刷新余额失败",
|
||||
"getDetailFailed": "获取账户详情失败",
|
||||
"openDetailFailed": "打开详情失败",
|
||||
"accountDetail": "账户详情",
|
||||
"accountId": "账户ID",
|
||||
"apiKey": "API Key",
|
||||
"apiSecret": "API Secret",
|
||||
"apiPassphrase": "API Passphrase",
|
||||
"configured": "已配置",
|
||||
"notConfiguredStatus": "未配置",
|
||||
"configStatus": "配置状态",
|
||||
"statistics": "交易统计",
|
||||
"totalOrders": "总订单数",
|
||||
"activeOrdersCount": "活跃订单数",
|
||||
"completedOrders": "已完成订单数",
|
||||
"positionCount": "持仓数量",
|
||||
"totalPnl": "总盈亏",
|
||||
"editAccount": "编辑账户",
|
||||
"editTip": "编辑提示",
|
||||
"editTipDesc": "API 凭证字段留空表示不修改。如需更新 API 凭证,请输入新值;如需保持原值不变,请留空。",
|
||||
"accountNamePlaceholder": "账户名称(可选)",
|
||||
"leaveEmptyToNotModify": "留空表示不修改",
|
||||
"updateSuccess": "更新账户成功",
|
||||
"updateFailed": "更新账户失败",
|
||||
"getDetailFailedForEdit": "获取账户详情失败",
|
||||
"loading": "加载中...",
|
||||
"fetchFailed": "获取账户列表失败"
|
||||
},
|
||||
"accountImport": {
|
||||
@@ -104,6 +148,7 @@
|
||||
"language": "语言",
|
||||
"apiHealth": "API健康",
|
||||
"proxy": "代理",
|
||||
"notifications": "消息推送",
|
||||
"logout": "退出登录",
|
||||
"logoutConfirm": "确认退出",
|
||||
"logoutConfirmDesc": "确定要退出登录吗?",
|
||||
@@ -330,5 +375,75 @@
|
||||
"deleteSuccess": "删除跟单成功",
|
||||
"deleteFailed": "删除跟单失败",
|
||||
"deleteConfirm": "确定要删除这个跟单关系吗?"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"title": "消息推送设置",
|
||||
"addConfig": "添加配置",
|
||||
"editConfig": "编辑配置",
|
||||
"configName": "配置名称",
|
||||
"configNamePlaceholder": "例如:我的 Telegram 机器人",
|
||||
"configNameRequired": "请输入配置名称",
|
||||
"type": "推送类型",
|
||||
"typeRequired": "请选择推送类型",
|
||||
"enabled": "启用状态",
|
||||
"status": "状态",
|
||||
"enabledStatus": "已启用",
|
||||
"disabledStatus": "已禁用",
|
||||
"chatIds": "Chat IDs",
|
||||
"chatIdsPlaceholder": "Chat IDs(点击上方按钮自动获取,或手动输入,多个用逗号分隔)",
|
||||
"chatIdsRequired": "请输入至少一个 Chat ID",
|
||||
"chatIdsExtra": "多个 Chat ID 用逗号分隔,例如:123456789,987654321。点击上方按钮可自动获取(需要先向机器人发送消息)",
|
||||
"chatIdsCount": "个接收者",
|
||||
"chatIdsNotConfigured": "未配置",
|
||||
"edit": "编辑",
|
||||
"test": "测试",
|
||||
"delete": "删除",
|
||||
"deleteConfirm": "确定要删除这个配置吗?",
|
||||
"enableSuccess": "已启用",
|
||||
"disableSuccess": "已禁用",
|
||||
"updateStatusFailed": "更新失败",
|
||||
"deleteSuccess": "删除成功",
|
||||
"deleteFailed": "删除失败",
|
||||
"createSuccess": "创建成功",
|
||||
"createFailed": "创建失败",
|
||||
"updateSuccess": "更新成功",
|
||||
"updateFailed": "更新失败",
|
||||
"fetchFailed": "获取配置列表失败",
|
||||
"testSuccess": "测试消息发送成功,请检查 Telegram",
|
||||
"testFailed": "测试消息发送失败",
|
||||
"getChatIdsSuccess": "成功获取 {count} 个 Chat ID",
|
||||
"getChatIdsFailed": "获取 Chat IDs 失败",
|
||||
"getChatIdsNoToken": "请先填写 Bot Token",
|
||||
"getChatIdsNoMessage": "未找到 Chat ID,请先向机器人发送一条消息(如 /start),然后重试",
|
||||
"getChatIdsButton": "获取 Chat ID"
|
||||
},
|
||||
"telegramConfig": {
|
||||
"title": "Telegram 配置说明",
|
||||
"step1": "通过 <strong>@BotFather</strong> 创建 Telegram 机器人,获取 Bot Token",
|
||||
"step2": "填写 Bot Token 后,点击\"获取 Chat ID\"按钮自动获取(需要先向机器人发送消息)",
|
||||
"step3": "或通过 <strong>@userinfobot</strong> 手动获取 Chat ID",
|
||||
"step4": "支持配置多个 Chat ID(用逗号分隔),所有配置的用户都会收到通知",
|
||||
"step5": "订单成功或失败时会自动发送 Telegram 消息",
|
||||
"botToken": "Bot Token",
|
||||
"botTokenPlaceholder": "Bot Token(从 @BotFather 获取)",
|
||||
"botTokenRequired": "请输入 Bot Token"
|
||||
},
|
||||
"discordConfig": {
|
||||
"title": "Discord 配置说明",
|
||||
"step1": "在 Discord 服务器中创建 Webhook",
|
||||
"step2": "复制 Webhook URL 并填入配置",
|
||||
"step3": "订单成功或失败时会自动发送 Discord 消息",
|
||||
"webhookUrl": "Webhook URL",
|
||||
"webhookUrlPlaceholder": "Webhook URL(从 Discord 服务器设置中获取)",
|
||||
"webhookUrlRequired": "请输入 Webhook URL"
|
||||
},
|
||||
"slackConfig": {
|
||||
"title": "Slack 配置说明",
|
||||
"step1": "在 Slack 工作区中创建 Incoming Webhook",
|
||||
"step2": "复制 Webhook URL 并填入配置",
|
||||
"step3": "订单成功或失败时会自动发送 Slack 消息",
|
||||
"webhookUrl": "Webhook URL",
|
||||
"webhookUrlPlaceholder": "Webhook URL(从 Slack App 设置中获取)",
|
||||
"webhookUrlRequired": "请输入 Webhook URL"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +203,7 @@
|
||||
"language": "語言",
|
||||
"apiHealth": "API健康",
|
||||
"proxy": "代理",
|
||||
"notifications": "消息推送",
|
||||
"logout": "退出登錄",
|
||||
"logoutConfirm": "確認退出",
|
||||
"logoutConfirmDesc": "確定要退出登錄嗎?",
|
||||
@@ -428,5 +429,75 @@
|
||||
"deleteSuccess": "刪除跟單成功",
|
||||
"deleteFailed": "刪除跟單失敗",
|
||||
"deleteConfirm": "確定要刪除這個跟單關係嗎?"
|
||||
},
|
||||
"notificationSettings": {
|
||||
"title": "消息推送設置",
|
||||
"addConfig": "添加配置",
|
||||
"editConfig": "編輯配置",
|
||||
"configName": "配置名稱",
|
||||
"configNamePlaceholder": "例如:我的 Telegram 機器人",
|
||||
"configNameRequired": "請輸入配置名稱",
|
||||
"type": "推送類型",
|
||||
"typeRequired": "請選擇推送類型",
|
||||
"enabled": "啟用狀態",
|
||||
"status": "狀態",
|
||||
"enabledStatus": "已啟用",
|
||||
"disabledStatus": "已禁用",
|
||||
"chatIds": "Chat IDs",
|
||||
"chatIdsPlaceholder": "Chat IDs(點擊上方按鈕自動獲取,或手動輸入,多個用逗號分隔)",
|
||||
"chatIdsRequired": "請輸入至少一個 Chat ID",
|
||||
"chatIdsExtra": "多個 Chat ID 用逗號分隔,例如:123456789,987654321。點擊上方按鈕可自動獲取(需要先向機器人發送消息)",
|
||||
"chatIdsCount": "個接收者",
|
||||
"chatIdsNotConfigured": "未配置",
|
||||
"edit": "編輯",
|
||||
"test": "測試",
|
||||
"delete": "刪除",
|
||||
"deleteConfirm": "確定要刪除這個配置嗎?",
|
||||
"enableSuccess": "已啟用",
|
||||
"disableSuccess": "已禁用",
|
||||
"updateStatusFailed": "更新失敗",
|
||||
"deleteSuccess": "刪除成功",
|
||||
"deleteFailed": "刪除失敗",
|
||||
"createSuccess": "創建成功",
|
||||
"createFailed": "創建失敗",
|
||||
"updateSuccess": "更新成功",
|
||||
"updateFailed": "更新失敗",
|
||||
"fetchFailed": "獲取配置列表失敗",
|
||||
"testSuccess": "測試消息發送成功,請檢查 Telegram",
|
||||
"testFailed": "測試消息發送失敗",
|
||||
"getChatIdsSuccess": "成功獲取 {count} 個 Chat ID",
|
||||
"getChatIdsFailed": "獲取 Chat IDs 失敗",
|
||||
"getChatIdsNoToken": "請先填寫 Bot Token",
|
||||
"getChatIdsNoMessage": "未找到 Chat ID,請先向機器人發送一條消息(如 /start),然後重試",
|
||||
"getChatIdsButton": "獲取 Chat ID"
|
||||
},
|
||||
"telegramConfig": {
|
||||
"title": "Telegram 配置說明",
|
||||
"step1": "通過 <strong>@BotFather</strong> 創建 Telegram 機器人,獲取 Bot Token",
|
||||
"step2": "填寫 Bot Token 後,點擊\"獲取 Chat ID\"按鈕自動獲取(需要先向機器人發送消息)",
|
||||
"step3": "或通過 <strong>@userinfobot</strong> 手動獲取 Chat ID",
|
||||
"step4": "支持配置多個 Chat ID(用逗號分隔),所有配置的用戶都會收到通知",
|
||||
"step5": "訂單成功或失敗時會自動發送 Telegram 消息",
|
||||
"botToken": "Bot Token",
|
||||
"botTokenPlaceholder": "Bot Token(從 @BotFather 獲取)",
|
||||
"botTokenRequired": "請輸入 Bot Token"
|
||||
},
|
||||
"discordConfig": {
|
||||
"title": "Discord 配置說明",
|
||||
"step1": "在 Discord 服務器中創建 Webhook",
|
||||
"step2": "複製 Webhook URL 並填入配置",
|
||||
"step3": "訂單成功或失敗時會自動發送 Discord 消息",
|
||||
"webhookUrl": "Webhook URL",
|
||||
"webhookUrlPlaceholder": "Webhook URL(從 Discord 服務器設置中獲取)",
|
||||
"webhookUrlRequired": "請輸入 Webhook URL"
|
||||
},
|
||||
"slackConfig": {
|
||||
"title": "Slack 配置說明",
|
||||
"step1": "在 Slack 工作區中創建 Incoming Webhook",
|
||||
"step2": "複製 Webhook URL 並填入配置",
|
||||
"step3": "訂單成功或失敗時會自動發送 Slack 消息",
|
||||
"webhookUrl": "Webhook URL",
|
||||
"webhookUrlPlaceholder": "Webhook URL(從 Slack App 設置中獲取)",
|
||||
"webhookUrlRequired": "請輸入 Webhook URL"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Modal, Form, Input, Switch } from 'antd'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SendOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiService } from '../services/api'
|
||||
import type { NotificationConfig, NotificationConfigRequest, NotificationConfigUpdateRequest } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { TelegramConfigForm, DiscordConfigForm, SlackConfigForm } from '../components/notifications'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
const NotificationSettings: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [configs, setConfigs] = useState<NotificationConfig[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [editingConfig, setEditingConfig] = useState<NotificationConfig | null>(null)
|
||||
const [form] = Form.useForm()
|
||||
const [testLoading, setTestLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfigs()
|
||||
}, [])
|
||||
|
||||
const fetchConfigs = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await apiService.notifications.list({ type: 'telegram' })
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setConfigs(response.data.data)
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.fetchFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('notificationSettings.fetchFailed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingConfig(null)
|
||||
form.resetFields()
|
||||
form.setFieldsValue({
|
||||
type: 'telegram',
|
||||
enabled: true,
|
||||
config: {
|
||||
botToken: '',
|
||||
chatIds: []
|
||||
}
|
||||
})
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
const handleEdit = (config: NotificationConfig) => {
|
||||
setEditingConfig(config)
|
||||
|
||||
// 处理配置数据:后端返回的是 NotificationConfigData.Telegram 结构
|
||||
// 结构可能是: { data: { botToken: string, chatIds: string[] } } 或直接 { botToken: string, chatIds: string[] }
|
||||
let botToken = ''
|
||||
let chatIds = ''
|
||||
|
||||
if (config.config) {
|
||||
// 检查是否是嵌套结构 (NotificationConfigData.Telegram)
|
||||
if ('data' in config.config && config.config.data) {
|
||||
const data = config.config.data as any
|
||||
botToken = data.botToken || ''
|
||||
if (data.chatIds) {
|
||||
if (Array.isArray(data.chatIds)) {
|
||||
chatIds = data.chatIds.join(',')
|
||||
} else if (typeof data.chatIds === 'string') {
|
||||
chatIds = data.chatIds
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 直接结构 (TelegramConfigData)
|
||||
if ('botToken' in config.config) {
|
||||
botToken = (config.config as any).botToken || ''
|
||||
}
|
||||
if ('chatIds' in config.config) {
|
||||
const ids = (config.config as any).chatIds
|
||||
if (Array.isArray(ids)) {
|
||||
chatIds = ids.join(',')
|
||||
} else if (typeof ids === 'string') {
|
||||
chatIds = ids
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
type: config.type,
|
||||
name: config.name,
|
||||
enabled: config.enabled,
|
||||
config: {
|
||||
botToken: botToken,
|
||||
chatIds: chatIds
|
||||
}
|
||||
})
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
const response = await apiService.notifications.delete({ id })
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('notificationSettings.deleteSuccess'))
|
||||
fetchConfigs()
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.deleteFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('notificationSettings.deleteFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateEnabled = async (id: number, enabled: boolean) => {
|
||||
try {
|
||||
const response = await apiService.notifications.updateEnabled({ id, enabled })
|
||||
if (response.data.code === 0) {
|
||||
message.success(enabled ? t('notificationSettings.enableSuccess') : t('notificationSettings.disableSuccess'))
|
||||
fetchConfigs()
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.updateStatusFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('notificationSettings.updateStatusFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleTest = async () => {
|
||||
setTestLoading(true)
|
||||
try {
|
||||
const response = await apiService.notifications.test({ message: '这是一条测试消息' })
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
message.success(t('notificationSettings.testSuccess'))
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.testFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('notificationSettings.testFailed'))
|
||||
} finally {
|
||||
setTestLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
|
||||
// 处理 chatIds:如果是字符串,转换为数组
|
||||
const chatIds = typeof values.config.chatIds === 'string'
|
||||
? values.config.chatIds.split(',').map((id: string) => id.trim()).filter((id: string) => id)
|
||||
: values.config.chatIds || []
|
||||
|
||||
const configData: NotificationConfigRequest | NotificationConfigUpdateRequest = {
|
||||
type: values.type,
|
||||
name: values.name,
|
||||
enabled: values.enabled,
|
||||
config: {
|
||||
botToken: values.config.botToken,
|
||||
chatIds: chatIds
|
||||
}
|
||||
}
|
||||
|
||||
if (editingConfig?.id) {
|
||||
// 更新
|
||||
const updateData = {
|
||||
...configData,
|
||||
id: editingConfig.id
|
||||
} as NotificationConfigUpdateRequest
|
||||
|
||||
const response = await apiService.notifications.update(updateData)
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('notificationSettings.updateSuccess'))
|
||||
setModalVisible(false)
|
||||
fetchConfigs()
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.updateFailed'))
|
||||
}
|
||||
} else {
|
||||
// 创建
|
||||
const response = await apiService.notifications.create(configData)
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('notificationSettings.createSuccess'))
|
||||
setModalVisible(false)
|
||||
fetchConfigs()
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.createFailed'))
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) {
|
||||
// 表单验证错误
|
||||
return
|
||||
}
|
||||
message.error(error.message || t('message.error'))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据推送类型获取配置表单组件
|
||||
*/
|
||||
const getConfigFormComponent = (type: string) => {
|
||||
switch (type?.toLowerCase()) {
|
||||
case 'telegram':
|
||||
return <TelegramConfigForm form={form} />
|
||||
case 'discord':
|
||||
return <DiscordConfigForm form={form} />
|
||||
case 'slack':
|
||||
return <SlackConfigForm form={form} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('notificationSettings.configName'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: t('notificationSettings.type'),
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
render: (type: string) => <Tag color="blue">{type.toUpperCase()}</Tag>
|
||||
},
|
||||
{
|
||||
title: t('notificationSettings.status'),
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
render: (enabled: boolean) => (
|
||||
<Tag color={enabled ? 'green' : 'default'}>
|
||||
{enabled ? t('notificationSettings.enabledStatus') : t('notificationSettings.disabledStatus')}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('notificationSettings.chatIds'),
|
||||
key: 'chatIds',
|
||||
render: (_: any, record: NotificationConfig) => {
|
||||
// 处理配置数据:后端返回的是 NotificationConfigData.Telegram 结构
|
||||
// 结构可能是: { data: { botToken: string, chatIds: string[] } } 或直接 { botToken: string, chatIds: string[] }
|
||||
let chatIds: string[] = []
|
||||
|
||||
if (record.config) {
|
||||
// 检查是否是嵌套结构 (NotificationConfigData.Telegram)
|
||||
if ('data' in record.config && record.config.data) {
|
||||
const data = (record.config as any).data
|
||||
if (data.chatIds) {
|
||||
if (Array.isArray(data.chatIds)) {
|
||||
chatIds = data.chatIds.filter((id: any) => id && String(id).trim())
|
||||
} else if (typeof data.chatIds === 'string') {
|
||||
chatIds = data.chatIds.split(',').map((id: string) => id.trim()).filter((id: string) => id)
|
||||
}
|
||||
}
|
||||
} else if ('chatIds' in record.config) {
|
||||
// 直接结构 (TelegramConfigData)
|
||||
const ids: any = (record.config as any).chatIds
|
||||
if (Array.isArray(ids)) {
|
||||
chatIds = ids.filter((id: any) => id && String(id).trim())
|
||||
} else if (typeof ids === 'string') {
|
||||
chatIds = (ids as string).split(',').map((id: string) => id.trim()).filter((id: string) => id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chatIds.length > 0 ? (
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{chatIds.join(', ')}
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="danger" style={{ fontSize: '12px' }}>{t('notificationSettings.chatIdsNotConfigured')}</Text>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('common.actions'),
|
||||
key: 'action',
|
||||
width: isMobile ? 120 : 200,
|
||||
render: (_: any, record: NotificationConfig) => (
|
||||
<Space size="small" wrap>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
{t('notificationSettings.edit')}
|
||||
</Button>
|
||||
<Switch
|
||||
checked={record.enabled}
|
||||
size="small"
|
||||
onChange={(checked) => handleUpdateEnabled(record.id!, checked)}
|
||||
/>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<SendOutlined />}
|
||||
loading={testLoading}
|
||||
onClick={handleTest}
|
||||
>
|
||||
{t('notificationSettings.test')}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={t('notificationSettings.deleteConfirm')}
|
||||
onConfirm={() => handleDelete(record.id!)}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
{t('notificationSettings.delete')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ padding: isMobile ? '16px' : '24px' }}>
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Title level={4} style={{ margin: 0 }}>{t('notificationSettings.title')}</Title>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
{t('notificationSettings.addConfig')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={configs}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
scroll={{ x: isMobile ? 600 : 'auto' }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={editingConfig ? t('notificationSettings.editConfig') : t('notificationSettings.addConfig')}
|
||||
open={modalVisible}
|
||||
onOk={handleSubmit}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
width={isMobile ? '90%' : 600}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="type"
|
||||
label={t('notificationSettings.type')}
|
||||
rules={[{ required: true, message: t('notificationSettings.typeRequired') }]}
|
||||
>
|
||||
<Input disabled value="telegram" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('notificationSettings.configName')}
|
||||
rules={[{ required: true, message: t('notificationSettings.configNameRequired') }]}
|
||||
>
|
||||
<Input placeholder={t('notificationSettings.configNamePlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="enabled"
|
||||
label={t('notificationSettings.enabled')}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
{/* 根据推送类型动态渲染配置表单组件 */}
|
||||
<Form.Item shouldUpdate={(prevValues, currentValues) => {
|
||||
// 监听 type 变化,以及编辑时 config 数据的变化
|
||||
return prevValues.type !== currentValues.type ||
|
||||
prevValues.config !== currentValues.config
|
||||
}}>
|
||||
{() => {
|
||||
const currentType = form.getFieldValue('type') || 'telegram'
|
||||
return getConfigFormComponent(currentType)
|
||||
}}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotificationSettings
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios, { AxiosInstance, AxiosError } from 'axios'
|
||||
import type { ApiResponse } from '../types'
|
||||
import type { ApiResponse, NotificationConfig, NotificationConfigRequest, NotificationConfigUpdateRequest } from '../types'
|
||||
import { getToken, setToken, removeToken } from '../utils'
|
||||
import { wsManager } from './websocket'
|
||||
import i18n from '../i18n/config'
|
||||
@@ -474,6 +474,59 @@ export const apiService = {
|
||||
responseTime?: number
|
||||
}>
|
||||
}>>('/proxy-config/api-health-check', {})
|
||||
},
|
||||
|
||||
/**
|
||||
* 消息推送配置 API
|
||||
*/
|
||||
notifications: {
|
||||
/**
|
||||
* 获取配置列表
|
||||
*/
|
||||
list: (data?: { type?: string }) =>
|
||||
apiClient.post<ApiResponse<NotificationConfig[]>>('/notifications/configs/list', data || {}),
|
||||
|
||||
/**
|
||||
* 获取配置详情
|
||||
*/
|
||||
detail: (data: { id: number }) =>
|
||||
apiClient.post<ApiResponse<NotificationConfig>>('/notifications/configs/detail', data),
|
||||
|
||||
/**
|
||||
* 创建配置
|
||||
*/
|
||||
create: (data: NotificationConfigRequest) =>
|
||||
apiClient.post<ApiResponse<NotificationConfig>>('/notifications/configs/create', data),
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
update: (data: NotificationConfigUpdateRequest) =>
|
||||
apiClient.post<ApiResponse<NotificationConfig>>('/notifications/configs/update', data),
|
||||
|
||||
/**
|
||||
* 更新启用状态
|
||||
*/
|
||||
updateEnabled: (data: { id: number; enabled: boolean }) =>
|
||||
apiClient.post<ApiResponse<NotificationConfig>>('/notifications/configs/update-enabled', data),
|
||||
|
||||
/**
|
||||
* 删除配置
|
||||
*/
|
||||
delete: (data: { id: number }) =>
|
||||
apiClient.post<ApiResponse<void>>('/notifications/configs/delete', data),
|
||||
|
||||
/**
|
||||
* 测试通知
|
||||
*/
|
||||
test: (data?: { message?: string }) =>
|
||||
apiClient.post<ApiResponse<boolean>>('/notifications/test', data || {}),
|
||||
|
||||
/**
|
||||
* 获取 Telegram Chat IDs
|
||||
*/
|
||||
getTelegramChatIds: (data: { botToken: string }) =>
|
||||
apiClient.post<ApiResponse<string[]>>('/notifications/telegram/get-chat-ids', data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -600,3 +600,49 @@ export interface OrderTrackingRequest {
|
||||
buyOrderId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息推送配置
|
||||
*/
|
||||
export interface NotificationConfig {
|
||||
id?: number
|
||||
type: string // telegram、discord、slack 等
|
||||
name: string // 配置名称
|
||||
enabled: boolean // 是否启用
|
||||
config: {
|
||||
botToken?: string // Telegram Bot Token
|
||||
chatIds?: string[] // Telegram Chat IDs
|
||||
[key: string]: any // 其他配置字段
|
||||
}
|
||||
createdAt?: number
|
||||
updatedAt?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知配置请求
|
||||
*/
|
||||
export interface NotificationConfigRequest {
|
||||
type: string
|
||||
name: string
|
||||
enabled?: boolean
|
||||
config: {
|
||||
botToken?: string
|
||||
chatIds?: string[] | string // 支持数组或逗号分隔的字符串
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知配置更新请求
|
||||
*/
|
||||
export interface NotificationConfigUpdateRequest {
|
||||
id: number
|
||||
type: string
|
||||
name: string
|
||||
enabled?: boolean
|
||||
config: {
|
||||
botToken?: string
|
||||
chatIds?: string[] | string
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user