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

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'
export async function Identify(props: {

View File

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

View File

@@ -1,3 +1,4 @@
'use server'
import {callByUser} from '@/actions/base'
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 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">

View File

@@ -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>
)