feat: 增强前端clipboard功能
This commit is contained in:
@@ -280,10 +280,12 @@
|
|||||||
import { ref, watch, nextTick } from 'vue'
|
import { ref, watch, nextTick } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import Modal from '@/components/common/Modal.vue'
|
import Modal from '@/components/common/Modal.vue'
|
||||||
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
import { adminAPI } from '@/api/admin'
|
import { adminAPI } from '@/api/admin'
|
||||||
import type { Account, ClaudeModel } from '@/types'
|
import type { Account, ClaudeModel } from '@/types'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const { copyToClipboard } = useClipboard()
|
||||||
|
|
||||||
interface OutputLine {
|
interface OutputLine {
|
||||||
text: string
|
text: string
|
||||||
@@ -501,6 +503,6 @@ const handleEvent = (event: {
|
|||||||
|
|
||||||
const copyOutput = () => {
|
const copyOutput = () => {
|
||||||
const text = outputLines.value.map((l) => l.text).join('\n')
|
const text = outputLines.value.map((l) => l.text).join('\n')
|
||||||
navigator.clipboard.writeText(text)
|
copyToClipboard(text, t('admin.accounts.outputCopied'))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
import { ref, computed, h, watch, type Component } from 'vue'
|
import { ref, computed, h, watch, type Component } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import Modal from '@/components/common/Modal.vue'
|
import Modal from '@/components/common/Modal.vue'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
import type { GroupPlatform } from '@/types'
|
import type { GroupPlatform } from '@/types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -150,7 +150,7 @@ const props = defineProps<Props>()
|
|||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const appStore = useAppStore()
|
const { copyToClipboard: clipboardCopy } = useClipboard()
|
||||||
|
|
||||||
const copiedIndex = ref<number | null>(null)
|
const copiedIndex = ref<number | null>(null)
|
||||||
const activeTab = ref<string>('unix')
|
const activeTab = ref<string>('unix')
|
||||||
@@ -340,14 +340,12 @@ ${key('requires_openai_auth')} ${operator('=')} ${keyword('true')}`
|
|||||||
}
|
}
|
||||||
|
|
||||||
const copyContent = async (content: string, index: number) => {
|
const copyContent = async (content: string, index: number) => {
|
||||||
try {
|
const success = await clipboardCopy(content, t('keys.copied'))
|
||||||
await navigator.clipboard.writeText(content)
|
if (success) {
|
||||||
copiedIndex.value = index
|
copiedIndex.value = index
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copiedIndex.value = null
|
copiedIndex.value = null
|
||||||
}, 2000)
|
}, 2000)
|
||||||
} catch (error) {
|
|
||||||
appStore.showError(t('common.copyFailed'))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,40 +1,65 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useAppStore } from '@/stores/app'
|
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() {
|
export function useClipboard() {
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const copied = ref(false)
|
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
|
if (!text) return false
|
||||||
|
|
||||||
try {
|
let success = false
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
copied.value = true
|
if (isClipboardSupported()) {
|
||||||
appStore.showSuccess(successMessage)
|
try {
|
||||||
setTimeout(() => {
|
await navigator.clipboard.writeText(text)
|
||||||
copied.value = false
|
success = true
|
||||||
}, 2000)
|
} catch {
|
||||||
return true
|
success = fallbackCopy(text)
|
||||||
} catch {
|
}
|
||||||
// Fallback for older browsers
|
} else {
|
||||||
const input = document.createElement('input')
|
success = fallbackCopy(text)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
copied.value = true
|
||||||
|
appStore.showSuccess(successMessage)
|
||||||
|
setTimeout(() => {
|
||||||
|
copied.value = false
|
||||||
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
appStore.showError('Copy failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
return success
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return { copied, copyToClipboard }
|
||||||
copied,
|
|
||||||
copyToClipboard
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -418,6 +418,7 @@
|
|||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
import { adminAPI } from '@/api/admin'
|
import { adminAPI } from '@/api/admin'
|
||||||
import { formatDateTime } from '@/utils/format'
|
import { formatDateTime } from '@/utils/format'
|
||||||
import type { RedeemCode, RedeemCodeType, Group } from '@/types'
|
import type { RedeemCode, RedeemCodeType, Group } from '@/types'
|
||||||
@@ -431,6 +432,7 @@ import Select from '@/components/common/Select.vue'
|
|||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const { copyToClipboard: clipboardCopy } = useClipboard()
|
||||||
|
|
||||||
const showGenerateDialog = ref(false)
|
const showGenerateDialog = ref(false)
|
||||||
const showResultDialog = ref(false)
|
const showResultDialog = ref(false)
|
||||||
@@ -618,15 +620,12 @@ const handleGenerateCodes = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const copyToClipboard = async (text: string) => {
|
const copyToClipboard = async (text: string) => {
|
||||||
try {
|
const success = await clipboardCopy(text, t('admin.redeem.copied'))
|
||||||
await navigator.clipboard.writeText(text)
|
if (success) {
|
||||||
copiedCode.value = text
|
copiedCode.value = text
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copiedCode.value = null
|
copiedCode.value = null
|
||||||
}, 2000)
|
}, 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 { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
import { formatDateTime } from '@/utils/format'
|
import { formatDateTime } from '@/utils/format'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -1191,6 +1192,7 @@ import Select from '@/components/common/Select.vue'
|
|||||||
import GroupBadge from '@/components/common/GroupBadge.vue'
|
import GroupBadge from '@/components/common/GroupBadge.vue'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const { copyToClipboard: clipboardCopy } = useClipboard()
|
||||||
|
|
||||||
const columns = computed<Column[]>(() => [
|
const columns = computed<Column[]>(() => [
|
||||||
{ key: 'email', label: t('admin.users.columns.user'), sortable: true },
|
{ key: 'email', label: t('admin.users.columns.user'), sortable: true },
|
||||||
@@ -1312,27 +1314,23 @@ const generateEditPassword = () => {
|
|||||||
|
|
||||||
const copyPassword = async () => {
|
const copyPassword = async () => {
|
||||||
if (!createForm.password) return
|
if (!createForm.password) return
|
||||||
try {
|
const success = await clipboardCopy(createForm.password, t('admin.users.passwordCopied'))
|
||||||
await navigator.clipboard.writeText(createForm.password)
|
if (success) {
|
||||||
passwordCopied.value = true
|
passwordCopied.value = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
passwordCopied.value = false
|
passwordCopied.value = false
|
||||||
}, 2000)
|
}, 2000)
|
||||||
} catch (error) {
|
|
||||||
appStore.showError(t('common.copyFailed'))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyEditPassword = async () => {
|
const copyEditPassword = async () => {
|
||||||
if (!editForm.password) return
|
if (!editForm.password) return
|
||||||
try {
|
const success = await clipboardCopy(editForm.password, t('admin.users.passwordCopied'))
|
||||||
await navigator.clipboard.writeText(editForm.password)
|
if (success) {
|
||||||
editPasswordCopied.value = true
|
editPasswordCopied.value = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
editPasswordCopied.value = false
|
editPasswordCopied.value = false
|
||||||
}, 2000)
|
}, 2000)
|
||||||
} catch (error) {
|
|
||||||
appStore.showError(t('common.copyFailed'))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -493,6 +493,7 @@
|
|||||||
import { ref, computed, onMounted, onUnmounted, type ComponentPublicInstance } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, type ComponentPublicInstance } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
import { keysAPI, authAPI, usageAPI, userGroupsAPI } from '@/api'
|
import { keysAPI, authAPI, usageAPI, userGroupsAPI } from '@/api'
|
||||||
@@ -520,6 +521,7 @@ interface GroupOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
const { copyToClipboard: clipboardCopy } = useClipboard()
|
||||||
|
|
||||||
const columns = computed<Column[]>(() => [
|
const columns = computed<Column[]>(() => [
|
||||||
{ key: 'name', label: t('common.name'), sortable: true },
|
{ key: 'name', label: t('common.name'), sortable: true },
|
||||||
@@ -616,14 +618,12 @@ const maskKey = (key: string): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const copyToClipboard = async (text: string, keyId: number) => {
|
const copyToClipboard = async (text: string, keyId: number) => {
|
||||||
try {
|
const success = await clipboardCopy(text, t('keys.copied'))
|
||||||
await navigator.clipboard.writeText(text)
|
if (success) {
|
||||||
copiedKeyId.value = keyId
|
copiedKeyId.value = keyId
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copiedKeyId.value = null
|
copiedKeyId.value = null
|
||||||
}, 2000)
|
}, 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