feat(payment): add complete payment system with multi-provider support
Add a full payment and subscription system supporting EasyPay (Alipay/WeChat), Stripe, and direct Alipay/WeChat Pay providers with multi-instance load balancing.
This commit is contained in:
106
frontend/src/components/payment/ProviderCard.vue
Normal file
106
frontend/src/components/payment/ProviderCard.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'group relative rounded-lg border transition-all',
|
||||
enabled ? 'border-gray-200 dark:border-dark-600' : 'border-gray-200 bg-gray-50 opacity-50 dark:border-dark-700 dark:bg-dark-800/50',
|
||||
]"
|
||||
:title="!enabled ? t('admin.settings.payment.typeDisabled') + ' — ' + t('admin.settings.payment.enableTypesFirst') : undefined"
|
||||
>
|
||||
<div :class="[
|
||||
'flex items-center justify-between px-4 py-2.5',
|
||||
!enabled && 'pointer-events-none',
|
||||
]">
|
||||
<!-- Left: icon + name + key badge + type badges -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div :class="[
|
||||
'rounded-md p-1.5',
|
||||
provider.enabled && enabled ? 'bg-green-100 dark:bg-green-900/30' : 'bg-gray-100 dark:bg-dark-700',
|
||||
]">
|
||||
<Icon
|
||||
name="server"
|
||||
size="sm"
|
||||
:class="provider.enabled && enabled ? 'text-green-600 dark:text-green-400' : 'text-gray-400'"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">{{ provider.name }}</span>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">{{ keyLabel }}</span>
|
||||
<span v-if="provider.payment_mode" class="text-xs text-gray-400 dark:text-gray-500">· {{ modeLabel }}</span>
|
||||
<span v-if="enabled && availableTypes.length" class="text-xs text-gray-300 dark:text-gray-600">|</span>
|
||||
<div v-if="enabled" class="flex items-center gap-1">
|
||||
<button
|
||||
v-for="pt in availableTypes"
|
||||
:key="pt.value"
|
||||
type="button"
|
||||
@click="emit('toggleType', pt.value)"
|
||||
:class="[
|
||||
'rounded px-2 py-0.5 text-xs font-medium transition-all',
|
||||
isSelected(pt.value)
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-gray-100 text-gray-400 dark:bg-dark-700 dark:text-gray-500',
|
||||
]"
|
||||
>{{ pt.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: toggles + actions -->
|
||||
<div class="flex items-center gap-4">
|
||||
<ToggleSwitch :label="t('common.enabled')" :checked="provider.enabled" @toggle="emit('toggleField', 'enabled')" />
|
||||
<ToggleSwitch :label="t('admin.settings.payment.refundEnabled')" :checked="provider.refund_enabled" @toggle="emit('toggleField', 'refund_enabled')" />
|
||||
<div class="flex items-center gap-2 border-l border-gray-200 pl-3 dark:border-dark-600">
|
||||
<button type="button" @click="emit('edit')" class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-900/20 dark:hover:text-blue-400">
|
||||
<Icon name="edit" size="sm" />
|
||||
<span class="text-xs">{{ t('common.edit') }}</span>
|
||||
</button>
|
||||
<button type="button" @click="emit('delete')" class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20 dark:hover:text-red-400">
|
||||
<Icon name="trash" size="sm" />
|
||||
<span class="text-xs">{{ t('common.delete') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import ToggleSwitch from './ToggleSwitch.vue'
|
||||
import type { ProviderInstance } from '@/types/payment'
|
||||
import type { TypeOption } from './providerConfig'
|
||||
import { PAYMENT_MODE_QRCODE, PAYMENT_MODE_POPUP } from './providerConfig'
|
||||
|
||||
const PROVIDER_KEY_LABELS: Record<string, string> = {
|
||||
easypay: 'admin.settings.payment.providerEasypay',
|
||||
alipay: 'admin.settings.payment.providerAlipay',
|
||||
wxpay: 'admin.settings.payment.providerWxpay',
|
||||
stripe: 'admin.settings.payment.providerStripe',
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
provider: ProviderInstance
|
||||
enabled: boolean
|
||||
availableTypes: TypeOption[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleField: [field: 'enabled' | 'refund_enabled']
|
||||
toggleType: [type: string]
|
||||
edit: []
|
||||
delete: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const keyLabel = computed(() => t(PROVIDER_KEY_LABELS[props.provider.provider_key] || props.provider.provider_key))
|
||||
|
||||
const modeLabel = computed(() => {
|
||||
if (props.provider.payment_mode === PAYMENT_MODE_QRCODE) return t('admin.settings.payment.modeQRCode')
|
||||
if (props.provider.payment_mode === PAYMENT_MODE_POPUP) return t('admin.settings.payment.modePopup')
|
||||
return ''
|
||||
})
|
||||
|
||||
function isSelected(type: string): boolean {
|
||||
return props.provider.supported_types.includes(type)
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user