增加客户端标识 & 套餐管理重置今日限额字段 &提取IP/IP管理模块新增字段

This commit is contained in:
Eamon-meng
2025-12-12 14:25:27 +08:00
parent ee7433e320
commit 26ea796b4d
9 changed files with 120 additions and 51 deletions

View File

@@ -40,14 +40,14 @@ export async function createResource(props: {
live: number live: number
mode: number mode: number
quota: number quota: number
expire: number expire_at: number
daily_limit: number daily_limit: number
} }
long?: { long?: {
live: number live: number
mode: number mode: number
quota: number quota: number
expire: number expire_at: number
daily_limit: number daily_limit: number
} }
}) { }) {
@@ -60,14 +60,14 @@ export async function prepareResource(props: {
live: number live: number
mode: number mode: number
quota: number quota: number
expire: number expire_at: number
daily_limit: number daily_limit: number
} }
long?: { long?: {
live: number live: number
mode: number mode: number
quota: number quota: number
expire: number expire_at: number
daily_limit: number daily_limit: number
} }
payment_method: number payment_method: number

View File

@@ -1,3 +1,4 @@
'use client'
import {useEffect, useState} from 'react' import {useEffect, useState} from 'react'
import {Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger} from '@/components/ui/dialog' import {Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger} from '@/components/ui/dialog'
import {Button} from '@/components/ui/button' import {Button} from '@/components/ui/button'

View File

@@ -16,7 +16,6 @@ import {useRouter} from 'next/navigation'
import {login} from '@/actions/auth' import {login} from '@/actions/auth'
import {useProfileStore} from '@/components/stores-provider' import {useProfileStore} from '@/components/stores-provider'
import Captcha from './captcha' import Captcha from './captcha'
import {merge} from '@/lib/utils'
const smsSchema = zod.object({ const smsSchema = zod.object({
username: zod.string().length(11, '请输入正确的手机号码'), username: zod.string().length(11, '请输入正确的手机号码'),
@@ -99,7 +98,7 @@ export default function LoginCard(props: {
{...field} {...field}
id={id} id={id}
type="tel" type="tel"
placeholder={mode === 'phone_code' ? '请输入手机号' : '请输入用户名'} placeholder={mode === 'phone_code' ? '请输入手机号' : '请输入用户名/手机号/邮箱'}
autoComplete="tel-national" autoComplete="tel-national"
/> />
)} )}
@@ -170,9 +169,9 @@ export default function LoginCard(props: {
</Button> </Button>
<p className="text-xs text-center text-gray-500"> <p className="text-xs text-center text-gray-500">
<a href="#" className="text-blue-600 hover:text-blue-500"></a> <a href="/userAgreement" className="text-blue-600 hover:text-blue-500"></a>
<a href="#" className="text-blue-600 hover:text-blue-500"></a> <a href="/privacyPolicy" className="text-blue-600 hover:text-blue-500"></a>
</p> </p>
</div> </div>
</Form> </Form>

View File

@@ -1,3 +1,4 @@
'use client'
import {createContext} from 'react' import {createContext} from 'react'
import Image, {StaticImageData} from 'next/image' import Image, {StaticImageData} from 'next/image'

View File

@@ -1,3 +1,4 @@
'use client'
import Wrap from '@/components/wrap' import Wrap from '@/components/wrap'
import s01 from '@/assets/header/solution/01.svg' import s01 from '@/assets/header/solution/01.svg'
import s02 from '@/assets/header/solution/02.svg' import s02 from '@/assets/header/solution/02.svg'

View File

@@ -7,7 +7,7 @@ import Page from '@/components/page'
import DataTable from '@/components/data-table' import DataTable from '@/components/data-table'
import {toast} from 'sonner' import {toast} from 'sonner'
import {listChannels} from '@/actions/channel' import {listChannels} from '@/actions/channel'
import {format} from 'date-fns' import {format, isBefore} from 'date-fns'
import {Form, FormField} from '@/components/ui/form' import {Form, FormField} from '@/components/ui/form'
import {z} from 'zod' import {z} from 'zod'
import {useForm} from 'react-hook-form' import {useForm} from 'react-hook-form'
@@ -16,7 +16,7 @@ import DatePicker from '@/components/date-picker'
import {Button} from '@/components/ui/button' import {Button} from '@/components/ui/button'
import {EraserIcon, SearchIcon} from 'lucide-react' import {EraserIcon, SearchIcon} from 'lucide-react'
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select' import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select'
import {Badge} from '@/components/ui/badge'
export type ChannelsPageProps = {} export type ChannelsPageProps = {}
export default function ChannelsPage(props: ChannelsPageProps) { export default function ChannelsPage(props: ChannelsPageProps) {
@@ -32,6 +32,12 @@ export default function ChannelsPage(props: ChannelsPageProps) {
list: [], list: [],
}) })
// 检查是否过期
const isExpired = (expiredAt: string | Date) => {
const date = typeof expiredAt === 'string' ? new Date(expiredAt) : expiredAt
return isBefore(date, new Date())
}
const refresh = async (page: number, size: number) => { const refresh = async (page: number, size: number) => {
try { try {
setStatus('load') setStatus('load')
@@ -39,6 +45,7 @@ export default function ChannelsPage(props: ChannelsPageProps) {
// 筛选条件 // 筛选条件
const filter = filterForm.getValues() const filter = filterForm.getValues()
const auth_type = filter.auth_type ? parseInt(filter.auth_type) : undefined const auth_type = filter.auth_type ? parseInt(filter.auth_type) : undefined
const expired_status = filter.expired_status
// 请求数据 // 请求数据
console.log({ console.log({
@@ -47,12 +54,26 @@ export default function ChannelsPage(props: ChannelsPageProps) {
const resp = await listChannels({ const resp = await listChannels({
page, size, ...filter, auth_type, page, size, ...filter, auth_type,
}) })
console.log(resp, 'ip管理的respresprespresp')
if (!resp.success) { if (!resp.success) {
throw new Error(resp.message) 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
}
// 更新数据 // 更新数据
setData(resp.data) setData({
...resp.data,
list: filteredList,
})
setStatus('done') setStatus('done')
} }
catch (e) { catch (e) {
@@ -74,6 +95,7 @@ export default function ChannelsPage(props: ChannelsPageProps) {
const filterSchema = z.object({ const filterSchema = z.object({
auth_type: z.enum(['0', '1', '2']), auth_type: z.enum(['0', '1', '2']),
expired_status: z.enum(['all', 'active']).default('all'),
expire_after: z.date().optional(), expire_after: z.date().optional(),
expire_before: z.date().optional(), expire_before: z.date().optional(),
}) })
@@ -83,12 +105,13 @@ export default function ChannelsPage(props: ChannelsPageProps) {
resolver: zodResolver(filterSchema), resolver: zodResolver(filterSchema),
defaultValues: { defaultValues: {
auth_type: '0', auth_type: '0',
expired_status: 'all',
expire_after: undefined, expire_after: undefined,
expire_before: undefined, expire_before: undefined,
}, },
}) })
const filterHandler = filterForm.handleSubmit(async (value) => { const filterHandler = filterForm.handleSubmit(async (value) => {
await refresh(data.page, data.size) await refresh(1, data.size)
}) })
// ====================== // ======================
@@ -100,6 +123,20 @@ export default function ChannelsPage(props: ChannelsPageProps) {
<section className="flex justify-between"> <section className="flex justify-between">
<div></div> <div></div>
<Form form={filterForm} handler={filterHandler} className="flex-auto flex flex-wrap gap-4 items-end"> <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>}> <FormField<FilterSchema, 'auth_type'> name="auth_type" label={<span className="text-sm"></span>}>
{({field}) => ( {({field}) => (
<Select value={field.value} onValueChange={field.onChange}> <Select value={field.value} onValueChange={field.onChange}>
@@ -136,7 +173,13 @@ export default function ChannelsPage(props: ChannelsPageProps) {
<SearchIcon/> <SearchIcon/>
</Button> </Button>
<Button theme="outline" className="h-9" onClick={() => filterForm.reset()}> <Button
theme="outline"
className="h-9"
onClick={() => {
filterForm.reset()
refresh(1, data.size)
}}>
<EraserIcon/> <EraserIcon/>
</Button> </Button>
@@ -155,11 +198,35 @@ export default function ChannelsPage(props: ChannelsPageProps) {
}} }}
columns={[ columns={[
{ {
header: '代理地址', cell: ({row}) => { header: '批次号',
cell: ({row}) => {
const channel = row.original const channel = row.original
const ip = channel.proxy?.ip const expired = isExpired(channel.expired_at)
return (
<div className={`${expired ? 'text-weak' : ''}`}>
<span>{row.original.batch_no}</span>
</div>
)
},
},
{
header: '代理地址',
cell: ({row}) => {
const channel = row.original
const ip = channel.host
const port = channel.port const port = channel.port
return `${ip}:${port}` const expired = isExpired(channel.expired_at)
return (
<div className={`${expired ? 'text-weak' : ''}`}>
<span>{ip}:{port}</span>
{expired && (
<Badge variant="secondary">
</Badge>
)}
</div>
)
}, },
}, },
{ {
@@ -176,12 +243,9 @@ export default function ChannelsPage(props: ChannelsPageProps) {
<span ></span> <span ></span>
<div className="flex flex-wrap gap-1 max-w-[200px]"> <div className="flex flex-wrap gap-1 max-w-[200px]">
{channel.whitelists.split(',').map((ip, index) => ( {channel.whitelists.split(',').map((ip, index) => (
<span <Badge key={index} variant="secondary">
key={index}
className="inline-block px-2 py-0.5 bg-gray-100 rounded text-xs text-gray-700 break-all"
>
{ip.trim()} {ip.trim()}
</span> </Badge >
))} ))}
</div> </div>
</div> </div>
@@ -200,10 +264,12 @@ export default function ChannelsPage(props: ChannelsPageProps) {
}, },
}, },
{ {
header: '过期时间', cell: ({row}) => format(row.original.expired_at, 'yyyy-MM-dd HH:mm:ss'), header: '提取时间',
cell: ({row}) => format(row.original.created_at, 'yyyy-MM-dd HH:mm'),
}, },
{ {
header: '操作', cell: ({row}) => <span>-</span>, header: '过期时间',
cell: ({row}) => format(row.original.expired_at, 'yyyy-MM-dd HH:mm:ss'),
}, },
]} ]}
/> />

View File

@@ -265,20 +265,19 @@ export default function LongResource(props: LongResourceProps) {
<div className="flex gap-1"> <div className="flex gap-1">
{row.original.long.type === 1 ? ( {row.original.long.type === 1 ? (
<div className="flex gap-1"> <div className="flex gap-1">
{isAfter(row.original.long.expire, new Date()) {isAfter(row.original.long.expire_at, new Date())
? <span className="text-green-500"></span> ? <span className="text-green-500"></span>
: <span className="text-red-500"></span>} : <span className="text-red-500"></span>}
<span>|</span> <span>|</span>
<span> <span>
{row.original.long.last_at
{row.original.long.daily_used} && new Date(row.original.long.last_at).toDateString() === new Date().toDateString()
{' '} ? row.original.long.daily
/ : 0}/{row.original.long.quota}
{row.original.long.daily_limit}
</span> </span>
<span>|</span> <span>|</span>
<span> <span>
{intlFormatDistance(row.original.long.expire, new Date())} {intlFormatDistance(row.original.long.expire_at, new Date())}
{' '} {' '}
</span> </span>
@@ -304,12 +303,14 @@ export default function LongResource(props: LongResourceProps) {
), ),
}, },
{ {
accessorKey: 'daily_last', header: '最近使用时间', cell: ({row}) => { accessorKey: 'last_at',
return ( header: '最近使用时间',
format(row.original.long.daily_last, 'yyyy-MM-dd') === '0001-01-01' cell: ({row}) => {
? '-' const lastAt = row.original.long.last_at
: format(row.original.long.daily_last, 'yyyy-MM-dd HH:mm') if (!lastAt) {
) return '暂未使用'
}
return format(lastAt, 'yyyy-MM-dd HH:mm')
}, },
}, },
{ {

View File

@@ -265,22 +265,19 @@ export default function ShortResource(props: ShortResourceProps) {
<div className="flex gap-1"> <div className="flex gap-1">
{row.original.short.type === 1 ? ( {row.original.short.type === 1 ? (
<div className="flex gap-1"> <div className="flex gap-1">
{isAfter(row.original.short.expire, new Date()) {isAfter(row.original.short.expire_at, new Date())
? <span className="text-green-500"></span> ? <span className="text-green-500"></span>
: <span className="text-red-500"></span>} : <span className="text-red-500"></span>}
<span>|</span> <span>|</span>
<span> <span>
{row.original.short.last_at
{row.original.short.daily_used} && new Date(row.original.short.last_at).toDateString() === new Date().toDateString()
{' '} ? row.original.short.daily
/ : 0}/{row.original.short.quota}
{row.original.short.daily_limit}
</span> </span>
<span>|</span> <span>|</span>
<span> <span>
{intlFormatDistance(row.original.short.expire, new Date())} {intlFormatDistance(row.original.short.expire_at, new Date())}
{' '}
</span> </span>
</div> </div>
) : row.original.short.type === 2 ? ( ) : row.original.short.type === 2 ? (
@@ -304,12 +301,14 @@ export default function ShortResource(props: ShortResourceProps) {
), ),
}, },
{ {
accessorKey: 'daily_last', header: '最近使用时间', cell: ({row}) => { accessorKey: 'last_at',
return ( header: '最近使用时间',
format(row.original.short.daily_last, 'yyyy-MM-dd') === '0001-01-01' cell: ({row}) => {
? '-' const lastAt = row.original.short.last_at
: format(row.original.short.daily_last, 'yyyy-MM-dd HH:mm') if (!lastAt) {
) return '暂未使用'
}
return format(lastAt, 'yyyy-MM-dd HH:mm')
}, },
}, },
{ {

View File

@@ -1,3 +1,4 @@
'use client'
import {StaticImageData} from 'next/image' import {StaticImageData} from 'next/image'
import {useEffect, useState} from 'react' import {useEffect, useState} from 'react'
import wechat from '@/components/composites/purchase/_assets/wechat.svg' import wechat from '@/components/composites/purchase/_assets/wechat.svg'