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

205 lines
6.2 KiB
TypeScript
Raw Normal View History

'use client'
import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog'
import {Button} from '@/components/ui/button'
import balance from './_assets/balance.svg'
import Image from 'next/image'
import {useState} from 'react'
import {useProfileStore} from '@/components/stores-provider'
import {Alert, AlertTitle} from '@/components/ui/alert'
import {toast} from 'sonner'
import {useRouter} from 'next/navigation'
import {completeResource, createResource, prepareResource} from '@/actions/resource'
import {
usePlatformType,
TradeMethod,
TradeMethodDecoration,
} from '@/lib/models/trade'
import {PaymentModal} from '@/components/composites/payment/payment-modal'
import {PaymentProps} from '@/components/composites/payment/type'
export type PayProps = {
method: 'alipay' | 'wechat' | 'balance'
amount: string
resource: Parameters<typeof createResource>[0]
}
export default function Pay(props: PayProps) {
const profile = useProfileStore(store => store.profile)
const refreshProfile = useProfileStore(store => store.refreshProfile)
const [open, setOpen] = useState(false)
const [trade, setTrade] = useState<PaymentProps | null>(null)
const router = useRouter()
const platform = usePlatformType()
const onOpen = async () => {
setOpen(true)
if (props.method === 'balance') return
const method = props.method === 'alipay'
? TradeMethod.SftAlipay
: TradeMethod.SftWechat
const res = {
...props.resource,
payment_method: method,
payment_platform: platform,
}
const resp = await prepareResource(res)
if (!resp.success) {
toast.error(`创建订单失败: ${resp.message}`)
setOpen(false)
return
}
setTrade({
inner_no: resp.data.trade_no,
pay_url: resp.data.pay_url,
amount: Number(props.amount),
platform: platform,
method: method,
decoration: TradeMethodDecoration[props.method],
})
}
const onSubmit = async () => {
try {
let resp: Awaited<ReturnType<typeof completeResource>> | Awaited<ReturnType<typeof createResource>>
if (props.method === 'balance') {
resp = await createResource(props.resource)
}
else if (trade) {
resp = await completeResource({trade_no: trade.inner_no})
}
else {
throw new Error('支付信息不存在')
}
if (!resp.success) throw new Error(resp.message)
toast.success('购买成功', {
action: {
label: '去提取',
onClick: () => router.push('/admin/extract'),
},
})
setOpen(false)
await refreshProfile()
}
catch (e) {
toast.error('购买失败', {
description: (e as Error).message,
})
}
}
const balanceEnough = profile && profile.balance >= Number(props.amount)
return (
<>
<Button className="mt-4 h-12" onClick={onOpen}>
</Button>
{/* 余额支付对话框 */}
{props.method === 'balance' && (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex gap-2 items-center">
<Image src={balance} alt="余额" width={20} height={20}/>
<span></span>
</DialogTitle>
</DialogHeader>
{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 variant="done">
<AlertTitle>
</AlertTitle>
</Alert>
) : (
<Alert variant="fail">
<AlertTitle>
</AlertTitle>
</Alert>
)}
</div>
)}
<DialogFooter>
<Button
disabled={!balanceEnough}
onClick={onSubmit}
>
</Button>
<Button theme="outline" onClick={() => setOpen(false)}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
{/* 支付宝/微信支付使用公共组件 */}
{props.method !== 'balance' && trade && (
<PaymentModal
{...trade}
2025-06-23 18:59:31 +08:00
onConfirm={async () => {
try {
const resp = await completeResource({trade_no: trade.inner_no})
if (!resp.success) {
throw new Error(resp.message)
}
toast.success('支付成功')
setTrade(null)
setOpen(false)
refreshProfile()
}
catch (e) {
toast.error('支付验证失败', {description: (e as Error).message})
}
}}
onClose={() => {
setTrade(null)
setOpen(false)
}}
/>
)}
</>
)
}