'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 { AppModal } from '@/components/ui/app-modal'; import { StatusBadge } from '@/components/ui/status-badge'; import { deleteManagedDomain, getManagedDomains, } from '@/features/managed-domains/api/managed-domains'; import type { ManagedDomainItem } from '@/features/managed-domains/types'; import { deleteTlsCertificate, getTlsCertificates, } 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 { WebsiteEditorModal } from '@/features/websites/components/website-editor-modal'; import { buildCertificateLabel, getCertificateStatus, getErrorMessage, getMatchTypeMeta, } 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; }; export function WebsitesPage() { const queryClient = useQueryClient(); const [feedback, setFeedback] = useState(null); const [isWebsiteModalOpen, setIsWebsiteModalOpen] = useState(false); const [isCertificateImportOpen, setIsCertificateImportOpen] = useState(false); const [isCertificateListOpen, setIsCertificateListOpen] = useState(false); const [selectedCertificateId, setSelectedCertificateId] = useState< number | null >(null); const [isCertificateDetailOpen, setIsCertificateDetailOpen] = useState(false); const [isCertificateEditorOpen, setIsCertificateEditorOpen] = useState(false); const [preferredCertificateId, setPreferredCertificateId] = useState< number | null >(null); const managedDomainsQuery = useQuery({ queryKey: ['managed-domains'], queryFn: getManagedDomains, }); const certificatesQuery = useQuery({ queryKey: ['tls-certificates', 'list'], queryFn: getTlsCertificates, }); const deleteDomainMutation = useMutation({ mutationFn: deleteManagedDomain, onSuccess: async () => { setFeedback({ tone: 'success', message: '网站已删除。' }); await queryClient.invalidateQueries({ queryKey: ['managed-domains'] }); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const deleteCertificateMutation = useMutation({ mutationFn: deleteTlsCertificate, onSuccess: async () => { setFeedback({ tone: 'success', message: '证书已删除。' }); await Promise.all([ queryClient.invalidateQueries({ queryKey: ['tls-certificates'] }), queryClient.invalidateQueries({ queryKey: ['managed-domains'] }), ]); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const domains = useMemo( () => managedDomainsQuery.data ?? [], [managedDomainsQuery.data], ); const certificates = useMemo( () => certificatesQuery.data ?? [], [certificatesQuery.data], ); const certificateMap = useMemo( () => new Map(certificates.map((item) => [item.id, item])), [certificates], ); const handleOpenWebsiteModal = () => { setPreferredCertificateId(null); setFeedback(null); setIsWebsiteModalOpen(true); }; const handleDeleteDomain = (domain: ManagedDomainItem) => { if (!window.confirm(`确认删除网站 ${domain.domain} 吗?`)) { return; } setFeedback(null); deleteDomainMutation.mutate(domain.id); }; const handleDeleteCertificate = (certificate: TlsCertificateItem) => { if (!window.confirm(`确认删除证书 ${certificate.name} 吗?`)) { return; } setFeedback(null); deleteCertificateMutation.mutate(certificate.id); }; const handleOpenCertificateDetail = (certificate: TlsCertificateItem) => { setSelectedCertificateId(certificate.id); setIsCertificateDetailOpen(true); }; const handleOpenCertificateEditor = (certificate: TlsCertificateItem) => { setSelectedCertificateId(certificate.id); setIsCertificateEditorOpen(true); }; return ( <>
setIsCertificateListOpen(true)} > 证书列表 新增网站 } /> {feedback ? ( ) : null} {managedDomainsQuery.isLoading ? ( ) : managedDomainsQuery.isError ? ( ) : domains.length === 0 ? ( ) : (
{domains.map((domain) => { const certificate = domain.cert_id ? certificateMap.get(domain.cert_id) ?? null : null; const matchType = getMatchTypeMeta(domain.domain); return (

{domain.domain}

{domain.remark || '暂无备注'}

绑定证书

{certificate ? buildCertificateLabel(certificate) : '未绑定证书'}

更新时间

{formatDateTime(domain.updated_at)}

详情 handleDeleteDomain(domain)} disabled={deleteDomainMutation.isPending} > 删除
); })}
)}
{ setIsWebsiteModalOpen(false); setPreferredCertificateId(null); }} certificates={certificates} certificatesLoading={certificatesQuery.isLoading} preferredCertificateId={preferredCertificateId} onRequestImportCertificate={() => setIsCertificateImportOpen(true)} onSaved={(_, mode) => { setPreferredCertificateId(null); setFeedback({ tone: 'success', message: mode === 'create' ? '网站已创建。' : '网站已更新。', }); }} /> setIsCertificateImportOpen(false)} onImported={(certificate) => { setPreferredCertificateId(certificate.id); setFeedback({ tone: 'success', message: `证书 ${certificate.name} 已导入,可直接用于当前网站。`, }); }} /> setIsCertificateListOpen(false)} title="证书列表" description="回到主界面后可在这里统一查看已添加的证书,并继续新增或删除证书。" size="xl" >
void queryClient.invalidateQueries({ queryKey: ['tls-certificates'], }) } > 刷新证书 setIsCertificateImportOpen(true)} > 添加证书
{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.remark || '暂无备注'}

handleOpenCertificateDetail(certificate)} className="px-3 py-2 text-xs" > 查看 handleOpenCertificateEditor(certificate)} className="px-3 py-2 text-xs" > 编辑 handleDeleteCertificate(certificate)} disabled={deleteCertificateMutation.isPending} className="px-3 py-2 text-xs" > 删除
); })}
)}
setIsCertificateDetailOpen(false)} onEdit={() => { setIsCertificateDetailOpen(false); setIsCertificateEditorOpen(true); }} onDelete={() => { const certificate = certificates.find( (item) => item.id === selectedCertificateId, ); if (certificate) { setIsCertificateDetailOpen(false); handleDeleteCertificate(certificate); } }} deleting={deleteCertificateMutation.isPending} /> setIsCertificateEditorOpen(false)} onSaved={(certificate) => { setFeedback({ tone: 'success', message: `证书 ${certificate.name} 已更新。`, }); }} /> ); }