import * as RadixDialog from '@radix-ui/react-dialog'; import * as Tooltip from '@radix-ui/react-tooltip'; import classNames from 'classnames'; import { motion, type Transition, type Variants } from 'framer-motion'; import { memo, useCallback, useEffect, useRef, useState } from 'react'; import { toast } from 'sonner'; import { type DeploymentRecord, useDeploymentRecords } from '~/lib/hooks/useDeploymentRecords'; import { DeploymentPlatformEnum, DeploymentStatusEnum } from '~/types/deployment'; import { ConfirmationDialog, DialogDescription, DialogTitle } from '../../ui/Dialog'; import { IconButton } from '../../ui/IconButton'; import { Tabs, TabsList, TabsTrigger } from '../../ui/Tabs'; const transition: Transition = { duration: 0.15, ease: [0.16, 1, 0.3, 1], }; const backdropVariants: Variants = { closed: { opacity: 0, transition, }, open: { opacity: 1, transition, }, }; const dialogVariants: Variants = { closed: { x: '-50%', y: '-40%', scale: 0.96, opacity: 0, transition, }, open: { x: '-50%', y: '-50%', scale: 1, opacity: 1, transition, }, }; interface DeploymentRecordsDialogProps { isOpen: boolean; onClose: () => void; } export const DeploymentRecordsDialog = memo(({ isOpen, onClose }: DeploymentRecordsDialogProps) => { const { deploymentRecords, totals, stats, isLoading, isPlatformLoading, loadPlatformRecords, refreshDeploymentRecords, toggleAccess, deletePage, } = useDeploymentRecords(); const [activePlatform, setActivePlatform] = useState(DeploymentPlatformEnum._1PANEL); const [loadedPlatforms, setLoadedPlatforms] = useState>(new Set()); const initialLoadDone = useRef(false); // 确认对话框状态 type ConfirmAction = 'toggle-access' | 'delete'; type ConfirmDialogState = { isOpen: boolean; action: ConfirmAction; recordId: string | null; platform: string | null; recordStatus?: string; }; const [confirmDialogState, setConfirmDialogState] = useState({ isOpen: false, action: 'toggle-access', recordId: null, platform: null, }); const [isConfirmationLoading, setIsConfirmationLoading] = useState(false); const formatDate = (dateString: string) => { const date = new Date(dateString); return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }).format(date); }; const loadMore = useCallback(() => { const currentRecords = deploymentRecords[activePlatform] || []; loadPlatformRecords({ offset: currentRecords.length, platform: activePlatform }); }, [activePlatform, deploymentRecords, loadPlatformRecords]); const handleTabChange = useCallback( (value: string) => { const newPlatform = value as DeploymentPlatformEnum; setActivePlatform(newPlatform); if (!loadedPlatforms.has(newPlatform)) { loadPlatformRecords({ platform: newPlatform }); setLoadedPlatforms((prev) => new Set(prev).add(newPlatform)); } }, [loadPlatformRecords, loadedPlatforms], ); useEffect(() => { if (isOpen && !initialLoadDone.current) { refreshDeploymentRecords(); setLoadedPlatforms((prev) => new Set(prev).add(activePlatform)); initialLoadDone.current = true; } if (!isOpen) { initialLoadDone.current = false; } }, [isOpen, activePlatform, refreshDeploymentRecords]); const openConfirmDialog = useCallback((action: ConfirmAction, record: DeploymentRecord) => { setConfirmDialogState({ isOpen: true, action, recordId: record.id, platform: record.platform, recordStatus: record.status, }); }, []); const closeConfirmDialog = useCallback(() => { setConfirmDialogState((prev) => ({ ...prev, isOpen: false })); }, []); const handleConfirmAction = useCallback(async () => { const { action, recordId, platform } = confirmDialogState; if (!recordId || !platform) { return; } setIsConfirmationLoading(true); try { if (action === 'toggle-access') { await toggleAccess(recordId, platform); } if (action === 'delete') { await deletePage(recordId, platform); } refreshDeploymentRecords(); closeConfirmDialog(); } catch (error) { console.error('操作失败:', error); toast.error('操作失败: ' + (error instanceof Error ? error.message : '未知错误')); } finally { setIsConfirmationLoading(false); } }, [confirmDialogState, toggleAccess, deletePage, refreshDeploymentRecords, closeConfirmDialog]); const handleToggleAccess = useCallback( (record: DeploymentRecord) => { openConfirmDialog('toggle-access', record); }, [openConfirmDialog], ); const handleDeletePage = useCallback( (record: DeploymentRecord) => { openConfirmDialog('delete', record); }, [openConfirmDialog], ); const cardClasses = classNames( 'p-4 rounded-lg shadow-sm', 'bg-upage-elements-bg-depth-1', 'border border-upage-elements-borderColor', ); const platformIcons = { [DeploymentPlatformEnum._1PANEL]: 'i-ph:browser', [DeploymentPlatformEnum.NETLIFY]: 'i-ph:cloud', [DeploymentPlatformEnum.VERCEL]: 'i-ph:triangle', }; // 状态配置类型 type StatusConfig = { text: string; bgClass: string; dotClass: string; icon: string; }; // 部署状态配置 const deploymentStatusConfig: Record = { [DeploymentStatusEnum.SUCCESS]: { text: '已部署', bgClass: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400', dotClass: 'bg-green-500 dark:bg-green-400', icon: 'i-carbon:checkmark-filled', }, [DeploymentStatusEnum.DEPLOYED]: { text: '已部署', bgClass: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400', dotClass: 'bg-green-500 dark:bg-green-400', icon: 'i-carbon:checkmark-filled', }, [DeploymentStatusEnum.PENDING]: { text: '部署中', bgClass: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400', dotClass: 'bg-yellow-500 dark:bg-yellow-400', icon: 'i-carbon:time', }, [DeploymentStatusEnum.DEPLOYING]: { text: '部署中', bgClass: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400', dotClass: 'bg-yellow-500 dark:bg-yellow-400', icon: 'i-carbon:in-progress', }, [DeploymentStatusEnum.FAILED]: { text: '失败', bgClass: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400', dotClass: 'bg-red-500 dark:bg-red-400', icon: 'i-carbon:close-filled', }, [DeploymentStatusEnum.INACTIVE]: { text: '已停用', bgClass: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400', dotClass: 'bg-gray-500 dark:bg-gray-400', icon: 'i-carbon:pause-filled', }, }; // 部署状态徽章组件 const DeploymentStatusBadge = ({ status }: { status: string }) => { // 获取状态配置,如果不存在则使用默认配置 const config = deploymentStatusConfig[status] || { text: status, bgClass: 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400', dotClass: 'bg-gray-500 dark:bg-gray-400', icon: 'i-carbon:help', }; return ( {config.text} ); }; const isActive = useCallback((status: string) => { return status === DeploymentStatusEnum.SUCCESS || status === DeploymentStatusEnum.DEPLOYED; }, []); return ( <> 展示网站部署记录,包括各平台部署的网站数量、运行状态及访问统计等信息。
部署记录
{isLoading && (
数据加载中...
)}
网站总数
{stats.totalSites || 0}
累计访问量
{stats.totalVisits.toLocaleString()}

部署详情

{Object.values(DeploymentPlatformEnum).map((platform, index) => { const count = stats.sitesByPlatform?.[platform] || 0; const isLoading = isPlatformLoading(platform); const isActive = activePlatform === platform; const isLast = index === Object.values(DeploymentPlatformEnum).length - 1; return (
{platform === DeploymentPlatformEnum._1PANEL ? '1Panel' : platform} {isLoading ? ( ) : ( {count || 0} )}
{isActive && (
)} {!isLast && (
)}
); })}
{isPlatformLoading(activePlatform) && !deploymentRecords[activePlatform]?.length ? ( ) : deploymentRecords[activePlatform]?.length > 0 ? ( deploymentRecords[activePlatform].map((record) => ( )) ) : ( )}
聊天 状态 部署地址 部署时间 更新时间 操作
加载中...
{record.chat?.description || '未命名聊天'}
{record.url} {formatDate(record.createdAt)} {formatDate(record.updatedAt)}
handleToggleAccess(record)} className="!text-gray-500 !hover:text-purple-600 dark:!text-gray-400 dark:!hover:text-purple-400" /> {isActive(record.status) ? '停止访问' : '开启访问'} { window.open(`/chat/${record.chatId}`); }} className="!text-gray-500 !hover:text-blue-600 dark:!text-gray-400 dark:!hover:text-blue-400" /> 编辑页面 handleDeletePage(record)} className="!text-gray-500 !hover:text-red-600 dark:!text-gray-400 dark:!hover:text-red-400" /> 删除页面
暂无部署记录
{deploymentRecords[activePlatform]?.length > 0 && deploymentRecords[activePlatform].length < (totals[activePlatform] || 0) && (
)}
); });