From 139eacab89dc524e1797ae35cc36d11e8b06d154 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 13 Mar 2026 16:58:19 +0800 Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]=20=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/performance-page.tsx | 1 - .../shared/components/resource-primitives.tsx | 4 +- .../components/website-detail-page.tsx | 954 +++++++++--------- .../websites/components/websites-page.tsx | 4 +- 4 files changed, 469 insertions(+), 494 deletions(-) diff --git a/atsf_server/web/features/performance/components/performance-page.tsx b/atsf_server/web/features/performance/components/performance-page.tsx index c4ab17ed..67c25e5a 100644 --- a/atsf_server/web/features/performance/components/performance-page.tsx +++ b/atsf_server/web/features/performance/components/performance-page.tsx @@ -762,7 +762,6 @@ export function PerformancePage() { diff --git a/atsf_server/web/features/shared/components/resource-primitives.tsx b/atsf_server/web/features/shared/components/resource-primitives.tsx index 4d561b52..50851a75 100644 --- a/atsf_server/web/features/shared/components/resource-primitives.tsx +++ b/atsf_server/web/features/shared/components/resource-primitives.tsx @@ -180,13 +180,13 @@ export function ToggleField({ onChange={(event) => onChange(event.target.checked)} className="mt-1 h-4 w-4 rounded border-[var(--border-default)] accent-[var(--brand-primary)]" /> - + {label} {tooltip ? : null} {description ? ( - + {description} ) : null} diff --git a/atsf_server/web/features/websites/components/website-detail-page.tsx b/atsf_server/web/features/websites/components/website-detail-page.tsx index 7e4305e0..1b050533 100644 --- a/atsf_server/web/features/websites/components/website-detail-page.tsx +++ b/atsf_server/web/features/websites/components/website-detail-page.tsx @@ -1,519 +1,495 @@ 'use client'; import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { useMemo, useState } from 'react'; +import {useRouter} from 'next/navigation'; +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 {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 {deleteManagedDomain, getManagedDomains,} from '@/features/managed-domains/api/managed-domains'; +import {getProxyRoutes,} from '@/features/proxy-routes/api/proxy-routes'; +import {deleteTlsCertificate, getTlsCertificates,} from '@/features/tls-certificates/api/tls-certificates'; +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 { - deleteManagedDomain, - getManagedDomains, -} from '@/features/managed-domains/api/managed-domains'; -import { - getProxyRoutes, -} from '@/features/proxy-routes/api/proxy-routes'; -import { - deleteTlsCertificate, - getTlsCertificates, -} from '@/features/tls-certificates/api/tls-certificates'; -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, - isRouteRelatedToManagedDomain, + buildCertificateLabel, + getCertificateStatus, + getErrorMessage, + getMatchTypeMeta, + isRouteRelatedToManagedDomain, } from '@/features/websites/utils'; -import { - DangerButton, - PrimaryButton, - SecondaryButton, -} from '@/features/shared/components/resource-primitives'; -import { formatDateTime } from '@/lib/utils/date'; +import {DangerButton, PrimaryButton, SecondaryButton,} from '@/features/shared/components/resource-primitives'; +import {formatDateTime} from '@/lib/utils/date'; type FeedbackState = { - tone: 'success' | 'danger'; - message: string; + tone: 'success' | 'danger'; + message: string; }; -export function WebsiteDetailPage({ websiteId }: { websiteId: string }) { - const router = useRouter(); - const queryClient = useQueryClient(); - const [feedback, setFeedback] = useState(null); - const [isEditorOpen, setIsEditorOpen] = useState(false); - const [isCertificateImportOpen, setIsCertificateImportOpen] = useState(false); - const [isCertificateDetailOpen, setIsCertificateDetailOpen] = useState(false); - const [isCertificateEditorOpen, setIsCertificateEditorOpen] = useState(false); - const [preferredCertificateId, setPreferredCertificateId] = useState< - number | null - >(null); +export function WebsiteDetailPage({websiteId}: { websiteId: string }) { + const router = useRouter(); + const queryClient = useQueryClient(); + const [feedback, setFeedback] = useState(null); + const [isEditorOpen, setIsEditorOpen] = useState(false); + const [isCertificateImportOpen, setIsCertificateImportOpen] = useState(false); + 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 proxyRoutesQuery = useQuery({ - queryKey: ['proxy-routes'], - queryFn: getProxyRoutes, - }); + const managedDomainsQuery = useQuery({ + queryKey: ['managed-domains'], + queryFn: getManagedDomains, + }); + const certificatesQuery = useQuery({ + queryKey: ['tls-certificates', 'list'], + queryFn: getTlsCertificates, + }); + const proxyRoutesQuery = useQuery({ + queryKey: ['proxy-routes'], + queryFn: getProxyRoutes, + }); - const deleteDomainMutation = useMutation({ - mutationFn: deleteManagedDomain, - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ['managed-domains'] }); - router.push('/website'); - }, - onError: (error) => { - setFeedback({ tone: 'danger', message: getErrorMessage(error) }); - }, - }); + const deleteDomainMutation = useMutation({ + mutationFn: deleteManagedDomain, + onSuccess: async () => { + await queryClient.invalidateQueries({queryKey: ['managed-domains']}); + router.push('/website'); + }, + 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 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 website = useMemo(() => { - return ( - (managedDomainsQuery.data ?? []).find( - (item) => String(item.id) === websiteId, - ) ?? null + const website = useMemo(() => { + return ( + (managedDomainsQuery.data ?? []).find( + (item) => String(item.id) === websiteId, + ) ?? null + ); + }, [managedDomainsQuery.data, websiteId]); + + const certificates = useMemo( + () => certificatesQuery.data ?? [], + [certificatesQuery.data], ); - }, [managedDomainsQuery.data, websiteId]); + const certificateMap = useMemo( + () => new Map(certificates.map((item) => [item.id, item])), + [certificates], + ); + const relatedRoutes = useMemo(() => { + if (!website) { + return []; + } - const certificates = useMemo( - () => certificatesQuery.data ?? [], - [certificatesQuery.data], - ); - const certificateMap = useMemo( - () => new Map(certificates.map((item) => [item.id, item])), - [certificates], - ); - const relatedRoutes = useMemo(() => { - if (!website) { - return []; + return (proxyRoutesQuery.data ?? []).filter((route) => + isRouteRelatedToManagedDomain(website.domain, route.domain), + ); + }, [proxyRoutesQuery.data, website]); + + const certificate = website?.cert_id + ? certificateMap.get(website.cert_id) ?? null + : null; + const enabledRoutesCount = relatedRoutes.filter((route) => route.enabled).length; + + const handleDeleteWebsite = () => { + if (!website) { + return; + } + + if (!window.confirm(`确认删除网站 ${website.domain} 吗?`)) { + return; + } + + setFeedback(null); + deleteDomainMutation.mutate(website.id); + }; + + const handleDeleteCertificate = () => { + if (!certificate) { + return; + } + + if (!window.confirm(`确认删除证书 ${certificate.name} 吗?`)) { + return; + } + + setFeedback(null); + deleteCertificateMutation.mutate(certificate.id); + }; + + if ( + managedDomainsQuery.isLoading || + certificatesQuery.isLoading || + proxyRoutesQuery.isLoading + ) { + return ; } - return (proxyRoutesQuery.data ?? []).filter((route) => - isRouteRelatedToManagedDomain(website.domain, route.domain), - ); - }, [proxyRoutesQuery.data, website]); - - const certificate = website?.cert_id - ? certificateMap.get(website.cert_id) ?? null - : null; - const enabledRoutesCount = relatedRoutes.filter((route) => route.enabled).length; - - const handleDeleteWebsite = () => { - if (!website) { - return; - } - - if (!window.confirm(`确认删除网站 ${website.domain} 吗?`)) { - return; - } - - setFeedback(null); - deleteDomainMutation.mutate(website.id); - }; - - const handleDeleteCertificate = () => { - if (!certificate) { - return; - } - - if (!window.confirm(`确认删除证书 ${certificate.name} 吗?`)) { - return; - } - - setFeedback(null); - deleteCertificateMutation.mutate(certificate.id); - }; - - if ( - managedDomainsQuery.isLoading || - certificatesQuery.isLoading || - proxyRoutesQuery.isLoading - ) { - return ; - } - - if (managedDomainsQuery.isError) { - return ( - - ); - } - - if (certificatesQuery.isError) { - return ( - - ); - } - - if (proxyRoutesQuery.isError) { - return ( - - ); - } - - if (!website) { - return ( - - ); - } - - const matchType = getMatchTypeMeta(website.domain); - const certificateStatus = certificate ? getCertificateStatus(certificate) : null; - - return ( - <> -
- - - 返回 - - setIsEditorOpen(true)} - > - 编辑网站 - - setIsCertificateImportOpen(true)} - > - 添加证书 - - - 删除网站 - - - } - /> - - {feedback ? ( - - ) : null} - -
- -
- -

- {website.domain.startsWith('*.') - ? '当前网站会覆盖该后缀下的子域名规则。' - : '当前网站只匹配同名精确域名。'} -

-
-
- - -
- -

- 更新时间:{formatDateTime(website.updated_at)} -

-
-
- - -
-

规则总数:{relatedRoutes.length}

-

已启用规则:{enabledRoutesCount}

-
-
- - -
- -

- {certificateStatus - ? `证书状态:${certificateStatus.label}` - : '当前网站未设置默认证书。'} -

-
-
-
- -
- -
-
-

- 域名 -

-

- {website.domain} -

-
-
-

- 创建时间 -

-

- {formatDateTime(website.created_at)} -

-
-
-

- 备注 -

-

- {website.remark || '暂无备注'} -

-
-
-
- - setIsCertificateImportOpen(true)} - > - 添加 - - } - > - {certificate ? ( -
-
- -
-
-

证书名称:{buildCertificateLabel(certificate)}

-

生效时间:{formatDateTime(certificate.not_before)}

-

到期时间:{formatDateTime(certificate.not_after)}

-

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

-
-
- setIsCertificateDetailOpen(true)} - > - 查看 - - setIsCertificateEditorOpen(true)} - > - 编辑 - -
- - 删除 - -
- ) : ( - - )} -
-
- - - {relatedRoutes.length === 0 ? ( - - ) : ( -
- - - - - - - - - - - - - {relatedRoutes.map((route) => { - const routeCertificate = route.cert_id - ? certificateMap.get(route.cert_id) ?? null - : null; + ); + } - return ( - - - - - - - - - ); - })} - -
规则域名源站HTTPS证书状态备注
-
-

{route.domain}

+ if (certificatesQuery.isError) { + return ( + + ); + } + + if (proxyRoutesQuery.isError) { + return ( + + ); + } + + if (!website) { + return ( + + ); + } + + const matchType = getMatchTypeMeta(website.domain); + const certificateStatus = certificate ? getCertificateStatus(certificate) : null; + + return ( + <> +
+ + + 返回 + + setIsEditorOpen(true)} + > + 编辑网站 + + setIsCertificateImportOpen(true)} + > + 添加证书 + + + 删除网站 + + + } + /> + + {feedback ? ( + + ) : null} + +
+ +
+ +

+ {website.domain.startsWith('*.') + ? '当前网站会覆盖该后缀下的子域名规则。' + : '当前网站只匹配同名精确域名。'} +

+
+
+ + +
-
-
-

{route.origin_url}

-
-
+

+ 更新时间:{formatDateTime(website.updated_at)} +

+
+ + + +
+

规则总数:{relatedRoutes.length}

+

已启用规则:{enabledRoutesCount}

+
+
+ + +
- {route.redirect_http ? ( - - ) : null} -
-
- {routeCertificate - ? routeCertificate.name - : '未绑定证书'} - - - - {route.remark || '暂无备注'} -
+

+ {certificateStatus + ? `证书状态:${certificateStatus.label}` + : '当前网站未设置默认证书。'} +

+
+
+
+ +
+ +
+
+

+ 域名 +

+

+ {website.domain} +

+
+
+

+ 创建时间 +

+

+ {formatDateTime(website.created_at)} +

+
+
+

+ 备注 +

+

+ {website.remark || '暂无备注'} +

+
+
+
+ + {certificate ? ( +
+
+ +
+
+

证书名称:{buildCertificateLabel(certificate)}

+

生效时间:{formatDateTime(certificate.not_before)}

+

到期时间:{formatDateTime(certificate.not_after)}

+

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

+
+
+ setIsCertificateDetailOpen(true)} + > + 查看 + + + 删除 + +
+
+ ) : ( + + )} +
+
+ + + {relatedRoutes.length === 0 ? ( + + ) : ( +
+ + + + + + + + + + + + + {relatedRoutes.map((route) => { + const routeCertificate = route.cert_id + ? certificateMap.get(route.cert_id) ?? null + : null; + + return ( + + + + + + + + + ); + })} + +
规则域名源站HTTPS证书状态备注
+
+

{route.domain}

+ +
+
+

{route.origin_url}

+
+
+ + {route.redirect_http ? ( + + ) : null} +
+
+ {routeCertificate + ? routeCertificate.name + : '未绑定证书'} + + + + {route.remark || '暂无备注'} +
+
+ )} +
- )} - - - { - setIsEditorOpen(false); - setPreferredCertificateId(null); - }} - initialDomain={website} - certificates={certificates} - certificatesLoading={certificatesQuery.isLoading} - preferredCertificateId={preferredCertificateId} - onRequestImportCertificate={() => setIsCertificateImportOpen(true)} - onSaved={() => { - setPreferredCertificateId(null); - setFeedback({ tone: 'success', message: '网站已更新。' }); - }} - /> + { + setIsEditorOpen(false); + setPreferredCertificateId(null); + }} + initialDomain={website} + certificates={certificates} + certificatesLoading={certificatesQuery.isLoading} + preferredCertificateId={preferredCertificateId} + onRequestImportCertificate={() => setIsCertificateImportOpen(true)} + onSaved={() => { + setPreferredCertificateId(null); + setFeedback({tone: 'success', message: '网站已更新。'}); + }} + /> - setIsCertificateImportOpen(false)} - onImported={(importedCertificate) => { - setPreferredCertificateId(importedCertificate.id); - setFeedback({ - tone: 'success', - message: `证书 ${importedCertificate.name} 已导入,可在编辑网站时直接应用。`, - }); - setIsEditorOpen(true); - }} - /> + setIsCertificateImportOpen(false)} + onImported={(importedCertificate) => { + setPreferredCertificateId(importedCertificate.id); + setFeedback({ + tone: 'success', + message: `证书 ${importedCertificate.name} 已导入,可在编辑网站时直接应用。`, + }); + setIsEditorOpen(true); + }} + /> - setIsCertificateDetailOpen(false)} - onEdit={() => { - setIsCertificateDetailOpen(false); - setIsCertificateEditorOpen(true); - }} - onDelete={() => { - setIsCertificateDetailOpen(false); - handleDeleteCertificate(); - }} - deleting={deleteCertificateMutation.isPending} - /> + setIsCertificateDetailOpen(false)} + onEdit={() => { + setIsCertificateDetailOpen(false); + setIsCertificateEditorOpen(true); + }} + onDelete={() => { + setIsCertificateDetailOpen(false); + handleDeleteCertificate(); + }} + deleting={deleteCertificateMutation.isPending} + /> - setIsCertificateEditorOpen(false)} - onSaved={(updatedCertificate) => { - setFeedback({ - tone: 'success', - message: `证书 ${updatedCertificate.name} 已更新。`, - }); - }} - /> - - ); + setIsCertificateEditorOpen(false)} + onSaved={(updatedCertificate) => { + setFeedback({ + tone: 'success', + message: `证书 ${updatedCertificate.name} 已更新。`, + }); + }} + /> + + ); } diff --git a/atsf_server/web/features/websites/components/websites-page.tsx b/atsf_server/web/features/websites/components/websites-page.tsx index ac0fbb8e..32b6ebeb 100644 --- a/atsf_server/web/features/websites/components/websites-page.tsx +++ b/atsf_server/web/features/websites/components/websites-page.tsx @@ -119,7 +119,7 @@ export function WebsitesPage() { {managedDomainsQuery.isLoading ? ( @@ -181,7 +181,7 @@ export function WebsitesPage() { -
+