优化完善套餐购买页面
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
'use client'
|
||||
import {useForm} from 'react-hook-form'
|
||||
import Center from '@/components/composites/purchase/long/center'
|
||||
import Right from '@/components/composites/purchase/long/right'
|
||||
import {Form} from '@/components/ui/form'
|
||||
import * as z from 'zod'
|
||||
import {zodResolver} from '@hookform/resolvers/zod'
|
||||
import {ProductItem} from '@/actions/product'
|
||||
import {parsePurchaseSkuList} from '../shared/sku'
|
||||
import {PurchaseSidePanel} from '../shared/side-panel'
|
||||
|
||||
// 定义表单验证架构
|
||||
const schema = z.object({
|
||||
type: z.enum(['1', '2']).default('2'),
|
||||
live: z.string(),
|
||||
@@ -16,27 +16,10 @@ const schema = z.object({
|
||||
daily_limit: z.number().min(100, '每日限额不能少于 100 个'),
|
||||
pay_type: z.enum(['wechat', 'alipay', 'balance']),
|
||||
})
|
||||
|
||||
// 从架构中推断类型
|
||||
export type Schema = z.infer<typeof schema>
|
||||
|
||||
export default function LongForm({skuList}: {skuList: ProductItem['skus']}) {
|
||||
if (!skuList) throw new Error('没有套餐数据')
|
||||
|
||||
const map = new Map<string, string>()
|
||||
// const _modeList = new Set<string>()
|
||||
const _liveList = new Set<number>()
|
||||
const _expireList = new Set<number>()
|
||||
for (const sku of skuList) {
|
||||
const params = new URLSearchParams(sku.code)
|
||||
// _modeList.add(params.get('mode') || '')
|
||||
_liveList.add(Number(params.get('live')))
|
||||
_expireList.add(Number(params.get('expire')))
|
||||
map.set(sku.code, sku.price)
|
||||
}
|
||||
// const modeList = Array.from(_modeList).filter(Boolean)
|
||||
const liveList = Array.from(_liveList).filter(Boolean).map(String)
|
||||
const expireList = Array.from(_expireList).filter(Boolean).map(String)
|
||||
const {priceMap, liveList, expireList} = parsePurchaseSkuList('long', skuList)
|
||||
|
||||
const form = useForm<Schema>({
|
||||
resolver: zodResolver(schema),
|
||||
@@ -52,8 +35,8 @@ export default function LongForm({skuList}: {skuList: ProductItem['skus']}) {
|
||||
|
||||
return (
|
||||
<Form form={form} className="flex flex-col lg:flex-row gap-4">
|
||||
<Center {...{liveList, map, expireList}}/>
|
||||
<Right/>
|
||||
<Center {...{liveList, priceMap, expireList}}/>
|
||||
<PurchaseSidePanel kind="long"/>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
'use client'
|
||||
import {Suspense, use, useEffect, useState} from 'react'
|
||||
import {useProfileStore} from '@/components/stores/profile'
|
||||
import Pay from '@/components/composites/purchase/pay'
|
||||
import {buttonVariants} from '@/components/ui/button'
|
||||
import Link from 'next/link'
|
||||
import {merge} from '@/lib/utils'
|
||||
import {useFormContext, useWatch} from 'react-hook-form'
|
||||
import {Schema} from '@/components/composites/purchase/long/form'
|
||||
import {Card} from '@/components/ui/card'
|
||||
import {getPrice, getPriceHome} from '@/actions/resource'
|
||||
import {ExtraResp} from '@/lib/api'
|
||||
import {FieldPayment} from '../shared/field-payment'
|
||||
|
||||
export default function Right() {
|
||||
const {control} = useFormContext<Schema>()
|
||||
const method = useWatch({control, name: 'pay_type'})
|
||||
const mode = useWatch({control, name: 'type'})
|
||||
const live = useWatch({control, name: 'live'})
|
||||
const quota = useWatch({control, name: 'quota'})
|
||||
const expire = useWatch({control, name: 'expire'})
|
||||
const dailyLimit = useWatch({control, name: 'daily_limit'})
|
||||
const [priceData, setPriceData] = useState<ExtraResp<typeof getPrice>>({
|
||||
price: '0.00',
|
||||
actual: '0.00',
|
||||
discounted: '0.00',
|
||||
})
|
||||
const profile = use(useProfileStore(store => store.profile))
|
||||
|
||||
useEffect(() => {
|
||||
const price = async () => {
|
||||
try {
|
||||
const resp = profile
|
||||
? await getPrice({
|
||||
type: 2,
|
||||
long: {
|
||||
live: Number(live),
|
||||
mode: Number(mode),
|
||||
quota: mode === '1' ? Number(dailyLimit) : Number(quota),
|
||||
expire: mode === '1' ? Number(expire) : undefined,
|
||||
},
|
||||
}) : await getPriceHome({
|
||||
type: 1,
|
||||
short: {
|
||||
live: Number(live),
|
||||
mode: Number(mode),
|
||||
quota: mode === '1' ? Number(dailyLimit) : Number(quota),
|
||||
expire: mode === '1' ? Number(expire) : undefined,
|
||||
},
|
||||
})
|
||||
if (!resp.success) {
|
||||
throw new Error('获取价格失败')
|
||||
}
|
||||
setPriceData({
|
||||
price: resp.data.price,
|
||||
actual: resp.data.actual ?? resp.data.price ?? '',
|
||||
discounted: resp.data.discounted,
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取价格失败:', error)
|
||||
setPriceData({
|
||||
price: '0.00',
|
||||
actual: '0.00',
|
||||
discounted: '0.00',
|
||||
})
|
||||
}
|
||||
}
|
||||
price()
|
||||
}, [dailyLimit, expire, live, quota, mode, profile])
|
||||
|
||||
const {price, actual: discountedPrice = ''} = priceData
|
||||
// 计算总折扣价(原价 - 实付价格)
|
||||
const calculateTotalDiscount = () => {
|
||||
const originalPrice = parseFloat(price)
|
||||
const actualPrice = parseFloat(discountedPrice)
|
||||
if (isNaN(originalPrice) || isNaN(actualPrice)) {
|
||||
return '0.00'
|
||||
}
|
||||
const discount = originalPrice - actualPrice
|
||||
return discount.toFixed(2)
|
||||
}
|
||||
|
||||
const totalDiscount = calculateTotalDiscount()
|
||||
const hasDiscount = parseFloat(totalDiscount) > 0
|
||||
return (
|
||||
<Card className={merge(
|
||||
`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">
|
||||
{live}
|
||||
{' '}
|
||||
分钟
|
||||
</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>
|
||||
<Suspense>
|
||||
<BalanceOrLogin {...{method, discountedPrice, mode, live, quota, expire, dailyLimit}}/>
|
||||
</Suspense>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function BalanceOrLogin(props: {
|
||||
method: 'wechat' | 'alipay' | 'balance'
|
||||
discountedPrice: string
|
||||
mode: string
|
||||
live: string
|
||||
quota: number
|
||||
expire: string
|
||||
dailyLimit: number
|
||||
}) {
|
||||
const profile = use(useProfileStore(store => store.profile))
|
||||
return profile ? (
|
||||
<>
|
||||
<FieldPayment/>
|
||||
<Pay
|
||||
method={props.method}
|
||||
balance={profile.balance}
|
||||
amount={props.discountedPrice}
|
||||
resource={{
|
||||
type: 2,
|
||||
long: {
|
||||
mode: Number(props.mode),
|
||||
live: Number(props.live),
|
||||
expire: props.mode === '1' ? Number(props.expire) : undefined,
|
||||
quota: props.mode === '1' ? Number(props.dailyLimit) : Number(props.quota),
|
||||
},
|
||||
}}/>
|
||||
</>
|
||||
) : (
|
||||
<Link href="/login" className={buttonVariants()}>
|
||||
登录后支付
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user