2026-04-18 14:30:30 +08:00
|
|
|
|
'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'
|
2026-04-20 16:22:49 +08:00
|
|
|
|
import {IdCard} from 'lucide-react'
|
2026-04-18 14:30:30 +08:00
|
|
|
|
|
|
|
|
|
|
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 ? (
|
2026-04-20 16:22:49 +08:00
|
|
|
|
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>
|
|
|
|
|
|
)
|
2026-04-18 14:30:30 +08:00
|
|
|
|
) : (
|
|
|
|
|
|
<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)
|
|
|
|
|
|
}
|