merge: 实装 PoW 配置面板

This commit is contained in:
ryan
2026-06-18 20:21:29 +08:00
3 changed files with 242 additions and 110 deletions
@@ -1,43 +1,244 @@
'use client';
import {Info} from 'lucide-react';
import {useEffect, useState} from 'react';
import {Switch} from '@/components/ui/switch';
import {Input} from '@/components/ui/input';
import {Label} from '@/components/ui/label';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {Switch} from '@/components/ui/switch';
import {Textarea} from '@/components/ui/textarea';
import type {ProxyRoutePoWConfig} from '@/lib/services/openflare';
import {listToText, parseTextareaList} from './helpers';
interface PowConfigPanelProps {
enabled: boolean;
config: ProxyRoutePoWConfig;
onChange: (enabled: boolean, config: ProxyRoutePoWConfig) => void;
}
export function PowConfigPanel({ enabled, config, onChange }: PowConfigPanelProps) {
return (
<div className="space-y-4">
<div className="flex gap-3 rounded-lg border border-dashed bg-muted/40 p-4 text-sm">
<Info className="size-4 shrink-0 text-primary mt-0.5" />
<div className="space-y-1">
<p className="font-medium">PoW 配置面板(占位)</p>
<p className="text-muted-foreground text-xs">
完整 PoW 黑白名单与难度参数编辑将在后续迭代中实装。当前可先切换启用状态并保存规则组。
</p>
</div>
</div>
type PowListDraft = {
whitelist: Record<keyof ProxyRoutePoWConfig['whitelist'], string>;
blacklist: Record<keyof ProxyRoutePoWConfig['blacklist'], string>;
};
function buildDraft(config: ProxyRoutePoWConfig): PowListDraft {
return {
whitelist: {
ips: listToText(config.whitelist?.ips),
ip_cidrs: listToText(config.whitelist?.ip_cidrs),
paths: listToText(config.whitelist?.paths),
path_regexes: listToText(config.whitelist?.path_regexes),
user_agents: listToText(config.whitelist?.user_agents),
},
blacklist: {
ips: listToText(config.blacklist?.ips),
ip_cidrs: listToText(config.blacklist?.ip_cidrs),
paths: listToText(config.blacklist?.paths),
path_regexes: listToText(config.blacklist?.path_regexes),
user_agents: listToText(config.blacklist?.user_agents),
},
};
}
function applyDraft(
config: ProxyRoutePoWConfig,
draft: PowListDraft,
): ProxyRoutePoWConfig {
return {
...config,
whitelist: {
ips: parseTextareaList(draft.whitelist.ips),
ip_cidrs: parseTextareaList(draft.whitelist.ip_cidrs),
paths: parseTextareaList(draft.whitelist.paths),
path_regexes: parseTextareaList(draft.whitelist.path_regexes),
user_agents: parseTextareaList(draft.whitelist.user_agents),
},
blacklist: {
ips: parseTextareaList(draft.blacklist.ips),
ip_cidrs: parseTextareaList(draft.blacklist.ip_cidrs),
paths: parseTextareaList(draft.blacklist.paths),
path_regexes: parseTextareaList(draft.blacklist.path_regexes),
user_agents: parseTextareaList(draft.blacklist.user_agents),
},
};
}
function PowListCard({
title,
scope,
draft,
onUpdate,
}: {
title: string;
scope: keyof PowListDraft;
draft: PowListDraft;
onUpdate: (
scope: keyof PowListDraft,
key: keyof ProxyRoutePoWConfig['whitelist'],
value: string,
) => void;
}) {
const fields: Array<{
key: keyof ProxyRoutePoWConfig['whitelist'];
label: string;
placeholder: string;
}> = [
{key: 'ips', label: 'IP', placeholder: '每行一个 IP'},
{key: 'ip_cidrs', label: 'IP CIDR', placeholder: '每行一个网段,如 10.0.0.0/8'},
{key: 'paths', label: '路径', placeholder: '每行一个路径前缀'},
{key: 'path_regexes', label: '路径正则', placeholder: '每行一个正则表达式'},
{key: 'user_agents', label: 'User-Agent', placeholder: '每行一个 UA 关键词或正则'},
];
return (
<div className="space-y-3 rounded-lg border border-dashed p-4">
<p className="text-sm font-medium">{title}</p>
<div className="space-y-3">
{fields.map((field) => (
<div key={field.key} className="space-y-1.5">
<Label className="text-xs text-muted-foreground">{field.label}</Label>
<Textarea
value={draft[scope][field.key]}
onChange={(event) => onUpdate(scope, field.key, event.target.value)}
placeholder={field.placeholder}
className="min-h-16 resize-y font-mono text-xs"
/>
</div>
))}
</div>
</div>
);
}
export function PowConfigPanel({enabled, config, onChange}: PowConfigPanelProps) {
const [draft, setDraft] = useState(() => buildDraft(config));
useEffect(() => {
setDraft(buildDraft(config));
}, [config]);
const emitChange = (
nextEnabled: boolean,
nextConfig: Partial<ProxyRoutePoWConfig>,
nextDraft?: PowListDraft,
) => {
const merged = {
...config,
...nextConfig,
};
onChange(nextEnabled, nextDraft ? applyDraft(merged, nextDraft) : merged);
};
const updateList = (
scope: keyof PowListDraft,
key: keyof ProxyRoutePoWConfig['whitelist'],
value: string,
) => {
const nextDraft: PowListDraft = {
...draft,
[scope]: {
...draft[scope],
[key]: value,
},
};
setDraft(nextDraft);
emitChange(enabled, {}, nextDraft);
};
return (
<div className="space-y-5">
<div className="flex items-center justify-between rounded-lg border border-dashed p-4">
<div className="space-y-1">
<div className="space-y-1 pr-4">
<Label htmlFor="pow-enabled">启用 PoW 防护</Label>
<p className="text-xs text-muted-foreground">
算法 {config.algorithm} · 难度 {config.difficulty}
启用后,命中该规则组的请求需要完成浏览器计算挑战。
</p>
</div>
<Switch
id="pow-enabled"
checked={enabled}
onCheckedChange={(checked) => onChange(checked, config)}
onCheckedChange={(checked) => emitChange(checked, {})}
/>
</div>
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">算法</Label>
<Select
value={config.algorithm}
onValueChange={(value: 'fast' | 'slow') =>
emitChange(enabled, {algorithm: value})
}
>
<SelectTrigger className="h-9">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="fast">Fast</SelectItem>
<SelectItem value="slow">Slow</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">难度</Label>
<Input
type="number"
min={1}
max={16}
value={config.difficulty}
onChange={(event) =>
emitChange(enabled, {difficulty: Number(event.target.value) || 1})
}
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">会话 TTL (秒)</Label>
<Input
type="number"
min={60}
value={config.session_ttl}
onChange={(event) =>
emitChange(enabled, {session_ttl: Number(event.target.value) || 60})
}
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs text-muted-foreground">挑战 TTL (秒)</Label>
<Input
type="number"
min={30}
value={config.challenge_ttl}
onChange={(event) =>
emitChange(enabled, {challenge_ttl: Number(event.target.value) || 30})
}
/>
</div>
</div>
<div className="grid gap-4 xl:grid-cols-2">
<PowListCard
title="白名单(跳过 PoW)"
scope="whitelist"
draft={draft}
onUpdate={updateList}
/>
<PowListCard
title="黑名单(必须 PoW)"
scope="blacklist"
draft={draft}
onUpdate={updateList}
/>
</div>
</div>
);
}
}