diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index f0e91bd..4ddd575 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -207,6 +207,11 @@ const Layout: React.FC = ({ children }) => { icon: , label: t('menu.systemOverview') || '通用设置' }, + { + key: '/system-settings/notification', + icon: , + label: t('menu.notifications') || '消息推送设置' + }, { key: '/system-settings/rpc-nodes', icon: , @@ -216,11 +221,6 @@ const Layout: React.FC = ({ children }) => { key: '/system-settings/api-health', icon: , label: t('menu.apiHealth') || 'API健康' - }, - { - key: '/system-settings/notification', - icon: , - label: t('menu.notifications') || '消息推送设置' } ] }, diff --git a/frontend/src/pages/NotificationSettingsPage.tsx b/frontend/src/pages/NotificationSettingsPage.tsx index 29dfa4b..147dfe5 100644 --- a/frontend/src/pages/NotificationSettingsPage.tsx +++ b/frontend/src/pages/NotificationSettingsPage.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useCallback } from 'react' import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Modal, Form, Input, Switch, Tooltip, Row, Col, Tabs } from 'antd' -import { PlusOutlined, EditOutlined, DeleteOutlined, SendOutlined, CopyOutlined, ReloadOutlined, CheckOutlined, RobotOutlined, FormOutlined } from '@ant-design/icons' +import { PlusOutlined, EditOutlined, DeleteOutlined, SendOutlined, ReloadOutlined, CheckOutlined, RobotOutlined, FormOutlined } from '@ant-design/icons' import { useTranslation } from 'react-i18next' import { apiService } from '../services/api' import type { NotificationConfig, NotificationConfigRequest, NotificationConfigUpdateRequest, NotificationTemplate, TemplateTypeInfo, TemplateVariablesResponse, TemplateVariable } from '../types' @@ -355,8 +355,35 @@ const NotificationSettingsPage: React.FC = () => { } const handleCopyVariable = useCallback((variable: string) => { - navigator.clipboard.writeText(`{{${variable}}}`) - message.success(t('notificationSettings.templates.copied')) + const text = `{{${variable}}}` + + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(text).then(() => { + message.success(t('notificationSettings.templates.copied')) + }).catch(() => { + fallbackCopy(text) + }) + } else { + fallbackCopy(text) + } + + function fallbackCopy(text: string) { + const textArea = document.createElement('textarea') + textArea.value = text + textArea.style.position = 'fixed' + textArea.style.left = '-9999px' + textArea.style.top = '-9999px' + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + try { + document.execCommand('copy') + message.success(t('notificationSettings.templates.copied')) + } catch { + message.error(t('common.copyFailed')) + } + document.body.removeChild(textArea) + } }, [t]) const [variableHoverKey, setVariableHoverKey] = useState(null) @@ -365,20 +392,31 @@ const NotificationSettingsPage: React.FC = () => { const isHover = variableHoverKey === variable.key const label = t(`notificationSettings.templates.variableLabels.${variable.key}`) const description = t(`notificationSettings.templates.variableDescriptions.${variable.key}`) + const variableElement = ( + handleCopyVariable(variable.key)} + onMouseEnter={() => !isMobile && setVariableHoverKey(variable.key)} + onMouseLeave={() => !isMobile && setVariableHoverKey(null)} + onKeyDown={(e) => e.key === 'Enter' && handleCopyVariable(variable.key)} + > + {label} + + ) + + if (isMobile) { + return ( + + {variableElement} + + ) + } + return ( - handleCopyVariable(variable.key)} - onMouseEnter={() => setVariableHoverKey(variable.key)} - onMouseLeave={() => setVariableHoverKey(null)} - onKeyDown={(e) => e.key === 'Enter' && handleCopyVariable(variable.key)} - > - - {label} - + {variableElement} ) } @@ -617,7 +655,7 @@ const NotificationSettingsPage: React.FC = () => {