优化完善套餐购买页面

This commit is contained in:
2026-04-18 14:30:30 +08:00
parent 8b65a1745c
commit 6aa108e8d3
17 changed files with 620 additions and 1207 deletions

View File

@@ -1,292 +1,96 @@
'use client'
import {FormField} from '@/components/ui/form'
import {RadioGroup} from '@/components/ui/radio-group'
import {Input} from '@/components/ui/input'
import {Button} from '@/components/ui/button'
import {Minus, Plus} from 'lucide-react'
import FormOption from '@/components/composites/purchase/option'
import Image from 'next/image'
import check from '../_assets/check.svg'
import {Schema} from '@/components/composites/purchase/long/form'
import {useFormContext, useWatch} from 'react-hook-form'
import {useWatch} from 'react-hook-form'
import {Card} from '@/components/ui/card'
import {BillingMethodField} from '../shared/billing-method-field'
import {FeatureList} from '../shared/feature-list'
import {NumberStepperField} from '../shared/number-stepper-field'
import {getPurchaseSkuPrice} from '../shared/sku'
export default function Center({map, expireList, liveList}: {
map: Map<string, string>
export default function Center({priceMap, expireList, liveList}: {
priceMap: Map<string, string>
liveList: string[]
expireList: string[]
}) {
const form = useFormContext<Schema>()
const type = useWatch({name: 'type'})
const type = useWatch<Schema>({name: 'type'}) as Schema['type']
const expire = useWatch<Schema>({name: 'expire'}) as Schema['expire']
return (
<Card className="flex-auto p-6 flex flex-col gap-6 relative">
<BillingMethodField expireList={expireList} timeDailyLimit={100}/>
{/* 计费方式 */}
<BillingMethod expireList={expireList}/>
{/* IP 时效 */}
<IpTime {...{map, liveList}}/>
{/* 根据套餐类型显示不同表单项 */}
{type === '2' ? (
/* 包量IP 购买数量 */
<FormField
className="space-y-4"
name="quota"
label="IP 购买数量">
{({id, field}) => {
const value = Number(field.value) || 500
const minValue = 500
const step = 100
return (
<div className="flex gap-2 items-center">
<Button
theme="outline"
type="button"
className="h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg"
onClick={() => form.setValue('quota', Math.max(minValue, value - step))}
disabled={value === minValue}>
<Minus/>
</Button>
<Input
{...field}
id={id}
type="number"
className="w-40 h-10 border border-gray-200 rounded-sm text-center"
min={minValue}
step={step}
onBlur={(e) => {
const value = Number(e.target.value)
if (value < 500) {
form.setValue('quota', 500)
}
}}
/>
<Button
theme="outline"
type="button"
className="h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg"
onClick={() => form.setValue('quota', value + step)}>
<Plus/>
</Button>
</div>
)
}}
</FormField>
) : (
<>
{/* 包时:套餐时效 */}
<ComboValidity expireList={expireList}/>
{/* 包时:每日提取上限 */}
<FormField
className="space-y-4"
name="daily_limit"
label="每日提取上限">
{({id, field}) => {
const value = Number(field.value) || 100
const minValue = 100
const step = 100
<FormField<Schema, 'live'>
className="space-y-4"
name="live"
label="IP 时效">
{({id, field}) => (
<RadioGroup
id={id}
value={field.value}
onValueChange={field.onChange}
className="grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-4">
{liveList.map((live) => {
const price = getPurchaseSkuPrice(priceMap, {
mode: type,
live,
expire: String(expire),
})
return (
<div className="flex gap-2 items-center">
<Button
theme="outline"
type="button"
className={`h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg ${
value === minValue ? 'opacity-50 cursor-not-allowed' : ''
}`}
onClick={() => form.setValue('daily_limit', Math.max(minValue, value - step))}
disabled={value === minValue}>
<Minus/>
</Button>
<Input
{...field}
id={id}
type="number"
className="w-40 h-10 border border-gray-200 rounded-sm text-center"
min={100}
step={100}
onBlur={(e) => {
const value = Number(e.target.value)
if (value < 100) {
form.setValue('daily_limit', 100)
}
}}
/>
<Button
theme="outline"
type="button"
className="h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg"
onClick={() => form.setValue('daily_limit', value + step)}>
<Plus/>
</Button>
</div>
<FormOption
key={live}
id={`${id}-${live}`}
value={live}
label={`${Number(live) / 60} 小时`}
description={price && `${price}/IP`}
compare={field.value}
/>
)
}}
</FormField>
</>
})}
</RadioGroup>
)}
</FormField>
{/* 套餐时效 */}
{type === '1' && (
<FormField className="space-y-4" name="expire" label="套餐时效">
{({id, field}) => (
<RadioGroup id={id} value={field.value} onValueChange={field.onChange} className="flex gap-4 flex-wrap">
{expireList.map(day => (
<FormOption
key={day}
id={`${id}-${day}`}
value={day}
label={`${day}`}
compare={field.value}
/>
))}
</RadioGroup>
)}
</FormField>
)}
{/* 产品特性 */}
<div className="space-y-6">
<h3></h3>
<div className="grid grid-cols-2 md:grid-cols-3 auto-rows-fr gap-4 gap-y-6">
<p className="flex gap-2 items-center">
<Image src={check} alt="check" aria-hidden className="w-4 h-4"/>
<span className="text-sm text-gray-500"></span>
</p>
<p className="flex gap-2 items-center">
<Image src={check} alt="check" aria-hidden className="w-4 h-4"/>
<span className="text-sm text-gray-500"></span>
</p>
<p className="flex gap-2 items-center">
<Image src={check} alt="check" aria-hidden className="w-4 h-4"/>
<span className="text-sm text-gray-500"></span>
</p>
<p className="flex gap-2 items-center">
<Image src={check} alt="check" aria-hidden className="w-4 h-4"/>
<span className="text-sm text-gray-500">API接口</span>
</p>
<p className="flex gap-2 items-center">
<Image src={check} alt="check" aria-hidden className="w-4 h-4"/>
<span className="text-sm text-gray-500">IP时效3-30()</span>
</p>
<p className="flex gap-2 items-center">
<Image src={check} alt="check" aria-hidden className="w-4 h-4"/>
<span className="text-sm text-gray-500">IP资源定期筛选</span>
</p>
<p className="flex gap-2 items-center">
<Image src={check} alt="check" aria-hidden className="w-4 h-4"/>
<span className="text-sm text-gray-500">API接口</span>
</p>
<p className="flex gap-2 items-center">
<Image src={check} alt="check" aria-hidden className="w-4 h-4"/>
<span className="text-sm text-gray-500">/</span>
</p>
<p className="flex gap-2 items-center">
<Image src={check} alt="check" aria-hidden className="w-4 h-4"/>
<span className="text-sm text-gray-500">500</span>
</p>
</div>
</div>
{/* 每日提取上限/购买数量 */}
{type === '1' ? (
<NumberStepperField
name="daily_limit"
label="每日提取上限"
min={100}
step={100}
/>
) : (
<NumberStepperField
name="quota"
label="IP 购买数量"
min={500}
step={100}
/>
)}
<FeatureList/>
</Card>
)
}
function BillingMethod(props: {
expireList: string[]
}) {
const {setValue} = useFormContext<Schema>()
return (
<FormField
className="flex flex-col gap-4"
name="type"
label="计费方式">
{({id, field}) => (
<RadioGroup
id={id}
value={field.value}
onValueChange={(v) => {
field.onChange(v)
if (v === '2') {
setValue('expire', '0')
}
else if (props.expireList.length > 0) {
setValue('expire', props.expireList[0])
}
}}
className="flex gap-4 max-md:flex-col">
<FormOption
id={`${id}-2`}
value="2"
label="包量套餐"
description="适用于短期或不定期高提取业务场景"
compare={field.value}/>
<FormOption
id={`${id}-1`}
value="1"
label="包时套餐"
description="适用于每日提取量稳定的业务场景"
compare={field.value}/>
</RadioGroup>
)}
</FormField>
)
}
function IpTime({map, liveList}: {
map: Map<string, string>
liveList: string[]}) {
const {control, getValues} = useFormContext<Schema>()
const values = useWatch({control})
return (
<FormField
className="space-y-4"
name="live"
label="IP 时效">
{({id, field}) => (
<RadioGroup id={id} value={field.value} onValueChange={field.onChange} className="grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-4">
{liveList.map((live) => {
const params = new URLSearchParams()
params.set('mode', {
1: 'time',
2: 'quota',
}[values.type || '2'])
params.set('live', live || '0')
params.set('expire', values.expire || '0')
const price = map.get(params.toString())
return (
<FormOption
key={live}
id={live}
value={live}
label={`${Number(live) / 60} 小时`}
description={price && `${price}/IP`}
compare={field.value}
/>
)
})}
</RadioGroup>
)}
</FormField>
)
}
function ComboValidity({expireList}: {expireList: string[]}) {
return (
<FormField
className="space-y-4"
name="expire"
label="套餐时效"
>
{({id, field}) => (
<RadioGroup
id={id}
value={field.value}
onValueChange={(val) => {
field.onChange(val)
}}
className="flex gap-4 flex-wrap"
>
{expireList.map(item => (
<FormOption
key={item}
id={`${id}-${item}`}
value={item}
label={`${item}`}
compare={field.value}
/>
))}
</RadioGroup>
)}
</FormField>
)
}