This commit is contained in:
ryan
2026-07-13 15:08:40 +08:00
parent 08e8eea932
commit 1eff7878a1
441 changed files with 26491 additions and 18405 deletions
@@ -1,18 +1,32 @@
'use client';
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';
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';
import {Form, FormControl, FormDescription, FormField, FormItem, FormLabel, 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 {Textarea} from '@/components/ui/textarea';
import type {ProxyRouteItem} from '@/lib/services/openflare';
import {NodeService, PagesService} from '@/lib/services/openflare';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
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 { Textarea } from '@/components/ui/textarea';
import type { ProxyRouteItem } from '@/lib/services/openflare';
import { NodeService, PagesService } from '@/lib/services/openflare';
import {
customHeadersToText,
@@ -21,9 +35,9 @@ import {
parseOriginUrls,
validateOriginHost,
} from '../../components/helpers';
import {proxyRouteFormIds} from '../helpers';
import {useRouteSectionSave} from '../hooks/use-route-section-save';
import {SectionShell} from './section-shell';
import { proxyRouteFormIds } from '../helpers';
import { useRouteSectionSave } from '../hooks/use-route-section-save';
import { SectionShell } from './section-shell';
const reverseProxySchema = z
.object({
@@ -86,7 +100,9 @@ const reverseProxySchema = z
});
}
const { error: headerError } = parseCustomHeadersText(value.custom_headers_text);
const { error: headerError } = parseCustomHeadersText(
value.custom_headers_text,
);
if (headerError) {
context.addIssue({
code: z.ZodIssueCode.custom,
@@ -104,8 +120,16 @@ interface ProxySectionProps {
onSavingChange?: (saving: boolean) => void;
}
export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySectionProps) {
const { saving, save } = useRouteSectionSave(route, onRouteUpdate, onSavingChange);
export function ProxySection({
route,
onRouteUpdate,
onSavingChange,
}: ProxySectionProps) {
const { saving, save } = useRouteSectionSave(
route,
onRouteUpdate,
onSavingChange,
);
const tunnelsQuery = useQuery({
queryKey: ['openflare', 'nodes'],
@@ -134,7 +158,9 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
tunnel_target_addr: route.tunnel_target_addr || '',
tunnel_target_protocol:
(route.tunnel_target_protocol as 'http' | 'https') || 'http',
pages_project_id: route.pages_project_id ? String(route.pages_project_id) : '',
pages_project_id: route.pages_project_id
? String(route.pages_project_id)
: '',
custom_headers_text: customHeadersToText(route.custom_header_list),
},
});
@@ -148,7 +174,9 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
tunnel_target_addr: route.tunnel_target_addr || '',
tunnel_target_protocol:
(route.tunnel_target_protocol as 'http' | 'https') || 'http',
pages_project_id: route.pages_project_id ? String(route.pages_project_id) : '',
pages_project_id: route.pages_project_id
? String(route.pages_project_id)
: '',
custom_headers_text: customHeadersToText(route.custom_header_list),
});
}, [form, route]);
@@ -157,15 +185,15 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
return (
<SectionShell
title="反向代理"
description="配置请求回源上游的策略与地址。"
title='反向代理'
description='配置请求回源上游的策略与地址。'
formId={proxyRouteFormIds.proxy}
saving={saving}
>
<Form {...form}>
<form
id={proxyRouteFormIds.proxy}
className="space-y-5"
className='space-y-5'
onSubmit={form.handleSubmit(async (values) => {
let originUrl = '';
let originScheme: 'http' | 'https' = 'http';
@@ -194,7 +222,9 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
originPort = '80';
}
const { headers } = parseCustomHeadersText(values.custom_headers_text);
const { headers } = parseCustomHeadersText(
values.custom_headers_text,
);
await save(
{
@@ -213,9 +243,13 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
? Number(values.tunnel_id)
: null,
tunnel_target_addr:
values.upstream_type === 'tunnel' ? values.tunnel_target_addr : '',
values.upstream_type === 'tunnel'
? values.tunnel_target_addr
: '',
tunnel_target_protocol:
values.upstream_type === 'tunnel' ? values.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)
@@ -227,11 +261,11 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
>
<FormField
control={form.control}
name="upstream_type"
name='upstream_type'
render={({ field }) => (
<FormItem className="space-y-3">
<FormItem className='space-y-3'>
<FormLabel>回源方式</FormLabel>
<div className="flex flex-wrap gap-4">
<div className='flex flex-wrap gap-4'>
{(
[
['direct', '直连上游'],
@@ -239,15 +273,18 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
['pages', 'Pages 静态站点'],
] as const
).map(([value, label]) => (
<label key={value} className="flex cursor-pointer items-center gap-2 text-sm">
<label
key={value}
className='flex cursor-pointer items-center gap-2 text-sm'
>
<input
type="radio"
type='radio'
value={value}
checked={field.value === value}
onChange={() => field.onChange(value)}
className="size-4 accent-primary"
className='size-4 accent-primary'
/>
<Label className="font-normal">{label}</Label>
<Label className='font-normal'>{label}</Label>
</label>
))}
</div>
@@ -259,13 +296,13 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
{upstreamType === 'direct' ? (
<FormField
control={form.control}
name="origin_urls_text"
name='origin_urls_text'
render={({ field }) => (
<FormItem>
<FormLabel>上游地址</FormLabel>
<FormControl>
<Textarea
className="min-h-40 font-mono text-xs"
className='min-h-40 font-mono text-xs'
placeholder={
'https://origin-a.internal:443\nhttps://origin-b.internal:443'
}
@@ -273,7 +310,9 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
/>
</FormControl>
<FormDescription>
每行一个完整 URL。第一行作为主回源,多上游模式请保持相同协议且不要包含 path 或 query。
每行一个完整
URL。第一行作为主回源,多上游模式请保持相同协议且不要包含
path 或 query。
</FormDescription>
<FormMessage />
</FormItem>
@@ -282,29 +321,37 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
) : null}
{upstreamType === 'tunnel' ? (
<div className="space-y-4 rounded-lg border border-dashed bg-muted/30 p-4">
<div className='space-y-4 rounded-lg border border-dashed bg-muted/30 p-4'>
<FormField
control={form.control}
name="tunnel_id"
name='tunnel_id'
render={({ field }) => (
<FormItem>
<FormLabel>选择内网穿透隧道</FormLabel>
<Select value={field.value || 'none'} onValueChange={(value) => field.onChange(value === 'none' ? '' : value)}>
<Select
value={field.value || 'none'}
onValueChange={(value) =>
field.onChange(value === 'none' ? '' : value)
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="请选择..." />
<SelectValue placeholder='请选择...' />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">请选择...</SelectItem>
<SelectItem value='none'>请选择...</SelectItem>
{tunnelClients.map((tunnel) => (
<SelectItem key={tunnel.id} value={String(tunnel.id)}>
{tunnel.name} ({tunnel.status === 'online' ? '在线' : '离线'})
{tunnel.name} (
{tunnel.status === 'online' ? '在线' : '离线'})
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>将请求转发到该隧道连接的客户端节点。</FormDescription>
<FormDescription>
将请求转发到该隧道连接的客户端节点。
</FormDescription>
<FormMessage />
</FormItem>
)}
@@ -312,7 +359,7 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
<FormField
control={form.control}
name="tunnel_target_protocol"
name='tunnel_target_protocol'
render={({ field }) => (
<FormItem>
<FormLabel>内网服务协议</FormLabel>
@@ -323,8 +370,8 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="http">HTTP</SelectItem>
<SelectItem value="https">HTTPS</SelectItem>
<SelectItem value='http'>HTTP</SelectItem>
<SelectItem value='https'>HTTPS</SelectItem>
</SelectContent>
</Select>
<FormMessage />
@@ -334,14 +381,16 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
<FormField
control={form.control}
name="tunnel_target_addr"
name='tunnel_target_addr'
render={({ field }) => (
<FormItem>
<FormLabel>内网服务地址</FormLabel>
<FormControl>
<Input placeholder="127.0.0.1:8080" {...field} />
<Input placeholder='127.0.0.1:8080' {...field} />
</FormControl>
<FormDescription>例如: 127.0.0.1:8080 或 192.168.1.10:80</FormDescription>
<FormDescription>
例如: 127.0.0.1:8080 或 192.168.1.10:80
</FormDescription>
<FormMessage />
</FormItem>
)}
@@ -350,29 +399,39 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
) : null}
{upstreamType === 'pages' ? (
<div className="rounded-lg border border-dashed bg-muted/30 p-4">
<div className='rounded-lg border border-dashed bg-muted/30 p-4'>
<FormField
control={form.control}
name="pages_project_id"
name='pages_project_id'
render={({ field }) => (
<FormItem>
<FormLabel>选择 Pages 项目</FormLabel>
<Select value={field.value || 'none'} onValueChange={(value) => field.onChange(value === 'none' ? '' : value)}>
<Select
value={field.value || 'none'}
onValueChange={(value) =>
field.onChange(value === 'none' ? '' : value)
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="请选择..." />
<SelectValue placeholder='请选择...' />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">请选择...</SelectItem>
<SelectItem value='none'>请选择...</SelectItem>
{pagesProjects.map((project) => (
<SelectItem key={project.id} value={String(project.id)}>
<SelectItem
key={project.id}
value={String(project.id)}
>
{project.name} ({project.slug})
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>仅显示已启用且已有激活部署的 Pages 项目。</FormDescription>
<FormDescription>
仅显示已启用且已有激活部署的 Pages 项目。
</FormDescription>
<FormMessage />
</FormItem>
)}
@@ -382,14 +441,16 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
<FormField
control={form.control}
name="origin_host"
name='origin_host'
render={({ field }) => (
<FormItem>
<FormLabel>Origin Host Header</FormLabel>
<FormControl>
<Input placeholder="origin.example.internal" {...field} />
<Input placeholder='origin.example.internal' {...field} />
</FormControl>
<FormDescription>留空时默认透传访问域名 $host。</FormDescription>
<FormDescription>
留空时默认透传访问域名 $host。
</FormDescription>
<FormMessage />
</FormItem>
)}
@@ -397,13 +458,13 @@ export function ProxySection({ route, onRouteUpdate, onSavingChange }: ProxySect
<FormField
control={form.control}
name="custom_headers_text"
name='custom_headers_text'
render={({ field }) => (
<FormItem>
<FormLabel>自定义请求头</FormLabel>
<FormControl>
<Textarea
className="min-h-32 font-mono text-xs"
className='min-h-32 font-mono text-xs'
placeholder={'X-Trace-Id: $request_id\nX-Site: marketing'}
{...field}
/>