'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 { EmptyStateWithBorder } from '@/components/layout/empty'; import { ErrorInline } from '@/components/layout/error'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { HtmlEditorWorkspace } from '@/components/common/html-editor-workspace'; import { OFFLINE_PAGE_TEMPLATES, DEFAULT_OFFLINE_PAGE_TEMPLATE_ID, effectiveOfflinePageHTML, getOfflinePageTemplate, } from '@/lib/openflare/offline-page-templates'; import { OptionService } from '@/lib/services/openflare'; import { invalidateResponseQueries, KEY_SW_HTML, mapOptionsToOfflineFields, OPTIONS_QUERY_KEY, optionsToMap, } from '../../components/shared'; const OFFLINE_PAGE_HTML_MAX_BYTES = 256 * 1024; export default function OfflinePageEditPage() { const { user, loading: authLoading } = useAuth(); const queryClient = useQueryClient(); const [html, setHtml] = useState(''); const [restoreOpen, setRestoreOpen] = useState(false); const [templateId, setTemplateId] = useState( DEFAULT_OFFLINE_PAGE_TEMPLATE_ID, ); const optionsQuery = useQuery({ queryKey: OPTIONS_QUERY_KEY, queryFn: () => OptionService.list(), enabled: !!user?.is_admin, }); useEffect(() => { if (!optionsQuery.data) return; setHtml(mapOptionsToOfflineFields(optionsToMap(optionsQuery.data)).html); }, [optionsQuery.data]); const saveMutation = useMutation({ mutationFn: async () => { const bytes = new TextEncoder().encode(html).length; if (bytes > OFFLINE_PAGE_HTML_MAX_BYTES) { throw new Error('HTML 大小超出限制(最大 256KB)'); } await OptionService.updateBatch([{ key: KEY_SW_HTML, value: html }]); }, onSuccess: async () => { toast.success('离线页 HTML 已保存,请前往版本发布使配置生效'); await invalidateResponseQueries(queryClient); }, onError: (error) => { toast.error(error instanceof Error ? error.message : '保存失败'); }, }); const loadSelectedTemplate = () => { const tmpl = getOfflinePageTemplate(templateId); if (!tmpl) { toast.error('未找到所选模板'); return; } setHtml(tmpl.html); toast.success(`已加载模板「${tmpl.name}」到编辑器`); }; const restoreDefault = () => { setHtml(''); setRestoreOpen(false); toast.success('已清空为使用内置默认模板(需保存后生效)'); }; if (authLoading) { return (
); } if (!user?.is_admin) { return (
); } if (optionsQuery.isLoading) { return (
); } if (optionsQuery.isError) { return (
void optionsQuery.refetch()} />
); } return (

编辑离线页 HTML

内置模板
} /> 恢复默认 HTML? 将清空编辑器内容,保存后使用内置默认模板。此操作不会自动保存。 取消 确认恢复
); }