'use client' import {z} from 'zod' import {zodResolver} from '@hookform/resolvers/zod' import {Form, FormField, FormLabel} from '@/components/ui/form' import {RadioGroup, RadioGroupItem} from '@/components/ui/radio-group' import {Input} from '@/components/ui/input' import {Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, SelectValue} from '@/components/ui/select' import {Button} from '@/components/ui/button' import {useForm, useFormContext, useWatch} from 'react-hook-form' import {Alert, AlertTitle} from '@/components/ui/alert' import {ArrowRight, Box, CircleAlert, CopyIcon, ExternalLinkIcon, LinkIcon, Loader, Plus, Timer} from 'lucide-react' import {memo, ReactNode, useEffect, useRef, useState} from 'react' import {useStatus} from '@/lib/states' import {allResource} from '@/actions/resource' import {Resource} from '@/lib/models' import {format, intlFormatDistance} from 'date-fns' import {toast} from 'sonner' import {merge} from '@/lib/utils' import {Combobox} from '@/components/ui/combobox' import cities from './_assets/cities.json' import ExtractDocs from '@/app/(home)/docs/(product)/api-docs/page.md' import Link from 'next/link' import {useProfileStore} from '@/components/stores/profile' const schema = z.object({ 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'], {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 type ExtractProps = { className?: string } export default function Extract(props: ExtractProps) { const form = useForm({ resolver: zodResolver(schema), defaultValues: { regionType: 'unlimited', isp: 'all', proto: 'all', authType: '1', count: 1, distinct: '1', format: 'text', breaker: '13,10', separator: '124', }, }) // ====================== // render // ====================== return (
提取IP前需要将本机IP添加到白名单后才可使用 添加白名单
) } function CardSection(props: { children: ReactNode }) { return (
{props.children}
) } const FormFields = memo(() => { return (
{/* 选择套餐 */} {/* 地区筛选 */} {/* 运营商筛选 */} {({id, field}) => ( 不限 电信 联通 移动 )} {/* 协议类型 */} {({id, field}) => ( 不限 HTTP HTTPS SOCKS5 )} {/* 认证方式 */} {({id, field}) => ( 白名单 密码 )} {/* 去重选项 */} {({id, field}) => ( 去重 不去重 )} {/* 导出格式 */} {({id, field}) => ( TXT 格式 JSON 格式 )} {/* 分隔符 */} {({id, field}) => ( 竖线 ( | ) 冒号 ( : ) 制表符 (\t) )} {/* 换行符 */} {({id, field}) => ( 回车换行 (\r\n) 换行 (\n) 回车 (\r) )} {/* 提取数量 */} {({id, field}) => (
{/* 减号按钮:移动端显示,步长1,最小1 */} {/* 数量输入框 */} { const value = Math.max(1, Number(e.target.value) || 1) field.onChange(value) }} /> {/* 加号按钮:移动端显示,步长1 */}
)}
) }) FormFields.displayName = 'FormFields' function SelectResource() { const [resources, setResources] = useState([]) const [status, setStatus] = useStatus() const profile = useProfileStore(state => state.profile) const getResources = async () => { setStatus('load') try { const resp = await allResource() if (!resp.success) { throw new Error('获取套餐失败,请稍后再试') } setResources(resp.data ?? []) setStatus('done') } catch (error) { toast.error((error as Error).message) setStatus('fail') } } useEffect(() => { getResources().then() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( {({field}) => ( )} ) } function SelectRegion() { const {control, setValue} = useFormContext() const regionType = useWatch({control, name: 'regionType'}) const prov = useWatch({control, name: 'prov'}) const city = useWatch({control, name: 'city'}) return (
{({id, field}) => ( { field.onChange(e) if (e === 'unlimited') { setValue('prov', '') setValue('city', '') } }} defaultValue={field.value} className="flex gap-4" > 不限地区 指定地区 )} {regionType === 'specific' && ( { setValue('prov', value[0]) setValue('city', value[1]) }} /> )}
) } function ApplyLink() { const form = useFormContext() useWatch() // let type: 'open' | 'copy' = 'open' const type = useRef<'open' | 'copy'>('open') const handler = form.handleSubmit( // eslint-disable-next-line react-hooks/refs async (values: z.infer) => { const params = link(values) switch (type.current) { case 'copy': const url = new URL(window.location.href).origin const text = `${url}${params}` // 使用 clipboard API 复制链接 let copied = false try { await navigator.clipboard.writeText(text) copied = true } catch (e) { console.log('剪贴板 API 调用失败,尝试备选方案') } // 使用 document.execCommand 作为备选方案 if (!copied) { const textarea = document.createElement('textarea') textarea.value = text document.body.appendChild(textarea) textarea.select() document.execCommand('copy') document.body.removeChild(textarea) } toast.success('链接已复制到剪贴板') break case 'open': window.open(params, '_blank') break } }, (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) => ( - {msg} )), }) }, ) const submit = (t: 'open' | 'copy') => { type.current = t handler() } return (

API 链接

{/* 展示链接地址 */}
{link(form.getValues())}
{/* 操作 */}
) } function link(values: Schema) { const {resource, prov, city, isp, proto, authType, distinct, format: formatType, separator, breaker, count} = values 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()}` } function name(resource: Resource) { switch (resource.type) { case 1: // 短效套餐 switch (resource.short.type) { case 1: return `短效包时 ${resource.short.live / 60} 分钟` case 2: return `短效包量 ${resource.short.live / 60} 分钟` } break case 2: // 长效套餐 switch (resource.long.type) { case 1: return `长效包时 ${resource.long.live} 小时` case 2: return `长效包量 ${resource.long.live} 小时` } break } }