"use client" import Link from "next/link" import {useMemo, useState} from "react" import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query" import {useSearchParams} from "next/navigation" import {ArrowLeft, ChevronDown, ChevronRight, FileText, Loader2, Trash2, Upload,} from "lucide-react" import {toast} from "sonner" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import {EmptyStateWithBorder} from "@/components/layout/empty" import {ErrorInline} from "@/components/layout/error" import {LoadingStateWithBorder} from "@/components/layout/loading" import {Badge} from "@/components/ui/badge" import {Button} from "@/components/ui/button" import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from "@/components/ui/table" import {type PagesDeployment, PagesService} from "@/lib/services/openflare" import {formatDateTime} from "@/lib/utils" import {DeploymentUploadDialog} from "../components/deployment-upload-dialog" import {ProjectEditorDialog} from "../components/project-editor-dialog" import { deploymentFilesQueryKey, deploymentsQueryKey, formatBytes, projectQueryKey, projectsQueryKey, } from "../components/pages-utils" function DeploymentFilesPanel({ projectId, deployment, }: { projectId: number deployment: PagesDeployment }) { const filesQuery = useQuery({ queryKey: deploymentFilesQueryKey(projectId, deployment.id), queryFn: () => PagesService.listDeploymentFiles(projectId, deployment.id), }) if (filesQuery.isLoading) { return

加载文件清单...

} if (filesQuery.isError) { return (

{filesQuery.error instanceof Error ? filesQuery.error.message : "加载失败"}

) } const files = filesQuery.data ?? [] if (files.length === 0) { return

暂无文件记录

} return (
路径 大小 {files.map((file) => ( {file.path} {formatBytes(file.size)} ))}
) } export function PagesDetailPageClient() { const searchParams = useSearchParams() const queryClient = useQueryClient() const projectId = searchParams.get("id")?.trim() ?? "" const parsedProjectId = Number(projectId) const [editorOpen, setEditorOpen] = useState(false) const [uploadOpen, setUploadOpen] = useState(false) const [expandedDeploymentId, setExpandedDeploymentId] = useState(null) const [deleteProjectOpen, setDeleteProjectOpen] = useState(false) const [pendingDeploymentAction, setPendingDeploymentAction] = useState<{ type: "activate" | "delete" deployment: PagesDeployment } | null>(null) const enabled = projectId !== "" && Number.isFinite(parsedProjectId) const projectQuery = useQuery({ queryKey: projectQueryKey(projectId), queryFn: () => PagesService.getProject(parsedProjectId), enabled, }) const deploymentsQuery = useQuery({ queryKey: deploymentsQueryKey(parsedProjectId), queryFn: () => PagesService.listDeployments(parsedProjectId), enabled, }) const activateMutation = useMutation({ mutationFn: (deploymentId: number) => PagesService.activateDeployment(parsedProjectId, deploymentId), onSuccess: async () => { toast.success("部署已激活") await Promise.all([ queryClient.invalidateQueries({ queryKey: deploymentsQueryKey(parsedProjectId) }), queryClient.invalidateQueries({ queryKey: projectQueryKey(projectId) }), queryClient.invalidateQueries({ queryKey: projectsQueryKey }), ]) setPendingDeploymentAction(null) }, onError: (error) => { toast.error(error instanceof Error ? error.message : "激活失败") }, }) const deleteDeploymentMutation = useMutation({ mutationFn: (deploymentId: number) => PagesService.deleteDeployment(parsedProjectId, deploymentId), onSuccess: async () => { toast.success("部署已删除") await Promise.all([ queryClient.invalidateQueries({ queryKey: deploymentsQueryKey(parsedProjectId) }), queryClient.invalidateQueries({ queryKey: projectQueryKey(projectId) }), queryClient.invalidateQueries({ queryKey: projectsQueryKey }), ]) setPendingDeploymentAction(null) }, onError: (error) => { toast.error(error instanceof Error ? error.message : "删除失败") }, }) const deleteProjectMutation = useMutation({ mutationFn: () => PagesService.deleteProject(parsedProjectId), onSuccess: async () => { toast.success("项目已删除") await queryClient.invalidateQueries({ queryKey: projectsQueryKey }) window.location.href = "/pages" }, onError: (error) => { toast.error(error instanceof Error ? error.message : "删除失败") }, }) const project = projectQuery.data const deployments = useMemo(() => deploymentsQuery.data ?? [], [deploymentsQuery.data]) if (!enabled) { return (
) } if (projectQuery.isLoading) { return (
) } if (projectQuery.isError) { return (
void projectQuery.refetch()} />
) } if (!project) { return (
) } return (

{project.name}

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

状态

{project.enabled ? "已启用" : "已停用"}

当前激活

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

更新时间

{formatDateTime(project.updated_at)}

部署历史

部署不可变;激活后发布配置,Agent 才会拉取并切换静态资源。

{deploymentsQuery.isLoading ? ( ) : deployments.length === 0 ? ( ) : (
{deployments.map((deployment) => { const expanded = expandedDeploymentId === deployment.id return (

#{deployment.deployment_number}

{deployment.status === "active" ? ( 已激活 ) : null}

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

创建于 {formatDateTime(deployment.created_at)}

{expanded ? ( ) : null}
) })}
)}
删除 Pages 项目 确认删除项目 {project.name} 吗?此操作不可恢复。 取消 deleteProjectMutation.mutate()} > {deleteProjectMutation.isPending ? ( ) : ( "确认删除" )} { if (!open) setPendingDeploymentAction(null) }} > {pendingDeploymentAction?.type === "activate" ? "激活部署" : "删除部署"} {pendingDeploymentAction?.type === "activate" ? `确认激活部署 #${pendingDeploymentAction.deployment.deployment_number} 吗?` : `确认删除部署 #${pendingDeploymentAction?.deployment.deployment_number} 吗?`} 取消 { if (!pendingDeploymentAction) return if (pendingDeploymentAction.type === "activate") { activateMutation.mutate(pendingDeploymentAction.deployment.id) } else { deleteDeploymentMutation.mutate(pendingDeploymentAction.deployment.id) } }} > 确认
) }