'use client'; import Link from 'next/link'; import { useEffect, useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Expand, FileWarning, Loader2, Pencil, Save } from 'lucide-react'; import { toast } from 'sonner'; import { useAuth } from '@/components/providers/auth-provider'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { TagsInput } from '@/components/ui/tags-input'; import { previewOriginErrorPageHTML } from '@/lib/openflare/default-origin-error-page-html'; import { validateStatusCodeTagMessage, validateStatusCodeTags, } from '@/lib/openflare/status-code-tags'; import { OptionService } from '@/lib/services/openflare'; import { ErrorPageGate } from './components/error-page-gate'; import { defaultErrorPageFields, invalidateErrorPageQueries, KEY_ENABLED, KEY_GET_ONLY, KEY_STATUS_CODES, mapOptionsToFields, OPTIONS_QUERY_KEY, optionsToMap, type ErrorPageFields, } from './components/shared'; export default function ErrorPagesPage() { const { user } = useAuth(); const queryClient = useQueryClient(); const [fields, setFields] = useState(defaultErrorPageFields); const [tagError, setTagError] = useState(null); const optionsQuery = useQuery({ queryKey: OPTIONS_QUERY_KEY, queryFn: () => OptionService.list(), enabled: !!user?.is_admin, }); useEffect(() => { if (!optionsQuery.data) return; setFields(mapOptionsToFields(optionsToMap(optionsQuery.data))); setTagError(null); }, [optionsQuery.data]); const previewSrcDoc = useMemo( () => previewOriginErrorPageHTML(fields.html), [fields.html], ); /** 仅保存策略项(HTML 在编辑页单独保存) */ const savePolicyMutation = useMutation({ mutationFn: async () => { validateStatusCodeTags(fields.statusCodes); await OptionService.updateBatch([ { key: KEY_ENABLED, value: String(fields.enabled) }, { key: KEY_GET_ONLY, value: String(fields.getOnly) }, { key: KEY_STATUS_CODES, value: JSON.stringify(fields.statusCodes), }, ]); }, onSuccess: async () => { toast.success('触发策略已保存,请前往版本发布使配置生效'); await invalidateErrorPageQueries(queryClient); }, onError: (error) => { toast.error(error instanceof Error ? error.message : '保存失败'); }, }); const handleValidateTag = (tag: string) => { const message = validateStatusCodeTagMessage(tag); if (message) { setTagError(message); toast.error(message); return message; } setTagError(null); return null; }; return (

错误页

配置源站/网关错误响应时的统一 HTML 页面。策略与模板保存后需发布配置版本后生效。

触发策略 配置源站错误页的启用条件与触发状态码。修改后需点击保存。

关闭后会透传源站或 Nginx 默认错误响应。

setFields((prev) => ({ ...prev, enabled })) } aria-label='启用源站错误页' className='mt-0.5 shrink-0' />

开启后仅对 GET 请求的匹配错误状态码返回自定义错误页;POST/PUT 等其它方法直接透传源站响应。

setFields((prev) => ({ ...prev, getOnly })) } aria-label='仅针对 GET 请求' className='mt-0.5 shrink-0' />

支持单码(如 502)或闭区间(如 500-599),范围 400–599。默认 500-599。

{ setFields((prev) => ({ ...prev, statusCodes })); setTagError(null); }} validateTag={handleValidateTag} placeholder='例如 502 或 500-599,回车添加' aria-invalid={!!tagError} disabled={!fields.enabled} /> {tagError ? (

{tagError}

) : null}
页面预览