From a2f838605ceebeb86eb7c96e4bcd5bf0a49c1368 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 18 Jun 2026 20:23:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(waf):=20=E8=A7=84=E5=88=99=E7=BB=84?= =?UTF-8?q?=E6=96=B0=E5=BB=BA/=E7=BC=96=E8=BE=91=E6=94=B9=E7=94=A8?= =?UTF-8?q?=E5=8F=B3=E4=BE=A7=20Sheet=20=E6=8A=BD=E5=B1=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 RuleGroupDialog 替换为 RuleGroupSheet,使用 shadcn Sheet 侧栏交互, 与站点绑定等操作保持一致;添加规则条目仍保留弹窗。 --- .../waf/components/rule-group-dialog.tsx | 399 ------------------ .../waf/components/rule-group-sheet.tsx | 399 ++++++++++++++++++ Wavelet/frontend/app/(main)/waf/page.tsx | 4 +- docs/changelog/index.md | 1 + 4 files changed, 402 insertions(+), 401 deletions(-) delete mode 100644 Wavelet/frontend/app/(main)/waf/components/rule-group-dialog.tsx create mode 100644 Wavelet/frontend/app/(main)/waf/components/rule-group-sheet.tsx diff --git a/Wavelet/frontend/app/(main)/waf/components/rule-group-dialog.tsx b/Wavelet/frontend/app/(main)/waf/components/rule-group-dialog.tsx deleted file mode 100644 index 1654f9cd..00000000 --- a/Wavelet/frontend/app/(main)/waf/components/rule-group-dialog.tsx +++ /dev/null @@ -1,399 +0,0 @@ -'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 { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import {Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,} from '@/components/ui/form'; -import {Input} from '@/components/ui/input'; -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 RuleGroupDialogProps { - open: boolean; - group: WAFRuleGroup | null; - ipGroups: WAFIPGroup[]; - submitting: boolean; - onOpenChange: (open: boolean) => void; - onSubmit: (payload: WAFRuleGroupPayload) => Promise; -} - -export function RuleGroupDialog({ - open, - group, - ipGroups, - submitting, - onOpenChange, - onSubmit, -}: RuleGroupDialogProps) { - 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) => ( - - ))} -
- -
- )} - /> - ( - - 拦截页面 - -