完善通道创建功能,添加参数验证与格式化输出
This commit is contained in:
24
src/actions/channel.ts
Normal file
24
src/actions/channel.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import {callByUser} from '@/actions/base'
|
||||||
|
|
||||||
|
async function createChannels(params: {
|
||||||
|
resource_id: number
|
||||||
|
protocol: number
|
||||||
|
auth_type: number
|
||||||
|
count: number
|
||||||
|
prov?: string
|
||||||
|
city?: string
|
||||||
|
isp?: string
|
||||||
|
}) {
|
||||||
|
return callByUser<CreateChannelsResp[]>('/api/channel/create', params)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateChannelsResp = {
|
||||||
|
host: string
|
||||||
|
port: string
|
||||||
|
username?: string
|
||||||
|
password?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
createChannels,
|
||||||
|
}
|
||||||
68
src/app/(api)/proxies/route.ts
Normal file
68
src/app/(api)/proxies/route.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import {NextRequest, NextResponse} from 'next/server'
|
||||||
|
import {createChannels} from '@/actions/channel'
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const params = req.nextUrl.searchParams
|
||||||
|
|
||||||
|
const resource_id = params.get('i')
|
||||||
|
if (!resource_id) {
|
||||||
|
throw new Error('需要指定资源ID')
|
||||||
|
}
|
||||||
|
let protocol = params.get('x')
|
||||||
|
if (!protocol) {
|
||||||
|
protocol = '1'
|
||||||
|
}
|
||||||
|
const auth_type = params.get('t')
|
||||||
|
if (!auth_type) {
|
||||||
|
throw new Error('需要指定认证类型')
|
||||||
|
}
|
||||||
|
const count = params.get('n')
|
||||||
|
if (!count) {
|
||||||
|
throw new Error('需要指定通道创建数量')
|
||||||
|
}
|
||||||
|
const prov = params.get('a') || undefined
|
||||||
|
const city = params.get('b') || undefined
|
||||||
|
const isp = params.get('s') || undefined
|
||||||
|
|
||||||
|
const result = await createChannels({
|
||||||
|
resource_id: Number(resource_id),
|
||||||
|
auth_type: Number(auth_type),
|
||||||
|
protocol: Number(protocol),
|
||||||
|
count: Number(count),
|
||||||
|
prov,
|
||||||
|
city,
|
||||||
|
isp,
|
||||||
|
})
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const format = params.get('rt')
|
||||||
|
const rBreaker = params.get('rb') || '13,10'
|
||||||
|
const rSeparator = params.get('rs') || '124'
|
||||||
|
|
||||||
|
const breaker = rBreaker.split(',').map(code => String.fromCharCode(parseInt(code))).join('')
|
||||||
|
const separator = rSeparator.split(',').map(code => String.fromCharCode(parseInt(code))).join('')
|
||||||
|
|
||||||
|
switch (format) {
|
||||||
|
case 'json':
|
||||||
|
const body = JSON.stringify(params)
|
||||||
|
return NextResponse.json(body)
|
||||||
|
case 'text':
|
||||||
|
const text = result.data.map(item => {
|
||||||
|
const list = [item.host, item.port]
|
||||||
|
if (item.username && item.password) {
|
||||||
|
list.push(item.username)
|
||||||
|
list.push(item.password)
|
||||||
|
}
|
||||||
|
return list.join(separator)
|
||||||
|
}).join(breaker)
|
||||||
|
return new NextResponse(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('Error creating channels:', error)
|
||||||
|
return NextResponse.json({error: error})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,8 @@ export type ExtractPageProps = {}
|
|||||||
|
|
||||||
export default async function ExtractPage(props: ExtractPageProps) {
|
export default async function ExtractPage(props: ExtractPageProps) {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page className={`p-0`}>
|
||||||
<Extract/>
|
<Extract className={`p-8`}/>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,32 +8,38 @@ import {Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, Selec
|
|||||||
import {Button} from '@/components/ui/button'
|
import {Button} from '@/components/ui/button'
|
||||||
import {useForm} from 'react-hook-form'
|
import {useForm} from 'react-hook-form'
|
||||||
import {Alert, AlertTitle} from '@/components/ui/alert'
|
import {Alert, AlertTitle} from '@/components/ui/alert'
|
||||||
import {Box, CircleAlert, Loader, Timer} from 'lucide-react'
|
import {Box, CircleAlert, CopyIcon, ExternalLinkIcon, Loader, Timer} from 'lucide-react'
|
||||||
import {useEffect, useState} from 'react'
|
import {useEffect, useMemo, useState} from 'react'
|
||||||
import {useStatus} from '@/lib/states'
|
import {useStatus} from '@/lib/states'
|
||||||
import {allResource} from '@/actions/resource'
|
import {allResource} from '@/actions/resource'
|
||||||
import {Resource, name} from '@/lib/models'
|
import {Resource, name} from '@/lib/models'
|
||||||
import {format, intlFormatDistance} from 'date-fns'
|
import {format, intlFormatDistance} from 'date-fns'
|
||||||
|
import {usePathname} from 'next/navigation'
|
||||||
|
import {toast} from 'sonner'
|
||||||
|
import {merge} from '@/lib/utils'
|
||||||
|
|
||||||
|
|
||||||
type ExtractProps = {}
|
type ExtractProps = {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function Extract(props: ExtractProps) {
|
export default function Extract(props: ExtractProps) {
|
||||||
const [resources, setResources] = useState<Resource[]>([])
|
const [resources, setResources] = useState<Resource[]>([])
|
||||||
const [status, setStatus] = useStatus()
|
const [status, setStatus] = useStatus()
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
resource: z.number().optional(),
|
resource: z.number({required_error: '请选择套餐'}),
|
||||||
prov: z.string().optional(),
|
prov: z.string().optional(),
|
||||||
city: z.string().optional(),
|
city: z.string().optional(),
|
||||||
regionType: z.enum(['unlimited', 'specific']).default('unlimited'),
|
regionType: z.enum(['unlimited', 'specific']).default('unlimited'),
|
||||||
isp: z.enum(['all', '1', '2', '3']),
|
isp: z.enum(['all', '1', '2', '3'], {required_error: '请选择运营商'}),
|
||||||
proto: z.enum(['all', '1', '2', '3']),
|
proto: z.enum(['all', '1', '2', '3'], {required_error: '请选择协议'}),
|
||||||
distinct: z.enum(['1', '0']),
|
authType: z.enum([ '1', '2'], {required_error: '请选择认证方式'}),
|
||||||
format: z.enum(['text', 'json']),
|
distinct: z.enum(['1', '0'], {required_error: '请选择去重选项'}),
|
||||||
separator: z.string(),
|
format: z.enum(['text', 'json'], {required_error: '请选择导出格式'}),
|
||||||
breaker: z.string(),
|
separator: z.string({required_error: '请选择分隔符'}),
|
||||||
count: z.number().min(1),
|
breaker: z.string({required_error: '请选择换行符'}),
|
||||||
|
count: z.number({required_error: '请输入有效的数量'}).min(1),
|
||||||
})
|
})
|
||||||
|
|
||||||
type Schema = z.infer<typeof schema>
|
type Schema = z.infer<typeof schema>
|
||||||
@@ -44,16 +50,46 @@ export default function Extract(props: ExtractProps) {
|
|||||||
regionType: 'unlimited',
|
regionType: 'unlimited',
|
||||||
isp: 'all',
|
isp: 'all',
|
||||||
proto: 'all',
|
proto: 'all',
|
||||||
|
authType: '1',
|
||||||
count: 1,
|
count: 1,
|
||||||
distinct: '1',
|
distinct: '1',
|
||||||
format: 'text',
|
format: 'text',
|
||||||
breaker: '\\n',
|
breaker: '13,10',
|
||||||
separator: '|',
|
separator: '124',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const regionType = form.watch('regionType')
|
const regionType = form.watch('regionType')
|
||||||
|
|
||||||
|
const resource = form.watch('resource')
|
||||||
|
const prov = form.watch('prov')
|
||||||
|
const city = form.watch('city')
|
||||||
|
const isp = form.watch('isp')
|
||||||
|
const proto = form.watch('proto')
|
||||||
|
const authType = form.watch('authType')
|
||||||
|
const distinct = form.watch('distinct')
|
||||||
|
const formatType = form.watch('format')
|
||||||
|
const separator = form.watch('separator')
|
||||||
|
const breaker = form.watch('breaker')
|
||||||
|
const count = form.watch('count')
|
||||||
|
|
||||||
|
const params = useMemo(() => {
|
||||||
|
const sp = new URLSearchParams()
|
||||||
|
if (resource) sp.set('i', String(resource))
|
||||||
|
if (authType) sp.set('t', authType)
|
||||||
|
if (proto != 'all') sp.set('x', proto)
|
||||||
|
if (prov) sp.set('a', prov)
|
||||||
|
if (city) sp.set('b', city)
|
||||||
|
if (isp != 'all') sp.set('s', isp)
|
||||||
|
sp.set('d', distinct)
|
||||||
|
sp.set('rt', formatType)
|
||||||
|
sp.set('rs', separator)
|
||||||
|
sp.set('rb', breaker)
|
||||||
|
sp.set('n', String(count))
|
||||||
|
|
||||||
|
return `/proxies?${sp.toString()}`
|
||||||
|
}, [resource, prov, city, isp, proto, distinct, formatType, separator, breaker, count])
|
||||||
|
|
||||||
const onSubmit = (values: z.infer<typeof schema>) => {
|
const onSubmit = (values: z.infer<typeof schema>) => {
|
||||||
console.log(values)
|
console.log(values)
|
||||||
}
|
}
|
||||||
@@ -82,14 +118,30 @@ export default function Extract(props: ExtractProps) {
|
|||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
className={`p-4 bg-white flex flex-col gap-4 rounded-md`}
|
onError={errors => {
|
||||||
|
const desc: (string | undefined)[] = []
|
||||||
|
Object.entries(errors).forEach(([field, error]) => {
|
||||||
|
if (error.message) {
|
||||||
|
desc.push(error.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
toast.error('请完成填写:', {
|
||||||
|
description: desc.map((msg, i) => (
|
||||||
|
<span key={i}>- {msg}</span>
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
className={merge(
|
||||||
|
`bg-white flex flex-col gap-4 rounded-md`,
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Alert variant={`warn`}>
|
<Alert variant={`warn`}>
|
||||||
<CircleAlert/>
|
<CircleAlert/>
|
||||||
<AlertTitle>提取IP前需要将本机IP添加到白名单后才可使用</AlertTitle>
|
<AlertTitle>提取IP前需要将本机IP添加到白名单后才可使用</AlertTitle>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<div className={`flex flex-col gap-y-6`}>
|
<div className={`flex flex-col gap-4`}>
|
||||||
{/* 选择套餐 */}
|
{/* 选择套餐 */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FormField name="resource" label={`选择套餐`}>
|
<FormField name="resource" label={`选择套餐`}>
|
||||||
@@ -108,9 +160,10 @@ export default function Extract(props: ExtractProps) {
|
|||||||
<span>加载中...</span>
|
<span>加载中...</span>
|
||||||
</div>
|
</div>
|
||||||
) : resources.length === 0 ? (
|
) : resources.length === 0 ? (
|
||||||
<SelectItem value="0">
|
<div className={`p-4 flex gap-1 items-center`}>
|
||||||
暂无可用套餐
|
<Loader className={`animate-spin`} size={20}/>
|
||||||
</SelectItem>
|
<span>暂无可用套餐</span>
|
||||||
|
</div>
|
||||||
) : resources.map((resource, i) => (<>
|
) : resources.map((resource, i) => (<>
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={`${resource.id}`} value={String(resource.id)} className={`p-3`}>
|
key={`${resource.id}`} value={String(resource.id)} className={`p-3`}>
|
||||||
@@ -274,6 +327,27 @@ export default function Extract(props: ExtractProps) {
|
|||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 认证方式 */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormField name="authType" label={`协议类型`}>
|
||||||
|
{({id, field}) => (
|
||||||
|
<RadioGroup
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
className="flex gap-4">
|
||||||
|
<FormLabel htmlFor={`${id}-v-http`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="1" id={`${id}-v-http`} className="mr-2"/>
|
||||||
|
<span>白名单</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel htmlFor={`${id}-v-https`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="2" id={`${id}-v-https`} className="mr-2"/>
|
||||||
|
<span>密码</span>
|
||||||
|
</FormLabel>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 去重选项 */}
|
{/* 去重选项 */}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FormField name="distinct" label={`去重选项`}>
|
<FormField name="distinct" label={`去重选项`}>
|
||||||
@@ -326,15 +400,15 @@ export default function Extract(props: ExtractProps) {
|
|||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
className="flex gap-4">
|
className="flex gap-4">
|
||||||
<FormLabel htmlFor={`${id}-v-comma`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
<FormLabel htmlFor={`${id}-v-comma`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
<RadioGroupItem value="|" id={`${id}-v-comma`} className="mr-2"/>
|
<RadioGroupItem value="124" id={`${id}-v-comma`} className="mr-2"/>
|
||||||
<span>竖线 ( | )</span>
|
<span>竖线 ( | )</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormLabel htmlFor={`${id}-v-semicolon`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
<FormLabel htmlFor={`${id}-v-semicolon`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
<RadioGroupItem value=":" id={`${id}-v-semicolon`} className="mr-2"/>
|
<RadioGroupItem value="58" id={`${id}-v-semicolon`} className="mr-2"/>
|
||||||
<span>冒号 ( : )</span>
|
<span>冒号 ( : )</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormLabel htmlFor={`${id}-v-space`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
<FormLabel htmlFor={`${id}-v-space`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
<RadioGroupItem value="\t" id={`${id}-v-space`} className="mr-2"/>
|
<RadioGroupItem value="9" id={`${id}-v-space`} className="mr-2"/>
|
||||||
<span>制表符 ( \t )</span>
|
<span>制表符 ( \t )</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
@@ -350,18 +424,18 @@ export default function Extract(props: ExtractProps) {
|
|||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
className="flex gap-4">
|
className="flex gap-4">
|
||||||
|
<FormLabel htmlFor={`${id}-v-newline2`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
|
<RadioGroupItem value="13,10" id={`${id}-v-newline2`} className="mr-2"/>
|
||||||
|
<span>回车换行 ( \r\n )</span>
|
||||||
|
</FormLabel>
|
||||||
<FormLabel htmlFor={`${id}-v-newline`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
<FormLabel htmlFor={`${id}-v-newline`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
<RadioGroupItem value="\n" id={`${id}-v-newline`} className="mr-2"/>
|
<RadioGroupItem value="10" id={`${id}-v-newline`} className="mr-2"/>
|
||||||
<span>换行 ( \n )</span>
|
<span>换行 ( \n )</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormLabel htmlFor={`${id}-v-newline3`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
<FormLabel htmlFor={`${id}-v-newline3`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
||||||
<RadioGroupItem value="\r" id={`${id}-v-newline3`} className="mr-2"/>
|
<RadioGroupItem value="13" id={`${id}-v-newline3`} className="mr-2"/>
|
||||||
<span>回车 ( \r )</span>
|
<span>回车 ( \r )</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormLabel htmlFor={`${id}-v-newline2`} className={`px-3 h-10 border rounded-md flex items-center w-40 text-sm`}>
|
|
||||||
<RadioGroupItem value="\r\n" id={`${id}-v-newline2`} className="mr-2"/>
|
|
||||||
<span>回车换行 ( \r\n )</span>
|
|
||||||
</FormLabel>
|
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
)}
|
)}
|
||||||
</FormField>
|
</FormField>
|
||||||
@@ -384,8 +458,38 @@ export default function Extract(props: ExtractProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex mt-6 justify-center">
|
<div className={merge(
|
||||||
<Button type="submit" className="w-40 h-10 bg-blue-500 text-white rounded-md">生成链接</Button>
|
`flex flex-col gap-4 sticky bottom-0 bg-white py-4`,
|
||||||
|
`border-t`,
|
||||||
|
)}>
|
||||||
|
{/* 展示链接地址 */}
|
||||||
|
<div className={`bg-card text-card-foreground p-4 rounded-md`}>
|
||||||
|
{params}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 操作 */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={async () => {
|
||||||
|
const url = new URL(window.location.href).origin
|
||||||
|
await navigator.clipboard.writeText(`${url}${params}`)
|
||||||
|
toast.success('链接已复制到剪贴板')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon/>
|
||||||
|
<span>复制链接</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
onClick={async () => {
|
||||||
|
window.open(params, '_blank')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExternalLinkIcon/>
|
||||||
|
<span>打开链接</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,29 +8,30 @@ import {
|
|||||||
ControllerProps,
|
ControllerProps,
|
||||||
SubmitHandler,
|
SubmitHandler,
|
||||||
FieldValues, useFormContext, FieldPath, UseFormReturn, ControllerRenderProps,
|
FieldValues, useFormContext, FieldPath, UseFormReturn, ControllerRenderProps,
|
||||||
ControllerFieldState, UseFormStateReturn, FieldError,
|
ControllerFieldState, UseFormStateReturn, FieldError, FieldErrors, SubmitErrorHandler,
|
||||||
} from 'react-hook-form'
|
} from 'react-hook-form'
|
||||||
|
|
||||||
import {merge} from '@/lib/utils'
|
import {merge} from '@/lib/utils'
|
||||||
import {Label} from '@/components/ui/label'
|
import {Label} from '@/components/ui/label'
|
||||||
|
|
||||||
import {ComponentProps, createContext, ReactNode, useContext, useId} from 'react'
|
import {BaseSyntheticEvent, ComponentProps, createContext, ReactNode, useContext, useId} from 'react'
|
||||||
|
|
||||||
type FormProps<T extends FieldValues> = {
|
type FormProps<T extends FieldValues> = {
|
||||||
form: UseFormReturn<T>
|
form: UseFormReturn<T>
|
||||||
onSubmit: SubmitHandler<T>
|
onSubmit: SubmitHandler<T>
|
||||||
} & Omit<ComponentProps<'form'>, 'onSubmit'>
|
onError?: SubmitErrorHandler<T>
|
||||||
|
} & Omit<ComponentProps<'form'>, 'onSubmit' | 'onError'>
|
||||||
|
|
||||||
function Form<T extends FieldValues>(rawProps: FormProps<T>) {
|
function Form<T extends FieldValues>(rawProps: FormProps<T>) {
|
||||||
|
|
||||||
const {children, onSubmit, ...props} = rawProps
|
const {children, onSubmit, onError, ...props} = rawProps
|
||||||
const form = props.form
|
const form = props.form
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<form {...props} onSubmit={event => {
|
<form {...props} onSubmit={event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
form.handleSubmit(onSubmit)(event)
|
form.handleSubmit(onSubmit, onError)(event)
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Reference in New Issue
Block a user