'use client'; import Link from 'next/link'; import {useRouter, useSearchParams} from 'next/navigation'; import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; import {useMemo, useState} from 'react'; import {ArrowLeft, Globe, Plus, Trash2} from 'lucide-react'; import {toast} from 'sonner'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import {Button} from '@/components/ui/button'; import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card'; import {EmptyStateWithBorder} from '@/components/layout/empty'; import {ErrorInline} from '@/components/layout/error'; import {LoadingStateWithBorder} from '@/components/layout/loading'; import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table'; import type {TlsCertificateItem} from '@/lib/services/openflare'; import {ProxyRouteService, TlsCertificateService, WebsiteService,} from '@/lib/services/openflare'; import {formatDateTime} from '@/lib/utils'; import {getUpstreamSummary} from '@/app/(main)/proxy-routes/components/helpers'; import {CertificateApplyDialog} from '../components/certificate-apply-dialog'; import {CertificateDetailDialog} from '../components/certificate-detail-dialog'; import {CertificateEditorDialog} from '../components/certificate-editor-dialog'; import {CertificateImportDialog} from '../components/certificate-import-dialog'; import {WebsiteStatusBadge} from '../components/status-badge'; import {WebsiteEditorDialog} from '../components/website-editor-dialog'; import { buildCertificateLabel, getCertificateStatus, getErrorMessage, getMatchTypeMeta, isRouteRelatedToManagedDomain, } from '../components/website-utils'; const domainsQueryKey = ['openflare', 'managed-domains']; const certificatesQueryKey = ['openflare', 'tls-certificates']; export function WebsiteDetailPageClient() { const router = useRouter(); const searchParams = useSearchParams(); const queryClient = useQueryClient(); const websiteId = searchParams.get('id')?.trim() ?? ''; const [editorOpen, setEditorOpen] = useState(false); const [importOpen, setImportOpen] = useState(false); const [detailOpen, setDetailOpen] = useState(false); const [certEditorOpen, setCertEditorOpen] = useState(false); const [deleteWebsiteOpen, setDeleteWebsiteOpen] = useState(false); const [deleteCertOpen, setDeleteCertOpen] = useState(false); const [preferredCertificateId, setPreferredCertificateId] = useState(null); const [convertCertificate, setConvertCertificate] = useState(null); const domainsQuery = useQuery({ queryKey: domainsQueryKey, queryFn: () => WebsiteService.list(), }); const certificatesQuery = useQuery({ queryKey: certificatesQueryKey, queryFn: () => TlsCertificateService.list(), }); const routesQuery = useQuery({ queryKey: ['openflare', 'proxy-routes'], queryFn: () => ProxyRouteService.list(), }); const website = useMemo( () => (domainsQuery.data ?? []).find((item) => String(item.id) === websiteId) ?? null, [domainsQuery.data, websiteId], ); const certificates = useMemo( () => certificatesQuery.data ?? [], [certificatesQuery.data], ); const certificateMap = useMemo( () => new Map(certificates.map((item) => [item.id, item])), [certificates], ); const certificate = website?.cert_id ? (certificateMap.get(website.cert_id) ?? null) : null; const relatedRoutes = useMemo(() => { if (!website) return []; return (routesQuery.data ?? []).filter((route) => isRouteRelatedToManagedDomain(website.domain, route), ); }, [routesQuery.data, website]); const deleteDomainMutation = useMutation({ mutationFn: (id: number) => WebsiteService.deleteById(id), onSuccess: async () => { await queryClient.invalidateQueries({queryKey: domainsQueryKey}); router.push('/websites'); }, onError: (error) => toast.error(getErrorMessage(error)), }); const deleteCertificateMutation = useMutation({ mutationFn: (id: number) => TlsCertificateService.deleteById(id), onSuccess: async () => { toast.success('证书已删除'); setDeleteCertOpen(false); await Promise.all([ queryClient.invalidateQueries({queryKey: certificatesQueryKey}), queryClient.invalidateQueries({queryKey: domainsQueryKey}), ]); }, onError: (error) => toast.error(getErrorMessage(error)), }); if (!websiteId) { return (
); } if ( domainsQuery.isLoading || certificatesQuery.isLoading || routesQuery.isLoading ) { return (
); } if (domainsQuery.isError) { return (
void domainsQuery.refetch()} className="justify-center" />
); } if (!website) { return (
); } const matchType = getMatchTypeMeta(website.domain); const certificateStatus = certificate ? getCertificateStatus(certificate) : null; const enabledRoutesCount = relatedRoutes.filter((route) => route.enabled).length; return (

{website.domain}

网站详情

匹配类型

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

网站状态

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

关联规则

规则总数:{relatedRoutes.length}

已启用规则:{enabledRoutesCount}

绑定证书

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

网站信息 当前网站的基础配置与托管信息。

域名

{website.domain}

创建时间

{formatDateTime(website.created_at)}

备注

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

证书信息 当前网站绑定的默认证书信息。若还没有证书,可直接添加后回填。 {certificate ? (
{certificateStatus ? ( ) : null}

证书名称:{buildCertificateLabel(certificate)}

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

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

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

) : ( )}
关联规则 命中当前网站域名的代理规则。 {relatedRoutes.length === 0 ? ( ) : ( 规则域名 源站 HTTPS 证书 状态 备注 {relatedRoutes.map((route) => { const routeCertificate = route.cert_id ? (certificateMap.get(route.cert_id) ?? null) : null; const matchedDomain = route.domains.find((d) => d === website.domain) ?? route.primary_domain; return (

{matchedDomain}

{getUpstreamSummary(route)} {routeCertificate ? routeCertificate.name : '未绑定证书'} {route.remark || '暂无备注'}
); })}
)}
setImportOpen(true)} onSaved={() => toast.success('网站已更新')} /> { setPreferredCertificateId(imported.id); toast.success(`证书 ${imported.name} 已导入,可在编辑网站时直接应用`); setEditorOpen(true); }} /> { setDetailOpen(false); setCertEditorOpen(true); }} onDelete={() => { setDetailOpen(false); setDeleteCertOpen(true); }} deleting={deleteCertificateMutation.isPending} /> toast.success(`证书 ${updated.name} 已更新`)} onConvert={(manualCertificate) => { setCertEditorOpen(false); setConvertCertificate(manualCertificate); }} /> {convertCertificate ? ( !open && setConvertCertificate(null)} mode="convert-upload" certificate={convertCertificate} onApplied={(converted) => { setConvertCertificate(null); toast.success(`证书 ${converted.name} 转换申请已提交`); }} /> ) : null} 确认删除网站 确认删除网站 {website.domain} 吗? 取消 deleteDomainMutation.mutate(website.id)} > 删除 确认删除证书 确认删除证书 {certificate?.name} 吗? 取消 certificate && deleteCertificateMutation.mutate(certificate.id)} > 删除
); }