From 1160d5846af9d27ad2a16d497f250fbae16f9ba1 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 12 Jul 2026 15:23:48 +0800 Subject: [PATCH] refactor(web): select route domains from zones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 反代路由创建/域名配置改为绑定 zone_domain_ids,移除手写域名列表与 旧证书字段;列表与 WAF/UptimeKuma 消费端同步读取 zone_domains。 --- .../components/uptimekuma-site-modal.tsx | 14 +- .../(main)/proxy-routes/components/helpers.ts | 41 ++- .../components/proxy-route-create-sheet.tsx | 82 +++--- .../components/zone-domain-selector.tsx | 161 +++++++++++ .../detail/components/domain-list-input.tsx | 262 ------------------ .../detail/components/domain-section.tsx | 147 +++++----- .../app/(main)/proxy-routes/detail/helpers.ts | 77 +---- .../app/(main)/proxy-routes/page-client.tsx | 18 +- .../waf/components/site-binding-sheet.tsx | 8 +- .../tests/zone/zone-domain-selector.test.tsx | 81 ++++++ 10 files changed, 415 insertions(+), 476 deletions(-) create mode 100644 frontend/app/(main)/proxy-routes/components/zone-domain-selector.tsx delete mode 100644 frontend/app/(main)/proxy-routes/detail/components/domain-list-input.tsx create mode 100644 frontend/tests/zone/zone-domain-selector.test.tsx diff --git a/frontend/app/(main)/admin/settings/components/uptimekuma-site-modal.tsx b/frontend/app/(main)/admin/settings/components/uptimekuma-site-modal.tsx index dafd7442..9d3f3b32 100644 --- a/frontend/app/(main)/admin/settings/components/uptimekuma-site-modal.tsx +++ b/frontend/app/(main)/admin/settings/components/uptimekuma-site-modal.tsx @@ -48,11 +48,13 @@ export function UptimeKumaSiteSelectModal({ const routes = routesQuery.data ?? [] const keyword = searchTerm.trim().toLowerCase() if (!keyword) return routes - return routes.filter( - (route) => + return routes.filter((route) => { + const domains = (route.zone_domains ?? []).map((item) => item.domain).join(' ') + return ( route.site_name.toLowerCase().includes(keyword) || - route.primary_domain.toLowerCase().includes(keyword), - ) + domains.toLowerCase().includes(keyword) + ) + }) }, [routesQuery.data, searchTerm]) const toggleSite = (siteName: string) => { @@ -147,7 +149,9 @@ export function UptimeKumaSiteSelectModal({ /> {route.site_name} - {route.primary_domain} + + {(route.zone_domains ?? []).map((item) => item.domain).join(', ') || '—'} + ) })} diff --git a/frontend/app/(main)/proxy-routes/components/helpers.ts b/frontend/app/(main)/proxy-routes/components/helpers.ts index b0184434..425ea523 100644 --- a/frontend/app/(main)/proxy-routes/components/helpers.ts +++ b/frontend/app/(main)/proxy-routes/components/helpers.ts @@ -3,13 +3,15 @@ import type { ProxyRouteCustomHeader, ProxyRouteItem, ProxyRouteMutationPayload, + ZoneDomainItem, } from '@/lib/services/openflare'; +import {ZoneService} from '@/lib/services/openflare'; export const proxyRouteConfigSections = [ { key: 'domains' as const, label: '域名设置', - description: '维护站点标识、域名列表和证书绑定。', + description: '维护站点标识,并从 Zone 中选择绑定域名。', }, { key: 'limits' as const, @@ -77,6 +79,20 @@ export function parseOriginUrl(originUrl: string) { }; } +/** Domain FQDNs bound to a proxy route (from zone_domains). */ +export function getRouteDomainNames(route: ProxyRouteItem): string[] { + return (route.zone_domains ?? []).map((item) => item.domain).filter(Boolean); +} + +export function getRoutePrimaryDomain(route: ProxyRouteItem): string { + return getRouteDomainNames(route)[0] ?? ''; +} + +export function getRouteDomainsLabel(route: ProxyRouteItem): string { + const names = getRouteDomainNames(route); + return names.length > 0 ? names.join(', ') : '未绑定域名'; +} + export function getUpstreamSummary(route: ProxyRouteItem): string { if (route.upstream_type === 'pages') { return route.pages_project_id @@ -88,7 +104,7 @@ export function getUpstreamSummary(route: ProxyRouteItem): string { const target = route.tunnel_target_addr || '未配置目标'; return `Tunnel → ${protocol}://${target}`; } - if (route.upstream_list.length <= 1) { + if ((route.upstream_list ?? []).length <= 1) { return route.origin_url; } return `${route.upstream_list.length} 个上游,主上游 ${route.origin_url}`; @@ -284,12 +300,11 @@ export function buildPayloadFromRoute( const primaryOrigin = route.upstream_type === 'pages' ? parseOriginUrl('http://127.0.0.1') - : parseOriginUrl(route.origin_url); + : parseOriginUrl(route.origin_url || 'http://127.0.0.1'); return { site_name: route.site_name, - domain: route.primary_domain, - domains: route.domains, + zone_domain_ids: route.zone_domain_ids ?? [], origin_id: null, origin_url: route.origin_url, origin_scheme: primaryOrigin.scheme, @@ -297,20 +312,17 @@ export function buildPayloadFromRoute( origin_port: primaryOrigin.port, origin_uri: primaryOrigin.uri, origin_host: route.origin_host || '', - upstreams: route.upstream_list.slice(1), + upstreams: (route.upstream_list ?? []).slice(1), enabled: route.enabled, enable_https: route.enable_https, - cert_id: route.cert_id, - cert_ids: route.cert_ids, - domain_cert_ids: route.domain_cert_ids, redirect_http: route.redirect_http, limit_conn_per_server: route.limit_conn_per_server, limit_conn_per_ip: route.limit_conn_per_ip, limit_rate: route.limit_rate, cache_enabled: route.cache_enabled, cache_policy: route.cache_policy || 'url', - cache_rules: route.cache_rule_list, - custom_headers: route.custom_header_list, + cache_rules: route.cache_rule_list ?? [], + custom_headers: route.custom_header_list ?? [], remark: route.remark || '', basic_auth_enabled: route.basic_auth_enabled, basic_auth_username: route.basic_auth_username, @@ -323,3 +335,10 @@ export function buildPayloadFromRoute( ...overrides, }; } + +/** Load all Zone domains across registered Zones for route binding selectors. */ +export async function listAllZoneDomains(): Promise { + const zones = await ZoneService.list(); + const overviews = await Promise.all(zones.map((zone) => ZoneService.getOverview(zone.id))); + return overviews.flatMap((overview) => overview.domains ?? []); +} diff --git a/frontend/app/(main)/proxy-routes/components/proxy-route-create-sheet.tsx b/frontend/app/(main)/proxy-routes/components/proxy-route-create-sheet.tsx index 2b157a4d..6473d354 100644 --- a/frontend/app/(main)/proxy-routes/components/proxy-route-create-sheet.tsx +++ b/frontend/app/(main)/proxy-routes/components/proxy-route-create-sheet.tsx @@ -2,6 +2,7 @@ import {useEffect} from 'react'; import {zodResolver} from '@hookform/resolvers/zod'; +import {useQuery} from '@tanstack/react-query'; import {useForm} from 'react-hook-form'; import {z} from 'zod'; @@ -12,28 +13,20 @@ import {Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTi import {Switch} from '@/components/ui/switch'; import {Textarea} from '@/components/ui/textarea'; import type {ProxyRouteItem} from '@/lib/services/openflare'; -import {ProxyRouteService} from '@/lib/services/openflare'; +import {ProxyRouteService, ZoneService, zoneQueryKey} from '@/lib/services/openflare'; -import {parseOriginUrl, validateDomain} from './helpers'; +import {listAllZoneDomains, parseOriginUrl} from './helpers'; +import {ZoneDomainSelector} from './zone-domain-selector'; const createProxyRouteSchema = z .object({ site_name: z.string().trim().max(255, '站点标识不能超过 255 个字符'), - domain: z.string().trim().min(1, '请输入域名'), + zone_domain_ids: z.array(z.number().int().positive()).min(1, '请至少选择一个域名'), origin_url: z.string().trim().min(1, '请输入上游地址'), enabled: z.boolean(), remark: z.string().max(255, '备注不能超过 255 个字符'), }) .superRefine((value, context) => { - const domainError = validateDomain(value.domain); - if (domainError) { - context.addIssue({ - code: z.ZodIssueCode.custom, - path: ['domain'], - message: domainError, - }); - } - try { const parsed = new URL(value.origin_url); if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { @@ -56,7 +49,7 @@ type CreateProxyRouteFormValues = z.infer; const defaultValues: CreateProxyRouteFormValues = { site_name: '', - domain: '', + zone_domain_ids: [], origin_url: '', enabled: true, remark: '', @@ -78,6 +71,18 @@ export function ProxyRouteCreateSheet({ defaultValues, }); + const zonesQuery = useQuery({ + queryKey: zoneQueryKey, + queryFn: () => ZoneService.list(), + enabled: open, + }); + + const domainsQuery = useQuery({ + queryKey: [...zoneQueryKey, 'all-domains'], + queryFn: () => listAllZoneDomains(), + enabled: open, + }); + useEffect(() => { if (!open) { form.reset(defaultValues); @@ -85,14 +90,17 @@ export function ProxyRouteCreateSheet({ }, [form, open]); const handleSubmit = form.handleSubmit(async (values) => { - const domain = values.domain.trim().toLowerCase(); const origin = parseOriginUrl(values.origin_url.trim()); + const selectedDomains = (domainsQuery.data ?? []).filter((domain) => + values.zone_domain_ids.includes(domain.id), + ); + const primaryDomain = selectedDomains[0]?.domain ?? ''; + const hasCert = selectedDomains.some((domain) => domain.cert_id != null); try { const route = await ProxyRouteService.create({ - site_name: values.site_name.trim() || domain, - domain, - domains: [domain], + site_name: values.site_name.trim() || primaryDomain, + zone_domain_ids: values.zone_domain_ids, origin_id: null, origin_url: values.origin_url.trim(), origin_scheme: origin.scheme, @@ -102,10 +110,7 @@ export function ProxyRouteCreateSheet({ origin_host: '', upstreams: [], enabled: values.enabled, - enable_https: false, - cert_id: null, - cert_ids: [], - domain_cert_ids: [0], + enable_https: hasCert, redirect_http: false, limit_conn_per_server: 0, limit_conn_per_ip: 0, @@ -135,7 +140,7 @@ export function ProxyRouteCreateSheet({ 新建规则 - 创建网站后可进入详情页继续配置 HTTPS、缓存和限流等高级选项。 + 从已注册的 Zone 域名中选择绑定关系,创建后可继续配置缓存和限流等高级选项。 @@ -150,7 +155,7 @@ export function ProxyRouteCreateSheet({ - 可选,留空时会自动使用域名。 + 可选,留空时会自动使用首个域名。 )} @@ -158,13 +163,22 @@ export function ProxyRouteCreateSheet({ ( - 主域名 + 绑定域名 - + + + 域名与证书在 Zone 中维护;此处只选择本站点要绑定的 FQDN。 + )} @@ -177,9 +191,8 @@ export function ProxyRouteCreateSheet({ 上游地址 - + - 主回源完整 URL,创建后可在详情页添加多上游。 )} @@ -191,8 +204,8 @@ export function ProxyRouteCreateSheet({ render={({ field }) => (
- 创建后立即启用 - 关闭后站点会以草稿保存。 + 启用站点 + 关闭后会保留配置,但不会参与发布。
@@ -208,20 +221,23 @@ export function ProxyRouteCreateSheet({ 备注 -