添加套餐提取记录功能
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import {callByUser, callPublic} from '@/actions/base'
|
||||
import {Channel} from '@/lib/models'
|
||||
import {PageRecord} from '@/lib/api'
|
||||
import {BatchRecord} from '@/lib/models/batch'
|
||||
|
||||
export async function listChannels(props: {
|
||||
page: number
|
||||
@@ -15,6 +16,10 @@ export async function listChannels(props: {
|
||||
return callByUser<PageRecord<Channel>>('/api/channel/list', props)
|
||||
}
|
||||
|
||||
export async function extractRecord(props: {page: number, size: number}) {
|
||||
return callByUser<PageRecord<BatchRecord>>('/api/batch/page', props)
|
||||
}
|
||||
|
||||
type CreateChannelsResp = {
|
||||
host: string
|
||||
port: string
|
||||
|
||||
@@ -82,11 +82,6 @@ export default function Charts({initialData}: ChartsProps) {
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<Form<FormValues> className={merge(`flex items-end gap-4 flex-wrap`)} handler={handler} form={form} >
|
||||
<FormField name="resource_no" label={<span className="text-sm">套餐编号</span>}>
|
||||
{({field}) => (
|
||||
<Input {...field} className="h-9"/>
|
||||
)}
|
||||
</FormField>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-sm">时间范围筛选</Label>
|
||||
<div className="flex items-center">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {ReactNode} from 'react'
|
||||
import {Metadata} from 'next'
|
||||
|
||||
import {Suspense} from 'react'
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
title: 'IP管理 - 蓝狐代理',
|
||||
@@ -12,5 +12,5 @@ export type ChannelsLayoutProps = {
|
||||
}
|
||||
|
||||
export default async function ChannelsLayout(props: ChannelsLayoutProps) {
|
||||
return props.children
|
||||
return <Suspense>{props.children}</Suspense>
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ export function Navbar() {
|
||||
<NavTitle label="资源管理"/>
|
||||
<NavItem href="/admin/resources" icon={<Package size={20}/>} label="我的套餐" expand={navbar}/>
|
||||
<NavItem href="/admin/channels" icon={<Eye size={20}/>} label="IP 管理" expand={navbar}/>
|
||||
<NavItem href="/admin" icon={<Archive size={20}/>} label="提取记录" expand={navbar}/>
|
||||
<NavItem href="/admin/record" icon={<Archive size={20}/>} label="提取记录" expand={navbar}/>
|
||||
{/* <NavTitle label="数据统计"/>
|
||||
<NavItem href="/admin" icon={<ArchiveRestore size={20}/>} label="使用记录" expand={navbar}/> */}
|
||||
</TooltipProvider>
|
||||
|
||||
16
src/app/admin/record/layout.tsx
Normal file
16
src/app/admin/record/layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import {ReactNode} from 'react'
|
||||
import {Metadata} from 'next'
|
||||
import {Suspense} from 'react'
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
title: '提取记录 - 蓝狐代理',
|
||||
}
|
||||
}
|
||||
|
||||
export type RecordLayoutProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default async function RecordLayout(props: RecordLayoutProps) {
|
||||
return <Suspense>{props.children}</Suspense>
|
||||
}
|
||||
188
src/app/admin/record/page.tsx
Normal file
188
src/app/admin/record/page.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
'use client'
|
||||
import {useCallback, useEffect, useState} from 'react'
|
||||
import {useStatus} from '@/lib/states'
|
||||
import {PageRecord} from '@/lib/api'
|
||||
import Page from '@/components/page'
|
||||
import DataTable from '@/components/data-table'
|
||||
import {toast} from 'sonner'
|
||||
import {extractRecord} from '@/actions/channel'
|
||||
import {BatchRecord} from '@/lib/models/batch'
|
||||
import {format} from 'date-fns'
|
||||
import {Form, FormField} from '@/components/ui/form'
|
||||
import {z} from 'zod'
|
||||
import {useForm} from 'react-hook-form'
|
||||
import {zodResolver} from '@hookform/resolvers/zod'
|
||||
import DatePicker from '@/components/date-picker'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {EraserIcon, SearchIcon} from 'lucide-react'
|
||||
|
||||
export type RecordPageProps = {}
|
||||
|
||||
export default function RecordPage(props: RecordPageProps) {
|
||||
const [status, setStatus] = useStatus()
|
||||
const [data, setData] = useState<PageRecord<BatchRecord>>({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0,
|
||||
list: [],
|
||||
})
|
||||
|
||||
// ======================
|
||||
// filter
|
||||
// ======================
|
||||
|
||||
const filterSchema = z.object({
|
||||
time_start: z.date().optional(),
|
||||
time_end: z.date().optional(),
|
||||
})
|
||||
type FilterSchema = z.infer<typeof filterSchema>
|
||||
|
||||
const filterForm = useForm<FilterSchema>({
|
||||
resolver: zodResolver(filterSchema),
|
||||
defaultValues: {
|
||||
time_start: undefined,
|
||||
time_end: undefined,
|
||||
},
|
||||
})
|
||||
|
||||
const fetchRecords = useCallback(async (page: number, size: number) => {
|
||||
try {
|
||||
setStatus('load')
|
||||
// 获取筛选条件
|
||||
const filter = filterForm.getValues()
|
||||
const result = await extractRecord({
|
||||
page,
|
||||
size,
|
||||
...filter,
|
||||
})
|
||||
|
||||
if (result.success && result.data) {
|
||||
setData(result.data)
|
||||
}
|
||||
else {
|
||||
throw new Error('获取数据失败')
|
||||
}
|
||||
setStatus('done')
|
||||
}
|
||||
catch (error) {
|
||||
setStatus('fail')
|
||||
console.error(error)
|
||||
toast.error('获取提取结果失败', {
|
||||
description: (error as Error).message,
|
||||
})
|
||||
}
|
||||
}, [filterForm, setStatus])
|
||||
|
||||
const filterHandler = filterForm.handleSubmit(async () => {
|
||||
// 重置到第一页进行筛选
|
||||
await fetchRecords(1, data.size)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
fetchRecords(data.page, data.size).then()
|
||||
}, [data.page, data.size, fetchRecords])
|
||||
|
||||
return (
|
||||
<Page>
|
||||
{/* 筛选表单 */}
|
||||
<section className="flex justify-between">
|
||||
<div></div>
|
||||
<Form form={filterForm} handler={filterHandler} className="flex-auto flex flex-wrap gap-4 items-end">
|
||||
<fieldset className="flex flex-col gap-2 items-start">
|
||||
<div>
|
||||
<legend className="block text-sm">提取时间</legend>
|
||||
</div>
|
||||
<div className="flex gap-1 items-center">
|
||||
<FormField<FilterSchema, 'time_start'> name="time_start">
|
||||
{({field}) => (
|
||||
<DatePicker
|
||||
placeholder="选择开始时间"
|
||||
{...field}
|
||||
format="yyyy-MM-dd"
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
<span>-</span>
|
||||
<FormField<FilterSchema, 'time_end'> name="time_end">
|
||||
{({field}) => (
|
||||
<DatePicker
|
||||
placeholder="选择结束时间"
|
||||
{...field}
|
||||
format="yyyy-MM-dd"
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<Button className="h-9" type="submit">
|
||||
<SearchIcon/>
|
||||
筛选
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
theme="outline"
|
||||
className="h-9"
|
||||
onClick={() => {
|
||||
filterForm.reset()
|
||||
fetchRecords(1, data.size)
|
||||
}}>
|
||||
<EraserIcon/>
|
||||
重置
|
||||
</Button>
|
||||
</Form>
|
||||
</section>
|
||||
|
||||
<DataTable
|
||||
status={status}
|
||||
data={data.list}
|
||||
pagination={{
|
||||
page: data.page,
|
||||
size: data.size,
|
||||
total: data.total,
|
||||
onPageChange: page => fetchRecords(page, data.size),
|
||||
onSizeChange: size => fetchRecords(1, size),
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
header: '批次号',
|
||||
cell: ({row}) => <div>{row.original.batch_no}</div>,
|
||||
accessorKey: 'batch_no',
|
||||
},
|
||||
{
|
||||
header: 'IP地址',
|
||||
cell: ({row}) => <div>{row.original.ip}</div>,
|
||||
accessorKey: 'ip',
|
||||
},
|
||||
{
|
||||
header: '运营商',
|
||||
cell: ({row}) => <div>{row.original.isp}</div>,
|
||||
accessorKey: 'isp',
|
||||
},
|
||||
{
|
||||
header: '地区',
|
||||
cell: ({row}) => <div>{row.original.prov}</div>,
|
||||
accessorKey: 'prov',
|
||||
},
|
||||
{
|
||||
header: '提取数量',
|
||||
cell: ({row}) => <div>{row.original.count}</div>,
|
||||
accessorKey: 'count',
|
||||
},
|
||||
{
|
||||
header: '资源数量',
|
||||
cell: ({row}) => <div>{row.original.resource_id}</div>,
|
||||
accessorKey: 'resource_id',
|
||||
},
|
||||
{
|
||||
header: '提取时间',
|
||||
cell: ({row}) => {
|
||||
return <div>{format(new Date(row.original.time), 'yyyy-MM-dd HH:mm:ss')}</div>
|
||||
},
|
||||
accessorKey: 'time',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -135,12 +135,14 @@ export default function Right() {
|
||||
¥{price}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">总折扣</span>
|
||||
<span className="text-sm">
|
||||
-¥{discounted}
|
||||
</span>
|
||||
</li>
|
||||
{discounted === 1 ? '' : (
|
||||
<li className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">总折扣</span>
|
||||
<span className="text-sm">
|
||||
-¥{discounted}
|
||||
</span>
|
||||
</li>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
11
src/lib/models/batch.ts
Normal file
11
src/lib/models/batch.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type BatchRecord = {
|
||||
batch_no: string
|
||||
ip: string
|
||||
id: number
|
||||
count: number
|
||||
isp: string
|
||||
resource_id: number
|
||||
time: string
|
||||
user_id: number
|
||||
prov: string
|
||||
}
|
||||
Reference in New Issue
Block a user