From ba1c9222c2d6ace836584791fa0ce3ff6f5b61dc Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 6 Aug 2026 14:43:01 +0800 Subject: [PATCH] refactor(error-pages): preset templates with OpenFlare branding --- docs/changelog/index.md | 2 +- docs/design/index.md | 2 +- docs/design/origin-error-page.md | 4 +- docs/reference/configuration.md | 2 +- .../components/error-page-gate.tsx | 70 ++++ .../components/html-editor-workspace.tsx | 141 +++++++ .../(main)/error-pages/components/shared.ts | 72 ++++ frontend/app/(main)/error-pages/edit/page.tsx | 206 +++++++++ frontend/app/(main)/error-pages/page.tsx | 394 +++++------------- .../app/(main)/error-pages/preview/page.tsx | 82 ++++ frontend/lib/navigation/openflare-nav.ts | 6 +- .../default-origin-error-page-html.ts | 85 +--- .../origin-error-page-templates/baohaus.ts | 293 +++++++++++++ .../origin-error-page-templates/index.ts | 46 ++ .../origin-error-page-templates/line-style.ts | 309 ++++++++++++++ .../origin-error-page-templates/minimalist.ts | 109 +++++ frontend/package.json | 1 + frontend/pnpm-lock.yaml | 325 +++++++++++---- pkg/render/openresty/origin_error_page.go | 141 ++++--- .../openresty/origin_error_page_test.go | 4 +- 20 files changed, 1759 insertions(+), 535 deletions(-) create mode 100644 frontend/app/(main)/error-pages/components/error-page-gate.tsx create mode 100644 frontend/app/(main)/error-pages/components/html-editor-workspace.tsx create mode 100644 frontend/app/(main)/error-pages/components/shared.ts create mode 100644 frontend/app/(main)/error-pages/edit/page.tsx create mode 100644 frontend/app/(main)/error-pages/preview/page.tsx create mode 100644 frontend/lib/openflare/origin-error-page-templates/baohaus.ts create mode 100644 frontend/lib/openflare/origin-error-page-templates/index.ts create mode 100644 frontend/lib/openflare/origin-error-page-templates/line-style.ts create mode 100644 frontend/lib/openflare/origin-error-page-templates/minimalist.ts diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 3ae7e38f..d5eb411a 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -24,7 +24,7 @@ sidebar: false ### 新增 -- 新增全局源站错误页:可在「网站管理 → 错误页」配置开关、触发状态码(支持 `500-599` 区间与单码)与自定义 HTML;默认启用 Cloudflare 风格页面并保持真实 HTTP 状态码,修改后随配置版本发布下发到边缘,关闭后恢复透传。 +- 新增全局源站错误页:可在「网站管理 → 错误页」配置开关、触发状态码(支持 `500-599` 区间与单码)与自定义 HTML;默认启用 OpenFlare 极简错误页并保持真实 HTTP 状态码,修改后随配置版本发布下发到边缘,关闭后恢复透传。 - 新增 Cloudflare DNS 指向管理:可复用现有 Cloudflare DNS 账号或配置独立 Token,按分组将 ZoneDomain 的单条 A 记录异步同步到边缘节点 IPv4,并支持成员橙云、同步状态与节点 IP 变更联动。 ### 修复 diff --git a/docs/design/index.md b/docs/design/index.md index 4512d3f6..a2df9e0d 100644 --- a/docs/design/index.md +++ b/docs/design/index.md @@ -22,7 +22,7 @@ OpenFlare 适合需要统一管理多台 OpenResty 代理节点的团队,具 | 能力 | 说明 | 详细设计/使用指南 | | --- | --- | --- | | **反代配置管理** | 以网站规则(Proxy Route)为聚合边界,支持多域名与多上游负载均衡 | [新建反代配置](../guide/proxy-config.md) | -| **源站错误页** | 全局可配置:源站/网关匹配状态码时返回 Cloudflare 风格或自定义 HTML,HTTP 状态码保持原值 | [源站错误页设计](./origin-error-page.md) | +| **源站错误页** | 全局可配置:源站/网关匹配状态码时返回 OpenFlare 默认或自定义 HTML,HTTP 状态码保持原值 | [源站错误页设计](./origin-error-page.md) | | **边缘缓存** | 单节点 OpenResty `proxy_cache`;默认 static 扩展名 + 源站头/Set-Cookie 闸门 + 默认 Edge TTL(对标 CF 默认模型) | [边缘缓存策略设计](./edge-cache-design.md) | | **Zone 与域名管理** | 以可注册根域为管理入口,聚合明确域名、域名证书与反代路由 | [Zone 与域名资源设计](./zone-design.md) | | **Cloudflare DNS 指向** | 以 ZoneDomain 为粒度,将单条 Cloudflare A 记录幂等指向边缘节点 IPv4;支持连接配置、分组、成员橙云与异步同步,一期不含自动故障切换 | [Cloudflare DNS 指向设计](./cloudflare-pointing.md) | diff --git a/docs/design/origin-error-page.md b/docs/design/origin-error-page.md index dc083be1..a977813d 100644 --- a/docs/design/origin-error-page.md +++ b/docs/design/origin-error-page.md @@ -12,7 +12,7 @@ * **可拦截**:在用户配置的状态码集合上,用统一 HTML 替换原先透传的源站/Nginx 默认错误响应。 * **可关闭**:全局开关关闭后行为与现状一致(透传 / Nginx 默认页)。 -* **默认可视**:默认启用,默认状态码标签 `500-599`,默认 Cloudflare 风格错误页(无 CF 商标)。 +* **默认可视**:默认启用,默认状态码标签 `500-599`,默认 OpenFlare 极简错误页。 * **可自定义**:管理员可在线编辑完整 HTML;空 HTML 表示使用内置默认模板。 * **状态码透传**:HTTP 响应 `status` 保持原错误码(如 502、522);页面正文通过 `{{status}}` 展示同一数值。 * **全局统一**:侧栏「网站管理 → 错误页」单一配置,全站反代路由共用。 @@ -68,7 +68,7 @@ Tags Input 每条标签: ### 2.4 默认页 -内置 Cloudflare 风格:浅色居中、大号状态码、简短英文/中性说明、小字 Host。不使用 Cloudflare 商标或 Ray ID 伪造。前后端共用同一默认 HTML 常量(或同源字符串),前端「加载默认模板」直接填入编辑器。 +内置 OpenFlare 极简白底默认页:大号透传状态码、简短英文说明、Host 与品牌页脚。支持占位符 `{{status}}` / `{{host}}`;前端可在编辑页从内置模板目录加载预制风格。 --- diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index ca367938..2a32daee 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -267,7 +267,7 @@ Server 的所有核心基础配置定义在 `config.yaml` 中,且均支持环 | --- | --- | --- | --- | | `origin_error_page_enabled` | `bool` | 是否启用全局源站错误页。开启后,源站或网关返回的匹配状态码由自定义/默认 HTML 替换,**HTTP 状态码保持原值**;关闭后不生成相关指令,恢复透传。修改后需发布配置版本生效 | `true` | | `origin_error_page_status_codes` | `json` | 触发错误页的状态码标签 JSON 数组。支持单码(如 `522`)与闭区间(如 `500-599`);单码与区间两端均须在 **400–599**,且 `lo ≤ hi`。启用时展开结果不能为空 | `["500-599"]` | -| `origin_error_page_html` | `string` | 错误页自定义 HTML。空字符串表示使用内置 Cloudflare 风格默认模板;支持占位符 `{{status}}`(与 HTTP 状态码一致)、`{{host}}`(请求 Host)。最大 **256 KiB**(按字节)。勿嵌入不可信第三方脚本 | 空 | +| `origin_error_page_html` | `string` | 错误页自定义 HTML。空字符串表示使用内置 OpenFlare 默认模板(极简白底);支持占位符 `{{status}}`(与 HTTP 状态码一致)、`{{host}}`(请求 Host)。最大 **256 KiB**(按字节)。勿嵌入不可信第三方脚本 | 空 | --- diff --git a/frontend/app/(main)/error-pages/components/error-page-gate.tsx b/frontend/app/(main)/error-pages/components/error-page-gate.tsx new file mode 100644 index 00000000..007737b3 --- /dev/null +++ b/frontend/app/(main)/error-pages/components/error-page-gate.tsx @@ -0,0 +1,70 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { FileWarning } from 'lucide-react'; +import type { UseQueryResult } from '@tanstack/react-query'; + +import { useAuth } from '@/components/providers/auth-provider'; +import { EmptyStateWithBorder } from '@/components/layout/empty'; +import { ErrorInline } from '@/components/layout/error'; +import { LoadingStateWithBorder } from '@/components/layout/loading'; + +type ErrorPageGateProps = { + optionsQuery: UseQueryResult, Error>; + children: ReactNode; +}; + +export function ErrorPageGate({ optionsQuery, children }: ErrorPageGateProps) { + const { user, loading: authLoading } = useAuth(); + + if (authLoading) { + return ( +
+ +
+ ); + } + + if (!user?.is_admin) { + return ( +
+ +
+ ); + } + + if (optionsQuery.isLoading) { + return ( +
+ +
+ ); + } + + if (optionsQuery.isError) { + return ( +
+ void optionsQuery.refetch()} + /> +
+ ); + } + + return <>{children}; +} diff --git a/frontend/app/(main)/error-pages/components/html-editor-workspace.tsx b/frontend/app/(main)/error-pages/components/html-editor-workspace.tsx new file mode 100644 index 00000000..c57896e0 --- /dev/null +++ b/frontend/app/(main)/error-pages/components/html-editor-workspace.tsx @@ -0,0 +1,141 @@ +'use client'; + +import { useRef, useState } from 'react'; +import { useTheme } from 'next-themes'; +import CodeMirror from '@uiw/react-codemirror'; +import { html } from '@codemirror/lang-html'; +import { Expand, Terminal } from 'lucide-react'; +import Link from 'next/link'; + +import { Button } from '@/components/ui/button'; +import { + ORIGIN_ERROR_PAGE_HTML_MAX_BYTES, + previewOriginErrorPageHTML, +} from '@/lib/openflare/default-origin-error-page-html'; + +type HtmlEditorWorkspaceProps = { + value: string; + onChange: (value: string) => void; + toolbarRight?: React.ReactNode; +}; + +/** + * SQL 查询终端同款工作区:整体边框 + 工具栏 + 可拖拽分隔; + * 本页为左代码 / 右实时预览横向布局。 + */ +export function HtmlEditorWorkspace({ + value, + onChange, + toolbarRight, +}: HtmlEditorWorkspaceProps) { + const containerRef = useRef(null); + const [editorWidthPercent, setEditorWidthPercent] = useState(48); + const { resolvedTheme } = useTheme(); + const cmTheme = resolvedTheme === 'dark' ? 'dark' : 'light'; + const previewSrcDoc = previewOriginErrorPageHTML(value); + const htmlBytes = new TextEncoder().encode(value).length; + + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + const container = containerRef.current; + if (!container) return; + const rect = container.getBoundingClientRect(); + + const handleMouseMove = (moveEvent: MouseEvent) => { + const x = moveEvent.clientX - rect.left; + const pct = (x / rect.width) * 100; + if (pct > 25 && pct < 75) { + setEditorWidthPercent(pct); + } + }; + + const handleMouseUp = () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }; + + return ( +
+
+
+ + HTML 编辑器 + + {htmlBytes} / {ORIGIN_ERROR_PAGE_HTML_MAX_BYTES} 字节 + {value.trim() === '' ? ' · 空则使用内置默认' : ''} + +
+
{toolbarRight}
+
+ +
+
+ +
+ +
+
+
+ +
+
+ 实时预览 +
+ + {'{{status}}'}→502 · {'{{host}}'}→example.com + + +
+
+
+