购买套餐里去充值桌面端和移动端支付流程封装
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user