'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; 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({ 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 => item != null); }, [domainsQuery.data, route.id, route.zone_domains, selectedIDs]); const hasCertificate = selectedDomains.some((domain) => domain.cert_id != null); return (
{ 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, }, '域名设置已保存', ); })} > (
启用站点 关闭后会保留配置,但不会参与发布。
)} /> ( 站点标识 建议使用稳定、可读的业务标识,不必与域名完全一致。 )} /> ( 绑定域名 仅选择绑定关系;证书与 FQDN 请到对应 Zone 详情页管理。 )} /> (
HTTP 自动跳转到 HTTPS {hasCertificate ? '开启后会额外生成 80 端口重定向规则。' : '所选域名至少绑定一张证书后才能启用。'}
)} />
); }