Add a full payment and subscription system supporting EasyPay (Alipay/WeChat), Stripe, and direct Alipay/WeChat Pay providers with multi-instance load balancing.
112 lines
3.0 KiB
Vue
112 lines
3.0 KiB
Vue
<template>
|
|
<div class="space-y-4">
|
|
<!-- Quick Amount Buttons -->
|
|
<div>
|
|
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{{ t('payment.quickAmounts') }}
|
|
</label>
|
|
<div class="grid grid-cols-3 gap-2">
|
|
<button
|
|
v-for="amt in filteredAmounts"
|
|
:key="amt"
|
|
type="button"
|
|
:class="[
|
|
'rounded-lg border-2 px-4 py-3 text-center font-medium transition-colors',
|
|
modelValue === amt
|
|
? 'border-primary-500 bg-primary-50 text-primary-700 dark:border-primary-400 dark:bg-primary-900/40 dark:text-primary-300'
|
|
: 'border-gray-200 bg-white text-gray-700 hover:border-gray-300 dark:border-dark-600 dark:bg-dark-800 dark:text-gray-200 dark:hover:border-dark-500',
|
|
]"
|
|
@click="selectAmount(amt)"
|
|
>
|
|
{{ amt }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Amount Input -->
|
|
<div>
|
|
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{{ t('payment.customAmount') }}
|
|
</label>
|
|
<div class="relative">
|
|
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-dark-500">
|
|
$
|
|
</span>
|
|
<input
|
|
type="text"
|
|
inputmode="decimal"
|
|
:value="customText"
|
|
:placeholder="placeholderText"
|
|
class="input w-full py-3 pl-8 pr-4"
|
|
@input="handleInput"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, watch } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
amounts?: number[]
|
|
modelValue: number | null
|
|
min?: number
|
|
max?: number
|
|
}>(), {
|
|
amounts: () => [10, 20, 50, 100, 200, 500, 1000, 2000, 5000],
|
|
min: 0,
|
|
max: 0,
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: number | null]
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
|
|
const customText = ref('')
|
|
|
|
// 0 = no limit
|
|
const filteredAmounts = computed(() =>
|
|
props.amounts.filter((a) => (props.min <= 0 || a >= props.min) && (props.max <= 0 || a <= props.max))
|
|
)
|
|
|
|
const placeholderText = computed(() => {
|
|
if (props.min > 0 && props.max > 0) return `${props.min} - ${props.max}`
|
|
if (props.min > 0) return `≥ ${props.min}`
|
|
if (props.max > 0) return `≤ ${props.max}`
|
|
return t('payment.enterAmount')
|
|
})
|
|
|
|
const AMOUNT_PATTERN = /^\d*(\.\d{0,2})?$/
|
|
|
|
function selectAmount(amt: number) {
|
|
customText.value = String(amt)
|
|
emit('update:modelValue', amt)
|
|
}
|
|
|
|
function handleInput(e: Event) {
|
|
const val = (e.target as HTMLInputElement).value
|
|
if (!AMOUNT_PATTERN.test(val)) return
|
|
customText.value = val
|
|
if (val === '') {
|
|
emit('update:modelValue', null)
|
|
return
|
|
}
|
|
const num = parseFloat(val)
|
|
if (!isNaN(num) && num > 0) {
|
|
emit('update:modelValue', num)
|
|
} else {
|
|
emit('update:modelValue', null)
|
|
}
|
|
}
|
|
|
|
watch(() => props.modelValue, (v) => {
|
|
if (v !== null && String(v) !== customText.value) {
|
|
customText.value = String(v)
|
|
}
|
|
}, { immediate: true })
|
|
</script>
|