From 8be45155aceef78582ec6c4ce5eb9f582a187478 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Wed, 31 Dec 2025 20:31:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E6=B7=BB=E5=8A=A0=E5=89=AA?= =?UTF-8?q?=E8=B4=B4=E6=9D=BF=E5=A4=8D=E5=88=B6=E5=85=BC=E5=AE=B9=20HTTP?= =?UTF-8?q?=20=E7=8E=AF=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 useClipboard composable,支持 execCommand 备用方案 - 修复 HTTP 环境下复制按钮无响应问题 --- web/src/composables/useClipboard.ts | 65 +++++++++++++++++++++++++++++ web/src/views/SettingsView.vue | 9 ++-- 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 web/src/composables/useClipboard.ts diff --git a/web/src/composables/useClipboard.ts b/web/src/composables/useClipboard.ts new file mode 100644 index 00000000..747621a2 --- /dev/null +++ b/web/src/composables/useClipboard.ts @@ -0,0 +1,65 @@ +import { ref } from 'vue' + +export function useClipboard() { + const copied = ref(false) + + // 检测是否支持原生 Clipboard API (需要安全上下文 + API 存在) + function canUseClipboardApi(): boolean { + return !!( + typeof navigator !== 'undefined' && + navigator.clipboard && + typeof navigator.clipboard.writeText === 'function' && + window.isSecureContext + ) + } + + // Fallback: 使用 execCommand (兼容 HTTP 环境) + function fallbackCopy(text: string): boolean { + const textarea = document.createElement('textarea') + textarea.value = text + textarea.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none' + document.body.appendChild(textarea) + textarea.focus() + textarea.select() + + let success = false + try { + success = document.execCommand('copy') + } catch { + success = false + } + + document.body.removeChild(textarea) + return success + } + + async function copy(text: string): Promise { + if (!text) return false + + try { + if (canUseClipboardApi()) { + await navigator.clipboard.writeText(text) + } else { + if (!fallbackCopy(text)) { + return false + } + } + + copied.value = true + setTimeout(() => (copied.value = false), 2000) + return true + } catch (e) { + // Clipboard API 失败时尝试 fallback + console.warn('Clipboard API failed, trying fallback:', e) + if (fallbackCopy(text)) { + copied.value = true + setTimeout(() => (copied.value = false), 2000) + return true + } + console.error('Copy failed:', e) + return false + } + } + + return { copy, copied } +} diff --git a/web/src/views/SettingsView.vue b/web/src/views/SettingsView.vue index 018ee760..b8a39f22 100644 --- a/web/src/views/SettingsView.vue +++ b/web/src/views/SettingsView.vue @@ -27,6 +27,7 @@ import type { AtxDevices, } from '@/types/generated' import { setLanguage } from '@/i18n' +import { useClipboard } from '@/composables/useClipboard' import AppLayout from '@/components/AppLayout.vue' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' @@ -173,6 +174,7 @@ const rustdeskStatus = ref(null) const rustdeskPassword = ref(null) const rustdeskLoading = ref(false) const rustdeskCopied = ref<'id' | 'password' | null>(null) +const { copy: clipboardCopy } = useClipboard() const rustdeskLocalConfig = ref({ enabled: false, rendezvous_server: '', @@ -860,11 +862,12 @@ async function regenerateRustdeskPassword() { } } -function copyToClipboard(text: string, type: 'id' | 'password') { - navigator.clipboard.writeText(text).then(() => { +async function copyToClipboard(text: string, type: 'id' | 'password') { + const success = await clipboardCopy(text) + if (success) { rustdeskCopied.value = type setTimeout(() => (rustdeskCopied.value = null), 2000) - }) + } } function getRustdeskServiceStatusText(status: string | undefined): string {