'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 { 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 { getConfigVersionDiff, publishConfigVersion, } from '@/features/config-versions/api/config-versions'; import { ProxyRouteCreateDrawer } from '@/features/proxy-routes/components/proxy-route-create-drawer'; import { getErrorMessage, getUpstreamSummary, getWebsiteStatusBadges, } from '@/features/proxy-routes/helpers'; import { deleteProxyRoute, getProxyRoutes, } from '@/features/proxy-routes/api/proxy-routes'; import type { ProxyRouteItem } from '@/features/proxy-routes/types'; import { DangerButton, PrimaryButton, ResourceInput, SecondaryButton, } from '@/features/shared/components/resource-primitives'; type FeedbackState = { tone: 'info' | 'success' | 'danger'; message: string; }; function hasConfigChanges(diff: { active_version?: string; added_sites: string[]; removed_sites: string[]; modified_sites: string[]; added_domains: string[]; removed_domains: string[]; modified_domains: string[]; main_config_changed: boolean; waf_config_changed?: boolean; changed_option_keys: string[]; }) { return ( diff.added_sites.length > 0 || diff.removed_sites.length > 0 || diff.modified_sites.length > 0 || diff.added_domains.length > 0 || diff.removed_domains.length > 0 || diff.modified_domains.length > 0 || diff.main_config_changed || Boolean(diff.waf_config_changed) || diff.changed_option_keys.length > 0 || !diff.active_version ); } export function ProxyRoutesPage() { const router = useRouter(); const queryClient = useQueryClient(); const [feedback, setFeedback] = useState(null); const [keyword, setKeyword] = useState(''); const [isCreateOpen, setIsCreateOpen] = useState(false); const routesQuery = useQuery({ queryKey: ['proxy-routes'], queryFn: getProxyRoutes, }); const diffQuery = useQuery({ queryKey: ['config-versions', 'diff'], queryFn: getConfigVersionDiff, }); const deleteMutation = useMutation({ mutationFn: deleteProxyRoute, onSuccess: async () => { setFeedback({ tone: 'success', message: '网站已删除。' }); await Promise.all([ queryClient.invalidateQueries({ queryKey: ['proxy-routes'] }), queryClient.invalidateQueries({ queryKey: ['config-versions', 'diff'], }), ]); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const publishMutation = useMutation({ mutationFn: publishConfigVersion, onSuccess: async (version) => { setFeedback({ tone: 'success', message: `配置已发布,版本号 ${version.version}。`, }); await Promise.all([ queryClient.invalidateQueries({ queryKey: ['config-versions'] }), queryClient.invalidateQueries({ queryKey: ['config-versions', 'diff'], }), ]); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const routes = useMemo(() => routesQuery.data ?? [], [routesQuery.data]); const filteredRoutes = useMemo(() => { const normalizedKeyword = keyword.trim().toLowerCase(); if (!normalizedKeyword) { return routes; } return routes.filter((route) => { const haystack = [ route.site_name, route.primary_domain, ...route.domains, ...route.upstream_list, route.remark, ] .join(' ') .toLowerCase(); return haystack.includes(normalizedKeyword); }); }, [keyword, routes]); const diff = diffQuery.data; const totalDomains = routes.reduce( (sum, route) => sum + route.domain_count, 0, ); const enabledCount = routes.filter((route) => route.enabled).length; const handleDelete = (route: ProxyRouteItem) => { if ( !window.confirm( `确认删除网站 ${route.site_name} 吗?\n这会删除该站点下的全部域名与配置。`, ) ) { return; } setFeedback(null); deleteMutation.mutate(route.id); }; const handlePublish = () => { if (!diff || !hasConfigChanges(diff)) { setFeedback({ tone: 'info', message: '当前草稿没有可发布的变更。' }); return; } const summary = [ `新增网站 ${diff.added_sites.length} 个`, `删除网站 ${diff.removed_sites.length} 个`, `修改网站 ${diff.modified_sites.length} 个`, `域名变更 ${diff.added_domains.length + diff.removed_domains.length + diff.modified_domains.length} 项`, ].join(','); if (!window.confirm(`确认发布当前配置吗?\n${summary}`)) { return; } setFeedback(null); publishMutation.mutate(false); }; if (routesQuery.isLoading) { return ; } if (routesQuery.isError) { return ( ); } return ( <>
{publishMutation.isPending ? '发布中...' : '发布配置'} { setFeedback(null); setIsCreateOpen(true); }} > 新建规则
} /> {feedback ? ( ) : null}

{routes.length}

已启用 {enabledCount} 个

{totalDomains}

每个网站第一行域名为主域名

{diff ? `网站变更 ${diff.added_sites.length + diff.removed_sites.length + diff.modified_sites.length} 项` : '正在读取 diff...'}

新增网站:{diff?.added_sites.length ?? 0}

删除网站:{diff?.removed_sites.length ?? 0}

修改网站:{diff?.modified_sites.length ?? 0}

setKeyword(event.target.value)} placeholder="搜索站点" />
{diff && hasConfigChanges(diff) ? (
{diff.modified_sites.length > 0 ? ( ) : null} {diff.added_sites.length > 0 ? ( ) : null} {diff.removed_sites.length > 0 ? ( ) : null}
) : null}
{filteredRoutes.length === 0 ? ( ) : (
{filteredRoutes.map((route) => (

{route.site_name}

{getWebsiteStatusBadges(route).map((badge) => ( ))}
配置 handleDelete(route)} disabled={deleteMutation.isPending} > 删除

域名列表

{route.domains.join(' / ')}

上游摘要

{getUpstreamSummary(route)}

{route.remark || null}

))}
)}
route.domains)} onCreated={async (route) => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ['proxy-routes'] }), queryClient.invalidateQueries({ queryKey: ['config-versions', 'diff'], }), ]); router.push(`/proxy-route/detail?id=${route.id}§ion=domains`); }} /> ); }