feat: add Airwallex payments and multi-currency support

This commit is contained in:
shaw
2026-05-11 10:45:07 +08:00
parent dbc8ae658c
commit b23055af5b
65 changed files with 3164 additions and 162 deletions
@@ -149,6 +149,12 @@
<svg v-else class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></svg>
</button>
</div>
<Select
v-else-if="field.options?.length"
v-model="config[field.key]"
:options="field.options"
:searchable="field.options.length > 5"
/>
<input
v-else
type="text"
@@ -156,6 +162,9 @@
class="input"
:placeholder="field.defaultValue || ''"
/>
<p v-if="field.hintKey" class="mt-1 text-xs leading-relaxed text-gray-500 dark:text-gray-400">
{{ t(field.hintKey) }}
</p>
</div>
</div>
@@ -177,14 +186,17 @@
</div>
</div>
<!-- Stripe webhook hint -->
<div v-if="stripeWebhookUrl" class="mt-3 rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-800/50 dark:bg-blue-900/20">
<!-- 服务商 Webhook 提示 -->
<div v-if="providerWebhookUrl" class="mt-3 rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-800/50 dark:bg-blue-900/20">
<p class="text-xs text-blue-700 dark:text-blue-300">
{{ t('admin.settings.payment.stripeWebhookHint') }}
{{ t(providerWebhookHint) }}
</p>
<code class="mt-1 block break-all rounded bg-blue-100 px-2 py-1 text-xs text-blue-800 dark:bg-blue-900/40 dark:text-blue-200">
{{ stripeWebhookUrl }}
{{ providerWebhookUrl }}
</code>
<p v-if="form.provider_key === 'stripe'" class="mt-2 text-xs leading-relaxed text-blue-700 dark:text-blue-300">
{{ t('admin.settings.payment.stripeWebhookApiVersionHint', { version: STRIPE_SDK_API_VERSION }) }}
</p>
</div>
</div>
@@ -266,6 +278,7 @@ import {
WEBHOOK_PATHS,
PAYMENT_MODE_QRCODE,
PAYMENT_MODE_POPUP,
STRIPE_SDK_API_VERSION,
getAvailableTypes,
extractBaseUrl,
} from './providerConfig'
@@ -330,8 +343,18 @@ const visibleFields = reactive<Record<string, boolean>>({})
// --- Computed ---
const defaultBaseUrl = typeof window !== 'undefined' ? window.location.origin : ''
const stripeWebhookUrl = computed(() =>
form.provider_key === 'stripe' ? defaultBaseUrl + WEBHOOK_PATHS.stripe : '',
const providerWebhookHintMap: Record<string, string> = {
stripe: 'admin.settings.payment.stripeWebhookHint',
airwallex: 'admin.settings.payment.airwallexWebhookHint',
}
const providerWebhookUrl = computed(() => {
const path = WEBHOOK_PATHS[form.provider_key]
return providerWebhookHintMap[form.provider_key] && path ? defaultBaseUrl + path : ''
})
const providerWebhookHint = computed(() =>
providerWebhookHintMap[form.provider_key] || 'admin.settings.payment.stripeWebhookHint',
)
const callbackPaths = computed(() => PROVIDER_CALLBACK_PATHS[form.provider_key] || null)
@@ -415,6 +438,14 @@ const paymentGuide = computed<PaymentGuide | null>(() => {
}
}
if (form.provider_key === 'airwallex') {
return {
summary: t('admin.settings.payment.airwallexGuideSummary'),
note: t('admin.settings.payment.airwallexGuideNote'),
items: [],
}
}
return null
})
@@ -527,9 +558,19 @@ function handleSave() {
}
}
const clearableConfigKeys = new Set(
(PROVIDER_CONFIG_FIELDS[form.provider_key] || [])
.filter(field => field.clearable)
.map(field => field.key),
)
const filteredConfig: Record<string, string> = {}
for (const [k, v] of Object.entries(config)) {
if (!v || !v.trim()) continue
if (!v || !v.trim()) {
if (clearableConfigKeys.has(k)) {
filteredConfig[k] = ''
}
continue
}
filteredConfig[k] = v
}