'use client'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect, 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 { 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 { getWAFSiteRuleGroups, replaceWAFSiteRuleGroups, } from '@/features/waf/api/waf'; 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 { WebsiteEditorModal } from '@/features/websites/components/website-editor-modal'; import type { TlsCertificateItem } from '@/features/tls-certificates/types'; import { buildCertificateLabel, getCertificateStatus, getErrorMessage, getMatchTypeMeta, isRouteRelatedToManagedDomain, } from '@/features/websites/utils'; import { DangerButton, PrimaryButton, SecondaryButton, ToggleField, } from '@/features/shared/components/resource-primitives'; import { formatDateTime } from '@/lib/utils/date'; type FeedbackState = { 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 [wafSelectedIDs, setWafSelectedIDs] = useState([]); const [convertCertificate, setConvertCertificate] = useState(null); 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 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 website = useMemo(() => { return ( (managedDomainsQuery.data ?? []).find( (item) => String(item.id) === websiteId, ) ?? null ); }, [managedDomainsQuery.data, websiteId]); 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 wafRouteID = relatedRoutes[0]?.id ?? null; const wafQuery = useQuery({ queryKey: ['waf', 'site-rule-groups', wafRouteID], queryFn: () => getWAFSiteRuleGroups(wafRouteID ?? 0), enabled: Boolean(wafRouteID), }); const wafMutation = useMutation({ mutationFn: (ids: number[]) => replaceWAFSiteRuleGroups(wafRouteID ?? 0, ids), onSuccess: async (view) => { setWafSelectedIDs(view.applied_ids); setFeedback({ tone: 'success', message: '网站 WAF 规则组已更新。' }); await Promise.all([ queryClient.invalidateQueries({ queryKey: ['waf'] }), queryClient.invalidateQueries({ queryKey: ['config-versions', 'diff'], }), ]); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); useEffect(() => { if (wafQuery.data) { setWafSelectedIDs(wafQuery.data.applied_ids); } }, [wafQuery.data]); 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 || '暂无备注'}

{certificate ? (

证书名称:{buildCertificateLabel(certificate)}

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

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

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

setIsCertificateDetailOpen(true)} > 查看 删除
) : ( )}
wafMutation.mutate(wafSelectedIDs)} > {wafMutation.isPending ? '保存中...' : '保存 WAF'} ) : null } > {!wafRouteID ? ( ) : wafQuery.isLoading ? ( ) : wafQuery.isError ? ( ) : (
{wafQuery.data?.global_rule_group ? (

{wafQuery.data.global_rule_group.name}

全局规则组默认应用到所有网站,不能在单站关闭。

) : null}
{(wafQuery.data?.rule_groups ?? []).map((group) => ( { setWafSelectedIDs((current) => checked ? [...current, group.id].sort( (left, right) => left - right, ) : current.filter((id) => id !== group.id), ); }} /> ))}
{(wafQuery.data?.rule_groups ?? []).length === 0 ? (

暂无自定义规则组,可在 WAF 页面创建后再绑定。

) : null}
)}
{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.origin_host || '$host'}

{route.redirect_http ? ( ) : null}
{routeCertificate ? routeCertificate.name : '未绑定证书'} {route.remark || '暂无备注'}
)}
{isEditorOpen ? ( { setIsEditorOpen(false); setPreferredCertificateId(null); }} initialDomain={website} certificates={certificates} certificatesLoading={certificatesQuery.isLoading} preferredCertificateId={preferredCertificateId} onRequestImportCertificate={() => setIsCertificateImportOpen(true)} onSaved={() => { setPreferredCertificateId(null); setFeedback({ tone: 'success', message: '网站已更新。' }); }} /> ) : null} {isCertificateImportOpen ? ( setIsCertificateImportOpen(false)} onImported={(importedCertificate) => { setPreferredCertificateId(importedCertificate.id); setFeedback({ tone: 'success', message: `证书 ${importedCertificate.name} 已导入,可在编辑网站时直接应用。`, }); setIsEditorOpen(true); }} /> ) : null} {isCertificateDetailOpen ? ( setIsCertificateDetailOpen(false)} onEdit={() => { setIsCertificateDetailOpen(false); setIsCertificateEditorOpen(true); }} onDelete={() => { setIsCertificateDetailOpen(false); handleDeleteCertificate(); }} deleting={deleteCertificateMutation.isPending} /> ) : null} {isCertificateEditorOpen ? ( setIsCertificateEditorOpen(false)} onSaved={(updatedCertificate) => { setFeedback({ tone: 'success', message: `证书 ${updatedCertificate.name} 已更新。`, }); }} onConvert={(manualCertificate) => { setIsCertificateEditorOpen(false); setConvertCertificate(manualCertificate); }} /> ) : null} {convertCertificate ? ( setConvertCertificate(null)} mode="convert-upload" certificate={convertCertificate} onApplied={(convertedCertificate) => { setFeedback({ tone: 'success', message: `证书 ${convertedCertificate.name} 转换申请已提交。`, }); }} /> ) : null} ); }