Compare commits
4 Commits
94ab3f55a8
...
7e69501e84
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e69501e84 | ||
|
|
c74e19a062 | ||
|
|
4f165c6a97 | ||
|
|
dc877c3ea0 |
@@ -8,7 +8,7 @@
|
||||
|
||||
| 参数名 | 类型 | 必填 | 描述 |
|
||||
|--------|----------|------|----------------------------------------------------------------------------------------------------------------------------|
|
||||
| i | number | 是 | 用于提取的套餐 ID |
|
||||
| i | string | 是 | 用于提取的套餐 ID |
|
||||
| t | number | 是 | 认证类型:1 - 白名单,2 - 密码 |
|
||||
| a | string | 否 | 归属地省份。默认全局随机 |
|
||||
| b | string | 否 | 归属地城市。默认全局随机 |
|
||||
|
||||
@@ -178,6 +178,21 @@ export default function ChannelsPage(props: ChannelsPageProps) {
|
||||
header: '代理地址',
|
||||
cell: ({row}) => <Addr channel={row.original}/>,
|
||||
},
|
||||
{
|
||||
header: '地区',
|
||||
cell: ({row}) => {
|
||||
const prov = row.original.filter_prov
|
||||
const city = row.original.filter_city
|
||||
const parts = []
|
||||
if (prov && prov !== 'all') parts.push(prov)
|
||||
if (city && city !== 'all') parts.push(city)
|
||||
return (
|
||||
<div className="text-sm">
|
||||
{parts.length > 0 ? parts.join(' / ') : '不限'}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
header: '认证方式',
|
||||
cell: ({row}) => {
|
||||
|
||||
@@ -26,10 +26,10 @@ const schema = z.object({
|
||||
prov: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
regionType: z.enum(['unlimited', 'specific']).default('unlimited'),
|
||||
isp: z.enum(['all', '1', '2', '3'], {required_error: '请选择运营商'}),
|
||||
// isp: z.enum(['all', '1', '2', '3'], {required_error: '请选择运营商'}),
|
||||
proto: z.enum(['all', '1', '2'], {required_error: '请选择协议'}),
|
||||
authType: z.enum(['1', '2'], {required_error: '请选择认证方式'}),
|
||||
distinct: z.enum(['1', '0'], {required_error: '请选择去重选项'}),
|
||||
// distinct: z.enum(['1', '0'], {required_error: '请选择去重选项'}),
|
||||
format: z.enum(['text', 'json'], {required_error: '请选择导出格式'}),
|
||||
// hostFormat: z.enum(['domain', 'ip'], {required_error: '请选择主机格式'}),
|
||||
separator: z.string({required_error: '请选择分隔符'}),
|
||||
@@ -48,11 +48,11 @@ export default function Extract(props: ExtractProps) {
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
regionType: 'unlimited',
|
||||
isp: 'all',
|
||||
// isp: 'all',
|
||||
proto: 'all',
|
||||
authType: '1',
|
||||
count: 1,
|
||||
distinct: '1',
|
||||
// distinct: '1',
|
||||
// hostFormat: 'ip',
|
||||
format: 'text',
|
||||
breaker: '13,10',
|
||||
@@ -108,7 +108,7 @@ const FormFields = memo(() => {
|
||||
<SelectRegion/>
|
||||
|
||||
{/* 运营商筛选 */}
|
||||
<FormField name="isp" label="运营商筛选" classNames={{label: 'max-md:text-sm'}}>
|
||||
{/* <FormField name="isp" label="运营商筛选" classNames={{label: 'max-md:text-sm'}}>
|
||||
{({id, field}) => (
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
@@ -133,7 +133,7 @@ const FormFields = memo(() => {
|
||||
</FormLabel>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</FormField>
|
||||
</FormField> */}
|
||||
|
||||
{/* 协议类型 */}
|
||||
<FormField name="proto" label="协议类型" classNames={{label: 'max-md:text-sm'}}>
|
||||
@@ -182,7 +182,7 @@ const FormFields = memo(() => {
|
||||
</FormField>
|
||||
|
||||
{/* 去重选项 */}
|
||||
<FormField name="distinct" className="md:max-w-[calc(160px*2+1rem)]" label="去重选项" classNames={{label: 'max-md:text-sm'}}>
|
||||
{/* <FormField name="distinct" className="md:max-w-[calc(160px*2+1rem)]" label="去重选项" classNames={{label: 'max-md:text-sm'}}>
|
||||
{({id, field}) => (
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
@@ -198,7 +198,7 @@ const FormFields = memo(() => {
|
||||
</FormLabel>
|
||||
</RadioGroup>
|
||||
)}
|
||||
</FormField>
|
||||
</FormField> */}
|
||||
|
||||
{/* 导出格式 */}
|
||||
<FormField name="format" className="md:max-w-[calc(160px*2+1rem)]" label="导出格式" classNames={{label: 'max-md:text-sm'}}>
|
||||
@@ -711,7 +711,7 @@ function ApplyLink() {
|
||||
}
|
||||
|
||||
function link(values: Schema) {
|
||||
const {resource, prov, city, isp, proto, authType, distinct, format: formatType, separator, breaker, count} = values
|
||||
const {resource, prov, city, proto, authType, format: formatType, separator, breaker, count} = values
|
||||
console.log(values, 'values')
|
||||
|
||||
const sp = new URLSearchParams()
|
||||
@@ -721,8 +721,8 @@ function link(values: Schema) {
|
||||
if (prov) sp.set('b', prov)
|
||||
if (city) sp.set('b', city)
|
||||
|
||||
if (isp != 'all') sp.set('s', isp)
|
||||
sp.set('d', distinct)
|
||||
sp.set('s', 'all')
|
||||
sp.set('d', '1')
|
||||
sp.set('rt', formatType)
|
||||
sp.set('rh', 'ip')
|
||||
sp.set('rs', separator)
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function Center({skuData}: {
|
||||
setValue('expire', nextExpireList[0])
|
||||
}
|
||||
}}
|
||||
className="grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-4">
|
||||
className="grid grid-cols-[repeat(auto-fill,minmax(150px,1fr))] gap-4">
|
||||
{liveList.map((live) => {
|
||||
const priceExpire = type === '1' && !hasPurchaseSku(skuData, {mode: type, live, expire})
|
||||
? getAvailablePurchaseExpires(skuData, {mode: type, live})[0] || '0'
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function FormOption(props: FormOptionProps) {
|
||||
<p className="text-sm text-gray-500">{props.description}</p>
|
||||
)}
|
||||
{isValidPrice && (
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="flex items-center flex-col">
|
||||
{hasDiscount ? (
|
||||
<>
|
||||
<p className="text-sm font-medium">¥{formatPrice(discountedPrice!)}/IP</p>
|
||||
|
||||
@@ -11,7 +11,7 @@ const defaultFeatures = [
|
||||
'IP时效3-30分钟(可定制)',
|
||||
'IP资源定期筛选',
|
||||
'包量/包时计费方式',
|
||||
'每日去重量:500万',
|
||||
'每日去重量:50万',
|
||||
]
|
||||
|
||||
export function FeatureList(props: {
|
||||
|
||||
@@ -135,7 +135,7 @@ export default function Center({
|
||||
setValue('expire', nextExpireList[0])
|
||||
}
|
||||
}}
|
||||
className="grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-4">
|
||||
className="grid grid-cols-[repeat(auto-fill,minmax(150px,1fr))] gap-4">
|
||||
{liveList.map((live) => {
|
||||
const priceExpire = type === '1' && !hasPurchaseSku(skuData, {mode: type, live, expire})
|
||||
? getAvailablePurchaseExpires(skuData, {mode: type, live})[0] || '0'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import {CheckIcon, ChevronsUpDown} from 'lucide-react'
|
||||
import {CheckIcon, ChevronDown, ChevronRight, ChevronsUpDown} from 'lucide-react'
|
||||
|
||||
import {merge} from '@/lib/utils'
|
||||
import {Button} from '@/components/ui/button'
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import {ReactNode, useRef, useState} from 'react'
|
||||
import {ReactNode, useState} from 'react'
|
||||
import {Input} from '@/components/ui/input'
|
||||
|
||||
type ComboboxItem = {
|
||||
@@ -28,6 +28,7 @@ type ComboboxProps = {
|
||||
value?: string[]
|
||||
onChange?: (value: string[]) => void
|
||||
children?: React.ReactNode
|
||||
searchPlaceholder?: string
|
||||
}
|
||||
|
||||
export function Combobox(props: ComboboxProps) {
|
||||
@@ -56,22 +57,51 @@ export function Combobox(props: ComboboxProps) {
|
||||
const [wait, setWait] = useState(false)
|
||||
const [filter, setFilter] = useState<string>('')
|
||||
const [filtered, setFiltered] = useState<ComboboxItem[]>([])
|
||||
const [expandedKeys, setExpandedKeys] = useState<Set<string>>(new Set())
|
||||
|
||||
const onFilter = async () => {
|
||||
const toggleExpand = (key: string) => {
|
||||
setExpandedKeys((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(key)) {
|
||||
next.delete(key)
|
||||
}
|
||||
else {
|
||||
next.add(key)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const onSearch = async () => {
|
||||
if (wait) return
|
||||
const cond = filter.trim()
|
||||
console.log('onFilter', cond)
|
||||
setWait(true)
|
||||
if (cond.length > 0) {
|
||||
setFiltered(mapFilter(JSON.parse(JSON.stringify(props.options)), cond))
|
||||
const result = mapFilter(JSON.parse(JSON.stringify(props.options)), cond)
|
||||
setFiltered(result)
|
||||
setExpandedKeys(collectAllKeys(result))
|
||||
}
|
||||
else {
|
||||
setFiltered(props.options)
|
||||
setExpandedKeys(new Set())
|
||||
}
|
||||
console.log('onFilter end')
|
||||
setWait(false)
|
||||
}
|
||||
|
||||
const collectAllKeys = (items: ComboboxItem[]): Set<string> => {
|
||||
const keys = new Set<string>()
|
||||
const walk = (list: ComboboxItem[]) => {
|
||||
list.forEach((item) => {
|
||||
if (item.children?.length) {
|
||||
keys.add(item.value)
|
||||
walk(item.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
walk(items)
|
||||
return keys
|
||||
}
|
||||
|
||||
const mapFilter = (items: ComboboxItem[], cond: string): ComboboxItem[] => {
|
||||
const nItems: ComboboxItem[] = []
|
||||
items.forEach((item) => {
|
||||
@@ -97,6 +127,7 @@ export function Combobox(props: ComboboxProps) {
|
||||
if (status) {
|
||||
setFiltered(props.options)
|
||||
setFilter('')
|
||||
setExpandedKeys(new Set())
|
||||
}
|
||||
}}>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -117,18 +148,18 @@ export function Combobox(props: ComboboxProps) {
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="p-0 rounded-lg w-[var(--radix-popover-trigger-width)] h-[var(--radix-popover-content-available-height)] flex flex-col overflow-hidden"
|
||||
className="p-0 rounded-lg w-(--radix-popover-trigger-width) h-(--radix-popover-content-available-height) flex flex-col overflow-hidden"
|
||||
align="start"
|
||||
collisionPadding={6}
|
||||
>
|
||||
<div className="p-2 flex gap-2 flex-none">
|
||||
<Input
|
||||
className="h-9 placeholder:text-weak placeholder:text-sm"
|
||||
placeholder="搜索地区"
|
||||
placeholder={props.searchPlaceholder || '搜索地区'}
|
||||
value={filter}
|
||||
onChange={event => setFilter(event.target.value)}
|
||||
/>
|
||||
<Button className="h-9" onClick={onFilter} disabled={wait}>
|
||||
<Button className="h-9" onClick={onSearch} disabled={wait}>
|
||||
搜索
|
||||
</Button>
|
||||
</div>
|
||||
@@ -136,11 +167,13 @@ export function Combobox(props: ComboboxProps) {
|
||||
<OptionList
|
||||
options={filtered}
|
||||
value={values}
|
||||
onChange={(value) => {
|
||||
console.log(value.map(item => item.value))
|
||||
props.onChange?.(value.map(item => item.value))
|
||||
expandedKeys={expandedKeys}
|
||||
onToggleExpand={toggleExpand}
|
||||
onChange={(pathValue) => {
|
||||
props.onChange?.(pathValue)
|
||||
setOpen(false)
|
||||
}}/>
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
@@ -150,51 +183,76 @@ export function Combobox(props: ComboboxProps) {
|
||||
function OptionList(props: {
|
||||
options: ComboboxItem[]
|
||||
value: string[]
|
||||
onChange: (value: ComboboxItem[]) => void
|
||||
path?: ComboboxItem[]
|
||||
expandedKeys: Set<string>
|
||||
onToggleExpand: (key: string) => void
|
||||
onChange: (value: string[]) => void
|
||||
path?: string[]
|
||||
depth?: number
|
||||
}) {
|
||||
const depth = props.depth || 0
|
||||
const parent = props.path || []
|
||||
const indent = depth * 16
|
||||
const parentPath = props.path || []
|
||||
|
||||
return (
|
||||
<ul style={{
|
||||
marginLeft: `${indent}px`,
|
||||
}}>
|
||||
<ul>
|
||||
{props.options.map((item, i) => {
|
||||
const path = [...parent, item]
|
||||
const pathValue = path.map(item => item.value)
|
||||
const equal = pathValue.join(`.`) === props.value?.join('.')
|
||||
const path = [...parentPath, item.value]
|
||||
const hasChildren = !!item.children?.length
|
||||
const expanded = props.expandedKeys.has(item.value)
|
||||
const isCurrentLevel = path.length === props.value.filter(Boolean).length
|
||||
const isActivePath = path.every((v, idx) => v === props.value[idx])
|
||||
|
||||
return (
|
||||
<li key={i}>
|
||||
<OptionItem key={`${i}`} item={item} active={equal} onChange={() => props.onChange?.(path)}/>
|
||||
{item.children?.length
|
||||
&& <OptionList depth={depth + 1} options={item.children} value={props.value} path={path} onChange={props.onChange}/>
|
||||
}
|
||||
<div
|
||||
className={merge(
|
||||
`transition-colors duration-100 ease-in-out`,
|
||||
`pr-4 py-2 rounded-md`,
|
||||
`flex items-center gap-1`,
|
||||
`hover:bg-muted hover:text-foreground cursor-pointer`,
|
||||
isActivePath ? 'text-foreground' : 'text-muted-foreground',
|
||||
)}
|
||||
style={{paddingLeft: `${depth * 20 + 16}px`}}
|
||||
>
|
||||
{hasChildren ? (
|
||||
<span
|
||||
className="flex-none cursor-pointer p-0.5 hover:bg-muted rounded shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
props.onToggleExpand(item.value)
|
||||
}}
|
||||
>
|
||||
{expanded
|
||||
? <ChevronDown className="h-4 w-4"/>
|
||||
: <ChevronRight className="h-4 w-4"/>
|
||||
}
|
||||
</span>
|
||||
) : (
|
||||
<span className="w-5 shrink-0"/>
|
||||
)}
|
||||
<span
|
||||
className="flex-1 min-w-0 truncate"
|
||||
onClick={() => props.onChange(path)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
{isCurrentLevel && isActivePath && (
|
||||
<CheckIcon className="h-4 w-4 shrink-0"/>
|
||||
)}
|
||||
</div>
|
||||
{hasChildren && expanded && (
|
||||
<OptionList
|
||||
depth={depth + 1}
|
||||
options={item.children!}
|
||||
value={props.value}
|
||||
expandedKeys={props.expandedKeys}
|
||||
onToggleExpand={props.onToggleExpand}
|
||||
onChange={props.onChange}
|
||||
path={path}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
function OptionItem(props: {
|
||||
key: string
|
||||
item: ComboboxItem
|
||||
active: boolean
|
||||
onChange?: () => void
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={merge(
|
||||
`transition-colors, duration-100 ease-in-out`,
|
||||
`px-4 py-2 text-muted-foreground rounded-md`,
|
||||
`flex justify-between items-center`,
|
||||
`hover:bg-muted hover:text-foreground`,
|
||||
)}
|
||||
onClick={props.onChange}>
|
||||
{props.item.label}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -104,6 +104,8 @@ export type Channel = {
|
||||
expired_at: Date
|
||||
host: string
|
||||
batch_no: string
|
||||
filter_prov: string
|
||||
filter_city: string
|
||||
}
|
||||
export type Proxy = {
|
||||
id: number
|
||||
|
||||
Reference in New Issue
Block a user