Files
OpenFlare/frontend/app/(main)/proxy-routes/detail/components/domain-section.tsx
T
ryan 1160d5846a refactor(web): select route domains from zones
反代路由创建/域名配置改为绑定 zone_domain_ids,移除手写域名列表与
旧证书字段;列表与 WAF/UptimeKuma 消费端同步读取 zone_domains。
2026-07-12 15:23:48 +08:00

219 lines
7.4 KiB
TypeScript

'use client';
import {useEffect, useMemo} from 'react';
import {zodResolver} from '@hookform/resolvers/zod';
import {useQuery} from '@tanstack/react-query';
import {useForm} from 'react-hook-form';
import {z} from 'zod';
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 type {ProxyRouteItem} from '@/lib/services/openflare';
import {ZoneService, zoneQueryKey} from '@/lib/services/openflare';
import {listAllZoneDomains} from '../../components/helpers';
import {ZoneDomainSelector} from '../../components/zone-domain-selector';
import {proxyRouteFormIds} from '../helpers';
import {useRouteSectionSave} from '../hooks/use-route-section-save';
import {SectionShell} from './section-shell';
const domainSettingsSchema = z
.object({
site_name: z
.string()
.trim()
.min(1, '请输入站点标识')
.max(255, '站点标识不能超过 255 个字符'),
zone_domain_ids: z.array(z.number().int().positive()).min(1, '请至少选择一个域名'),
enabled: z.boolean(),
redirect_http: z.boolean(),
});
type DomainSettingsValues = z.infer<typeof domainSettingsSchema>;
interface DomainSectionProps {
route: ProxyRouteItem;
onRouteUpdate: (route: ProxyRouteItem) => void;
onSavingChange?: (saving: boolean) => void;
}
export function DomainSection({ route, onRouteUpdate, onSavingChange }: DomainSectionProps) {
const { saving, save } = useRouteSectionSave(route, onRouteUpdate, onSavingChange);
const zonesQuery = useQuery({
queryKey: zoneQueryKey,
queryFn: () => ZoneService.list(),
});
const domainsQuery = useQuery({
queryKey: [...zoneQueryKey, 'all-domains'],
queryFn: () => listAllZoneDomains(),
});
const form = useForm<DomainSettingsValues>({
resolver: zodResolver(domainSettingsSchema),
defaultValues: {
site_name: route.site_name,
zone_domain_ids: route.zone_domain_ids ?? [],
enabled: route.enabled,
redirect_http: route.redirect_http,
},
});
useEffect(() => {
form.reset({
site_name: route.site_name,
zone_domain_ids: route.zone_domain_ids ?? [],
enabled: route.enabled,
redirect_http: route.redirect_http,
});
}, [form, route]);
const selectedIDs = form.watch('zone_domain_ids');
const selectedDomains = useMemo(() => {
const fromApi = domainsQuery.data ?? [];
const byId = new Map(fromApi.map((domain) => [domain.id, domain]));
// Prefer live catalog; fall back to route-bound domains for display before catalog loads.
return selectedIDs
.map((id) => {
const catalog = byId.get(id);
if (catalog) {
return catalog;
}
const bound = (route.zone_domains ?? []).find((item) => item.id === id);
if (!bound) {
return null;
}
return {
id: bound.id,
zone_id: bound.zone_id,
proxy_route_id: route.id,
domain: bound.domain,
cert_id: bound.cert_id,
remark: '',
created_at: '',
updated_at: '',
};
})
.filter((item): item is NonNullable<typeof item> => item != null);
}, [domainsQuery.data, route.id, route.zone_domains, selectedIDs]);
const hasCertificate = selectedDomains.some((domain) => domain.cert_id != null);
return (
<SectionShell
title="域名设置"
description="绑定已在 Zone 中注册的 FQDN。证书请在 Zone 域名管理中维护。"
formId={proxyRouteFormIds.domains}
saving={saving}
>
<Form {...form}>
<form
id={proxyRouteFormIds.domains}
className="space-y-5"
onSubmit={form.handleSubmit(async (values) => {
if (values.redirect_http && !hasCertificate) {
form.setError('redirect_http', {
message: '启用 HTTP 跳转前,请先为所选域名绑定证书(在 Zone 中配置)',
});
return;
}
await save(
{
site_name: values.site_name.trim(),
zone_domain_ids: values.zone_domain_ids,
enabled: values.enabled,
enable_https: hasCertificate,
redirect_http: hasCertificate ? values.redirect_http : false,
},
'域名设置已保存',
);
})}
>
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<FormLabel>启用站点</FormLabel>
<FormDescription>关闭后会保留配置,但不会参与发布。</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="site_name"
render={({ field }) => (
<FormItem>
<FormLabel>站点标识</FormLabel>
<FormControl>
<Input placeholder="marketing-site" {...field} />
</FormControl>
<FormDescription>建议使用稳定、可读的业务标识,不必与域名完全一致。</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="zone_domain_ids"
render={({ field }) => (
<FormItem>
<FormLabel>绑定域名</FormLabel>
<FormControl>
<ZoneDomainSelector
value={field.value}
onChange={field.onChange}
domains={domainsQuery.data ?? []}
zones={zonesQuery.data ?? []}
currentRouteId={route.id}
disabled={domainsQuery.isLoading}
/>
</FormControl>
<FormDescription>
仅选择绑定关系;证书与 FQDN 请到对应 Zone 详情页管理。
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="redirect_http"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<FormLabel>HTTP 自动跳转到 HTTPS</FormLabel>
<FormDescription>
{hasCertificate
? '开启后会额外生成 80 端口重定向规则。'
: '所选域名至少绑定一张证书后才能启用。'}
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
disabled={!hasCertificate}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SectionShell>
);
}