'use client'; import {useEffect, useMemo, useState} from 'react'; import {zodResolver} from '@hookform/resolvers/zod'; import {Plus} from 'lucide-react'; import {useForm} from 'react-hook-form'; import {z} from 'zod'; import {Button} from '@/components/ui/button'; import {Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,} from '@/components/ui/form'; import {Input} from '@/components/ui/input'; import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from '@/components/ui/sheet'; import {Switch} from '@/components/ui/switch'; import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'; import {Textarea} from '@/components/ui/textarea'; import type {WAFIPGroup, WAFRuleGroup, WAFRuleGroupPayload} from '@/lib/services/openflare'; import { buildCountryOptions, buildRuleGroupDraft, countRuleEntries, emptyRuleGroupDraft, formatCountryItem, getListFieldKey, type ListFieldKey, normalizeItems, textToList, updateDraftList, } from './helpers'; import {PowConfigPanel} from './pow-config-panel'; import {RuleEntryDialog, type RuleEntryFormValues} from './rule-entry-dialog'; import {RuleListSection} from './rule-list-section'; const ruleGroupSchema = z.object({ name: z.string().trim().min(1, '请输入规则组名称').max(255, '名称不能超过 255 个字符'), enabled: z.boolean(), block_status_code: z.number().int().min(400).max(599), block_response_body: z.string(), remark: z.string().max(500, '备注不能超过 500 个字符'), pow_enabled: z.boolean(), }); type RuleGroupFormValues = z.infer; interface RuleGroupSheetProps { open: boolean; group: WAFRuleGroup | null; ipGroups: WAFIPGroup[]; submitting: boolean; onOpenChange: (open: boolean) => void; onSubmit: (payload: WAFRuleGroupPayload) => Promise; } export function RuleGroupSheet({ open, group, ipGroups, submitting, onOpenChange, onSubmit, }: RuleGroupSheetProps) { const [listsDraft, setListsDraft] = useState(emptyRuleGroupDraft); const [ruleEntryOpen, setRuleEntryOpen] = useState(false); const countryOptions = useMemo(() => buildCountryOptions(), []); const countryLabelMap = useMemo( () => new Map(countryOptions.map((option) => [option.code, option.label])), [countryOptions], ); const form = useForm({ resolver: zodResolver(ruleGroupSchema), defaultValues: { name: '', enabled: true, block_status_code: 418, block_response_body: '', remark: '', pow_enabled: false, }, }); useEffect(() => { if (!open) return; const draft = buildRuleGroupDraft(group); setListsDraft(draft); form.reset({ name: draft.name, enabled: draft.enabled, block_status_code: draft.block_status_code, block_response_body: draft.block_response_body, remark: draft.remark, pow_enabled: draft.pow_enabled, }); setRuleEntryOpen(false); }, [form, group, open]); const ipGroupByID = new Map(ipGroups.map((item) => [item.id, item])); const whitelistGroupItems = listsDraft.ip_whitelist_group_ids .map((id) => ipGroupByID.get(id)) .filter((item): item is WAFIPGroup => Boolean(item)); const blacklistGroupItems = listsDraft.ip_blacklist_group_ids .map((id) => ipGroupByID.get(id)) .filter((item): item is WAFIPGroup => Boolean(item)); const removeRuleItem = (key: ListFieldKey, value: string) => { setListsDraft((current) => updateDraftList(current, key, (items) => items.filter((item) => item !== value)), ); }; const removeRuleGroup = (key: ListFieldKey, id: number) => { setListsDraft((current) => updateDraftList(current, key, (items) => items.filter((item) => item !== String(id)), ), ); }; const applyRuleEntry = (entry: RuleEntryFormValues) => { const values = entry.dimension === 'ip' ? textToList(entry.ipValue) : entry.dimension === 'ip_group' ? entry.ipGroupIDs.map(String) : normalizeItems(entry.countryValues); const listKey = getListFieldKey(entry.listType, entry.dimension); setListsDraft((current) => updateDraftList(current, listKey, (items) => normalizeItems([...items, ...values]), ), ); setRuleEntryOpen(false); }; const handleSubmit = form.handleSubmit(async (values) => { const payload: WAFRuleGroupPayload = { ...listsDraft, name: values.name, enabled: values.enabled, block_status_code: values.block_status_code, block_response_body: values.block_response_body, remark: values.remark, pow_enabled: values.pow_enabled, }; try { await onSubmit(payload); onOpenChange(false); } catch (error) { form.setError('root', { message: error instanceof Error ? error.message : '保存失败', }); } }); const isGlobal = group?.is_global ?? false; return ( <> {group ? `编辑 ${group.name}` : '新建规则组'} 白名单命中后直接放行;未命中白名单时继续判断黑名单。当前规则数{' '} {countRuleEntries(listsDraft)} 条。
基本信息 黑白名单 PoW 拦截返回
( 规则组名称 )} /> (
启用规则组 关闭后保留配置,但不会参与匹配。
)} /> ( 备注 )} />

通过弹窗添加 IP、IP 组或地域规则。

removeRuleItem('ip_whitelist', item)} onRemoveGroup={(id) => removeRuleGroup('ip_whitelist_group_ids', id)} /> removeRuleItem('ip_blacklist', item)} onRemoveGroup={(id) => removeRuleGroup('ip_blacklist_group_ids', id)} /> formatCountryItem(code, countryLabelMap), )} tone="whitelist" emptyText="暂无地域白名单规则。" onRemove={(item) => { const code = item.split(' ')[0] ?? item; removeRuleItem('country_whitelist', code); }} /> formatCountryItem(code, countryLabelMap), )} tone="blacklist" emptyText="暂无地域黑名单规则。" onRemove={(item) => { const code = item.split(' ')[0] ?? item; removeRuleItem('country_blacklist', code); }} />
{ form.setValue('pow_enabled', enabled); setListsDraft((current) => ({ ...current, pow_enabled: enabled, pow_config: config, })); }} />
( 拦截状态码 field.onChange(Number(event.target.value)) } />
{[403, 418, 451, 503].map((code) => ( ))}
)} /> ( 拦截页面