From 4315c8eba93e33abd32617e1a0092bbcae1810f0 Mon Sep 17 00:00:00 2001 From: luorijun Date: Mon, 14 Apr 2025 16:00:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E9=80=9A=E9=81=93=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E9=AA=8C=E8=AF=81=E4=B8=8E=E6=A0=BC=E5=BC=8F=E5=8C=96?= =?UTF-8?q?=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/actions/channel.ts | 24 +++ src/app/(api)/proxies/route.ts | 68 ++++++++ src/app/admin/extract/page.tsx | 4 +- src/components/composites/extract/index.tsx | 162 ++++++++++++++++---- src/components/ui/form.tsx | 11 +- 5 files changed, 233 insertions(+), 36 deletions(-) create mode 100644 src/actions/channel.ts create mode 100644 src/app/(api)/proxies/route.ts diff --git a/src/actions/channel.ts b/src/actions/channel.ts new file mode 100644 index 0000000..bbb6187 --- /dev/null +++ b/src/actions/channel.ts @@ -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('/api/channel/create', params) +} + +type CreateChannelsResp = { + host: string + port: string + username?: string + password?: string +} + +export { + createChannels, +} diff --git a/src/app/(api)/proxies/route.ts b/src/app/(api)/proxies/route.ts new file mode 100644 index 0000000..312fbb5 --- /dev/null +++ b/src/app/(api)/proxies/route.ts @@ -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}) + } +} diff --git a/src/app/admin/extract/page.tsx b/src/app/admin/extract/page.tsx index 94d1e6a..85785e1 100644 --- a/src/app/admin/extract/page.tsx +++ b/src/app/admin/extract/page.tsx @@ -5,8 +5,8 @@ export type ExtractPageProps = {} export default async function ExtractPage(props: ExtractPageProps) { return ( - - + + ) } diff --git a/src/components/composites/extract/index.tsx b/src/components/composites/extract/index.tsx index deccb20..b3f055e 100644 --- a/src/components/composites/extract/index.tsx +++ b/src/components/composites/extract/index.tsx @@ -8,32 +8,38 @@ import {Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, Selec import {Button} from '@/components/ui/button' import {useForm} from 'react-hook-form' import {Alert, AlertTitle} from '@/components/ui/alert' -import {Box, CircleAlert, Loader, Timer} from 'lucide-react' -import {useEffect, useState} from 'react' +import {Box, CircleAlert, CopyIcon, ExternalLinkIcon, Loader, Timer} from 'lucide-react' +import {useEffect, useMemo, useState} from 'react' import {useStatus} from '@/lib/states' import {allResource} from '@/actions/resource' import {Resource, name} from '@/lib/models' 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) { const [resources, setResources] = useState([]) const [status, setStatus] = useStatus() const schema = z.object({ - resource: z.number().optional(), + resource: z.number({required_error: '请选择套餐'}), prov: z.string().optional(), city: z.string().optional(), regionType: z.enum(['unlimited', 'specific']).default('unlimited'), - isp: z.enum(['all', '1', '2', '3']), - proto: z.enum(['all', '1', '2', '3']), - distinct: z.enum(['1', '0']), - format: z.enum(['text', 'json']), - separator: z.string(), - breaker: z.string(), - count: z.number().min(1), + isp: z.enum(['all', '1', '2', '3'], {required_error: '请选择运营商'}), + proto: z.enum(['all', '1', '2', '3'], {required_error: '请选择协议'}), + authType: z.enum([ '1', '2'], {required_error: '请选择认证方式'}), + distinct: z.enum(['1', '0'], {required_error: '请选择去重选项'}), + format: z.enum(['text', 'json'], {required_error: '请选择导出格式'}), + separator: z.string({required_error: '请选择分隔符'}), + breaker: z.string({required_error: '请选择换行符'}), + count: z.number({required_error: '请输入有效的数量'}).min(1), }) type Schema = z.infer @@ -44,16 +50,46 @@ export default function Extract(props: ExtractProps) { regionType: 'unlimited', isp: 'all', proto: 'all', + authType: '1', count: 1, distinct: '1', format: 'text', - breaker: '\\n', - separator: '|', + breaker: '13,10', + separator: '124', }, }) 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) => { console.log(values) } @@ -82,14 +118,30 @@ export default function Extract(props: ExtractProps) {
{ + const desc: (string | undefined)[] = [] + Object.entries(errors).forEach(([field, error]) => { + if (error.message) { + desc.push(error.message) + } + }) + toast.error('请完成填写:', { + description: desc.map((msg, i) => ( + - {msg} + )), + }) + }} + className={merge( + `bg-white flex flex-col gap-4 rounded-md`, + props.className, + )} > 提取IP前需要将本机IP添加到白名单后才可使用 -
+
{/* 选择套餐 */}
@@ -108,9 +160,10 @@ export default function Extract(props: ExtractProps) { 加载中...
) : resources.length === 0 ? ( - - 暂无可用套餐 - +
+ + 暂无可用套餐 +
) : resources.map((resource, i) => (<> @@ -274,6 +327,27 @@ export default function Extract(props: ExtractProps) {
+ {/* 认证方式 */} +
+ + {({id, field}) => ( + + + + 白名单 + + + + 密码 + + + )} + +
+ {/* 去重选项 */}
@@ -326,15 +400,15 @@ export default function Extract(props: ExtractProps) { defaultValue={field.value} className="flex gap-4"> - + 竖线 ( | ) - + 冒号 ( : ) - + 制表符 ( \t ) @@ -350,18 +424,18 @@ export default function Extract(props: ExtractProps) { onValueChange={field.onChange} defaultValue={field.value} className="flex gap-4"> + + + 回车换行 ( \r\n ) + - + 换行 ( \n ) - + 回车 ( \r ) - - - 回车换行 ( \r\n ) - )} @@ -384,8 +458,38 @@ export default function Extract(props: ExtractProps) {
-
- +
+ {/* 展示链接地址 */} +
+ {params} +
+ + {/* 操作 */} +
+ + +
) diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx index fb87524..efde24e 100644 --- a/src/components/ui/form.tsx +++ b/src/components/ui/form.tsx @@ -8,29 +8,30 @@ import { ControllerProps, SubmitHandler, FieldValues, useFormContext, FieldPath, UseFormReturn, ControllerRenderProps, - ControllerFieldState, UseFormStateReturn, FieldError, + ControllerFieldState, UseFormStateReturn, FieldError, FieldErrors, SubmitErrorHandler, } from 'react-hook-form' import {merge} from '@/lib/utils' 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 = { form: UseFormReturn onSubmit: SubmitHandler -} & Omit, 'onSubmit'> + onError?: SubmitErrorHandler +} & Omit, 'onSubmit' | 'onError'> function Form(rawProps: FormProps) { - const {children, onSubmit, ...props} = rawProps + const {children, onSubmit, onError, ...props} = rawProps const form = props.form return (
{ event.preventDefault() - form.handleSubmit(onSubmit)(event) + form.handleSubmit(onSubmit, onError)(event) event.stopPropagation() }}> {children}