fix: clipboard copy works on HTTP
This commit is contained in:
1 parent
7dae775208
commit
79484eef50
6 files changed
+51
-13
No files matched your search
@@ -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'},
|
||||
|
||||
@@ -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: '启用用户'},
|
||||
|
||||
@@ -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 = [
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space style={{ width: '100%' }}>
|
||||
<Input value={uri} readOnly />
|
||||
<Button icon={<CopyOutlined />} onClick={() => { navigator.clipboard.writeText(uri); message.success(t('common.copy')); }} />
|
||||
<Button icon={<CopyOutlined />} onClick={async () => { const ok = await copyText(uri); if (ok) message.success(t('common.copied') || t('common.copy')); else message.error(t('common.copyFailed') || 'Copy failed'); }} />
|
||||
</Space>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<img alt="qr" width={160} height={160} src={`https://api.qrserver.com/v1/create-qr-code/?size=160x160&data=${encodeURIComponent(uri)}`} />
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { Card, Button, Space, Typography, Tag, message, Upload, Form, Input, Switch, Select, InputNumber } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useI18n } from '../i18n';
|
||||
import { copyText } from '../utils/clipboard';
|
||||
import { useThemeStore } from '../stores/themeStore';
|
||||
import { LockOutlined, GlobalOutlined, BgColorsOutlined, InfoCircleOutlined, CloudDownloadOutlined, CloudUploadOutlined, ApiOutlined } from '@ant-design/icons';
|
||||
import { downloadBackup, restoreDatabase, openApiUrl } from '../api/system';
|
||||
@@ -268,7 +269,7 @@ const Settings: React.FC = () => {
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Text type="secondary">{t('settings.acmeHint')}</Text>
|
||||
<Input.TextArea style={{ marginTop: 8 }} rows={3} value={acmeCmd} readOnly />
|
||||
<Button style={{ marginTop: 8 }} onClick={() => { navigator.clipboard.writeText(acmeCmd); message.success(t('common.copied')); }}>{t('common.copy')}</Button>
|
||||
<Button style={{ marginTop: 8 }} onClick={async () => { const ok = await copyText(acmeCmd); if (ok) message.success(t('common.copied')); else message.error(t('common.copyFailed') || 'Copy failed'); }}>{t('common.copy')}</Button>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -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={<CopyOutlined />}
|
||||
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={<CopyOutlined />}
|
||||
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={<CopyOutlined />}
|
||||
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');
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/** Copy text in both secure (HTTPS) and insecure (HTTP IP:port) contexts. */
|
||||
export async function copyText(text: string): Promise<boolean> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user