mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
refactor(error-pages): preset templates with OpenFlare branding
This commit is contained in:
@@ -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 变更联动。
|
||||
|
||||
### 修复
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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}}`;前端可在编辑页从内置模板目录加载预制风格。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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**(按字节)。勿嵌入不可信第三方脚本 | 空 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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<Array<{ key: string; value: string }>, Error>;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function ErrorPageGate({ optionsQuery, children }: ErrorPageGateProps) {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
|
||||
if (authLoading) {
|
||||
return (
|
||||
<div className='py-6 px-1'>
|
||||
<LoadingStateWithBorder
|
||||
icon={FileWarning}
|
||||
description='加载权限信息...'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user?.is_admin) {
|
||||
return (
|
||||
<div className='py-6 px-1'>
|
||||
<EmptyStateWithBorder
|
||||
icon={FileWarning}
|
||||
title='权限不足'
|
||||
description='只有管理员可以访问源站错误页设置。'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (optionsQuery.isLoading) {
|
||||
return (
|
||||
<div className='py-6 px-1'>
|
||||
<LoadingStateWithBorder
|
||||
icon={FileWarning}
|
||||
description='加载错误页配置...'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (optionsQuery.isError) {
|
||||
return (
|
||||
<div className='py-6 px-1'>
|
||||
<ErrorInline
|
||||
message={
|
||||
optionsQuery.error instanceof Error
|
||||
? optionsQuery.error.message
|
||||
: '加载失败'
|
||||
}
|
||||
onRetry={() => void optionsQuery.refetch()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -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<HTMLDivElement>(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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className='w-full border border-border/40 bg-card/60 backdrop-blur-md rounded-lg overflow-hidden flex flex-col shadow-sm h-[calc(100vh-160px)] min-h-[520px]'
|
||||
>
|
||||
<div className='flex items-center justify-between px-4 py-2 border-b bg-muted/40 shrink-0 gap-4 flex-wrap'>
|
||||
<div className='flex items-center gap-2 min-w-0'>
|
||||
<Terminal className='size-4 text-primary shrink-0' />
|
||||
<span className='text-xs font-semibold'>HTML 编辑器</span>
|
||||
<span className='text-[11px] text-muted-foreground font-mono truncate'>
|
||||
{htmlBytes} / {ORIGIN_ERROR_PAGE_HTML_MAX_BYTES} 字节
|
||||
{value.trim() === '' ? ' · 空则使用内置默认' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center gap-1.5 flex-wrap'>{toolbarRight}</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-1 min-h-0 overflow-hidden'>
|
||||
<div
|
||||
style={{ width: `${editorWidthPercent}%` }}
|
||||
className='h-full overflow-hidden relative min-w-[200px] bg-background border-r border-border/40'
|
||||
>
|
||||
<CodeMirror
|
||||
value={value}
|
||||
height='100%'
|
||||
extensions={[html()]}
|
||||
theme={cmTheme}
|
||||
onChange={onChange}
|
||||
className='h-full text-xs font-mono'
|
||||
basicSetup={{
|
||||
lineNumbers: true,
|
||||
foldGutter: true,
|
||||
dropCursor: true,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: true,
|
||||
}}
|
||||
placeholder='留空使用内置默认模板…'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onMouseDown={handleMouseDown}
|
||||
className='w-1.5 bg-border/60 hover:bg-primary/50 cursor-col-resize transition-colors flex items-center justify-center shrink-0 select-none z-10'
|
||||
title='拖动调整宽度'
|
||||
>
|
||||
<div className='h-8 w-1 rounded bg-muted-foreground/30' />
|
||||
</div>
|
||||
|
||||
<div className='flex-1 min-w-[200px] flex flex-col overflow-hidden bg-muted/10'>
|
||||
<div className='flex items-center justify-between px-4 py-1.5 border-b bg-muted/20 shrink-0 text-[11px] text-muted-foreground font-mono gap-2'>
|
||||
<span className='font-semibold'>实时预览</span>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='hidden sm:inline'>
|
||||
{'{{status}}'}→502 · {'{{host}}'}→example.com
|
||||
</span>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='h-6 px-2 text-[11px]'
|
||||
asChild
|
||||
>
|
||||
<Link href='/error-pages/preview'>
|
||||
<Expand className='size-3' />
|
||||
真实预览
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-1 min-h-0 overflow-hidden bg-background'>
|
||||
<iframe
|
||||
title='源站错误页实时预览'
|
||||
sandbox=''
|
||||
srcDoc={previewSrcDoc}
|
||||
className='h-full w-full border-0 bg-background'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { ORIGIN_ERROR_PAGE_HTML_MAX_BYTES } from '@/lib/openflare/default-origin-error-page-html';
|
||||
import {
|
||||
DEFAULT_ORIGIN_ERROR_PAGE_STATUS_TAGS,
|
||||
parseStatusCodeTagsJSON,
|
||||
validateStatusCodeTags,
|
||||
} from '@/lib/openflare/status-code-tags';
|
||||
|
||||
export const OPTIONS_QUERY_KEY = ['openflare', 'options'] as const;
|
||||
|
||||
export const KEY_ENABLED = 'origin_error_page_enabled';
|
||||
export const KEY_STATUS_CODES = 'origin_error_page_status_codes';
|
||||
export const KEY_HTML = 'origin_error_page_html';
|
||||
|
||||
export type ErrorPageFields = {
|
||||
enabled: boolean;
|
||||
statusCodes: string[];
|
||||
html: string;
|
||||
};
|
||||
|
||||
export const defaultErrorPageFields: ErrorPageFields = {
|
||||
enabled: true,
|
||||
statusCodes: [...DEFAULT_ORIGIN_ERROR_PAGE_STATUS_TAGS],
|
||||
html: '',
|
||||
};
|
||||
|
||||
export function optionsToMap(options: Array<{ key: string; value: string }>) {
|
||||
return options.reduce<Record<string, string>>((acc, option) => {
|
||||
acc[option.key] = option.value;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function mapOptionsToFields(
|
||||
optionMap: Record<string, string>,
|
||||
): ErrorPageFields {
|
||||
const enabledRaw = optionMap[KEY_ENABLED];
|
||||
return {
|
||||
enabled: enabledRaw === undefined ? true : enabledRaw === 'true',
|
||||
statusCodes: parseStatusCodeTagsJSON(optionMap[KEY_STATUS_CODES]),
|
||||
html: optionMap[KEY_HTML] ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
export function validateErrorPageFields(fields: ErrorPageFields) {
|
||||
validateStatusCodeTags(fields.statusCodes);
|
||||
validateErrorPageHTML(fields.html);
|
||||
}
|
||||
|
||||
export function validateErrorPageHTML(html: string) {
|
||||
const htmlBytes = new TextEncoder().encode(html).length;
|
||||
if (htmlBytes > ORIGIN_ERROR_PAGE_HTML_MAX_BYTES) {
|
||||
throw new Error(
|
||||
`HTML 超过最大长度限制(${ORIGIN_ERROR_PAGE_HTML_MAX_BYTES} 字节)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function invalidateErrorPageQueries(queryClient: {
|
||||
invalidateQueries: (opts: {
|
||||
queryKey: readonly unknown[];
|
||||
}) => Promise<unknown>;
|
||||
}) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: OPTIONS_QUERY_KEY }),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['openflare', 'config-preview'],
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['openflare', 'config-versions'],
|
||||
}),
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
ArrowLeft,
|
||||
FileCode2,
|
||||
Loader2,
|
||||
RotateCcw,
|
||||
Save,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { useAuth } from '@/components/providers/auth-provider';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
DEFAULT_ORIGIN_ERROR_PAGE_TEMPLATE_ID,
|
||||
getOriginErrorPageTemplate,
|
||||
ORIGIN_ERROR_PAGE_TEMPLATES,
|
||||
} from '@/lib/openflare/origin-error-page-templates';
|
||||
import { OptionService } from '@/lib/services/openflare';
|
||||
|
||||
import { ErrorPageGate } from '../components/error-page-gate';
|
||||
import { HtmlEditorWorkspace } from '../components/html-editor-workspace';
|
||||
import {
|
||||
invalidateErrorPageQueries,
|
||||
KEY_HTML,
|
||||
mapOptionsToFields,
|
||||
OPTIONS_QUERY_KEY,
|
||||
optionsToMap,
|
||||
validateErrorPageHTML,
|
||||
} from '../components/shared';
|
||||
|
||||
export default function ErrorPageEditPage() {
|
||||
const { user } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const [html, setHtml] = useState('');
|
||||
const [restoreOpen, setRestoreOpen] = useState(false);
|
||||
const [templateId, setTemplateId] = useState(
|
||||
DEFAULT_ORIGIN_ERROR_PAGE_TEMPLATE_ID,
|
||||
);
|
||||
|
||||
const optionsQuery = useQuery({
|
||||
queryKey: OPTIONS_QUERY_KEY,
|
||||
queryFn: () => OptionService.list(),
|
||||
enabled: !!user?.is_admin,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!optionsQuery.data) return;
|
||||
setHtml(mapOptionsToFields(optionsToMap(optionsQuery.data)).html);
|
||||
}, [optionsQuery.data]);
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
validateErrorPageHTML(html);
|
||||
await OptionService.updateBatch([{ key: KEY_HTML, value: html }]);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
toast.success('HTML 模板已保存,请前往版本发布使配置生效');
|
||||
await invalidateErrorPageQueries(queryClient);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error instanceof Error ? error.message : '保存失败');
|
||||
},
|
||||
});
|
||||
|
||||
const loadSelectedTemplate = () => {
|
||||
const tmpl = getOriginErrorPageTemplate(templateId);
|
||||
if (!tmpl) {
|
||||
toast.error('未找到所选模板');
|
||||
return;
|
||||
}
|
||||
setHtml(tmpl.html);
|
||||
toast.success(`已加载模板「${tmpl.name}」到编辑器`);
|
||||
};
|
||||
|
||||
const restoreDefault = () => {
|
||||
setHtml('');
|
||||
setRestoreOpen(false);
|
||||
toast.success('已清空为使用服务端内置默认模板(需保存后生效)');
|
||||
};
|
||||
|
||||
return (
|
||||
<ErrorPageGate optionsQuery={optionsQuery}>
|
||||
<div className='flex flex-col gap-4 py-6 px-1'>
|
||||
<div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<Button variant='outline' size='icon' className='h-8 w-8' asChild>
|
||||
<Link href='/error-pages' aria-label='返回错误页'>
|
||||
<ArrowLeft className='size-4' />
|
||||
</Link>
|
||||
</Button>
|
||||
<div className='flex items-center gap-2'>
|
||||
<FileCode2 className='size-5 text-primary' />
|
||||
<div>
|
||||
<h1 className='text-2xl font-semibold tracking-tight'>
|
||||
编辑
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HtmlEditorWorkspace
|
||||
value={html}
|
||||
onChange={setHtml}
|
||||
toolbarRight={
|
||||
<>
|
||||
<div className='flex items-center gap-1.5'>
|
||||
<span className='text-[11px] text-muted-foreground hidden sm:inline'>
|
||||
内置模板
|
||||
</span>
|
||||
<Select value={templateId} onValueChange={setTemplateId}>
|
||||
<SelectTrigger className='h-7 w-[160px] text-[11px] bg-background'>
|
||||
<SelectValue placeholder='选择模板' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ORIGIN_ERROR_PAGE_TEMPLATES.map((tmpl) => (
|
||||
<SelectItem
|
||||
key={tmpl.id}
|
||||
value={tmpl.id}
|
||||
className='text-xs'
|
||||
>
|
||||
{tmpl.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='h-7 px-2 text-[11px]'
|
||||
onClick={loadSelectedTemplate}
|
||||
>
|
||||
<Sparkles className='size-3.5' />
|
||||
加载模板
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
className='h-7 px-2 text-[11px]'
|
||||
onClick={() => setRestoreOpen(true)}
|
||||
>
|
||||
<RotateCcw className='size-3.5' />
|
||||
恢复默认
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
className='h-7 px-3 text-[11px]'
|
||||
disabled={saveMutation.isPending}
|
||||
onClick={() => saveMutation.mutate()}
|
||||
>
|
||||
{saveMutation.isPending ? (
|
||||
<Loader2 className='size-3 animate-spin' />
|
||||
) : (
|
||||
<Save className='size-3' />
|
||||
)}
|
||||
保存
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AlertDialog open={restoreOpen} onOpenChange={setRestoreOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>恢复默认 HTML?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
将清空编辑器内容,保存后使用服务端内置默认模板。此操作不会自动保存。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={restoreDefault}>
|
||||
确认恢复
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</ErrorPageGate>
|
||||
);
|
||||
}
|
||||
@@ -4,29 +4,15 @@ import Link from 'next/link';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
ExternalLink,
|
||||
Expand,
|
||||
FileWarning,
|
||||
Loader2,
|
||||
RotateCcw,
|
||||
Pencil,
|
||||
Save,
|
||||
Sparkles,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
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';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
@@ -38,76 +24,33 @@ import {
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { TagsInput } from '@/components/ui/tags-input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { previewOriginErrorPageHTML } from '@/lib/openflare/default-origin-error-page-html';
|
||||
import {
|
||||
DEFAULT_ORIGIN_ERROR_PAGE_HTML,
|
||||
ORIGIN_ERROR_PAGE_HTML_MAX_BYTES,
|
||||
previewOriginErrorPageHTML,
|
||||
} from '@/lib/openflare/default-origin-error-page-html';
|
||||
import {
|
||||
DEFAULT_ORIGIN_ERROR_PAGE_STATUS_TAGS,
|
||||
parseStatusCodeTagsJSON,
|
||||
validateStatusCodeTagMessage,
|
||||
validateStatusCodeTags,
|
||||
} from '@/lib/openflare/status-code-tags';
|
||||
import { OptionService } from '@/lib/services/openflare';
|
||||
|
||||
const optionsQueryKey = ['openflare', 'options'] as const;
|
||||
|
||||
const KEY_ENABLED = 'origin_error_page_enabled';
|
||||
const KEY_STATUS_CODES = 'origin_error_page_status_codes';
|
||||
const KEY_HTML = 'origin_error_page_html';
|
||||
|
||||
type ErrorPageFields = {
|
||||
enabled: boolean;
|
||||
statusCodes: string[];
|
||||
html: string;
|
||||
};
|
||||
|
||||
const defaultFields: ErrorPageFields = {
|
||||
enabled: true,
|
||||
statusCodes: [...DEFAULT_ORIGIN_ERROR_PAGE_STATUS_TAGS],
|
||||
html: '',
|
||||
};
|
||||
|
||||
function optionsToMap(options: Array<{ key: string; value: string }>) {
|
||||
return options.reduce<Record<string, string>>((acc, option) => {
|
||||
acc[option.key] = option.value;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function mapOptionsToFields(
|
||||
optionMap: Record<string, string>,
|
||||
): ErrorPageFields {
|
||||
const enabledRaw = optionMap[KEY_ENABLED];
|
||||
return {
|
||||
enabled: enabledRaw === undefined ? true : enabledRaw === 'true',
|
||||
statusCodes: parseStatusCodeTagsJSON(optionMap[KEY_STATUS_CODES]),
|
||||
html: optionMap[KEY_HTML] ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
function validateFields(fields: ErrorPageFields) {
|
||||
validateStatusCodeTags(fields.statusCodes);
|
||||
|
||||
const htmlBytes = new TextEncoder().encode(fields.html).length;
|
||||
if (htmlBytes > ORIGIN_ERROR_PAGE_HTML_MAX_BYTES) {
|
||||
throw new Error(
|
||||
`HTML 超过最大长度限制(${ORIGIN_ERROR_PAGE_HTML_MAX_BYTES} 字节)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
import { ErrorPageGate } from './components/error-page-gate';
|
||||
import {
|
||||
defaultErrorPageFields,
|
||||
invalidateErrorPageQueries,
|
||||
KEY_ENABLED,
|
||||
KEY_STATUS_CODES,
|
||||
mapOptionsToFields,
|
||||
OPTIONS_QUERY_KEY,
|
||||
optionsToMap,
|
||||
type ErrorPageFields,
|
||||
} from './components/shared';
|
||||
|
||||
export default function ErrorPagesPage() {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const { user } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const [fields, setFields] = useState<ErrorPageFields>(defaultFields);
|
||||
const [restoreOpen, setRestoreOpen] = useState(false);
|
||||
const [fields, setFields] = useState<ErrorPageFields>(defaultErrorPageFields);
|
||||
const [tagError, setTagError] = useState<string | null>(null);
|
||||
|
||||
const optionsQuery = useQuery({
|
||||
queryKey: optionsQueryKey,
|
||||
queryKey: OPTIONS_QUERY_KEY,
|
||||
queryFn: () => OptionService.list(),
|
||||
enabled: !!user?.is_admin,
|
||||
});
|
||||
@@ -123,40 +66,27 @@ export default function ErrorPagesPage() {
|
||||
[fields.html],
|
||||
);
|
||||
|
||||
const saveMutation = useMutation({
|
||||
/** 仅保存启用开关 + 触发状态码(HTML 在编辑页单独保存) */
|
||||
const savePolicyMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
validateFields(fields);
|
||||
validateStatusCodeTags(fields.statusCodes);
|
||||
await OptionService.updateBatch([
|
||||
{ key: KEY_ENABLED, value: String(fields.enabled) },
|
||||
{
|
||||
key: KEY_STATUS_CODES,
|
||||
value: JSON.stringify(fields.statusCodes),
|
||||
},
|
||||
{ key: KEY_HTML, value: fields.html },
|
||||
]);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
toast.success('源站错误页已保存,请前往版本发布使配置生效');
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: optionsQueryKey }),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['openflare', 'config-preview'],
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['openflare', 'config-versions'],
|
||||
}),
|
||||
]);
|
||||
toast.success('触发策略已保存,请前往版本发布使配置生效');
|
||||
await invalidateErrorPageQueries(queryClient);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error instanceof Error ? error.message : '保存失败');
|
||||
},
|
||||
});
|
||||
|
||||
const handleStatusCodesChange = (statusCodes: string[]) => {
|
||||
setFields((prev) => ({ ...prev, statusCodes }));
|
||||
setTagError(null);
|
||||
};
|
||||
|
||||
const handleValidateTag = (tag: string) => {
|
||||
const message = validateStatusCodeTagMessage(tag);
|
||||
if (message) {
|
||||
@@ -168,213 +98,101 @@ export default function ErrorPagesPage() {
|
||||
return null;
|
||||
};
|
||||
|
||||
const loadDefaultTemplate = () => {
|
||||
setFields((prev) => ({
|
||||
...prev,
|
||||
html: DEFAULT_ORIGIN_ERROR_PAGE_HTML,
|
||||
}));
|
||||
toast.success('已加载默认 HTML 模板到编辑器');
|
||||
};
|
||||
|
||||
const restoreDefault = () => {
|
||||
setFields((prev) => ({
|
||||
...prev,
|
||||
html: '',
|
||||
statusCodes: [...DEFAULT_ORIGIN_ERROR_PAGE_STATUS_TAGS],
|
||||
}));
|
||||
setRestoreOpen(false);
|
||||
setTagError(null);
|
||||
toast.success('已恢复默认:状态码 500-599,HTML 使用服务端内置模板');
|
||||
};
|
||||
|
||||
if (authLoading) {
|
||||
return (
|
||||
<div className='py-6 px-1'>
|
||||
<LoadingStateWithBorder
|
||||
icon={FileWarning}
|
||||
description='加载权限信息...'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user?.is_admin) {
|
||||
return (
|
||||
<div className='py-6 px-1'>
|
||||
<EmptyStateWithBorder
|
||||
icon={FileWarning}
|
||||
title='权限不足'
|
||||
description='只有管理员可以访问源站错误页设置。'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (optionsQuery.isLoading) {
|
||||
return (
|
||||
<div className='py-6 px-1'>
|
||||
<LoadingStateWithBorder
|
||||
icon={FileWarning}
|
||||
description='加载错误页配置...'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (optionsQuery.isError) {
|
||||
return (
|
||||
<div className='py-6 px-1'>
|
||||
<ErrorInline
|
||||
message={
|
||||
optionsQuery.error instanceof Error
|
||||
? optionsQuery.error.message
|
||||
: '加载失败'
|
||||
}
|
||||
onRetry={() => void optionsQuery.refetch()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-6 py-6 px-1'>
|
||||
<div className='flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between'>
|
||||
<ErrorPageGate optionsQuery={optionsQuery}>
|
||||
<div className='flex flex-col gap-6 py-6 px-1'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<FileWarning className='size-5 text-primary' />
|
||||
<div>
|
||||
<h1 className='text-2xl font-semibold tracking-tight'>错误页</h1>
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
配置源站/网关错误响应时的统一 HTML
|
||||
页面。保存后需发布配置版本后生效。
|
||||
页面。策略与模板保存后需发布配置版本后生效。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button variant='outline' size='sm' asChild>
|
||||
<Link href='/config-versions'>
|
||||
<ExternalLink className='size-3.5' />
|
||||
查看配置预览
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
disabled={saveMutation.isPending}
|
||||
onClick={() => saveMutation.mutate()}
|
||||
>
|
||||
{saveMutation.isPending ? (
|
||||
<Loader2 className='size-3.5 animate-spin' />
|
||||
) : (
|
||||
<Save className='size-3.5' />
|
||||
)}
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className='border-dashed shadow-none'>
|
||||
<CardHeader className='flex flex-row items-center justify-between gap-4'>
|
||||
<div>
|
||||
<CardTitle className='text-base'>启用源站错误页</CardTitle>
|
||||
<CardDescription>
|
||||
关闭后透传源站或 Nginx 默认错误响应,不注入 error_page 指令。
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Switch
|
||||
checked={fields.enabled}
|
||||
onCheckedChange={(enabled) =>
|
||||
setFields((prev) => ({ ...prev, enabled }))
|
||||
}
|
||||
aria-label='启用源站错误页'
|
||||
/>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card className='border-dashed shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-base'>触发状态码</CardTitle>
|
||||
<CardDescription>
|
||||
支持单码(如 502)或闭区间(如 500-599),范围 400–599。默认
|
||||
500-599。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='flex flex-col gap-2'>
|
||||
<Label htmlFor='origin-error-status-codes' className='sr-only'>
|
||||
状态码标签
|
||||
</Label>
|
||||
<TagsInput
|
||||
id='origin-error-status-codes'
|
||||
value={fields.statusCodes}
|
||||
onChange={handleStatusCodesChange}
|
||||
validateTag={handleValidateTag}
|
||||
placeholder='例如 502 或 500-599,回车添加'
|
||||
aria-invalid={!!tagError}
|
||||
/>
|
||||
{tagError ? (
|
||||
<p className='text-xs text-destructive'>{tagError}</p>
|
||||
) : (
|
||||
<p className='text-xs text-muted-foreground'>
|
||||
输入后按 Enter 或逗号添加;Backspace 可删除最后一个标签。
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className='grid gap-6 xl:grid-cols-2'>
|
||||
<Card className='border-dashed shadow-none'>
|
||||
<CardHeader className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between'>
|
||||
<CardHeader className='flex flex-row items-center justify-between gap-4'>
|
||||
<div>
|
||||
<CardTitle className='text-base'>HTML 模板</CardTitle>
|
||||
<CardTitle className='text-base'>启用源站错误页</CardTitle>
|
||||
<CardDescription>
|
||||
支持占位符 {'{{status}}'} 与 {'{{host}}'}
|
||||
。留空则使用服务端内置默认页。
|
||||
关闭后会透传源站或 Nginx 默认错误响应。
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={loadDefaultTemplate}
|
||||
>
|
||||
<Sparkles className='size-3.5' />
|
||||
加载默认模板
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => setRestoreOpen(true)}
|
||||
>
|
||||
<RotateCcw className='size-3.5' />
|
||||
恢复默认
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
id='origin-error-html'
|
||||
value={fields.html}
|
||||
onChange={(event) =>
|
||||
setFields((prev) => ({ ...prev, html: event.target.value }))
|
||||
<Switch
|
||||
checked={fields.enabled}
|
||||
onCheckedChange={(enabled) =>
|
||||
setFields((prev) => ({ ...prev, enabled }))
|
||||
}
|
||||
placeholder='留空使用内置默认模板…'
|
||||
className='min-h-[28rem] font-mono text-xs leading-relaxed'
|
||||
spellCheck={false}
|
||||
aria-label='启用源站错误页'
|
||||
/>
|
||||
<p className='mt-2 text-xs text-muted-foreground'>
|
||||
当前 {new TextEncoder().encode(fields.html).length} /{' '}
|
||||
{ORIGIN_ERROR_PAGE_HTML_MAX_BYTES} 字节
|
||||
{fields.html.trim() === '' ? '(使用内置默认)' : ''}
|
||||
</p>
|
||||
</CardContent>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card className='border-dashed shadow-none'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-base'>预览</CardTitle>
|
||||
<CardDescription>
|
||||
客户端预览:{'{{status}}'}→502,{'{{host}}'}→example.com
|
||||
</CardDescription>
|
||||
<CardHeader className='flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between space-y-0'>
|
||||
<div className='space-y-1.5'>
|
||||
<CardTitle className='text-base'>触发状态码</CardTitle>
|
||||
<CardDescription>
|
||||
支持单码(如 502)或闭区间(如 500-599),范围
|
||||
400–599。默认 500-599。修改启用开关或状态码后需点击保存。
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button
|
||||
size='sm'
|
||||
className='shrink-0'
|
||||
disabled={savePolicyMutation.isPending}
|
||||
onClick={() => savePolicyMutation.mutate()}
|
||||
>
|
||||
{savePolicyMutation.isPending ? (
|
||||
<Loader2 className='size-3.5 animate-spin' />
|
||||
) : (
|
||||
<Save className='size-3.5' />
|
||||
)}
|
||||
保存
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className='flex flex-col gap-2'>
|
||||
<Label htmlFor='origin-error-status-codes' className='sr-only'>
|
||||
状态码标签
|
||||
</Label>
|
||||
<TagsInput
|
||||
id='origin-error-status-codes'
|
||||
value={fields.statusCodes}
|
||||
onChange={(statusCodes) => {
|
||||
setFields((prev) => ({ ...prev, statusCodes }));
|
||||
setTagError(null);
|
||||
}}
|
||||
validateTag={handleValidateTag}
|
||||
placeholder='例如 502 或 500-599,回车添加'
|
||||
aria-invalid={!!tagError}
|
||||
/>
|
||||
{tagError ? (
|
||||
<p className='text-xs text-destructive'>{tagError}</p>
|
||||
) : (null)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className='border-dashed shadow-none overflow-hidden'>
|
||||
<CardHeader className='flex flex-row items-start justify-between gap-3 space-y-0'>
|
||||
<div className='space-y-1.5'>
|
||||
<CardTitle className='text-base'>页面预览</CardTitle>
|
||||
</div>
|
||||
<div className='flex shrink-0 flex-wrap gap-2'>
|
||||
<Button variant='outline' size='sm' asChild>
|
||||
<Link href='/error-pages/preview'>
|
||||
<Expand className='size-3.5' />
|
||||
预览
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size='sm' asChild>
|
||||
<Link href='/error-pages/edit'>
|
||||
<Pencil className='size-3.5' />
|
||||
编辑
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className='overflow-hidden rounded-md border bg-muted/30'>
|
||||
@@ -388,24 +206,6 @@ export default function ErrorPagesPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<AlertDialog open={restoreOpen} onOpenChange={setRestoreOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>恢复默认配置?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
将状态码重置为 500-599,并将 HTML
|
||||
清空为「使用服务端内置默认模板」。此操作不会自动保存,仍需点击保存并发布配置版本。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={restoreDefault}>
|
||||
确认恢复
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</ErrorPageGate>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ArrowLeft, Pencil, X } from 'lucide-react';
|
||||
|
||||
import { useAuth } from '@/components/providers/auth-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { previewOriginErrorPageHTML } from '@/lib/openflare/default-origin-error-page-html';
|
||||
import { OptionService } from '@/lib/services/openflare';
|
||||
|
||||
import { ErrorPageGate } from '../components/error-page-gate';
|
||||
import {
|
||||
mapOptionsToFields,
|
||||
OPTIONS_QUERY_KEY,
|
||||
optionsToMap,
|
||||
} from '../components/shared';
|
||||
|
||||
/**
|
||||
* 全屏「真实预览」:覆盖主布局,按访客视角展示替换后的 HTML。
|
||||
*/
|
||||
export default function ErrorPagePreviewPage() {
|
||||
const { user } = useAuth();
|
||||
|
||||
const optionsQuery = useQuery({
|
||||
queryKey: OPTIONS_QUERY_KEY,
|
||||
queryFn: () => OptionService.list(),
|
||||
enabled: !!user?.is_admin,
|
||||
});
|
||||
|
||||
const html = useMemo(() => {
|
||||
if (!optionsQuery.data) return '';
|
||||
return mapOptionsToFields(optionsToMap(optionsQuery.data)).html;
|
||||
}, [optionsQuery.data]);
|
||||
|
||||
const previewSrcDoc = useMemo(
|
||||
() => previewOriginErrorPageHTML(html),
|
||||
[html],
|
||||
);
|
||||
|
||||
return (
|
||||
<ErrorPageGate optionsQuery={optionsQuery}>
|
||||
<div className='fixed inset-0 z-50 flex flex-col bg-background'>
|
||||
<div className='flex items-center justify-between gap-3 border-b bg-background/95 px-4 py-2 backdrop-blur shrink-0'>
|
||||
<div className='flex items-center gap-2 min-w-0'>
|
||||
<Button variant='outline' size='icon' className='h-8 w-8' asChild>
|
||||
<Link href='/error-pages' aria-label='返回错误页'>
|
||||
<ArrowLeft className='size-4' />
|
||||
</Link>
|
||||
</Button>
|
||||
<div className='min-w-0'>
|
||||
<p className='text-sm font-semibold truncate'>错误页真实预览</p>
|
||||
<p className='text-[11px] text-muted-foreground font-mono truncate'>
|
||||
{'{{status}}'}→502 · {'{{host}}'}→example.com · 全屏展示
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-2 shrink-0'>
|
||||
<Button variant='outline' size='sm' asChild>
|
||||
<Link href='/error-pages/edit'>
|
||||
<Pencil className='size-3.5' />
|
||||
编辑
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant='ghost' size='icon' className='h-8 w-8' asChild>
|
||||
<Link href='/error-pages' aria-label='关闭预览'>
|
||||
<X className='size-4' />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<iframe
|
||||
title='源站错误页真实预览'
|
||||
sandbox=''
|
||||
srcDoc={previewSrcDoc}
|
||||
className='flex-1 w-full border-0 bg-background min-h-0'
|
||||
/>
|
||||
</div>
|
||||
</ErrorPageGate>
|
||||
);
|
||||
}
|
||||
@@ -60,7 +60,11 @@ export const openflareWebsiteNavGroup: OpenFlareNavGroup = {
|
||||
childUrls: ['/cloudflare/settings'],
|
||||
},
|
||||
{ title: '源站地址', url: '/origins', childUrls: ['/origins/detail'] },
|
||||
{ title: '错误页', url: '/error-pages' },
|
||||
{
|
||||
title: '错误页',
|
||||
url: '/error-pages',
|
||||
childUrls: ['/error-pages/edit', '/error-pages/preview'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -1,81 +1,14 @@
|
||||
import {
|
||||
DEFAULT_ORIGIN_ERROR_PAGE_TEMPLATE_ID,
|
||||
getOriginErrorPageTemplate,
|
||||
} from '@/lib/openflare/origin-error-page-templates';
|
||||
|
||||
/**
|
||||
* Built-in Cloudflare-style origin error page.
|
||||
* Keep in sync with Go DefaultOriginErrorPageHTML in
|
||||
* pkg/render/openresty/origin_error_page.go
|
||||
* 预览/回退用的默认 HTML:与内置模板目录中的 minimalist 对齐。
|
||||
* 边缘侧空配置时的默认页仍由 Go DefaultOriginErrorPageHTML 提供。
|
||||
*/
|
||||
export const DEFAULT_ORIGIN_ERROR_PAGE_HTML = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{status}} | 源站暂时无法提供服务</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
background: #f6f7f9;
|
||||
color: #1f2937;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body { background: #0f1419; color: #e5e7eb; }
|
||||
.card { background: #1a2332; border-color: #2d3a4d; box-shadow: none; }
|
||||
.status { color: #f3f4f6; }
|
||||
.muted { color: #9ca3af; }
|
||||
.host { background: #243044; color: #d1d5db; }
|
||||
}
|
||||
.card {
|
||||
width: min(32rem, calc(100% - 2rem));
|
||||
padding: 2.5rem 2rem;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
|
||||
text-align: center;
|
||||
}
|
||||
.status {
|
||||
margin: 0;
|
||||
font-size: clamp(3.5rem, 12vw, 5.5rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 1;
|
||||
color: #111827;
|
||||
}
|
||||
h1 {
|
||||
margin: 1rem 0 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
p { margin: 0.4rem 0; line-height: 1.6; }
|
||||
.muted { color: #6b7280; font-size: 0.95rem; }
|
||||
.host {
|
||||
display: inline-block;
|
||||
margin-top: 1.25rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
font-size: 0.85rem;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="card" role="main">
|
||||
<p class="status" aria-label="HTTP status">{{status}}</p>
|
||||
<h1>源站暂时无法提供服务</h1>
|
||||
<p class="muted">网关已拦截源站错误响应。请稍后重试;若问题持续,请联系站点管理员。</p>
|
||||
<p class="host">{{host}}</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
export const DEFAULT_ORIGIN_ERROR_PAGE_HTML =
|
||||
getOriginErrorPageTemplate(DEFAULT_ORIGIN_ERROR_PAGE_TEMPLATE_ID)?.html ?? '';
|
||||
|
||||
/** Max HTML size in bytes (aligned with backend 256 KiB). */
|
||||
export const ORIGIN_ERROR_PAGE_HTML_MAX_BYTES = 256 * 1024;
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
/** 内置错误页模板:baohaus(包豪斯风格) */
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{status}} | OpenFlare</title>
|
||||
<style>
|
||||
:root {
|
||||
--color-red: #E53935;
|
||||
--color-yellow: #FDD835;
|
||||
--color-blue: #1E88E5;
|
||||
--color-black: #212121;
|
||||
--color-white: #FAFAFA;
|
||||
--color-grey: #9E9E9E;
|
||||
--font-primary: 'Futura', 'Century Gothic', 'Tw Cen MT', sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-white);
|
||||
color: var(--color-black);
|
||||
font-family: var(--font-primary);
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: radial-gradient(var(--color-grey) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
opacity: 0.15;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.layout-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.618fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.zone-left {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1 / 3;
|
||||
border-right: 4px solid var(--color-black);
|
||||
border-bottom: 4px solid var(--color-black);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--color-white);
|
||||
padding: 2rem;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.decor-circle {
|
||||
position: absolute;
|
||||
width: 40vh;
|
||||
height: 40vh;
|
||||
background-color: var(--color-yellow);
|
||||
border-radius: 50%;
|
||||
top: 15%;
|
||||
left: 10%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.error-number {
|
||||
font-size: clamp(6rem, 12vw, 14rem);
|
||||
font-weight: 700;
|
||||
line-height: 0.9;
|
||||
letter-spacing: -0.04em;
|
||||
color: var(--color-black);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.brand-footer {
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
left: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: var(--color-black);
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
|
||||
.zone-top-right {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 1 / 2;
|
||||
border-bottom: 4px solid var(--color-black);
|
||||
background-color: var(--color-white);
|
||||
padding: 4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: clamp(1.5rem, 3vw, 2.5rem);
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.5rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.error-desc {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
max-width: 450px;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.meta-data {
|
||||
margin-top: 2rem;
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-grey);
|
||||
border-top: 1px solid var(--color-grey);
|
||||
padding-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.zone-bottom-right {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 2 / 3;
|
||||
background-color: var(--color-white);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.geo-line {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: var(--color-black);
|
||||
top: 50%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.geo-triangle {
|
||||
position: absolute;
|
||||
right: 15%;
|
||||
bottom: 15%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 80px solid transparent;
|
||||
border-right: 80px solid transparent;
|
||||
border-bottom: 140px solid var(--color-blue);
|
||||
}
|
||||
|
||||
.geo-rect {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 20%;
|
||||
width: 150px;
|
||||
height: 30px;
|
||||
background-color: var(--color-red);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.layout-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.zone-left {
|
||||
border-right: none;
|
||||
border-bottom: 4px solid var(--color-black);
|
||||
min-height: 50vh;
|
||||
padding-top: 4rem;
|
||||
}
|
||||
|
||||
.decor-circle {
|
||||
width: 30vh;
|
||||
height: 30vh;
|
||||
top: 10%;
|
||||
}
|
||||
|
||||
.zone-top-right {
|
||||
border-bottom: 4px solid var(--color-black);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.zone-bottom-right {
|
||||
min-height: 30vh;
|
||||
}
|
||||
|
||||
.geo-triangle {
|
||||
border-left-width: 50px;
|
||||
border-right-width: 50px;
|
||||
border-bottom-width: 90px;
|
||||
right: 10%;
|
||||
}
|
||||
|
||||
.geo-rect {
|
||||
width: 100px;
|
||||
height: 20px;
|
||||
left: 15%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout-container">
|
||||
<div class="zone-left">
|
||||
<div class="decor-circle"></div>
|
||||
<div class="error-number" aria-label="HTTP status">{{status}}</div>
|
||||
|
||||
<div class="brand-footer">
|
||||
<svg class="brand-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
|
||||
</svg>
|
||||
<span class="brand-text">OpenFlare</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="zone-top-right">
|
||||
<h1 class="error-title">Origin Unavailable</h1>
|
||||
<p class="error-desc">
|
||||
The server received an invalid response from the upstream server.
|
||||
<br><br>
|
||||
<strong>Form follows function:</strong> The connection path is broken. Please verify the origin server status.
|
||||
</p>
|
||||
<div class="meta-data">
|
||||
<div>ERR_CODE: {{status}}</div>
|
||||
<div>HOST: {{host}}</div>
|
||||
<div>PROXY: OPENFLARE_EDGE</div>
|
||||
<div id="timestamp">--:--:-- UTC</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="zone-bottom-right">
|
||||
<div class="geo-line"></div>
|
||||
<div class="geo-rect"></div>
|
||||
<div class="geo-triangle"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const tsElement = document.getElementById('timestamp');
|
||||
if (tsElement) {
|
||||
const now = new Date();
|
||||
tsElement.textContent = now.toISOString().split('T')[1].split('.')[0] + ' UTC';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
export default html;
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 内置源站错误页 HTML 模板目录。
|
||||
* 每个模板对应本目录下一个独立 `.ts` 模块(export default 完整 HTML 字符串)。
|
||||
* 新增模板:新增 `*.ts` 并在下方数组注册。
|
||||
*
|
||||
* 模板须使用占位符 {{status}} / {{host}},禁止写死状态码。
|
||||
*/
|
||||
import baohausHtml from './baohaus';
|
||||
import lineStyleHtml from './line-style';
|
||||
import minimalistHtml from './minimalist';
|
||||
|
||||
export type OriginErrorPageTemplate = {
|
||||
/** 稳定 ID,与文件名一致 */
|
||||
id: string;
|
||||
/** 下拉展示名称 */
|
||||
name: string;
|
||||
/** 完整 HTML,含 {{status}} / {{host}} */
|
||||
html: string;
|
||||
};
|
||||
|
||||
export const ORIGIN_ERROR_PAGE_TEMPLATES: OriginErrorPageTemplate[] = [
|
||||
{
|
||||
id: 'minimalist',
|
||||
name: '极简白底',
|
||||
html: minimalistHtml,
|
||||
},
|
||||
{
|
||||
id: 'line-style',
|
||||
name: '线框拓扑',
|
||||
html: lineStyleHtml,
|
||||
},
|
||||
{
|
||||
id: 'baohaus',
|
||||
name: '包豪斯',
|
||||
html: baohausHtml,
|
||||
},
|
||||
];
|
||||
|
||||
/** 默认预制模板(前端预览空 HTML 回退 & 加载模板默认选中) */
|
||||
export const DEFAULT_ORIGIN_ERROR_PAGE_TEMPLATE_ID = 'minimalist';
|
||||
|
||||
export function getOriginErrorPageTemplate(
|
||||
id: string,
|
||||
): OriginErrorPageTemplate | undefined {
|
||||
return ORIGIN_ERROR_PAGE_TEMPLATES.find((item) => item.id === id);
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
/** 内置错误页模板:line-style */
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{status}} | OpenFlare</title>
|
||||
<style>
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
--color-bg: #fcfcfc;
|
||||
--color-line: #e0e0e0;
|
||||
--color-line-hover: #1a1a1a;
|
||||
--color-text-main: #1a1a1a;
|
||||
--color-text-sub: #666666;
|
||||
--color-text-muted: #999999;
|
||||
--color-accent: #d4d4d4;
|
||||
--font-geo: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-geo);
|
||||
background-color: var(--color-bg);
|
||||
background-image: radial-gradient(circle, var(--color-accent) 0.8px, transparent 0.8px);
|
||||
background-size: 24px 24px;
|
||||
color: var(--color-text-main);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 48px 24px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 48px;
|
||||
}
|
||||
|
||||
.line-box {
|
||||
border: 1px solid var(--color-line);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
padding: 32px;
|
||||
transition: all var(--transition-fast);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.line-box:hover {
|
||||
border-color: var(--color-line-hover);
|
||||
box-shadow: 0 0 0 1px var(--color-line-hover);
|
||||
}
|
||||
|
||||
.line-box:active {
|
||||
transform: scale(0.995);
|
||||
transition-duration: 100ms;
|
||||
}
|
||||
|
||||
.error-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
font-size: 64px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--color-text-main);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.error-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-sub);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.error-label::before,
|
||||
.error-label::after {
|
||||
content: '';
|
||||
width: 24px;
|
||||
height: 1px;
|
||||
background-color: var(--color-line);
|
||||
}
|
||||
|
||||
.topology-wrapper {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.topology-svg {
|
||||
width: 100%;
|
||||
height: 140px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.node-circle {
|
||||
fill: var(--color-bg);
|
||||
stroke: var(--color-text-main);
|
||||
stroke-width: 1.5;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.node-circle.error {
|
||||
stroke: var(--color-text-muted);
|
||||
stroke-dasharray: 3 3;
|
||||
}
|
||||
|
||||
.flow-line {
|
||||
stroke: var(--color-accent);
|
||||
stroke-width: 1.5;
|
||||
fill: none;
|
||||
transition: stroke var(--transition-fast);
|
||||
}
|
||||
|
||||
.flow-line.error {
|
||||
stroke: var(--color-text-muted);
|
||||
stroke-dasharray: 4 4;
|
||||
}
|
||||
|
||||
.node-text {
|
||||
font-family: var(--font-geo);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
fill: var(--color-text-sub);
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-family: var(--font-geo);
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.05em;
|
||||
fill: var(--color-text-muted);
|
||||
text-anchor: middle;
|
||||
}
|
||||
|
||||
.status-text.error {
|
||||
fill: #b0b0b0;
|
||||
}
|
||||
|
||||
.description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.desc-text {
|
||||
font-size: 18px;
|
||||
line-height: 1.6;
|
||||
color: var(--color-text-sub);
|
||||
font-weight: 400;
|
||||
max-width: 480px;
|
||||
margin: 0 auto 24px;
|
||||
}
|
||||
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
border-top: 1px solid var(--color-line);
|
||||
padding-top: 24px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.meta-key {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.meta-val {
|
||||
font-size: 13px;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace, var(--font-geo);
|
||||
color: var(--color-text-main);
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.05em;
|
||||
transition: color var(--transition-fast);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.footer-brand:hover {
|
||||
color: var(--color-text-main);
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.5;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
body {
|
||||
padding: 32px 16px;
|
||||
}
|
||||
.error-code {
|
||||
font-size: 48px;
|
||||
}
|
||||
.line-box {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
.meta-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-container">
|
||||
<div class="line-box error-header">
|
||||
<div class="error-code" aria-label="HTTP status">{{status}}</div>
|
||||
<div class="error-label">Origin Unavailable</div>
|
||||
</div>
|
||||
|
||||
<div class="line-box topology-wrapper">
|
||||
<svg class="topology-svg" viewBox="0 0 600 140" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 1 L 8 5 L 0 9 z" fill="#d4d4d4" />
|
||||
</marker>
|
||||
<marker id="arrow-error" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 1 L 8 5 L 0 9 z" fill="#999999" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<line x1="135" y1="70" x2="265" y2="70" class="flow-line" marker-end="url(#arrow)" />
|
||||
<line x1="335" y1="70" x2="465" y2="70" class="flow-line error" marker-end="url(#arrow-error)" />
|
||||
|
||||
<circle cx="110" cy="70" r="20" class="node-circle" />
|
||||
<text x="110" y="74" class="node-text" style="font-size: 8px; fill: #1a1a1a;">CLI</text>
|
||||
<text x="110" y="115" class="node-text">CLIENT</text>
|
||||
<text x="110" y="130" class="status-text">REQ_SENT</text>
|
||||
|
||||
<circle cx="300" cy="70" r="20" class="node-circle" />
|
||||
<path d="M296 70 L304 64 L301 70 L305 70 L296 76 L299 70 Z" fill="none" stroke="#1a1a1a" stroke-width="1" stroke-linejoin="round"/>
|
||||
<text x="300" y="115" class="node-text">OPENFLARE</text>
|
||||
<text x="300" y="130" class="status-text">EDGE_PROXY</text>
|
||||
|
||||
<circle cx="490" cy="70" r="20" class="node-circle error" />
|
||||
<line x1="484" y1="64" x2="496" y2="76" stroke="#999" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<line x1="496" y1="64" x2="484" y2="76" stroke="#999" stroke-width="1.5" stroke-linecap="round"/>
|
||||
<text x="490" y="115" class="node-text" style="fill: #999;">ORIGIN</text>
|
||||
<text x="490" y="130" class="status-text error">UNAVAILABLE</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="footer-brand">
|
||||
<svg class="brand-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
|
||||
</svg>
|
||||
<span>OpenFlare</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const tsEl = document.getElementById('timestamp');
|
||||
if (tsEl) {
|
||||
const now = new Date();
|
||||
tsEl.textContent = now.toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
export default html;
|
||||
@@ -0,0 +1,109 @@
|
||||
/** 内置错误页模板:minimalist(默认) */
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{status}} | OpenFlare</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
background-color: #ffffff;
|
||||
color: #333333;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #333333;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
font-size: 20px;
|
||||
line-height: 1.6;
|
||||
color: #666666;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.host {
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: currentColor;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.error-code {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.error-description {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1 class="error-code" aria-label="HTTP status">{{status}}</h1>
|
||||
<p class="error-description">
|
||||
The upstream server is unreachable. Please try again later or contact the site administrator if the problem persists.
|
||||
</p>
|
||||
<p class="host">{{host}}</p>
|
||||
<div class="footer">
|
||||
<svg class="brand-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M13 2L3 14H12L11 22L21 10H12L13 2Z" />
|
||||
</svg>
|
||||
<span>OpenFlare</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
export default html;
|
||||
@@ -14,6 +14,7 @@
|
||||
"format:check": "biome format ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "^6.4.12",
|
||||
"@codemirror/lang-sql": "^6.10.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
|
||||
Generated
+237
-88
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@codemirror/lang-html':
|
||||
specifier: ^6.4.12
|
||||
version: 6.4.12
|
||||
'@codemirror/lang-sql':
|
||||
specifier: ^6.10.0
|
||||
version: 6.10.0
|
||||
@@ -109,7 +112,7 @@ importers:
|
||||
version: 12.11.2(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
axios:
|
||||
specifier: ^1.15.0
|
||||
version: 1.15.0(debug@4.4.3(supports-color@7.2.0))
|
||||
version: 1.15.0(debug@4.4.3(supports-color@8.1.1))
|
||||
babel-plugin-react-compiler:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
@@ -181,7 +184,7 @@ importers:
|
||||
version: 11.0.10(react@19.2.3)
|
||||
react-markdown:
|
||||
specifier: ^10.1.0
|
||||
version: 10.1.0(@types/react@19.2.2)(react@19.2.3)(supports-color@7.2.0)
|
||||
version: 10.1.0(@types/react@19.2.2)(react@19.2.3)(supports-color@8.1.1)
|
||||
react-syntax-highlighter:
|
||||
specifier: ^16.1.0
|
||||
version: 16.1.0(react@19.2.3)
|
||||
@@ -196,7 +199,7 @@ importers:
|
||||
version: 6.0.0
|
||||
remark-gfm:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1(supports-color@7.2.0)
|
||||
version: 4.0.1(supports-color@8.1.1)
|
||||
sonner:
|
||||
specifier: ^2.0.7
|
||||
version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
@@ -215,7 +218,7 @@ importers:
|
||||
version: 2.5.5
|
||||
'@eslint/eslintrc':
|
||||
specifier: ^3
|
||||
version: 3.3.1(supports-color@7.2.0)
|
||||
version: 3.3.1(supports-color@8.1.1)
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4
|
||||
version: 4.1.16
|
||||
@@ -248,7 +251,7 @@ importers:
|
||||
version: 15.5.6(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
eslint-plugin-unused-imports:
|
||||
specifier: ^4.4.1
|
||||
version: 4.4.1(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))
|
||||
version: 4.4.1(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))
|
||||
jsdom:
|
||||
specifier: ^29.1.1
|
||||
version: 29.1.1
|
||||
@@ -263,7 +266,7 @@ importers:
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^4.1.10
|
||||
version: 4.1.10(@types/node@20.19.24)(jsdom@29.1.1)(vite@8.1.4(@types/node@20.19.24)(jiti@2.6.1))
|
||||
version: 4.1.10(@types/node@20.19.24)(jsdom@29.1.1)(vite@8.1.4(@types/node@20.19.24)(jiti@2.6.1)(terser@5.49.1))
|
||||
|
||||
packages:
|
||||
|
||||
@@ -380,6 +383,15 @@ packages:
|
||||
'@codemirror/commands@6.10.3':
|
||||
resolution: {integrity: sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==}
|
||||
|
||||
'@codemirror/lang-css@6.3.1':
|
||||
resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==}
|
||||
|
||||
'@codemirror/lang-html@6.4.12':
|
||||
resolution: {integrity: sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w==}
|
||||
|
||||
'@codemirror/lang-javascript@6.2.5':
|
||||
resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==}
|
||||
|
||||
'@codemirror/lang-sql@6.10.0':
|
||||
resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==}
|
||||
|
||||
@@ -735,6 +747,9 @@ packages:
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/source-map@0.3.11':
|
||||
resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
@@ -744,9 +759,18 @@ packages:
|
||||
'@lezer/common@1.5.2':
|
||||
resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==}
|
||||
|
||||
'@lezer/css@1.3.6':
|
||||
resolution: {integrity: sha512-YJE78Wcg+zX8f10hiHWQ4Az48Qr/c13eId0VtRQYLBpxHDmDeSrXIlkbl+fJGW42rWC/uoUco9mhBZeVWP/A1g==}
|
||||
|
||||
'@lezer/highlight@1.2.3':
|
||||
resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
|
||||
|
||||
'@lezer/html@1.3.13':
|
||||
resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==}
|
||||
|
||||
'@lezer/javascript@1.5.4':
|
||||
resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==}
|
||||
|
||||
'@lezer/lr@1.4.10':
|
||||
resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==}
|
||||
|
||||
@@ -2250,6 +2274,11 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
acorn@8.18.0:
|
||||
resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
ajv@6.12.6:
|
||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||
|
||||
@@ -2371,6 +2400,9 @@ packages:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2453,6 +2485,9 @@ packages:
|
||||
comma-separated-tokens@2.0.3:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
|
||||
commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
@@ -4188,6 +4223,13 @@ packages:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
source-map-support@0.5.21:
|
||||
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
|
||||
|
||||
source-map@0.6.1:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
space-separated-tokens@2.0.2:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
@@ -4272,6 +4314,10 @@ packages:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
supports-color@8.1.1:
|
||||
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4293,6 +4339,11 @@ packages:
|
||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
terser@5.49.1:
|
||||
resolution: {integrity: sha512-7A2xlQ5EnGT8KPA92dUh6RbRYTVw8hEaEN9L1K68l4UOXFuV511NnAqObGoRqGOQofQcMypisu1s3xawCEHrvA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
text-segmentation@1.0.3:
|
||||
resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==}
|
||||
|
||||
@@ -4736,6 +4787,36 @@ snapshots:
|
||||
'@codemirror/view': 6.43.0
|
||||
'@lezer/common': 1.5.2
|
||||
|
||||
'@codemirror/lang-css@6.3.1':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.20.3
|
||||
'@codemirror/language': 6.12.3
|
||||
'@codemirror/state': 6.6.0
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/css': 1.3.6
|
||||
|
||||
'@codemirror/lang-html@6.4.12':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.20.3
|
||||
'@codemirror/lang-css': 6.3.1
|
||||
'@codemirror/lang-javascript': 6.2.5
|
||||
'@codemirror/language': 6.12.3
|
||||
'@codemirror/state': 6.6.0
|
||||
'@codemirror/view': 6.43.0
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/css': 1.3.6
|
||||
'@lezer/html': 1.3.13
|
||||
|
||||
'@codemirror/lang-javascript@6.2.5':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.20.3
|
||||
'@codemirror/language': 6.12.3
|
||||
'@codemirror/lint': 6.9.6
|
||||
'@codemirror/state': 6.6.0
|
||||
'@codemirror/view': 6.43.0
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/javascript': 1.5.4
|
||||
|
||||
'@codemirror/lang-sql@6.10.0':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.20.3
|
||||
@@ -4897,10 +4978,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
|
||||
'@eslint/eslintrc@3.3.1(supports-color@7.2.0)':
|
||||
'@eslint/eslintrc@3.3.1(supports-color@8.1.1)':
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
espree: 10.4.0
|
||||
globals: 14.0.0
|
||||
ignore: 5.3.2
|
||||
@@ -5066,6 +5147,12 @@ snapshots:
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/source-map@0.3.11':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
optional: true
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
@@ -5075,10 +5162,28 @@ snapshots:
|
||||
|
||||
'@lezer/common@1.5.2': {}
|
||||
|
||||
'@lezer/css@1.3.6':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.3
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/highlight@1.2.3':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
|
||||
'@lezer/html@1.3.13':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.3
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/javascript@1.5.4':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.3
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/lr@1.4.10':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
@@ -6273,13 +6378,13 @@ snapshots:
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)':
|
||||
'@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3)
|
||||
'@typescript-eslint/scope-manager': 8.46.3
|
||||
'@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/type-utils': 8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.46.3
|
||||
eslint: 9.39.1(jiti@2.6.1)(supports-color@7.2.0)
|
||||
graphemer: 1.4.0
|
||||
@@ -6290,11 +6395,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)':
|
||||
'@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.46.3
|
||||
'@typescript-eslint/types': 8.46.3
|
||||
'@typescript-eslint/typescript-estree': 8.46.3(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.46.3(supports-color@8.1.1)(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.46.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
eslint: 9.39.1(jiti@2.6.1)(supports-color@7.2.0)
|
||||
@@ -6302,11 +6407,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/project-service@8.46.3(supports-color@7.2.0)(typescript@5.9.3)':
|
||||
'@typescript-eslint/project-service@8.46.3(supports-color@8.1.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.46.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -6320,12 +6425,12 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
'@typescript-eslint/type-utils@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)':
|
||||
'@typescript-eslint/type-utils@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.46.3
|
||||
'@typescript-eslint/typescript-estree': 8.46.3(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
'@typescript-eslint/typescript-estree': 8.46.3(supports-color@8.1.1)(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
eslint: 9.39.1(jiti@2.6.1)(supports-color@7.2.0)
|
||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
@@ -6334,13 +6439,13 @@ snapshots:
|
||||
|
||||
'@typescript-eslint/types@8.46.3': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.46.3(supports-color@7.2.0)(typescript@5.9.3)':
|
||||
'@typescript-eslint/typescript-estree@8.46.3(supports-color@8.1.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.46.3(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/project-service': 8.46.3(supports-color@8.1.1)(typescript@5.9.3)
|
||||
'@typescript-eslint/tsconfig-utils': 8.46.3(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.46.3
|
||||
'@typescript-eslint/visitor-keys': 8.46.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
fast-glob: 3.3.3
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.5
|
||||
@@ -6350,12 +6455,12 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)':
|
||||
'@typescript-eslint/utils@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))
|
||||
'@typescript-eslint/scope-manager': 8.46.3
|
||||
'@typescript-eslint/types': 8.46.3
|
||||
'@typescript-eslint/typescript-estree': 8.46.3(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/typescript-estree': 8.46.3(supports-color@8.1.1)(typescript@5.9.3)
|
||||
eslint: 9.39.1(jiti@2.6.1)(supports-color@7.2.0)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@@ -6463,13 +6568,13 @@ snapshots:
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@vitest/mocker@4.1.10(vite@8.1.4(@types/node@20.19.24)(jiti@2.6.1))':
|
||||
'@vitest/mocker@4.1.10(vite@8.1.4(@types/node@20.19.24)(jiti@2.6.1)(terser@5.49.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.1.10
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 8.1.4(@types/node@20.19.24)(jiti@2.6.1)
|
||||
vite: 8.1.4(@types/node@20.19.24)(jiti@2.6.1)(terser@5.49.1)
|
||||
|
||||
'@vitest/pretty-format@4.1.10':
|
||||
dependencies:
|
||||
@@ -6526,6 +6631,9 @@ snapshots:
|
||||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
acorn@8.18.0:
|
||||
optional: true
|
||||
|
||||
ajv@6.12.6:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
@@ -6634,9 +6742,9 @@ snapshots:
|
||||
|
||||
axe-core@4.11.0: {}
|
||||
|
||||
axios@1.15.0(debug@4.4.3(supports-color@7.2.0)):
|
||||
axios@1.15.0(debug@4.4.3(supports-color@8.1.1)):
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11(debug@4.4.3(supports-color@7.2.0))
|
||||
follow-redirects: 1.15.11(debug@4.4.3(supports-color@8.1.1))
|
||||
form-data: 4.0.5
|
||||
proxy-from-env: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
@@ -6674,6 +6782,9 @@ snapshots:
|
||||
dependencies:
|
||||
fill-range: 7.1.1
|
||||
|
||||
buffer-from@1.1.2:
|
||||
optional: true
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -6768,6 +6879,9 @@ snapshots:
|
||||
|
||||
comma-separated-tokens@2.0.3: {}
|
||||
|
||||
commander@2.20.3:
|
||||
optional: true
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
@@ -6902,12 +7016,24 @@ snapshots:
|
||||
optionalDependencies:
|
||||
supports-color: 7.2.0
|
||||
|
||||
debug@3.2.7(supports-color@8.1.1):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 8.1.1
|
||||
|
||||
debug@4.4.3(supports-color@7.2.0):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 7.2.0
|
||||
|
||||
debug@4.4.3(supports-color@8.1.1):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 8.1.1
|
||||
|
||||
decimal.js-light@2.5.1: {}
|
||||
|
||||
decimal.js@10.6.0: {}
|
||||
@@ -7110,12 +7236,12 @@ snapshots:
|
||||
dependencies:
|
||||
'@next/eslint-plugin-next': 15.5.6
|
||||
'@rushstack/eslint-patch': 1.14.1
|
||||
'@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3)
|
||||
eslint: 9.39.1(jiti@2.6.1)(supports-color@7.2.0)
|
||||
eslint-import-resolver-node: 0.3.9(supports-color@7.2.0)
|
||||
eslint-import-resolver-node: 0.3.9(supports-color@8.1.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))
|
||||
@@ -7126,9 +7252,9 @@ snapshots:
|
||||
- eslint-plugin-import-x
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-node@0.3.9(supports-color@7.2.0):
|
||||
eslint-import-resolver-node@0.3.9(supports-color@8.1.1):
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@7.2.0)
|
||||
debug: 3.2.7(supports-color@8.1.1)
|
||||
is-core-module: 2.16.1
|
||||
resolve: 1.22.11
|
||||
transitivePeerDependencies:
|
||||
@@ -7145,22 +7271,22 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0):
|
||||
dependencies:
|
||||
debug: 3.2.7(supports-color@7.2.0)
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3)
|
||||
eslint: 9.39.1(jiti@2.6.1)(supports-color@7.2.0)
|
||||
eslint-import-resolver-node: 0.3.9(supports-color@7.2.0)
|
||||
eslint-import-resolver-node: 0.3.9(supports-color@8.1.1)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
@@ -7170,8 +7296,8 @@ snapshots:
|
||||
debug: 3.2.7(supports-color@7.2.0)
|
||||
doctrine: 2.1.0
|
||||
eslint: 9.39.1(jiti@2.6.1)(supports-color@7.2.0)
|
||||
eslint-import-resolver-node: 0.3.9(supports-color@7.2.0)
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
eslint-import-resolver-node: 0.3.9(supports-color@8.1.1)
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@@ -7183,7 +7309,7 @@ snapshots:
|
||||
string.prototype.trimend: 1.0.9
|
||||
tsconfig-paths: 3.15.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/parser': 8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
@@ -7234,11 +7360,11 @@ snapshots:
|
||||
string.prototype.matchall: 4.0.12
|
||||
string.prototype.repeat: 1.0.0
|
||||
|
||||
eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0)):
|
||||
eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0)):
|
||||
dependencies:
|
||||
eslint: 9.39.1(jiti@2.6.1)(supports-color@7.2.0)
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@typescript-eslint/eslint-plugin': 8.46.3(@typescript-eslint/parser@8.46.3(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)(supports-color@7.2.0))(supports-color@8.1.1)(typescript@5.9.3)
|
||||
|
||||
eslint-scope@8.4.0:
|
||||
dependencies:
|
||||
@@ -7256,7 +7382,7 @@ snapshots:
|
||||
'@eslint/config-array': 0.21.1(supports-color@7.2.0)
|
||||
'@eslint/config-helpers': 0.4.2
|
||||
'@eslint/core': 0.17.0
|
||||
'@eslint/eslintrc': 3.3.1(supports-color@7.2.0)
|
||||
'@eslint/eslintrc': 3.3.1(supports-color@8.1.1)
|
||||
'@eslint/js': 9.39.1
|
||||
'@eslint/plugin-kit': 0.4.1
|
||||
'@humanfs/node': 0.16.7
|
||||
@@ -7388,9 +7514,9 @@ snapshots:
|
||||
|
||||
flatted@3.3.3: {}
|
||||
|
||||
follow-redirects@1.15.11(debug@4.4.3(supports-color@7.2.0)):
|
||||
follow-redirects@1.15.11(debug@4.4.3(supports-color@8.1.1)):
|
||||
optionalDependencies:
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
|
||||
for-each@0.3.5:
|
||||
dependencies:
|
||||
@@ -7520,7 +7646,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.6(supports-color@7.2.0):
|
||||
hast-util-to-jsx-runtime@2.3.6(supports-color@8.1.1):
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
'@types/hast': 3.0.4
|
||||
@@ -7529,9 +7655,9 @@ snapshots:
|
||||
devlop: 1.1.0
|
||||
estree-util-is-identifier-name: 3.0.0
|
||||
hast-util-whitespace: 3.0.0
|
||||
mdast-util-mdx-expression: 2.0.1(supports-color@7.2.0)
|
||||
mdast-util-mdx-jsx: 3.2.0(supports-color@7.2.0)
|
||||
mdast-util-mdxjs-esm: 2.0.1(supports-color@7.2.0)
|
||||
mdast-util-mdx-expression: 2.0.1(supports-color@8.1.1)
|
||||
mdast-util-mdx-jsx: 3.2.0(supports-color@8.1.1)
|
||||
mdast-util-mdxjs-esm: 2.0.1(supports-color@8.1.1)
|
||||
property-information: 7.1.0
|
||||
space-separated-tokens: 2.0.2
|
||||
style-to-js: 1.1.21
|
||||
@@ -7976,14 +8102,14 @@ snapshots:
|
||||
unist-util-is: 6.0.1
|
||||
unist-util-visit-parents: 6.0.2
|
||||
|
||||
mdast-util-from-markdown@2.0.2(supports-color@7.2.0):
|
||||
mdast-util-from-markdown@2.0.2(supports-color@8.1.1):
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
'@types/unist': 3.0.3
|
||||
decode-named-character-reference: 1.2.0
|
||||
devlop: 1.1.0
|
||||
mdast-util-to-string: 4.0.0
|
||||
micromark: 4.0.2(supports-color@7.2.0)
|
||||
micromark: 4.0.2(supports-color@8.1.1)
|
||||
micromark-util-decode-numeric-character-reference: 2.0.2
|
||||
micromark-util-decode-string: 2.0.1
|
||||
micromark-util-normalize-identifier: 2.0.1
|
||||
@@ -8001,67 +8127,67 @@ snapshots:
|
||||
mdast-util-find-and-replace: 3.0.2
|
||||
micromark-util-character: 2.1.1
|
||||
|
||||
mdast-util-gfm-footnote@2.1.0(supports-color@7.2.0):
|
||||
mdast-util-gfm-footnote@2.1.0(supports-color@8.1.1):
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@7.2.0)
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@8.1.1)
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
micromark-util-normalize-identifier: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-gfm-strikethrough@2.0.0(supports-color@7.2.0):
|
||||
mdast-util-gfm-strikethrough@2.0.0(supports-color@8.1.1):
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@7.2.0)
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@8.1.1)
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-gfm-table@2.0.0(supports-color@7.2.0):
|
||||
mdast-util-gfm-table@2.0.0(supports-color@8.1.1):
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
markdown-table: 3.0.4
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@7.2.0)
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@8.1.1)
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-gfm-task-list-item@2.0.0(supports-color@7.2.0):
|
||||
mdast-util-gfm-task-list-item@2.0.0(supports-color@8.1.1):
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@7.2.0)
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@8.1.1)
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-gfm@3.1.0(supports-color@7.2.0):
|
||||
mdast-util-gfm@3.1.0(supports-color@8.1.1):
|
||||
dependencies:
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@7.2.0)
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@8.1.1)
|
||||
mdast-util-gfm-autolink-literal: 2.0.1
|
||||
mdast-util-gfm-footnote: 2.1.0(supports-color@7.2.0)
|
||||
mdast-util-gfm-strikethrough: 2.0.0(supports-color@7.2.0)
|
||||
mdast-util-gfm-table: 2.0.0(supports-color@7.2.0)
|
||||
mdast-util-gfm-task-list-item: 2.0.0(supports-color@7.2.0)
|
||||
mdast-util-gfm-footnote: 2.1.0(supports-color@8.1.1)
|
||||
mdast-util-gfm-strikethrough: 2.0.0(supports-color@8.1.1)
|
||||
mdast-util-gfm-table: 2.0.0(supports-color@8.1.1)
|
||||
mdast-util-gfm-task-list-item: 2.0.0(supports-color@8.1.1)
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-mdx-expression@2.0.1(supports-color@7.2.0):
|
||||
mdast-util-mdx-expression@2.0.1(supports-color@8.1.1):
|
||||
dependencies:
|
||||
'@types/estree-jsx': 1.0.5
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@7.2.0)
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@8.1.1)
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-mdx-jsx@3.2.0(supports-color@7.2.0):
|
||||
mdast-util-mdx-jsx@3.2.0(supports-color@8.1.1):
|
||||
dependencies:
|
||||
'@types/estree-jsx': 1.0.5
|
||||
'@types/hast': 3.0.4
|
||||
@@ -8069,7 +8195,7 @@ snapshots:
|
||||
'@types/unist': 3.0.3
|
||||
ccount: 2.0.1
|
||||
devlop: 1.1.0
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@7.2.0)
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@8.1.1)
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
parse-entities: 4.0.2
|
||||
stringify-entities: 4.0.4
|
||||
@@ -8078,13 +8204,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-mdxjs-esm@2.0.1(supports-color@7.2.0):
|
||||
mdast-util-mdxjs-esm@2.0.1(supports-color@8.1.1):
|
||||
dependencies:
|
||||
'@types/estree-jsx': 1.0.5
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@7.2.0)
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@8.1.1)
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -8295,10 +8421,10 @@ snapshots:
|
||||
|
||||
micromark-util-types@2.0.2: {}
|
||||
|
||||
micromark@4.0.2(supports-color@7.2.0):
|
||||
micromark@4.0.2(supports-color@8.1.1):
|
||||
dependencies:
|
||||
'@types/debug': 4.1.12
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
decode-named-character-reference: 1.2.0
|
||||
devlop: 1.1.0
|
||||
micromark-core-commonmark: 2.0.3
|
||||
@@ -8654,17 +8780,17 @@ snapshots:
|
||||
|
||||
react-is@18.3.1: {}
|
||||
|
||||
react-markdown@10.1.0(@types/react@19.2.2)(react@19.2.3)(supports-color@7.2.0):
|
||||
react-markdown@10.1.0(@types/react@19.2.2)(react@19.2.3)(supports-color@8.1.1):
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
'@types/react': 19.2.2
|
||||
devlop: 1.1.0
|
||||
hast-util-to-jsx-runtime: 2.3.6(supports-color@7.2.0)
|
||||
hast-util-to-jsx-runtime: 2.3.6(supports-color@8.1.1)
|
||||
html-url-attributes: 3.0.1
|
||||
mdast-util-to-hast: 13.2.1
|
||||
react: 19.2.3
|
||||
remark-parse: 11.0.0(supports-color@7.2.0)
|
||||
remark-parse: 11.0.0(supports-color@8.1.1)
|
||||
remark-rehype: 11.1.2
|
||||
unified: 11.0.5
|
||||
unist-util-visit: 5.0.0
|
||||
@@ -8796,21 +8922,21 @@ snapshots:
|
||||
hast-util-to-string: 3.0.1
|
||||
unist-util-visit: 5.0.0
|
||||
|
||||
remark-gfm@4.0.1(supports-color@7.2.0):
|
||||
remark-gfm@4.0.1(supports-color@8.1.1):
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-gfm: 3.1.0(supports-color@7.2.0)
|
||||
mdast-util-gfm: 3.1.0(supports-color@8.1.1)
|
||||
micromark-extension-gfm: 3.0.0
|
||||
remark-parse: 11.0.0(supports-color@7.2.0)
|
||||
remark-parse: 11.0.0(supports-color@8.1.1)
|
||||
remark-stringify: 11.0.0
|
||||
unified: 11.0.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
remark-parse@11.0.0(supports-color@7.2.0):
|
||||
remark-parse@11.0.0(supports-color@8.1.1):
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@7.2.0)
|
||||
mdast-util-from-markdown: 2.0.2(supports-color@8.1.1)
|
||||
micromark-util-types: 2.0.2
|
||||
unified: 11.0.5
|
||||
transitivePeerDependencies:
|
||||
@@ -9006,6 +9132,15 @@ snapshots:
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
source-map-support@0.5.21:
|
||||
dependencies:
|
||||
buffer-from: 1.1.2
|
||||
source-map: 0.6.1
|
||||
optional: true
|
||||
|
||||
source-map@0.6.1:
|
||||
optional: true
|
||||
|
||||
space-separated-tokens@2.0.2: {}
|
||||
|
||||
stable-hash@0.0.5: {}
|
||||
@@ -9104,6 +9239,11 @@ snapshots:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
supports-color@8.1.1:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
optional: true
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
svg-pathdata@6.0.3:
|
||||
@@ -9117,6 +9257,14 @@ snapshots:
|
||||
|
||||
tapable@2.3.0: {}
|
||||
|
||||
terser@5.49.1:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.11
|
||||
acorn: 8.18.0
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
optional: true
|
||||
|
||||
text-segmentation@1.0.3:
|
||||
dependencies:
|
||||
utrie: 1.0.2
|
||||
@@ -9357,7 +9505,7 @@ snapshots:
|
||||
d3-time: 3.1.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
vite@8.1.4(@types/node@20.19.24)(jiti@2.6.1):
|
||||
vite@8.1.4(@types/node@20.19.24)(jiti@2.6.1)(terser@5.49.1):
|
||||
dependencies:
|
||||
lightningcss: 1.32.0
|
||||
picomatch: 4.0.5
|
||||
@@ -9368,11 +9516,12 @@ snapshots:
|
||||
'@types/node': 20.19.24
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.6.1
|
||||
terser: 5.49.1
|
||||
|
||||
vitest@4.1.10(@types/node@20.19.24)(jsdom@29.1.1)(vite@8.1.4(@types/node@20.19.24)(jiti@2.6.1)):
|
||||
vitest@4.1.10(@types/node@20.19.24)(jsdom@29.1.1)(vite@8.1.4(@types/node@20.19.24)(jiti@2.6.1)(terser@5.49.1)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.10
|
||||
'@vitest/mocker': 4.1.10(vite@8.1.4(@types/node@20.19.24)(jiti@2.6.1))
|
||||
'@vitest/mocker': 4.1.10(vite@8.1.4(@types/node@20.19.24)(jiti@2.6.1)(terser@5.49.1))
|
||||
'@vitest/pretty-format': 4.1.10
|
||||
'@vitest/runner': 4.1.10
|
||||
'@vitest/snapshot': 4.1.10
|
||||
@@ -9389,7 +9538,7 @@ snapshots:
|
||||
tinyexec: 1.2.4
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 3.1.0
|
||||
vite: 8.1.4(@types/node@20.19.24)(jiti@2.6.1)
|
||||
vite: 8.1.4(@types/node@20.19.24)(jiti@2.6.1)(terser@5.49.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.24
|
||||
|
||||
@@ -16,78 +16,87 @@ const (
|
||||
defaultOriginErrorPageStatusTag = "500-599"
|
||||
)
|
||||
|
||||
// DefaultOriginErrorPageHTML is the built-in Cloudflare-style origin error page.
|
||||
// DefaultOriginErrorPageHTML is the built-in default (aligned with frontend minimalist).
|
||||
// Placeholders {{status}} and {{host}} are substituted at request time by Lua.
|
||||
const DefaultOriginErrorPageHTML = `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{status}} | 源站暂时无法提供服务</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
background: #f6f7f9;
|
||||
color: #1f2937;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body { background: #0f1419; color: #e5e7eb; }
|
||||
.card { background: #1a2332; border-color: #2d3a4d; box-shadow: none; }
|
||||
.status { color: #f3f4f6; }
|
||||
.muted { color: #9ca3af; }
|
||||
.host { background: #243044; color: #d1d5db; }
|
||||
}
|
||||
.card {
|
||||
width: min(32rem, calc(100% - 2rem));
|
||||
padding: 2.5rem 2rem;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
|
||||
text-align: center;
|
||||
}
|
||||
.status {
|
||||
margin: 0;
|
||||
font-size: clamp(3.5rem, 12vw, 5.5rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 1;
|
||||
color: #111827;
|
||||
}
|
||||
h1 {
|
||||
margin: 1rem 0 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
p { margin: 0.4rem 0; line-height: 1.6; }
|
||||
.muted { color: #6b7280; font-size: 0.95rem; }
|
||||
.host {
|
||||
display: inline-block;
|
||||
margin-top: 1.25rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
font-size: 0.85rem;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{status}} | OpenFlare</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background-color: #ffffff;
|
||||
color: #333333;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
.error-code {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: #333333;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.error-description {
|
||||
font-size: 20px;
|
||||
line-height: 1.6;
|
||||
color: #666666;
|
||||
max-width: 480px;
|
||||
}
|
||||
.host {
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: #999999;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.brand-icon { width: 24px; height: 24px; fill: currentColor; display: block; }
|
||||
@media (max-width: 480px) {
|
||||
.error-code { font-size: 36px; }
|
||||
.error-description { font-size: 18px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="card" role="main">
|
||||
<p class="status" aria-label="HTTP status">{{status}}</p>
|
||||
<h1>源站暂时无法提供服务</h1>
|
||||
<p class="muted">网关已拦截源站错误响应。请稍后重试;若问题持续,请联系站点管理员。</p>
|
||||
<p class="host">{{host}}</p>
|
||||
</main>
|
||||
<div class="container">
|
||||
<h1 class="error-code" aria-label="HTTP status">{{status}}</h1>
|
||||
<p class="error-description">
|
||||
The upstream server is unreachable. Please try again later or contact the site administrator if the problem persists.
|
||||
</p>
|
||||
<p class="host">{{host}}</p>
|
||||
<div class="footer">
|
||||
<svg class="brand-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M13 2L3 14H12L11 22L21 10H12L13 2Z" />
|
||||
</svg>
|
||||
<span>OpenFlare</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
@@ -112,8 +112,8 @@ func TestRenderOriginErrorPageDefaultsEmptyHTMLAndStatusCodes(t *testing.T) {
|
||||
if !strings.Contains(html, "{{status}}") || !strings.Contains(html, "{{host}}") {
|
||||
t.Fatal("default HTML must include placeholders")
|
||||
}
|
||||
if !strings.Contains(html, "源站暂时无法提供服务") {
|
||||
t.Fatal("default HTML missing neutral copy")
|
||||
if !strings.Contains(html, "OpenFlare") || !strings.Contains(html, "upstream server is unreachable") {
|
||||
t.Fatal("default HTML missing minimalist copy")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user