656 lines
24 KiB
TypeScript
656 lines
24 KiB
TypeScript
'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} from 'react-hook-form'
|
||
import {Alert, AlertTitle} from '@/components/ui/alert'
|
||
import {Box, CircleAlert, CopyIcon, ExternalLinkIcon, 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 '@/components/docs/extract.mdx'
|
||
import Markdown from '@/components/markdown'
|
||
import Link from 'next/link'
|
||
import {useProfileStore} from '@/components/stores-provider'
|
||
|
||
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<typeof schema>
|
||
|
||
type ExtractProps = {
|
||
className?: string
|
||
}
|
||
|
||
export default function Extract(props: ExtractProps) {
|
||
const form = useForm<Schema>({
|
||
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 (
|
||
<Form
|
||
form={form}
|
||
className={merge(
|
||
`flex flex-col gap-6`,
|
||
)}
|
||
>
|
||
<CardSection>
|
||
<Alert variant="warn" className="flex items-center">
|
||
<CircleAlert/>
|
||
<AlertTitle className="flex">提取IP前需要将本机IP添加到白名单后才可使用</AlertTitle>
|
||
<Link href="/admin/whitelist">
|
||
<Button ><Plus/>添加白名单</Button>
|
||
</Link>
|
||
</Alert>
|
||
|
||
<FormFields/>
|
||
</CardSection>
|
||
|
||
<CardSection>
|
||
<ApplyLink/>
|
||
</CardSection>
|
||
|
||
<CardSection>
|
||
<Markdown>
|
||
<ExtractDocs/>
|
||
</Markdown>
|
||
</CardSection>
|
||
</Form>
|
||
)
|
||
}
|
||
|
||
function CardSection(props: {
|
||
children: ReactNode
|
||
}) {
|
||
return (
|
||
<div className="flex flex-col gap-4 p-4 md:p-6 bg-white rounded-lg">
|
||
{props.children}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const FormFields = memo(() => {
|
||
return (
|
||
<div className="flex flex-col gap-6 items-stretch max-w-[calc(160px*4+1rem*3)]">
|
||
{/* 选择套餐 */}
|
||
<SelectResource/>
|
||
|
||
{/* 地区筛选 */}
|
||
<SelectRegion/>
|
||
|
||
{/* 运营商筛选 */}
|
||
<FormField name="isp" label="运营商筛选" classNames={{label: 'max-md:text-sm'}}>
|
||
{({id, field}) => (
|
||
<RadioGroup
|
||
onValueChange={field.onChange}
|
||
defaultValue={field.value}
|
||
className="grid grid-cols-2 md:grid-cols-4 gap-4"
|
||
>
|
||
<FormLabel htmlFor={`${id}-v-all`} className="px-3 h-10 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="all" id={`${id}-v-all`}/>
|
||
<span>不限</span>
|
||
</FormLabel>
|
||
<FormLabel htmlFor={`${id}-v-telecom`} className="px-3 h-10 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="1" id={`${id}-v-telecom`}/>
|
||
<span>电信</span>
|
||
</FormLabel>
|
||
<FormLabel htmlFor={`${id}-v-mobile`} className="px-3 h-10 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="2" id={`${id}-v-mobile`}/>
|
||
<span>联通</span>
|
||
</FormLabel>
|
||
<FormLabel htmlFor={`${id}-v-unicom`} className="px-3 h-10 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="3" id={`${id}-v-unicom`}/>
|
||
<span>移动</span>
|
||
</FormLabel>
|
||
</RadioGroup>
|
||
)}
|
||
</FormField>
|
||
|
||
{/* 协议类型 */}
|
||
<FormField name="proto" label="协议类型" classNames={{label: 'max-md:text-sm'}}>
|
||
{({id, field}) => (
|
||
<RadioGroup
|
||
onValueChange={field.onChange}
|
||
defaultValue={field.value}
|
||
className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||
<FormLabel htmlFor={`${id}-v-all`} className="px-3 h-10 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="all" id={`${id}-v-all`} className="mr-2"/>
|
||
<span>不限</span>
|
||
</FormLabel>
|
||
<FormLabel htmlFor={`${id}-v-http`} className="px-3 h-10 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="1" id={`${id}-v-http`} className="mr-2"/>
|
||
<span>HTTP</span>
|
||
</FormLabel>
|
||
<FormLabel htmlFor={`${id}-v-https`} className="px-3 h-10 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="2" id={`${id}-v-https`} className="mr-2"/>
|
||
<span>HTTPS</span>
|
||
</FormLabel>
|
||
<FormLabel htmlFor={`${id}-v-socks5`} className="px-3 h-10 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="3" id={`${id}-v-socks5`} className="mr-2"/>
|
||
<span>SOCKS5</span>
|
||
</FormLabel>
|
||
</RadioGroup>
|
||
)}
|
||
</FormField>
|
||
|
||
{/* 认证方式 */}
|
||
<FormField name="authType" className="md:max-w-[calc(160px*2+1rem)]" label="认证方式" classNames={{label: 'max-md:text-sm'}}>
|
||
{({id, field}) => (
|
||
<RadioGroup
|
||
onValueChange={field.onChange}
|
||
defaultValue={field.value}
|
||
className="flex gap-4">
|
||
<FormLabel htmlFor={`${id}-v-http`} className="px-3 h-10 flex-1 border rounded-md flex items-center 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 flex-1 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="2" id={`${id}-v-https`} className="mr-2"/>
|
||
<span>密码</span>
|
||
</FormLabel>
|
||
</RadioGroup>
|
||
)}
|
||
</FormField>
|
||
|
||
{/* 去重选项 */}
|
||
<FormField name="distinct" className="md:max-w-[calc(160px*2+1rem)]" label="去重选项" classNames={{label: 'max-md:text-sm'}}>
|
||
{({id, field}) => (
|
||
<RadioGroup
|
||
onValueChange={field.onChange}
|
||
defaultValue={field.value}
|
||
className="flex gap-4">
|
||
<FormLabel htmlFor={`${id}-v-true`} className="px-3 h-10 flex-1 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="1" id={`${id}-v-true`} className="mr-2"/>
|
||
<span>去重</span>
|
||
</FormLabel>
|
||
<FormLabel htmlFor={`${id}-v-false`} className="px-3 h-10 flex-1 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="0" id={`${id}-v-false`} className="mr-2"/>
|
||
<span>不去重</span>
|
||
</FormLabel>
|
||
</RadioGroup>
|
||
)}
|
||
</FormField>
|
||
|
||
{/* 导出格式 */}
|
||
<FormField name="format" className="md:max-w-[calc(160px*2+1rem)]" label="导出格式" classNames={{label: 'max-md:text-sm'}}>
|
||
{({id, field}) => (
|
||
<RadioGroup
|
||
onValueChange={field.onChange}
|
||
defaultValue={field.value}
|
||
className="flex gap-4"
|
||
>
|
||
<FormLabel htmlFor={`${id}-v-txt`} className="px-3 h-10 flex-1 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="text" id={`${id}-v-txt`} className="mr-2"/>
|
||
<span>TXT 格式</span>
|
||
</FormLabel>
|
||
<FormLabel htmlFor={`${id}-v-json`} className="px-3 h-10 flex-1 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="json" id={`${id}-v-json`} className="mr-2"/>
|
||
<span>JSON 格式</span>
|
||
</FormLabel>
|
||
</RadioGroup>
|
||
)}
|
||
</FormField>
|
||
|
||
{/* 分隔符 */}
|
||
<FormField name="separator" className="md:max-w-[calc(160px*3+1rem*2)]" label="分隔符" classNames={{label: 'max-md:text-sm'}}>
|
||
{({id, field}) => (
|
||
<RadioGroup
|
||
onValueChange={field.onChange}
|
||
defaultValue={field.value}
|
||
className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||
<FormLabel htmlFor={`${id}-v-comma`} className="px-3 h-10 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="124" id={`${id}-v-comma`} className="mr-2"/>
|
||
<span>竖线 ( | )</span>
|
||
</FormLabel>
|
||
<FormLabel htmlFor={`${id}-v-semicolon`} className="px-3 h-10 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="58" id={`${id}-v-semicolon`} className="mr-2"/>
|
||
<span>冒号 ( : )</span>
|
||
</FormLabel>
|
||
<FormLabel htmlFor={`${id}-v-space`} className="px-3 h-10 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="9" id={`${id}-v-space`} className="mr-2"/>
|
||
<span>制表符 ( \t )</span>
|
||
</FormLabel>
|
||
</RadioGroup>
|
||
)}
|
||
</FormField>
|
||
|
||
{/* 换行符 */}
|
||
<FormField name="breaker" className="md:max-w-[calc(160px*3+1rem*2)]" label="换行符" classNames={{label: 'max-md:text-sm'}}>
|
||
{({id, field}) => (
|
||
<RadioGroup
|
||
onValueChange={field.onChange}
|
||
defaultValue={field.value}
|
||
className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||
<FormLabel htmlFor={`${id}-v-newline2`} className="px-3 h-10 border rounded-md flex items-center 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 text-sm">
|
||
<RadioGroupItem value="10" id={`${id}-v-newline`} className="mr-2"/>
|
||
<span>换行 ( \n )</span>
|
||
</FormLabel>
|
||
<FormLabel htmlFor={`${id}-v-newline3`} className="px-3 h-10 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="13" id={`${id}-v-newline3`} className="mr-2"/>
|
||
<span>回车 ( \r )</span>
|
||
</FormLabel>
|
||
</RadioGroup>
|
||
)}
|
||
</FormField>
|
||
|
||
{/* 提取数量 */}
|
||
<FormField
|
||
name="count"
|
||
className="max-w[160px*2+1rem]"
|
||
label="提取数量"
|
||
classNames={{label: 'max-md:text-sm'}}
|
||
>
|
||
{({id, field}) => (
|
||
<div className="flex gap-2 items-center">
|
||
{/* 减号按钮:移动端显示,步长1,最小1 */}
|
||
<Button
|
||
theme="outline"
|
||
type="button"
|
||
className="h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg"
|
||
onClick={() => field.onChange(Math.max(1, Number(field.value) - 1))}
|
||
disabled={Number(field.value) === 1}
|
||
>
|
||
-
|
||
</Button>
|
||
|
||
{/* 数量输入框 */}
|
||
<Input
|
||
{...field}
|
||
id={id}
|
||
type="number"
|
||
min={1}
|
||
step={1}
|
||
className="w-40 h-10 border border-gray-200 rounded-sm text-center"
|
||
placeholder="输入提取数量"
|
||
onChange={(e) => {
|
||
const value = Math.max(1, Number(e.target.value) || 1)
|
||
field.onChange(value)
|
||
}}
|
||
/>
|
||
|
||
{/* 加号按钮:移动端显示,步长1 */}
|
||
<Button
|
||
theme="outline"
|
||
type="button"
|
||
className="h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg"
|
||
onClick={() => field.onChange(Number(field.value) + 1)}
|
||
>
|
||
+
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</FormField>
|
||
</div>
|
||
)
|
||
})
|
||
FormFields.displayName = 'FormFields'
|
||
|
||
function SelectResource() {
|
||
const [resources, setResources] = useState<Resource[]>([])
|
||
const [status, setStatus] = useStatus()
|
||
const profile = useProfileStore(state => state.profile)
|
||
const getResources = async () => {
|
||
setStatus('load')
|
||
try {
|
||
const resp = await allResource()
|
||
if (!resp.success) {
|
||
console.log(11111)
|
||
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 (
|
||
<FormField name="resource" className="md:max-w-[calc(160px*2+1rem)]" label="选择套餐" classNames={{label: 'max-md:text-sm'}}>
|
||
{({field}) => (
|
||
<Select
|
||
value={field.value ? String(field.value) : undefined}
|
||
onValueChange={value => field.onChange(Number(value))}
|
||
>
|
||
<SelectTrigger className="min-h-10 h-auto w-full">
|
||
<SelectValue placeholder="选择套餐"/>
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{status === 'load' ? (
|
||
<div className="p-4 flex gap-1 items-center">
|
||
<Loader className="animate-spin" size={20}/>
|
||
<span>加载中...</span>
|
||
</div>
|
||
) : !profile ? (
|
||
<div className="p-4 flex gap-1 items-center">
|
||
<Loader className="animate-spin" size={20}/>
|
||
<span>请先登录账号,<Link href="/login" className="text-blue-600 hover:underline">去登录</Link></span>
|
||
</div>
|
||
) : resources.length === 0 ? (
|
||
<div className="p-4 flex gap-1 items-center">
|
||
<Loader className="animate-spin" size={20}/>
|
||
<span>暂无可用套餐</span>
|
||
</div>
|
||
) : resources.map((resource, i) => (
|
||
<>
|
||
<SelectItem
|
||
key={`${resource.id}`}
|
||
value={String(resource.id)}
|
||
className="p-3">
|
||
<div className="flex flex-col gap-2 w-72">
|
||
{resource.type === 1 && resource.short.type === 1 && (
|
||
<>
|
||
<div className="flex gap-2 items-center bg-green-50 w-fit px-2 py-1 rounded-md text-sm">
|
||
<Timer size={20}/>
|
||
<span>{name(resource)}</span>
|
||
</div>
|
||
<div className="flex justify-between gap-2 text-xs text-weak">
|
||
<span>
|
||
到期时间:
|
||
{format(resource.short.expire, 'yyyy-MM-dd HH:mm')}
|
||
</span>
|
||
<span>{intlFormatDistance(resource.short.expire, new Date())}</span>
|
||
</div>
|
||
</>
|
||
)}
|
||
{resource.type === 1 && resource.short.type === 2 && (
|
||
<>
|
||
<div className="flex gap-2 items-center bg-blue-50 w-fit px-2 py-1 rounded-md text-sm">
|
||
<Box size={20}/>
|
||
<span>{name(resource)}</span>
|
||
</div>
|
||
<div className="flex justify-between gap-2 text-xs text-weak">
|
||
<span>
|
||
提取数量:
|
||
{resource.short.used}
|
||
{' '}
|
||
/
|
||
{resource.short.quota}
|
||
</span>
|
||
<span>
|
||
剩余
|
||
{resource.short.quota - resource.short.used}
|
||
</span>
|
||
</div>
|
||
</>
|
||
)}
|
||
{resource.type === 2 && resource.long.type === 1 && (
|
||
<>
|
||
<div className="flex gap-2 items-center bg-green-50 w-fit px-2 py-1 rounded-md text-sm">
|
||
<Timer size={20}/>
|
||
<span>{name(resource)}</span>
|
||
</div>
|
||
<div className="flex justify-between gap-2 text-xs text-weak">
|
||
<span>
|
||
到期时间:
|
||
{format(resource.long.expire, 'yyyy-MM-dd HH:mm')}
|
||
</span>
|
||
<span>{intlFormatDistance(resource.long.expire, new Date())}</span>
|
||
</div>
|
||
</>
|
||
)}
|
||
{resource.type === 2 && resource.long.type === 2 && (
|
||
<>
|
||
<div className="flex gap-2 items-center bg-blue-50 w-fit px-2 py-1 rounded-md text-sm">
|
||
<Box size={20}/>
|
||
<span>{name(resource)}</span>
|
||
</div>
|
||
<div className="flex justify-between gap-2 text-xs text-weak">
|
||
<span>
|
||
提取数量:
|
||
{resource.long.used}
|
||
{' '}
|
||
/
|
||
{resource.long.quota}
|
||
</span>
|
||
<span>
|
||
剩余
|
||
{resource.long.quota - resource.long.used}
|
||
</span>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</SelectItem>
|
||
{i < resources.length - 1 && <SelectSeparator className="m-1"/>}
|
||
</>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
)}
|
||
</FormField>
|
||
)
|
||
}
|
||
|
||
function SelectRegion() {
|
||
const form = useFormContext<Schema>()
|
||
const regionType = form.watch('regionType')
|
||
const prov = form.watch('prov')
|
||
const city = form.watch('city')
|
||
|
||
return (
|
||
<div className="flex flex-col gap-4 md:max-w-[calc(160px*2+1rem)]">
|
||
<FormField name="regionType" label="地区筛选" classNames={{label: 'max-md:text-sm'}}>
|
||
{({id, field}) => (
|
||
<RadioGroup
|
||
onValueChange={(e) => {
|
||
field.onChange(e)
|
||
if (e === 'unlimited') {
|
||
form.setValue('prov', '')
|
||
form.setValue('city', '')
|
||
}
|
||
}}
|
||
defaultValue={field.value}
|
||
className="flex gap-4"
|
||
>
|
||
<FormLabel htmlFor={`${id}-v-unlimited`} className="px-3 h-10 flex-1 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="unlimited" id={`${id}-v-unlimited`} className="mr-2"/>
|
||
<span>不限地区</span>
|
||
</FormLabel>
|
||
<FormLabel htmlFor={`${id}-v-specific`} className="px-3 h-10 flex-1 border rounded-md flex items-center text-sm">
|
||
<RadioGroupItem value="specific" id={`${id}-v-specific`} className="mr-2"/>
|
||
<span>指定地区</span>
|
||
</FormLabel>
|
||
</RadioGroup>
|
||
)}
|
||
</FormField>
|
||
|
||
{regionType === 'specific' && (
|
||
<Combobox
|
||
placeholder="请选择地区"
|
||
options={cities.options}
|
||
value={[prov || '', city || '']}
|
||
onChange={(value) => {
|
||
form.setValue('prov', value[0])
|
||
form.setValue('city', value[1])
|
||
}}
|
||
/>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function ApplyLink() {
|
||
const form = useFormContext<Schema>()
|
||
const values = form.watch()
|
||
|
||
// 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<typeof schema>) => {
|
||
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) => (
|
||
<span key={i}>
|
||
-
|
||
{msg}
|
||
</span>
|
||
)),
|
||
})
|
||
},
|
||
)
|
||
|
||
const submit = (t: 'open' | 'copy') => {
|
||
type.current = t
|
||
handler()
|
||
}
|
||
|
||
return (
|
||
<div className={merge(
|
||
`flex flex-col gap-4`,
|
||
`rounded-lg`,
|
||
)}>
|
||
<h4>API 链接</h4>
|
||
|
||
{/* 展示链接地址 */}
|
||
<div className="bg-secondary p-4 rounded-md break-all">
|
||
{link(values)}
|
||
</div>
|
||
|
||
{/* 操作 */}
|
||
<div className="flex gap-4">
|
||
<Button type="button" onClick={() => submit('copy')}>
|
||
<CopyIcon/>
|
||
<span>复制链接</span>
|
||
</Button>
|
||
<Button type="button" onClick={() => submit('open')}>
|
||
<ExternalLinkIcon/>
|
||
<span>打开链接</span>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|