229 lines
7.7 KiB
TypeScript
229 lines
7.7 KiB
TypeScript
'use client'
|
|
import {Suspense, useCallback, useEffect, useState} from 'react'
|
|
import {useStatus} from '@/lib/states'
|
|
import {PageRecord} from '@/lib/api'
|
|
import {Channel} from '@/lib/models'
|
|
import Page from '@/components/page'
|
|
import DataTable from '@/components/data-table'
|
|
import {toast} from 'sonner'
|
|
import {listChannels} from '@/actions/channel'
|
|
import {format, isBefore} 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'
|
|
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'
|
|
import {Badge} from '@/components/ui/badge'
|
|
import Addr from '../_components/addr'
|
|
export type ChannelsPageProps = {}
|
|
|
|
export default function ChannelsPage(props: ChannelsPageProps) {
|
|
// ======================
|
|
// data
|
|
// ======================
|
|
|
|
const [status, setStatus] = useStatus()
|
|
const [data, setData] = useState<PageRecord<Channel>>({
|
|
page: 1,
|
|
size: 10,
|
|
total: 0,
|
|
list: [],
|
|
})
|
|
// ======================
|
|
// filter
|
|
// ======================
|
|
const filterSchema = z.object({
|
|
auth_type: z.enum(['0', '1', '2']),
|
|
expired_status: z.enum(['all', 'active']).default('all'),
|
|
expire_after: z.date().optional(),
|
|
expire_before: z.date().optional(),
|
|
})
|
|
type FilterSchema = z.infer<typeof filterSchema>
|
|
const filterForm = useForm<FilterSchema>({
|
|
resolver: zodResolver(filterSchema),
|
|
defaultValues: {
|
|
auth_type: '0',
|
|
expired_status: 'all',
|
|
expire_after: undefined,
|
|
expire_before: undefined,
|
|
},
|
|
})
|
|
|
|
const refresh = useCallback(async (page: number, size: number) => {
|
|
try {
|
|
setStatus('load')
|
|
|
|
// 筛选条件
|
|
const filter = filterForm.getValues()
|
|
const auth_type = filter.auth_type ? parseInt(filter.auth_type) : undefined
|
|
|
|
// 请求数据
|
|
const resp = await listChannels({
|
|
page, size, ...filter, auth_type,
|
|
})
|
|
|
|
if (!resp.success) {
|
|
throw new Error(resp.message)
|
|
}
|
|
|
|
// 更新数据
|
|
setData(resp.data)
|
|
setStatus('done')
|
|
}
|
|
catch (e) {
|
|
setStatus('fail')
|
|
console.error(e)
|
|
toast.error('获取提取结果失败', {
|
|
description: (e as Error).message,
|
|
})
|
|
}
|
|
}, [setStatus, filterForm])
|
|
|
|
useEffect(() => {
|
|
refresh(data.page, data.size).then()
|
|
}, [data.page, data.size, refresh])
|
|
|
|
const filterHandler = filterForm.handleSubmit(async (value) => {
|
|
await refresh(1, data.size)
|
|
})
|
|
|
|
// ======================
|
|
// render
|
|
// ======================
|
|
|
|
return (
|
|
<Page>
|
|
<section className="flex justify-between">
|
|
<div></div>
|
|
<Form form={filterForm} handler={filterHandler} className="flex-auto flex flex-wrap gap-4 items-end">
|
|
<FormField<FilterSchema, 'expired_status'> name="expired_status" label={<span className="text-sm">状态</span>}>
|
|
{({field}) => (
|
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
<SelectTrigger className="h-9 w-32">
|
|
<SelectValue placeholder="选择状态"/>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">全部</SelectItem>
|
|
<SelectItem value="active">未过期</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
</FormField>
|
|
|
|
<FormField<FilterSchema, 'auth_type'> name="auth_type" label={<span className="text-sm">认证方式</span>}>
|
|
{({field}) => (
|
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
<SelectTrigger className="h-9 w-36">
|
|
<SelectValue placeholder="选择认证方式"/>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="0">全部</SelectItem>
|
|
<SelectItem value="1">IP 白名单</SelectItem>
|
|
<SelectItem value="2">账号密码</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
)}
|
|
</FormField>
|
|
<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, 'expire_after'> name="expire_after">
|
|
{({field}) => (
|
|
<DatePicker placeholder="选择开始时间" {...field} format="yyyy-MM-dd"/>
|
|
)}
|
|
</FormField>
|
|
<span>-</span>
|
|
<FormField<FilterSchema, 'expire_before'> name="expire_before">
|
|
{({field}) => (
|
|
<DatePicker placeholder="选择结束时间" {...field} format="yyyy-MM-dd"/>
|
|
)}
|
|
</FormField>
|
|
</div>
|
|
</fieldset>
|
|
<Button className="h-9">
|
|
<SearchIcon/>
|
|
筛选
|
|
</Button>
|
|
<Button
|
|
theme="outline"
|
|
className="h-9"
|
|
onClick={() => {
|
|
filterForm.reset()
|
|
refresh(1, data.size)
|
|
}}>
|
|
<EraserIcon/>
|
|
重置
|
|
</Button>
|
|
</Form>
|
|
</section>
|
|
|
|
<Suspense>
|
|
<DataTable
|
|
status={status}
|
|
data={data.list}
|
|
pagination={{
|
|
page: data.page,
|
|
size: data.size,
|
|
total: data.total,
|
|
onPageChange: page => refresh(page, data.size),
|
|
onSizeChange: size => refresh(1, size),
|
|
}}
|
|
columns={[
|
|
{
|
|
header: '代理地址',
|
|
cell: ({row}) => <Addr channel={row.original}/>,
|
|
},
|
|
{
|
|
header: '认证方式',
|
|
cell: ({row}) => {
|
|
const channel = row.original
|
|
const hasWhitelist = channel.whitelists && channel.whitelists.trim() !== ''
|
|
const hasAuth = channel.username && channel.password
|
|
|
|
return (
|
|
<div className="flex flex-col gap-1 min-w-0">
|
|
{hasWhitelist ? (
|
|
<div className="flex flex-col">
|
|
<span >白名单</span>
|
|
<div className="flex flex-wrap gap-1 max-w-[200px]">
|
|
{channel.whitelists.split(',').map((ip, index) => (
|
|
<Badge key={index} className="bg-green-100 text-green-700 hover:bg-green-100 dark:bg-green-900/30 dark:text-green-400">
|
|
{ip.trim()}
|
|
</Badge >
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : hasAuth ? (
|
|
<div className="flex flex-col">
|
|
<span>账号密码</span>
|
|
<Badge className="bg-blue-100 text-blue-700 hover:bg-blue-100 dark:bg-blue-900/30 dark:text-blue-400">
|
|
{channel.username}:{channel.password}
|
|
</Badge >
|
|
</div>
|
|
) : (
|
|
<span className="text-sm text-gray-400">无认证</span>
|
|
)}
|
|
</div>
|
|
)
|
|
},
|
|
},
|
|
{
|
|
header: '提取时间',
|
|
cell: ({row}) => format(row.original.created_at, 'yyyy-MM-dd HH:mm'),
|
|
},
|
|
{
|
|
header: '过期时间',
|
|
cell: ({row}) => format(row.original.expired_at, 'yyyy-MM-dd HH:mm:ss'),
|
|
},
|
|
]}
|
|
/>
|
|
</Suspense>
|
|
</Page>
|
|
)
|
|
}
|