Files
web/src/components/composites/purchase/shared/side-panel.tsx

206 lines
7.2 KiB
TypeScript
Raw Normal View History

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'
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 ? (
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)
}