'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 (
}>
);
}