feat: 增强前端clipboard功能
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user