'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 { deleteManagedDomain, getManagedDomains, } from '@/features/managed-domains/api/managed-domains'; import type { ManagedDomainItem } from '@/features/managed-domains/types'; 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, getErrorMessage, getMatchTypeMeta, } from '@/features/websites/utils'; import { DangerButton, PrimaryButton, } from '@/features/shared/components/resource-primitives'; 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 [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 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); }; return ( <>
证书列表 新增网站
} /> {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) : '未绑定证书'}

详情 handleDeleteDomain(domain)} disabled={deleteDomainMutation.isPending} > 删除
); })}
)}
{isWebsiteModalOpen ? ( { setIsWebsiteModalOpen(false); setPreferredCertificateId(null); }} certificates={certificates} certificatesLoading={certificatesQuery.isLoading} preferredCertificateId={preferredCertificateId} onRequestImportCertificate={() => setIsCertificateImportOpen(true)} onSaved={(_, mode) => { setPreferredCertificateId(null); setFeedback({ tone: 'success', message: mode === 'create' ? '网站已创建。' : '网站已更新。', }); }} /> ) : null} {isCertificateImportOpen ? ( setIsCertificateImportOpen(false)} onImported={(certificate) => { setPreferredCertificateId(certificate.id); setFeedback({ tone: 'success', message: `证书 ${certificate.name} 已导入,可直接用于当前网站。`, }); }} /> ) : null} ); }