From 285f127d48d5119fb38f4d571ffee4290f77b78a Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 17 Jul 2026 18:20:21 +0800 Subject: [PATCH] feat(proxy-routes): align create form with upstream type selection Let new proxy rules choose direct, tunnel, or Pages origin the same way as the detail reverse-proxy section, instead of only accepting a single upstream URL. --- .../components/proxy-route-create-sheet.tsx | 331 ++++++++++++++++-- frontend/lib/theme/themes.json | 2 +- 2 files changed, 304 insertions(+), 29 deletions(-) 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 32220c3c..156058d3 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 @@ -17,6 +17,14 @@ import { FormMessage, } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { Sheet, SheetContent, @@ -26,14 +34,17 @@ import { SheetTitle, } from '@/components/ui/sheet'; import { Switch } from '@/components/ui/switch'; +import { Textarea } from '@/components/ui/textarea'; import type { ProxyRouteItem } from '@/lib/services/openflare'; import { + NodeService, + PagesService, ProxyRouteService, ZoneService, zoneQueryKey, } from '@/lib/services/openflare'; -import { listAllZoneDomains, parseOriginUrl } from './helpers'; +import { listAllZoneDomains, parseOriginUrl, parseOriginUrls } from './helpers'; import { ZoneDomainSelector } from './zone-domain-selector'; const createProxyRouteSchema = z @@ -42,24 +53,56 @@ const createProxyRouteSchema = z zone_domain_ids: z .array(z.number().int().positive()) .min(1, '请至少选择一个域名'), - origin_url: z.string().trim().min(1, '请输入上游地址'), + upstream_type: z.enum(['direct', 'tunnel', 'pages']), + origin_urls_text: z.string().trim(), + tunnel_id: z.string().optional(), + tunnel_target_addr: z.string().trim().optional(), + tunnel_target_protocol: z.enum(['http', 'https']).optional(), + pages_project_id: z.string().optional(), enabled: z.boolean(), }) .superRefine((value, context) => { - try { - const parsed = new URL(value.origin_url); - if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + if (value.upstream_type === 'direct') { + if (!value.origin_urls_text.trim()) { context.addIssue({ code: z.ZodIssueCode.custom, - path: ['origin_url'], - message: '上游地址必须以 http:// 或 https:// 开头', + path: ['origin_urls_text'], + message: '请至少填写一个上游地址', + }); + } else { + const { error } = parseOriginUrls(value.origin_urls_text); + if (error) { + context.addIssue({ + code: z.ZodIssueCode.custom, + path: ['origin_urls_text'], + message: error, + }); + } + } + return; + } + if (value.upstream_type === 'tunnel') { + if (!value.tunnel_id) { + context.addIssue({ + code: z.ZodIssueCode.custom, + path: ['tunnel_id'], + message: '请选择内网穿透隧道', }); } - } catch { + if (!value.tunnel_target_addr?.trim()) { + context.addIssue({ + code: z.ZodIssueCode.custom, + path: ['tunnel_target_addr'], + message: '请填写内网服务地址 (如 127.0.0.1:8080)', + }); + } + return; + } + if (!value.pages_project_id) { context.addIssue({ code: z.ZodIssueCode.custom, - path: ['origin_url'], - message: '上游地址格式不合法', + path: ['pages_project_id'], + message: '请选择 Pages 项目', }); } }); @@ -69,7 +112,12 @@ type CreateProxyRouteFormValues = z.infer; const defaultValues: CreateProxyRouteFormValues = { site_name: '', zone_domain_ids: [], - origin_url: '', + upstream_type: 'direct', + origin_urls_text: '', + tunnel_id: '', + tunnel_target_addr: '', + tunnel_target_protocol: 'http', + pages_project_id: '', enabled: true, }; @@ -101,6 +149,27 @@ export function ProxyRouteCreateSheet({ enabled: open, }); + const tunnelsQuery = useQuery({ + queryKey: ['openflare', 'nodes'], + queryFn: () => NodeService.listNodes(), + enabled: open, + }); + + const pagesProjectsQuery = useQuery({ + queryKey: ['openflare', 'pages-projects'], + queryFn: () => PagesService.listProjects(), + enabled: open, + }); + + const tunnelClients = (tunnelsQuery.data ?? []).filter( + (node) => node.node_type === 'tunnel_client', + ); + const pagesProjects = (pagesProjectsQuery.data ?? []).filter( + (project) => project.enabled && project.active_deployment_id, + ); + + const upstreamType = form.watch('upstream_type'); + useEffect(() => { if (!open) { form.reset(defaultValues); @@ -108,7 +177,33 @@ export function ProxyRouteCreateSheet({ }, [form, open]); const handleSubmit = form.handleSubmit(async (values) => { - const origin = parseOriginUrl(values.origin_url.trim()); + let originUrl = ''; + let originScheme: 'http' | 'https' = 'http'; + let originAddress = ''; + let originPort = ''; + let originUri = ''; + let upstreams: string[] = []; + + if (values.upstream_type === 'direct') { + const { urls } = parseOriginUrls(values.origin_urls_text); + const primaryOrigin = parseOriginUrl(urls[0]); + originUrl = urls[0]; + originScheme = primaryOrigin.scheme; + originAddress = primaryOrigin.address; + originPort = primaryOrigin.port; + originUri = primaryOrigin.uri; + upstreams = urls.slice(1); + } else if (values.upstream_type === 'tunnel') { + originUrl = `${values.tunnel_target_protocol}://${values.tunnel_target_addr}`; + originScheme = values.tunnel_target_protocol as 'http' | 'https'; + originAddress = values.tunnel_target_addr || ''; + } else { + originUrl = 'http://127.0.0.1'; + originScheme = 'http'; + originAddress = '127.0.0.1'; + originPort = '80'; + } + const selectedDomains = (domainsQuery.data ?? []).filter((domain) => values.zone_domain_ids.includes(domain.id), ); @@ -120,13 +215,13 @@ export function ProxyRouteCreateSheet({ 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, - origin_address: origin.address, - origin_port: origin.port, - origin_uri: origin.uri, + origin_url: originUrl, + origin_scheme: originScheme, + origin_address: originAddress, + origin_port: originPort, + origin_uri: originUri, origin_host: '', - upstreams: [], + upstreams, enabled: values.enabled, enable_https: hasCert, redirect_http: false, @@ -138,7 +233,21 @@ export function ProxyRouteCreateSheet({ cache_rules: [], custom_headers: [], basic_auth_enabled: false, - upstream_type: 'direct', + upstream_type: values.upstream_type, + tunnel_node_id: + values.upstream_type === 'tunnel' && values.tunnel_id + ? Number(values.tunnel_id) + : null, + tunnel_target_addr: + values.upstream_type === 'tunnel' ? values.tunnel_target_addr : '', + tunnel_target_protocol: + values.upstream_type === 'tunnel' + ? values.tunnel_target_protocol + : '', + pages_project_id: + values.upstream_type === 'pages' && values.pages_project_id + ? Number(values.pages_project_id) + : null, }); form.reset(defaultValues); @@ -200,9 +309,6 @@ export function ProxyRouteCreateSheet({ }} /> - - 勾选已登记域名,或使用「快捷新增域名」创建后自动勾选。 - )} @@ -210,18 +316,187 @@ export function ProxyRouteCreateSheet({ ( - - 上游地址 - - - + + 回源方式 +
+ {( + [ + ['direct', '直连上游'], + ['tunnel', '内网穿透 (Tunnel)'], + ['pages', 'Pages 静态站点'], + ] as const + ).map(([value, label]) => ( + + ))} +
)} /> + {upstreamType === 'direct' ? ( + ( + + 上游地址 + +