'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 && (
)}
{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 ? '保存中...' : '保存修改'}
}
>
);
}
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 ? '创建中...' : '创建项目'}
}
>
);
}
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)}
查看详情 →
);
}