'use client'; import Link from 'next/link'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; import { EmptyState } from '@/components/feedback/empty-state'; import { ErrorState } from '@/components/feedback/error-state'; import { InlineMessage } from '@/components/feedback/inline-message'; import { LoadingState } from '@/components/feedback/loading-state'; import { PageHeader } from '@/components/layout/page-header'; import { AppCard } from '@/components/ui/app-card'; import { StatusBadge } from '@/components/ui/status-badge'; import { deleteTlsCertificate, getTlsCertificates, renewTlsCertificate, } from '@/features/tls-certificates/api/tls-certificates'; import type { TlsCertificateItem } from '@/features/tls-certificates/types'; import { CertificateDetailModal } from '@/features/websites/components/certificate-detail-modal'; import { CertificateEditorModal } from '@/features/websites/components/certificate-editor-modal'; import { CertificateImportModal } from '@/features/websites/components/certificate-import-modal'; import { CertificateApplyModal } from '@/features/websites/components/certificate-apply-modal'; import { getCertificateStatus, getErrorMessage } from '@/features/websites/utils'; import { DangerButton, PrimaryButton, SecondaryButton, } from '@/features/shared/components/resource-primitives'; import { formatDateTime } from '@/lib/utils/date'; type FeedbackState = { tone: 'info' | 'success' | 'danger'; message: string; }; const certificatesQueryKey = ['tls-certificates', 'list'] as const; export function TlsCertificatesPage() { const queryClient = useQueryClient(); const [feedback, setFeedback] = useState(null); const [isImportOpen, setIsImportOpen] = useState(false); const [isApplyOpen, setIsApplyOpen] = useState(false); const [selectedCertificateId, setSelectedCertificateId] = useState< number | null >(null); const [isDetailOpen, setIsDetailOpen] = useState(false); const [isEditorOpen, setIsEditorOpen] = useState(false); const [editAcmeCertificate, setEditAcmeCertificate] = useState(null); const certificatesQuery = useQuery({ queryKey: certificatesQueryKey, queryFn: getTlsCertificates, }); const deleteCertificateMutation = useMutation({ mutationFn: deleteTlsCertificate, onSuccess: async () => { setFeedback({ tone: 'success', message: '证书已删除。' }); await queryClient.invalidateQueries({ queryKey: ['tls-certificates'] }); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const renewCertificateMutation = useMutation({ mutationFn: renewTlsCertificate, onSuccess: async (cert) => { setFeedback({ tone: 'success', message: `证书 ${cert.name} 续期任务已提交。` }); await queryClient.invalidateQueries({ queryKey: ['tls-certificates'] }); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const certificates = useMemo( () => certificatesQuery.data ?? [], [certificatesQuery.data], ); const handleDeleteCertificate = (certificate: TlsCertificateItem) => { if (!window.confirm(`确认删除证书 ${certificate.name} 吗?`)) { return; } setFeedback(null); deleteCertificateMutation.mutate(certificate.id); }; const handleRenewCertificate = (certificate: TlsCertificateItem) => { if (!window.confirm(`确认提交证书 ${certificate.name} 的续期申请吗?`)) { return; } setFeedback(null); renewCertificateMutation.mutate(certificate.id); }; const handleOpenCertificateDetail = (certificate: TlsCertificateItem) => { setSelectedCertificateId(certificate.id); setIsDetailOpen(true); }; const handleOpenCertificateEditor = (certificate: TlsCertificateItem) => { if (certificate.provider === 'acme') { setEditAcmeCertificate(certificate); } else { setSelectedCertificateId(certificate.id); setIsEditorOpen(true); } }; return ( <>
返回网站 void queryClient.invalidateQueries({ queryKey: ['tls-certificates'], }) } > 刷新证书 DNS 账号 setIsImportOpen(true)}> 导入证书 setIsApplyOpen(true)}> 申请证书
} /> {feedback ? ( ) : null} {certificatesQuery.isLoading ? ( ) : certificatesQuery.isError ? ( ) : certificates.length === 0 ? ( ) : (
{certificates.map((certificate) => { const status = getCertificateStatus(certificate); return (

{certificate.name}

生效:{formatDateTime(certificate.not_before)}

到期:{formatDateTime(certificate.not_after)}

来源:{certificate.provider === 'acme' ? 'ACME 申请' : '手动上传'}

{certificate.apply_status === 'applying' &&

状态:申请中...

} {certificate.apply_status === 'error' &&

状态:申请失败 ({certificate.apply_message})

}

备注:{certificate.remark || '暂无备注'}

handleOpenCertificateDetail(certificate)} className="px-3 py-2 text-xs" > 查看 handleOpenCertificateEditor(certificate)} className="px-3 py-2 text-xs" > 编辑 {certificate.provider === 'acme' && ( handleRenewCertificate(certificate)} disabled={renewCertificateMutation.isPending} className="px-3 py-2 text-xs" > 续期 )} handleDeleteCertificate(certificate)} disabled={deleteCertificateMutation.isPending} className="px-3 py-2 text-xs" > 删除
); })}
)}
{isImportOpen ? ( setIsImportOpen(false)} onImported={(certificate) => { setFeedback({ tone: 'success', message: `证书 ${certificate.name} 已导入。`, }); }} /> ) : null} {isApplyOpen ? ( setIsApplyOpen(false)} onApplied={(certificate) => { setFeedback({ tone: 'success', message: `证书 ${certificate.name} 申请任务已提交。`, }); }} /> ) : null} {editAcmeCertificate ? ( setEditAcmeCertificate(null)} editCertificate={editAcmeCertificate} onApplied={(certificate) => { setFeedback({ tone: 'success', message: `证书 ${certificate.name} 配置已更新,重新申请中...`, }); }} /> ) : null} {isDetailOpen ? ( setIsDetailOpen(false)} onEdit={() => { setIsDetailOpen(false); const certificate = certificates.find( (item) => item.id === selectedCertificateId, ); if (certificate) { handleOpenCertificateEditor(certificate); } }} onDelete={() => { const certificate = certificates.find( (item) => item.id === selectedCertificateId, ); if (certificate) { setIsDetailOpen(false); handleDeleteCertificate(certificate); } }} deleting={deleteCertificateMutation.isPending} /> ) : null} {isEditorOpen ? ( setIsEditorOpen(false)} onSaved={(certificate) => { setFeedback({ tone: 'success', message: `证书 ${certificate.name} 已更新。`, }); }} /> ) : null} ); }