96 lines
3.5 KiB
Vue
96 lines
3.5 KiB
Vue
<template>
|
|
<div>
|
|
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{{ t('payment.paymentMethod') }}
|
|
</label>
|
|
<div class="grid grid-cols-2 gap-3 sm:flex">
|
|
<button
|
|
v-for="method in sortedMethods"
|
|
:key="method.type"
|
|
type="button"
|
|
:disabled="!method.available"
|
|
:class="[
|
|
'relative flex h-[60px] flex-col items-center justify-center rounded-lg border px-3 transition-all sm:flex-1',
|
|
!method.available
|
|
? 'cursor-not-allowed border-gray-200 bg-gray-50 opacity-50 dark:border-dark-700 dark:bg-dark-800/50'
|
|
: selected === method.type
|
|
? methodSelectedClass(method.type)
|
|
: 'border-gray-300 bg-white text-gray-700 hover:border-gray-400 dark:border-dark-600 dark:bg-dark-800 dark:text-gray-200 dark:hover:border-dark-500',
|
|
]"
|
|
@click="method.available && emit('select', method.type)"
|
|
>
|
|
<span class="flex items-center gap-2">
|
|
<img :src="methodIcon(method.type)" :alt="t(`payment.methods.${method.type}`)" class="h-7 w-7 object-contain" />
|
|
<span class="flex flex-col items-start leading-none">
|
|
<span class="text-base font-semibold">{{ t(`payment.methods.${method.type}`) }}</span>
|
|
<span
|
|
v-if="method.fee_rate > 0"
|
|
class="text-[10px] tracking-wide text-gray-500 dark:text-dark-400"
|
|
>
|
|
{{ t('payment.fee') }} {{ method.fee_rate }}%
|
|
</span>
|
|
</span>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { METHOD_ORDER } from './providerConfig'
|
|
import alipayIcon from '@/assets/icons/alipay.svg'
|
|
import wxpayIcon from '@/assets/icons/wxpay.svg'
|
|
import stripeIcon from '@/assets/icons/stripe.svg'
|
|
import airwallexIcon from '@/assets/icons/airwallex.svg'
|
|
|
|
export interface PaymentMethodOption {
|
|
type: string
|
|
fee_rate: number
|
|
available: boolean
|
|
}
|
|
|
|
const props = defineProps<{
|
|
methods: PaymentMethodOption[]
|
|
selected: string
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
select: [type: string]
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
|
|
const METHOD_ICONS: Record<string, string> = {
|
|
alipay: alipayIcon,
|
|
wxpay: wxpayIcon,
|
|
stripe: stripeIcon,
|
|
airwallex: airwallexIcon,
|
|
}
|
|
|
|
const sortedMethods = computed(() => {
|
|
const order: readonly string[] = METHOD_ORDER
|
|
return [...props.methods].sort((a, b) => {
|
|
const ai = order.indexOf(a.type)
|
|
const bi = order.indexOf(b.type)
|
|
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi)
|
|
})
|
|
})
|
|
|
|
function methodIcon(type: string): string {
|
|
if (type.includes('alipay')) return METHOD_ICONS.alipay
|
|
if (type.includes('wxpay')) return METHOD_ICONS.wxpay
|
|
if (type === 'airwallex') return METHOD_ICONS.airwallex
|
|
return METHOD_ICONS[type] || alipayIcon
|
|
}
|
|
|
|
function methodSelectedClass(type: string): string {
|
|
if (type.includes('alipay')) return 'border-[#02A9F1] bg-blue-50 text-gray-900 shadow-sm dark:bg-blue-950 dark:text-gray-100'
|
|
if (type.includes('wxpay')) return 'border-[#09BB07] bg-green-50 text-gray-900 shadow-sm dark:bg-green-950 dark:text-gray-100'
|
|
if (type === 'stripe') return 'border-[#676BE5] bg-indigo-50 text-gray-900 shadow-sm dark:bg-indigo-950 dark:text-gray-100'
|
|
if (type === 'airwallex') return 'border-[#FF6B3D] bg-orange-50 text-gray-900 shadow-sm dark:border-[#FF8E3C] dark:bg-orange-950 dark:text-gray-100'
|
|
return 'border-primary-500 bg-primary-50 text-gray-900 shadow-sm dark:bg-primary-950 dark:text-gray-100'
|
|
}
|
|
</script>
|