更新资源列表接口,调整相关数据结构,优化页面布局和样式
This commit is contained in:
39
README.md
39
README.md
@@ -1,39 +0,0 @@
|
||||
## TODO
|
||||
|
||||
- 总览页
|
||||
- 页面图片替换
|
||||
- 网页标题根据实际页面变化
|
||||
- 表格页筛选日期,范围筛选需要联动;检查时间范围选择,限定到一定范围内
|
||||
- 中间件 Limiter
|
||||
- 购买套餐页的冗余组件
|
||||
- 确认各个页面操作列的内容
|
||||
- 首次登录弹窗:需要设置初始密码,展示 实名->购买->提取 的流程介绍
|
||||
- 丰富账单页表格内容与样式
|
||||
- 后台页面:
|
||||
- 提取记录
|
||||
- 使用记录
|
||||
- 登录流程优化,在人机验证前不允许提交登录请求
|
||||
|
||||
- 账单页面,可以继续完成未支付的订单
|
||||
- 需要先验证订单是否已完成支付
|
||||
- 如果未完成支付,才根据保存的支付链接弹出二维码
|
||||
- 封装二维码组件,如果以后需要调整二维码大小可以快速操作
|
||||
- 弃用 form 组件中 onSubmit 参数,统一使用 handler 参数
|
||||
|
||||
### 下阶段
|
||||
|
||||
- markdown 文档渲染
|
||||
- 后台首页改为 grid 布局,需要额外实现用于布局的客户端组件
|
||||
- 检查页面,为后端请求标记 wait 实现防抖机制
|
||||
- 页面切换动效
|
||||
- 使用 pure js 的包代替 canvas,加快编译速度
|
||||
- 验证码读秒用 store 保存到本地,(全局共享读秒时间)?
|
||||
- 将翻页操作反映在路由历史中,可以通过后退返回到上一个翻页状态?
|
||||
|
||||
### 长期
|
||||
|
||||
- 检查扩大服务端组件边界
|
||||
- 检查 Card 替换 section 或 div
|
||||
- 检查页面请求异常处理
|
||||
- 实现完整的客户端 ip 有效性检查
|
||||
- 提取页表单性能优化,树组件性能优化
|
||||
|
||||
@@ -14,7 +14,7 @@ async function listResourcePss(props: {
|
||||
expire_after?: Date
|
||||
expire_before?: Date
|
||||
}) {
|
||||
return await callByUser<PageRecord<Resource>>('/api/resource/list/pss', props)
|
||||
return await callByUser<PageRecord<Resource>>('/api/resource/list/short', props)
|
||||
}
|
||||
|
||||
async function allResource(){
|
||||
|
||||
@@ -15,7 +15,7 @@ import actionBill from './_assets/action-bill.webp'
|
||||
import actionBuy from './_assets/action-buy.webp'
|
||||
import actionLogout from './_assets/action-logout.webp'
|
||||
import Link from 'next/link'
|
||||
import { listAnnouncements } from '@/actions/announcement'
|
||||
import {listAnnouncements} from '@/actions/announcement'
|
||||
import DashboardChart from './_client/chart'
|
||||
|
||||
export type DashboardPageProps = {}
|
||||
@@ -115,7 +115,7 @@ async function Charts() {
|
||||
return (
|
||||
<Card className={`col-start-1 row-start-3 col-span-3 row-span-2`}>
|
||||
<CardContent className={`overflow-hidden`}>
|
||||
<Tabs defaultValue={`dynamic`} className='h-full gap-4'>
|
||||
<Tabs defaultValue={`dynamic`} className="h-full gap-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value={`dynamic`} className={`data-[state=active]:text-primary`}>动态 IP 套餐</TabsTrigger>
|
||||
<TabsTrigger value={`static`} className={`data-[state=active]:text-primary`}>静态 IP 套餐</TabsTrigger>
|
||||
@@ -191,11 +191,11 @@ async function UserCenter() {
|
||||
<Image alt={`bill icon`} src={actionBill} height={48}/>
|
||||
<span className={`text-sm text-weak`}>我的帐单</span>
|
||||
</Link>
|
||||
<Link href='/admin/purchase' className={merge(buttonVariants({variant: `ghost`}), `flex flex-col gap-2 py-2 px-3 h-auto`)}>
|
||||
<Link href="/admin/purchase" className={merge(buttonVariants({variant: `ghost`}), `flex flex-col gap-2 py-2 px-3 h-auto`)}>
|
||||
<Image alt={`buy icon`} src={actionBuy} height={48}/>
|
||||
<span className={`text-sm text-weak`}>购买产品</span>
|
||||
</Link>
|
||||
<Link href='/admin/profile' className={merge(buttonVariants({variant: `ghost`}), `flex flex-col gap-2 py-2 px-3 h-auto`)}>
|
||||
<Link href="/admin/profile" className={merge(buttonVariants({variant: `ghost`}), `flex flex-col gap-2 py-2 px-3 h-auto`)}>
|
||||
<Image alt={`logout icon`} src={actionLogout} height={48}/>
|
||||
<span className={`text-sm text-weak`}>个人中心</span>
|
||||
</Link>
|
||||
@@ -249,4 +249,4 @@ async function Announcements() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {Box, Eraser, Search, Timer} from 'lucide-react'
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import DataTable from '@/components/data-table'
|
||||
import {format, intlFormatDistance, isAfter, isEqual, parse} from 'date-fns'
|
||||
import {format, intlFormatDistance, isAfter} from 'date-fns'
|
||||
import DatePicker from '@/components/date-picker'
|
||||
import {Form, FormField} from '@/components/ui/form'
|
||||
import {useForm} from 'react-hook-form'
|
||||
@@ -133,7 +133,7 @@ export default function ResourcesPage(props: ResourcesPageProps) {
|
||||
)}
|
||||
</FormField>
|
||||
<FormField name={`type`} label={<span className={`text-sm`}>类型</span>}>
|
||||
{({id, field}) => (
|
||||
{({field}) => (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className={`w-24 h-9`}>
|
||||
<SelectValue placeholder={`选择套餐类型`}/>
|
||||
@@ -240,13 +240,13 @@ export default function ResourcesPage(props: ResourcesPageProps) {
|
||||
{
|
||||
accessorKey: 'type', header: `类型`, cell: ({row}) => (
|
||||
<div className={`flex gap-2 items-center`}>
|
||||
{row.original.pss.type === 1 && (
|
||||
{row.original.short.type === 1 && (
|
||||
<div className={`flex gap-2 items-center bg-green-50 w-fit px-2 py-1 rounded-md`}>
|
||||
<Timer size={20}/>
|
||||
<span>包时</span>
|
||||
</div>
|
||||
)}
|
||||
{row.original.pss.type === 2 && (
|
||||
{row.original.short.type === 2 && (
|
||||
<div className={`flex gap-2 items-center bg-blue-50 w-fit px-2 py-1 rounded-md`}>
|
||||
<Box size={20}/>
|
||||
<span>包量</span>
|
||||
@@ -258,30 +258,30 @@ export default function ResourcesPage(props: ResourcesPageProps) {
|
||||
{
|
||||
accessorKey: 'live', header: `IP 时效`, cell: ({row}) => (
|
||||
<span>
|
||||
{row.original.pss.live / 60} 分钟
|
||||
</span>
|
||||
{row.original.short.live / 60} 分钟
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'expire', header: `使用情况`, cell: ({row}) => (
|
||||
<div className={`flex gap-1`}>
|
||||
{row.original.pss.type === 1 ? (
|
||||
{row.original.short.type === 1 ? (
|
||||
<div className={`flex gap-1`}>
|
||||
{isAfter(row.original.pss.expire, new Date())
|
||||
{isAfter(row.original.short.expire, new Date())
|
||||
? <span className={`text-green-500`}>正常</span>
|
||||
: <span className={`text-red-500`}>过期</span>}
|
||||
<span>|</span>
|
||||
<span>今日限额:{row.original.pss.daily_used} / {row.original.pss.daily_limit}</span>
|
||||
<span>今日限额:{row.original.short.daily_used} / {row.original.short.daily_limit}</span>
|
||||
<span>|</span>
|
||||
<span>{intlFormatDistance(row.original.pss.expire, new Date())} 到期</span>
|
||||
<span>{intlFormatDistance(row.original.short.expire, new Date())} 到期</span>
|
||||
</div>
|
||||
) : row.original.pss.type === 2 ? (
|
||||
) : row.original.short.type === 2 ? (
|
||||
<div className={`flex gap-1`}>
|
||||
{row.original.pss.used < row.original.pss.quota
|
||||
{row.original.short.used < row.original.short.quota
|
||||
? <span className={`text-green-500`}>正常</span>
|
||||
: <span className={`text-red-500`}>已用完</span>}
|
||||
<span>|</span>
|
||||
<span>用量统计:{row.original.pss.used} / {row.original.pss.quota}</span>
|
||||
<span>用量统计:{row.original.short.used} / {row.original.short.quota}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span>-</span>
|
||||
@@ -292,9 +292,9 @@ export default function ResourcesPage(props: ResourcesPageProps) {
|
||||
{
|
||||
accessorKey: 'daily_last', header: '最近使用时间', cell: ({row}) => {
|
||||
return (
|
||||
format(row.original.pss.daily_last, "yyyy-MM-dd") === "0001-01-01"
|
||||
format(row.original.short.daily_last, 'yyyy-MM-dd') === '0001-01-01'
|
||||
? '-'
|
||||
: format(row.original.pss.daily_last, 'yyyy-MM-dd HH:mm')
|
||||
: format(row.original.short.daily_last, 'yyyy-MM-dd HH:mm')
|
||||
)
|
||||
},
|
||||
},
|
||||
|
||||
@@ -206,24 +206,24 @@ export default function Extract(props: ExtractProps) {
|
||||
<SelectItem
|
||||
key={`${resource.id}`} value={String(resource.id)} className={`p-3`}>
|
||||
<div className={`flex flex-col gap-2 w-72`}>
|
||||
{resource.type === 1 && resource.pss.type === 1 && (<>
|
||||
{resource.type === 1 && resource.short.type === 1 && (<>
|
||||
<div className={`flex gap-2 items-center bg-green-50 w-fit px-2 py-1 rounded-md text-sm`}>
|
||||
<Timer size={20}/>
|
||||
<span>{name(resource)}</span>
|
||||
</div>
|
||||
<div className={`flex justify-between gap-2 text-xs text-weak`}>
|
||||
<span>到期时间:{format(resource.pss.expire, 'yyyy-MM-dd HH:mm')}</span>
|
||||
<span>{intlFormatDistance(resource.pss.expire, new Date())}</span>
|
||||
<span>到期时间:{format(resource.short.expire, 'yyyy-MM-dd HH:mm')}</span>
|
||||
<span>{intlFormatDistance(resource.short.expire, new Date())}</span>
|
||||
</div>
|
||||
</>)}
|
||||
{resource.type === 1 && resource.pss.type === 2 && (<>
|
||||
{resource.type === 1 && resource.short.type === 2 && (<>
|
||||
<div className={`flex gap-2 items-center bg-blue-50 w-fit px-2 py-1 rounded-md text-sm`}>
|
||||
<Box size={20}/>
|
||||
<span>{name(resource)}</span>
|
||||
</div>
|
||||
<div className={`flex justify-between gap-2 text-xs text-weak`}>
|
||||
<span>提取数量:{resource.pss.used} / {resource.pss.quota}</span>
|
||||
<span>剩余 {resource.pss.quota - resource.pss.used}</span>
|
||||
<span>提取数量:{resource.short.used} / {resource.short.quota}</span>
|
||||
<span>剩余 {resource.short.quota - resource.short.used}</span>
|
||||
</div>
|
||||
</>)}
|
||||
</div>
|
||||
|
||||
@@ -210,9 +210,6 @@ export default function Center() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 左右的边框 */}
|
||||
<div className={`absolute inset-0 my-8 border-l border-r border-gray-200 pointer-events-none`}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
'use client'
|
||||
import {createContext, useContext} from 'react'
|
||||
import {createContext} 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 {StoreContext} from '@/components/providers/StoreProvider'
|
||||
|
||||
// 定义表单验证架构
|
||||
const schema = z.object({
|
||||
@@ -51,7 +46,6 @@ export default function PurchaseForm(props: PurchaseFormProps) {
|
||||
<section role={`tabpanel`} className={`bg-white rounded-lg`}>
|
||||
<Form form={form} className={`flex flex-row`}>
|
||||
<PurchaseFormContext.Provider value={{form}}>
|
||||
<Left/>
|
||||
<Center/>
|
||||
<Right/>
|
||||
</PurchaseFormContext.Provider>
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
|
||||
27
src/components/composites/purchase/_client/nav.tsx
Normal file
27
src/components/composites/purchase/_client/nav.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
import {ReactNode, useState} from 'react'
|
||||
|
||||
export type NavProps = {
|
||||
}
|
||||
|
||||
export default function Nav(props: NavProps) {
|
||||
|
||||
const [type, setType] = useState()
|
||||
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -32,8 +32,8 @@ export type PayProps = {
|
||||
|
||||
export default function Pay(props: PayProps) {
|
||||
|
||||
const profile = useProfileStore(store=>store.profile)
|
||||
const refreshProfile = useProfileStore(store=>store.refreshProfile)
|
||||
const profile = useProfileStore(store => store.profile)
|
||||
const refreshProfile = useProfileStore(store => store.refreshProfile)
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [payInfo, setPayInfo] = useState<CreateResourceResp | undefined>()
|
||||
@@ -185,7 +185,7 @@ export default function Pay(props: PayProps) {
|
||||
<div className="bg-gray-100 size-50 flex items-center justify-center">
|
||||
{payInfo ? (
|
||||
props.method === 'alipay'
|
||||
? <iframe src={payInfo.pay_url} className="w-full h-full" />
|
||||
? <iframe src={payInfo.pay_url} className="w-full h-full"/>
|
||||
: <canvas ref={canvas} className="w-full h-full"/>
|
||||
) : (
|
||||
<Loader size={40} className={`animate-spin text-weak`}/>
|
||||
|
||||
@@ -13,6 +13,7 @@ import RechargeModal from '@/components/composites/recharge'
|
||||
import Pay from '@/components/composites/purchase/_client/pay'
|
||||
import {buttonVariants} from '@/components/ui/button'
|
||||
import Link from 'next/link'
|
||||
import {merge} from '@/lib/utils'
|
||||
|
||||
export type RightProps = {}
|
||||
|
||||
@@ -48,7 +49,10 @@ export default function Right(props: RightProps) {
|
||||
}, [watchDailyLimit, watchExpire, watchLive, watchQuota, watchType])
|
||||
|
||||
return (
|
||||
<div className={`flex-none basis-80 p-6 flex flex-col gap-6`}>
|
||||
<div className={merge(
|
||||
`flex-none basis-80 p-6 flex flex-col gap-6 relative`,
|
||||
`after:absolute after:inset-0 after:my-6 after:border-l after:border-gray-200 after:select-none after:pointer-events-none`,
|
||||
)}>
|
||||
<h3>订单详情</h3>
|
||||
<ul className={`flex flex-col gap-3`}>
|
||||
<li className={`flex justify-between items-center`}>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import PurchaseForm from '@/components/composites/purchase/_client/form'
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'
|
||||
import {ReactNode} from 'react'
|
||||
import {merge} from '@/lib/utils'
|
||||
|
||||
export type PurchaseProps = {}
|
||||
|
||||
@@ -6,22 +9,35 @@ 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/>
|
||||
<Tabs defaultValue={`short`} className={`gap-4`}>
|
||||
<TabsList className={`w-full p-2 bg-white rounded-lg justify-center`}>
|
||||
<Tab value={`short`}>短效动态</Tab>
|
||||
<Tab value={`long`}>长效静态</Tab>
|
||||
<Tab value={`fixed`}>固定套餐</Tab>
|
||||
<Tab value={`custom`}>定制套餐</Tab>
|
||||
</TabsList>
|
||||
<TabsContent value={`short`}>
|
||||
<PurchaseForm/>
|
||||
</TabsContent>
|
||||
<TabsContent value={`long`}>
|
||||
<PurchaseForm/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Tab(props: {
|
||||
value: string
|
||||
children: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<TabsTrigger className={merge(
|
||||
`w-36 h-12 text-base font-normal flex-none`,
|
||||
`data-[state=active]:text-primary data-[state=active]:bg-primary-muted`,
|
||||
)} value={props.value}>
|
||||
{props.children}
|
||||
</TabsTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ function TabsList({
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={merge(
|
||||
"bg-muted text-muted-foreground inline-flex h-10 w-fit items-center justify-center rounded-lg p-1",
|
||||
"bg-muted text-muted-foreground inline-flex w-fit items-center justify-center rounded-lg p-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -28,23 +28,23 @@ export type Resource = {
|
||||
type: number
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
pss: ResourcePss
|
||||
short: ResourceShort
|
||||
}
|
||||
|
||||
export function name(obj: Resource) {
|
||||
switch (obj.type) {
|
||||
case 1:
|
||||
switch (obj.short.type) {
|
||||
case 1:
|
||||
switch (obj.pss.type) {
|
||||
case 1:
|
||||
return `包时 ${obj.pss.live/60}分钟`
|
||||
case 2:
|
||||
return `包量 ${obj.pss.live/60}分钟`
|
||||
}
|
||||
break
|
||||
return `包时 ${obj.short.live / 60}分钟`
|
||||
case 2:
|
||||
return `包量 ${obj.short.live / 60}分钟`
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export type ResourcePss = {
|
||||
export type ResourceShort = {
|
||||
id: number
|
||||
resource_id: number
|
||||
type: number
|
||||
@@ -134,4 +134,4 @@ export type Announcement = {
|
||||
sort: number
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user