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
20062b44
"git@web.lueluesay.top:chenxi/sub2api.git" did not exist on "7ef7fd19e7ca0d46bc7bafa58a578a9b4d1a29ba"
Commit
20062b44
authored
Apr 21, 2026
by
IanShaw027
Browse files
frontend: normalize profile and admin i18n cleanup
parent
a6b919eb
Changes
10
Expand all
Hide whitespace changes
Inline
Side-by-side
frontend/src/components/user/profile/ProfileAvatarCard.vue
View file @
20062b44
...
...
@@ -92,7 +92,7 @@ const avatarQualitySteps = [0.92, 0.84, 0.76, 0.68, 0.6, 0.52, 0.44, 0.36]
const
avatarDraft
=
ref
(
''
)
const
avatarSaving
=
ref
(
false
)
const
displayName
=
computed
(()
=>
props
.
user
?.
username
?.
trim
()
||
props
.
user
?.
email
?.
trim
()
||
'
U
ser
'
)
const
displayName
=
computed
(()
=>
props
.
user
?.
username
?.
trim
()
||
props
.
user
?.
email
?.
trim
()
||
t
(
'
profile.u
ser
'
)
)
const
avatarInitial
=
computed
(()
=>
displayName
.
value
.
charAt
(
0
).
toUpperCase
()
||
'
U
'
)
const
avatarPreviewUrl
=
computed
(()
=>
avatarDraft
.
value
.
trim
()
||
props
.
user
?.
avatar_url
?.
trim
()
||
''
)
...
...
frontend/src/components/user/profile/ProfileInfoCard.vue
View file @
20062b44
...
...
@@ -29,7 +29,11 @@
<span
:class=
"['badge', user?.status === 'active' ? 'badge-success' : 'badge-danger']"
>
{{
user
?.
status
}}
{{
user
?.
status
===
'
active
'
?
t
(
'
common.active
'
)
:
t
(
'
common.disabled
'
)
}}
</span>
</div>
</div>
...
...
@@ -80,7 +84,7 @@ const props = defineProps<{
const
{
t
}
=
useI18n
()
const
avatarUrl
=
computed
(()
=>
props
.
user
?.
avatar_url
?.
trim
()
||
''
)
const
displayName
=
computed
(()
=>
props
.
user
?.
username
?.
trim
()
||
props
.
user
?.
email
?.
trim
()
||
'
U
ser
'
)
const
displayName
=
computed
(()
=>
props
.
user
?.
username
?.
trim
()
||
props
.
user
?.
email
?.
trim
()
||
t
(
'
profile.u
ser
'
)
)
const
avatarInitial
=
computed
(()
=>
displayName
.
value
.
charAt
(
0
).
toUpperCase
()
||
'
U
'
)
const
providerLabels
=
computed
<
Record
<
UserAuthProvider
,
string
>>
(()
=>
({
...
...
frontend/src/components/user/profile/ProfilePasswordForm.vue
View file @
20062b44
...
...
@@ -50,12 +50,6 @@
autocomplete=
"new-password"
class=
"input"
/>
<p
v-if=
"form.new_password && form.confirm_password && form.new_password !== form.confirm_password"
class=
"input-error-text"
>
{{
t
(
'
profile.passwordsNotMatch
'
)
}}
</p>
</div>
<div
class=
"flex justify-end pt-4"
>
...
...
frontend/src/components/user/profile/TotpDisableDialog.vue
View file @
20062b44
...
...
@@ -63,11 +63,6 @@
/>
<
/div
>
<!--
Error
-->
<
div
v
-
if
=
"
error
"
class
=
"
rounded-lg bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-400
"
>
{{
error
}}
<
/div
>
<!--
Actions
-->
<
div
class
=
"
flex justify-end gap-3 pt-4
"
>
<
button
type
=
"
button
"
class
=
"
btn btn-secondary
"
@
click
=
"
$emit('close')
"
>
...
...
@@ -104,7 +99,6 @@ const appStore = useAppStore()
const
methodLoading
=
ref
(
true
)
const
verificationMethod
=
ref
<
'
email
'
|
'
password
'
>
(
'
password
'
)
const
loading
=
ref
(
false
)
const
error
=
ref
(
''
)
const
sendingCode
=
ref
(
false
)
const
codeCooldown
=
ref
(
0
)
const
cooldownTimer
=
ref
<
ReturnType
<
typeof
setInterval
>
|
null
>
(
null
)
...
...
@@ -164,7 +158,6 @@ const handleDisable = async () => {
if
(
!
canSubmit
.
value
)
return
loading
.
value
=
true
error
.
value
=
''
try
{
const
request
=
verificationMethod
.
value
===
'
email
'
...
...
@@ -175,7 +168,7 @@ const handleDisable = async () => {
appStore
.
showSuccess
(
t
(
'
profile.totp.disableSuccess
'
))
emit
(
'
success
'
)
}
catch
(
err
:
any
)
{
error
.
value
=
err
.
response
?.
data
?.
message
||
t
(
'
profile.totp.disableFailed
'
)
appStore
.
showError
(
err
.
response
?.
data
?.
message
||
t
(
'
profile.totp.disableFailed
'
)
)
}
finally
{
loading
.
value
=
false
}
...
...
frontend/src/components/user/profile/TotpSetupModal.vue
View file @
20062b44
...
...
@@ -61,10 +61,6 @@
<
/div
>
<
/div
>
<
div
v
-
if
=
"
verifyError
"
class
=
"
rounded-lg bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-400
"
>
{{
verifyError
}}
<
/div
>
<
div
class
=
"
flex justify-end gap-3 pt-4
"
>
<
button
type
=
"
button
"
class
=
"
btn btn-secondary
"
@
click
=
"
$emit('close')
"
>
{{
t
(
'
common.cancel
'
)
}}
...
...
@@ -151,10 +147,6 @@
<
/div
>
<
/div
>
<
div
v
-
if
=
"
error
"
class
=
"
mb-4 rounded-lg bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-400
"
>
{{
error
}}
<
/div
>
<
div
class
=
"
flex justify-end gap-3
"
>
<
button
type
=
"
button
"
class
=
"
btn btn-secondary
"
@
click
=
"
step = 1
"
>
{{
t
(
'
common.back
'
)
}}
...
...
@@ -195,7 +187,6 @@ const step = ref(0)
const
methodLoading
=
ref
(
true
)
const
verificationMethod
=
ref
<
'
email
'
|
'
password
'
>
(
'
password
'
)
const
verifyForm
=
ref
({
emailCode
:
''
,
password
:
''
}
)
const
verifyError
=
ref
(
''
)
const
sendingCode
=
ref
(
false
)
const
codeCooldown
=
ref
(
0
)
const
cooldownTimer
=
ref
<
ReturnType
<
typeof
setInterval
>
|
null
>
(
null
)
...
...
@@ -203,7 +194,6 @@ const cooldownTimer = ref<ReturnType<typeof setInterval> | null>(null)
const
setupLoading
=
ref
(
false
)
const
setupData
=
ref
<
TotpSetupResponse
|
null
>
(
null
)
const
verifying
=
ref
(
false
)
const
error
=
ref
(
''
)
const
code
=
ref
<
string
[]
>
([
''
,
''
,
''
,
''
,
''
,
''
])
const
inputRefs
=
ref
<
(
HTMLInputElement
|
null
)[]
>
([])
const
qrCodeDataUrl
=
ref
(
''
)
...
...
@@ -361,7 +351,6 @@ const handleSendCode = async () => {
const
handleVerifyAndSetup
=
async
()
=>
{
setupLoading
.
value
=
true
verifyError
.
value
=
''
try
{
const
request
=
verificationMethod
.
value
===
'
email
'
...
...
@@ -371,7 +360,7 @@ const handleVerifyAndSetup = async () => {
setupData
.
value
=
await
totpAPI
.
initiateSetup
(
request
)
step
.
value
=
1
}
catch
(
err
:
any
)
{
verifyError
.
value
=
err
.
response
?.
data
?.
message
||
t
(
'
profile.totp.setupFailed
'
)
appStore
.
showError
(
err
.
response
?.
data
?.
message
||
t
(
'
profile.totp.setupFailed
'
)
)
}
finally
{
setupLoading
.
value
=
false
}
...
...
@@ -382,7 +371,6 @@ const handleVerify = async () => {
if
(
totpCode
.
length
!==
6
||
!
setupData
.
value
)
return
verifying
.
value
=
true
error
.
value
=
''
try
{
await
totpAPI
.
enable
({
...
...
@@ -392,7 +380,7 @@ const handleVerify = async () => {
appStore
.
showSuccess
(
t
(
'
profile.totp.enableSuccess
'
))
emit
(
'
success
'
)
}
catch
(
err
:
any
)
{
error
.
value
=
err
.
response
?.
data
?.
message
||
t
(
'
profile.totp.verifyFailed
'
)
appStore
.
showError
(
err
.
response
?.
data
?.
message
||
t
(
'
profile.totp.verifyFailed
'
)
)
code
.
value
=
[
''
,
''
,
''
,
''
,
''
,
''
]
nextTick
(()
=>
{
inputRefs
.
value
[
0
]?.
focus
()
...
...
frontend/src/components/user/profile/__tests__/ProfilePasswordForm.spec.ts
0 → 100644
View file @
20062b44
import
{
mount
}
from
'
@vue/test-utils
'
import
{
describe
,
expect
,
it
,
vi
}
from
'
vitest
'
import
ProfilePasswordForm
from
'
@/components/user/profile/ProfilePasswordForm.vue
'
const
{
changePasswordMock
,
showSuccessMock
,
showErrorMock
}
=
vi
.
hoisted
(()
=>
({
changePasswordMock
:
vi
.
fn
(),
showSuccessMock
:
vi
.
fn
(),
showErrorMock
:
vi
.
fn
()
}))
vi
.
mock
(
'
@/api
'
,
()
=>
({
userAPI
:
{
changePassword
:
changePasswordMock
}
}))
vi
.
mock
(
'
@/stores/app
'
,
()
=>
({
useAppStore
:
()
=>
({
showSuccess
:
showSuccessMock
,
showError
:
showErrorMock
})
}))
vi
.
mock
(
'
vue-i18n
'
,
async
(
importOriginal
)
=>
{
const
actual
=
await
importOriginal
<
typeof
import
(
'
vue-i18n
'
)
>
()
return
{
...
actual
,
useI18n
:
()
=>
({
t
:
(
key
:
string
)
=>
{
const
translations
:
Record
<
string
,
string
>
=
{
'
profile.changePassword
'
:
'
Change Password
'
,
'
profile.currentPassword
'
:
'
Current Password
'
,
'
profile.newPassword
'
:
'
New Password
'
,
'
profile.confirmNewPassword
'
:
'
Confirm New Password
'
,
'
profile.passwordHint
'
:
'
Password must be at least 8 characters long
'
,
'
profile.changingPassword
'
:
'
Changing...
'
,
'
profile.changePasswordButton
'
:
'
Change Password
'
,
'
profile.passwordsNotMatch
'
:
'
New passwords do not match
'
,
'
profile.passwordTooShort
'
:
'
Password must be at least 8 characters long
'
,
'
profile.passwordChangeSuccess
'
:
'
Password changed successfully
'
,
'
profile.passwordChangeFailed
'
:
'
Failed to change password
'
}
return
translations
[
key
]
??
key
}
})
}
})
describe
(
'
ProfilePasswordForm
'
,
()
=>
{
it
(
'
shows validation failures as toast messages instead of inline errors
'
,
async
()
=>
{
const
wrapper
=
mount
(
ProfilePasswordForm
)
await
wrapper
.
get
(
'
#old_password
'
).
setValue
(
'
old-password
'
)
await
wrapper
.
get
(
'
#new_password
'
).
setValue
(
'
new-password
'
)
await
wrapper
.
get
(
'
#confirm_password
'
).
setValue
(
'
different-password
'
)
await
wrapper
.
get
(
'
form
'
).
trigger
(
'
submit.prevent
'
)
expect
(
changePasswordMock
).
not
.
toHaveBeenCalled
()
expect
(
showErrorMock
).
toHaveBeenCalledWith
(
'
New passwords do not match
'
)
expect
(
wrapper
.
find
(
'
.input-error-text
'
).
exists
()).
toBe
(
false
)
})
it
(
'
shows API failures as toast messages
'
,
async
()
=>
{
changePasswordMock
.
mockRejectedValue
({
response
:
{
data
:
{
detail
:
'
backend failure
'
}
}
})
const
wrapper
=
mount
(
ProfilePasswordForm
)
await
wrapper
.
get
(
'
#old_password
'
).
setValue
(
'
old-password
'
)
await
wrapper
.
get
(
'
#new_password
'
).
setValue
(
'
new-password
'
)
await
wrapper
.
get
(
'
#confirm_password
'
).
setValue
(
'
new-password
'
)
await
wrapper
.
get
(
'
form
'
).
trigger
(
'
submit.prevent
'
)
expect
(
changePasswordMock
).
toHaveBeenCalledWith
(
'
old-password
'
,
'
new-password
'
)
expect
(
showErrorMock
).
toHaveBeenCalledWith
(
'
backend failure
'
)
expect
(
wrapper
.
find
(
'
.input-error-text
'
).
exists
()).
toBe
(
false
)
})
})
frontend/src/components/user/profile/__tests__/totp-timer-cleanup.spec.ts
View file @
20062b44
...
...
@@ -7,7 +7,10 @@ const mocks = vi.hoisted(() => ({
showSuccess
:
vi
.
fn
(),
showError
:
vi
.
fn
(),
getVerificationMethod
:
vi
.
fn
(),
sendVerifyCode
:
vi
.
fn
()
sendVerifyCode
:
vi
.
fn
(),
initiateSetup
:
vi
.
fn
(),
enable
:
vi
.
fn
(),
disable
:
vi
.
fn
()
}))
vi
.
mock
(
'
vue-i18n
'
,
()
=>
({
...
...
@@ -27,9 +30,9 @@ vi.mock('@/api', () => ({
totpAPI
:
{
getVerificationMethod
:
mocks
.
getVerificationMethod
,
sendVerifyCode
:
mocks
.
sendVerifyCode
,
initiateSetup
:
vi
.
fn
()
,
enable
:
vi
.
fn
()
,
disable
:
vi
.
fn
()
initiateSetup
:
mocks
.
initiateSetup
,
enable
:
mocks
.
enable
,
disable
:
mocks
.
disable
}
}))
...
...
@@ -49,9 +52,19 @@ describe('TOTP 弹窗定时器清理', () => {
mocks
.
showError
.
mockReset
()
mocks
.
getVerificationMethod
.
mockReset
()
mocks
.
sendVerifyCode
.
mockReset
()
mocks
.
initiateSetup
.
mockReset
()
mocks
.
enable
.
mockReset
()
mocks
.
disable
.
mockReset
()
mocks
.
getVerificationMethod
.
mockResolvedValue
({
method
:
'
email
'
})
mocks
.
sendVerifyCode
.
mockResolvedValue
({
success
:
true
})
mocks
.
initiateSetup
.
mockResolvedValue
({
qr_code_url
:
'
otpauth://totp/Sub2API:test?secret=ABC123
'
,
secret
:
'
ABC123
'
,
setup_token
:
'
setup-token
'
})
mocks
.
enable
.
mockResolvedValue
({
success
:
true
})
mocks
.
disable
.
mockResolvedValue
({
success
:
true
})
setIntervalSpy
=
vi
.
spyOn
(
window
,
'
setInterval
'
).
mockImplementation
(((
handler
:
TimerHandler
)
=>
{
void
handler
...
...
@@ -105,4 +118,40 @@ describe('TOTP 弹窗定时器清理', () => {
expect
(
clearIntervalSpy
).
toHaveBeenCalledWith
(
timerId
)
})
it
(
'
TotpSetupModal 失败时改用 toast 并不渲染内联错误
'
,
async
()
=>
{
mocks
.
getVerificationMethod
.
mockResolvedValue
({
method
:
'
password
'
})
mocks
.
initiateSetup
.
mockRejectedValue
({
response
:
{
data
:
{
message
:
'
setup failed
'
}
}
})
const
wrapper
=
mount
(
TotpSetupModal
)
await
flushPromises
()
await
wrapper
.
get
(
'
input[type="password"]
'
).
setValue
(
'
correct horse battery staple
'
)
await
wrapper
.
get
(
'
button[type="button"].btn-primary
'
).
trigger
(
'
click
'
)
await
flushPromises
()
expect
(
mocks
.
showError
).
toHaveBeenCalledWith
(
'
setup failed
'
)
expect
(
wrapper
.
text
()).
not
.
toContain
(
'
setup failed
'
)
expect
(
wrapper
.
find
(
'
.bg-red-50
'
).
exists
()).
toBe
(
false
)
})
it
(
'
TotpDisableDialog 失败时改用 toast 并不渲染内联错误
'
,
async
()
=>
{
mocks
.
getVerificationMethod
.
mockResolvedValue
({
method
:
'
password
'
})
mocks
.
disable
.
mockRejectedValue
({
response
:
{
data
:
{
message
:
'
disable failed
'
}
}
})
const
wrapper
=
mount
(
TotpDisableDialog
)
await
flushPromises
()
await
wrapper
.
get
(
'
input[type="password"]
'
).
setValue
(
'
correct horse battery staple
'
)
await
wrapper
.
get
(
'
form
'
).
trigger
(
'
submit.prevent
'
)
await
flushPromises
()
expect
(
mocks
.
showError
).
toHaveBeenCalledWith
(
'
disable failed
'
)
expect
(
wrapper
.
text
()).
not
.
toContain
(
'
disable failed
'
)
expect
(
wrapper
.
find
(
'
.bg-red-50
'
).
exists
()).
toBe
(
false
)
})
})
frontend/src/views/admin/SettingsView.vue
View file @
20062b44
This diff is collapsed.
Click to expand it.
frontend/src/views/admin/UsersView.vue
View file @
20062b44
...
...
@@ -700,7 +700,7 @@ const getAttributeValue = (userId: number, attrId: number): string => {
// All possible columns (for column settings)
const
allColumns
=
computed
<
Column
[]
>
(()
=>
[
{
key
:
'
email
'
,
label
:
t
(
'
admin.users.columns.user
'
),
sortable
:
true
},
{
key
:
'
id
'
,
label
:
'
ID
'
,
sortable
:
true
},
{
key
:
'
id
'
,
label
:
t
(
'
admin.users.columns.id
'
)
,
sortable
:
true
},
{
key
:
'
username
'
,
label
:
t
(
'
admin.users.columns.username
'
),
sortable
:
true
},
{
key
:
'
notes
'
,
label
:
t
(
'
admin.users.columns.notes
'
),
sortable
:
false
},
// Dynamic attribute columns
...
...
frontend/src/views/admin/__tests__/SettingsView.spec.ts
View file @
20062b44
...
...
@@ -93,10 +93,61 @@ vi.mock("@/utils/apiError", () => ({
vi
.
mock
(
"
vue-i18n
"
,
async
()
=>
{
const
actual
=
await
vi
.
importActual
<
typeof
import
(
"
vue-i18n
"
)
>
(
"
vue-i18n
"
);
const
translations
:
Record
<
string
,
string
>
=
{
"
admin.settings.wechatConnect.title
"
:
"
微信登录
"
,
"
admin.settings.wechatConnect.description
"
:
"
用于微信开放平台或公众号/小程序的第三方登录配置。
"
,
"
admin.settings.wechatConnect.enabledLabel
"
:
"
启用微信登录
"
,
"
admin.settings.wechatConnect.enabledHint
"
:
"
开启后可使用微信第三方登录回调与授权配置。
"
,
"
admin.settings.wechatConnect.appIdLabel
"
:
"
AppID
"
,
"
admin.settings.wechatConnect.appIdPlaceholder
"
:
"
微信开放平台 AppID
"
,
"
admin.settings.wechatConnect.appSecretLabel
"
:
"
AppSecret
"
,
"
admin.settings.wechatConnect.appSecretConfiguredPlaceholder
"
:
"
密钥已配置,留空以保留当前值。
"
,
"
admin.settings.wechatConnect.appSecretPlaceholder
"
:
"
微信开放平台 AppSecret
"
,
"
admin.settings.wechatConnect.appSecretConfiguredHint
"
:
"
密钥已配置,留空以保留当前值。
"
,
"
admin.settings.wechatConnect.appSecretHint
"
:
"
填写后会覆盖当前微信密钥。
"
,
"
admin.settings.wechatConnect.modeLabel
"
:
"
模式
"
,
"
admin.settings.wechatConnect.openModeLabel
"
:
"
非微信环境使用开放平台
"
,
"
admin.settings.wechatConnect.openModeHint
"
:
"
浏览器不在微信内时,自动走开放平台扫码授权。
"
,
"
admin.settings.wechatConnect.mpModeLabel
"
:
"
微信环境使用公众号
"
,
"
admin.settings.wechatConnect.mpModeHint
"
:
"
浏览器在微信内时,自动走公众号授权。
"
,
"
admin.settings.wechatConnect.redirectUrlLabel
"
:
"
回调地址
"
,
"
admin.settings.wechatConnect.redirectUrlPlaceholder
"
:
"
https://your-site.com/api/v1/auth/oauth/wechat/callback
"
,
"
admin.settings.wechatConnect.generateAndCopy
"
:
"
使用当前站点生成并复制
"
,
"
admin.settings.wechatConnect.redirectUrlSetAndCopied
"
:
"
已使用当前站点生成回调地址并复制到剪贴板
"
,
"
admin.settings.wechatConnect.frontendRedirectUrlLabel
"
:
"
前端回调地址
"
,
"
admin.settings.wechatConnect.frontendRedirectUrlPlaceholder
"
:
"
/auth/wechat/callback
"
,
"
admin.settings.wechatConnect.frontendRedirectUrlHint
"
:
"
通常用于前端路由回调地址,需与后端配置保持一致。
"
,
"
admin.settings.authSourceDefaults.title
"
:
"
认证来源默认值
"
,
"
admin.settings.authSourceDefaults.description
"
:
"
按注册来源配置新用户默认余额、并发、订阅与授权策略。
"
,
"
admin.settings.authSourceDefaults.requireEmailLabel
"
:
"
第三方注册强制补充邮箱
"
,
"
admin.settings.authSourceDefaults.requireEmailHint
"
:
"
启用后,Linux DO、OIDC、微信注册缺少邮箱时必须先补充邮箱地址。
"
,
"
admin.settings.authSourceDefaults.enabledHint
"
:
"
以下默认值会在该来源注册新用户时发放;首次绑定时授权仅作用于已有账号绑定该来源。
"
,
"
admin.settings.authSourceDefaults.sources.email.title
"
:
"
邮箱注册
"
,
"
admin.settings.authSourceDefaults.sources.email.description
"
:
"
适用于邮箱密码注册的新用户默认配额。
"
,
"
admin.settings.authSourceDefaults.sources.linuxdo.title
"
:
"
Linux DO 登录
"
,
"
admin.settings.authSourceDefaults.sources.linuxdo.description
"
:
"
适用于 Linux DO 第三方注册的新用户默认配额。
"
,
"
admin.settings.authSourceDefaults.sources.oidc.title
"
:
"
OIDC 登录
"
,
"
admin.settings.authSourceDefaults.sources.oidc.description
"
:
"
适用于 OIDC 第三方注册的新用户默认配额。
"
,
"
admin.settings.authSourceDefaults.sources.wechat.title
"
:
"
微信登录
"
,
"
admin.settings.authSourceDefaults.sources.wechat.description
"
:
"
适用于微信第三方注册的新用户默认配额。
"
,
"
admin.settings.authSourceDefaults.grantOnFirstBindLabel
"
:
"
首次绑定时授权
"
,
"
admin.settings.authSourceDefaults.grantOnFirstBindHint
"
:
"
已有账号首次绑定该来源时发放默认权益。
"
,
"
admin.settings.authSourceDefaults.defaultSubscriptionsLabel
"
:
"
默认订阅
"
,
"
admin.settings.authSourceDefaults.defaultSubscriptionsHint
"
:
"
仅对当前认证来源生效,未配置时不追加来源专属订阅。
"
,
"
admin.settings.authSourceDefaults.noSourceSubscriptions
"
:
"
当前来源未配置专属默认订阅。
"
,
"
admin.settings.paymentVisibleMethods.methodLabel
"
:
"
{title} 可见方式
"
,
"
admin.settings.paymentVisibleMethods.methodHint
"
:
"
控制前台结算页是否展示该方式,以及展示时使用的来源键。
"
,
"
admin.settings.paymentVisibleMethods.sourceLabel
"
:
"
支付来源
"
,
"
admin.settings.paymentVisibleMethods.sourceHint
"
:
"
启用后必须明确选择一个来源;未配置状态不会对外展示该支付方式。
"
,
"
admin.settings.paymentVisibleMethods.sourceRequiredError
"
:
"
{title} 已启用,请先选择支付来源。
"
,
"
admin.settings.openaiExperimentalScheduler.title
"
:
"
OpenAI 实验调度策略
"
,
"
admin.settings.openaiExperimentalScheduler.description
"
:
"
默认关闭。开启后仅影响本网关在 OpenAI 账号间的实验性调度选择逻辑,不代表上游 OpenAI 官方能力。
"
,
};
return
{
...
actual
,
useI18n
:
()
=>
({
t
:
(
key
:
string
)
=>
key
,
t
:
(
key
:
string
,
params
?:
Record
<
string
,
string
>
)
=>
(
translations
[
key
]
??
key
).
replace
(
/
\{(\w
+
)\}
/g
,
(
_
,
token
)
=>
params
?.[
token
]
??
`{
${
token
}
}`
),
locale
:
ref
(
"
zh-CN
"
),
}),
};
...
...
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