"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}