206 lines
7.2 KiB
TypeScript
206 lines
7.2 KiB
TypeScript
'use client'
|
||
|
||
import {use, useEffect, useRef, useState} from 'react'
|
||
import Link from 'next/link'
|
||
import {useFormContext, useWatch} from 'react-hook-form'
|
||
import {Card} from '@/components/ui/card'
|
||
import {buttonVariants} from '@/components/ui/button'
|
||
import {useProfileStore} from '@/components/stores/profile'
|
||
import Pay from '@/components/composites/purchase/pay'
|
||
import {FieldPayment} from './field-payment'
|
||
import {buildPurchaseResource, PurchaseKind, PurchaseSelection} from './resource'
|
||
import {getPrice, getPriceHome} from '@/actions/resource'
|
||
import {ExtraResp} from '@/lib/api'
|
||
import {formatPurchaseLiveLabel} from './sku'
|
||
import {User} from '@/lib/models'
|
||
import {PurchaseFormValues} from './form-values'
|
||
import {IdCard} from 'lucide-react'
|
||
|
||
const emptyPrice: ExtraResp<typeof getPrice> = {
|
||
price: '0.00',
|
||
actual: '0.00',
|
||
discounted: '0.00',
|
||
}
|
||
|
||
export type PurchaseSidePanelProps = {
|
||
kind: PurchaseKind
|
||
}
|
||
|
||
export function PurchaseSidePanel(props: PurchaseSidePanelProps) {
|
||
const {control} = useFormContext<PurchaseFormValues>()
|
||
const method = useWatch<PurchaseFormValues>({control, name: 'pay_type'}) as PurchaseFormValues['pay_type']
|
||
const mode = useWatch<PurchaseFormValues>({control, name: 'type'}) as PurchaseFormValues['type']
|
||
const live = useWatch<PurchaseFormValues>({control, name: 'live'}) as PurchaseFormValues['live']
|
||
const quota = useWatch<PurchaseFormValues>({control, name: 'quota'}) as PurchaseFormValues['quota']
|
||
const expire = useWatch<PurchaseFormValues>({control, name: 'expire'}) as PurchaseFormValues['expire']
|
||
const dailyLimit = useWatch<PurchaseFormValues>({control, name: 'daily_limit'}) as PurchaseFormValues['daily_limit']
|
||
const profile = use(useProfileStore(store => store.profile))
|
||
const selection: PurchaseSelection = {
|
||
kind: props.kind,
|
||
mode,
|
||
live,
|
||
quota,
|
||
expire,
|
||
dailyLimit,
|
||
}
|
||
const priceData = usePurchasePrice(profile, selection)
|
||
const {price, actual: discountedPrice = '0.00'} = priceData
|
||
const totalDiscount = getTotalDiscount(price, discountedPrice)
|
||
const hasDiscount = Number(totalDiscount) > 0
|
||
const liveLabel = formatPurchaseLiveLabel(live, props.kind)
|
||
const resource = buildPurchaseResource(selection)
|
||
|
||
return (
|
||
<Card className="flex-none basis-90 p-6 flex flex-col gap-6 relative">
|
||
<h3>订单详情</h3>
|
||
<ul className="flex flex-col gap-3">
|
||
<li className="flex justify-between items-center">
|
||
<span className="text-sm text-gray-500">套餐名称</span>
|
||
<span className="text-sm">{mode === '2' ? '包量套餐' : '包时套餐'}</span>
|
||
</li>
|
||
<li className="flex justify-between items-center">
|
||
<span className="text-sm text-gray-500">IP 时效</span>
|
||
<span className="text-sm">{liveLabel}</span>
|
||
</li>
|
||
{mode === '2' ? (
|
||
<>
|
||
<li className="flex justify-between items-center">
|
||
<span className="text-sm text-gray-500">购买 IP 量</span>
|
||
<span className="text-sm">{quota} 个</span>
|
||
</li>
|
||
<li className="flex justify-between items-center">
|
||
<span className="text-sm text-gray-500">原价</span>
|
||
<span className="text-sm">¥{price}</span>
|
||
</li>
|
||
{hasDiscount && (
|
||
<li className="flex justify-between items-center">
|
||
<span className="text-sm text-gray-500">总折扣</span>
|
||
<span className="text-sm">-¥{totalDiscount}</span>
|
||
</li>
|
||
)}
|
||
</>
|
||
) : (
|
||
<>
|
||
<li className="flex justify-between items-center">
|
||
<span className="text-sm text-gray-500">套餐时长</span>
|
||
<span className="text-sm">{expire} 天</span>
|
||
</li>
|
||
<li className="flex justify-between items-center">
|
||
<span className="text-sm text-gray-500">每日限额</span>
|
||
<span className="text-sm">{dailyLimit} 个</span>
|
||
</li>
|
||
<li className="flex justify-between items-center">
|
||
<span className="text-sm text-gray-500">原价</span>
|
||
<span className="text-sm">¥{price}</span>
|
||
</li>
|
||
{hasDiscount && (
|
||
<li className="flex justify-between items-center">
|
||
<span className="text-sm text-gray-500">总折扣</span>
|
||
<span className="text-sm">-¥{totalDiscount}</span>
|
||
</li>
|
||
)}
|
||
</>
|
||
)}
|
||
</ul>
|
||
<div className="border-b border-gray-200"></div>
|
||
<p className="flex justify-between items-center">
|
||
<span>实付价格</span>
|
||
<span className="text-xl text-orange-500">¥{discountedPrice}</span>
|
||
</p>
|
||
{profile ? (
|
||
profile.id_type !== 0 ? (
|
||
<>
|
||
<FieldPayment balance={profile.balance}/>
|
||
<Pay
|
||
method={method}
|
||
balance={profile.balance}
|
||
amount={discountedPrice}
|
||
resource={resource}
|
||
/>
|
||
</>
|
||
) : (
|
||
<div className="flex flex-col gap-3">
|
||
<p className="text-sm text-gray-500">
|
||
根据监管要求,您需要完成实名认证后才能支付。
|
||
</p>
|
||
<Link
|
||
href="/admin/identify"
|
||
className={buttonVariants()}
|
||
>
|
||
<IdCard size={16} className="mr-1"/>
|
||
去实名认证
|
||
</Link>
|
||
</div>
|
||
)
|
||
) : (
|
||
<Link href="/login" className={buttonVariants()}>
|
||
登录后支付
|
||
</Link>
|
||
)}
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
function usePurchasePrice(profile: User | null, selection: PurchaseSelection) {
|
||
const [priceData, setPriceData] = useState<ExtraResp<typeof getPrice>>(emptyPrice)
|
||
const requestIdRef = useRef(0)
|
||
const {kind, mode, live, quota, expire, dailyLimit} = selection
|
||
|
||
useEffect(() => {
|
||
const requestId = ++requestIdRef.current
|
||
|
||
const loadPrice = async () => {
|
||
try {
|
||
const resource = buildPurchaseResource({
|
||
kind,
|
||
mode,
|
||
live,
|
||
quota,
|
||
expire,
|
||
dailyLimit,
|
||
})
|
||
const response = profile
|
||
? await getPrice(resource)
|
||
: await getPriceHome(resource)
|
||
|
||
if (requestId !== requestIdRef.current) {
|
||
return
|
||
}
|
||
|
||
if (!response.success) {
|
||
throw new Error(response.message || '获取价格失败')
|
||
}
|
||
|
||
setPriceData({
|
||
price: response.data.price,
|
||
actual: response.data.actual ?? response.data.price ?? '0.00',
|
||
discounted: response.data.discounted ?? '0.00',
|
||
})
|
||
}
|
||
catch (error) {
|
||
if (requestId !== requestIdRef.current) {
|
||
return
|
||
}
|
||
|
||
console.error('获取价格失败:', error)
|
||
setPriceData(emptyPrice)
|
||
}
|
||
}
|
||
|
||
loadPrice()
|
||
}, [dailyLimit, expire, kind, live, mode, profile, quota])
|
||
|
||
return priceData
|
||
}
|
||
|
||
function getTotalDiscount(price: string, discountedPrice: string) {
|
||
const originalPrice = Number.parseFloat(price)
|
||
const actualPrice = Number.parseFloat(discountedPrice)
|
||
|
||
if (Number.isNaN(originalPrice) || Number.isNaN(actualPrice)) {
|
||
return '0.00'
|
||
}
|
||
|
||
return (originalPrice - actualPrice).toFixed(2)
|
||
}
|