完善充值功能,添加支付宝和微信的充值逻辑及二维码展示
This commit is contained in:
@@ -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: {
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
'use server'
|
||||||
import {callByUser} from '@/actions/base'
|
import {callByUser} from '@/actions/base'
|
||||||
|
|
||||||
async function createChannels(params: {
|
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 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">
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user