完善充值功能,添加支付宝和微信的充值逻辑及二维码展示

This commit is contained in:
2025-04-18 16:22:50 +08:00
parent c9e65b6617
commit 687c48c1b8
7 changed files with 260 additions and 94 deletions

View File

@@ -1,3 +1,4 @@
'use server'
import {callByUser, callPublic} from '@/actions/base' import {callByUser, callPublic} from '@/actions/base'
export async function Identify(props: { export async function Identify(props: {

View File

@@ -1,3 +1,4 @@
'use server'
import {Bill} from '@/lib/models' import {Bill} from '@/lib/models'
import {callByUser} from '@/actions/base' import {callByUser} from '@/actions/base'
import {PageRecord} from '@/lib/api' import {PageRecord} from '@/lib/api'

View File

@@ -1,3 +1,4 @@
'use server'
import {callByUser} from '@/actions/base' import {callByUser} from '@/actions/base'
async function createChannels(params: { async function createChannels(params: {

View File

@@ -1,3 +0,0 @@
export async function tradeCallbackByAlipay() {
}

33
src/actions/user.ts Normal file
View 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)
}

View File

@@ -5,7 +5,7 @@ import alipay from '../_assets/alipay.svg'
import wechat from '../_assets/wechat.svg' import wechat from '../_assets/wechat.svg'
import balance from '../_assets/balance.svg' import balance from '../_assets/balance.svg'
import Image from 'next/image' import Image from 'next/image'
import {useContext, useState} from 'react' import {useContext, useRef, useState} from 'react'
import {AuthContext} from '@/components/providers/AuthProvider' import {AuthContext} from '@/components/providers/AuthProvider'
import {Alert, AlertDescription} from '@/components/ui/alert' import {Alert, AlertDescription} from '@/components/ui/alert'
import { import {
@@ -21,6 +21,7 @@ import {ApiResponse} from '@/lib/api'
import {toast} from 'sonner' import {toast} from 'sonner'
import {Loader} from 'lucide-react' import {Loader} from 'lucide-react'
import {useRouter} from 'next/navigation' import {useRouter} from 'next/navigation'
import * as qrcode from 'qrcode'
export type PayProps = { export type PayProps = {
method: 'alipay' | 'wechat' | 'balance' method: 'alipay' | 'wechat' | 'balance'
@@ -34,6 +35,7 @@ export default function Pay(props: PayProps) {
const ctx = useContext(AuthContext) const ctx = useContext(AuthContext)
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [payInfo, setPayInfo] = useState<CreateResourceResp | undefined>() const [payInfo, setPayInfo] = useState<CreateResourceResp | undefined>()
const canvas = useRef<HTMLCanvasElement>(null)
const onOpen = async () => { const onOpen = async () => {
setOpen(true) setOpen(true)
@@ -58,6 +60,7 @@ export default function Pay(props: PayProps) {
} }
setPayInfo(resp.data) setPayInfo(resp.data)
await qrcode.toCanvas(canvas.current, resp.data.pay_url)
} }
const router = useRouter() const router = useRouter()
@@ -172,13 +175,15 @@ export default function Pay(props: PayProps) {
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="bg-gray-100 w-52 h-52 flex items-center justify-center"> <div className="bg-gray-100 w-52 h-52 flex items-center justify-center">
{payInfo ? ( {payInfo ? (
<iframe props.method === 'alipay'
src={payInfo.pay_url} ? <iframe
className="w-full h-full" src={payInfo.pay_url}
title="支付二维码" 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> </div>
<p className="text-sm text-gray-600 text-center"> <p className="text-sm text-gray-600 text-center">

View File

@@ -15,9 +15,13 @@ import {RadioGroup} from '@/components/ui/radio-group'
import Image from 'next/image' import Image from 'next/image'
import {zodResolver} from '@hookform/resolvers/zod' import {zodResolver} from '@hookform/resolvers/zod'
import {toast} from 'sonner' import {toast} from 'sonner'
import {useRouter} from 'next/navigation'
import wechat from '@/components/composites/purchase/_assets/wechat.svg' import wechat from '@/components/composites/purchase/_assets/wechat.svg'
import alipay from '@/components/composites/purchase/_assets/alipay.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({ const schema = zod.object({
method: zod.enum(['alipay', 'wechat']), method: zod.enum(['alipay', 'wechat']),
@@ -30,6 +34,8 @@ export type RechargeModelProps = {}
export default function RechargeModal(props: RechargeModelProps) { export default function RechargeModal(props: RechargeModelProps) {
const [open, setOpen] = useState(false)
const form = useForm<Schema>({ const form = useForm<Schema>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { 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 { try {
// const resp = await tradeRecharge(data) switch (data.method) {
// if (!resp.success) { case 'alipay':
// throw new Error(resp.message) const aliRes = await RechargeByAlipay({
// } amount: data.amount,
})
// todo 跳转支付页 if (aliRes.success) {
router.push('/pay') 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) { catch (e) {
toast.error(`充值失败`, { 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 ( return (
<Dialog> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button theme={`accent`} type={`button`} className={`px-4 h-8`}></Button> <Button theme={`accent`} type={`button`} className={`px-4 h-8`}></Button>
</DialogTrigger> </DialogTrigger>
@@ -68,86 +152,130 @@ export default function RechargeModal(props: RechargeModelProps) {
</DialogTitle> </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`}> <FormField<Schema> name={`amount`} label={`充值额度`} className={`flex flex-col gap-4`}>
{({id, field}) => ( {({id, field}) => (
<RadioGroup <RadioGroup
id={id} id={id}
defaultValue={String(field.value)} defaultValue={String(field.value)}
onValueChange={v => field.onChange(Number(v))} onValueChange={v => field.onChange(Number(v))}
className={`flex flex-col gap-2`}> className={`flex flex-col gap-2`}>
<div className={`flex items-center gap-2`}> <div className={`flex items-center gap-2`}>
<FormOption <FormOption
id={`${id}-20`} value={`20`} label={`20元`} id={`${id}-20`} value={`20`} label={`20元`}
compare={String(field.value)} compare={String(field.value)}
className={`flex-1`} className={`flex-1`}
/> />
<FormOption <FormOption
id={`${id}-50`} value={`50`} label={`50元`} id={`${id}-50`} value={`50`} label={`50元`}
compare={String(field.value)} compare={String(field.value)}
className={`flex-1`} className={`flex-1`}
/> />
<FormOption <FormOption
id={`${id}-100`} value={`100`} label={`100元`} id={`${id}-100`} value={`100`} label={`100元`}
compare={String(field.value)} compare={String(field.value)}
className={`flex-1`} className={`flex-1`}
/> />
</div> </div>
<div className={`flex items-center gap-2`}> <div className={`flex items-center gap-2`}>
<FormOption <FormOption
id={`${id}-200`} value={`200`} label={`200元`} id={`${id}-200`} value={`200`} label={`200元`}
compare={String(field.value)} compare={String(field.value)}
className={`flex-1`} className={`flex-1`}
/> />
<FormOption <FormOption
id={`${id}-500`} value={`500`} label={`500元`} id={`${id}-500`} value={`500`} label={`500元`}
compare={String(field.value)} compare={String(field.value)}
className={`flex-1`} className={`flex-1`}
/> />
<FormOption <FormOption
id={`${id}-1000`} value={`1000`} label={`1000元`} id={`${id}-1000`} value={`1000`} label={`1000元`}
compare={String(field.value)} compare={String(field.value)}
className={`flex-1`} className={`flex-1`}
/> />
</div> </div>
</RadioGroup> </RadioGroup>
)} )}
</FormField> </FormField>
{/* 支付方式 */} {/* 支付方式 */}
<FormField name={`method`} label={`支付方式`} className={`flex flex-col gap-4`}> <FormField name={`method`} label={`支付方式`} className={`flex flex-col gap-4`}>
{({id, field}) => ( {({id, field}) => (
<RadioGroup <RadioGroup
id={id} id={id}
defaultValue={field.value} defaultValue={field.value}
onValueChange={field.onChange} onValueChange={field.onChange}
className={`flex gap-2`}> className={`flex gap-2`}>
<FormOption <FormOption
id={`${id}-alipay`} value={`alipay`} id={`${id}-alipay`} value={`alipay`}
compare={field.value} compare={field.value}
className={`flex-1 flex-row justify-center items-center`}> className={`flex-1 flex-row justify-center items-center`}>
<Image src={alipay} alt={`支付宝 logo`} className={`w-6 h-6`}/> <Image src={alipay} alt={`支付宝 logo`} className={`w-6 h-6`}/>
<span></span> <span></span>
</FormOption> </FormOption>
<FormOption <FormOption
id={`${id}-wechat`} value={`wechat`} id={`${id}-wechat`} value={`wechat`}
compare={field.value} compare={field.value}
className={`flex-1 flex-row justify-center items-center`}> className={`flex-1 flex-row justify-center items-center`}>
<Image src={wechat} alt={`微信 logo`} className={`w-6 h-6`}/> <Image src={wechat} alt={`微信 logo`} className={`w-6 h-6`}/>
<span></span> <span></span>
</FormOption> </FormOption>
</RadioGroup> </RadioGroup>
)} )}
</FormField> </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`}> <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> </DialogFooter>
</Form> </>}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) )