Files
web/src/app/admin/resources/_components/list.tsx
2026-03-13 18:12:22 +08:00

267 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import {useCallback, useEffect, useMemo, useState} from 'react'
import {useSearchParams} from 'next/navigation'
import {useForm} from 'react-hook-form'
import {zodResolver} from '@hookform/resolvers/zod'
import zod from 'zod'
import {toast} from 'sonner'
import {useStatus} from '@/lib/states'
import {ExtraResp} from '@/lib/api'
import {listResourceLong, listResourceShort} from '@/actions/resource'
import DataTable from '@/components/data-table'
import {ColumnDef} from '@tanstack/react-table'
import {Resource} from '@/lib/models/resource'
import ResourceFilter, {ResourceFilterValues} from './filter'
import {
ExpireBadge,
formatDateTime,
getTodayUsage,
isValidResourcestatus,
isValidResourceType,
ResourceTypeBadge,
} from './utils'
const filterSchema = zod.object({
resource_no: zod.string().optional().default(''),
type: zod.enum(['expire', 'quota', 'all']).default('all'),
status: zod.enum(['0', '1', '2']).default('1'),
create_after: zod.date().optional(),
create_before: zod.date().optional(),
expire_after: zod.date().optional(),
expire_before: zod.date().optional(),
})
interface ResourceListProps {
resourceType: 'long' | 'short'
}
export default function ResourceList({resourceType}: ResourceListProps) {
const isLong = resourceType === 'long'
const [status, setStatus] = useStatus()
const [data, setData] = useState<ExtraResp<typeof listResourceLong | typeof listResourceShort>>({
page: 1,
size: 10,
total: 0,
list: [],
})
// 从 URL 参数初始化筛选条件
const params = useSearchParams()
const paramType = params.get('type')
const paramStatus = params.get('status')
const form = useForm<ResourceFilterValues>({
resolver: zodResolver(filterSchema),
defaultValues: {
resource_no: params.get('resource_no') || '',
type: isValidResourceType(paramType) ? paramType : 'all',
status: isValidResourcestatus(paramStatus) ? paramStatus : '1',
create_after: params.get('create_after') ? new Date(params.get('create_after')!) : undefined,
create_before: params.get('create_before') ? new Date(params.get('create_before')!) : undefined,
expire_after: params.get('expire_after') ? new Date(params.get('expire_after')!) : undefined,
expire_before: params.get('expire_before') ? new Date(params.get('expire_before')!) : undefined,
},
})
const getValues = form.getValues
// 查询数据
const refresh = useCallback(async (page: number, size: number) => {
setStatus('load')
try {
const type = {
all: undefined,
expire: 1,
quota: 2,
}[getValues('type')]
const status = getValues('status')
const create_after = getValues('create_after')
const create_before = getValues('create_before')
const expire_after = getValues('expire_after')
const expire_before = getValues('expire_before')
const resource_no = getValues('resource_no')
const listFn = isLong ? listResourceLong : listResourceShort
const res = await listFn({
page,
size,
type,
status: Number(status),
create_after,
create_before,
expire_after,
expire_before,
resource_no,
})
if (res.success) {
setData(res.data)
setStatus('done')
}
else {
throw new Error(`Failed to load ${resourceType} resource`)
}
}
catch (e) {
setStatus('fail')
toast.error(e instanceof Error ? e.message : `加载${isLong ? '长' : '短'}效资源失败`)
}
}, [getValues, setStatus, isLong, resourceType])
useEffect(() => {
refresh(1, 10).then()
}, [refresh])
// 处理筛选提交
const handleSubmit = async () => {
await refresh(1, data.size)
}
// 处理重置
const handleReset = () => {
form.reset({
type: 'all',
status: '1',
resource_no: '',
create_after: undefined,
create_before: undefined,
expire_after: undefined,
expire_before: undefined,
})
}
// 表格列定义
const columns = useMemo<ColumnDef<Resource<1> | Resource<2>>[]>(() => {
const resourceKey = isLong ? 'long' : 'short'
const baseColumns: ColumnDef<Resource<1> | Resource<2>>[] = [
{
header: '套餐编号',
cell: ({row}) => {
const expireAt = resourceKey === 'long'
? (row.original as Resource<2>).long.expire_at
: (row.original as Resource<1>).short.expire_at
return (
<div className="flex gap-1">
<span>{row.original.resource_no}</span>
<ExpireBadge expireAt={expireAt}/>
</div>
)
},
},
{
header: '类型',
cell: ({row}) => {
const type = resourceKey === 'long'
? (row.original as Resource<2>).long.type
: (row.original as Resource<1>).short.type
return <ResourceTypeBadge type={type}/>
},
},
{
header: 'IP 时效',
cell: ({row}) => {
const live = resourceKey === 'long'
? (row.original as Resource<2>).long.live
: (row.original as Resource<1>).short.live
return <span>{isLong ? `${live}小时` : `${live / 60}分钟`}</span>
},
},
{
header: '使用情况',
cell: ({row}) => {
const resource = resourceKey === 'long'
? (row.original as Resource<2>).long
: (row.original as Resource<1>).short
if (resource.type === 1) {
const todayUsage = getTodayUsage(resource.last_at, resource.daily)
return (
<div className="flex gap-1">
<span>{todayUsage}/{resource.quota}</span>
</div>
)
}
else if (resource.type === 2) {
if (isLong) {
return (
<div className="flex gap-1">
{resource.used < resource.quota
? <span className="text-green-500"></span>
: <span className="text-red-500"></span>}
<span>|</span>
<span>{resource.used} / {resource.quota}</span>
</div>
)
}
else {
return (
<div className="flex gap-1">
<span>{resource.used}/{resource.quota}</span>
</div>
)
}
}
return <span>-</span>
},
},
{
header: '最近使用时间',
cell: ({row}) => {
const lastAt = resourceKey === 'long'
? (row.original as Resource<2>).long.last_at
: (row.original as Resource<1>).short.last_at
return lastAt ? formatDateTime(lastAt) : '暂未使用'
},
},
{
header: '开通时间',
cell: ({row}) => formatDateTime(row.original.created_at),
},
]
// 短效资源增加到期时间列
if (!isLong) {
baseColumns.push({
header: '到期时间',
cell: ({row}) => formatDateTime((row.original as Resource<1>).short.expire_at),
})
}
return baseColumns
}, [isLong])
return (
<>
{/* 操作区 */}
<section className="flex justify-between flex-wrap">
<div></div>
<ResourceFilter
form={form}
onSubmit={handleSubmit}
onReset={handleReset}
/>
</section>
{/* 数据表 */}
<DataTable
data={data.list}
status={status}
pagination={{
total: data.total,
page: data.page,
size: data.size,
onPageChange: async (page: number) => {
await refresh(page, data.size)
},
onSizeChange: async (size: number) => {
await refresh(data.page, size)
},
}}
columns={columns}
/>
</>
)
}