'use client'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQuery } from '@tanstack/react-query'; import { useEffect, useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { z } from 'zod'; import { Drawer } from '@/components/ui/drawer'; import { getManagedDomains } from '@/features/managed-domains/api/managed-domains'; import { createProxyRoute } from '@/features/proxy-routes/api/proxy-routes'; import { DomainListInput, type DomainListRow, } from '@/features/proxy-routes/components/domain-list-input'; import { buildOriginUrl, getErrorMessage, parseOriginUrl, parseOriginUrls, validateDomains, } from '@/features/proxy-routes/helpers'; import type { ProxyRouteItem } from '@/features/proxy-routes/types'; import { getTlsCertificates } from '@/features/tls-certificates/api/tls-certificates'; import { PrimaryButton, ResourceField, ResourceInput, ResourceTextarea, ToggleField, } from '@/features/shared/components/resource-primitives'; const domainRowSchema = z.object({ domain: z.string(), certificateId: z.string(), }); const createWebsiteSchema = z .object({ site_name: z.string().trim().max(255, '站点标识不能超过 255 个字符'), domain_rows: z.array(domainRowSchema).min(1), origin_urls_text: z.string().trim().min(1, '请至少填写一个上游地址'), enabled: z.boolean(), redirect_http: z.boolean(), remark: z.string().max(255, '备注不能超过 255 个字符'), }) .superRefine((value, context) => { const domains = value.domain_rows .map((item) => item.domain.trim().toLowerCase()) .filter(Boolean); const domainError = validateDomains(domains); if (domainError) { context.addIssue({ code: z.ZodIssueCode.custom, path: ['domain_rows'], message: domainError, }); } const { error } = parseOriginUrls(value.origin_urls_text); if (error) { context.addIssue({ code: z.ZodIssueCode.custom, path: ['origin_urls_text'], message: error, }); } const selectedCertificateCount = new Set( value.domain_rows .map((item) => Number(item.certificateId)) .filter((item) => Number.isFinite(item) && item > 0), ).size; if (value.redirect_http && selectedCertificateCount === 0) { context.addIssue({ code: z.ZodIssueCode.custom, path: ['redirect_http'], message: '启用 HTTP 跳转前,请先为域名选择证书', }); } }); type CreateWebsiteFormValues = z.infer; const defaultValues: CreateWebsiteFormValues = { site_name: '', domain_rows: [{ domain: '', certificateId: '' }], origin_urls_text: '', enabled: true, redirect_http: false, remark: '', }; function normalizeSelectedCertificateIDs(rows: DomainListRow[]) { return Array.from( new Set( rows .filter((item) => item.domain.trim() !== '') .map((item) => Number(item.certificateId)) .filter((item) => Number.isFinite(item) && item > 0), ), ); } function buildDomainCertificateIDs(rows: DomainListRow[]) { return rows .filter((item) => item.domain.trim() !== '') .map((item) => { const certificateID = Number(item.certificateId); return Number.isFinite(certificateID) && certificateID > 0 ? certificateID : 0; }); } export function ProxyRouteCreateDrawer({ open, onOpenChange, onCreated, domainSuggestionSources = [], }: { open: boolean; onOpenChange: (open: boolean) => void; onCreated: (route: ProxyRouteItem) => void; domainSuggestionSources?: string[]; }) { const form = useForm({ resolver: zodResolver(createWebsiteSchema), defaultValues, }); const managedDomainsQuery = useQuery({ queryKey: ['managed-domains'], queryFn: getManagedDomains, enabled: open, }); const certificatesQuery = useQuery({ queryKey: ['tls-certificates', 'list'], queryFn: getTlsCertificates, enabled: open, }); const combinedDomainSuggestions = useMemo( () => [ ...domainSuggestionSources, ...(managedDomainsQuery.data?.map((item) => item.domain) ?? []), ], [domainSuggestionSources, managedDomainsQuery.data], ); const selectedCertificateIDs = normalizeSelectedCertificateIDs( form.watch('domain_rows'), ); const createMutation = useMutation({ mutationFn: async (values: CreateWebsiteFormValues) => { const domains = values.domain_rows .map((item) => item.domain.trim().toLowerCase()) .filter(Boolean); const domainCertIDs = buildDomainCertificateIDs(values.domain_rows); const selectedCertIDs = normalizeSelectedCertificateIDs( values.domain_rows, ); const { urls } = parseOriginUrls(values.origin_urls_text); const primaryOrigin = parseOriginUrl(urls[0]); return createProxyRoute({ site_name: values.site_name.trim() || domains[0], domain: domains[0], domains, origin_id: null, origin_url: buildOriginUrl( primaryOrigin.scheme, primaryOrigin.address, primaryOrigin.port, primaryOrigin.uri, ), origin_scheme: primaryOrigin.scheme, origin_address: primaryOrigin.address, origin_port: primaryOrigin.port, origin_uri: primaryOrigin.uri, origin_host: '', upstreams: urls.slice(1), enabled: values.enabled, enable_https: selectedCertIDs.length > 0, cert_id: selectedCertIDs[0] ?? null, cert_ids: selectedCertIDs, domain_cert_ids: domainCertIDs, redirect_http: selectedCertIDs.length > 0 ? values.redirect_http : false, limit_conn_per_server: 0, limit_conn_per_ip: 0, limit_rate: '', cache_enabled: false, cache_policy: 'url', cache_rules: [], custom_headers: [], pow_enabled: false, pow_config: '{}', basic_auth_enabled: false, basic_auth_username: '', basic_auth_password: '', remark: values.remark.trim(), }); }, onSuccess: (route) => { form.reset(defaultValues); onOpenChange(false); onCreated(route); }, }); useEffect(() => { if (!open) { form.reset(defaultValues); } }, [form, open]); return ( {createMutation.isPending ? '创建中...' : '创建'} } >
createMutation.mutate(values))} > ( )} /> 0 ? '勾选后会额外生成 80 端口重定向规则。' : '至少为一个域名选择证书后才能启用。' } checked={form.watch('redirect_http')} disabled={selectedCertificateIDs.length === 0} onChange={(checked) => form.setValue('redirect_http', checked, { shouldDirty: true }) } /> form.setValue('enabled', checked, { shouldDirty: true }) } /> {createMutation.isError ? (

{getErrorMessage(createMutation.error)}

) : null}
); }