购买套餐里去充值桌面端和移动端支付流程封装

This commit is contained in:
Eamon-meng
2025-06-22 14:42:21 +08:00
parent 483a33296a
commit 50cd4c5760
13 changed files with 713 additions and 464 deletions

View File

@@ -12,20 +12,20 @@ import {useForm} from 'react-hook-form'
import zod from 'zod'
import FormOption from '@/components/composites/purchase/option'
import {RadioGroup} from '@/components/ui/radio-group'
import Image from 'next/image'
import {zodResolver} from '@hookform/resolvers/zod'
import {toast} from 'sonner'
import {useEffect, useMemo, useRef, useState} from 'react'
import {Loader} from 'lucide-react'
import {RechargeComplete, RechargePrepare} from '@/actions/user'
import * as qrcode from 'qrcode'
import {useMemo, useState} from 'react'
import {RechargePrepare} from '@/actions/user'
import {useProfileStore} from '@/components/stores-provider'
import {merge} from '@/lib/utils'
import {
Platform,
PAYMENT_METHODS,
usePlatformType,
PaymentMethod,
} from '@/lib/models/trade'
import {PaymentModal} from '@/components/composites/payment/payment-modal'
import type {Trade} from '@/components/composites/payment/types'
const schema = zod.object({
method: zod.enum(['alipay', 'wechat', 'sft', 'sftAlipay', 'sftWeChat']),
@@ -53,17 +53,11 @@ export default function RechargeModal(props: RechargeModelProps) {
const method = form.watch('method')
const amount = form.watch('amount')
const [step, setStep] = useState(0)
const [payInfo, setPayInfo] = useState<{
trade_no: string
pay_url: string
}>()
const [trade, setTrade] = useState<Trade | null>(null)
const refreshProfile = useProfileStore(store => store.refreshProfile)
// 获取当前平台可用的支付方法
const availableMethods = useMemo(() => {
console.log(PAYMENT_METHODS, 'PAYMENT_METHODS')
return Object.values(PAYMENT_METHODS)
.filter(method => method.availablePlatforms.includes(platform))
.map(method => ({
@@ -72,41 +66,40 @@ export default function RechargeModal(props: RechargeModelProps) {
icon: method.icon,
}))
}, [platform])
const canvas = useRef<HTMLCanvasElement>(null)
useEffect(() => {
console.log('Canvas ref:', canvas.current)
if (!payInfo || !canvas.current || method.includes('alipay')) return
qrcode.toCanvas(canvas.current, payInfo.pay_url, {
width: 200,
margin: 0,
})
}, [payInfo, method])
const refreshProfile = useProfileStore(store => store.refreshProfile)
const createRecharge = async (data: Schema) => {
try {
const paymentMethod = Object.entries(PAYMENT_METHODS).find(
([_, config]) => config.formValue === data.method,
)
console.log(paymentMethod, 'paymentMethod')
if (!paymentMethod) {
throw new Error('无效的支付方式')
}
const actualMethod = paymentMethod[1].getActualMethod(platform)
console.log('转换后的支付方式:', {
formValue: data.method,
platform: platform === Platform.Mobile ? 'Mobile' : 'Desktop',
actualMethod,
methodName: actualMethod === PaymentMethod.Alipay ? 'Alipay'
: actualMethod === PaymentMethod.SftAlipay ? 'SftAlipay'
: actualMethod === PaymentMethod.WeChat ? 'WeChat'
: actualMethod === PaymentMethod.SftWeChat ? 'SftWeChat' : 'Unknown',
})
const resp = {
amount: data.amount.toString(),
platform: platform,
method: paymentMethod[1].actualMethod,
method: actualMethod,
}
console.log(resp, 'resp')
const result = await RechargePrepare(resp)
console.log(result, 'result')
if (result.success) {
setStep(1)
setPayInfo(result.data)
setTrade({
inner_no: result.data.trade_no,
method: actualMethod,
pay_url: result.data.pay_url,
amount: data.amount,
})
}
else {
throw new Error(result.message)
@@ -119,203 +112,117 @@ export default function RechargeModal(props: RechargeModelProps) {
}
}
const confirmRecharge = async () => {
if (!payInfo) {
toast.error(`充值失败`, {
description: `订单信息不存在`,
})
return
}
const handlePaymentSuccess = async () => {
try {
switch (method) {
case 'alipay':
const aliRes = await RechargeComplete({
trade_no: payInfo.trade_no,
})
if (!aliRes.success) {
throw new Error(aliRes.message)
}
break
case 'wechat':
const weRes = await RechargeComplete({
trade_no: payInfo.trade_no,
})
if (!weRes.success) {
throw new Error(weRes.message)
}
break
}
toast.success(`充值成功`)
closeDialog()
await refreshProfile()
toast.success('充值成功')
setOpen(false)
form.reset()
}
catch (e) {
toast.error(`充值失败`, {
toast.error('刷新账户信息失败', {
description: (e as Error).message,
})
}
}
const closeDialog = () => {
setOpen(false)
setPayInfo(undefined)
setStep(0)
form.reset()
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button theme="accent" type="button" className={merge(`px-4 h-8`, props.classNames?.trigger)}></Button>
<Button
theme="accent"
type="button"
className={merge(`px-4 h-8`, props.classNames?.trigger)}
>
</Button>
</DialogTrigger>
<DialogContent className={platform === Platform.Mobile ? 'max-w-[95vw]' : 'max-w-md'}>
<DialogTitle className="flex flex-col gap-2">
</DialogTitle>
{step === 0 && (
<Form form={form} onSubmit={createRecharge} className="flex flex-col gap-8">
{/* 充值额度 */}
<FormField<Schema> name="amount" label="充值额度" className="flex flex-col gap-4">
{({id, field}) => (
<RadioGroup
id={id}
defaultValue={String(field.value)}
onValueChange={v => field.onChange(Number(v))}
className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<FormOption
id={`${id}-20`}
value="20"
label="20元"
compare={String(field.value)}
className="flex-1 max-sm:text-sm max-sm:px-0"
/>
<FormOption
id={`${id}-50`}
value="50"
label="50元"
compare={String(field.value)}
className="flex-1 max-sm:text-sm max-sm:px-0"
/>
<FormOption
id={`${id}-100`}
value="100"
label="100元"
compare={String(field.value)}
className="flex-1 max-sm:text-sm max-sm:px-0"
/>
</div>
<div className="flex items-center gap-2">
<FormOption
id={`${id}-200`}
value="200"
label="200元"
compare={String(field.value)}
className="flex-1 max-sm:text-sm max-sm:px-0"
/>
<FormOption
id={`${id}-500`}
value="500"
label="500元"
compare={String(field.value)}
className="flex-1 max-sm:text-sm max-sm:px-0"
/>
<FormOption
id={`${id}-1000`}
value="1000"
label="1000元"
compare={String(field.value)}
className="flex-1 max-sm:text-sm max-sm:px-0"
/>
</div>
</RadioGroup>
)}
</FormField>
{/* 支付方式 */}
<FormField name="method" label="支付方式" className="flex flex-col gap-4">
{({id, field}) => (
<RadioGroup
id={id}
defaultValue={field.value}
onValueChange={field.onChange}
className="flex gap-2">
{availableMethods.map(({value, name, icon}) => (
<FormOption
key={value}
id={`${id}-${value}`}
value={value}
compare={field.value}
className="flex-1 flex-row justify-center items-center"
>
{icon && <Image src={icon} alt={`${name} logo`} className="w-6 h-6"/>}
<span>{name}</span>
</FormOption>
))}
</RadioGroup>
)}
</FormField>
<DialogFooter className="!flex !flex-row !justify-center">
<Button className="px-8 h-12 text-lg"></Button>
</DialogFooter>
</Form>
)}
{step == 1 && (
{!trade ? (
<>
<div className="flex flex-col items-center gap-4">
<div className="flex flex-col items-center gap-3">
<div className="bg-gray-100 size-50 flex items-center justify-center">
{payInfo
? method === 'alipay'
? <iframe src={payInfo.pay_url} className="w-full h-full"/>
: <canvas ref={canvas} className="w-full h-full"/>
: (
<Loader size={40} className="animate-spin text-weak"/>
)}
</div>
<p className="text-sm text-gray-600 text-center">
使
{method === 'alipay' ? '支付宝' : '微信'}
</p>
</div>
<div className="text-center space-y-1">
<p className="font-medium">
:
<span className="text-accent">
{amount}
</span>
</p>
<p className="text-xs text-gray-500">
:
{payInfo?.trade_no || '创建订单中...'}
</p>
</div>
</div>
<DialogTitle className="flex flex-col gap-2"></DialogTitle>
<Form form={form} onSubmit={createRecharge} className="flex flex-col gap-8">
{/* 充值额度 */}
<FormField<Schema> name="amount" label="充值额度" className="flex flex-col gap-4">
{({id, field}) => (
<RadioGroup
id={id}
defaultValue={String(field.value)}
onValueChange={v => field.onChange(Number(v))}
className="flex flex-col gap-2"
>
<div className="flex items-center gap-2">
{[20, 50, 100].map(value => (
<FormOption
key={value}
id={`${id}-${value}`}
value={String(value)}
label={`${value}`}
compare={String(field.value)}
className="flex-1 max-sm:text-sm max-sm:px-0"
/>
))}
</div>
<div className="flex items-center gap-2">
{[200, 500, 1000].map(value => (
<FormOption
key={value}
id={`${id}-${value}`}
value={String(value)}
label={`${value}`}
compare={String(field.value)}
className="flex-1 max-sm:text-sm max-sm:px-0"
/>
))}
</div>
</RadioGroup>
)}
</FormField>
<DialogFooter className="!flex !flex-row !justify-center">
<Button
className="px-8 text-lg"
onClick={confirmRecharge}
>
</Button>
<Button
theme="outline"
className="px-8 text-lg"
onClick={closeDialog}
>
</Button>
</DialogFooter>
{/* 支付方式 */}
<FormField name="method" label="支付方式" className="flex flex-col gap-4">
{({id, field}) => (
<RadioGroup
id={id}
defaultValue={field.value}
onValueChange={field.onChange}
className="flex gap-2"
>
{availableMethods.map(({value, name, icon}) => (
<FormOption
key={value}
id={`${id}-${value}`}
value={value}
compare={field.value}
className="flex-1 flex-row justify-center items-center"
>
{icon && <img src={icon.src} alt={`${name} logo`} className="w-6 h-6 mr-2"/>}
<span>{name}</span>
</FormOption>
))}
</RadioGroup>
)}
</FormField>
<DialogFooter className="!flex !flex-row !justify-center">
<Button className="px-8 h-12 text-lg"></Button>
</DialogFooter>
</Form>
</>
) : (
<PaymentModal
open={open}
onOpenChange={(open) => {
if (!open) {
setTrade(null)
setOpen(false)
}
}}
trade={trade}
platform={platform}
onSuccess={handlePaymentSuccess}
/>
)}
</DialogContent>
</Dialog>