refactor: remove unused remarks and add quick domain create

Drop remark fields from Zone, Zone domains, proxy routes, WAF rule
groups and IP groups across models, APIs, UI and DB columns (keep
certificate/origin remarks). Add quick-create domain input for short
labels, @ apex and full FQDNs when binding domains.
This commit is contained in:
ryan
2026-07-12 16:16:56 +08:00
parent 8afd103751
commit d615d85a26
43 changed files with 616 additions and 433 deletions
@@ -1,152 +1,30 @@
'use client';
import {useEffect} from 'react';
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import {zodResolver} from '@hookform/resolvers/zod';
import {useForm} from 'react-hook-form';
import {Loader2} from 'lucide-react';
import {toast} from 'sonner';
import {z} from 'zod';
import {Button} from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {Input} from '@/components/ui/input';
import {Label} from '@/components/ui/label';
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select';
import {Textarea} from '@/components/ui/textarea';
import {TlsCertificateService, ZoneDomainService} from '@/lib/services/openflare';
const schema = z.object({
domain: z
.string()
.trim()
.min(1, '请输入完整域名')
.refine((value) => !value.includes('*.'), 'Zone 域名不支持通配符'),
cert_id: z.string(),
remark: z.string().max(255),
});
type Values = z.infer<typeof schema>;
import {QuickCreateZoneDomainDialog} from '../../components/quick-create-zone-domain-dialog';
/** Zone 详情页添加域名(固定 Zone,支持简写 / @ / 完整 FQDN)。 */
export function ZoneDomainDialog({
open,
onOpenChange,
zoneId,
zoneRoot,
onSaved,
}: {
open: boolean;
onOpenChange(open: boolean): void;
zoneId: number;
zoneRoot: string;
onSaved(): Promise<unknown> | void;
}) {
const form = useForm<Values>({
resolver: zodResolver(schema),
defaultValues: {domain: '', cert_id: '', remark: ''},
});
const queryClient = useQueryClient();
const certificatesQuery = useQuery({
queryKey: ['openflare', 'tls-certificates'],
queryFn: () => TlsCertificateService.list(),
enabled: open,
});
useEffect(() => {
if (open) {
form.reset({domain: '', cert_id: '', remark: ''});
}
}, [form, open]);
const mutation = useMutation({
mutationFn: (values: Values) =>
ZoneDomainService.create(zoneId, {
domain: values.domain.toLowerCase(),
cert_id: values.cert_id ? Number(values.cert_id) : null,
remark: values.remark.trim(),
}),
onSuccess: async () => {
toast.success('域名已添加');
await Promise.all([
onSaved(),
queryClient.invalidateQueries({queryKey: ['openflare', 'zones']}),
]);
onOpenChange(false);
},
onError: (error) =>
toast.error(error instanceof Error ? error.message : '保存失败'),
});
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>添加 Zone 域名</DialogTitle>
<DialogDescription>
填写明确 FQDN;通配符仅可存在于所选证书的 SAN 中。
</DialogDescription>
</DialogHeader>
<form
id="zone-domain-create"
className="space-y-4"
onSubmit={form.handleSubmit((values) => mutation.mutate(values))}
>
<div className="space-y-1.5">
<Label htmlFor="fqdn">完整域名</Label>
<Input id="fqdn" placeholder="api.arctel.de" {...form.register('domain')} />
{form.formState.errors.domain ? (
<p className="text-xs text-destructive">
{form.formState.errors.domain.message}
</p>
) : null}
</div>
<div className="space-y-1.5">
<Label>证书</Label>
<Select
value={form.watch('cert_id') || '__none'}
onValueChange={(value) =>
form.setValue('cert_id', value === '__none' ? '' : value)
}
>
<SelectTrigger>
<SelectValue placeholder="不绑定证书" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none">不绑定证书</SelectItem>
{(certificatesQuery.data ?? []).map((certificate) => (
<SelectItem key={certificate.id} value={String(certificate.id)}>
{certificate.name} · {certificate.primary_domain}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label htmlFor="domain-remark">备注</Label>
<Textarea id="domain-remark" rows={2} {...form.register('remark')} />
</div>
</form>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
取消
</Button>
<Button
type="submit"
form="zone-domain-create"
disabled={mutation.isPending}
>
{mutation.isPending ? (
<Loader2 className="mr-1 size-4 animate-spin" />
) : null}
添加域名
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<QuickCreateZoneDomainDialog
open={open}
onOpenChange={onOpenChange}
fixedZoneId={zoneId}
fixedZoneRoot={zoneRoot}
onCreated={async () => {
await onSaved();
}}
/>
);
}
@@ -32,6 +32,7 @@ import {ZoneDomainDialog} from './zone-domain-dialog';
export function ZoneDomainsTable({
zoneId,
zoneRoot,
domains,
certificates,
routes,
@@ -39,6 +40,7 @@ export function ZoneDomainsTable({
onChanged,
}: {
zoneId: number;
zoneRoot: string;
domains: ZoneDomainItem[];
certificates: TlsCertificateItem[];
routes: ProxyRouteItem[];
@@ -98,9 +100,6 @@ export function ZoneDomainsTable({
<TableHead className="h-8 whitespace-nowrap py-2 min-w-[200px]">
上游
</TableHead>
<TableHead className="h-8 whitespace-nowrap py-2 min-w-[120px]">
备注
</TableHead>
<TableHead className="sticky right-0 z-10 h-8 w-[90px] bg-background py-2 text-center">
操作
</TableHead>
@@ -160,9 +159,6 @@ export function ZoneDomainsTable({
</div>
)}
</TableCell>
<TableCell className="max-w-[160px] truncate py-1 font-mono text-[10px] text-muted-foreground">
{domain.remark || '—'}
</TableCell>
<TableCell
className="sticky right-0 z-10 bg-background py-1 text-center"
onClick={(event) => event.stopPropagation()}
@@ -220,6 +216,7 @@ export function ZoneDomainsTable({
open={createOpen}
onOpenChange={setCreateOpen}
zoneId={zoneId}
zoneRoot={zoneRoot}
onSaved={onChanged}
/>
@@ -11,15 +11,78 @@ import {Button} from '@/components/ui/button'
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from '@/components/ui/dialog'
import {Input} from '@/components/ui/input'
import {Label} from '@/components/ui/label'
import {Textarea} from '@/components/ui/textarea'
import {ZoneService, zoneQueryKey, type ZoneItem} from '@/lib/services/openflare'
const schema = z.object({domain: z.string().trim().min(1, '请输入 Zone 根域').refine((value) => !/[*/?#@]|:\/\//.test(value), '请输入不含协议或通配符的根域'), remark: z.string().max(255)})
const schema = z.object({
domain: z
.string()
.trim()
.min(1, '请输入 Zone 根域')
.refine((value) => !/[*/?#@]|:\/\//.test(value), '请输入不含协议或通配符的根域'),
})
type Values = z.infer<typeof schema>
export function ZoneEditorDialog({open, onOpenChange, zone}: {open: boolean; onOpenChange(open: boolean): void; zone?: ZoneItem | null}) {
const queryClient = useQueryClient(); const form = useForm<Values>({resolver: zodResolver(schema), defaultValues: {domain: '', remark: ''}})
useEffect(() => { if (open) form.reset({domain: zone?.domain ?? '', remark: zone?.remark ?? ''}) }, [form, open, zone])
const mutation = useMutation({mutationFn: (values: Values) => zone ? ZoneService.update(zone.id, values) : ZoneService.create(values), onSuccess: async () => { toast.success(zone ? 'Zone 已更新' : 'Zone 已创建'); await queryClient.invalidateQueries({queryKey: zoneQueryKey}); onOpenChange(false) }, onError: (error) => toast.error(error instanceof Error ? error.message : '保存失败')})
return <Dialog open={open} onOpenChange={onOpenChange}><DialogContent><DialogHeader><DialogTitle>{zone ? '编辑 Zone' : '新增 Zone'}</DialogTitle><DialogDescription>Zone 仅接受可注册根域,例如 arctel.de。</DialogDescription></DialogHeader><form id="zone-editor" className="space-y-4" onSubmit={form.handleSubmit((values) => mutation.mutate({domain: values.domain.toLowerCase(), remark: values.remark.trim()}))}><div className="space-y-1.5"><Label htmlFor="zone-domain">根域</Label><Input id="zone-domain" placeholder="arctel.de" {...form.register('domain')} />{form.formState.errors.domain && <p className="text-xs text-destructive">{form.formState.errors.domain.message}</p>}</div><div className="space-y-1.5"><Label htmlFor="zone-remark">备注</Label><Textarea id="zone-remark" rows={2} {...form.register('remark')} /></div></form><DialogFooter><Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button><Button form="zone-editor" type="submit" disabled={mutation.isPending}>{mutation.isPending && <Loader2 className="mr-1 size-4 animate-spin" />}{zone ? '保存修改' : '新增 Zone'}</Button></DialogFooter></DialogContent></Dialog>
export function ZoneEditorDialog({
open,
onOpenChange,
zone,
}: {
open: boolean
onOpenChange(open: boolean): void
zone?: ZoneItem | null
}) {
const queryClient = useQueryClient()
const form = useForm<Values>({
resolver: zodResolver(schema),
defaultValues: {domain: ''},
})
useEffect(() => {
if (open) form.reset({domain: zone?.domain ?? ''})
}, [form, open, zone])
const mutation = useMutation({
mutationFn: (values: Values) =>
zone
? ZoneService.update(zone.id, {domain: values.domain.toLowerCase()})
: ZoneService.create({domain: values.domain.toLowerCase()}),
onSuccess: async () => {
toast.success(zone ? 'Zone 已更新' : 'Zone 已创建')
await queryClient.invalidateQueries({queryKey: zoneQueryKey})
onOpenChange(false)
},
onError: (error) => toast.error(error instanceof Error ? error.message : '保存失败'),
})
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{zone ? '编辑 Zone' : '新增 Zone'}</DialogTitle>
<DialogDescription>Zone 仅接受可注册根域,例如 arctel.de。</DialogDescription>
</DialogHeader>
<form
id="zone-editor"
className="space-y-4"
onSubmit={form.handleSubmit((values) =>
mutation.mutate({domain: values.domain.toLowerCase()}),
)}
>
<div className="space-y-1.5">
<Label htmlFor="zone-domain">根域</Label>
<Input id="zone-domain" placeholder="arctel.de" {...form.register('domain')} />
{form.formState.errors.domain && (
<p className="text-xs text-destructive">{form.formState.errors.domain.message}</p>
)}
</div>
</form>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
取消
</Button>
<Button form="zone-editor" type="submit" disabled={mutation.isPending}>
{mutation.isPending && <Loader2 className="mr-1 size-4 animate-spin" />}
{zone ? '保存修改' : '新增 Zone'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -9,6 +9,6 @@ export function ZoneOverviewPanel({overview}: {overview: ZoneOverview}) {
<Card className="shadow-none"><CardHeader className="pb-2"><CardTitle className="text-sm">域名</CardTitle><CardDescription>显式 FQDN</CardDescription></CardHeader><CardContent className="text-2xl font-semibold">{overview.domains.length}</CardContent></Card>
<Card className="shadow-none"><CardHeader className="pb-2"><CardTitle className="text-sm">关联路由</CardTitle><CardDescription>已绑定的不同规则</CardDescription></CardHeader><CardContent className="text-2xl font-semibold">{boundRoutes}</CardContent></Card>
<Card className="shadow-none"><CardHeader className="pb-2"><CardTitle className="text-sm">已配证书</CardTitle><CardDescription>域名级 TLS 绑定</CardDescription></CardHeader><CardContent className="text-2xl font-semibold">{certificates}</CardContent></Card>
<Card className="shadow-none md:col-span-3"><CardHeader className="pb-2"><CardTitle className="text-sm">Zone 信息</CardTitle></CardHeader><CardContent className="grid gap-3 text-sm sm:grid-cols-2"><div><p className="text-xs text-muted-foreground">根域</p><p className="mt-1 font-medium">{overview.zone.domain}</p></div><div><p className="text-xs text-muted-foreground">创建时间</p><p className="mt-1">{formatDateTime(overview.zone.created_at)}</p></div><div className="sm:col-span-2"><p className="text-xs text-muted-foreground">备注</p><p className="mt-1">{overview.zone.remark || '暂无备注'}</p></div></CardContent></Card>
<Card className="shadow-none md:col-span-3"><CardHeader className="pb-2"><CardTitle className="text-sm">Zone 信息</CardTitle></CardHeader><CardContent className="grid gap-3 text-sm sm:grid-cols-2"><div><p className="text-xs text-muted-foreground">根域</p><p className="mt-1 font-medium">{overview.zone.domain}</p></div><div><p className="text-xs text-muted-foreground">创建时间</p><p className="mt-1">{formatDateTime(overview.zone.created_at)}</p></div></CardContent></Card>
</div>
}
@@ -199,6 +199,7 @@ export function ZonePageClient({zoneId}: {zoneId: number}) {
<TabsContent value="domains">
<ZoneDomainsTable
zoneId={zoneId}
zoneRoot={overview.zone.domain}
domains={overview.domains}
certificates={certificates}
routes={routes}