From 63c320472655b77ec8b0b7be85b343133c077167 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 13 Mar 2026 14:06:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0TLS=E8=AF=81=E4=B9=A6?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=EF=BC=8C=E6=94=AF=E6=8C=81=E8=AF=81=E4=B9=A6?= =?UTF-8?q?=E7=9A=84=E6=9F=A5=E7=9C=8B=E3=80=81=E5=AF=BC=E5=85=A5=E3=80=81?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E5=92=8C=E5=88=A0=E9=99=A4=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(dashboard)/website/certificate/page.tsx | 5 + .../components/tls-certificates-page.tsx | 243 ++++++++++++++++++ .../websites/components/websites-page.tsx | 194 +------------- 3 files changed, 255 insertions(+), 187 deletions(-) create mode 100644 atsf_server/web/app/(dashboard)/website/certificate/page.tsx create mode 100644 atsf_server/web/features/tls-certificates/components/tls-certificates-page.tsx diff --git a/atsf_server/web/app/(dashboard)/website/certificate/page.tsx b/atsf_server/web/app/(dashboard)/website/certificate/page.tsx new file mode 100644 index 00000000..39cad7d6 --- /dev/null +++ b/atsf_server/web/app/(dashboard)/website/certificate/page.tsx @@ -0,0 +1,5 @@ +import { TlsCertificatesPage } from '@/features/tls-certificates/components/tls-certificates-page'; + +export default function WebsiteCertificateRoute() { + return ; +} diff --git a/atsf_server/web/features/tls-certificates/components/tls-certificates-page.tsx b/atsf_server/web/features/tls-certificates/components/tls-certificates-page.tsx new file mode 100644 index 00000000..50733230 --- /dev/null +++ b/atsf_server/web/features/tls-certificates/components/tls-certificates-page.tsx @@ -0,0 +1,243 @@ +'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, +} 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 { 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 [selectedCertificateId, setSelectedCertificateId] = useState< + number | null + >(null); + const [isDetailOpen, setIsDetailOpen] = useState(false); + const [isEditorOpen, setIsEditorOpen] = useState(false); + + 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 certificates = useMemo( + () => certificatesQuery.data ?? [], + [certificatesQuery.data], + ); + + const handleDeleteCertificate = (certificate: TlsCertificateItem) => { + if (!window.confirm(`确认删除证书 ${certificate.name} 吗?`)) { + return; + } + + setFeedback(null); + deleteCertificateMutation.mutate(certificate.id); + }; + + const handleOpenCertificateDetail = (certificate: TlsCertificateItem) => { + setSelectedCertificateId(certificate.id); + setIsDetailOpen(true); + }; + + const handleOpenCertificateEditor = (certificate: TlsCertificateItem) => { + setSelectedCertificateId(certificate.id); + setIsEditorOpen(true); + }; + + return ( + <> +
+ + + 返回网站 + + + void queryClient.invalidateQueries({ + queryKey: ['tls-certificates'], + }) + } + > + 刷新证书 + + setIsImportOpen(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.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" + > + 删除 + +
+
+
+ ); + })} +
+ )} +
+ + + setIsImportOpen(false)} + onImported={(certificate) => { + setFeedback({ + tone: 'success', + message: `证书 ${certificate.name} 已导入。`, + }); + }} + /> + + setIsDetailOpen(false)} + onEdit={() => { + setIsDetailOpen(false); + setIsEditorOpen(true); + }} + onDelete={() => { + const certificate = certificates.find( + (item) => item.id === selectedCertificateId, + ); + if (certificate) { + setIsDetailOpen(false); + handleDeleteCertificate(certificate); + } + }} + deleting={deleteCertificateMutation.isPending} + /> + + setIsEditorOpen(false)} + onSaved={(certificate) => { + setFeedback({ + tone: 'success', + message: `证书 ${certificate.name} 已更新。`, + }); + }} + /> + + ); +} diff --git a/atsf_server/web/features/websites/components/websites-page.tsx b/atsf_server/web/features/websites/components/websites-page.tsx index 693062b4..d2dc30bb 100644 --- a/atsf_server/web/features/websites/components/websites-page.tsx +++ b/atsf_server/web/features/websites/components/websites-page.tsx @@ -10,32 +10,23 @@ 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 { getTlsCertificates } from '@/features/tls-certificates/api/tls-certificates'; 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'; @@ -49,12 +40,6 @@ export function WebsitesPage() { 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); @@ -79,20 +64,6 @@ export function WebsitesPage() { }, }); - 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], @@ -121,25 +92,6 @@ export function WebsitesPage() { 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 ( <>
@@ -147,17 +99,17 @@ export function WebsitesPage() { title="网站" description="主界面只保留网站列表卡片。新增网站时可直接绑定证书,也可以在弹窗内先添加证书后再应用。" action={ - <> - setIsCertificateListOpen(true)} +
+ 证书列表 - + 新增网站 - +
} /> @@ -291,138 +243,6 @@ export function WebsitesPage() { }); }} /> - - 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} 已更新。`, - }); - }} - /> ); }