'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; type CertificateApplyMode = 'edit-acme' | 'convert-upload'; 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 [applyCertificate, setApplyCertificate] = useState(null); const [applyMode, setApplyMode] = useState('edit-acme'); 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') { setApplyMode('edit-acme'); setApplyCertificate(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.provider === 'upload' && certificate.apply_status === 'applying' ? (

状态:转换申请中...

) : certificate.apply_status === 'applying' ? (

状态:申请中...

) : null} {certificate.provider === 'upload' && certificate.apply_status === 'error' ? (

状态:转换失败 ({certificate.apply_message})

) : certificate.apply_status === 'error' ? (

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

) : null}

备注:{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} {applyCertificate ? ( setApplyCertificate(null)} mode={applyMode} certificate={applyCertificate} onApplied={(certificate) => { setFeedback({ tone: 'success', message: applyMode === 'convert-upload' ? `证书 ${certificate.name} 转换申请已提交。` : `证书 ${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} 已更新。`, }); }} onConvert={(certificate) => { setIsEditorOpen(false); setApplyMode('convert-upload'); setApplyCertificate(certificate); }} /> ) : null} ); }