Files
web/src/app/admin/channels/page.tsx

263 lines
8.6 KiB
TypeScript
Raw Normal View History

2025-04-28 17:41:54 +08:00
'use client'
import {useCallback, useEffect, useState} from 'react'
2025-04-28 17:41:54 +08:00
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'
2025-04-28 17:41:54 +08:00
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'
2025-04-28 17:41:54 +08:00
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 isExpired = (expiredAt: string | Date) => {
const date = typeof expiredAt === 'string' ? new Date(expiredAt) : expiredAt
return isBefore(date, new Date())
}
const refresh = useCallback(async (page: number, size: number) => {
2025-04-28 17:41:54 +08:00
try {
setStatus('load')
// 筛选条件
const filter = filterForm.getValues()
const auth_type = filter.auth_type ? parseInt(filter.auth_type) : undefined
const expired_status = filter.expired_status
2025-04-28 17:41:54 +08:00
// 请求数据
console.log({
page, size, ...filter, auth_type,
})
const resp = await listChannels({
page, size, ...filter, auth_type,
})
2025-04-28 17:41:54 +08:00
if (!resp.success) {
throw new Error(resp.message)
}
let filteredList = resp.data.list
if (expired_status !== undefined && expired_status !== 'all') {
filteredList = resp.data.list.filter((channel) => {
const expired = isExpired(channel.expired_at)
return !expired
})
resp.data.total = filteredList.length
}
2025-04-28 17:41:54 +08:00
// 更新数据
setData({
...resp.data,
list: filteredList,
})
2025-04-28 17:41:54 +08:00
setStatus('done')
}
catch (e) {
setStatus('fail')
console.error(e)
toast.error('获取提取结果失败', {
description: (e as Error).message,
})
}
}, [setStatus, filterForm])
2025-04-28 17:41:54 +08:00
useEffect(() => {
refresh(data.page, data.size).then()
}, [data.page, data.size, refresh])
2025-04-28 17:41:54 +08:00
const filterHandler = filterForm.handleSubmit(async (value) => {
await refresh(1, data.size)
2025-04-28 17:41:54 +08:00
})
// ======================
// render
// ======================
return (
<Page>
<section className="flex justify-between">
2025-04-28 17:41:54 +08:00
<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>}>
2025-04-28 17:41:54 +08:00
{({field}) => (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="h-9 w-36">
<SelectValue placeholder="选择认证方式"/>
2025-04-28 17:41:54 +08:00
</SelectTrigger>
<SelectContent>
<SelectItem value="0"></SelectItem>
<SelectItem value="1">IP </SelectItem>
<SelectItem value="2"></SelectItem>
2025-04-28 17:41:54 +08:00
</SelectContent>
</Select>
)}
</FormField>
<fieldset className="flex flex-col gap-2 items-start">
2025-04-28 17:41:54 +08:00
<div>
<legend className="block text-sm"></legend>
2025-04-28 17:41:54 +08:00
</div>
<div className="flex gap-1 items-center">
<FormField<FilterSchema, 'expire_after'> name="expire_after">
2025-04-28 17:41:54 +08:00
{({field}) => (
<DatePicker placeholder="选择开始时间" {...field} format="yyyy-MM-dd"/>
2025-04-28 17:41:54 +08:00
)}
</FormField>
<span>-</span>
<FormField<FilterSchema, 'expire_before'> name="expire_before">
2025-04-28 17:41:54 +08:00
{({field}) => (
<DatePicker placeholder="选择结束时间" {...field} format="yyyy-MM-dd"/>
2025-04-28 17:41:54 +08:00
)}
</FormField>
</div>
</fieldset>
<Button className="h-9">
<SearchIcon/>
2025-04-28 17:41:54 +08:00
</Button>
<Button
theme="outline"
className="h-9"
onClick={() => {
filterForm.reset()
refresh(1, data.size)
}}>
<EraserIcon/>
2025-04-28 17:41:54 +08:00
</Button>
</Form>
</section>
<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),
2025-04-28 17:41:54 +08:00
}}
columns={[
{
header: '代理地址',
cell: ({row}) => {
const channel = row.original
const ip = channel.host
const port = channel.port
const expired = isExpired(channel.expired_at)
return (
<div className={`${expired ? 'text-weak' : ''}`}>
<span>{ip}:{port}</span>
{expired && (
<Badge variant="secondary">
</Badge>
)}
</div>
)
},
2025-04-28 17:41:54 +08:00
},
{
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} variant="secondary">
{ip.trim()}
</Badge >
))}
</div>
</div>
) : hasAuth ? (
<div className="flex flex-col">
<span></span>
<Badge variant="secondary">
{channel.username}:{channel.password}
</Badge >
</div>
) : (
<span className="text-sm text-gray-400"></span>
)}
</div>
)
2025-04-28 17:41:54 +08:00
},
},
{
header: '提取时间',
cell: ({row}) => format(row.original.created_at, 'yyyy-MM-dd HH:mm'),
2025-04-28 17:41:54 +08:00
},
{
header: '过期时间',
cell: ({row}) => format(row.original.expired_at, 'yyyy-MM-dd HH:mm:ss'),
2025-04-28 17:41:54 +08:00
},
]}
/>
</Page>
)
}