diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 204cf4d..6b81223 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -6,7 +6,7 @@ export default { prev: 'Previous', next: 'Next', saved: 'Saved', - copied: 'Copied', save: 'Save', cancel: 'Cancel', delete: 'Delete', edit: 'Edit', create: 'Create', confirm: 'Confirm', close: 'Close', copy: 'Copy', download: 'Download', refresh: 'Refresh', loading: 'Loading…', empty: 'No data', search: 'Search', actions: 'Actions', status: 'Status', enabled: 'Enabled', disabled: 'Disabled', name: 'Name', type: 'Type', port: 'Port', protocol: 'Protocol', address: 'Address', password: 'Password', username: 'Username', submit: 'Submit', success: 'Success', error: 'Error', confirmDelete: 'Are you sure to delete?', back: 'Back', generate: 'Generate', validate: 'Validate' }, + copied: 'Copied', copyFailed: 'Copy failed — select and copy manually', save: 'Save', cancel: 'Cancel', delete: 'Delete', edit: 'Edit', create: 'Create', confirm: 'Confirm', close: 'Close', copy: 'Copy', download: 'Download', refresh: 'Refresh', loading: 'Loading…', empty: 'No data', search: 'Search', actions: 'Actions', status: 'Status', enabled: 'Enabled', disabled: 'Disabled', name: 'Name', type: 'Type', port: 'Port', protocol: 'Protocol', address: 'Address', password: 'Password', username: 'Username', submit: 'Submit', success: 'Success', error: 'Error', confirmDelete: 'Are you sure to delete?', back: 'Back', generate: 'Generate', validate: 'Validate' }, login: { title: '3M-UI', subtitle: 'Mihomo Core Management Console', username: 'Username', password: 'Password', button: 'Login', welcomeBack: 'Welcome back', failed: 'Login failed' }, password: { title: 'Change Password', current: 'Current Password', new: 'New Password', confirm: 'Confirm New Password', button: 'Change Password', success: 'Password updated', failed: 'Failed to change password', mismatch: 'Passwords do not match' }, dashboard: { title: 'Dashboard', subtitle: 'System & Core Overview', status: 'Core Status', running: 'Running', stoppedStatus: 'Stopped', version: 'Version', pid: 'PID', uptime: 'Uptime', listeners: 'Node Stats', total: 'Total', enabled: 'Enabled', disabled: 'Disabled', system: 'System Resources', cpu: 'CPU', memory: 'Memory', disk: 'Disk', network: 'Network', upload: 'Upload', download: 'Download', traffic: 'Traffic Stats', uploadRate: 'Upload rate', downloadRate: 'Download rate', totalUpload: 'Total upload', totalDownload: 'Total download', onlineUsers: 'Online users', activeConnections: 'Active connections', start: 'Start', stop: 'Stop', restart: 'Restart', started: 'Core started', stopped: 'Core stopped', restarted: 'Core restarted', operationFailed: 'Operation failed', unavailable: 'Status unavailable' , users: 'Users', totalUsers: 'Total users', enabledUsers: 'Enabled users'}, diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index ca95ae7..191f6c1 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -6,7 +6,7 @@ export default { prev: '上一步', next: '下一步', saved: '已保存', - copied: '已复制', save: '保存', cancel: '取消', delete: '删除', edit: '编辑', create: '创建', confirm: '确认', close: '关闭', copy: '复制', download: '下载', refresh: '刷新', loading: '加载中…', empty: '暂无数据', search: '搜索', actions: '操作', status: '状态', enabled: '启用', disabled: '禁用', name: '名称', type: '类型', port: '端口', protocol: '协议', address: '地址', password: '密码', username: '用户名', submit: '提交', success: '操作成功', error: '操作失败', confirmDelete: '确定要删除吗?', back: '返回', generate: '生成', validate: '验证' }, + copied: '已复制', copyFailed: '复制失败,请手动选中复制', save: '保存', cancel: '取消', delete: '删除', edit: '编辑', create: '创建', confirm: '确认', close: '关闭', copy: '复制', download: '下载', refresh: '刷新', loading: '加载中…', empty: '暂无数据', search: '搜索', actions: '操作', status: '状态', enabled: '启用', disabled: '禁用', name: '名称', type: '类型', port: '端口', protocol: '协议', address: '地址', password: '密码', username: '用户名', submit: '提交', success: '操作成功', error: '操作失败', confirmDelete: '确定要删除吗?', back: '返回', generate: '生成', validate: '验证' }, login: { title: '3M-UI', subtitle: 'Mihomo 核心管理控制台', username: '用户名', password: '密码', button: '登录', welcomeBack: '欢迎回来', failed: '登录失败' }, password: { title: '修改密码', current: '当前密码', new: '新密码', confirm: '确认新密码', button: '修改密码', success: '密码已更新', failed: '修改密码失败', mismatch: '两次输入的密码不一致' }, dashboard: { title: '仪表盘', subtitle: '系统与核心状态概览', status: '核心状态', running: '运行中', stoppedStatus: '已停止', version: '版本', pid: '进程 ID', uptime: '运行时间', listeners: '节点统计', total: '总数', enabled: '已启用', disabled: '已禁用', system: '系统资源', cpu: 'CPU', memory: '内存', disk: '磁盘', network: '网络', upload: '上传', download: '下载', traffic: '流量统计', uploadRate: '上传速率', downloadRate: '下载速率', totalUpload: '总上传', totalDownload: '总下载', onlineUsers: '在线用户', activeConnections: '活跃连接', start: '启动', stop: '停止', restart: '重启', started: '核心已启动', stopped: '核心已停止', restarted: '核心已重启', operationFailed: '操作失败', unavailable: '无法获取状态' , users: '用户', totalUsers: '用户总数', enabledUsers: '启用用户'}, diff --git a/frontend/src/pages/Listeners.tsx b/frontend/src/pages/Listeners.tsx index 7605a1c..05ea883 100644 --- a/frontend/src/pages/Listeners.tsx +++ b/frontend/src/pages/Listeners.tsx @@ -10,6 +10,7 @@ import { ListenerTemplate, ListenerVersion, } from '../api/listeners'; import { useI18n } from '../i18n'; +import { copyText } from '../utils/clipboard'; import ListenerConfigFields, { configToFormValues, formValuesToConfig, protocolSupportsUDP } from '../components/ListenerConfigFields'; import CapabilityFormFields, { capabilityFormToConfig } from '../components/CapabilityFormFields'; import { fetchCapabilities, protocolCapability, CapabilityManifest } from '../api/capabilities'; @@ -156,7 +157,7 @@ const columns = [ - + )} diff --git a/frontend/src/pages/Users.tsx b/frontend/src/pages/Users.tsx index d548a81..c12b6d3 100644 --- a/frontend/src/pages/Users.tsx +++ b/frontend/src/pages/Users.tsx @@ -11,6 +11,7 @@ import { } from '../api/users'; import { fetchListeners, Listener } from '../api/nodes'; import { useI18n } from '../i18n'; +import { copyText } from '../utils/clipboard'; const formatBytes = (n?: number) => { const bytes = n || 0; @@ -469,10 +470,11 @@ const Users: React.FC = () => { size="small" icon={} disabled={!shareUrl} - onClick={() => { + onClick={async () => { if (!shareUrl) return; - navigator.clipboard.writeText(shareUrl); - message.success(t('common.copied') || 'Copied'); + const ok = await copyText(shareUrl); + if (ok) message.success(t('common.copied') || 'Copied'); + else message.error(t('common.copyFailed') || 'Copy failed — select and copy manually'); }} /> } @@ -489,10 +491,11 @@ const Users: React.FC = () => { type="text" size="small" icon={} - onClick={() => { + onClick={async () => { const u = `${shareUrl}${shareUrl.includes('?') ? '&' : '?'}target=v2ray`; - navigator.clipboard.writeText(u); - message.success(t('common.copied') || 'Copied'); + const ok = await copyText(u); + if (ok) message.success(t('common.copied') || 'Copied'); + else message.error(t('common.copyFailed') || 'Copy failed — select and copy manually'); }} /> } @@ -510,10 +513,11 @@ const Users: React.FC = () => { type="text" size="small" icon={} - onClick={() => { + onClick={async () => { const u = `${shareUrl}${shareUrl.includes('?') ? '&' : '?'}target=singbox`; - navigator.clipboard.writeText(u); - message.success(t('common.copied') || 'Copied'); + const ok = await copyText(u); + if (ok) message.success(t('common.copied') || 'Copied'); + else message.error(t('common.copyFailed') || 'Copy failed — select and copy manually'); }} /> } diff --git a/frontend/src/utils/clipboard.ts b/frontend/src/utils/clipboard.ts new file mode 100644 index 0000000..2373bc6 --- /dev/null +++ b/frontend/src/utils/clipboard.ts @@ -0,0 +1,32 @@ +/** Copy text in both secure (HTTPS) and insecure (HTTP IP:port) contexts. */ +export async function copyText(text: string): Promise { + const value = String(text ?? ''); + if (!value) return false; + + try { + if (typeof navigator !== 'undefined' && navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(value); + return true; + } + } catch { + // fall through + } + + try { + const ta = document.createElement('textarea'); + ta.value = value; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; + ta.style.left = '-9999px'; + ta.style.top = '0'; + document.body.appendChild(ta); + ta.focus(); + ta.select(); + ta.setSelectionRange(0, value.length); + const ok = document.execCommand('copy'); + document.body.removeChild(ta); + return ok; + } catch { + return false; + } +}