feat(web): 添加剪贴板复制兼容 HTTP 环境

- 新增 useClipboard composable,支持 execCommand 备用方案
- 修复 HTTP 环境下复制按钮无响应问题
This commit is contained in:
mofeng-git
2025-12-31 20:31:42 +08:00
parent d0e2e13b35
commit 8be45155ac
2 changed files with 71 additions and 3 deletions

View File

@@ -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<boolean> {
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 }
}

View File

@@ -27,6 +27,7 @@ import type {
AtxDevices, AtxDevices,
} from '@/types/generated' } from '@/types/generated'
import { setLanguage } from '@/i18n' import { setLanguage } from '@/i18n'
import { useClipboard } from '@/composables/useClipboard'
import AppLayout from '@/components/AppLayout.vue' import AppLayout from '@/components/AppLayout.vue'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
@@ -173,6 +174,7 @@ const rustdeskStatus = ref<RustDeskStatusResponse | null>(null)
const rustdeskPassword = ref<RustDeskPasswordResponse | null>(null) const rustdeskPassword = ref<RustDeskPasswordResponse | null>(null)
const rustdeskLoading = ref(false) const rustdeskLoading = ref(false)
const rustdeskCopied = ref<'id' | 'password' | null>(null) const rustdeskCopied = ref<'id' | 'password' | null>(null)
const { copy: clipboardCopy } = useClipboard()
const rustdeskLocalConfig = ref({ const rustdeskLocalConfig = ref({
enabled: false, enabled: false,
rendezvous_server: '', rendezvous_server: '',
@@ -860,11 +862,12 @@ async function regenerateRustdeskPassword() {
} }
} }
function copyToClipboard(text: string, type: 'id' | 'password') { async function copyToClipboard(text: string, type: 'id' | 'password') {
navigator.clipboard.writeText(text).then(() => { const success = await clipboardCopy(text)
if (success) {
rustdeskCopied.value = type rustdeskCopied.value = type
setTimeout(() => (rustdeskCopied.value = null), 2000) setTimeout(() => (rustdeskCopied.value = null), 2000)
}) }
} }
function getRustdeskServiceStatusText(status: string | undefined): string { function getRustdeskServiceStatusText(status: string | undefined): string {