新增通道列表查询页面
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
## TODO
|
||||
|
||||
检查时间范围选择,限定到一定范围内
|
||||
|
||||
将翻页操作反映在路由历史中,可以通过后退返回到上一个翻页状态
|
||||
|
||||
使用 pure js 的包代替 canvas,加快编译速度
|
||||
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
'use server'
|
||||
import {callByUser} from '@/actions/base'
|
||||
import {Channel} from '@/lib/models'
|
||||
import {PageRecord} from '@/lib/api'
|
||||
|
||||
async function createChannels(params: {
|
||||
export async function listChannels(props: {
|
||||
page: number
|
||||
size: number
|
||||
auth_type?: number
|
||||
prov?: string
|
||||
city?: string
|
||||
isp?: string
|
||||
expiration?: Date
|
||||
}) {
|
||||
return callByUser<PageRecord<Channel>>('/api/channel/list', props)
|
||||
}
|
||||
|
||||
type CreateChannelsResp = {
|
||||
host: string
|
||||
port: string
|
||||
username?: string
|
||||
password?: string
|
||||
}
|
||||
|
||||
export async function createChannels(params: {
|
||||
resource_id: number
|
||||
protocol: number
|
||||
auth_type: number
|
||||
@@ -12,14 +33,3 @@ async function createChannels(params: {
|
||||
}) {
|
||||
return callByUser<CreateChannelsResp[]>('/api/channel/create', params)
|
||||
}
|
||||
|
||||
type CreateChannelsResp = {
|
||||
host: string
|
||||
port: string
|
||||
username?: string
|
||||
password?: string
|
||||
}
|
||||
|
||||
export {
|
||||
createChannels,
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function Navbar(props: NavbarProps) {
|
||||
<NavItem href={`/admin/resources`} icon={`📦`} label={`套餐管理`}/>
|
||||
<NavTitle label={`IP 管理`}/>
|
||||
<NavItem href={`/admin/extract`} icon={`📤`} label={`提取 IP`}/>
|
||||
<NavItem href={`/admin`} icon={`👁️`} label={`IP 管理`}/>
|
||||
<NavItem href={`/admin/channels`} icon={`👁️`} label={`IP 管理`}/>
|
||||
<NavItem href={`/admin`} icon={`📜`} label={`提取记录`}/>
|
||||
<NavItem href={`/admin`} icon={`🗂️`} label={`使用记录`}/>
|
||||
</section>
|
||||
|
||||
@@ -9,19 +9,10 @@ export type ProfileProps = {}
|
||||
|
||||
export default function Profile(props: ProfileProps) {
|
||||
const refreshProfile = useProfileStore(store => store.refreshProfile)
|
||||
const router = useRouter()
|
||||
const doLogout = async () => {
|
||||
try {
|
||||
const resp = await logout()
|
||||
if (resp.success) {
|
||||
await refreshProfile()
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
toast.error('退出登录失败', {
|
||||
description: (e as Error).message,
|
||||
})
|
||||
const resp = await logout()
|
||||
if (resp.success) {
|
||||
await refreshProfile()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
184
src/app/admin/channels/page.tsx
Normal file
184
src/app/admin/channels/page.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
'use client'
|
||||
import {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} 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'
|
||||
|
||||
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: [],
|
||||
})
|
||||
|
||||
const refresh = async (page: number, size: number) => {
|
||||
try {
|
||||
setStatus('load')
|
||||
|
||||
// 筛选条件
|
||||
const filter = filterForm.getValues()
|
||||
const auth_type = filter.auth_type ? parseInt(filter.auth_type) : undefined
|
||||
|
||||
// 请求数据
|
||||
console.log({
|
||||
page, size, ...filter, auth_type,
|
||||
})
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh(data.page, data.size).then()
|
||||
}, [])
|
||||
|
||||
// ======================
|
||||
// filter
|
||||
// ======================
|
||||
|
||||
const filterSchema = z.object({
|
||||
auth_type: z.enum(['0', '1', '2']),
|
||||
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',
|
||||
expire_after: undefined,
|
||||
expire_before: undefined,
|
||||
},
|
||||
})
|
||||
const filterHandler = filterForm.handleSubmit(async value => {
|
||||
await refresh(data.page, data.size)
|
||||
})
|
||||
|
||||
// ======================
|
||||
// render
|
||||
// ======================
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<section className={`flex justify-between`}>
|
||||
<div></div>
|
||||
<Form form={filterForm} handler={filterHandler} className={`flex-none flex gap-4 items-end`}>
|
||||
<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()}>
|
||||
<EraserIcon/>重置
|
||||
</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),
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
header: '代理地址', cell: ({row}) => `${row.original.proxy_host}:${row.original.proxy_port}`,
|
||||
},
|
||||
{
|
||||
header: '认证方式', cell: ({row}) => {
|
||||
return <div className={`flex flex-col gap-1`}>
|
||||
{row.original.auth_ip && (<>
|
||||
<span className={`text-weak`}>IP 白名单</span>
|
||||
<span>{row.original.user_host}</span>
|
||||
</>)}
|
||||
{row.original.auth_pass && (<>
|
||||
<span className={`text-weak`}>账号密码</span>
|
||||
<span>{row.original.username}:{row.original.password}</span>
|
||||
</>)}
|
||||
</div>
|
||||
},
|
||||
},
|
||||
{
|
||||
header: '过期时间', cell: ({row}) => format(row.original.expiration, 'yyyy-MM-dd HH:mm:ss'),
|
||||
},
|
||||
{
|
||||
header: '操作', cell: ({row}) => <span>-</span>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import * as qrcode from 'qrcode'
|
||||
import Link from 'next/link'
|
||||
import RechargeModal from '@/components/composites/purchase/_client/recharge'
|
||||
import {User} from '@/lib/models'
|
||||
import { Label } from '@/components/ui/label'
|
||||
|
||||
export type ProfilePageProps = {}
|
||||
|
||||
@@ -454,7 +455,7 @@ function ChangePhoneDialog(props: {
|
||||
<Form form={form} onSubmit={onSubmit} className="space-y-4 py-4">
|
||||
{currentPhone && (
|
||||
<div className="space-y-1">
|
||||
<FormLabel>当前手机号</FormLabel>
|
||||
<Label>当前手机号</Label>
|
||||
<Input value={maskPhone(currentPhone)} disabled/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -136,7 +136,7 @@ export default function ResourcesPage(props: ResourcesPageProps) {
|
||||
<FormField name={`type`} label={<span className={`text-sm`}>类型</span>}>
|
||||
{({id, field}) => (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className={`w-24`}>
|
||||
<SelectTrigger className={`w-24 h-9`}>
|
||||
<SelectValue placeholder={`选择套餐类型`}/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
'use client'
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@/components/ui/table'
|
||||
import {createWhitelist, removeWhitelist, listWhitelist, Whitelist, updateWhitelist} from '@/actions/whitelist'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog'
|
||||
import {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {useEffect, useRef, useState} from 'react'
|
||||
import {PageRecord} from '@/lib/api'
|
||||
import {useStatus} from '@/lib/states'
|
||||
import {toast} from 'sonner'
|
||||
@@ -21,8 +20,6 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
import {Pagination} from '@/components/ui/pagination'
|
||||
import {Checkbox} from '@/components/ui/checkbox'
|
||||
import Page from '@/components/page'
|
||||
import DataTable from '@/components/data-table'
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ export default function Extract(props: ExtractProps) {
|
||||
</div>
|
||||
) : resources.map((resource, i) => (<>
|
||||
<SelectItem
|
||||
key={`${resource.id}-${i}`} value={String(resource.id)} className={`p-3`}>
|
||||
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 && (<>
|
||||
<div className={`flex gap-2 items-center bg-green-50 w-fit px-2 py-1 rounded-md text-sm`}>
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function DataTable<T extends Record<string, unknown>>(props: Data
|
||||
|
||||
return (<>
|
||||
{/* 数据表*/}
|
||||
<div className={`border rounded-md relative bg-card`}>
|
||||
<div className={`rounded-md relative bg-card`}>
|
||||
<TableRoot>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map(group => (
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function DatePicker(props: DatePickerProps) {
|
||||
)}
|
||||
>
|
||||
<CalendarIcon/>
|
||||
<span className={`text-sm`}>
|
||||
<span className={merge(`text-sm`, !props.value && 'text-weak')}>
|
||||
{props.value
|
||||
? format(props.value, props.format || 'yyyy-MM-dd HH:mm:ss')
|
||||
: props.placeholder || '选择日期'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ComponentProps, ReactNode} from 'react'
|
||||
import {ComponentProps} from 'react'
|
||||
import {merge} from '@/lib/utils'
|
||||
|
||||
export type PageProps = {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
'use client'
|
||||
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import {Slot} from '@radix-ui/react-slot'
|
||||
import {
|
||||
@@ -8,13 +7,13 @@ import {
|
||||
ControllerProps,
|
||||
SubmitHandler,
|
||||
FieldValues, useFormContext, FieldPath, UseFormReturn, ControllerRenderProps,
|
||||
ControllerFieldState, UseFormStateReturn, FieldError, FieldErrors, SubmitErrorHandler,
|
||||
ControllerFieldState, UseFormStateReturn, FieldError, SubmitErrorHandler,
|
||||
} from 'react-hook-form'
|
||||
|
||||
import {merge} from '@/lib/utils'
|
||||
import {Label} from '@/components/ui/label'
|
||||
|
||||
import React, {BaseSyntheticEvent, ComponentProps, createContext, ReactNode, useContext, useId} from 'react'
|
||||
import React, {ComponentProps, createContext, ReactNode, useContext, useId} from 'react'
|
||||
|
||||
type FormProps<T extends FieldValues> = {
|
||||
form: UseFormReturn<T>
|
||||
@@ -60,13 +59,6 @@ type FormFieldProps<
|
||||
}) => ReactNode
|
||||
} & Omit<ControllerProps<V, N>, 'control' | 'render'>
|
||||
|
||||
type FormFieldContext = {
|
||||
id: string
|
||||
error?: FieldError
|
||||
}
|
||||
|
||||
const FormFieldContext = createContext<FormFieldContext | null>(null)
|
||||
|
||||
function FormField<
|
||||
V extends FieldValues = FieldValues,
|
||||
N extends FieldPath<V> = FieldPath<V>,
|
||||
@@ -76,68 +68,48 @@ function FormField<
|
||||
return (
|
||||
<Controller<V, N> name={props.name} control={form.control} render={({field, fieldState, formState}) => (
|
||||
<div data-slot="form-field" className={merge('grid gap-2', props.className)}>
|
||||
<FormFieldContext value={{id: id, error: fieldState.error}}>
|
||||
{!!props.label &&
|
||||
<Label
|
||||
data-slot="form-label"
|
||||
data-fail={!!fieldState.error}
|
||||
className={merge('data-[error=true]:text-fail')}
|
||||
htmlFor={id}>
|
||||
{props.label}
|
||||
</Label>
|
||||
}
|
||||
{!!props.label &&
|
||||
<FormLabel error={fieldState.error}>
|
||||
{props.label}
|
||||
</FormLabel>
|
||||
}
|
||||
|
||||
<Slot
|
||||
data-slot="form-control"
|
||||
aria-invalid={!!fieldState.error}
|
||||
aria-describedby={
|
||||
!!fieldState.error
|
||||
? `${id}-description`
|
||||
: `${id}-description ${id}-message`
|
||||
}>
|
||||
{props.children({id, field, fieldState, formState})}
|
||||
</Slot>
|
||||
<Slot
|
||||
data-slot="form-control"
|
||||
aria-invalid={!!fieldState.error}
|
||||
aria-describedby={
|
||||
!!fieldState.error
|
||||
? `${id}-description`
|
||||
: `${id}-description ${id}-message`
|
||||
}>
|
||||
{props.children({id, field, fieldState, formState})}
|
||||
</Slot>
|
||||
|
||||
{!fieldState.error ? null : (
|
||||
<p
|
||||
data-slot="form-message"
|
||||
className={merge('text-fail text-sm')}>
|
||||
{fieldState.error?.message}
|
||||
</p>
|
||||
)}
|
||||
</FormFieldContext>
|
||||
{!fieldState.error ? null : (
|
||||
<FormMessage error={fieldState.error}/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
)}/>
|
||||
)
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const context = useContext(FormFieldContext)
|
||||
if (!context) {
|
||||
throw new Error('FormField components must be used within a FormField component')
|
||||
}
|
||||
return context
|
||||
type FormState = {
|
||||
error?: FieldError
|
||||
}
|
||||
|
||||
|
||||
function FormLabel({className, ...props}: ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
const {id, error} = useFormField()
|
||||
|
||||
function FormLabel({className, id, error, ...props}: ComponentProps<typeof LabelPrimitive.Root> & FormState) {
|
||||
return (
|
||||
<Label
|
||||
data-slot="form-label"
|
||||
data-fail={!!error}
|
||||
className={merge('data-[error=true]:text-fail', className)}
|
||||
className={merge('data-[fail=true]:text-fail', className)}
|
||||
htmlFor={id}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FormDescription({className, ...props}: ComponentProps<'p'>) {
|
||||
const {id} = useFormField()
|
||||
|
||||
function FormDescription({className, id, error, ...props}: ComponentProps<'p'> & FormState) {
|
||||
return (
|
||||
<p
|
||||
data-slot="form-description"
|
||||
@@ -148,8 +120,7 @@ function FormDescription({className, ...props}: ComponentProps<'p'>) {
|
||||
)
|
||||
}
|
||||
|
||||
function FormMessage({className, ...props}: ComponentProps<'p'>) {
|
||||
const {id, error} = useFormField()
|
||||
function FormMessage({className, id, error, ...props}: ComponentProps<'p'> & FormState) {
|
||||
const body = error ? String(error?.message ?? '') : props.children
|
||||
|
||||
if (!body) {
|
||||
|
||||
@@ -37,7 +37,7 @@ function SelectTrigger({
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={merge(
|
||||
'border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground ',
|
||||
'border-input data-[placeholder]:text-weak [&_svg:not([class*=\'text-\'])]:text-muted-foreground ',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-fail/20 dark:aria-invalid:ring-fail/40 ',
|
||||
'aria-invalid:border-fail dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 ',
|
||||
'rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap transition-[color] ',
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import * as React from 'react'
|
||||
|
||||
import { merge } from "@/lib/utils"
|
||||
import {merge} from '@/lib/utils'
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
function Table({className, ...props}: React.ComponentProps<'table'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
@@ -12,79 +12,79 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={merge("w-full caption-bottom text-sm", className)}
|
||||
className={merge('w-full caption-bottom text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
function TableHeader({className, ...props}: React.ComponentProps<'thead'>) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={merge("[&_tr]:border-b", className)}
|
||||
className={merge('[&_tr]:border-b', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
function TableBody({className, ...props}: React.ComponentProps<'tbody'>) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={merge("[&_tr:last-child]:border-0", className)}
|
||||
className={merge('[&_tr:last-child]:border-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
function TableFooter({className, ...props}: React.ComponentProps<'tfoot'>) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={merge(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
function TableRow({className, ...props}: React.ComponentProps<'tr'>) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={merge(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
function TableHead({className, ...props}: React.ComponentProps<'th'>) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={merge(
|
||||
"text-weak h-10 px-2 text-left align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
'text-weak h-10 px-2 text-left align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
function TableCell({className, ...props}: React.ComponentProps<'td'>) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={merge(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -94,11 +94,11 @@ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
}: React.ComponentProps<'caption'>) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={merge("text-muted-foreground mt-4 text-sm", className)}
|
||||
className={merge('text-muted-foreground mt-4 text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import ResourcesPage from '@/app/admin/resources/page'
|
||||
|
||||
export type User = {
|
||||
id: number
|
||||
admin_id: number
|
||||
@@ -106,3 +104,22 @@ export type Refund = {
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
}
|
||||
|
||||
export type Channel = {
|
||||
id: number
|
||||
user_id: number
|
||||
proxy_id: number
|
||||
node_id: number
|
||||
proxy_host: string
|
||||
proxy_port: number
|
||||
user_host: string
|
||||
node_host: string
|
||||
auth_ip: boolean
|
||||
auth_pass: boolean
|
||||
protocol: number
|
||||
username: string
|
||||
password: string
|
||||
expiration: Date
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user