'use client'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { useEffect, useState, type FormEvent } from 'react'; import { EmptyState } from '@/components/feedback/empty-state'; import { ErrorState } from '@/components/feedback/error-state'; import { LoadingState } from '@/components/feedback/loading-state'; import { PageHeader } from '@/components/layout/page-header'; import { AppCard } from '@/components/ui/app-card'; import { AppModal } from '@/components/ui/app-modal'; import { StatusBadge } from '@/components/ui/status-badge'; import { activatePagesDeployment, createPagesProject, deletePagesDeployment, deletePagesProject, getPagesProject, getPagesDeployments, getPagesProjects, updatePagesProject, uploadPagesDeployment, } from '@/features/pages/api/pages'; import type { PagesProject } from '@/features/pages/types'; import { DangerButton, PrimaryButton, ResourceField, ResourceInput, SecondaryButton, ToggleField, } from '@/features/shared/components/resource-primitives'; const projectsQueryKey = ['pages-projects']; function projectQueryKey(projectId: string | number) { return ['pages-project', String(projectId)]; } function deploymentsQueryKey(projectId: string | number) { return ['pages-deployments', Number(projectId)]; } function formatBytes(value: number) { if (value < 1024) { return `${value} B`; } if (value < 1024 * 1024) { return `${(value / 1024).toFixed(1)} KiB`; } return `${(value / 1024 / 1024).toFixed(1)} MiB`; } function formatDate(value?: string | null) { if (!value) { return '未激活'; } return new Date(value).toLocaleString(); } export function PagesPage() { const [isCreateModalOpen, setCreateModalOpen] = useState(false); const projectsQuery = useQuery({ queryKey: projectsQueryKey, queryFn: getPagesProjects, }); return (

OpenFlare Pages

边缘静态站点托管

创建 Pages 项目,上传已构建的 zip 静态资源包,然后在规则中选择 Pages 项目作为上游。发布后 Agent 会拉取部署包并在边缘节点本地服务静态文件。

setCreateModalOpen(true)} > 新建 Pages 项目
{projectsQuery.isLoading ? ( 正在加载 Pages 项目... ) : projectsQuery.error ? (

{projectsQuery.error.message}

) : (projectsQuery.data ?? []).length === 0 ? ( setCreateModalOpen(true)} > 新建 Pages 项目 } > ) : ( (projectsQuery.data ?? []).map((project) => ( )) )}
setCreateModalOpen(false)} />
); } export function PagesProjectDetailPage({ projectId }: { projectId: string }) { const router = useRouter(); const queryClient = useQueryClient(); const [isEditModalOpen, setEditModalOpen] = useState(false); const [isUploadModalOpen, setUploadModalOpen] = useState(false); const parsedProjectId = Number(projectId); const projectQuery = useQuery({ queryKey: projectQueryKey(projectId), queryFn: () => getPagesProject(parsedProjectId), enabled: projectId !== '' && Number.isFinite(parsedProjectId), }); const deploymentsQuery = useQuery({ queryKey: deploymentsQueryKey(parsedProjectId), queryFn: () => getPagesDeployments(parsedProjectId), enabled: projectId !== '' && Number.isFinite(parsedProjectId), }); const activateMutation = useMutation({ mutationFn: (deploymentId: number) => activatePagesDeployment(parsedProjectId, deploymentId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: deploymentsQueryKey(parsedProjectId), }); queryClient.invalidateQueries({ queryKey: projectQueryKey(projectId) }); queryClient.invalidateQueries({ queryKey: projectsQueryKey }); }, }); const deleteDeploymentMutation = useMutation({ mutationFn: (deploymentId: number) => deletePagesDeployment(parsedProjectId, deploymentId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: deploymentsQueryKey(parsedProjectId), }); queryClient.invalidateQueries({ queryKey: projectQueryKey(projectId) }); queryClient.invalidateQueries({ queryKey: projectsQueryKey }); }, }); const deleteProjectMutation = useMutation({ mutationFn: () => deletePagesProject(parsedProjectId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: projectsQueryKey }); router.push('/pages'); }, }); if (projectId === '' || !Number.isFinite(parsedProjectId)) { return ( ); } if (projectQuery.isLoading) { return ; } if (projectQuery.isError) { return ( ); } const project = projectQuery.data; if (!project) { return ( ); } const handleDeleteProject = () => { if (!window.confirm(`确认删除 Pages 项目 ${project.name} 吗?`)) { return; } deleteProjectMutation.mutate(); }; return (
返回列表 setEditModalOpen(true)} > 编辑项目 setUploadModalOpen(true)} > 上传部署包 删除项目 } /> 0 ? ( setUploadModalOpen(true)} > 上传部署包 ) : null } > {deploymentsQuery.isLoading ? (

加载中...

) : deploymentsQuery.isError ? (

{deploymentsQuery.error.message}

) : (deploymentsQuery.data ?? []).length === 0 ? ( setUploadModalOpen(true)} > 上传部署包 ) : (
{(deploymentsQuery.data ?? []).map((deployment) => (

#{deployment.deployment_number}{' '} {deployment.status === 'active' ? '· 已激活' : ''}

{deployment.checksum.slice(0, 16)} ·{' '} {deployment.file_count} files ·{' '} {formatBytes(deployment.total_size)}

{ if ( window.confirm( `确认激活部署 #${deployment.deployment_number} 吗?`, ) ) { activateMutation.mutate(deployment.id); } }} > 激活 { if ( window.confirm( `确认删除部署 #${deployment.deployment_number} 吗?`, ) ) { deleteDeploymentMutation.mutate(deployment.id); } }} > 删除
))}
)}
setEditModalOpen(false)} project={project} /> setUploadModalOpen(false)} projectId={parsedProjectId} />
); } function PagesDeploymentUploadModal({ isOpen, onClose, projectId, }: { isOpen: boolean; onClose: () => void; projectId: number; }) { const queryClient = useQueryClient(); const [file, setFile] = useState(null); const [entryFile, setEntryFile] = useState('index.html'); const [uploadProgress, setUploadProgress] = useState(null); const [errorMessage, setErrorMessage] = useState(null); const resetForm = () => { setFile(null); setEntryFile('index.html'); setUploadProgress(null); setErrorMessage(null); }; const handleClose = () => { resetForm(); onClose(); }; const uploadMutation = useMutation({ mutationFn: async ({ shouldActivate }: { shouldActivate: boolean }) => { if (!file) { throw new Error('请选择 zip 文件'); } setUploadProgress(0); setErrorMessage(null); const deployment = await uploadPagesDeployment( projectId, file, entryFile, (percent) => { setUploadProgress(percent); } ); if (shouldActivate) { await activatePagesDeployment(projectId, deployment.id); } return deployment; }, onSuccess: () => { resetForm(); queryClient.invalidateQueries({ queryKey: deploymentsQueryKey(projectId), }); queryClient.invalidateQueries({ queryKey: projectQueryKey(projectId), }); queryClient.invalidateQueries({ queryKey: projectsQueryKey, }); onClose(); }, onError: (error) => { setUploadProgress(null); setErrorMessage(error instanceof Error ? error.message : '上传失败'); }, }); const handleUploadOnly = () => { uploadMutation.mutate({ shouldActivate: false }); }; const handleUploadAndDeploy = () => { uploadMutation.mutate({ shouldActivate: true }); }; return ( 取消 {uploadMutation.isPending && !uploadMutation.variables?.shouldActivate ? `上传中 (${uploadProgress ?? 0}%)...` : '上传'} {uploadMutation.isPending && uploadMutation.variables?.shouldActivate ? `上传并部署中 (${uploadProgress ?? 0}%)...` : '上传并部署'} } >
setFile(event.target.files?.[0] ?? null)} /> setEntryFile(event.target.value)} /> {uploadProgress !== null && (
上传进度 {uploadProgress}%
)} {errorMessage ? (

{errorMessage}

) : null}
); } function PagesProjectEditModal({ isOpen, onClose, project, }: { isOpen: boolean; onClose: () => void; project: PagesProject; }) { const queryClient = useQueryClient(); const [name, setName] = useState(project.name); const [slug, setSlug] = useState(project.slug); const [description, setDescription] = useState(project.description || ''); const [spaFallbackEnabled, setSpaFallbackEnabled] = useState(project.spa_fallback_enabled); const [spaFallbackPath, setSpaFallbackPath] = useState(project.spa_fallback_path); const [apiProxyEnabled, setApiProxyEnabled] = useState(project.api_proxy_enabled || false); const [apiProxyPath, setApiProxyPath] = useState(project.api_proxy_path || ''); const [apiProxyPass, setApiProxyPass] = useState(project.api_proxy_pass || ''); const [apiProxyRewrite, setApiProxyRewrite] = useState(project.api_proxy_rewrite || ''); useEffect(() => { setName(project.name); setSlug(project.slug); setDescription(project.description || ''); setSpaFallbackEnabled(project.spa_fallback_enabled); setSpaFallbackPath(project.spa_fallback_path); setApiProxyEnabled(project.api_proxy_enabled || false); setApiProxyPath(project.api_proxy_path || ''); setApiProxyPass(project.api_proxy_pass || ''); setApiProxyRewrite(project.api_proxy_rewrite || ''); }, [project]); const updateMutation = useMutation({ mutationFn: () => updatePagesProject(project.id, { name, slug, description, enabled: project.enabled, spa_fallback_enabled: spaFallbackEnabled, spa_fallback_path: spaFallbackPath, api_proxy_enabled: apiProxyEnabled, api_proxy_path: apiProxyPath, api_proxy_pass: apiProxyPass, api_proxy_rewrite: apiProxyRewrite, }), onSuccess: () => { onClose(); queryClient.invalidateQueries({ queryKey: projectQueryKey(project.id) }); queryClient.invalidateQueries({ queryKey: projectsQueryKey }); }, }); function handleUpdate(event: FormEvent) { event.preventDefault(); updateMutation.mutate(); } return ( 取消 {updateMutation.isPending ? '保存中...' : '保存修改'}
} >
setName(event.target.value)} required /> setSlug(event.target.value)} /> setDescription(event.target.value)} /> setSpaFallbackPath(event.target.value)} /> {apiProxyEnabled && ( <> setApiProxyPath(event.target.value)} required /> setApiProxyPass(event.target.value)} required /> setApiProxyRewrite(event.target.value)} /> )} {updateMutation.error ? (

{updateMutation.error.message}

) : null}
); } function PagesProjectCreateModal({ isOpen, onClose, }: { isOpen: boolean; onClose: () => void; }) { const queryClient = useQueryClient(); const [name, setName] = useState(''); const [slug, setSlug] = useState(''); const [description, setDescription] = useState(''); const [spaFallbackEnabled, setSpaFallbackEnabled] = useState(false); const [spaFallbackPath, setSpaFallbackPath] = useState('/index.html'); const resetForm = () => { setName(''); setSlug(''); setDescription(''); setSpaFallbackEnabled(false); setSpaFallbackPath('/index.html'); }; const closeModal = () => { resetForm(); onClose(); }; const createMutation = useMutation({ mutationFn: () => createPagesProject({ name, slug, description, enabled: true, spa_fallback_enabled: spaFallbackEnabled, spa_fallback_path: spaFallbackPath, api_proxy_enabled: false, api_proxy_path: '', api_proxy_pass: '', api_proxy_rewrite: '', }), onSuccess: () => { resetForm(); onClose(); queryClient.invalidateQueries({ queryKey: projectsQueryKey }); }, }); function handleCreate(event: FormEvent) { event.preventDefault(); createMutation.mutate(); } return ( 取消 {createMutation.isPending ? '创建中...' : '创建项目'} } >
setName(event.target.value)} required /> setSlug(event.target.value)} /> setDescription(event.target.value)} /> setSpaFallbackPath(event.target.value)} /> {createMutation.error ? (

{createMutation.error.message}

) : null}
); } function PagesProjectListItem({ project }: { project: PagesProject }) { return (

{project.name}

{project.slug}

{project.description ? (

{project.description}

) : null} {project.spa_fallback_enabled ? (

回退路径:{project.spa_fallback_path || '/index.html'}

) : null}

部署数

{project.deployment_count}

当前激活

{project.active_deployment ? `#${project.active_deployment.deployment_number}` : '暂无'}

激活时间:{formatDate(project.active_deployment?.activated_at)}

查看详情 →
); }