增加客户端标识 & 套餐管理重置今日限额字段 &提取IP/IP管理模块新增字段
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user