'use client'; import Link from 'next/link'; import { useEffect, useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Expand, Loader2, Pencil, Plus, Save, X } from 'lucide-react'; import { toast } from 'sonner'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { Label } from '@/components/ui/label'; import { Switch } from '@/components/ui/switch'; import { effectiveOfflinePageHTML } from '@/lib/openflare/offline-page-templates'; import { OptionService, ZoneService, zoneQueryKey, } from '@/lib/services/openflare'; import { ScopeDomainDialog } from './scope-domain-dialog'; import { defaultOfflinePageFields, invalidateResponseQueries, KEY_SW_DOMAINS, KEY_SW_ENABLED, KEY_SW_HTML, mapOptionsToOfflineFields, type OfflinePageFields, } from './shared'; export function OfflinePageTab({ optionMap, }: { optionMap: Record; }) { const queryClient = useQueryClient(); const [fields, setFields] = useState( defaultOfflinePageFields, ); const [scopeOpen, setScopeOpen] = useState(false); useEffect(() => { setFields(mapOptionsToOfflineFields(optionMap)); }, [optionMap]); const previewSrcDoc = useMemo( () => effectiveOfflinePageHTML(fields.html), [fields.html], ); const zonesQuery = useQuery({ queryKey: [...zoneQueryKey, 'sw-scope'], queryFn: async () => { const zones = await ZoneService.list(); const overviews = await Promise.all( zones.map((zone) => ZoneService.getOverview(zone.id)), ); return overviews.map((ov) => ({ zoneDomain: ov.zone.domain, domains: [ov.zone.domain, ...ov.domains.map((d) => d.domain)], })); }, }); const saveMutation = useMutation({ mutationFn: async () => { await OptionService.updateBatch([ { key: KEY_SW_ENABLED, value: String(fields.enabled) }, { key: KEY_SW_HTML, value: fields.html }, { key: KEY_SW_DOMAINS, value: JSON.stringify(fields.domains) }, ]); }, onSuccess: async () => { toast.success('离线页已保存,请前往版本发布使配置生效'); await invalidateResponseQueries(queryClient); }, onError: (error) => { toast.error(error instanceof Error ? error.message : '保存失败'); }, }); return (
离线兜底 启用后给启用 HTTPS 的网站下发 Service Worker,域名被墙时浏览器从缓存展示此离线页。

仅对 HTTPS 网站生效;未启用的站点不受影响。

setFields((prev) => ({ ...prev, enabled })) } aria-label='启用离线兜底' className='mt-0.5 shrink-0' />

仅对选中的 HTTPS 域名生效;留空则不注入。

{fields.domains.length === 0 ? (

{fields.enabled ? '尚未选择生效域名,保存后不注入任何站点。' : '启用离线兜底后可选择生效域名。'}

) : (
{fields.domains.map((domain) => ( {domain} ))}
)}
页面预览