完善账单页面,抽取公共页面组件
This commit is contained in:
10
src/components/composites/purchase/_assets/alipay.svg
Normal file
10
src/components/composites/purchase/_assets/alipay.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_98_223)">
|
||||
<path d="M20.001 13.692V3.845C20.0005 2.82523 19.5951 1.8474 18.8739 1.12641C18.1527 0.405415 17.1748 0.000264944 16.155 0L3.845 0C2.82541 0.000529935 1.84772 0.405797 1.12676 1.12676C0.405797 1.84772 0.000529935 2.82541 0 3.845V16.155C0.000265092 17.1747 0.405447 18.1525 1.12647 18.8735C1.84749 19.5946 2.82532 19.9997 3.845 20H16.155C17.057 19.9994 17.9301 19.682 18.622 19.1034C19.3139 18.5248 19.7808 17.7216 19.941 16.834C18.921 16.392 14.501 14.484 12.198 13.384C10.446 15.507 8.61 16.781 5.844 16.781C3.078 16.781 1.231 15.077 1.453 12.991C1.599 11.623 2.538 9.386 6.615 9.769C8.765 9.971 9.748 10.372 11.501 10.951C11.954 10.119 12.331 9.204 12.617 8.231H4.845V7.461H8.691V6.077H4V5.23H8.69V3.235C8.69 3.235 8.732 2.923 9.077 2.923H11V5.23H16V6.078H11V7.46H15.079C14.7272 8.91647 14.1692 10.3152 13.422 11.614C14.607 12.044 20 13.692 20 13.692H20.001ZM5.538 15.461C2.615 15.461 2.153 13.616 2.308 12.845C2.461 12.077 3.308 11.075 4.933 11.075C6.8 11.075 8.473 11.553 10.481 12.531C9.071 14.367 7.338 15.461 5.538 15.461Z" fill="#00A5F1"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_98_223">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
4
src/components/composites/purchase/_assets/balance.svg
Normal file
4
src/components/composites/purchase/_assets/balance.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.99996 0C4.4771 0 0 4.47714 0 10C0 15.5229 4.47712 20 9.99996 20C15.5228 20 19.9999 15.5228 19.9999 10C19.9999 4.47716 15.5228 0 9.99996 0ZM7.05159 4.29727C7.78297 3.68016 9.40572 4.98294 9.97711 4.93723C10.5485 4.98294 12.1713 3.68016 12.9026 4.29727C13.634 4.91437 12.017 6.325 12.017 6.325H7.93688C7.93688 6.325 6.32021 4.91437 7.05159 4.29727ZM12.1713 7.05639C12.1713 7.25852 12.0077 7.42208 11.8056 7.42208H8.14866C7.9465 7.42208 7.78297 7.25852 7.78297 7.05639C7.78297 6.85425 7.9465 6.6907 8.14866 6.6907H11.8056C12.0077 6.6907 12.1713 6.85425 12.1713 7.05639ZM9.97711 15.8251C7.14941 15.8251 4.85744 16.0801 4.85744 13.6881C4.85744 11.9096 6.12522 9.10908 7.93688 7.78775H12.017C13.829 9.10911 15.0968 11.9096 15.0968 13.6881C15.0968 16.0801 12.8048 15.8251 9.97711 15.8251Z" fill="#FF6B00"/>
|
||||
<path d="M10.0331 10.4641C10.1948 10.4641 10.321 10.5147 10.4125 10.6152C10.504 10.7158 10.5501 10.8605 10.5501 11.0487H11.5651C11.5651 10.7314 11.457 10.4759 11.2417 10.2807C11.0263 10.0854 10.7292 9.96605 10.3514 9.92196V9.375H9.82754V9.91174C9.43225 9.94399 9.11936 10.0511 8.88808 10.2334C8.65681 10.4157 8.54116 10.6486 8.54116 10.9315C8.54116 11.2434 8.6553 11.4833 8.88429 11.6511C9.11333 11.8189 9.46855 11.969 9.94998 12.1013C10.1714 12.1846 10.3249 12.268 10.4103 12.3519C10.4956 12.4358 10.5387 12.5498 10.5387 12.6939C10.5387 12.8349 10.4972 12.9473 10.4126 13.0322C10.3279 13.1167 10.1971 13.1592 10.0188 13.1592C9.82683 13.1592 9.6726 13.1113 9.55846 13.0167C9.44432 12.922 9.38689 12.7634 9.38689 12.5412H8.39678L8.38921 12.5541C8.37939 12.915 8.50335 13.1888 8.7611 13.3755C9.01882 13.5621 9.35064 13.6712 9.75495 13.7035V14.2043H10.2825V13.7009C10.6778 13.6686 10.9892 13.5653 11.2167 13.3894C11.4442 13.2136 11.5583 12.9801 11.5583 12.6887C11.5583 12.3783 11.4411 12.1352 11.2076 11.9588C10.974 11.7824 10.6234 11.6334 10.1563 11.5113C9.92274 11.4183 9.76476 11.3317 9.68312 11.2521C9.60152 11.1725 9.56073 11.0665 9.55995 10.9348C9.55995 10.7922 9.597 10.6781 9.6703 10.5927C9.74364 10.5072 9.86457 10.4641 10.0331 10.4641Z" fill="#FF6B00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
BIN
src/components/composites/purchase/_assets/banner.webp
Normal file
BIN
src/components/composites/purchase/_assets/banner.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
10
src/components/composites/purchase/_assets/check.svg
Normal file
10
src/components/composites/purchase/_assets/check.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_98_189)">
|
||||
<path d="M3.46154 0H14.5385C15.4565 0 16.337 0.364697 16.9861 1.01386C17.6353 1.66303 18 2.54348 18 3.46154V14.5385C18 15.4565 17.6353 16.337 16.9861 16.9861C16.337 17.6353 15.4565 18 14.5385 18H3.46154C2.54348 18 1.66303 17.6353 1.01386 16.9861C0.364697 16.337 0 15.4565 0 14.5385L0 3.46154C0 2.54348 0.364697 1.66303 1.01386 1.01386C1.66303 0.364697 2.54348 0 3.46154 0ZM3.46154 1.38462C2.9107 1.38462 2.38243 1.60343 1.99293 1.99293C1.60343 2.38243 1.38462 2.9107 1.38462 3.46154V14.5385C1.38462 14.8112 1.43834 15.0813 1.54271 15.3333C1.64709 15.5852 1.80007 15.8142 1.99293 16.0071C2.18579 16.1999 2.41475 16.3529 2.66673 16.4573C2.91872 16.5617 3.18879 16.6154 3.46154 16.6154H14.5385C14.8112 16.6154 15.0813 16.5617 15.3333 16.4573C15.5852 16.3529 15.8142 16.1999 16.0071 16.0071C16.1999 15.8142 16.3529 15.5852 16.4573 15.3333C16.5617 15.0813 16.6154 14.8112 16.6154 14.5385V3.46154C16.6154 3.18879 16.5617 2.91872 16.4573 2.66673C16.3529 2.41475 16.1999 2.18579 16.0071 1.99293C15.8142 1.80007 15.5852 1.64709 15.3333 1.54271C15.0813 1.43834 14.8112 1.38462 14.5385 1.38462H3.46154ZM13.743 5.92338C13.8041 5.98151 13.8531 6.0511 13.8873 6.12818C13.9214 6.20526 13.9401 6.28831 13.9421 6.3726C13.9442 6.45689 13.9296 6.54076 13.8993 6.61941C13.8689 6.69806 13.8233 6.76996 13.7652 6.831L8.50085 12.3348C8.49877 12.3376 8.49392 12.339 8.48977 12.3432L8.48285 12.3528C8.44477 12.3902 8.39977 12.4096 8.35615 12.4359C8.32846 12.4512 8.307 12.474 8.27862 12.4858C8.20213 12.5178 8.12007 12.5345 8.03714 12.5347C7.95421 12.5349 7.87205 12.5188 7.79538 12.4872C7.77323 12.4782 7.75523 12.4588 7.73308 12.447C7.68462 12.4214 7.63477 12.3972 7.59185 12.357C7.58838 12.3542 7.587 12.3494 7.58285 12.3452C7.57938 12.3432 7.57592 12.3418 7.57315 12.339L4.92162 9.58638C4.86293 9.5258 4.81679 9.45421 4.78585 9.37574C4.75491 9.29727 4.73978 9.21346 4.74132 9.12912C4.74287 9.04479 4.76106 8.96159 4.79485 8.8843C4.82864 8.80702 4.87737 8.73717 4.93823 8.67877C4.99877 8.62 5.07035 8.57379 5.14882 8.5428C5.2273 8.5118 5.31113 8.49664 5.3955 8.49819C5.47986 8.49973 5.56308 8.51795 5.64037 8.5518C5.71766 8.58564 5.78749 8.63444 5.84585 8.69538L8.03354 10.9648L12.834 5.94415C12.8923 5.88305 12.9621 5.83404 13.0393 5.79992C13.1166 5.7658 13.1998 5.74724 13.2842 5.74532C13.3686 5.74339 13.4526 5.75812 13.5313 5.78868C13.6101 5.81923 13.682 5.86501 13.743 5.92338Z" fill="#2470F9"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_98_189">
|
||||
<rect width="18" height="18" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
16
src/components/composites/purchase/_assets/wechat.svg
Normal file
16
src/components/composites/purchase/_assets/wechat.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_98_228)">
|
||||
<path d="M3.6 0H16.4C18.8 0 20 1.2 20 3.6V16.4C20 18.8 18.8 20 16.4 20H3.6C1.2 20 0 18.8 0 16.4V3.6C0 1.2 1.2 0 3.6 0Z" fill="#28D846"/>
|
||||
<path d="M12.9534 7.94922C10.4386 7.94922 8.39999 9.65422 8.39999 11.7574C8.39999 13.8606 10.4386 15.5656 12.9534 15.5656C13.5434 15.5658 14.1294 15.4683 14.6876 15.2772L15.9912 15.9716L15.8158 14.7192C16.3212 14.4045 16.7403 13.969 17.0355 13.452C17.3307 12.9349 17.4927 12.3526 17.5068 11.7574C17.5066 9.65422 15.468 7.94922 12.9534 7.94922Z" fill="white"/>
|
||||
<path d="M7.89999 3.80078C10.576 3.80078 12.7884 5.44078 13.2938 7.61478C12.3402 7.53638 7.39379 8.06938 8.17939 12.9148C7.46839 12.9168 6.49939 12.8856 5.80539 12.6518L4.23059 13.4912L4.44259 11.9784C3.21699 11.1678 2.39999 9.86418 2.39999 8.40078C2.39999 5.86078 4.86239 3.80078 7.89999 3.80078Z" fill="white"/>
|
||||
<path d="M5.39999 6.88016C5.39999 7.07111 5.47585 7.25425 5.61088 7.38927C5.7459 7.5243 5.92904 7.60016 6.11999 7.60016C6.31095 7.60016 6.49408 7.5243 6.62911 7.38927C6.76414 7.25425 6.83999 7.07111 6.83999 6.88016C6.83999 6.6892 6.76414 6.50607 6.62911 6.37104C6.49408 6.23601 6.31095 6.16016 6.11999 6.16016C5.92904 6.16016 5.7459 6.23601 5.61088 6.37104C5.47585 6.50607 5.39999 6.6892 5.39999 6.88016Z" fill="#28D846"/>
|
||||
<path d="M9 6.88016C9 7.07111 9.07586 7.25425 9.21088 7.38927C9.34591 7.5243 9.52904 7.60016 9.72 7.60016C9.91096 7.60016 10.0941 7.5243 10.2291 7.38927C10.3641 7.25425 10.44 7.07111 10.44 6.88016C10.44 6.6892 10.3641 6.50607 10.2291 6.37104C10.0941 6.23601 9.91096 6.16016 9.72 6.16016C9.52904 6.16016 9.34591 6.23601 9.21088 6.37104C9.07586 6.50607 9 6.6892 9 6.88016Z" fill="#28D846"/>
|
||||
<path d="M10.8 10.64C10.8 10.8097 10.8674 10.9725 10.9874 11.0925C11.1075 11.2126 11.2702 11.28 11.44 11.28C11.6097 11.28 11.7725 11.2126 11.8925 11.0925C12.0126 10.9725 12.08 10.8097 12.08 10.64C12.08 10.4703 12.0126 10.3075 11.8925 10.1875C11.7725 10.0674 11.6097 10 11.44 10C11.2702 10 11.1075 10.0674 10.9874 10.1875C10.8674 10.3075 10.8 10.4703 10.8 10.64Z" fill="#28D846"/>
|
||||
<path d="M13.84 10.64C13.84 10.8097 13.9074 10.9725 14.0274 11.0925C14.1475 11.2126 14.3103 11.28 14.48 11.28C14.6497 11.28 14.8125 11.2126 14.9325 11.0925C15.0526 10.9725 15.12 10.8097 15.12 10.64C15.12 10.4703 15.0526 10.3075 14.9325 10.1875C14.8125 10.0674 14.6497 10 14.48 10C14.3103 10 14.1475 10.0674 14.0274 10.1875C13.9074 10.3075 13.84 10.4703 13.84 10.64Z" fill="#28D846"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_98_228">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
218
src/components/composites/purchase/_client/center.tsx
Normal file
218
src/components/composites/purchase/_client/center.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
'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 {PurchaseFormContext, Schema} from '@/components/composites/purchase/_client/form'
|
||||
import {useContext} from 'react'
|
||||
import FormOption from '@/components/composites/purchase/_client/option'
|
||||
import Image from 'next/image'
|
||||
import check from '@/components/composites/purchase/_assets/check.svg'
|
||||
|
||||
export default function Center() {
|
||||
|
||||
const form = useContext(PurchaseFormContext)?.form
|
||||
if (!form) {
|
||||
throw new Error(`Center component must be used within PurchaseFormContext`)
|
||||
}
|
||||
|
||||
const watchType = form.watch('type')
|
||||
|
||||
return (
|
||||
<div className={`flex-auto p-8 flex flex-col gap-8 relative`}>
|
||||
|
||||
{/* 计费方式 */}
|
||||
<FormField
|
||||
className={`flex flex-col gap-4`}
|
||||
name={`type`}
|
||||
label={`计费方式`}>
|
||||
{({id, field}) => (
|
||||
<RadioGroup
|
||||
id={id}
|
||||
defaultValue={field.value}
|
||||
onValueChange={field.onChange}
|
||||
className={`flex gap-4`}>
|
||||
|
||||
<FormOption
|
||||
id={`${id}-2`}
|
||||
value="2"
|
||||
label="包量套餐"
|
||||
description="适用于短期或不定期高提取业务场景"
|
||||
compare={field.value}/>
|
||||
|
||||
<FormOption
|
||||
id={`${id}-1`}
|
||||
value="1"
|
||||
label="包时套餐"
|
||||
description="适用于每日提取量稳定的业务场景"
|
||||
compare={field.value}/>
|
||||
|
||||
</RadioGroup>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
{/* IP 时效 */}
|
||||
<FormField
|
||||
className={`space-y-4`}
|
||||
name={`live`}
|
||||
label={`IP 时效`}>
|
||||
{({id, field}) => (
|
||||
<RadioGroup
|
||||
id={id}
|
||||
defaultValue={field.value}
|
||||
onValueChange={field.onChange}
|
||||
className={`flex gap-4 flex-wrap`}>
|
||||
|
||||
<FormOption id={`${id}-3`} value="3" label="3 分钟" description="¥0.005/IP" compare={field.value}/>
|
||||
<FormOption id={`${id}-5`} value="5" label="5 分钟" description="¥0.007/IP" compare={field.value}/>
|
||||
<FormOption id={`${id}-10`} value="10" label="10 分钟" description="¥0.010/IP" compare={field.value}/>
|
||||
<FormOption id={`${id}-20`} value="20" label="20 分钟" description="¥0.015/IP" compare={field.value}/>
|
||||
<FormOption id={`${id}-30`} value="30" label="30 分钟" description="¥0.020/IP" compare={field.value}/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
{/* 根据套餐类型显示不同表单项 */}
|
||||
{watchType === '2' ? (
|
||||
/* 包量:IP 购买数量 */
|
||||
<FormField
|
||||
className={`space-y-4`}
|
||||
name={`quota`}
|
||||
label={`IP 购买数量`}>
|
||||
{({id, field}) => (
|
||||
<div className={`flex gap-2 items-center`}>
|
||||
<Button
|
||||
variant={`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(10_000, Number(field.value) - 5_000))}
|
||||
disabled={Number(field.value) === 10_000}>
|
||||
<Minus/>
|
||||
</Button>
|
||||
<Input
|
||||
{...field}
|
||||
id={id}
|
||||
type="number"
|
||||
className={`w-40 h-10 border border-gray-200 rounded-sm text-center`}
|
||||
min={10_000}
|
||||
step={5_000}
|
||||
/>
|
||||
<Button
|
||||
variant={`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', Number(field.value) + 5_000)}>
|
||||
<Plus/>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</FormField>
|
||||
) : (
|
||||
<>
|
||||
{/* 包时:套餐时效 */}
|
||||
<FormField
|
||||
className={`space-y-4`}
|
||||
name={`expire`}
|
||||
label={`套餐时效`}>
|
||||
{({id, field}) => (
|
||||
<RadioGroup
|
||||
id={id}
|
||||
defaultValue={field.value}
|
||||
onValueChange={field.onChange}
|
||||
className={`flex gap-4 flex-wrap`}>
|
||||
|
||||
<FormOption id={`${id}-7`} value="7" label="7天" compare={field.value}/>
|
||||
<FormOption id={`${id}-15`} value="15" label="15天" compare={field.value}/>
|
||||
<FormOption id={`${id}-30`} value="30" label="30天" compare={field.value}/>
|
||||
<FormOption id={`${id}-90`} value="90" label="90天" compare={field.value}/>
|
||||
<FormOption id={`${id}-180`} value="180" label="180天" compare={field.value}/>
|
||||
<FormOption id={`${id}-365`} value="365" label="365天" compare={field.value}/>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
{/* 包时:每日提取上限 */}
|
||||
<FormField
|
||||
className={`space-y-4`}
|
||||
name={`daily_limit`}
|
||||
label={`每日提取上限`}>
|
||||
{({id, field}) => (
|
||||
<div className={`flex gap-2 items-center`}>
|
||||
<Button
|
||||
variant={`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', Math.max(2_000, Number(field.value) - 1_000))}
|
||||
disabled={Number(field.value) === 2_000}>
|
||||
<Minus/>
|
||||
</Button>
|
||||
<Input
|
||||
{...field}
|
||||
id={id}
|
||||
type="number"
|
||||
className={`w-40 h-10 border border-gray-200 rounded-sm text-center`}
|
||||
min={2_000}
|
||||
step={1_000}
|
||||
/>
|
||||
<Button
|
||||
variant={`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', Number(field.value) + 1_000)}>
|
||||
<Plus/>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</FormField>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 产品特性 */}
|
||||
<div className={`space-y-6`}>
|
||||
<h3>产品特性</h3>
|
||||
<div className={`grid grid-cols-3 auto-rows-fr 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>
|
||||
|
||||
{/* 左右的边框 */}
|
||||
<div className={`absolute inset-0 my-8 border-l border-r border-gray-200 pointer-events-none`}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
103
src/components/composites/purchase/_client/form.tsx
Normal file
103
src/components/composites/purchase/_client/form.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
'use client'
|
||||
import {createContext, useContext} from 'react'
|
||||
import {useForm, UseFormReturn} from 'react-hook-form'
|
||||
import Center from '@/components/composites/purchase/_client/center'
|
||||
import Right from '@/components/composites/purchase/_client/right'
|
||||
import Left from '@/components/composites/purchase/_client/left'
|
||||
import {Form} from '@/components/ui/form'
|
||||
import * as z from 'zod'
|
||||
import {zodResolver} from '@hookform/resolvers/zod'
|
||||
import {createResourceByBalance} from '@/actions/resource'
|
||||
import {toast} from 'sonner'
|
||||
import {useRouter} from 'next/navigation'
|
||||
import {AuthContext} from '@/components/providers/AuthProvider'
|
||||
|
||||
// 定义表单验证架构
|
||||
const schema = z.object({
|
||||
type: z.enum(['1', '2']).default('2'),
|
||||
live: z.enum(['3', '5', '10', '20', '30']),
|
||||
quota: z.number().min(10000, '购买数量不能少于10000个'),
|
||||
expire: z.enum(['7', '15', '30', '90', '180', '365']),
|
||||
daily_limit: z.number().min(2000, '每日限额不能少于2000个'),
|
||||
pay_type: z.enum(['wechat', 'alipay', 'balance']),
|
||||
})
|
||||
|
||||
// 从架构中推断类型
|
||||
export type Schema = z.infer<typeof schema>
|
||||
|
||||
type PurchaseFormContextType = {
|
||||
form: UseFormReturn<Schema>
|
||||
}
|
||||
|
||||
export const PurchaseFormContext = createContext<PurchaseFormContextType | undefined>(undefined)
|
||||
|
||||
export type PurchaseFormProps = {}
|
||||
|
||||
export default function PurchaseForm(props: PurchaseFormProps) {
|
||||
console.log('PurchaseForm render')
|
||||
const authCtx = useContext(AuthContext)
|
||||
|
||||
const form = useForm<Schema>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
type: '2', // 默认为包量套餐
|
||||
live: '3', // 分钟
|
||||
quota: 10_000, // >= 10000
|
||||
expire: '30', // 天
|
||||
daily_limit: 2_000, // >= 2000
|
||||
pay_type: 'balance', // 余额支付
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const toExtract = () => {
|
||||
router.push('/admin/extract')
|
||||
}
|
||||
|
||||
const onSubmit = async (value: Schema) => {
|
||||
try {
|
||||
const resp = await createResourceByBalance({
|
||||
type: Number(value.type),
|
||||
live: Number(value.live) * 60,
|
||||
quota: Number(value.quota),
|
||||
expire: Number(value.expire) * 24 * 3600,
|
||||
daily_limit: Number(value.daily_limit),
|
||||
})
|
||||
|
||||
if (!resp.success) {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
|
||||
toast.success('购买成功', {
|
||||
duration: 10 * 1000,
|
||||
closeButton: true,
|
||||
action: {
|
||||
label: `去提取`,
|
||||
onClick: toExtract,
|
||||
},
|
||||
})
|
||||
|
||||
await authCtx.refreshProfile()
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
toast.error('购买失败', {
|
||||
description: (e as Error).message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section role={`tabpanel`} className={`bg-white rounded-lg`}>
|
||||
<Form form={form} onSubmit={onSubmit} className={`flex flex-row`}>
|
||||
<PurchaseFormContext.Provider value={{form}}>
|
||||
<Left/>
|
||||
<Center/>
|
||||
<Right/>
|
||||
</PurchaseFormContext.Provider>
|
||||
</Form>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
124
src/components/composites/purchase/_client/left.tsx
Normal file
124
src/components/composites/purchase/_client/left.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
'use client'
|
||||
import {useState} from 'react'
|
||||
import Image from 'next/image'
|
||||
import banner from '@/components/composites/purchase/_assets/banner.webp'
|
||||
|
||||
export type LeftProps = {
|
||||
}
|
||||
|
||||
export default function Left(props: LeftProps) {
|
||||
return (
|
||||
<div className="flex-none basis-56 p-8 flex flex-col gap-4">
|
||||
<Image src={banner} alt={`banner`} className={`w-full`}/>
|
||||
<h3 className={`text-lg`}>包量套餐</h3>
|
||||
<ul className={`flex flex-col gap-3`}>
|
||||
<Combo name={`3分钟`} level={[
|
||||
{number: 30000, discount: 10},
|
||||
{number: 80000, discount: 20},
|
||||
{number: 200000, discount: 30},
|
||||
{number: 450000, discount: 40},
|
||||
{number: 1000000, discount: 50},
|
||||
{number: 1600000, discount: 65},
|
||||
]}/>
|
||||
<Combo name={`5分钟`} level={[
|
||||
{number: 30000, discount: 10},
|
||||
{number: 80000, discount: 20},
|
||||
{number: 200000, discount: 30},
|
||||
{number: 450000, discount: 40},
|
||||
{number: 1000000, discount: 50},
|
||||
{number: 1600000, discount: 65},
|
||||
]}/>
|
||||
<Combo name={`10分钟`} level={[
|
||||
{number: 30000, discount: 10},
|
||||
{number: 80000, discount: 20},
|
||||
{number: 200000, discount: 30},
|
||||
{number: 450000, discount: 40},
|
||||
{number: 1000000, discount: 50},
|
||||
{number: 1600000, discount: 65},
|
||||
]}/>
|
||||
<Combo name={`15分钟`} level={[
|
||||
{number: 30000, discount: 10},
|
||||
{number: 80000, discount: 20},
|
||||
{number: 200000, discount: 30},
|
||||
{number: 450000, discount: 40},
|
||||
{number: 1000000, discount: 50},
|
||||
{number: 1600000, discount: 65},
|
||||
]}/>
|
||||
<Combo name={`30分钟`} level={[
|
||||
{number: 30000, discount: 10},
|
||||
{number: 80000, discount: 20},
|
||||
{number: 200000, discount: 30},
|
||||
{number: 450000, discount: 40},
|
||||
{number: 1000000, discount: 50},
|
||||
{number: 1600000, discount: 65},
|
||||
]}/>
|
||||
</ul>
|
||||
<div className={`border-b border-gray-200`}></div>
|
||||
<h3 className={`text-lg`}>包时套餐</h3>
|
||||
<ul className={`flex flex-col gap-3`}>
|
||||
<li className={`flex justify-between`}>
|
||||
<span className={`text-sm text-gray-500`}>7天</span>
|
||||
<span className={`text-orange-500 text-xs text-light px-2 py-1 bg-orange-50 rounded-full`}>9折</span>
|
||||
</li>
|
||||
<li className={`flex justify-between`}>
|
||||
<span className={`text-sm text-gray-500`}>30天</span>
|
||||
<span className={`text-orange-500 text-xs text-light px-2 py-1 bg-orange-50 rounded-full`}>8折</span>
|
||||
</li>
|
||||
<li className={`flex justify-between`}>
|
||||
<span className={`text-sm text-gray-500`}>90天</span>
|
||||
<span className={`text-orange-500 text-xs text-light px-2 py-1 bg-orange-50 rounded-full`}>7折</span>
|
||||
</li>
|
||||
<li className={`flex justify-between`}>
|
||||
<span className={`text-sm text-gray-500`}>180天</span>
|
||||
<span className={`text-orange-500 text-xs text-light px-2 py-1 bg-orange-50 rounded-full`}>6折</span>
|
||||
</li>
|
||||
<li className={`flex justify-between`}>
|
||||
<span className={`text-sm text-gray-500`}>360天</span>
|
||||
<span className={`text-orange-500 text-xs text-light px-2 py-1 bg-orange-50 rounded-full`}>5折</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Combo(props: {
|
||||
name: string
|
||||
level?: {
|
||||
number: number
|
||||
discount: number
|
||||
}[]
|
||||
}) {
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<li>
|
||||
<p className={`flex justify-between items-center`}>
|
||||
<span>{props.name}</span>
|
||||
<button
|
||||
className={`text-gray-500 text-sm`}
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
{open ? '收起' : '展开'}
|
||||
</button>
|
||||
</p>
|
||||
{props.level && (
|
||||
<ul className={[
|
||||
`flex flex-col gap-3 overflow-hidden`,
|
||||
`transition-[opacity,padding,max-height] transition-discrete duration-200 ease-in-out`,
|
||||
open
|
||||
? 'delay-[0s, 0s] opacity-100 py-3 max-h-80'
|
||||
: 'delay-[0s, 0.2s] opacity-0 p-0 max-h-0',
|
||||
].join(' ')}>
|
||||
{props.level.map((item, index) => (
|
||||
<li key={index} className={`flex flex-row justify-between items-center`}>
|
||||
<span className={`text-gray-500 text-sm`}>{item.number}</span>
|
||||
<span className={`text-orange-500 text-xs text-light px-2 py-1 bg-orange-50 rounded-full`}>赠送 {item.discount} %</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
34
src/components/composites/purchase/_client/option.tsx
Normal file
34
src/components/composites/purchase/_client/option.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
import {FormLabel} from '@/components/ui/form'
|
||||
import {merge} from '@/lib/utils'
|
||||
import {RadioGroupItem} from '@/components/ui/radio-group'
|
||||
import {ReactNode} from 'react'
|
||||
|
||||
export type FormOptionProps = {
|
||||
id: string
|
||||
value: string
|
||||
label?: string
|
||||
description?: string
|
||||
compare: string
|
||||
className?: string
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export default function FormOption(props: FormOptionProps) {
|
||||
return <>
|
||||
<FormLabel
|
||||
htmlFor={props.id}
|
||||
className={merge(
|
||||
`transition-colors duration-150 ease-in-out`,
|
||||
`px-6 py-4 border rounded-md flex flex-col gap-2 cursor-pointer`,
|
||||
props.compare === props.value ? `bg-primary/10 border-primary` : `border-gray-200`,
|
||||
props.className,
|
||||
)}>
|
||||
{props.children ? props.children : <>
|
||||
<span>{props.label}</span>
|
||||
{props.description && <p className={`text-sm text-gray-500`}>{props.description}</p>}
|
||||
</>}
|
||||
</FormLabel>
|
||||
<RadioGroupItem id={props.id} value={props.value} className={`hidden`}/>
|
||||
</>
|
||||
}
|
||||
155
src/components/composites/purchase/_client/recharge.tsx
Normal file
155
src/components/composites/purchase/_client/recharge.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
'use client'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {Form, FormField} from '@/components/ui/form'
|
||||
import {useForm} from 'react-hook-form'
|
||||
import zod from 'zod'
|
||||
import FormOption from '@/components/composites/purchase/_client/option'
|
||||
import {RadioGroup} from '@/components/ui/radio-group'
|
||||
import Image from 'next/image'
|
||||
import wechat from '@/components/composites/purchase/_assets/wechat.svg'
|
||||
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
|
||||
import {zodResolver} from '@hookform/resolvers/zod'
|
||||
import {tradeRecharge} from '@/actions/trade'
|
||||
import {toast} from 'sonner'
|
||||
import {useRouter} from 'next/navigation'
|
||||
|
||||
const schema = zod.object({
|
||||
method: zod.enum(['alipay', 'wechat']),
|
||||
amount: zod.number().min(1, '充值金额必须大于 0'),
|
||||
})
|
||||
|
||||
type Schema = zod.infer<typeof schema>
|
||||
|
||||
export type RechargeModelProps = {}
|
||||
|
||||
export default function RechargeModal(props: RechargeModelProps) {
|
||||
|
||||
const form = useForm<Schema>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
method: 'alipay',
|
||||
amount: 50,
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
try {
|
||||
const resp = await tradeRecharge(data)
|
||||
if (!resp.success) {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
|
||||
// todo 跳转支付页
|
||||
router.push('/pay')
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(`充值失败`, {
|
||||
description: (e as Error).message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant={`accent`} type={`button`} className={`px-4 h-8`}>去充值</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogTitle className={`flex flex-col gap-2`}>
|
||||
充值中心
|
||||
</DialogTitle>
|
||||
|
||||
<Form form={form} onSubmit={onSubmit} className={`flex flex-col gap-8`}>
|
||||
|
||||
{/* 充值额度 */}
|
||||
<FormField<Schema> name={`amount`} label={`充值额度`} className={`flex flex-col gap-4`}>
|
||||
{({id, field}) => (
|
||||
<RadioGroup
|
||||
id={id}
|
||||
defaultValue={String(field.value)}
|
||||
onValueChange={v => field.onChange(Number(v))}
|
||||
className={`flex flex-col gap-2`}>
|
||||
|
||||
<div className={`flex items-center gap-2`}>
|
||||
<FormOption
|
||||
id={`${id}-20`} value={`20`} label={`20元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
<FormOption
|
||||
id={`${id}-50`} value={`50`} label={`50元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
<FormOption
|
||||
id={`${id}-100`} value={`100`} label={`100元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center gap-2`}>
|
||||
<FormOption
|
||||
id={`${id}-200`} value={`200`} label={`200元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
<FormOption
|
||||
id={`${id}-500`} value={`500`} label={`500元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
<FormOption
|
||||
id={`${id}-1000`} value={`1000`} label={`1000元`}
|
||||
compare={String(field.value)}
|
||||
className={`flex-1`}
|
||||
/>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
{/* 支付方式 */}
|
||||
<FormField name={`method`} label={`支付方式`} className={`flex flex-col gap-4`}>
|
||||
{({id, field}) => (
|
||||
<RadioGroup
|
||||
id={id}
|
||||
defaultValue={field.value}
|
||||
onValueChange={field.onChange}
|
||||
className={`flex gap-2`}>
|
||||
<FormOption
|
||||
id={`${id}-alipay`} value={`alipay`}
|
||||
compare={field.value}
|
||||
className={`flex-1 flex-row justify-center items-center`}>
|
||||
<Image src={alipay} alt={`支付宝 logo`} className={`w-6 h-6`}/>
|
||||
<span>支付宝</span>
|
||||
</FormOption>
|
||||
<FormOption
|
||||
id={`${id}-wechat`} value={`wechat`}
|
||||
compare={field.value}
|
||||
className={`flex-1 flex-row justify-center items-center`}>
|
||||
<Image src={wechat} alt={`微信 logo`} className={`w-6 h-6`}/>
|
||||
<span>微信</span>
|
||||
</FormOption>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
<DialogFooter className={`!flex !flex-row !justify-center`}>
|
||||
<Button className={`px-8 h-12 text-lg`}>立即支付</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
128
src/components/composites/purchase/_client/right.tsx
Normal file
128
src/components/composites/purchase/_client/right.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
'use client'
|
||||
import {useContext} from 'react'
|
||||
import {PurchaseFormContext} from '@/components/composites/purchase/_client/form'
|
||||
import {RadioGroup} from '@/components/ui/radio-group'
|
||||
import {FormField} from '@/components/ui/form'
|
||||
import FormOption from '@/components/composites/purchase/_client/option'
|
||||
import Image from 'next/image'
|
||||
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
|
||||
import wechat from '@/components/composites/purchase/_assets/wechat.svg'
|
||||
import balance from '@/components/composites/purchase/_assets/balance.svg'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {AuthContext} from '@/components/providers/AuthProvider'
|
||||
import RechargeModal from '@/components/composites/purchase/_client/recharge'
|
||||
|
||||
export type RightProps = {}
|
||||
|
||||
export default function Right(props: RightProps) {
|
||||
console.log('Right render')
|
||||
|
||||
const authCtx = useContext(AuthContext)
|
||||
const profile = authCtx.profile
|
||||
|
||||
const form = useContext(PurchaseFormContext)?.form
|
||||
if (!form) {
|
||||
throw new Error(`Center component must be used within PurchaseFormContext`)
|
||||
}
|
||||
|
||||
const watchType = form.watch('type')
|
||||
const watchLive = form.watch('live')
|
||||
const watchQuota = form.watch('quota')
|
||||
const watchExpire = form.watch('expire')
|
||||
const watchDailyLimit = form.watch('daily_limit')
|
||||
|
||||
return (
|
||||
<div className={`flex-none basis-80 p-6 flex flex-col gap-6`}>
|
||||
<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`}>
|
||||
{watchType === '2' ? `包量套餐` : `包时套餐`}
|
||||
</span>
|
||||
</li>
|
||||
<li className={`flex justify-between items-center`}>
|
||||
<span className={`text-sm text-gray-500`}>IP 时效</span>
|
||||
<span className={`text-sm`}>
|
||||
{watchLive}分钟
|
||||
</span>
|
||||
</li>
|
||||
{watchType === '2' ? (
|
||||
<li className={`flex justify-between items-center`}>
|
||||
<span className={`text-sm text-gray-500`}>购买 IP 量</span>
|
||||
<span className={`text-sm`}>
|
||||
{watchQuota}个
|
||||
</span>
|
||||
</li>
|
||||
) : <>
|
||||
<li className={`flex justify-between items-center`}>
|
||||
<span className={`text-sm text-gray-500`}>套餐时长</span>
|
||||
<span className={`text-sm`}>
|
||||
{watchExpire}天
|
||||
</span>
|
||||
</li>
|
||||
<li className={`flex justify-between items-center`}>
|
||||
<span className={`text-sm text-gray-500`}>每日限额</span>
|
||||
<span className={`text-sm`}>
|
||||
{watchDailyLimit}个
|
||||
</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`}>¥--</span>
|
||||
</p>
|
||||
<FormField name={`pay_type`} label={`支付方式`} className={`flex flex-col gap-6`}>
|
||||
{({id, field}) => (
|
||||
<RadioGroup
|
||||
id={id}
|
||||
defaultValue={field.value}
|
||||
onValueChange={field.onChange}
|
||||
className={`flex flex-col gap-3`}>
|
||||
<div className={`w-full p-3 flex flex-col gap-4 bg-gray-100 rounded-md`}>
|
||||
<p className={`flex items-center gap-3`}>
|
||||
<Image src={balance} alt={`余额icon`}/>
|
||||
<span className={`text-sm text-gray-500`}>账户余额</span>
|
||||
</p>
|
||||
<p className={`flex justify-between items-center`}>
|
||||
<span className={`text-xl`}>{profile?.balance}</span>
|
||||
<RechargeModal/>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<FormOption
|
||||
id={`${id}-balance`}
|
||||
value={`balance`}
|
||||
compare={field.value}
|
||||
className={`p-3 w-full flex-row gap-2 justify-center`}>
|
||||
<Image src={balance} alt={`余额 icon`}/>
|
||||
<span>余额</span>
|
||||
</FormOption>
|
||||
<FormOption
|
||||
id={`${id}-wechat`}
|
||||
value={`wechat`}
|
||||
compare={field.value}
|
||||
className={`p-3 w-full flex-row gap-2 justify-center`}>
|
||||
<Image src={wechat} alt={`微信 logo`}/>
|
||||
<span>微信</span>
|
||||
</FormOption>
|
||||
<FormOption
|
||||
id={`${id}-alipay`}
|
||||
value={`alipay`}
|
||||
compare={field.value}
|
||||
className={`p-3 w-full flex-row gap-2 justify-center`}>
|
||||
<Image src={alipay} alt={`支付宝 logo`}/>
|
||||
<span>支付宝</span>
|
||||
</FormOption>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</FormField>
|
||||
<Button className={`mt-4 h-12`} type="submit">
|
||||
立即支付
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
27
src/components/composites/purchase/purchase.tsx
Normal file
27
src/components/composites/purchase/purchase.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import PurchaseForm from '@/components/composites/purchase/_client/form'
|
||||
|
||||
export type PurchaseProps = {}
|
||||
|
||||
export default async function Purchase(props: PurchaseProps) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<ul role={`tablist`} className={`flex justify-center items-stretch bg-white rounded-lg`}>
|
||||
<li role={`tab`}>
|
||||
<button className={`h-14 px-8 text-lg`}>短效动态套餐</button>
|
||||
</li>
|
||||
<li role={`tab`}>
|
||||
<button className={`h-14 px-8 text-lg`}>长效静态套餐</button>
|
||||
</li>
|
||||
<li role={`tab`}>
|
||||
<button className={`h-14 px-8 text-lg`}>固定套餐</button>
|
||||
</li>
|
||||
<li role={`tab`}>
|
||||
<button className={`h-14 px-8 text-lg`}>定制套餐</button>
|
||||
</li>
|
||||
</ul>
|
||||
<PurchaseForm/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user