Files
sub2api/frontend/src/components/payment/PaymentProviderList.vue
erio 63d1860dc0 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.
2026-04-11 13:16:35 +08:00

151 lines
5.0 KiB
Vue

<template>
<div class="card">
<!-- Header -->
<div class="border-b border-gray-100 px-4 py-3 dark:border-dark-700">
<div class="flex items-center justify-between">
<div>
<h2 class="text-base font-semibold text-gray-900 dark:text-white">
{{ t('admin.settings.payment.providerManagement') }}
</h2>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
{{ t('admin.settings.payment.providerManagementDesc') }}
</p>
</div>
<div class="flex items-center gap-2">
<button
type="button"
@click="emit('refresh')"
:disabled="loading"
class="btn btn-secondary btn-sm"
:title="t('common.refresh')"
>
<Icon name="refresh" size="sm" :class="loading ? 'animate-spin' : ''" />
</button>
<button
type="button"
@click="emit('create')"
:disabled="!canCreate"
:class="canCreate
? 'btn btn-primary btn-sm'
: 'btn btn-secondary btn-sm cursor-not-allowed opacity-50'"
>
{{ t('admin.settings.payment.createProvider') }}
</button>
</div>
</div>
</div>
<!-- List -->
<div class="p-4">
<!-- Loading -->
<div v-if="loading && !providers.length" class="flex items-center justify-center py-6">
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary-500 border-t-transparent" />
</div>
<!-- Provider cards (draggable) -->
<VueDraggable
v-if="providers.length"
v-model="localProviders"
:animation="200"
handle=".drag-handle"
class="space-y-3"
@end="onDragEnd"
>
<div v-for="p in localProviders" :key="p.id" class="flex items-start gap-2">
<div class="drag-handle mt-3 flex cursor-grab items-center text-gray-300 hover:text-gray-500 active:cursor-grabbing dark:text-dark-600 dark:hover:text-dark-400">
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M7 2a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM13 2a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM7 8a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM13 8a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM7 14a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM13 14a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"/>
</svg>
</div>
<div class="min-w-0 flex-1">
<ProviderCard
:provider="p"
:enabled="isEnabled(p.provider_key)"
:available-types="getTypes(p.provider_key)"
@toggle-field="(field) => emit('toggleField', p, field)"
@toggle-type="(type) => emit('toggleType', p, type)"
@edit="emit('edit', p)"
@delete="emit('delete', p)"
/>
</div>
</div>
</VueDraggable>
<!-- Empty -->
<div v-else-if="!loading" class="py-6 text-center">
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ canCreate
? t('admin.settings.payment.noProviders')
: t('admin.settings.payment.enableTypesFirst') }}
</p>
<button
type="button"
v-if="canCreate"
@click="emit('create')"
class="btn btn-primary btn-sm mt-2"
>
{{ t('admin.settings.payment.createProvider') }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { VueDraggable } from 'vue-draggable-plus'
import Icon from '@/components/icons/Icon.vue'
import ProviderCard from './ProviderCard.vue'
import type { ProviderInstance } from '@/types/payment'
import type { TypeOption } from './providerConfig'
import { getAvailableTypes } from './providerConfig'
const props = defineProps<{
providers: ProviderInstance[]
loading: boolean
canCreate: boolean
enabledPaymentTypes: string[]
allPaymentTypes: TypeOption[]
redirectLabel: string
}>()
const emit = defineEmits<{
refresh: []
create: []
edit: [provider: ProviderInstance]
delete: [provider: ProviderInstance]
toggleField: [provider: ProviderInstance, field: 'enabled' | 'refund_enabled']
toggleType: [provider: ProviderInstance, type: string]
reorder: [providers: { id: number; sort_order: number }[]]
}>()
const { t } = useI18n()
const localProviders = ref<ProviderInstance[]>([])
watch(() => props.providers, (val) => {
localProviders.value = [...val]
}, { immediate: true })
function onDragEnd() {
const updates = localProviders.value.map((p, idx) => ({
id: p.id,
sort_order: idx,
}))
emit('reorder', updates)
}
function isEnabled(providerKey: string): boolean {
return props.enabledPaymentTypes.includes(providerKey)
}
function getTypes(providerKey: string): TypeOption[] {
return getAvailableTypes(providerKey, props.allPaymentTypes, props.redirectLabel)
.map(opt => opt.label === opt.value
? { ...opt, label: t(`payment.methods.${opt.value}`, opt.value) }
: opt,
)
}
</script>