'use client'; import { useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { ChevronDown, ChevronRight, 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 { Spinner } from '@/components/ui/spinner'; import { type PagesDeployment, PagesService } from '@/lib/services/openflare'; import { formatDateTime } from '@/lib/utils'; import { DeploymentUploadDialog } from '../../components/deployment-upload-dialog'; import { deploymentFilesQueryKey, deploymentsQueryKey, formatBytes, projectQueryKey, projectsQueryKey, sourceQueryKey, } from '../../components/pages-utils'; import { DeploymentFilesPanel } from './deployment-files-panel'; const SOURCE_LABELS: Record = { manual_upload: '本地上传', manual_url: 'URL 导入', remote_url: 'Remote URL', github_release: 'GitHub', }; const TRIGGER_LABELS: Record = { manual_upload: '手动上传', manual_url: '手动导入', manual_sync: '手动同步', scheduled_auto_update: '定时更新', }; interface DeploymentHistoryProps { projectId: number; activeDeploymentId?: number | null; rootDir?: string; entryFile?: string; } type PendingAction = { type: 'activate' | 'delete'; deployment: PagesDeployment; }; function deploymentSnapshot(deployment: PagesDeployment) { return [ SOURCE_LABELS[deployment.source_type], deployment.source_label, TRIGGER_LABELS[deployment.trigger_type], ] .filter(Boolean) .join(' · '); } export function DeploymentHistory({ projectId, activeDeploymentId, rootDir = '', entryFile = 'index.html', }: DeploymentHistoryProps) { const queryClient = useQueryClient(); const [uploadOpen, setUploadOpen] = useState(false); const [expandedDeploymentId, setExpandedDeploymentId] = useState< number | null >(null); const [pendingAction, setPendingAction] = useState( null, ); const deploymentsQuery = useQuery({ queryKey: deploymentsQueryKey(projectId), queryFn: () => PagesService.listDeployments(projectId), }); const deployments = useMemo(() => { const records = [...(deploymentsQuery.data ?? [])]; return records.sort((left, right) => { const leftActive = left.id === activeDeploymentId || left.status === 'active'; const rightActive = right.id === activeDeploymentId || right.status === 'active'; if (leftActive !== rightActive) return leftActive ? -1 : 1; return right.deployment_number - left.deployment_number; }); }, [activeDeploymentId, deploymentsQuery.data]); const invalidateDeploymentState = async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: deploymentsQueryKey(projectId), }), queryClient.invalidateQueries({ queryKey: projectQueryKey(projectId) }), queryClient.invalidateQueries({ queryKey: sourceQueryKey(projectId) }), queryClient.invalidateQueries({ queryKey: projectsQueryKey }), ]); }; const activateMutation = useMutation({ mutationFn: (deploymentId: number) => PagesService.activateDeployment(projectId, deploymentId), onSuccess: async () => { toast.success('历史部署已激活;自动更新(如已开启)已关闭'); await invalidateDeploymentState(); setPendingAction(null); }, onError: (error) => { toast.error(error instanceof Error ? error.message : '激活失败'); }, }); const deleteMutation = useMutation({ mutationFn: (deploymentId: number) => PagesService.deleteDeployment(projectId, deploymentId), onSuccess: async (_, deploymentId) => { toast.success('部署已删除'); queryClient.removeQueries({ queryKey: deploymentFilesQueryKey(projectId, deploymentId), }); await invalidateDeploymentState(); setPendingAction(null); }, onError: (error) => { toast.error(error instanceof Error ? error.message : '删除失败'); }, }); const actionPending = activateMutation.isPending || deleteMutation.isPending; return ( <> 部署历史 部署记录不可变,来源信息是创建部署时的安全快照。 {deploymentsQuery.isLoading ? ( ) : deploymentsQuery.isError ? (
void deploymentsQuery.refetch()} />
) : deployments.length === 0 ? ( ) : ( deployments.map((deployment) => { const active = deployment.id === activeDeploymentId || deployment.status === 'active'; const expanded = expandedDeploymentId === deployment.id; return (
部署 #{deployment.deployment_number} {active ? 'Production' : '历史部署'} {deploymentSnapshot(deployment)}

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

创建于 {formatDateTime(deployment.created_at)}

{expanded ? ( ) : null}
); }) )}
{ if (!open && !actionPending) setPendingAction(null); }} > {pendingAction?.type === 'activate' ? '激活历史部署' : '删除部署'} {pendingAction?.type === 'activate' ? '激活其它历史部署会终止当前来源任务;若已开启自动更新,将同时关闭自动更新。' : `确认删除部署 #${pendingAction?.deployment.deployment_number} 吗?此操作不可恢复。`} 取消 { event.preventDefault(); if (!pendingAction) return; if (pendingAction.type === 'activate') { activateMutation.mutate(pendingAction.deployment.id); } else { deleteMutation.mutate(pendingAction.deployment.id); } }} > {actionPending ? : null} 确认 ); }