'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} from 'react-hook-form' import {Alert, AlertTitle} from '@/components/ui/alert' import {Box, CircleAlert, CopyIcon, ExternalLinkIcon, Loader, Timer} from 'lucide-react' import {useEffect, useMemo, useRef, 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 {toast} from 'sonner' import {merge} from '@/lib/utils' import {Combobox} from '@/components/ui/combobox' import cities from './_assets/cities.json' 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({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 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', }, }) 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, authType, proto, isp, distinct, formatType, separator, breaker, count, prov, city]) const type = useRef<'copy' | 'open'>('open') const onSubmit = async (values: z.infer) => { 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 } } const getResources = async () => { setStatus('load') try { const resp = await allResource() if (!resp.success) { throw new Error('Unable to fetch packages.') } setResources(resp.data) setStatus('done') } catch (error) { console.error('Error fetching packages:', error) setStatus('fail') } } useEffect(() => { getResources().then() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // ====================== // render // ====================== return (
{ 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添加到白名单后才可使用
{/* 选择套餐 */}
{({field}) => ( )}
{/* 地区筛选 */}
{({id, field}) => ( { field.onChange(e) if (e === 'unlimited') { form.setValue('prov', '') form.setValue('city', '') } }} defaultValue={field.value} className="flex gap-4" > 不限地区 指定地区 )} {regionType === 'specific' && ( { form.setValue('prov', value[0]) form.setValue('city', value[1]) }} /> )}
{/* 运营商筛选 */}
{({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}) => ( field.onChange(Number(e.target.value))} className="h-10 w-84" placeholder="输入提取数量" /> )}
{/* 展示链接地址 */}
{params}
{/* 操作 */}
) }