Files
web/src/components/composites/purchase/pay.tsx

243 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger} from '@/components/ui/dialog'
import {Button} from '@/components/ui/button'
import alipay from './_assets/alipay.svg'
import wechat from './_assets/wechat.svg'
import balance from './_assets/balance.svg'
import Image from 'next/image'
import {useEffect, useRef, useState} from 'react'
import {useProfileStore} from '@/components/providers/StoreProvider'
import {Alert, AlertDescription} from '@/components/ui/alert'
import {ApiResponse, ExtraResp, ExtraReq} from '@/lib/api'
import {toast} from 'sonner'
import {Loader} from 'lucide-react'
import {useRouter} from 'next/navigation'
import * as qrcode from 'qrcode'
import {completeResource, createResource, prepareResource} from '@/actions/resource'
export type PayProps = {
method: 'alipay' | 'wechat' | 'balance'
amount: string
resource: ExtraReq<typeof createResource>
}
export default function Pay(props: PayProps) {
const profile = useProfileStore(store => store.profile)
const refreshProfile = useProfileStore(store => store.refreshProfile)
const [open, setOpen] = useState(false)
const [payInfo, setPayInfo] = useState<ExtraResp<typeof prepareResource> | undefined>()
const canvas = useRef<HTMLCanvasElement>(null)
useEffect(() => {
if (canvas.current && payInfo) {
qrcode.toCanvas(canvas.current, payInfo.pay_url, {
width: 200,
margin: 0,
}).then()
}
}, [payInfo])
const onOpen = async () => {
setOpen(true)
if (props.method === 'balance') {
return
}
const method = {
alipay: 1,
wechat: 2,
}[props.method]
const resp = await prepareResource({
...props.resource,
method,
})
if (!resp.success) {
toast.error(`创建订单失败: ${resp.message}`)
setOpen(false)
return
}
setPayInfo(resp.data)
}
const router = useRouter()
const onSubmit = async () => {
let resp: ApiResponse
try {
switch (props.method) {
case 'alipay':
case 'wechat':
if (!payInfo) {
toast.error('无法读取支付信息', {
description: `请联系客服确认支付状态`,
})
return
}
resp = await completeResource({
trade_no: payInfo.trade_no,
})
break
case 'balance':
resp = await createResource(props.resource)
break
}
if (!resp.success) {
throw new Error(resp.message)
}
toast.success('购买成功', {
duration: 10 * 1000,
closeButton: true,
action: {
label: `去提取`,
onClick: () => router.push('/admin/extract'),
},
})
setOpen(false)
await refreshProfile()
}
catch (e) {
console.log(e)
toast.error('购买失败', {
description: (e as Error).message,
})
}
}
const balanceEnough = profile && profile.balance >= Number(props.amount)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="mt-4 h-12" onClick={onOpen}>
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex gap-2 items-center">
{props.method === 'alipay' && (
<>
<Image src={alipay} alt="支付宝" width={20} height={20}/>
<span></span>
</>
)}
{props.method === 'wechat' && (
<>
<Image src={wechat} alt="微信" width={20} height={20}/>
<span></span>
</>
)}
{props.method === 'balance' && (
<>
<Image src={balance} alt="余额" width={20} height={20}/>
<span></span>
</>
)}
</DialogTitle>
</DialogHeader>
{props.method === 'balance' ? (
profile && (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<div className="flex justify-between items-center">
<span className="text-weak text-sm"></span>
<span className="text-lg">
{profile.balance}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-weak text-sm"></span>
<span className="text-lg text-accent">
-
{props.amount}
</span>
</div>
<hr className="my-2"/>
<div className="flex justify-between items-center">
<span className="text-weak text-sm"></span>
<span className={`text-lg ${balanceEnough ? 'text-done' : `text-fail`}`}>
{(profile.balance - Number(props.amount)).toFixed(2)}
</span>
</div>
</div>
{balanceEnough ? (
<Alert>
<AlertDescription>
</AlertDescription>
</Alert>
) : (
<Alert variant="fail">
<AlertDescription>
</AlertDescription>
</Alert>
)}
</div>
)
) : (
<div className="flex flex-col items-center gap-4">
<div className="flex flex-col items-center gap-3">
<div className="bg-gray-100 size-50 flex items-center justify-center">
{payInfo ? (
props.method === 'alipay'
? <iframe src={payInfo.pay_url} className="w-full h-full"/>
: <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">
使
{props.method === 'alipay' ? '支付宝' : '微信'}
</p>
</div>
<div className="text-center space-y-1">
<p className="font-medium">
:
<span className="text-accent">
{props.amount}
</span>
</p>
<p className="text-xs text-gray-500">
:
{payInfo?.trade_no || '创建订单中...'}
</p>
</div>
</div>
)}
<DialogFooter>
<Button
type="button"
disabled={props.method === 'balance' && !!profile && !balanceEnough}
onClick={onSubmit}
>
{props.method === 'balance' ? '确认支付' : '已完成支付'}
</Button>
<Button
type="button"
theme="outline"
onClick={() => setOpen(false)}
>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}