mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-13 02:26:36 +08:00
prettier
This commit is contained in:
@@ -40,7 +40,7 @@ export function CreateRuleDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<form
|
||||
className="flex flex-col gap-4"
|
||||
className='flex flex-col gap-4'
|
||||
onSubmit={async (event) => {
|
||||
event.preventDefault();
|
||||
if (!trimmedName || pending) return;
|
||||
@@ -55,28 +55,28 @@ export function CreateRuleDialog({
|
||||
</DialogHeader>
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="waf-rule-name">规则名称</FieldLabel>
|
||||
<FieldLabel htmlFor='waf-rule-name'>规则名称</FieldLabel>
|
||||
<Input
|
||||
id="waf-rule-name"
|
||||
id='waf-rule-name'
|
||||
value={name}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
placeholder="例如:入口防护"
|
||||
autoComplete="off"
|
||||
placeholder='例如:入口防护'
|
||||
autoComplete='off'
|
||||
autoFocus
|
||||
/>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
type='button'
|
||||
variant='outline'
|
||||
disabled={pending}
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit" disabled={!trimmedName || pending}>
|
||||
{pending ? <Spinner data-icon="inline-start" /> : null}
|
||||
<Button type='submit' disabled={!trimmedName || pending}>
|
||||
{pending ? <Spinner data-icon='inline-start' /> : null}
|
||||
创建并编排
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type {
|
||||
WAFIPGroup,
|
||||
WAFIPGroupPayload,
|
||||
} from '@/lib/services/openflare';
|
||||
import type { WAFIPGroup, WAFIPGroupPayload } from '@/lib/services/openflare';
|
||||
|
||||
export function getErrorMessage(error: unknown) {
|
||||
return error instanceof Error ? error.message : '操作失败';
|
||||
@@ -18,7 +15,6 @@ export function parseTextareaList(text: string) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
|
||||
export function parseAutomaticConfig(text: string): Record<string, unknown> {
|
||||
const parsed = JSON.parse(text || '{}') as unknown;
|
||||
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
|
||||
@@ -83,7 +79,9 @@ export function formatIPGroupBanRemaining(
|
||||
if (expireDate.getTime() <= now.getTime()) {
|
||||
return '已过期';
|
||||
}
|
||||
const diffMins = Math.round((expireDate.getTime() - now.getTime()) / (60 * 1000));
|
||||
const diffMins = Math.round(
|
||||
(expireDate.getTime() - now.getTime()) / (60 * 1000),
|
||||
);
|
||||
if (diffMins < 60) {
|
||||
return `${diffMins} 分钟后`;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import {useEffect} from 'react';
|
||||
import {zodResolver} from '@hookform/resolvers/zod';
|
||||
import {Plus} from 'lucide-react';
|
||||
import {useForm} from 'react-hook-form';
|
||||
import {z} from 'zod';
|
||||
import { useEffect } 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 { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -15,11 +15,25 @@ import {
|
||||
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 {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';
|
||||
import {Switch} from '@/components/ui/switch';
|
||||
import {Textarea} from '@/components/ui/textarea';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import type {
|
||||
WAFIPGroup,
|
||||
WAFIPGroupPayload,
|
||||
@@ -27,7 +41,12 @@ import type {
|
||||
WAFIPGroupType,
|
||||
} from '@/lib/services/openflare';
|
||||
|
||||
import {automaticPresetRules, listToText, parseAutomaticConfig, parseTextareaList,} from './helpers';
|
||||
import {
|
||||
automaticPresetRules,
|
||||
listToText,
|
||||
parseAutomaticConfig,
|
||||
parseTextareaList,
|
||||
} from './helpers';
|
||||
|
||||
const ipGroupSchema = z
|
||||
.object({
|
||||
@@ -126,7 +145,9 @@ function appendAutomaticPresetRule(
|
||||
return JSON.stringify(
|
||||
{
|
||||
lookback_minutes:
|
||||
typeof config.lookback_minutes === 'number' ? config.lookback_minutes : 60,
|
||||
typeof config.lookback_minutes === 'number'
|
||||
? config.lookback_minutes
|
||||
: 60,
|
||||
...config,
|
||||
rules: nextRules,
|
||||
},
|
||||
@@ -175,20 +196,22 @@ export function IPGroupDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogContent className='max-w-3xl max-h-[90vh] overflow-y-auto'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{group ? `编辑 ${group.name}` : '新建 IP 组'}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{group ? `编辑 ${group.name}` : '新建 IP 组'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
维护可被 WAF IP 黑白名单引用的手动、自动与订阅 IP 集合。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<form onSubmit={handleSubmit} className='space-y-4'>
|
||||
<div className='grid gap-4 md:grid-cols-2'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
name='name'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>IP 组名称</FormLabel>
|
||||
@@ -201,7 +224,7 @@ export function IPGroupDialog({
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
name='type'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>类型</FormLabel>
|
||||
@@ -217,9 +240,9 @@ export function IPGroupDialog({
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="manual">手动</SelectItem>
|
||||
<SelectItem value="automatic">自动</SelectItem>
|
||||
<SelectItem value="subscription">订阅</SelectItem>
|
||||
<SelectItem value='manual'>手动</SelectItem>
|
||||
<SelectItem value='automatic'>自动</SelectItem>
|
||||
<SelectItem value='subscription'>订阅</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
@@ -228,17 +251,20 @@ export function IPGroupDialog({
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
name='enabled'
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border border-dashed p-4 md:col-span-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormItem className='flex items-center justify-between rounded-lg border border-dashed p-4 md:col-span-2'>
|
||||
<div className='space-y-0.5'>
|
||||
<FormLabel>启用 IP 组</FormLabel>
|
||||
<FormDescription>
|
||||
关闭后保留配置,但发布时不会展开到 WAF 运行时名单。
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -246,15 +272,18 @@ export function IPGroupDialog({
|
||||
</div>
|
||||
|
||||
{type === 'subscription' ? (
|
||||
<div className="grid gap-4 md:grid-cols-2 rounded-lg border border-dashed p-4">
|
||||
<div className='grid gap-4 md:grid-cols-2 rounded-lg border border-dashed p-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subscription_url"
|
||||
name='subscription_url'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>订阅 URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://example.com/ip-list.txt" {...field} />
|
||||
<Input
|
||||
placeholder='https://example.com/ip-list.txt'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -262,7 +291,7 @@ export function IPGroupDialog({
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subscription_format"
|
||||
name='subscription_format'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>订阅格式</FormLabel>
|
||||
@@ -278,8 +307,8 @@ export function IPGroupDialog({
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="text">文本列表</SelectItem>
|
||||
<SelectItem value="json">JSON</SelectItem>
|
||||
<SelectItem value='text'>文本列表</SelectItem>
|
||||
<SelectItem value='json'>JSON</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
@@ -288,13 +317,13 @@ export function IPGroupDialog({
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sync_interval_minutes"
|
||||
name='sync_interval_minutes'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>同步间隔(分钟)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
type='number'
|
||||
min={5}
|
||||
value={field.value}
|
||||
onChange={(event) =>
|
||||
@@ -302,21 +331,25 @@ export function IPGroupDialog({
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>最小 5 分钟,默认 1440 分钟。</FormDescription>
|
||||
<FormDescription>
|
||||
最小 5 分钟,默认 1440 分钟。
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subscription_mapping_rule"
|
||||
name='subscription_mapping_rule'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>JSON 映射规则</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
disabled={form.watch('subscription_format') !== 'json'}
|
||||
placeholder="留空表示根数组"
|
||||
disabled={
|
||||
form.watch('subscription_format') !== 'json'
|
||||
}
|
||||
placeholder='留空表示根数组'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -328,16 +361,16 @@ export function IPGroupDialog({
|
||||
) : null}
|
||||
|
||||
{type === 'automatic' ? (
|
||||
<div className="space-y-4 rounded-lg border border-dashed p-4">
|
||||
<div className='space-y-4 rounded-lg border border-dashed p-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sync_interval_minutes"
|
||||
name='sync_interval_minutes'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>同步间隔(分钟)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
type='number'
|
||||
min={5}
|
||||
value={field.value}
|
||||
onChange={(event) =>
|
||||
@@ -352,15 +385,15 @@ export function IPGroupDialog({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<div className='space-y-2'>
|
||||
<FormLabel>预设规则</FormLabel>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{automaticPresetRules.map((rule) => (
|
||||
<Button
|
||||
key={rule.expr}
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
type='button'
|
||||
size='sm'
|
||||
variant='outline'
|
||||
onClick={() => {
|
||||
const current = form.getValues('auto_config_text');
|
||||
form.setValue(
|
||||
@@ -369,7 +402,7 @@ export function IPGroupDialog({
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3.5 mr-1" />
|
||||
<Plus className='size-3.5 mr-1' />
|
||||
{rule.name}
|
||||
</Button>
|
||||
))}
|
||||
@@ -377,12 +410,15 @@ export function IPGroupDialog({
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="auto_config_text"
|
||||
name='auto_config_text'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>自动配置 JSON</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea className="min-h-48 font-mono text-xs" {...field} />
|
||||
<Textarea
|
||||
className='min-h-48 font-mono text-xs'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -394,13 +430,13 @@ export function IPGroupDialog({
|
||||
{type !== 'automatic' ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ip_list_text"
|
||||
name='ip_list_text'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>IP / IP 段</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="min-h-48 font-mono text-xs"
|
||||
className='min-h-48 font-mono text-xs'
|
||||
placeholder={'203.0.113.10\n198.51.100.0/24'}
|
||||
{...field}
|
||||
/>
|
||||
@@ -417,14 +453,20 @@ export function IPGroupDialog({
|
||||
) : null}
|
||||
|
||||
{form.formState.errors.root ? (
|
||||
<p className="text-sm text-destructive">{form.formState.errors.root.message}</p>
|
||||
<p className='text-sm text-destructive'>
|
||||
{form.formState.errors.root.message}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit" disabled={submitting}>
|
||||
<Button type='submit' disabled={submitting}>
|
||||
{submitting ? '保存中...' : '保存 IP 组'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import type {WAFIPGroupAutoTestResult} from '@/lib/services/openflare';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { WAFIPGroupAutoTestResult } from '@/lib/services/openflare';
|
||||
|
||||
interface IPGroupTestDialogProps {
|
||||
open: boolean;
|
||||
@@ -26,7 +26,7 @@ export function IPGroupTestDialog({
|
||||
}: IPGroupTestDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogContent className='max-w-2xl'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>自动规则测试结果</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -35,32 +35,38 @@ export function IPGroupTestDialog({
|
||||
</DialogHeader>
|
||||
|
||||
{loading ? (
|
||||
<p className="text-sm text-muted-foreground py-6 text-center">测试中...</p>
|
||||
<p className='text-sm text-muted-foreground py-6 text-center'>
|
||||
测试中...
|
||||
</p>
|
||||
) : result ? (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-dashed p-4 text-sm">
|
||||
<div className='space-y-4'>
|
||||
<div className='rounded-lg border border-dashed p-4 text-sm'>
|
||||
<p>
|
||||
回看 {result.lookback_minutes} 分钟 · 规则 {result.rule_count} 条 · 命中{' '}
|
||||
{result.matched_count} 个 IP
|
||||
回看 {result.lookback_minutes} 分钟 · 规则 {result.rule_count}{' '}
|
||||
条 · 命中 {result.matched_count} 个 IP
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
<p className='text-xs text-muted-foreground mt-1'>
|
||||
测试时间:{new Date(result.tested_at).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
{result.matched_count > 0 ? (
|
||||
<pre className="max-h-64 overflow-auto rounded-lg border bg-muted/40 p-3 text-xs whitespace-pre-wrap break-all">
|
||||
<pre className='max-h-64 overflow-auto rounded-lg border bg-muted/40 p-3 text-xs whitespace-pre-wrap break-all'>
|
||||
{result.matched_ips.join('\n')}
|
||||
</pre>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">当前没有匹配到任何 IP。</p>
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
当前没有匹配到任何 IP。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground py-6 text-center">暂无测试结果。</p>
|
||||
<p className='text-sm text-muted-foreground py-6 text-center'>
|
||||
暂无测试结果。
|
||||
</p>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" onClick={() => onOpenChange(false)}>
|
||||
<Button type='button' onClick={() => onOpenChange(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import {Loader2, Trash2} from 'lucide-react';
|
||||
import {useMemo, useState} from 'react';
|
||||
import { Loader2, Trash2 } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -22,12 +22,23 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {EmptyStateWithBorder} from '@/components/layout/empty';
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table';
|
||||
import type {WAFIPGroup} from '@/lib/services/openflare';
|
||||
import {formatDateTime} from '@/lib/utils';
|
||||
import { EmptyStateWithBorder } from '@/components/layout/empty';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import type { WAFIPGroup } from '@/lib/services/openflare';
|
||||
import { formatDateTime } from '@/lib/utils';
|
||||
|
||||
import {getIPGroupViewEntries, ipGroupTypeLabels, type IPGroupViewEntry} from './helpers';
|
||||
import {
|
||||
getIPGroupViewEntries,
|
||||
ipGroupTypeLabels,
|
||||
type IPGroupViewEntry,
|
||||
} from './helpers';
|
||||
|
||||
interface IPGroupViewDialogProps {
|
||||
open: boolean;
|
||||
@@ -46,7 +57,9 @@ export function IPGroupViewDialog({
|
||||
onOpenChange,
|
||||
onRemoveIp,
|
||||
}: IPGroupViewDialogProps) {
|
||||
const [deleteTarget, setDeleteTarget] = useState<IPGroupViewEntry | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<IPGroupViewEntry | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const entries = useMemo(
|
||||
() => (group ? getIPGroupViewEntries(group) : []),
|
||||
@@ -66,9 +79,11 @@ export function IPGroupViewDialog({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogContent className='max-w-3xl max-h-[90vh] overflow-y-auto'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{group ? `查看 ${group.name}` : '查看 IP 组'}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{group ? `查看 ${group.name}` : '查看 IP 组'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{group
|
||||
? `${ipGroupTypeLabels[group.type]} · 共 ${entries.length} 条 IP`
|
||||
@@ -77,12 +92,14 @@ export function IPGroupViewDialog({
|
||||
</DialogHeader>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center gap-2 py-12 text-sm text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
<div className='flex items-center justify-center gap-2 py-12 text-sm text-muted-foreground'>
|
||||
<Loader2 className='size-4 animate-spin' />
|
||||
加载 IP 列表...
|
||||
</div>
|
||||
) : !group ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">未选择 IP 组。</p>
|
||||
<p className='py-8 text-center text-sm text-muted-foreground'>
|
||||
未选择 IP 组。
|
||||
</p>
|
||||
) : entries.length === 0 ? (
|
||||
<EmptyStateWithBorder
|
||||
description={
|
||||
@@ -92,13 +109,13 @@ export function IPGroupViewDialog({
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className='space-y-3'>
|
||||
{group.type === 'subscription' ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className='text-xs text-muted-foreground'>
|
||||
订阅类型在下次同步时可能重新拉取已删除的 IP。
|
||||
</p>
|
||||
) : null}
|
||||
<div className="rounded-lg border border-dashed">
|
||||
<div className='rounded-lg border border-dashed'>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -109,36 +126,44 @@ export function IPGroupViewDialog({
|
||||
<TableHead>封禁剩余</TableHead>
|
||||
</>
|
||||
) : null}
|
||||
<TableHead className="w-[80px] text-right">操作</TableHead>
|
||||
<TableHead className='w-[80px] text-right'>
|
||||
操作
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{entries.map((entry) => (
|
||||
<TableRow key={entry.ip}>
|
||||
<TableCell className="font-mono text-sm">{entry.ip}</TableCell>
|
||||
<TableCell className='font-mono text-sm'>
|
||||
{entry.ip}
|
||||
</TableCell>
|
||||
{showAutomaticMeta ? (
|
||||
<>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{entry.capturedAt ? formatDateTime(entry.capturedAt) : '—'}
|
||||
<TableCell className='text-sm text-muted-foreground'>
|
||||
{entry.capturedAt
|
||||
? formatDateTime(entry.capturedAt)
|
||||
: '—'}
|
||||
</TableCell>
|
||||
<TableCell className='text-sm'>
|
||||
{entry.banRemaining ?? '—'}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{entry.banRemaining ?? '—'}</TableCell>
|
||||
</>
|
||||
) : null}
|
||||
<TableCell className="text-right">
|
||||
<TableCell className='text-right'>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 text-destructive hover:text-destructive"
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='size-8 text-destructive hover:text-destructive'
|
||||
disabled={removingIp === entry.ip}
|
||||
onClick={() => setDeleteTarget(entry)}
|
||||
>
|
||||
{removingIp === entry.ip ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
<Loader2 className='size-4 animate-spin' />
|
||||
) : (
|
||||
<Trash2 className="size-4" />
|
||||
<Trash2 className='size-4' />
|
||||
)}
|
||||
<span className="sr-only">删除 {entry.ip}</span>
|
||||
<span className='sr-only'>删除 {entry.ip}</span>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -150,7 +175,11 @@ export function IPGroupViewDialog({
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -169,9 +198,11 @@ export function IPGroupViewDialog({
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={Boolean(removingIp)}>取消</AlertDialogCancel>
|
||||
<AlertDialogCancel disabled={Boolean(removingIp)}>
|
||||
取消
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive text-white hover:bg-destructive/90"
|
||||
className='bg-destructive text-white hover:bg-destructive/90'
|
||||
disabled={Boolean(removingIp)}
|
||||
onClick={async () => {
|
||||
if (!deleteTarget) return;
|
||||
@@ -186,4 +217,4 @@ export function IPGroupViewDialog({
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import {Download, Eye, MoreHorizontal, Pencil, Play, Trash2} from 'lucide-react';
|
||||
import {
|
||||
Download,
|
||||
Eye,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
Play,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
|
||||
import {Badge} from '@/components/ui/badge';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -11,11 +18,18 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table';
|
||||
import type {WAFIPGroup} from '@/lib/services/openflare';
|
||||
import {formatDateTime} from '@/lib/utils';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import type { WAFIPGroup } from '@/lib/services/openflare';
|
||||
import { formatDateTime } from '@/lib/utils';
|
||||
|
||||
import {ipGroupTypeLabels} from './helpers';
|
||||
import { ipGroupTypeLabels } from './helpers';
|
||||
|
||||
interface IPGroupsTableProps {
|
||||
groups: WAFIPGroup[];
|
||||
@@ -47,15 +61,15 @@ export function IPGroupsTable({
|
||||
<TableHead>引用次数</TableHead>
|
||||
<TableHead>同步状态</TableHead>
|
||||
<TableHead>更新时间</TableHead>
|
||||
<TableHead className="w-[80px] text-right">操作</TableHead>
|
||||
<TableHead className='w-[80px] text-right'>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{groups.map((group) => (
|
||||
<TableRow key={group.id}>
|
||||
<TableCell className="font-medium">{group.name}</TableCell>
|
||||
<TableCell className='font-medium'>{group.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{ipGroupTypeLabels[group.type]}</Badge>
|
||||
<Badge variant='outline'>{ipGroupTypeLabels[group.type]}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={group.enabled ? 'default' : 'secondary'}>
|
||||
@@ -64,42 +78,43 @@ export function IPGroupsTable({
|
||||
</TableCell>
|
||||
<TableCell>{group.ip_list.length}</TableCell>
|
||||
<TableCell>{group.referenced_by_rule_count}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground max-w-[200px] truncate">
|
||||
<TableCell className='text-sm text-muted-foreground max-w-[200px] truncate'>
|
||||
{group.last_sync_status
|
||||
? `${group.last_sync_status}: ${group.last_sync_message}`
|
||||
: '尚无同步记录'}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">
|
||||
<TableCell className='text-muted-foreground text-sm'>
|
||||
{group.updated_at ? formatDateTime(group.updated_at) : '—'}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<TableCell className='text-right'>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="size-8">
|
||||
<MoreHorizontal className="size-4" />
|
||||
<Button variant='ghost' size='icon' className='size-8'>
|
||||
<MoreHorizontal className='size-4' />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem onClick={() => onView(group)}>
|
||||
<Eye className="size-4 mr-2" />
|
||||
<Eye className='size-4 mr-2' />
|
||||
查看
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onEdit(group)}>
|
||||
<Pencil className="size-4 mr-2" />
|
||||
<Pencil className='size-4 mr-2' />
|
||||
编辑
|
||||
</DropdownMenuItem>
|
||||
{group.type === 'automatic' ? (
|
||||
<DropdownMenuItem onClick={() => onTest(group)}>
|
||||
<Play className="size-4 mr-2" />
|
||||
<Play className='size-4 mr-2' />
|
||||
测试规则
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{group.type === 'subscription' || group.type === 'automatic' ? (
|
||||
{group.type === 'subscription' ||
|
||||
group.type === 'automatic' ? (
|
||||
<DropdownMenuItem
|
||||
disabled={syncingId === group.id}
|
||||
onClick={() => onSync(group)}
|
||||
>
|
||||
<Download className="size-4 mr-2" />
|
||||
<Download className='size-4 mr-2' />
|
||||
{syncingId === group.id
|
||||
? '同步中...'
|
||||
: group.type === 'automatic'
|
||||
@@ -109,10 +124,10 @@ export function IPGroupsTable({
|
||||
) : null}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive"
|
||||
className='text-destructive focus:text-destructive'
|
||||
onClick={() => onDelete(group)}
|
||||
>
|
||||
<Trash2 className="size-4 mr-2" />
|
||||
<Trash2 className='size-4 mr-2' />
|
||||
删除
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import {Globe2, MoreHorizontal, Pencil, ShieldCheck, Trash2} from 'lucide-react';
|
||||
import {
|
||||
Globe2,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
ShieldCheck,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
|
||||
import {Badge} from '@/components/ui/badge';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -12,9 +18,16 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table';
|
||||
import type {WAFRule} from '@/lib/services/openflare';
|
||||
import {formatDateTime} from '@/lib/utils';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import type { WAFRule } from '@/lib/services/openflare';
|
||||
import { formatDateTime } from '@/lib/utils';
|
||||
|
||||
interface RuleGroupsTableProps {
|
||||
groups: WAFRule[];
|
||||
@@ -37,24 +50,26 @@ export function RuleGroupsTable({
|
||||
<TableHead>节点数</TableHead>
|
||||
<TableHead>应用范围</TableHead>
|
||||
<TableHead>更新时间</TableHead>
|
||||
<TableHead className="w-[80px] text-right">操作</TableHead>
|
||||
<TableHead className='w-[80px] text-right'>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{groups.map((group) => (
|
||||
<TableRow key={group.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className='flex items-center gap-2'>
|
||||
{group.is_global ? (
|
||||
<Globe2 className="size-4 text-primary shrink-0" />
|
||||
<Globe2 className='size-4 text-primary shrink-0' />
|
||||
) : (
|
||||
<ShieldCheck className="size-4 text-muted-foreground shrink-0" />
|
||||
<ShieldCheck className='size-4 text-muted-foreground shrink-0' />
|
||||
)}
|
||||
<span className="font-medium">{group.name}</span>
|
||||
<span className='font-medium'>{group.name}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{group.is_global ? '全局' : '自定义'}</Badge>
|
||||
<Badge variant='outline'>
|
||||
{group.is_global ? '全局' : '自定义'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={group.enabled ? 'default' : 'secondary'}>
|
||||
@@ -67,17 +82,17 @@ export function RuleGroupsTable({
|
||||
? '全部网站'
|
||||
: `${group.applied_site_count} 个网站`}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">
|
||||
<TableCell className='text-muted-foreground text-sm'>
|
||||
{group.updated_at ? formatDateTime(group.updated_at) : '—'}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<TableCell className='text-right'>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="size-8">
|
||||
<Button variant='ghost' size='icon' className='size-8'>
|
||||
<MoreHorizontal />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem onClick={() => onEdit(group)}>
|
||||
<Pencil />
|
||||
@@ -89,7 +104,7 @@ export function RuleGroupsTable({
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
variant='destructive'
|
||||
onClick={() => onDelete(group)}
|
||||
>
|
||||
<Trash2 />
|
||||
|
||||
Reference in New Issue
Block a user