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
da1d2600
Commit
da1d2600
authored
Apr 22, 2026
by
IanShaw027
Browse files
Merge branch 'main' into rebuild/auth-identity-foundation
parents
e4cfcae6
78f691d2
Changes
67
Hide whitespace changes
Inline
Side-by-side
backend/internal/service/upstream_response_limit.go
View file @
da1d2600
...
@@ -12,7 +12,9 @@ import (
...
@@ -12,7 +12,9 @@ import (
var
ErrUpstreamResponseBodyTooLarge
=
errors
.
New
(
"upstream response body too large"
)
var
ErrUpstreamResponseBodyTooLarge
=
errors
.
New
(
"upstream response body too large"
)
const
defaultUpstreamResponseReadMaxBytes
int64
=
8
*
1024
*
1024
// defaultUpstreamResponseReadMaxBytes 源自 config.DefaultUpstreamResponseReadMaxBytes,
// 仅在 cfg 为 nil 时作为兜底(测试或极端场景)。
const
defaultUpstreamResponseReadMaxBytes
=
config
.
DefaultUpstreamResponseReadMaxBytes
func
resolveUpstreamResponseReadLimit
(
cfg
*
config
.
Config
)
int64
{
func
resolveUpstreamResponseReadLimit
(
cfg
*
config
.
Config
)
int64
{
if
cfg
!=
nil
&&
cfg
.
Gateway
.
UpstreamResponseReadMaxBytes
>
0
{
if
cfg
!=
nil
&&
cfg
.
Gateway
.
UpstreamResponseReadMaxBytes
>
0
{
...
...
frontend/src/components/account/AccountStatusIndicator.vue
View file @
da1d2600
...
@@ -284,6 +284,16 @@ const hasError = computed(() => {
...
@@ -284,6 +284,16 @@ const hasError = computed(() => {
return
props
.
account
.
status
===
'
error
'
return
props
.
account
.
status
===
'
error
'
})
})
const
isQuotaExceeded
=
computed
(()
=>
{
const
exceeded
=
(
used
?:
number
|
null
,
limit
?:
number
|
null
)
=>
typeof
limit
===
'
number
'
&&
limit
>
0
&&
typeof
used
===
'
number
'
&&
used
>=
limit
return
(
exceeded
(
props
.
account
.
quota_used
,
props
.
account
.
quota_limit
)
||
exceeded
(
props
.
account
.
quota_daily_used
,
props
.
account
.
quota_daily_limit
)
||
exceeded
(
props
.
account
.
quota_weekly_used
,
props
.
account
.
quota_weekly_limit
)
)
})
// Computed: countdown text for rate limit (429)
// Computed: countdown text for rate limit (429)
const
rateLimitCountdown
=
computed
(()
=>
{
const
rateLimitCountdown
=
computed
(()
=>
{
return
formatCountdown
(
props
.
account
.
rate_limit_reset_at
)
return
formatCountdown
(
props
.
account
.
rate_limit_reset_at
)
...
@@ -307,19 +317,16 @@ const statusClass = computed(() => {
...
@@ -307,19 +317,16 @@ const statusClass = computed(() => {
if
(
isTempUnschedulable
.
value
)
{
if
(
isTempUnschedulable
.
value
)
{
return
'
badge-warning
'
return
'
badge-warning
'
}
}
if
(
props
.
account
.
status
!==
'
active
'
)
{
return
props
.
account
.
status
===
'
error
'
?
'
badge-danger
'
:
'
badge-gray
'
}
if
(
isQuotaExceeded
.
value
)
{
return
'
badge-warning
'
}
if
(
!
props
.
account
.
schedulable
)
{
if
(
!
props
.
account
.
schedulable
)
{
return
'
badge-gray
'
return
'
badge-gray
'
}
}
switch
(
props
.
account
.
status
)
{
return
'
badge-success
'
case
'
active
'
:
return
'
badge-success
'
case
'
inactive
'
:
return
'
badge-gray
'
case
'
error
'
:
return
'
badge-danger
'
default
:
return
'
badge-gray
'
}
})
})
// Computed: status text
// Computed: status text
...
@@ -330,6 +337,12 @@ const statusText = computed(() => {
...
@@ -330,6 +337,12 @@ const statusText = computed(() => {
if
(
isTempUnschedulable
.
value
)
{
if
(
isTempUnschedulable
.
value
)
{
return
t
(
'
admin.accounts.status.tempUnschedulable
'
)
return
t
(
'
admin.accounts.status.tempUnschedulable
'
)
}
}
if
(
props
.
account
.
status
!==
'
active
'
)
{
return
t
(
`admin.accounts.status.
${
props
.
account
.
status
}
`
)
}
if
(
isQuotaExceeded
.
value
)
{
return
t
(
'
admin.accounts.status.quotaExceeded
'
)
}
if
(
!
props
.
account
.
schedulable
)
{
if
(
!
props
.
account
.
schedulable
)
{
return
t
(
'
admin.accounts.status.paused
'
)
return
t
(
'
admin.accounts.status.paused
'
)
}
}
...
...
frontend/src/components/account/EditAccountModal.vue
View file @
da1d2600
...
@@ -52,6 +52,10 @@
...
@@ -52,6 +52,10 @@
v-model=
"editApiKey"
v-model=
"editApiKey"
type=
"password"
type=
"password"
class=
"input font-mono"
class=
"input font-mono"
autocomplete=
"new-password"
data-1p-ignore
data-lpignore=
"true"
data-bwignore=
"true"
:placeholder=
"
:placeholder=
"
account.platform === 'openai'
account.platform === 'openai'
? 'sk-proj-...'
? 'sk-proj-...'
...
...
frontend/src/components/admin/group/GroupRateMultipliersModal.vue
View file @
da1d2600
...
@@ -166,7 +166,7 @@
...
@@ -166,7 +166,7 @@
<input
<input
type=
"number"
type=
"number"
step=
"0.001"
step=
"0.001"
min=
"0"
min=
"0
.001
"
autocomplete=
"off"
autocomplete=
"off"
:value=
"entry.rate_multiplier"
:value=
"entry.rate_multiplier"
class=
"hide-spinner w-20 rounded border border-gray-200 bg-white px-2 py-1 text-center text-sm font-medium transition-colors focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500/20 dark:border-dark-500 dark:bg-dark-700 dark:focus:border-primary-500"
class=
"hide-spinner w-20 rounded border border-gray-200 bg-white px-2 py-1 text-center text-sm font-medium transition-colors focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500/20 dark:border-dark-500 dark:bg-dark-700 dark:focus:border-primary-500"
...
...
frontend/src/components/admin/user/UserAllowedGroupsModal.vue
View file @
da1d2600
...
@@ -81,7 +81,7 @@
...
@@ -81,7 +81,7 @@
<
input
<
input
type
=
"
number
"
type
=
"
number
"
step
=
"
0.001
"
step
=
"
0.001
"
min
=
"
0
"
min
=
"
0
.001
"
:
value
=
"
config.customRate ?? ''
"
:
value
=
"
config.customRate ?? ''
"
@
input
=
"
updateCustomRate(config.groupId, ($event.target as HTMLInputElement).value)
"
@
input
=
"
updateCustomRate(config.groupId, ($event.target as HTMLInputElement).value)
"
:
placeholder
=
"
String(config.defaultRate)
"
:
placeholder
=
"
String(config.defaultRate)
"
...
@@ -139,7 +139,7 @@
...
@@ -139,7 +139,7 @@
<
input
<
input
type
=
"
number
"
type
=
"
number
"
step
=
"
0.001
"
step
=
"
0.001
"
min
=
"
0
"
min
=
"
0
.001
"
:
value
=
"
config.customRate ?? ''
"
:
value
=
"
config.customRate ?? ''
"
@
input
=
"
updateCustomRate(config.groupId, ($event.target as HTMLInputElement).value)
"
@
input
=
"
updateCustomRate(config.groupId, ($event.target as HTMLInputElement).value)
"
:
placeholder
=
"
String(config.defaultRate)
"
:
placeholder
=
"
String(config.defaultRate)
"
...
...
frontend/src/components/keys/UseKeyModal.vue
View file @
da1d2600
...
@@ -617,66 +617,6 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
...
@@ -617,66 +617,6 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
}
}
}
}
const
openaiModels
=
{
const
openaiModels
=
{
'
gpt-5-codex
'
:
{
name
:
'
GPT-5 Codex
'
,
limit
:
{
context
:
400000
,
output
:
128000
},
options
:
{
store
:
false
},
variants
:
{
low
:
{},
medium
:
{},
high
:
{}
}
},
'
gpt-5.1-codex
'
:
{
name
:
'
GPT-5.1 Codex
'
,
limit
:
{
context
:
400000
,
output
:
128000
},
options
:
{
store
:
false
},
variants
:
{
low
:
{},
medium
:
{},
high
:
{}
}
},
'
gpt-5.1-codex-max
'
:
{
name
:
'
GPT-5.1 Codex Max
'
,
limit
:
{
context
:
400000
,
output
:
128000
},
options
:
{
store
:
false
},
variants
:
{
low
:
{},
medium
:
{},
high
:
{}
}
},
'
gpt-5.1-codex-mini
'
:
{
name
:
'
GPT-5.1 Codex Mini
'
,
limit
:
{
context
:
400000
,
output
:
128000
},
options
:
{
store
:
false
},
variants
:
{
low
:
{},
medium
:
{},
high
:
{}
}
},
'
gpt-5.2
'
:
{
'
gpt-5.2
'
:
{
name
:
'
GPT-5.2
'
,
name
:
'
GPT-5.2
'
,
limit
:
{
limit
:
{
...
@@ -725,22 +665,6 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
...
@@ -725,22 +665,6 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
xhigh
:
{}
xhigh
:
{}
}
}
},
},
'
gpt-5.4-nano
'
:
{
name
:
'
GPT-5.4 Nano
'
,
limit
:
{
context
:
400000
,
output
:
128000
},
options
:
{
store
:
false
},
variants
:
{
low
:
{},
medium
:
{},
high
:
{},
xhigh
:
{}
}
},
'
gpt-5.3-codex-spark
'
:
{
'
gpt-5.3-codex-spark
'
:
{
name
:
'
GPT-5.3 Codex Spark
'
,
name
:
'
GPT-5.3 Codex Spark
'
,
limit
:
{
limit
:
{
...
@@ -773,22 +697,6 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
...
@@ -773,22 +697,6 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
xhigh
:
{}
xhigh
:
{}
}
}
},
},
'
gpt-5.2-codex
'
:
{
name
:
'
GPT-5.2 Codex
'
,
limit
:
{
context
:
400000
,
output
:
128000
},
options
:
{
store
:
false
},
variants
:
{
low
:
{},
medium
:
{},
high
:
{},
xhigh
:
{}
}
},
'
codex-mini-latest
'
:
{
'
codex-mini-latest
'
:
{
name
:
'
Codex Mini
'
,
name
:
'
Codex Mini
'
,
limit
:
{
limit
:
{
...
...
frontend/src/components/keys/__tests__/UseKeyModal.spec.ts
View file @
da1d2600
...
@@ -17,7 +17,7 @@ vi.mock('@/composables/useClipboard', () => ({
...
@@ -17,7 +17,7 @@ vi.mock('@/composables/useClipboard', () => ({
import
UseKeyModal
from
'
../UseKeyModal.vue
'
import
UseKeyModal
from
'
../UseKeyModal.vue
'
describe
(
'
UseKeyModal
'
,
()
=>
{
describe
(
'
UseKeyModal
'
,
()
=>
{
it
(
'
renders
updated
GPT-5.4 mini
/nano names
in OpenCode config
'
,
async
()
=>
{
it
(
'
renders GPT-5.4 mini
entry
in OpenCode config
'
,
async
()
=>
{
const
wrapper
=
mount
(
UseKeyModal
,
{
const
wrapper
=
mount
(
UseKeyModal
,
{
props
:
{
props
:
{
show
:
true
,
show
:
true
,
...
@@ -48,6 +48,6 @@ describe('UseKeyModal', () => {
...
@@ -48,6 +48,6 @@ describe('UseKeyModal', () => {
const
codeBlock
=
wrapper
.
find
(
'
pre code
'
)
const
codeBlock
=
wrapper
.
find
(
'
pre code
'
)
expect
(
codeBlock
.
exists
()).
toBe
(
true
)
expect
(
codeBlock
.
exists
()).
toBe
(
true
)
expect
(
codeBlock
.
text
()).
toContain
(
'
"name": "GPT-5.4 Mini"
'
)
expect
(
codeBlock
.
text
()).
toContain
(
'
"name": "GPT-5.4 Mini"
'
)
expect
(
codeBlock
.
text
()).
toContain
(
'
"name": "GPT-5.4 Nano"
'
)
expect
(
codeBlock
.
text
()).
not
.
toContain
(
'
"name": "GPT-5.4 Nano"
'
)
})
})
})
})
frontend/src/components/payment/PaymentProviderDialog.vue
View file @
da1d2600
...
@@ -88,13 +88,24 @@
...
@@ -88,13 +88,24 @@
v-model=
"config[field.key]"
v-model=
"config[field.key]"
rows=
"3"
rows=
"3"
class=
"input font-mono text-xs"
class=
"input font-mono text-xs"
autocomplete=
"new-password"
data-1p-ignore
data-lpignore=
"true"
data-bwignore=
"true"
spellcheck=
"false"
:placeholder=
"editing ? t('admin.accounts.leaveEmptyToKeep') : ''"
/>
/>
<div
v-else-if=
"field.sensitive"
class=
"relative"
>
<div
v-else-if=
"field.sensitive"
class=
"relative"
>
<input
<input
:type=
"visibleFields[field.key] ? 'text' : 'password'"
:type=
"visibleFields[field.key] ? 'text' : 'password'"
v-model=
"config[field.key]"
v-model=
"config[field.key]"
class=
"input pr-10"
class=
"input pr-10"
:placeholder=
"field.defaultValue || ''"
autocomplete=
"new-password"
data-1p-ignore
data-lpignore=
"true"
data-bwignore=
"true"
spellcheck=
"false"
:placeholder=
"editing ? t('admin.accounts.leaveEmptyToKeep') : (field.defaultValue || '')"
/>
/>
<button
<button
type=
"button"
type=
"button"
...
@@ -398,9 +409,12 @@ function handleSave() {
...
@@ -398,9 +409,12 @@ function handleSave() {
emitValidationError
(
t
(
'
admin.settings.payment.validationNameRequired
'
))
emitValidationError
(
t
(
'
admin.settings.payment.validationNameRequired
'
))
return
return
}
}
// Validate required config fields — all non-optional fields must be filled
// Validate required config fields — all non-optional fields must be filled.
// In edit mode, sensitive fields may be left blank to preserve the stored
// value (backend merges blanks by preserving the existing secret).
for
(
const
f
of
PROVIDER_CONFIG_FIELDS
[
form
.
provider_key
]
||
[])
{
for
(
const
f
of
PROVIDER_CONFIG_FIELDS
[
form
.
provider_key
]
||
[])
{
if
(
f
.
optional
)
continue
if
(
f
.
optional
)
continue
if
(
props
.
editing
&&
f
.
sensitive
)
continue
const
val
=
(
config
[
f
.
key
]
||
''
).
trim
()
const
val
=
(
config
[
f
.
key
]
||
''
).
trim
()
if
(
!
val
)
{
if
(
!
val
)
{
const
label
=
f
.
label
||
t
(
`admin.settings.payment.field_
${
f
.
key
}
`
)
const
label
=
f
.
label
||
t
(
`admin.settings.payment.field_
${
f
.
key
}
`
)
...
@@ -412,8 +426,6 @@ function handleSave() {
...
@@ -412,8 +426,6 @@ function handleSave() {
const
filteredConfig
:
Record
<
string
,
string
>
=
{}
const
filteredConfig
:
Record
<
string
,
string
>
=
{}
for
(
const
[
k
,
v
]
of
Object
.
entries
(
config
))
{
for
(
const
[
k
,
v
]
of
Object
.
entries
(
config
))
{
if
(
!
v
||
!
v
.
trim
())
continue
if
(
!
v
||
!
v
.
trim
())
continue
// Skip masked values — backend keeps existing credentials
if
(
v
===
'
••••••••
'
)
continue
filteredConfig
[
k
]
=
v
filteredConfig
[
k
]
=
v
}
}
...
@@ -470,7 +482,8 @@ function loadProvider(provider: ProviderInstance) {
...
@@ -470,7 +482,8 @@ function loadProvider(provider: ProviderInstance) {
form
.
refund_enabled
=
provider
.
refund_enabled
form
.
refund_enabled
=
provider
.
refund_enabled
form
.
allow_user_refund
=
provider
.
allow_user_refund
form
.
allow_user_refund
=
provider
.
allow_user_refund
clearConfig
()
clearConfig
()
// Pre-fill config from API response (non-sensitive in cleartext, sensitive masked as ••••••••)
// Pre-fill config from API response. Backend omits sensitive fields entirely,
// so those inputs stay blank — submitting blank preserves the stored secret.
if
(
provider
.
config
)
{
if
(
provider
.
config
)
{
for
(
const
[
k
,
v
]
of
Object
.
entries
(
provider
.
config
))
{
for
(
const
[
k
,
v
]
of
Object
.
entries
(
provider
.
config
))
{
// Skip notifyUrl/returnUrl — they are derived from callbackBaseUrl
// Skip notifyUrl/returnUrl — they are derived from callbackBaseUrl
...
...
frontend/src/components/payment/PaymentQRDialog.vue
View file @
da1d2600
...
@@ -78,8 +78,8 @@ import Icon from '@/components/icons/Icon.vue'
...
@@ -78,8 +78,8 @@ import Icon from '@/components/icons/Icon.vue'
import
{
usePaymentStore
}
from
'
@/stores/payment
'
import
{
usePaymentStore
}
from
'
@/stores/payment
'
import
{
useAppStore
}
from
'
@/stores
'
import
{
useAppStore
}
from
'
@/stores
'
import
{
paymentAPI
}
from
'
@/api/payment
'
import
{
paymentAPI
}
from
'
@/api/payment
'
import
{
extract
Api
ErrorMessage
}
from
'
@/utils/apiError
'
import
{
extract
I18n
ErrorMessage
}
from
'
@/utils/apiError
'
import
{
POPUP_WINDOW_FEATURES
}
from
'
@/components/payment/providerConfig
'
import
{
getPaymentPopupFeatures
}
from
'
@/components/payment/providerConfig
'
import
type
{
PaymentOrder
}
from
'
@/types/payment
'
import
type
{
PaymentOrder
}
from
'
@/types/payment
'
import
QRCode
from
'
qrcode
'
import
QRCode
from
'
qrcode
'
import
alipayIcon
from
'
@/assets/icons/alipay.svg
'
import
alipayIcon
from
'
@/assets/icons/alipay.svg
'
...
@@ -147,7 +147,7 @@ function getLogoForType(): string | null {
...
@@ -147,7 +147,7 @@ function getLogoForType(): string | null {
function
reopenPopup
()
{
function
reopenPopup
()
{
if
(
props
.
payUrl
)
{
if
(
props
.
payUrl
)
{
window
.
open
(
props
.
payUrl
,
'
paymentPopup
'
,
POPUP_WINDOW_FEATURES
)
window
.
open
(
props
.
payUrl
,
'
paymentPopup
'
,
getPaymentPopupFeatures
()
)
}
}
}
}
...
@@ -222,7 +222,7 @@ async function handleCancel() {
...
@@ -222,7 +222,7 @@ async function handleCancel() {
cleanup
()
cleanup
()
emit
(
'
close
'
)
emit
(
'
close
'
)
}
catch
(
err
:
unknown
)
{
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
extract
Api
ErrorMessage
(
err
,
t
(
'
common.error
'
)))
appStore
.
showError
(
extract
I18n
ErrorMessage
(
err
,
t
,
'
payment.errors
'
,
t
(
'
common.error
'
)))
}
finally
{
}
finally
{
cancelling
.
value
=
false
cancelling
.
value
=
false
}
}
...
...
frontend/src/components/payment/PaymentStatusPanel.vue
View file @
da1d2600
...
@@ -124,8 +124,8 @@ import { useI18n } from 'vue-i18n'
...
@@ -124,8 +124,8 @@ import { useI18n } from 'vue-i18n'
import
{
usePaymentStore
}
from
'
@/stores/payment
'
import
{
usePaymentStore
}
from
'
@/stores/payment
'
import
{
useAppStore
}
from
'
@/stores
'
import
{
useAppStore
}
from
'
@/stores
'
import
{
paymentAPI
}
from
'
@/api/payment
'
import
{
paymentAPI
}
from
'
@/api/payment
'
import
{
extract
Api
ErrorMessage
}
from
'
@/utils/apiError
'
import
{
extract
I18n
ErrorMessage
}
from
'
@/utils/apiError
'
import
{
POPUP_WINDOW_FEATURES
}
from
'
@/components/payment/providerConfig
'
import
{
getPaymentPopupFeatures
}
from
'
@/components/payment/providerConfig
'
import
type
{
PaymentOrder
}
from
'
@/types/payment
'
import
type
{
PaymentOrder
}
from
'
@/types/payment
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
QRCode
from
'
qrcode
'
import
QRCode
from
'
qrcode
'
...
@@ -200,7 +200,7 @@ function isSuccessStatus(status: string | null | undefined): boolean {
...
@@ -200,7 +200,7 @@ function isSuccessStatus(status: string | null | undefined): boolean {
function
reopenPopup
()
{
function
reopenPopup
()
{
if
(
props
.
payUrl
)
{
if
(
props
.
payUrl
)
{
const
win
=
window
.
open
(
props
.
payUrl
,
'
paymentPopup
'
,
POPUP_WINDOW_FEATURES
)
const
win
=
window
.
open
(
props
.
payUrl
,
'
paymentPopup
'
,
getPaymentPopupFeatures
()
)
if
(
!
win
||
win
.
closed
)
{
if
(
!
win
||
win
.
closed
)
{
window
.
location
.
href
=
props
.
payUrl
window
.
location
.
href
=
props
.
payUrl
}
}
...
@@ -257,7 +257,7 @@ async function handleCancel() {
...
@@ -257,7 +257,7 @@ async function handleCancel() {
cleanup
()
cleanup
()
setOutcome
(
'
cancelled
'
)
setOutcome
(
'
cancelled
'
)
}
catch
(
err
:
unknown
)
{
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
extract
Api
ErrorMessage
(
err
,
t
(
'
common.error
'
)))
appStore
.
showError
(
extract
I18n
ErrorMessage
(
err
,
t
,
'
payment.errors
'
,
t
(
'
common.error
'
)))
}
finally
{
}
finally
{
cancelling
.
value
=
false
cancelling
.
value
=
false
}
}
...
...
frontend/src/components/payment/StripePaymentInline.vue
View file @
da1d2600
...
@@ -67,10 +67,10 @@
...
@@ -67,10 +67,10 @@
import
{
ref
,
onMounted
,
nextTick
}
from
'
vue
'
import
{
ref
,
onMounted
,
nextTick
}
from
'
vue
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useI18n
}
from
'
vue-i18n
'
import
{
useRouter
}
from
'
vue-router
'
import
{
useRouter
}
from
'
vue-router
'
import
{
extract
Api
ErrorMessage
}
from
'
@/utils/apiError
'
import
{
extract
I18n
ErrorMessage
}
from
'
@/utils/apiError
'
import
{
paymentAPI
}
from
'
@/api/payment
'
import
{
paymentAPI
}
from
'
@/api/payment
'
import
{
useAppStore
}
from
'
@/stores
'
import
{
useAppStore
}
from
'
@/stores
'
import
{
STRIPE_POPUP_WINDOW_FEATURES
}
from
'
@/components/payment/providerConfig
'
import
{
getPaymentPopupFeatures
}
from
'
@/components/payment/providerConfig
'
import
type
{
Stripe
,
StripeElements
}
from
'
@stripe/stripe-js
'
import
type
{
Stripe
,
StripeElements
}
from
'
@stripe/stripe-js
'
import
Icon
from
'
@/components/icons/Icon.vue
'
import
Icon
from
'
@/components/icons/Icon.vue
'
...
@@ -132,7 +132,7 @@ onMounted(async () => {
...
@@ -132,7 +132,7 @@ onMounted(async () => {
selectedType
.
value
=
event
.
value
.
type
selectedType
.
value
=
event
.
value
.
type
})
})
}
catch
(
err
:
unknown
)
{
}
catch
(
err
:
unknown
)
{
initError
.
value
=
extract
Api
ErrorMessage
(
err
,
t
(
'
payment.stripeLoadFailed
'
))
initError
.
value
=
extract
I18n
ErrorMessage
(
err
,
t
,
'
payment.errors
'
,
t
(
'
payment.stripeLoadFailed
'
))
}
finally
{
}
finally
{
loading
.
value
=
false
loading
.
value
=
false
}
}
...
@@ -151,7 +151,7 @@ async function handlePay() {
...
@@ -151,7 +151,7 @@ async function handlePay() {
amount
:
String
(
props
.
payAmount
),
amount
:
String
(
props
.
payAmount
),
},
},
}).
href
}).
href
const
popup
=
window
.
open
(
popupUrl
,
'
paymentPopup
'
,
STRIPE_POPUP_WINDOW_FEATURES
)
const
popup
=
window
.
open
(
popupUrl
,
'
paymentPopup
'
,
getPaymentPopupFeatures
()
)
const
onReady
=
(
event
:
MessageEvent
)
=>
{
const
onReady
=
(
event
:
MessageEvent
)
=>
{
if
(
event
.
source
!==
popup
||
event
.
data
?.
type
!==
'
STRIPE_POPUP_READY
'
)
return
if
(
event
.
source
!==
popup
||
event
.
data
?.
type
!==
'
STRIPE_POPUP_READY
'
)
return
...
@@ -186,7 +186,7 @@ async function handlePay() {
...
@@ -186,7 +186,7 @@ async function handlePay() {
emit
(
'
success
'
)
emit
(
'
success
'
)
}
}
}
catch
(
err
:
unknown
)
{
}
catch
(
err
:
unknown
)
{
error
.
value
=
extract
Api
ErrorMessage
(
err
,
t
(
'
payment.result.failed
'
))
error
.
value
=
extract
I18n
ErrorMessage
(
err
,
t
,
'
payment.errors
'
,
t
(
'
payment.result.failed
'
))
}
finally
{
}
finally
{
submitting
.
value
=
false
submitting
.
value
=
false
}
}
...
@@ -199,7 +199,7 @@ async function handleCancel() {
...
@@ -199,7 +199,7 @@ async function handleCancel() {
await
paymentAPI
.
cancelOrder
(
props
.
orderId
)
await
paymentAPI
.
cancelOrder
(
props
.
orderId
)
emit
(
'
back
'
)
emit
(
'
back
'
)
}
catch
(
err
:
unknown
)
{
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
extract
Api
ErrorMessage
(
err
,
t
(
'
common.error
'
)))
appStore
.
showError
(
extract
I18n
ErrorMessage
(
err
,
t
,
'
payment.errors
'
,
t
(
'
common.error
'
)))
}
finally
{
}
finally
{
cancelling
.
value
=
false
cancelling
.
value
=
false
}
}
...
...
frontend/src/components/payment/providerConfig.ts
View file @
da1d2600
...
@@ -43,11 +43,24 @@ export const METHOD_ORDER = ['alipay', 'alipay_direct', 'wxpay', 'wxpay_direct',
...
@@ -43,11 +43,24 @@ export const METHOD_ORDER = ['alipay', 'alipay_direct', 'wxpay', 'wxpay_direct',
export
const
PAYMENT_MODE_QRCODE
=
'
qrcode
'
export
const
PAYMENT_MODE_QRCODE
=
'
qrcode
'
export
const
PAYMENT_MODE_POPUP
=
'
popup
'
export
const
PAYMENT_MODE_POPUP
=
'
popup
'
/** Window features for payment popup windows */
/** Preferred popup size for payment gateways. Alipay's standard checkout
export
const
POPUP_WINDOW_FEATURES
=
'
width=1000,height=750,left=100,top=80,scrollbars=yes,resizable=yes
'
* (QR + account login panel) needs ~1200×900 to render without any scrolling. */
const
PAYMENT_POPUP_PREFERRED_WIDTH
=
1250
/** Wider popup for Stripe redirect methods (Alipay checkout page needs ~1200px) */
const
PAYMENT_POPUP_PREFERRED_HEIGHT
=
900
export
const
STRIPE_POPUP_WINDOW_FEATURES
=
'
width=1250,height=780,left=80,top=60,scrollbars=yes,resizable=yes
'
/** Build a window.open features string sized to fit within the current screen
* while preferring the above dimensions. Centers the popup on the available
* work area so nothing is clipped on smaller laptop displays. */
export
function
getPaymentPopupFeatures
():
string
{
const
screen
=
typeof
window
!==
'
undefined
'
?
window
.
screen
:
null
const
availW
=
screen
?.
availWidth
??
PAYMENT_POPUP_PREFERRED_WIDTH
const
availH
=
screen
?.
availHeight
??
PAYMENT_POPUP_PREFERRED_HEIGHT
const
width
=
Math
.
min
(
PAYMENT_POPUP_PREFERRED_WIDTH
,
availW
-
40
)
const
height
=
Math
.
min
(
PAYMENT_POPUP_PREFERRED_HEIGHT
,
availH
-
40
)
const
left
=
Math
.
max
(
0
,
Math
.
floor
((
availW
-
width
)
/
2
))
const
top
=
Math
.
max
(
0
,
Math
.
floor
((
availH
-
height
)
/
2
))
return
`width=
${
width
}
,height=
${
height
}
,left=
${
left
}
,top=
${
top
}
,scrollbars=yes,resizable=yes`
}
/** Webhook paths for each provider (relative to origin). */
/** Webhook paths for each provider (relative to origin). */
export
const
WEBHOOK_PATHS
:
Record
<
string
,
string
>
=
{
export
const
WEBHOOK_PATHS
:
Record
<
string
,
string
>
=
{
...
@@ -87,9 +100,9 @@ export const PROVIDER_CONFIG_FIELDS: Record<string, ConfigFieldDef[]> = {
...
@@ -87,9 +100,9 @@ export const PROVIDER_CONFIG_FIELDS: Record<string, ConfigFieldDef[]> = {
{
key
:
'
mchId
'
,
label
:
''
,
sensitive
:
false
},
{
key
:
'
mchId
'
,
label
:
''
,
sensitive
:
false
},
{
key
:
'
privateKey
'
,
label
:
''
,
sensitive
:
true
},
{
key
:
'
privateKey
'
,
label
:
''
,
sensitive
:
true
},
{
key
:
'
apiV3Key
'
,
label
:
''
,
sensitive
:
true
},
{
key
:
'
apiV3Key
'
,
label
:
''
,
sensitive
:
true
},
{
key
:
'
certSerial
'
,
label
:
''
,
sensitive
:
false
},
{
key
:
'
publicKey
'
,
label
:
''
,
sensitive
:
true
},
{
key
:
'
publicKey
'
,
label
:
''
,
sensitive
:
true
},
{
key
:
'
publicKeyId
'
,
label
:
''
,
sensitive
:
false
},
{
key
:
'
publicKeyId
'
,
label
:
''
,
sensitive
:
false
},
{
key
:
'
certSerial
'
,
label
:
''
,
sensitive
:
false
},
{
key
:
'
h5AppName
'
,
label
:
''
,
sensitive
:
false
,
optional
:
true
},
{
key
:
'
h5AppName
'
,
label
:
''
,
sensitive
:
false
,
optional
:
true
},
{
key
:
'
h5AppUrl
'
,
label
:
''
,
sensitive
:
false
,
optional
:
true
},
{
key
:
'
h5AppUrl
'
,
label
:
''
,
sensitive
:
false
,
optional
:
true
},
],
],
...
...
frontend/src/composables/__tests__/useModelWhitelist.spec.ts
View file @
da1d2600
...
@@ -12,10 +12,20 @@ describe('useModelWhitelist', () => {
...
@@ -12,10 +12,20 @@ describe('useModelWhitelist', () => {
expect
(
models
).
toContain
(
'
gpt-5.4
'
)
expect
(
models
).
toContain
(
'
gpt-5.4
'
)
expect
(
models
).
toContain
(
'
gpt-5.4-mini
'
)
expect
(
models
).
toContain
(
'
gpt-5.4-mini
'
)
expect
(
models
).
toContain
(
'
gpt-5.4-nano
'
)
expect
(
models
).
toContain
(
'
gpt-5.4-2026-03-05
'
)
expect
(
models
).
toContain
(
'
gpt-5.4-2026-03-05
'
)
})
})
it
(
'
openai 模型列表不再暴露已下线的 ChatGPT 登录 Codex 模型
'
,
()
=>
{
const
models
=
getModelsByPlatform
(
'
openai
'
)
expect
(
models
).
not
.
toContain
(
'
gpt-5
'
)
expect
(
models
).
not
.
toContain
(
'
gpt-5.1
'
)
expect
(
models
).
not
.
toContain
(
'
gpt-5.1-codex
'
)
expect
(
models
).
not
.
toContain
(
'
gpt-5.1-codex-max
'
)
expect
(
models
).
not
.
toContain
(
'
gpt-5.1-codex-mini
'
)
expect
(
models
).
not
.
toContain
(
'
gpt-5.2-codex
'
)
})
it
(
'
antigravity 模型列表包含图片模型兼容项
'
,
()
=>
{
it
(
'
antigravity 模型列表包含图片模型兼容项
'
,
()
=>
{
const
models
=
getModelsByPlatform
(
'
antigravity
'
)
const
models
=
getModelsByPlatform
(
'
antigravity
'
)
...
@@ -55,12 +65,11 @@ describe('useModelWhitelist', () => {
...
@@ -55,12 +65,11 @@ describe('useModelWhitelist', () => {
})
})
})
})
it
(
'
whitelist keeps GPT-5.4 mini
and nano
exact mappings
'
,
()
=>
{
it
(
'
whitelist keeps GPT-5.4 mini exact mappings
'
,
()
=>
{
const
mapping
=
buildModelMappingObject
(
'
whitelist
'
,
[
'
gpt-5.4-mini
'
,
'
gpt-5.4-nano
'
],
[])
const
mapping
=
buildModelMappingObject
(
'
whitelist
'
,
[
'
gpt-5.4-mini
'
],
[])
expect
(
mapping
).
toEqual
({
expect
(
mapping
).
toEqual
({
'
gpt-5.4-mini
'
:
'
gpt-5.4-mini
'
,
'
gpt-5.4-mini
'
:
'
gpt-5.4-mini
'
'
gpt-5.4-nano
'
:
'
gpt-5.4-nano
'
})
})
})
})
})
})
frontend/src/composables/useModelWhitelist.ts
View file @
da1d2600
...
@@ -13,19 +13,11 @@ const openaiModels = [
...
@@ -13,19 +13,11 @@ const openaiModels = [
'
o1
'
,
'
o1-preview
'
,
'
o1-mini
'
,
'
o1-pro
'
,
'
o1
'
,
'
o1-preview
'
,
'
o1-mini
'
,
'
o1-pro
'
,
'
o3
'
,
'
o3-mini
'
,
'
o3-pro
'
,
'
o3
'
,
'
o3-mini
'
,
'
o3-pro
'
,
'
o4-mini
'
,
'
o4-mini
'
,
// GPT-5 系列(同步后端定价文件)
'
gpt-5
'
,
'
gpt-5-2025-08-07
'
,
'
gpt-5-chat
'
,
'
gpt-5-chat-latest
'
,
'
gpt-5-codex
'
,
'
gpt-5.3-codex-spark
'
,
'
gpt-5-pro
'
,
'
gpt-5-pro-2025-10-06
'
,
'
gpt-5-mini
'
,
'
gpt-5-mini-2025-08-07
'
,
'
gpt-5-nano
'
,
'
gpt-5-nano-2025-08-07
'
,
// GPT-5.1 系列
'
gpt-5.1
'
,
'
gpt-5.1-2025-11-13
'
,
'
gpt-5.1-chat-latest
'
,
'
gpt-5.1-codex
'
,
'
gpt-5.1-codex-max
'
,
'
gpt-5.1-codex-mini
'
,
// GPT-5.2 系列
// GPT-5.2 系列
'
gpt-5.2
'
,
'
gpt-5.2-2025-12-11
'
,
'
gpt-5.2-chat-latest
'
,
'
gpt-5.2
'
,
'
gpt-5.2-2025-12-11
'
,
'
gpt-5.2-chat-latest
'
,
'
gpt-5.2-codex
'
,
'
gpt-5.2-pro
'
,
'
gpt-5.2-pro-2025-12-11
'
,
'
gpt-5.2-pro
'
,
'
gpt-5.2-pro-2025-12-11
'
,
// GPT-5.4 系列
// GPT-5.4 系列
'
gpt-5.4
'
,
'
gpt-5.4-mini
'
,
'
gpt-5.4-nano
'
,
'
gpt-5.4-2026-03-05
'
,
'
gpt-5.4
'
,
'
gpt-5.4-mini
'
,
'
gpt-5.4-2026-03-05
'
,
// GPT-5.3 系列
// GPT-5.3 系列
'
gpt-5.3-codex
'
,
'
gpt-5.3-codex-spark
'
,
'
gpt-5.3-codex
'
,
'
gpt-5.3-codex-spark
'
,
'
chatgpt-4o-latest
'
,
'
chatgpt-4o-latest
'
,
...
@@ -264,12 +256,9 @@ const openaiPresetMappings = [
...
@@ -264,12 +256,9 @@ const openaiPresetMappings = [
{
label
:
'
GPT-4.1
'
,
from
:
'
gpt-4.1
'
,
to
:
'
gpt-4.1
'
,
color
:
'
bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400
'
},
{
label
:
'
GPT-4.1
'
,
from
:
'
gpt-4.1
'
,
to
:
'
gpt-4.1
'
,
color
:
'
bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400
'
},
{
label
:
'
o1
'
,
from
:
'
o1
'
,
to
:
'
o1
'
,
color
:
'
bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400
'
},
{
label
:
'
o1
'
,
from
:
'
o1
'
,
to
:
'
o1
'
,
color
:
'
bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400
'
},
{
label
:
'
o3
'
,
from
:
'
o3
'
,
to
:
'
o3
'
,
color
:
'
bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400
'
},
{
label
:
'
o3
'
,
from
:
'
o3
'
,
to
:
'
o3
'
,
color
:
'
bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400
'
},
{
label
:
'
GPT-5
'
,
from
:
'
gpt-5
'
,
to
:
'
gpt-5
'
,
color
:
'
bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400
'
},
{
label
:
'
GPT-5.3 Codex Spark
'
,
from
:
'
gpt-5.3-codex-spark
'
,
to
:
'
gpt-5.3-codex-spark
'
,
color
:
'
bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400
'
},
{
label
:
'
GPT-5.3 Codex Spark
'
,
from
:
'
gpt-5.3-codex-spark
'
,
to
:
'
gpt-5.3-codex-spark
'
,
color
:
'
bg-teal-100 text-teal-700 hover:bg-teal-200 dark:bg-teal-900/30 dark:text-teal-400
'
},
{
label
:
'
GPT-5.1
'
,
from
:
'
gpt-5.1
'
,
to
:
'
gpt-5.1
'
,
color
:
'
bg-orange-100 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400
'
},
{
label
:
'
GPT-5.2
'
,
from
:
'
gpt-5.2
'
,
to
:
'
gpt-5.2
'
,
color
:
'
bg-red-100 text-red-700 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400
'
},
{
label
:
'
GPT-5.2
'
,
from
:
'
gpt-5.2
'
,
to
:
'
gpt-5.2
'
,
color
:
'
bg-red-100 text-red-700 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400
'
},
{
label
:
'
GPT-5.4
'
,
from
:
'
gpt-5.4
'
,
to
:
'
gpt-5.4
'
,
color
:
'
bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400
'
},
{
label
:
'
GPT-5.4
'
,
from
:
'
gpt-5.4
'
,
to
:
'
gpt-5.4
'
,
color
:
'
bg-rose-100 text-rose-700 hover:bg-rose-200 dark:bg-rose-900/30 dark:text-rose-400
'
},
{
label
:
'
GPT-5.1 Codex
'
,
from
:
'
gpt-5.1-codex
'
,
to
:
'
gpt-5.1-codex
'
,
color
:
'
bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400
'
},
{
label
:
'
Haiku→5.4
'
,
from
:
'
claude-haiku-4-5-20251001
'
,
to
:
'
gpt-5.4
'
,
color
:
'
bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400
'
},
{
label
:
'
Haiku→5.4
'
,
from
:
'
claude-haiku-4-5-20251001
'
,
to
:
'
gpt-5.4
'
,
color
:
'
bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400
'
},
{
label
:
'
Opus→5.4
'
,
from
:
'
claude-opus-4-6
'
,
to
:
'
gpt-5.4
'
,
color
:
'
bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400
'
},
{
label
:
'
Opus→5.4
'
,
from
:
'
claude-opus-4-6
'
,
to
:
'
gpt-5.4
'
,
color
:
'
bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400
'
},
{
label
:
'
Sonnet→5.4
'
,
from
:
'
claude-sonnet-4-6
'
,
to
:
'
gpt-5.4
'
,
color
:
'
bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400
'
}
{
label
:
'
Sonnet→5.4
'
,
from
:
'
claude-sonnet-4-6
'
,
to
:
'
gpt-5.4
'
,
color
:
'
bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400
'
}
...
...
frontend/src/i18n/locales/en.ts
View file @
da1d2600
...
@@ -895,6 +895,14 @@ export default {
...
@@ -895,6 +895,14 @@ export default {
accountBalance
:
'
Account Balance
'
,
accountBalance
:
'
Account Balance
'
,
concurrencyLimit
:
'
Concurrency Limit
'
,
concurrencyLimit
:
'
Concurrency Limit
'
,
memberSince
:
'
Member Since
'
,
memberSince
:
'
Member Since
'
,
overviewTitle
:
'
Account Overview
'
,
overviewDescription
:
'
Check account status, profile sources, and common actions at a glance.
'
,
basicsTitle
:
'
Profile & Avatar
'
,
basicsDescription
:
'
Keep your public profile details and avatar aligned.
'
,
linkedProfileSources
:
'
Profile Sources
'
,
linkedProfileSourcesDescription
:
'
Some profile details may stay synced from third-party sign-in methods.
'
,
securityTitle
:
'
Security Settings
'
,
securityDescription
:
'
Password, two-factor authentication, and alerts live in the right rail.
'
,
administrator
:
'
Administrator
'
,
administrator
:
'
Administrator
'
,
user
:
'
User
'
,
user
:
'
User
'
,
username
:
'
Username
'
,
username
:
'
Username
'
,
...
@@ -1015,10 +1023,15 @@ export default {
...
@@ -1015,10 +1023,15 @@ export default {
passwordPlaceholder
:
'
Set a login password
'
,
passwordPlaceholder
:
'
Set a login password
'
,
replaceEmailPasswordPlaceholder
:
'
Enter current password
'
,
replaceEmailPasswordPlaceholder
:
'
Enter current password
'
,
sendCodeAction
:
'
Send code
'
,
sendCodeAction
:
'
Send code
'
,
manageEmailAction
:
'
Manage email
'
,
hideEmailFormAction
:
'
Hide email form
'
,
confirmEmailBindAction
:
'
Bind email
'
,
confirmEmailBindAction
:
'
Bind email
'
,
confirmEmailReplaceAction
:
'
Replace primary email
'
,
confirmEmailReplaceAction
:
'
Replace primary email
'
,
codeSentTo
:
'
Code sent to {email}
'
,
codeSentTo
:
'
Code sent to {email}
'
,
replaceSuccess
:
'
Primary email updated
'
,
replaceSuccess
:
'
Primary email updated
'
,
unbindAction
:
'
Unbind
'
,
unbindSuccess
:
'
{providerName} unbound
'
,
boundCount
:
'
{count} linked records
'
,
status
:
{
status
:
{
bound
:
'
Bound
'
,
bound
:
'
Bound
'
,
notBound
:
'
Not bound
'
,
notBound
:
'
Not bound
'
,
...
@@ -2222,6 +2235,7 @@ export default {
...
@@ -2222,6 +2235,7 @@ export default {
rateLimited
:
'
Rate Limited
'
,
rateLimited
:
'
Rate Limited
'
,
overloaded
:
'
Overloaded
'
,
overloaded
:
'
Overloaded
'
,
tempUnschedulable
:
'
Temp Unschedulable
'
,
tempUnschedulable
:
'
Temp Unschedulable
'
,
quotaExceeded
:
'
Quota Exceeded
'
,
unschedulable
:
'
Unschedulable
'
,
unschedulable
:
'
Unschedulable
'
,
rateLimitedUntil
:
'
Rate limited and removed from scheduling. Auto resumes at {time}
'
,
rateLimitedUntil
:
'
Rate limited and removed from scheduling. Auto resumes at {time}
'
,
rateLimitedAutoResume
:
'
Auto resumes in {time}
'
,
rateLimitedAutoResume
:
'
Auto resumes in {time}
'
,
...
@@ -5612,8 +5626,34 @@ export default {
...
@@ -5612,8 +5626,34 @@ export default {
alipayDesktopQrHint
:
'
Desktop Alipay should render a QR code. Refresh and retry, or make sure the payment page was not blocked.
'
,
alipayDesktopQrHint
:
'
Desktop Alipay should render a QR code. Refresh and retry, or make sure the payment page was not blocked.
'
,
alipayMobileUnavailable
:
'
This page could not hand off to Alipay.
'
,
alipayMobileUnavailable
:
'
This page could not hand off to Alipay.
'
,
alipayMobileOpenHint
:
'
Allow the current page to open the Alipay app, or retry from the system browser.
'
,
alipayMobileOpenHint
:
'
Allow the current page to open the Alipay app, or retry from the system browser.
'
,
// Structured error codes (reason strings from backend ApplicationError)
PAYMENT_DISABLED
:
'
Payment system is disabled.
'
,
USER_INACTIVE
:
'
Your account is disabled.
'
,
BALANCE_PAYMENT_DISABLED
:
'
Balance recharge has been disabled.
'
,
INVALID_AMOUNT
:
'
Invalid amount.
'
,
INVALID_INPUT
:
'
Invalid request.
'
,
PLAN_NOT_AVAILABLE
:
'
Plan not found or no longer available.
'
,
GROUP_NOT_FOUND
:
'
Subscription group is no longer available.
'
,
GROUP_TYPE_MISMATCH
:
'
Group is not a subscription type.
'
,
TOO_MANY_PENDING
:
'
Too many pending orders (max {max}). Please complete or cancel existing orders first.
'
,
DAILY_LIMIT_EXCEEDED
:
'
Daily recharge limit reached. Remaining: {remaining}.
'
,
PAYMENT_GATEWAY_ERROR
:
'
Payment method is unavailable.
'
,
NO_AVAILABLE_INSTANCE
:
'
No payment channel available right now.
'
,
PAYMENT_PROVIDER_MISCONFIGURED
:
'
Payment provider misconfigured. Please contact an administrator.
'
,
WXPAY_CONFIG_MISSING_KEY
:
'
WeChat Pay config missing required key: {key}.
'
,
WXPAY_CONFIG_INVALID_KEY_LENGTH
:
'
WeChat Pay {key} length is invalid (expected {expected} bytes, got {actual}).
'
,
WXPAY_CONFIG_INVALID_KEY
:
'
WeChat Pay {key} is malformed. Make sure you copied the full PEM content.
'
,
PENDING_ORDERS
:
'
This provider has pending orders. Please wait for them to complete before making changes.
'
,
PENDING_ORDERS
:
'
This provider has pending orders. Please wait for them to complete before making changes.
'
,
PAYMENT_PROVIDER_CONFLICT
:
'
Another enabled provider instance is already serving this payment method. Disable it before continuing.
'
,
PAYMENT_PROVIDER_CONFLICT
:
'
Another enabled provider instance is already serving this payment method. Disable it before continuing.
'
,
CANCEL_RATE_LIMITED
:
'
Too many cancellations. Please try again later.
'
,
NOT_FOUND
:
'
Order not found.
'
,
FORBIDDEN
:
'
No permission for this order.
'
,
CONFLICT
:
'
Order status has changed. Please refresh.
'
,
INVALID_ORDER_TYPE
:
'
Only balance orders can request a refund.
'
,
INVALID_STATUS
:
'
The current order status does not allow this operation.
'
,
BALANCE_NOT_ENOUGH
:
'
Refund amount exceeds balance.
'
,
REFUND_AMOUNT_EXCEEDED
:
'
Refund amount exceeds the recharge amount.
'
,
REFUND_FAILED
:
'
Refund failed.
'
,
},
},
stripePay
:
'
Pay Now
'
,
stripePay
:
'
Pay Now
'
,
stripeSuccessProcessing
:
'
Payment successful, processing your order...
'
,
stripeSuccessProcessing
:
'
Payment successful, processing your order...
'
,
...
...
frontend/src/i18n/locales/zh.ts
View file @
da1d2600
...
@@ -899,6 +899,14 @@ export default {
...
@@ -899,6 +899,14 @@ export default {
accountBalance
:
'
账户余额
'
,
accountBalance
:
'
账户余额
'
,
concurrencyLimit
:
'
并发限制
'
,
concurrencyLimit
:
'
并发限制
'
,
memberSince
:
'
注册时间
'
,
memberSince
:
'
注册时间
'
,
overviewTitle
:
'
账户总览
'
,
overviewDescription
:
'
快速查看账号状态、资料来源与常用设置。
'
,
basicsTitle
:
'
资料与头像
'
,
basicsDescription
:
'
维护公开展示信息,并保持头像与昵称风格一致。
'
,
linkedProfileSources
:
'
资料来源
'
,
linkedProfileSourcesDescription
:
'
部分头像和昵称可能同步自第三方登录方式。
'
,
securityTitle
:
'
安全设置
'
,
securityDescription
:
'
密码、双因素认证和通知提醒集中放在右侧。
'
,
administrator
:
'
管理员
'
,
administrator
:
'
管理员
'
,
user
:
'
用户
'
,
user
:
'
用户
'
,
username
:
'
用户名
'
,
username
:
'
用户名
'
,
...
@@ -1019,10 +1027,15 @@ export default {
...
@@ -1019,10 +1027,15 @@ export default {
passwordPlaceholder
:
'
设置登录密码
'
,
passwordPlaceholder
:
'
设置登录密码
'
,
replaceEmailPasswordPlaceholder
:
'
输入当前密码
'
,
replaceEmailPasswordPlaceholder
:
'
输入当前密码
'
,
sendCodeAction
:
'
发送验证码
'
,
sendCodeAction
:
'
发送验证码
'
,
manageEmailAction
:
'
管理邮箱
'
,
hideEmailFormAction
:
'
收起邮箱表单
'
,
confirmEmailBindAction
:
'
绑定邮箱
'
,
confirmEmailBindAction
:
'
绑定邮箱
'
,
confirmEmailReplaceAction
:
'
更换主邮箱
'
,
confirmEmailReplaceAction
:
'
更换主邮箱
'
,
codeSentTo
:
'
验证码已发送到 {email}
'
,
codeSentTo
:
'
验证码已发送到 {email}
'
,
replaceSuccess
:
'
主邮箱已更新
'
,
replaceSuccess
:
'
主邮箱已更新
'
,
unbindAction
:
'
解绑
'
,
unbindSuccess
:
'
{providerName} 已解绑
'
,
boundCount
:
'
已关联 {count} 条记录
'
,
status
:
{
status
:
{
bound
:
'
已绑定
'
,
bound
:
'
已绑定
'
,
notBound
:
'
未绑定
'
,
notBound
:
'
未绑定
'
,
...
@@ -2411,6 +2424,7 @@ export default {
...
@@ -2411,6 +2424,7 @@ export default {
rateLimited
:
'
限流中
'
,
rateLimited
:
'
限流中
'
,
overloaded
:
'
过载中
'
,
overloaded
:
'
过载中
'
,
tempUnschedulable
:
'
临时不可调度
'
,
tempUnschedulable
:
'
临时不可调度
'
,
quotaExceeded
:
'
配额超限
'
,
unschedulable
:
'
不可调度
'
,
unschedulable
:
'
不可调度
'
,
rateLimitedUntil
:
'
限流中,当前不参与调度,预计 {time} 自动恢复
'
,
rateLimitedUntil
:
'
限流中,当前不参与调度,预计 {time} 自动恢复
'
,
rateLimitedAutoResume
:
'
{time} 自动恢复
'
,
rateLimitedAutoResume
:
'
{time} 自动恢复
'
,
...
@@ -5800,8 +5814,34 @@ export default {
...
@@ -5800,8 +5814,34 @@ export default {
alipayDesktopQrHint
:
'
电脑端支付宝应展示扫码单,请刷新后重试,或确认浏览器未拦截当前支付页。
'
,
alipayDesktopQrHint
:
'
电脑端支付宝应展示扫码单,请刷新后重试,或确认浏览器未拦截当前支付页。
'
,
alipayMobileUnavailable
:
'
当前页面未成功跳转到支付宝。
'
,
alipayMobileUnavailable
:
'
当前页面未成功跳转到支付宝。
'
,
alipayMobileOpenHint
:
'
请允许当前页面打开支付宝 App,或改用系统浏览器重新发起支付。
'
,
alipayMobileOpenHint
:
'
请允许当前页面打开支付宝 App,或改用系统浏览器重新发起支付。
'
,
// Structured error codes (reason strings from backend ApplicationError)
PAYMENT_DISABLED
:
'
支付系统已关闭
'
,
USER_INACTIVE
:
'
账号已被禁用
'
,
BALANCE_PAYMENT_DISABLED
:
'
余额充值功能已关闭
'
,
INVALID_AMOUNT
:
'
金额无效
'
,
INVALID_INPUT
:
'
参数有误
'
,
PLAN_NOT_AVAILABLE
:
'
套餐不存在或已下架
'
,
GROUP_NOT_FOUND
:
'
订阅分组不可用
'
,
GROUP_TYPE_MISMATCH
:
'
分组类型不是订阅类型
'
,
TOO_MANY_PENDING
:
'
待支付订单过多(最多 {max} 个),请先完成或取消现有订单
'
,
DAILY_LIMIT_EXCEEDED
:
'
今日充值已达上限,剩余额度 {remaining}
'
,
PAYMENT_GATEWAY_ERROR
:
'
支付方式不可用
'
,
NO_AVAILABLE_INSTANCE
:
'
暂无可用的支付通道
'
,
PAYMENT_PROVIDER_MISCONFIGURED
:
'
支付通道配置错误,请联系管理员
'
,
WXPAY_CONFIG_MISSING_KEY
:
'
微信支付配置缺少必填项:{key}
'
,
WXPAY_CONFIG_INVALID_KEY_LENGTH
:
'
微信支付 {key} 长度错误,应为 {expected} 字节(实际 {actual})
'
,
WXPAY_CONFIG_INVALID_KEY
:
'
微信支付 {key} 格式错误,请确认复制了完整的 PEM 内容
'
,
PENDING_ORDERS
:
'
该服务商有未完成的订单,请等待订单完成后再操作
'
,
PENDING_ORDERS
:
'
该服务商有未完成的订单,请等待订单完成后再操作
'
,
PAYMENT_PROVIDER_CONFLICT
:
'
该支付方式已有其他启用中的服务商实例,请先停用后再继续。
'
,
PAYMENT_PROVIDER_CONFLICT
:
'
该支付方式已有其他启用中的服务商实例,请先停用后再继续。
'
,
CANCEL_RATE_LIMITED
:
'
取消订单过于频繁,请稍后再试
'
,
NOT_FOUND
:
'
订单不存在
'
,
FORBIDDEN
:
'
无权限操作此订单
'
,
CONFLICT
:
'
订单状态已变更,请刷新
'
,
INVALID_ORDER_TYPE
:
'
仅余额订单可申请退款
'
,
INVALID_STATUS
:
'
当前订单状态不允许此操作
'
,
BALANCE_NOT_ENOUGH
:
'
退款金额超过余额
'
,
REFUND_AMOUNT_EXCEEDED
:
'
退款金额超过充值金额
'
,
REFUND_FAILED
:
'
退款失败
'
,
},
},
stripePay
:
'
立即支付
'
,
stripePay
:
'
立即支付
'
,
stripeSuccessProcessing
:
'
支付成功,正在处理订单...
'
,
stripeSuccessProcessing
:
'
支付成功,正在处理订单...
'
,
...
...
frontend/src/utils/apiError.ts
View file @
da1d2600
...
@@ -23,14 +23,96 @@ interface ApiErrorLike {
...
@@ -23,14 +23,96 @@ interface ApiErrorLike {
/**
/**
* Extract the error code from an API error object.
* Extract the error code from an API error object.
*
* Prefers the string `reason` (e.g. "PAYMENT_PROVIDER_MISCONFIGURED") over the
* numeric HTTP `code`, because reason is granular enough to drive i18n lookup
* while HTTP code is not.
*/
*/
export
function
extractApiErrorCode
(
err
:
unknown
):
string
|
undefined
{
export
function
extractApiErrorCode
(
err
:
unknown
):
string
|
undefined
{
if
(
!
err
||
typeof
err
!==
'
object
'
)
return
undefined
if
(
!
err
||
typeof
err
!==
'
object
'
)
return
undefined
const
e
=
err
as
ApiErrorLike
const
e
=
err
as
ApiErrorLike
const
code
=
e
.
code
??
e
.
reason
??
e
.
response
?.
data
?.
code
const
code
=
e
.
reason
??
e
.
code
??
e
.
response
?.
data
?.
code
return
code
!=
null
?
String
(
code
)
:
undefined
return
code
!=
null
?
String
(
code
)
:
undefined
}
}
/**
* Extract metadata (interpolation params) from an API error object.
* Backend errors carry `metadata` with template variables that fill i18n placeholders.
*/
export
function
extractApiErrorMetadata
(
err
:
unknown
):
Record
<
string
,
unknown
>
|
undefined
{
if
(
!
err
||
typeof
err
!==
'
object
'
)
return
undefined
const
e
=
err
as
ApiErrorLike
return
e
.
metadata
}
type
TranslateFn
=
(
key
:
string
,
params
?:
Record
<
string
,
unknown
>
)
=>
string
type
TranslateWithExistsFn
=
TranslateFn
&
{
te
?:
(
key
:
string
)
=>
boolean
}
/**
* Translate a value via i18n if a matching key exists, otherwise return the original.
* Example: "certSerial" → t('admin.settings.payment.field_certSerial') → "证书序列号".
*/
function
tryTranslate
(
t
:
TranslateFn
,
key
:
string
,
fallback
:
string
):
string
{
const
translated
=
t
(
key
)
if
(
translated
===
key
)
return
fallback
const
te
=
(
t
as
TranslateWithExistsFn
).
te
if
(
te
&&
!
te
(
key
))
return
fallback
return
translated
}
/**
* Replace raw config field names in metadata (e.g. "certSerial") with their
* localized UI labels (e.g. "证书序列号"), using the provider-config field i18n namespace.
* Handles both single `key` and `/`-joined `keys` patterns used by wxpay errors.
*/
function
localizeMetadata
(
metadata
:
Record
<
string
,
unknown
>
,
t
:
TranslateFn
):
Record
<
string
,
unknown
>
{
const
out
:
Record
<
string
,
unknown
>
=
{
...
metadata
}
if
(
typeof
out
.
key
===
'
string
'
)
{
out
.
key
=
tryTranslate
(
t
,
`admin.settings.payment.field_
${
out
.
key
}
`
,
out
.
key
)
}
if
(
typeof
out
.
keys
===
'
string
'
)
{
out
.
keys
=
out
.
keys
.
split
(
'
/
'
)
.
map
(
k
=>
tryTranslate
(
t
,
`admin.settings.payment.field_
${
k
}
`
,
k
))
.
join
(
'
/
'
)
}
return
out
}
/**
* Extract a localized error message from an API error by looking up
* `<namespace>.<REASON>` in i18n and substituting metadata as placeholders.
*
* Config-field names in metadata (`key` / `keys`) are automatically translated
* to their UI labels before substitution, so error messages read like
* "缺少必填项:证书序列号" instead of "缺少必填项:certSerial".
*
* @param err - The caught error
* @param t - Vue i18n translate function
* @param namespace- i18n key prefix, e.g. "payment.errors"
* @param fallback - Fallback key or plain string if no localized mapping exists
*/
export
function
extractI18nErrorMessage
(
err
:
unknown
,
t
:
TranslateFn
,
namespace
:
string
,
fallback
:
string
,
):
string
{
const
code
=
extractApiErrorCode
(
err
)
if
(
code
)
{
const
key
=
`
${
namespace
}
.
${
code
}
`
const
rawMetadata
=
extractApiErrorMetadata
(
err
)
??
{}
const
metadata
=
localizeMetadata
(
rawMetadata
,
t
)
const
translated
=
t
(
key
,
metadata
)
// Vue i18n returns the key itself when missing; detect that and fall back.
if
(
translated
!==
key
)
return
translated
// If the framework exposes `te`, use it to double-check.
const
te
=
(
t
as
TranslateWithExistsFn
).
te
if
(
te
&&
te
(
key
))
return
translated
}
return
extractApiErrorMessage
(
err
,
fallback
)
}
/**
/**
* Extract a displayable error message from an API error.
* Extract a displayable error message from an API error.
*
*
...
...
frontend/src/utils/format.ts
View file @
da1d2600
...
@@ -193,7 +193,9 @@ export function formatReasoningEffort(effort: string | null | undefined): string
...
@@ -193,7 +193,9 @@ export function formatReasoningEffort(effort: string | null | undefined): string
return
'
High
'
return
'
High
'
case
'
xhigh
'
:
case
'
xhigh
'
:
case
'
extrahigh
'
:
case
'
extrahigh
'
:
return
'
Xhigh
'
return
'
XHigh
'
case
'
max
'
:
return
'
Max
'
case
'
none
'
:
case
'
none
'
:
case
'
minimal
'
:
case
'
minimal
'
:
return
'
-
'
return
'
-
'
...
...
frontend/src/views/admin/SettingsView.vue
View file @
da1d2600
...
@@ -4710,7 +4710,7 @@ import ProxySelector from "@/components/common/ProxySelector.vue";
...
@@ -4710,7 +4710,7 @@ import ProxySelector from "@/components/common/ProxySelector.vue";
import
ImageUpload
from
"
@/components/common/ImageUpload.vue
"
;
import
ImageUpload
from
"
@/components/common/ImageUpload.vue
"
;
import
BackupSettings
from
"
@/views/admin/BackupView.vue
"
;
import
BackupSettings
from
"
@/views/admin/BackupView.vue
"
;
import
{
useClipboard
}
from
"
@/composables/useClipboard
"
;
import
{
useClipboard
}
from
"
@/composables/useClipboard
"
;
import
{
extractApiErrorMessage
}
from
"
@/utils/apiError
"
;
import
{
extractApiErrorMessage
,
extractI18nErrorMessage
}
from
"
@/utils/apiError
"
;
import
{
useAppStore
}
from
"
@/stores
"
;
import
{
useAppStore
}
from
"
@/stores
"
;
import
{
useAdminSettingsStore
}
from
"
@/stores/adminSettings
"
;
import
{
useAdminSettingsStore
}
from
"
@/stores/adminSettings
"
;
import
{
normalizeVisibleMethod
}
from
"
@/components/payment/paymentFlow
"
;
import
{
normalizeVisibleMethod
}
from
"
@/components/payment/paymentFlow
"
;
...
@@ -6431,11 +6431,6 @@ const cancelRateLimitModeOptions = computed(() => [
...
@@ -6431,11 +6431,6 @@ const cancelRateLimitModeOptions = computed(() => [
}
,
}
,
]);
]);
const
paymentErrorMap
=
computed
(()
=>
({
PENDING_ORDERS
:
t
(
"
payment.errors.PENDING_ORDERS
"
),
PAYMENT_PROVIDER_CONFLICT
:
t
(
"
payment.errors.PAYMENT_PROVIDER_CONFLICT
"
),
}
));
type
ProviderEnablementCandidate
=
Pick
<
type
ProviderEnablementCandidate
=
Pick
<
ProviderInstance
,
ProviderInstance
,
"
id
"
|
"
provider_key
"
|
"
supported_types
"
|
"
enabled
"
|
"
name
"
"
id
"
|
"
provider_key
"
|
"
supported_types
"
|
"
enabled
"
|
"
name
"
...
@@ -6531,7 +6526,7 @@ async function loadProviders() {
...
@@ -6531,7 +6526,7 @@ async function loadProviders() {
const
res
=
await
adminAPI
.
payment
.
getProviders
();
const
res
=
await
adminAPI
.
payment
.
getProviders
();
providers
.
value
=
res
.
data
||
[];
providers
.
value
=
res
.
data
||
[];
}
catch
(
err
:
unknown
)
{
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
extract
Api
ErrorMessage
(
err
,
t
(
"
common.error
"
)));
appStore
.
showError
(
extract
I18n
ErrorMessage
(
err
,
t
,
"
payment.errors
"
,
t
(
"
common.error
"
)));
}
finally
{
}
finally
{
providersLoading
.
value
=
false
;
providersLoading
.
value
=
false
;
}
}
...
@@ -6580,9 +6575,7 @@ async function handleSaveProvider(payload: Partial<ProviderInstance>) {
...
@@ -6580,9 +6575,7 @@ async function handleSaveProvider(payload: Partial<ProviderInstance>) {
// Auto-save settings so provider changes take effect immediately
// Auto-save settings so provider changes take effect immediately
await
saveSettings
();
await
saveSettings
();
}
catch
(
err
:
unknown
)
{
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
appStore
.
showError
(
extractI18nErrorMessage
(
err
,
t
,
"
payment.errors
"
,
t
(
"
common.error
"
)));
extractApiErrorMessage
(
err
,
t
(
"
common.error
"
),
paymentErrorMap
.
value
),
);
}
finally
{
}
finally
{
providerSaving
.
value
=
false
;
providerSaving
.
value
=
false
;
}
}
...
@@ -6620,9 +6613,7 @@ async function handleToggleField(
...
@@ -6620,9 +6613,7 @@ async function handleToggleField(
await
adminAPI
.
payment
.
updateProvider
(
provider
.
id
,
payload
);
await
adminAPI
.
payment
.
updateProvider
(
provider
.
id
,
payload
);
await
loadProviders
();
await
loadProviders
();
}
catch
(
err
:
unknown
)
{
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
appStore
.
showError
(
extractI18nErrorMessage
(
err
,
t
,
"
payment.errors
"
,
t
(
"
common.error
"
)));
extractApiErrorMessage
(
err
,
t
(
"
common.error
"
),
paymentErrorMap
.
value
),
);
}
}
}
}
...
@@ -6647,9 +6638,7 @@ async function handleToggleType(provider: ProviderInstance, type: string) {
...
@@ -6647,9 +6638,7 @@ async function handleToggleType(provider: ProviderInstance, type: string) {
}
as
any
);
}
as
any
);
await
loadProviders
();
await
loadProviders
();
}
catch
(
err
:
unknown
)
{
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
appStore
.
showError
(
extractI18nErrorMessage
(
err
,
t
,
"
payment.errors
"
,
t
(
"
common.error
"
)));
extractApiErrorMessage
(
err
,
t
(
"
common.error
"
),
paymentErrorMap
.
value
),
);
}
}
}
}
...
@@ -6671,7 +6660,7 @@ async function handleReorderProviders(
...
@@ -6671,7 +6660,7 @@ async function handleReorderProviders(
);
);
await
loadProviders
();
await
loadProviders
();
}
catch
(
err
:
unknown
)
{
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
extract
Api
ErrorMessage
(
err
,
t
(
"
common.error
"
)));
appStore
.
showError
(
extract
I18n
ErrorMessage
(
err
,
t
,
"
payment.errors
"
,
t
(
"
common.error
"
)));
loadProviders
();
loadProviders
();
}
}
}
}
...
@@ -6684,9 +6673,7 @@ async function handleDeleteProvider() {
...
@@ -6684,9 +6673,7 @@ async function handleDeleteProvider() {
showDeleteProviderDialog
.
value
=
false
;
showDeleteProviderDialog
.
value
=
false
;
loadProviders
();
loadProviders
();
}
catch
(
err
:
unknown
)
{
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
appStore
.
showError
(
extractI18nErrorMessage
(
err
,
t
,
"
payment.errors
"
,
t
(
"
common.error
"
)));
extractApiErrorMessage
(
err
,
t
(
"
common.error
"
),
paymentErrorMap
.
value
),
);
}
}
}
}
...
...
frontend/src/views/admin/orders/AdminOrdersView.vue
View file @
da1d2600
...
@@ -116,7 +116,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
...
@@ -116,7 +116,7 @@ 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
{
adminPaymentAPI
}
from
'
@/api/admin/payment
'
import
{
adminPaymentAPI
}
from
'
@/api/admin/payment
'
import
{
extract
Api
ErrorMessage
}
from
'
@/utils/apiError
'
import
{
extract
I18n
ErrorMessage
}
from
'
@/utils/apiError
'
import
{
formatOrderDateTime
}
from
'
@/components/payment/orderUtils
'
import
{
formatOrderDateTime
}
from
'
@/components/payment/orderUtils
'
import
type
{
PaymentOrder
}
from
'
@/types/payment
'
import
type
{
PaymentOrder
}
from
'
@/types/payment
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
import
AppLayout
from
'
@/components/layout/AppLayout.vue
'
...
@@ -167,7 +167,7 @@ async function loadOrders() {
...
@@ -167,7 +167,7 @@ async function loadOrders() {
orders
.
value
=
res
.
data
.
items
||
[]
orders
.
value
=
res
.
data
.
items
||
[]
orderPagination
.
total
=
res
.
data
.
total
||
0
orderPagination
.
total
=
res
.
data
.
total
||
0
}
catch
(
err
:
unknown
)
{
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
extract
Api
ErrorMessage
(
err
,
t
(
'
common.error
'
)))
appStore
.
showError
(
extract
I18n
ErrorMessage
(
err
,
t
,
'
payment.errors
'
,
t
(
'
common.error
'
)))
}
finally
{
ordersLoading
.
value
=
false
}
}
finally
{
ordersLoading
.
value
=
false
}
}
}
...
@@ -214,12 +214,12 @@ async function showOrderDetail(order: PaymentOrder) {
...
@@ -214,12 +214,12 @@ async function showOrderDetail(order: PaymentOrder) {
async
function
handleCancelOrder
(
order
:
PaymentOrder
)
{
async
function
handleCancelOrder
(
order
:
PaymentOrder
)
{
try
{
await
adminPaymentAPI
.
cancelOrder
(
order
.
id
);
appStore
.
showSuccess
(
t
(
'
payment.admin.orderCancelled
'
));
loadOrders
()
}
try
{
await
adminPaymentAPI
.
cancelOrder
(
order
.
id
);
appStore
.
showSuccess
(
t
(
'
payment.admin.orderCancelled
'
));
loadOrders
()
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
extract
Api
ErrorMessage
(
err
,
t
(
'
common.error
'
)))
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
extract
I18n
ErrorMessage
(
err
,
t
,
'
payment.errors
'
,
t
(
'
common.error
'
)))
}
}
}
async
function
handleRetryOrder
(
order
:
PaymentOrder
)
{
async
function
handleRetryOrder
(
order
:
PaymentOrder
)
{
try
{
await
adminPaymentAPI
.
retryRecharge
(
order
.
id
);
appStore
.
showSuccess
(
t
(
'
payment.admin.retrySuccess
'
));
loadOrders
()
}
try
{
await
adminPaymentAPI
.
retryRecharge
(
order
.
id
);
appStore
.
showSuccess
(
t
(
'
payment.admin.retrySuccess
'
));
loadOrders
()
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
extract
Api
ErrorMessage
(
err
,
t
(
'
common.error
'
)))
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
extract
I18n
ErrorMessage
(
err
,
t
,
'
payment.errors
'
,
t
(
'
common.error
'
)))
}
}
}
function
openRefundDialog
(
order
:
PaymentOrder
)
{
selectedOrder
.
value
=
order
;
showRefundDialog
.
value
=
true
}
function
openRefundDialog
(
order
:
PaymentOrder
)
{
selectedOrder
.
value
=
order
;
showRefundDialog
.
value
=
true
}
...
@@ -230,7 +230,7 @@ async function handleRefund(data: { amount: number; reason: string; deduct_balan
...
@@ -230,7 +230,7 @@ async function handleRefund(data: { amount: number; reason: string; deduct_balan
try
{
try
{
await
adminPaymentAPI
.
refundOrder
(
selectedOrder
.
value
.
id
,
{
amount
:
data
.
amount
,
reason
:
data
.
reason
,
deduct_balance
:
data
.
deduct_balance
,
force
:
data
.
force
})
await
adminPaymentAPI
.
refundOrder
(
selectedOrder
.
value
.
id
,
{
amount
:
data
.
amount
,
reason
:
data
.
reason
,
deduct_balance
:
data
.
deduct_balance
,
force
:
data
.
force
})
appStore
.
showSuccess
(
t
(
'
payment.admin.refundSuccess
'
));
showRefundDialog
.
value
=
false
;
loadOrders
()
appStore
.
showSuccess
(
t
(
'
payment.admin.refundSuccess
'
));
showRefundDialog
.
value
=
false
;
loadOrders
()
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
extract
Api
ErrorMessage
(
err
,
t
(
'
common.error
'
)))
}
}
catch
(
err
:
unknown
)
{
appStore
.
showError
(
extract
I18n
ErrorMessage
(
err
,
t
,
'
payment.errors
'
,
t
(
'
common.error
'
)))
}
finally
{
refundSubmitting
.
value
=
false
}
finally
{
refundSubmitting
.
value
=
false
}
}
}
...
...
Prev
1
2
3
4
Next
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