'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 { zoneQueryKey, ZoneService } 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, 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, }, '域名设置已保存', ); })} > (
启用站点 关闭后会保留配置,但不会参与发布。
)} /> ( 站点标识 建议使用稳定、可读的业务标识,不必与域名完全一致。 )} /> ( 绑定域名 { await domainsQuery.refetch(); }} /> 从已登记域名中勾选绑定;可用「快捷新增域名」在 Zone 下创建 FQDN 并自动勾选。 )} /> (
HTTP 自动跳转到 HTTPS {hasCertificate ? '开启后会额外生成 80 端口重定向规则。' : '所选域名至少绑定一张证书后才能启用。'}
)} />
); }