Files
web/src/components/composites/recharge/index.tsx

331 lines
10 KiB
TypeScript

'use client'
import {
Dialog,
DialogContent,
DialogFooter,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import {Button} from '@/components/ui/button'
import {Form, FormField} from '@/components/ui/form'
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 wechat from '@/components/composites/purchase/_assets/wechat.svg'
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
import {useEffect, useMemo, useRef, useState} from 'react'
import {Loader} from 'lucide-react'
import {RechargeByPay, RechargeByAlipayConfirm, RechargeByWechat, RechargeByWechatConfirm} from '@/actions/user'
import * as qrcode from 'qrcode'
import {useProfileStore} from '@/components/providers/StoreProvider'
import {merge} from '@/lib/utils'
import {
Platform,
PAYMENT_METHODS,
usePlatformType,
} from '@/lib/models/trade'
const schema = zod.object({
method: zod.enum(['alipay', 'wechat', 'sft', 'sftAlipay', 'sftWeChat']),
amount: zod.number().min(1, '充值金额必须大于 0'),
})
type Schema = zod.infer<typeof schema>
export type RechargeModelProps = {
classNames?: {
trigger?: string
}
}
export default function RechargeModal(props: RechargeModelProps) {
const [open, setOpen] = useState(false)
const platform = usePlatformType()
const form = useForm<Schema>({
resolver: zodResolver(schema),
defaultValues: {
method: 'alipay',
amount: 50,
},
})
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 availableMethods = useMemo(() => {
console.log(PAYMENT_METHODS, 'PAYMENT_METHODS')
return Object.values(PAYMENT_METHODS)
.filter(method => method.availablePlatforms.includes(platform))
.map(method => ({
value: method.formValue,
name: method.name,
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 resp = {
amount: data.amount.toString(),
platform: platform,
method: paymentMethod[1].actualMethod,
}
console.log(resp, 'resp')
// const result = await RechargeByPay({
// amount: data.amount.toString(),
// platform: platform,
// method: parseInt(paymentMethod[0]) as PaymentMethod,
// })
const result = await RechargeByPay(resp)
console.log(result, 'result')
if (result.success) {
setStep(1)
setPayInfo(result.data)
}
else {
throw new Error(result.message)
}
}
catch (error) {
toast.error('创建订单失败', {
description: error instanceof Error ? error.message : '未知错误',
})
}
}
const confirmRecharge = async () => {
if (!payInfo) {
toast.error(`充值失败`, {
description: `订单信息不存在`,
})
return
}
try {
switch (method) {
case 'alipay':
const aliRes = await RechargeByAlipayConfirm({
trade_no: payInfo.trade_no,
})
if (!aliRes.success) {
throw new Error(aliRes.message)
}
break
case 'wechat':
const weRes = await RechargeByWechatConfirm({
trade_no: payInfo.trade_no,
})
if (!weRes.success) {
throw new Error(weRes.message)
}
break
}
toast.success(`充值成功`)
closeDialog()
await refreshProfile()
}
catch (e) {
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>
</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 && (
<>
<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>
<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>
</>
)}
</DialogContent>
</Dialog>
)
}