完善充值功能,添加支付宝和微信的充值逻辑及二维码展示
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
'use server'
|
||||
import {callByUser, callPublic} from '@/actions/base'
|
||||
|
||||
export async function Identify(props: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use server'
|
||||
import {Bill} from '@/lib/models'
|
||||
import {callByUser} from '@/actions/base'
|
||||
import {PageRecord} from '@/lib/api'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use server'
|
||||
import {callByUser} from '@/actions/base'
|
||||
|
||||
async function createChannels(params: {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export async function tradeCallbackByAlipay() {
|
||||
|
||||
}
|
||||
33
src/actions/user.ts
Normal file
33
src/actions/user.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
'use server'
|
||||
|
||||
import {callByUser} from '@/actions/base'
|
||||
|
||||
export async function RechargeByAlipay(props: {
|
||||
amount: number
|
||||
}) {
|
||||
return callByUser<{
|
||||
trade_no: string
|
||||
pay_url: string
|
||||
}>('/api/user/recharge/prepare/alipay', props)
|
||||
}
|
||||
|
||||
export async function RechargeByAlipayConfirm(props: {
|
||||
trade_no: string
|
||||
}) {
|
||||
return callByUser('/api/user/recharge/confirm/alipay', props)
|
||||
}
|
||||
|
||||
export async function RechargeByWechat(props: {
|
||||
amount: number
|
||||
}) {
|
||||
return callByUser<{
|
||||
trade_no: string
|
||||
pay_url: string
|
||||
}>('/api/user/recharge/prepare/wechat', props)
|
||||
}
|
||||
|
||||
export async function RechargeByWechatConfirm(props: {
|
||||
trade_no: string
|
||||
}) {
|
||||
return callByUser('/api/user/recharge/confirm/wechat', props)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import alipay from '../_assets/alipay.svg'
|
||||
import wechat from '../_assets/wechat.svg'
|
||||
import balance from '../_assets/balance.svg'
|
||||
import Image from 'next/image'
|
||||
import {useContext, useState} from 'react'
|
||||
import {useContext, useRef, useState} from 'react'
|
||||
import {AuthContext} from '@/components/providers/AuthProvider'
|
||||
import {Alert, AlertDescription} from '@/components/ui/alert'
|
||||
import {
|
||||
@@ -21,6 +21,7 @@ import {ApiResponse} from '@/lib/api'
|
||||
import {toast} from 'sonner'
|
||||
import {Loader} from 'lucide-react'
|
||||
import {useRouter} from 'next/navigation'
|
||||
import * as qrcode from 'qrcode'
|
||||
|
||||
export type PayProps = {
|
||||
method: 'alipay' | 'wechat' | 'balance'
|
||||
@@ -34,6 +35,7 @@ export default function Pay(props: PayProps) {
|
||||
const ctx = useContext(AuthContext)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [payInfo, setPayInfo] = useState<CreateResourceResp | undefined>()
|
||||
const canvas = useRef<HTMLCanvasElement>(null)
|
||||
|
||||
const onOpen = async () => {
|
||||
setOpen(true)
|
||||
@@ -58,6 +60,7 @@ export default function Pay(props: PayProps) {
|
||||
}
|
||||
|
||||
setPayInfo(resp.data)
|
||||
await qrcode.toCanvas(canvas.current, resp.data.pay_url)
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
@@ -172,13 +175,15 @@ export default function Pay(props: PayProps) {
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="bg-gray-100 w-52 h-52 flex items-center justify-center">
|
||||
{payInfo ? (
|
||||
<iframe
|
||||
src={payInfo.pay_url}
|
||||
className="w-full h-full"
|
||||
title="支付二维码"
|
||||
/>
|
||||
props.method === 'alipay'
|
||||
? <iframe
|
||||
src={payInfo.pay_url}
|
||||
className="w-full h-full"
|
||||
title="支付二维码"
|
||||
/>
|
||||
: <canvas ref={canvas} className="w-full h-full"/>
|
||||
) : (
|
||||
<Loader size={40} className={`animate-spin`}/>
|
||||
<Loader size={40} className={`animate-spin text-weak`}/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 text-center">
|
||||
|
||||
@@ -15,9 +15,13 @@ import {RadioGroup} from '@/components/ui/radio-group'
|
||||
import Image from 'next/image'
|
||||
import {zodResolver} from '@hookform/resolvers/zod'
|
||||
import {toast} from 'sonner'
|
||||
import {useRouter} from 'next/navigation'
|
||||
import wechat from '@/components/composites/purchase/_assets/wechat.svg'
|
||||
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
|
||||
import {useContext, useRef, useState} from 'react'
|
||||
import {Loader} from 'lucide-react'
|
||||
import {RechargeByAlipay, RechargeByAlipayConfirm, RechargeByWechat, RechargeByWechatConfirm} from '@/actions/user'
|
||||
import * as qrcode from 'qrcode'
|
||||
import {AuthContext} from '@/components/providers/AuthProvider'
|
||||
|
||||
const schema = zod.object({
|
||||
method: zod.enum(['alipay', 'wechat']),
|
||||
@@ -30,6 +34,8 @@ export type RechargeModelProps = {}
|
||||
|
||||
export default function RechargeModal(props: RechargeModelProps) {
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const form = useForm<Schema>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
@@ -38,17 +44,50 @@ export default function RechargeModal(props: RechargeModelProps) {
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const method = form.watch('method')
|
||||
const amount = form.watch('amount')
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
const canvas = useRef<HTMLCanvasElement>(null)
|
||||
const [payInfo, setPayInfo] = useState<{
|
||||
trade_no: string
|
||||
pay_url: string
|
||||
}>()
|
||||
|
||||
const ctx = useContext(AuthContext)
|
||||
|
||||
const createRecharge = async (data: Schema) => {
|
||||
try {
|
||||
// const resp = await tradeRecharge(data)
|
||||
// if (!resp.success) {
|
||||
// throw new Error(resp.message)
|
||||
// }
|
||||
|
||||
// todo 跳转支付页
|
||||
router.push('/pay')
|
||||
switch (data.method) {
|
||||
case 'alipay':
|
||||
const aliRes = await RechargeByAlipay({
|
||||
amount: data.amount,
|
||||
})
|
||||
if (aliRes.success) {
|
||||
setStep(1)
|
||||
setPayInfo(aliRes.data)
|
||||
}
|
||||
else {
|
||||
toast.error(`充值失败`, {
|
||||
description: aliRes.message,
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'wechat':
|
||||
const weRes = await RechargeByWechat({
|
||||
amount: data.amount,
|
||||
})
|
||||
if (weRes.success) {
|
||||
setStep(1)
|
||||
setPayInfo(weRes.data)
|
||||
await qrcode.toCanvas(canvas.current, weRes.data.pay_url)
|
||||
}
|
||||
else {
|
||||
toast.error(`充值失败`, {
|
||||
description: weRes.message,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(`充值失败`, {
|
||||
@@ -57,8 +96,53 @@ export default function RechargeModal(props: RechargeModelProps) {
|
||||
}
|
||||
}
|
||||
|
||||
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 ctx.refreshProfile()
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(`充值失败`, {
|
||||
description: (e as Error).message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
setOpen(false)
|
||||
setPayInfo(undefined)
|
||||
setStep(0)
|
||||
}
|
||||
|
||||
const [step, setStep] = useState(0)
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button theme={`accent`} type={`button`} className={`px-4 h-8`}>去充值</Button>
|
||||
</DialogTrigger>
|
||||
@@ -68,86 +152,130 @@ export default function RechargeModal(props: RechargeModelProps) {
|
||||
充值中心
|
||||
</DialogTitle>
|
||||
|
||||
<Form form={form} onSubmit={onSubmit} className={`flex flex-col gap-8`}>
|
||||
{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`}>
|
||||
{/* 充值额度 */}
|
||||
<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`}
|
||||
/>
|
||||
<FormOption
|
||||
id={`${id}-50`} value={`50`} label={`50元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
<FormOption
|
||||
id={`${id}-100`} value={`100`} label={`100元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
</div>
|
||||
<div className={`flex items-center gap-2`}>
|
||||
<FormOption
|
||||
id={`${id}-20`} value={`20`} label={`20元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
<FormOption
|
||||
id={`${id}-50`} value={`50`} label={`50元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
<FormOption
|
||||
id={`${id}-100`} value={`100`} label={`100元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center gap-2`}>
|
||||
<FormOption
|
||||
id={`${id}-200`} value={`200`} label={`200元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
<FormOption
|
||||
id={`${id}-500`} value={`500`} label={`500元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
<FormOption
|
||||
id={`${id}-1000`} value={`1000`} label={`1000元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</FormField>
|
||||
<div className={`flex items-center gap-2`}>
|
||||
<FormOption
|
||||
id={`${id}-200`} value={`200`} label={`200元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
<FormOption
|
||||
id={`${id}-500`} value={`500`} label={`500元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
<FormOption
|
||||
id={`${id}-1000`} value={`1000`} label={`1000元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
</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`}>
|
||||
<FormOption
|
||||
id={`${id}-alipay`} value={`alipay`}
|
||||
compare={field.value}
|
||||
className={`flex-1 flex-row justify-center items-center`}>
|
||||
<Image src={alipay} alt={`支付宝 logo`} className={`w-6 h-6`}/>
|
||||
<span>支付宝</span>
|
||||
</FormOption>
|
||||
<FormOption
|
||||
id={`${id}-wechat`} value={`wechat`}
|
||||
compare={field.value}
|
||||
className={`flex-1 flex-row justify-center items-center`}>
|
||||
<Image src={wechat} alt={`微信 logo`} className={`w-6 h-6`}/>
|
||||
<span>微信</span>
|
||||
</FormOption>
|
||||
</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`}>
|
||||
<FormOption
|
||||
id={`${id}-alipay`} value={`alipay`}
|
||||
compare={field.value}
|
||||
className={`flex-1 flex-row justify-center items-center`}>
|
||||
<Image src={alipay} alt={`支付宝 logo`} className={`w-6 h-6`}/>
|
||||
<span>支付宝</span>
|
||||
</FormOption>
|
||||
<FormOption
|
||||
id={`${id}-wechat`} value={`wechat`}
|
||||
compare={field.value}
|
||||
className={`flex-1 flex-row justify-center items-center`}>
|
||||
<Image src={wechat} alt={`微信 logo`} className={`w-6 h-6`}/>
|
||||
<span>微信</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 w-52 h-52 flex items-center justify-center">
|
||||
{payInfo ?
|
||||
method === 'alipay'
|
||||
? <iframe
|
||||
src={payInfo.pay_url}
|
||||
className="w-full h-full"
|
||||
title="支付二维码"
|
||||
/>
|
||||
: <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 h-12 text-lg`}>立即支付</Button>
|
||||
<Button
|
||||
className={`px-8 text-lg`}
|
||||
onClick={confirmRecharge}
|
||||
>
|
||||
已完成支付
|
||||
</Button>
|
||||
<Button
|
||||
theme={`outline`}
|
||||
className={`px-8 text-lg`}
|
||||
onClick={closeDialog}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</>}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user