feat: 增强前端clipboard功能
This commit is contained in:
1 parent
f1e47291cd
commit
016d7ef645
7 files changed
+73
-52
No files matched your search
@@ -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'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"fileNames":["./node_modules/typescript/lib/lib.d.ts","./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.webworker.importscripts.d.ts","./node_modules/typescript/lib/lib.scripthost.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/@types/node/compatibility/disposable.d.ts","./node_modules/@types/node/compatibility/indexable.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/compatibility/index.d.ts","./node_modules/@types/node/ts5.6/globals.typedarray.d.ts","./node_modules/@types/node/ts5.6/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/events.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/file.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/filereader.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/asLine truncated
|
||||
Reference in new issue
Block a user