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

230 lines
7.1 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 {zodResolver} from '@hookform/resolvers/zod'
import {toast} from 'sonner'
import {useState} from 'react'
import {RechargeComplete, RechargePrepare} from '@/actions/user'
import {useProfileStore} from '@/components/stores/profile'
import {merge} from '@/lib/utils'
import {
TradePlatform,
TradeMethod,
} from '@/lib/models/trade'
import {PaymentModal} from '@/components/composites/payment/payment-modal'
import Image from 'next/image'
import wechat from '@/components/composites/purchase/_assets/wechat.svg'
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
import {PaymentProps} from '@/components/composites/payment/type'
import {usePlatformType} from '@/lib/hooks'
const schema = zod.object({
method: zod.enum(['alipay', 'wechat']),
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 [trade, setTrade] = useState<PaymentProps>()
const refreshProfile = useProfileStore(store => store.refreshProfile)
const createRecharge = async (data: Schema) => {
try {
const method = data.method === 'alipay'
? TradeMethod.SftAlipay
: TradeMethod.SftWechat
const req = {
platform: platform,
method: method,
amount: Number(data.amount) * 100,
}
const result = await RechargePrepare(req)
if (result.success) {
setTrade({
inner_no: result.data.trade_no,
pay_url: result.data.pay_url,
amount: data.amount,
platform: platform,
method: method,
})
}
else {
throw new Error(result.message)
}
}
catch (error) {
toast.error('创建订单失败', {
description: error instanceof Error ? error.message : '未知错误',
})
}
}
const handlePaymentSuccess = async (showFail: boolean) => {
if (!trade) return
try {
const resp = await RechargeComplete({
trade_no: trade.inner_no,
method: trade.method,
})
if (!resp.success) {
throw new Error(resp.message)
}
toast.success('充值成功')
setTrade(undefined) // 清除交易状态
setOpen(false) // 关闭弹窗
form.reset() // 重置表单
await refreshProfile()
}
catch (e) {
if (showFail) {
toast.error('支付验证失败', {description: (e as Error).message})
}
}
}
const handleClose = () => {
setTrade(undefined)
}
return (
<Dialog
open={open}
onOpenChange={(isOpen) => {
if (!isOpen) {
setTrade(undefined)
form.reset()
}
setOpen(isOpen)
}}>
<DialogTrigger asChild>
<Button
theme="accent"
type="button"
className={merge(`px-4 h-8`, props.classNames?.trigger)}
>
</Button>
</DialogTrigger>
<DialogContent className={platform === TradePlatform.Mobile ? 'max-w-[95vw]' : 'max-w-md'}>
{!trade ? (
<>
<DialogTitle className="flex flex-col gap-2"></DialogTitle>
<Form form={form} handler={form.handleSubmit(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>
{/* 支付方式 */}
<FormField name="method" label="支付方式" className="flex flex-col gap-4">
{({id, field}) => (
<RadioGroup
id={id}
{...field}
onValueChange={field.onChange}
className="flex gap-2"
>
<FormOption
id="alipay"
value="alipay"
compare={field.value}
className="flex-1 flex-row justify-center items-center"
>
<Image src={alipay} alt="logo" aria-hidden className="w-6 h-6 mr-2"/>
<span></span>
</FormOption>
<FormOption
id="wechat"
value="wechat"
compare={field.value}
className="flex-1 flex-row justify-center items-center"
>
<Image src={wechat} alt="logo" aria-hidden className="w-6 h-6 mr-2"/>
<span></span>
</FormOption>
</RadioGroup>
)}
</FormField>
<DialogFooter>
<Button></Button>
</DialogFooter>
</Form>
</>
) : (
<PaymentModal
{...trade}
onConfirm={handlePaymentSuccess}
onClose={handleClose}
/>
)}
</DialogContent>
</Dialog>
)
}