'use client'; import Link from 'next/link'; import { Suspense, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useRouter, useSearchParams } from 'next/navigation'; import { ArrowLeft, FileText, Pencil, Trash2, Upload } from 'lucide-react'; import { toast } from 'sonner'; 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 { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton'; import { Spinner } from '@/components/ui/spinner'; import { PagesService } from '@/lib/services/openflare'; import { formatDateTime } from '@/lib/utils'; import { DeploymentUploadDialog, pagesEntryPath, } from '../components/deployment-upload-dialog'; import { ProjectEditorDialog } from '../components/project-editor-dialog'; import { projectQueryKey, projectsQueryKey, sourceQueryKey, } from '../components/pages-utils'; import { DeploymentHistory } from './components/deployment-history'; import { PagesSourceCard } from './components/pages-source-card'; function PagesDetailPageFallback() { return (
); } function PagesDetailRoute() { const searchParams = useSearchParams(); const router = useRouter(); const queryClient = useQueryClient(); const [editorOpen, setEditorOpen] = useState(false); const [uploadOpen, setUploadOpen] = useState(false); const [deleteProjectOpen, setDeleteProjectOpen] = useState(false); const rawProjectId = searchParams.get('id')?.trim() ?? ''; const projectId = Number(rawProjectId); const validProjectId = rawProjectId !== '' && Number.isInteger(projectId) && projectId > 0; const projectQuery = useQuery({ queryKey: projectQueryKey(projectId), queryFn: () => PagesService.getProject(projectId), enabled: validProjectId, }); const deleteProjectMutation = useMutation({ mutationFn: () => PagesService.deleteProject(projectId), onSuccess: async () => { toast.success('项目已删除'); await queryClient.invalidateQueries({ queryKey: projectsQueryKey }); router.push('/pages'); }, onError: (error) => { toast.error(error instanceof Error ? error.message : '删除失败'); }, }); if (!validProjectId) { return (
); } if (projectQuery.isLoading) { return (
); } if (projectQuery.isError) { return (
void projectQuery.refetch()} />
); } const project = projectQuery.data; if (!project) { return (
); } const activeDeployment = project.active_deployment; const entryPath = pagesEntryPath(project.root_dir ?? '', project.entry_file); return (

{project.name}

{project.slug} · {project.deployment_count} 个部署

当前生产部署 Agent 当前应拉取并提供服务的不可变部署。 {activeDeployment ? '生产中' : '未发布'} {activeDeployment ? ( <>

部署 #{activeDeployment.deployment_number}

{activeDeployment.checksum.slice(0, 20)}

激活于{' '} {activeDeployment.activated_at ? formatDateTime(activeDeployment.activated_at) : '未知时间'}

) : (

尚无生产部署。上传或同步来源后,从部署历史激活一个版本。

)}
站点入口 解包校验、发布快照与 Agent 切换共同使用此路径。 {project.enabled ? '项目已启用' : '项目已停用'} {entryPath}

项目更新于 {formatDateTime(project.updated_at)}

{ setEditorOpen(nextOpen); if (!nextOpen) { void queryClient.invalidateQueries({ queryKey: sourceQueryKey(projectId), }); } }} project={project} /> 删除 Pages 项目 确认删除项目 {project.name} 吗?此操作不可恢复。 取消 { event.preventDefault(); deleteProjectMutation.mutate(); }} > {deleteProjectMutation.isPending ? ( ) : null} 确认删除
); } export default function PagesDetailPage() { return ( }> ); }