更新资源列表接口,调整相关数据结构,优化页面布局和样式

This commit is contained in:
2025-05-19 11:04:40 +08:00
parent 52c0184482
commit 9652181fe4
14 changed files with 105 additions and 230 deletions

View File

@@ -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(){

View File

@@ -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>
)
}
}

View File

@@ -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')
)
},
},

View File

@@ -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>

View File

@@ -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>
)
}

View File

@@ -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>

View File

@@ -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>
)
}

View 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>
)
}

View File

@@ -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`}/>

View File

@@ -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`}>

View File

@@ -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>
)
}

View File

@@ -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}

View File

@@ -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
}
}