Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
016d7ef6
Commit
016d7ef6
authored
Dec 27, 2025
by
shaw
Browse files
feat: 增强前端clipboard功能
parent
f1e47291
Changes
7
Expand all
Hide whitespace changes
Inline
Side-by-side
frontend/src/components/account/AccountTestModal.vue
View file @
016d7ef6
...
@@ -280,10 +280,12 @@
...
@@ -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: {
...
@@ -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
>
frontend/src/components/keys/UseKeyModal.vue
View file @
016d7ef6
...
@@ -119,7 +119,7 @@
...
@@ -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
{
use
AppStore
}
from
'
@/
stores/app
'
import
{
use
Clipboard
}
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>()
...
@@ -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')}`
...
@@ -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
>
frontend/src/composables/useClipboard.ts
View file @
016d7ef6
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
()
if
(
success
)
{
document
.
execCommand
(
'
copy
'
)
document
.
body
.
removeChild
(
input
)
copied
.
value
=
true
copied
.
value
=
true
appStore
.
showSuccess
(
successMessage
)
appStore
.
showSuccess
(
successMessage
)
setTimeout
(()
=>
{
setTimeout
(()
=>
{
copied
.
value
=
false
copied
.
value
=
false
},
2000
)
},
2000
)
return
true
}
else
{
appStore
.
showError
(
'
Copy failed
'
)
}
}
}
return
{
return
success
copied
,
copyToClipboard
}
}
return
{
copied
,
copyToClipboard
}
}
}
frontend/src/views/admin/RedeemView.vue
View file @
016d7ef6
...
@@ -418,6 +418,7 @@
...
@@ -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'
...
@@ -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 () => {
...
@@ -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
)
}
}
}
}
...
...
frontend/src/views/admin/UsersView.vue
View file @
016d7ef6
...
@@ -1173,6 +1173,7 @@
...
@@ -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'
...
@@ -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 = () => {
...
@@ -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
'
))
}
}
}
}
...
...
frontend/src/views/user/KeysView.vue
View file @
016d7ef6
...
@@ -493,6 +493,7 @@
...
@@ -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 {
...
@@ -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 => {
...
@@ -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
'
))
}
}
}
}
...
...
frontend/tsconfig.node.tsbuildinfo
deleted
100644 → 0
View file @
f1e47291
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment