feat: 增强前端clipboard功能

This commit is contained in:
shaw
2025-12-27 15:16:52 +08:00
parent f1e47291cd
commit 016d7ef645
7 changed files with 73 additions and 52 deletions

View File

@@ -280,10 +280,12 @@
import { ref, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import Modal from '@/components/common/Modal.vue'
import { useClipboard } from '@/composables/useClipboard'
import { adminAPI } from '@/api/admin'
import type { Account, ClaudeModel } from '@/types'
const { t } = useI18n()
const { copyToClipboard } = useClipboard()
interface OutputLine {
text: string
@@ -501,6 +503,6 @@ const handleEvent = (event: {
const copyOutput = () => {
const text = outputLines.value.map((l) => l.text).join('\n')
navigator.clipboard.writeText(text)
copyToClipboard(text, t('admin.accounts.outputCopied'))
}
</script>

View File

@@ -119,7 +119,7 @@
import { ref, computed, h, watch, type Component } from 'vue'
import { useI18n } from 'vue-i18n'
import Modal from '@/components/common/Modal.vue'
import { useAppStore } from '@/stores/app'
import { useClipboard } from '@/composables/useClipboard'
import type { GroupPlatform } from '@/types'
interface Props {
@@ -150,7 +150,7 @@ const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const { t } = useI18n()
const appStore = useAppStore()
const { copyToClipboard: clipboardCopy } = useClipboard()
const copiedIndex = ref<number | null>(null)
const activeTab = ref<string>('unix')
@@ -340,14 +340,12 @@ ${key('requires_openai_auth')} ${operator('=')} ${keyword('true')}`
}
const copyContent = async (content: string, index: number) => {
try {
await navigator.clipboard.writeText(content)
const success = await clipboardCopy(content, t('keys.copied'))
if (success) {
copiedIndex.value = index
setTimeout(() => {
copiedIndex.value = null
}, 2000)
} catch (error) {
appStore.showError(t('common.copyFailed'))
}
}
</script>

View File

@@ -1,40 +1,65 @@
import { ref } from 'vue'
import { useAppStore } from '@/stores/app'
/**
* 检测是否支持 Clipboard API需要安全上下文HTTPS/localhost
*/
function isClipboardSupported(): boolean {
return !!(navigator.clipboard && window.isSecureContext)
}
/**
* 降级方案:使用 textarea + execCommand
* 使用 textarea 而非 input以正确处理多行文本
*/
function fallbackCopy(text: string): boolean {
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.cssText = 'position:fixed;left:-9999px;top:-9999px'
document.body.appendChild(textarea)
textarea.select()
try {
return document.execCommand('copy')
} finally {
document.body.removeChild(textarea)
}
}
export function useClipboard() {
const appStore = useAppStore()
const copied = ref(false)
const copyToClipboard = async (text: string, successMessage = 'Copied to clipboard') => {
const copyToClipboard = async (
text: string,
successMessage = 'Copied to clipboard'
): Promise<boolean> => {
if (!text) return false
try {
await navigator.clipboard.writeText(text)
copied.value = true
appStore.showSuccess(successMessage)
setTimeout(() => {
copied.value = false
}, 2000)
return true
} catch {
// Fallback for older browsers
const input = document.createElement('input')
input.value = text
document.body.appendChild(input)
input.select()
document.execCommand('copy')
document.body.removeChild(input)
copied.value = true
appStore.showSuccess(successMessage)
setTimeout(() => {
copied.value = false
}, 2000)
return true
let success = false
if (isClipboardSupported()) {
try {
await navigator.clipboard.writeText(text)
success = true
} catch {
success = fallbackCopy(text)
}
} else {
success = fallbackCopy(text)
}
if (success) {
copied.value = true
appStore.showSuccess(successMessage)
setTimeout(() => {
copied.value = false
}, 2000)
} else {
appStore.showError('Copy failed')
}
return success
}
return {
copied,
copyToClipboard
}
return { copied, copyToClipboard }
}

View File

@@ -418,6 +418,7 @@
import { ref, reactive, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { useClipboard } from '@/composables/useClipboard'
import { adminAPI } from '@/api/admin'
import { formatDateTime } from '@/utils/format'
import type { RedeemCode, RedeemCodeType, Group } from '@/types'
@@ -431,6 +432,7 @@ import Select from '@/components/common/Select.vue'
const { t } = useI18n()
const appStore = useAppStore()
const { copyToClipboard: clipboardCopy } = useClipboard()
const showGenerateDialog = ref(false)
const showResultDialog = ref(false)
@@ -618,15 +620,12 @@ const handleGenerateCodes = async () => {
}
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
const success = await clipboardCopy(text, t('admin.redeem.copied'))
if (success) {
copiedCode.value = text
setTimeout(() => {
copiedCode.value = null
}, 2000)
} catch (error) {
appStore.showError(t('admin.redeem.failedToCopy'))
console.error('Error copying to clipboard:', error)
}
}

View File

@@ -1173,6 +1173,7 @@
import { ref, reactive, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { useClipboard } from '@/composables/useClipboard'
import { formatDateTime } from '@/utils/format'
const { t } = useI18n()
@@ -1191,6 +1192,7 @@ import Select from '@/components/common/Select.vue'
import GroupBadge from '@/components/common/GroupBadge.vue'
const appStore = useAppStore()
const { copyToClipboard: clipboardCopy } = useClipboard()
const columns = computed<Column[]>(() => [
{ key: 'email', label: t('admin.users.columns.user'), sortable: true },
@@ -1312,27 +1314,23 @@ const generateEditPassword = () => {
const copyPassword = async () => {
if (!createForm.password) return
try {
await navigator.clipboard.writeText(createForm.password)
const success = await clipboardCopy(createForm.password, t('admin.users.passwordCopied'))
if (success) {
passwordCopied.value = true
setTimeout(() => {
passwordCopied.value = false
}, 2000)
} catch (error) {
appStore.showError(t('common.copyFailed'))
}
}
const copyEditPassword = async () => {
if (!editForm.password) return
try {
await navigator.clipboard.writeText(editForm.password)
const success = await clipboardCopy(editForm.password, t('admin.users.passwordCopied'))
if (success) {
editPasswordCopied.value = true
setTimeout(() => {
editPasswordCopied.value = false
}, 2000)
} catch (error) {
appStore.showError(t('common.copyFailed'))
}
}

View File

@@ -493,6 +493,7 @@
import { ref, computed, onMounted, onUnmounted, type ComponentPublicInstance } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { useClipboard } from '@/composables/useClipboard'
const { t } = useI18n()
import { keysAPI, authAPI, usageAPI, userGroupsAPI } from '@/api'
@@ -520,6 +521,7 @@ interface GroupOption {
}
const appStore = useAppStore()
const { copyToClipboard: clipboardCopy } = useClipboard()
const columns = computed<Column[]>(() => [
{ key: 'name', label: t('common.name'), sortable: true },
@@ -616,14 +618,12 @@ const maskKey = (key: string): string => {
}
const copyToClipboard = async (text: string, keyId: number) => {
try {
await navigator.clipboard.writeText(text)
const success = await clipboardCopy(text, t('keys.copied'))
if (success) {
copiedKeyId.value = keyId
setTimeout(() => {
copiedKeyId.value = null
}, 2000)
} catch (error) {
appStore.showError(t('common.copyFailed'))
}
}

File diff suppressed because one or more lines are too long