'use client'; import { useEffect } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { Loader2 } from 'lucide-react'; import { z } from 'zod'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { Textarea } from '@/components/ui/textarea'; import { type PagesProject, PagesService } from '@/lib/services/openflare'; import { projectQueryKey, projectsQueryKey } from './pages-utils'; const pagesProjectSchema = z .object({ name: z.string().trim().min(1, '请输入项目名称').max(255), slug: z.string().trim().max(255).optional().or(z.literal('')), description: z.string().trim().max(1000).optional().or(z.literal('')), spa_fallback_enabled: z.boolean(), spa_fallback_path: z.string().trim(), api_proxy_enabled: z.boolean(), api_proxy_path: z.string().trim(), api_proxy_pass: z.string().trim(), api_proxy_rewrite: z.string().trim(), root_dir: z.string().trim().max(512).optional().or(z.literal('')), entry_file: z.string().trim().min(1, '请输入入口文件').max(512), }) .superRefine((data, ctx) => { if (data.spa_fallback_enabled && !data.spa_fallback_path.startsWith('/')) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['spa_fallback_path'], message: '回退路径必须以 / 开头', }); } if (data.api_proxy_enabled) { if (!data.api_proxy_path.startsWith('/')) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['api_proxy_path'], message: '匹配路径必须以 / 开头', }); } if (!/^https?:\/\//i.test(data.api_proxy_pass)) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['api_proxy_pass'], message: '后端地址必须以 http:// 或 https:// 开头', }); } } }); type PagesProjectFormValues = z.infer; function toFormValues(project?: PagesProject | null): PagesProjectFormValues { if (!project) { return { name: '', slug: '', description: '', spa_fallback_enabled: false, spa_fallback_path: '/index.html', api_proxy_enabled: false, api_proxy_path: '', api_proxy_pass: '', api_proxy_rewrite: '', root_dir: '', entry_file: 'index.html', }; } return { name: project.name, slug: project.slug, description: project.description || '', spa_fallback_enabled: project.spa_fallback_enabled, spa_fallback_path: project.spa_fallback_path, api_proxy_enabled: project.api_proxy_enabled || false, api_proxy_path: project.api_proxy_path || '', api_proxy_pass: project.api_proxy_pass || '', api_proxy_rewrite: project.api_proxy_rewrite || '', root_dir: project.root_dir || '', entry_file: project.entry_file || 'index.html', }; } interface ProjectEditorDialogProps { open: boolean; onOpenChange: (open: boolean) => void; project?: PagesProject | null; } export function ProjectEditorDialog({ open, onOpenChange, project, }: ProjectEditorDialogProps) { const queryClient = useQueryClient(); const form = useForm({ resolver: zodResolver(pagesProjectSchema), defaultValues: toFormValues(project), }); useEffect(() => { if (open) form.reset(toFormValues(project)); }, [form, project, open]); const mutation = useMutation({ mutationFn: async (values: PagesProjectFormValues) => { const payload = { name: values.name.trim(), slug: values.slug?.trim() || '', description: values.description?.trim() || '', enabled: project ? project.enabled : true, spa_fallback_enabled: values.spa_fallback_enabled, spa_fallback_path: values.spa_fallback_enabled ? values.spa_fallback_path.trim() : project?.spa_fallback_path || '/index.html', api_proxy_enabled: values.api_proxy_enabled, api_proxy_path: values.api_proxy_enabled ? values.api_proxy_path.trim() : '', api_proxy_pass: values.api_proxy_enabled ? values.api_proxy_pass.trim() : '', api_proxy_rewrite: values.api_proxy_enabled ? values.api_proxy_rewrite.trim() : '', root_dir: values.root_dir?.trim() || '', entry_file: values.entry_file.trim(), }; return project ? PagesService.updateProject(project.id, payload) : PagesService.createProject(payload); }, onSuccess: async () => { toast.success(project ? '项目已更新' : '项目已创建'); await queryClient.invalidateQueries({ queryKey: projectsQueryKey }); if (project) { await queryClient.invalidateQueries({ queryKey: projectQueryKey(project.id), }); } onOpenChange(false); }, onError: (error) => { toast.error(error instanceof Error ? error.message : '保存失败'); }, }); const spaEnabled = form.watch('spa_fallback_enabled'); const apiEnabled = form.watch('api_proxy_enabled'); return ( {project ? '编辑 Pages 项目' : '新建 Pages 项目'} 配置静态站点托管参数,上传部署包后在代理规则中选择 Pages 上游。
mutation.mutate(values))} >
{form.formState.errors.name ? (

{form.formState.errors.name.message}

) : null}