Merge branch 'main' into rebuild/auth-identity-foundation
This commit is contained in:
@@ -284,6 +284,16 @@ const hasError = computed(() => {
|
||||
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)
|
||||
const rateLimitCountdown = computed(() => {
|
||||
return formatCountdown(props.account.rate_limit_reset_at)
|
||||
@@ -307,19 +317,16 @@ const statusClass = computed(() => {
|
||||
if (isTempUnschedulable.value) {
|
||||
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) {
|
||||
return 'badge-gray'
|
||||
}
|
||||
switch (props.account.status) {
|
||||
case 'active':
|
||||
return 'badge-success'
|
||||
case 'inactive':
|
||||
return 'badge-gray'
|
||||
case 'error':
|
||||
return 'badge-danger'
|
||||
default:
|
||||
return 'badge-gray'
|
||||
}
|
||||
return 'badge-success'
|
||||
})
|
||||
|
||||
// Computed: status text
|
||||
@@ -330,6 +337,12 @@ const statusText = computed(() => {
|
||||
if (isTempUnschedulable.value) {
|
||||
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) {
|
||||
return t('admin.accounts.status.paused')
|
||||
}
|
||||
|
||||
@@ -52,6 +52,10 @@
|
||||
v-model="editApiKey"
|
||||
type="password"
|
||||
class="input font-mono"
|
||||
autocomplete="new-password"
|
||||
data-1p-ignore
|
||||
data-lpignore="true"
|
||||
data-bwignore="true"
|
||||
:placeholder="
|
||||
account.platform === 'openai'
|
||||
? 'sk-proj-...'
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
<input
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
min="0.001"
|
||||
autocomplete="off"
|
||||
: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"
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<input
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
min="0.001"
|
||||
:value="config.customRate ?? ''"
|
||||
@input="updateCustomRate(config.groupId, ($event.target as HTMLInputElement).value)"
|
||||
:placeholder="String(config.defaultRate)"
|
||||
@@ -139,7 +139,7 @@
|
||||
<input
|
||||
type="number"
|
||||
step="0.001"
|
||||
min="0"
|
||||
min="0.001"
|
||||
:value="config.customRate ?? ''"
|
||||
@input="updateCustomRate(config.groupId, ($event.target as HTMLInputElement).value)"
|
||||
:placeholder="String(config.defaultRate)"
|
||||
|
||||
@@ -617,66 +617,6 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
|
||||
}
|
||||
}
|
||||
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': {
|
||||
name: 'GPT-5.2',
|
||||
limit: {
|
||||
@@ -725,22 +665,6 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
|
||||
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': {
|
||||
name: 'GPT-5.3 Codex Spark',
|
||||
limit: {
|
||||
@@ -773,22 +697,6 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
|
||||
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': {
|
||||
name: 'Codex Mini',
|
||||
limit: {
|
||||
|
||||
@@ -17,7 +17,7 @@ vi.mock('@/composables/useClipboard', () => ({
|
||||
import UseKeyModal from '../UseKeyModal.vue'
|
||||
|
||||
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, {
|
||||
props: {
|
||||
show: true,
|
||||
@@ -48,6 +48,6 @@ describe('UseKeyModal', () => {
|
||||
const codeBlock = wrapper.find('pre code')
|
||||
expect(codeBlock.exists()).toBe(true)
|
||||
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"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -88,13 +88,24 @@
|
||||
v-model="config[field.key]"
|
||||
rows="3"
|
||||
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">
|
||||
<input
|
||||
:type="visibleFields[field.key] ? 'text' : 'password'"
|
||||
v-model="config[field.key]"
|
||||
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
|
||||
type="button"
|
||||
@@ -398,9 +409,12 @@ function handleSave() {
|
||||
emitValidationError(t('admin.settings.payment.validationNameRequired'))
|
||||
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] || []) {
|
||||
if (f.optional) continue
|
||||
if (props.editing && f.sensitive) continue
|
||||
const val = (config[f.key] || '').trim()
|
||||
if (!val) {
|
||||
const label = f.label || t(`admin.settings.payment.field_${f.key}`)
|
||||
@@ -412,8 +426,6 @@ function handleSave() {
|
||||
const filteredConfig: Record<string, string> = {}
|
||||
for (const [k, v] of Object.entries(config)) {
|
||||
if (!v || !v.trim()) continue
|
||||
// Skip masked values — backend keeps existing credentials
|
||||
if (v === '••••••••') continue
|
||||
filteredConfig[k] = v
|
||||
}
|
||||
|
||||
@@ -470,7 +482,8 @@ function loadProvider(provider: ProviderInstance) {
|
||||
form.refund_enabled = provider.refund_enabled
|
||||
form.allow_user_refund = provider.allow_user_refund
|
||||
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) {
|
||||
for (const [k, v] of Object.entries(provider.config)) {
|
||||
// Skip notifyUrl/returnUrl — they are derived from callbackBaseUrl
|
||||
|
||||
@@ -78,8 +78,8 @@ import Icon from '@/components/icons/Icon.vue'
|
||||
import { usePaymentStore } from '@/stores/payment'
|
||||
import { useAppStore } from '@/stores'
|
||||
import { paymentAPI } from '@/api/payment'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import { POPUP_WINDOW_FEATURES } from '@/components/payment/providerConfig'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import { getPaymentPopupFeatures } from '@/components/payment/providerConfig'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
import QRCode from 'qrcode'
|
||||
import alipayIcon from '@/assets/icons/alipay.svg'
|
||||
@@ -147,7 +147,7 @@ function getLogoForType(): string | null {
|
||||
|
||||
function reopenPopup() {
|
||||
if (props.payUrl) {
|
||||
window.open(props.payUrl, 'paymentPopup', POPUP_WINDOW_FEATURES)
|
||||
window.open(props.payUrl, 'paymentPopup', getPaymentPopupFeatures())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ async function handleCancel() {
|
||||
cleanup()
|
||||
emit('close')
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error')))
|
||||
} finally {
|
||||
cancelling.value = false
|
||||
}
|
||||
|
||||
@@ -124,8 +124,8 @@ import { useI18n } from 'vue-i18n'
|
||||
import { usePaymentStore } from '@/stores/payment'
|
||||
import { useAppStore } from '@/stores'
|
||||
import { paymentAPI } from '@/api/payment'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import { POPUP_WINDOW_FEATURES } from '@/components/payment/providerConfig'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import { getPaymentPopupFeatures } from '@/components/payment/providerConfig'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import QRCode from 'qrcode'
|
||||
@@ -200,7 +200,7 @@ function isSuccessStatus(status: string | null | undefined): boolean {
|
||||
|
||||
function reopenPopup() {
|
||||
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) {
|
||||
window.location.href = props.payUrl
|
||||
}
|
||||
@@ -257,7 +257,7 @@ async function handleCancel() {
|
||||
cleanup()
|
||||
setOutcome('cancelled')
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error')))
|
||||
} finally {
|
||||
cancelling.value = false
|
||||
}
|
||||
|
||||
@@ -67,10 +67,10 @@
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import { paymentAPI } from '@/api/payment'
|
||||
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 Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
@@ -132,7 +132,7 @@ onMounted(async () => {
|
||||
selectedType.value = event.value.type
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
initError.value = extractApiErrorMessage(err, t('payment.stripeLoadFailed'))
|
||||
initError.value = extractI18nErrorMessage(err, t, 'payment.errors', t('payment.stripeLoadFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -151,7 +151,7 @@ async function handlePay() {
|
||||
amount: String(props.payAmount),
|
||||
},
|
||||
}).href
|
||||
const popup = window.open(popupUrl, 'paymentPopup', STRIPE_POPUP_WINDOW_FEATURES)
|
||||
const popup = window.open(popupUrl, 'paymentPopup', getPaymentPopupFeatures())
|
||||
|
||||
const onReady = (event: MessageEvent) => {
|
||||
if (event.source !== popup || event.data?.type !== 'STRIPE_POPUP_READY') return
|
||||
@@ -186,7 +186,7 @@ async function handlePay() {
|
||||
emit('success')
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
error.value = extractApiErrorMessage(err, t('payment.result.failed'))
|
||||
error.value = extractI18nErrorMessage(err, t, 'payment.errors', t('payment.result.failed'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -199,7 +199,7 @@ async function handleCancel() {
|
||||
await paymentAPI.cancelOrder(props.orderId)
|
||||
emit('back')
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error')))
|
||||
} finally {
|
||||
cancelling.value = false
|
||||
}
|
||||
|
||||
@@ -43,11 +43,24 @@ export const METHOD_ORDER = ['alipay', 'alipay_direct', 'wxpay', 'wxpay_direct',
|
||||
export const PAYMENT_MODE_QRCODE = 'qrcode'
|
||||
export const PAYMENT_MODE_POPUP = 'popup'
|
||||
|
||||
/** Window features for payment popup windows */
|
||||
export const POPUP_WINDOW_FEATURES = 'width=1000,height=750,left=100,top=80,scrollbars=yes,resizable=yes'
|
||||
/** Preferred popup size for payment gateways. Alipay's standard checkout
|
||||
* (QR + account login panel) needs ~1200×900 to render without any scrolling. */
|
||||
const PAYMENT_POPUP_PREFERRED_WIDTH = 1250
|
||||
const PAYMENT_POPUP_PREFERRED_HEIGHT = 900
|
||||
|
||||
/** Wider popup for Stripe redirect methods (Alipay checkout page needs ~1200px) */
|
||||
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). */
|
||||
export const WEBHOOK_PATHS: Record<string, string> = {
|
||||
@@ -87,9 +100,9 @@ export const PROVIDER_CONFIG_FIELDS: Record<string, ConfigFieldDef[]> = {
|
||||
{ key: 'mchId', label: '', sensitive: false },
|
||||
{ key: 'privateKey', label: '', sensitive: true },
|
||||
{ key: 'apiV3Key', label: '', sensitive: true },
|
||||
{ key: 'certSerial', label: '', sensitive: false },
|
||||
{ key: 'publicKey', label: '', sensitive: true },
|
||||
{ key: 'publicKeyId', label: '', sensitive: false },
|
||||
{ key: 'certSerial', label: '', sensitive: false },
|
||||
{ key: 'h5AppName', label: '', sensitive: false, optional: true },
|
||||
{ key: 'h5AppUrl', label: '', sensitive: false, optional: true },
|
||||
],
|
||||
|
||||
@@ -12,10 +12,20 @@ describe('useModelWhitelist', () => {
|
||||
|
||||
expect(models).toContain('gpt-5.4')
|
||||
expect(models).toContain('gpt-5.4-mini')
|
||||
expect(models).toContain('gpt-5.4-nano')
|
||||
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 模型列表包含图片模型兼容项', () => {
|
||||
const models = getModelsByPlatform('antigravity')
|
||||
|
||||
@@ -55,12 +65,11 @@ describe('useModelWhitelist', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('whitelist keeps GPT-5.4 mini and nano exact mappings', () => {
|
||||
const mapping = buildModelMappingObject('whitelist', ['gpt-5.4-mini', 'gpt-5.4-nano'], [])
|
||||
it('whitelist keeps GPT-5.4 mini exact mappings', () => {
|
||||
const mapping = buildModelMappingObject('whitelist', ['gpt-5.4-mini'], [])
|
||||
|
||||
expect(mapping).toEqual({
|
||||
'gpt-5.4-mini': 'gpt-5.4-mini',
|
||||
'gpt-5.4-nano': 'gpt-5.4-nano'
|
||||
'gpt-5.4-mini': 'gpt-5.4-mini'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,19 +13,11 @@ const openaiModels = [
|
||||
'o1', 'o1-preview', 'o1-mini', 'o1-pro',
|
||||
'o3', 'o3-mini', 'o3-pro',
|
||||
'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-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-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-codex', 'gpt-5.3-codex-spark',
|
||||
'chatgpt-4o-latest',
|
||||
@@ -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: '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: '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.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.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: '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' }
|
||||
|
||||
@@ -895,6 +895,14 @@ export default {
|
||||
accountBalance: 'Account Balance',
|
||||
concurrencyLimit: 'Concurrency Limit',
|
||||
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',
|
||||
user: 'User',
|
||||
username: 'Username',
|
||||
@@ -1015,10 +1023,15 @@ export default {
|
||||
passwordPlaceholder: 'Set a login password',
|
||||
replaceEmailPasswordPlaceholder: 'Enter current password',
|
||||
sendCodeAction: 'Send code',
|
||||
manageEmailAction: 'Manage email',
|
||||
hideEmailFormAction: 'Hide email form',
|
||||
confirmEmailBindAction: 'Bind email',
|
||||
confirmEmailReplaceAction: 'Replace primary email',
|
||||
codeSentTo: 'Code sent to {email}',
|
||||
replaceSuccess: 'Primary email updated',
|
||||
unbindAction: 'Unbind',
|
||||
unbindSuccess: '{providerName} unbound',
|
||||
boundCount: '{count} linked records',
|
||||
status: {
|
||||
bound: 'Bound',
|
||||
notBound: 'Not bound',
|
||||
@@ -2222,6 +2235,7 @@ export default {
|
||||
rateLimited: 'Rate Limited',
|
||||
overloaded: 'Overloaded',
|
||||
tempUnschedulable: 'Temp Unschedulable',
|
||||
quotaExceeded: 'Quota Exceeded',
|
||||
unschedulable: 'Unschedulable',
|
||||
rateLimitedUntil: 'Rate limited and removed from scheduling. Auto resumes at {time}',
|
||||
rateLimitedAutoResume: 'Auto resumes in {time}',
|
||||
@@ -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.',
|
||||
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.',
|
||||
// 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.',
|
||||
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',
|
||||
stripeSuccessProcessing: 'Payment successful, processing your order...',
|
||||
|
||||
@@ -899,6 +899,14 @@ export default {
|
||||
accountBalance: '账户余额',
|
||||
concurrencyLimit: '并发限制',
|
||||
memberSince: '注册时间',
|
||||
overviewTitle: '账户总览',
|
||||
overviewDescription: '快速查看账号状态、资料来源与常用设置。',
|
||||
basicsTitle: '资料与头像',
|
||||
basicsDescription: '维护公开展示信息,并保持头像与昵称风格一致。',
|
||||
linkedProfileSources: '资料来源',
|
||||
linkedProfileSourcesDescription: '部分头像和昵称可能同步自第三方登录方式。',
|
||||
securityTitle: '安全设置',
|
||||
securityDescription: '密码、双因素认证和通知提醒集中放在右侧。',
|
||||
administrator: '管理员',
|
||||
user: '用户',
|
||||
username: '用户名',
|
||||
@@ -1019,10 +1027,15 @@ export default {
|
||||
passwordPlaceholder: '设置登录密码',
|
||||
replaceEmailPasswordPlaceholder: '输入当前密码',
|
||||
sendCodeAction: '发送验证码',
|
||||
manageEmailAction: '管理邮箱',
|
||||
hideEmailFormAction: '收起邮箱表单',
|
||||
confirmEmailBindAction: '绑定邮箱',
|
||||
confirmEmailReplaceAction: '更换主邮箱',
|
||||
codeSentTo: '验证码已发送到 {email}',
|
||||
replaceSuccess: '主邮箱已更新',
|
||||
unbindAction: '解绑',
|
||||
unbindSuccess: '{providerName} 已解绑',
|
||||
boundCount: '已关联 {count} 条记录',
|
||||
status: {
|
||||
bound: '已绑定',
|
||||
notBound: '未绑定',
|
||||
@@ -2411,6 +2424,7 @@ export default {
|
||||
rateLimited: '限流中',
|
||||
overloaded: '过载中',
|
||||
tempUnschedulable: '临时不可调度',
|
||||
quotaExceeded: '配额超限',
|
||||
unschedulable: '不可调度',
|
||||
rateLimitedUntil: '限流中,当前不参与调度,预计 {time} 自动恢复',
|
||||
rateLimitedAutoResume: '{time} 自动恢复',
|
||||
@@ -5800,8 +5814,34 @@ export default {
|
||||
alipayDesktopQrHint: '电脑端支付宝应展示扫码单,请刷新后重试,或确认浏览器未拦截当前支付页。',
|
||||
alipayMobileUnavailable: '当前页面未成功跳转到支付宝。',
|
||||
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: '该服务商有未完成的订单,请等待订单完成后再操作',
|
||||
PAYMENT_PROVIDER_CONFLICT: '该支付方式已有其他启用中的服务商实例,请先停用后再继续。',
|
||||
CANCEL_RATE_LIMITED: '取消订单过于频繁,请稍后再试',
|
||||
NOT_FOUND: '订单不存在',
|
||||
FORBIDDEN: '无权限操作此订单',
|
||||
CONFLICT: '订单状态已变更,请刷新',
|
||||
INVALID_ORDER_TYPE: '仅余额订单可申请退款',
|
||||
INVALID_STATUS: '当前订单状态不允许此操作',
|
||||
BALANCE_NOT_ENOUGH: '退款金额超过余额',
|
||||
REFUND_AMOUNT_EXCEEDED: '退款金额超过充值金额',
|
||||
REFUND_FAILED: '退款失败',
|
||||
},
|
||||
stripePay: '立即支付',
|
||||
stripeSuccessProcessing: '支付成功,正在处理订单...',
|
||||
|
||||
@@ -23,14 +23,96 @@ interface ApiErrorLike {
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (!err || typeof err !== 'object') return undefined
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
||||
@@ -193,7 +193,9 @@ export function formatReasoningEffort(effort: string | null | undefined): string
|
||||
return 'High'
|
||||
case 'xhigh':
|
||||
case 'extrahigh':
|
||||
return 'Xhigh'
|
||||
return 'XHigh'
|
||||
case 'max':
|
||||
return 'Max'
|
||||
case 'none':
|
||||
case 'minimal':
|
||||
return '-'
|
||||
|
||||
@@ -4710,7 +4710,7 @@ import ProxySelector from "@/components/common/ProxySelector.vue";
|
||||
import ImageUpload from "@/components/common/ImageUpload.vue";
|
||||
import BackupSettings from "@/views/admin/BackupView.vue";
|
||||
import { useClipboard } from "@/composables/useClipboard";
|
||||
import { extractApiErrorMessage } from "@/utils/apiError";
|
||||
import { extractApiErrorMessage, extractI18nErrorMessage } from "@/utils/apiError";
|
||||
import { useAppStore } from "@/stores";
|
||||
import { useAdminSettingsStore } from "@/stores/adminSettings";
|
||||
import { normalizeVisibleMethod } from "@/components/payment/paymentFlow";
|
||||
@@ -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<
|
||||
ProviderInstance,
|
||||
"id" | "provider_key" | "supported_types" | "enabled" | "name"
|
||||
@@ -6531,7 +6526,7 @@ async function loadProviders() {
|
||||
const res = await adminAPI.payment.getProviders();
|
||||
providers.value = res.data || [];
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t("common.error")));
|
||||
appStore.showError(extractI18nErrorMessage(err, t, "payment.errors", t("common.error")));
|
||||
} finally {
|
||||
providersLoading.value = false;
|
||||
}
|
||||
@@ -6580,9 +6575,7 @@ async function handleSaveProvider(payload: Partial<ProviderInstance>) {
|
||||
// Auto-save settings so provider changes take effect immediately
|
||||
await saveSettings();
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(
|
||||
extractApiErrorMessage(err, t("common.error"), paymentErrorMap.value),
|
||||
);
|
||||
appStore.showError(extractI18nErrorMessage(err, t, "payment.errors", t("common.error")));
|
||||
} finally {
|
||||
providerSaving.value = false;
|
||||
}
|
||||
@@ -6620,9 +6613,7 @@ async function handleToggleField(
|
||||
await adminAPI.payment.updateProvider(provider.id, payload);
|
||||
await loadProviders();
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(
|
||||
extractApiErrorMessage(err, t("common.error"), paymentErrorMap.value),
|
||||
);
|
||||
appStore.showError(extractI18nErrorMessage(err, t, "payment.errors", t("common.error")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6647,9 +6638,7 @@ async function handleToggleType(provider: ProviderInstance, type: string) {
|
||||
} as any);
|
||||
await loadProviders();
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(
|
||||
extractApiErrorMessage(err, t("common.error"), paymentErrorMap.value),
|
||||
);
|
||||
appStore.showError(extractI18nErrorMessage(err, t, "payment.errors", t("common.error")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6671,7 +6660,7 @@ async function handleReorderProviders(
|
||||
);
|
||||
await loadProviders();
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t("common.error")));
|
||||
appStore.showError(extractI18nErrorMessage(err, t, "payment.errors", t("common.error")));
|
||||
loadProviders();
|
||||
}
|
||||
}
|
||||
@@ -6684,9 +6673,7 @@ async function handleDeleteProvider() {
|
||||
showDeleteProviderDialog.value = false;
|
||||
loadProviders();
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(
|
||||
extractApiErrorMessage(err, t("common.error"), paymentErrorMap.value),
|
||||
);
|
||||
appStore.showError(extractI18nErrorMessage(err, t, "payment.errors", t("common.error")));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { adminPaymentAPI } from '@/api/admin/payment'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import { formatOrderDateTime } from '@/components/payment/orderUtils'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||
@@ -167,7 +167,7 @@ async function loadOrders() {
|
||||
orders.value = res.data.items || []
|
||||
orderPagination.total = res.data.total || 0
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error')))
|
||||
} finally { ordersLoading.value = false }
|
||||
}
|
||||
|
||||
@@ -214,12 +214,12 @@ async function showOrderDetail(order: PaymentOrder) {
|
||||
|
||||
async function handleCancelOrder(order: PaymentOrder) {
|
||||
try { await adminPaymentAPI.cancelOrder(order.id); appStore.showSuccess(t('payment.admin.orderCancelled')); loadOrders() }
|
||||
catch (err: unknown) { appStore.showError(extractApiErrorMessage(err, t('common.error'))) }
|
||||
catch (err: unknown) { appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error'))) }
|
||||
}
|
||||
|
||||
async function handleRetryOrder(order: PaymentOrder) {
|
||||
try { await adminPaymentAPI.retryRecharge(order.id); appStore.showSuccess(t('payment.admin.retrySuccess')); loadOrders() }
|
||||
catch (err: unknown) { appStore.showError(extractApiErrorMessage(err, t('common.error'))) }
|
||||
catch (err: unknown) { appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error'))) }
|
||||
}
|
||||
|
||||
function openRefundDialog(order: PaymentOrder) { selectedOrder.value = order; showRefundDialog.value = true }
|
||||
@@ -230,7 +230,7 @@ async function handleRefund(data: { amount: number; reason: string; deduct_balan
|
||||
try {
|
||||
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()
|
||||
} catch (err: unknown) { appStore.showError(extractApiErrorMessage(err, t('common.error'))) }
|
||||
} catch (err: unknown) { appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error'))) }
|
||||
finally { refundSubmitting.value = false }
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ import { ref, watch, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { adminPaymentAPI } from '@/api/admin/payment'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import type { DashboardStats } from '@/types/payment'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||
import LoadingSpinner from '@/components/common/LoadingSpinner.vue'
|
||||
@@ -110,7 +110,7 @@ async function loadDashboard() {
|
||||
const res = await adminPaymentAPI.getDashboard(days.value)
|
||||
stats.value = res.data
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error')))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { adminPaymentAPI } from '@/api/admin/payment'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import adminAPI from '@/api/admin'
|
||||
import type { SubscriptionPlan } from '@/types/payment'
|
||||
import type { AdminGroup } from '@/types'
|
||||
@@ -150,7 +150,7 @@ async function loadPlans() {
|
||||
: (p.features || []),
|
||||
}))
|
||||
}
|
||||
catch (err: unknown) { appStore.showError(extractApiErrorMessage(err, t('common.error'))) }
|
||||
catch (err: unknown) { appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error'))) }
|
||||
finally { plansLoading.value = false }
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ async function toggleForSale(plan: SubscriptionPlan) {
|
||||
await adminPaymentAPI.updatePlan(plan.id, { for_sale: !plan.for_sale })
|
||||
plan.for_sale = !plan.for_sale
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error')))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ function confirmDeletePlan(plan: SubscriptionPlan) { deletingPlanId.value = plan
|
||||
async function handleDeletePlan() {
|
||||
if (!deletingPlanId.value) return
|
||||
try { await adminPaymentAPI.deletePlan(deletingPlanId.value); appStore.showSuccess(t('common.deleted')); showDeletePlanDialog.value = false; loadPlans() }
|
||||
catch (err: unknown) { appStore.showError(extractApiErrorMessage(err, t('common.error'))) }
|
||||
catch (err: unknown) { appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error'))) }
|
||||
}
|
||||
|
||||
// ==================== Lifecycle ====================
|
||||
|
||||
@@ -39,7 +39,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||
import { usePaymentStore } from '@/stores/payment'
|
||||
import { paymentAPI } from '@/api/payment'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import { useAppStore } from '@/stores'
|
||||
import QRCode from 'qrcode'
|
||||
import alipayIcon from '@/assets/icons/alipay.svg'
|
||||
@@ -167,7 +167,7 @@ async function handleCancel() {
|
||||
cleanup()
|
||||
router.push('/purchase')
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error')))
|
||||
} finally {
|
||||
cancelling.value = false
|
||||
}
|
||||
|
||||
@@ -252,13 +252,13 @@ import { usePaymentStore } from '@/stores/payment'
|
||||
import { useSubscriptionStore } from '@/stores/subscriptions'
|
||||
import { useAppStore } from '@/stores'
|
||||
import { paymentAPI } from '@/api/payment'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import { extractApiErrorMessage, extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import { isMobileDevice } from '@/utils/device'
|
||||
import type { SubscriptionPlan, CheckoutInfoResponse, CreateOrderResult, OrderType } from '@/types/payment'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||
import AmountInput from '@/components/payment/AmountInput.vue'
|
||||
import PaymentMethodSelector from '@/components/payment/PaymentMethodSelector.vue'
|
||||
import { METHOD_ORDER, POPUP_WINDOW_FEATURES, STRIPE_POPUP_WINDOW_FEATURES } from '@/components/payment/providerConfig'
|
||||
import { METHOD_ORDER, getPaymentPopupFeatures } from '@/components/payment/providerConfig'
|
||||
import {
|
||||
PAYMENT_RECOVERY_STORAGE_KEY,
|
||||
buildCreateOrderPayload,
|
||||
@@ -630,8 +630,8 @@ async function createOrder(orderAmount: number, orderType: OrderType, planId?: n
|
||||
payload.is_mobile = isMobileDevice()
|
||||
|
||||
const result = await paymentStore.createOrder(payload) as CreateOrderResult & { resume_token?: string }
|
||||
const openWindow = (url: string, features = POPUP_WINDOW_FEATURES) => {
|
||||
const win = window.open(url, 'paymentPopup', features)
|
||||
const openWindow = (url: string) => {
|
||||
const win = window.open(url, 'paymentPopup', getPaymentPopupFeatures())
|
||||
if (!win || win.closed) {
|
||||
window.location.href = url
|
||||
}
|
||||
@@ -672,7 +672,7 @@ async function createOrder(orderAmount: number, orderType: OrderType, planId?: n
|
||||
persistRecoverySnapshot(decision.recovery)
|
||||
|
||||
if (decision.kind === 'stripe_popup') {
|
||||
openWindow(decision.paymentState.payUrl, STRIPE_POPUP_WINDOW_FEATURES)
|
||||
openWindow(decision.paymentState.payUrl)
|
||||
return
|
||||
}
|
||||
if (decision.kind === 'stripe_route') {
|
||||
@@ -710,8 +710,8 @@ async function createOrder(orderAmount: number, orderType: OrderType, planId?: n
|
||||
err,
|
||||
normalizeVisibleMethod(options.paymentType || selectedMethod.value) || selectedMethod.value,
|
||||
)
|
||||
if (!errorMessage.value) {
|
||||
errorMessage.value = extractApiErrorMessage(err, t('payment.result.failed'))
|
||||
if (!handled) {
|
||||
errorMessage.value = extractI18nErrorMessage(err, t, 'payment.errors', extractApiErrorMessage(err, t('payment.result.failed')))
|
||||
errorHintMessage.value = ''
|
||||
}
|
||||
if (handled) {
|
||||
@@ -825,7 +825,7 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) { appStore.showError(extractApiErrorMessage(err, t('common.error'))) }
|
||||
} catch (err: unknown) { appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error'))) }
|
||||
finally { loading.value = false }
|
||||
// Fetch active subscriptions (uses cache, non-blocking)
|
||||
subscriptionStore.fetchActiveSubscriptions().catch(() => {})
|
||||
|
||||
@@ -99,7 +99,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { usePaymentStore } from '@/stores/payment'
|
||||
import { paymentAPI } from '@/api/payment'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import { isMobileDevice } from '@/utils/device'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
import type { Stripe, StripeElements } from '@stripe/stripe-js'
|
||||
@@ -167,7 +167,7 @@ onMounted(async () => {
|
||||
mountPaymentElement(stripe, clientSecret)
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
initError.value = extractApiErrorMessage(err, t('payment.stripeLoadFailed'))
|
||||
initError.value = extractI18nErrorMessage(err, t, 'payment.errors', t('payment.stripeLoadFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -248,7 +248,7 @@ async function handleGenericPay() {
|
||||
scheduleClose()
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
stripeError.value = extractApiErrorMessage(err, t('payment.result.failed'))
|
||||
stripeError.value = extractI18nErrorMessage(err, t, 'payment.errors', t('payment.result.failed'))
|
||||
} finally {
|
||||
stripeSubmitting.value = false
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import { isMobileDevice } from '@/utils/device'
|
||||
|
||||
interface StripeWithWechatPay {
|
||||
@@ -143,7 +143,7 @@ async function initStripe(clientSecret: string, publishableKey: string) {
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
error.value = extractApiErrorMessage(err, t('payment.stripeLoadFailed'))
|
||||
error.value = extractI18nErrorMessage(err, t, 'payment.errors', t('payment.stripeLoadFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/stores'
|
||||
import { paymentAPI } from '@/api/payment'
|
||||
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||
import { extractI18nErrorMessage } from '@/utils/apiError'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||
import Pagination from '@/components/common/Pagination.vue'
|
||||
@@ -128,7 +128,7 @@ async function fetchOrders() {
|
||||
orders.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error')))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -148,7 +148,7 @@ async function confirmCancel() {
|
||||
cancelTargetId.value = null
|
||||
await fetchOrders()
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error')))
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
}
|
||||
@@ -166,7 +166,7 @@ async function confirmRefund() {
|
||||
refundReason.value = ''
|
||||
await fetchOrders()
|
||||
} catch (err: unknown) {
|
||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||
appStore.showError(extractI18nErrorMessage(err, t, 'payment.errors', t('common.error')))
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user