'use client'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; import Link from 'next/link'; 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 { createTunnel, deleteTunnel, getTunnels, updateTunnel, rotateTunnelToken, } from '@/features/tunnels/api/tunnels'; import { TunnelEditorModal } from '@/features/tunnels/components/tunnel-editor-modal'; import { TunnelDeploymentModal } from '@/features/tunnels/components/tunnel-deployment-modal'; import type { TunnelItem, TunnelMutationPayload } from '@/features/tunnels/types'; import { PrimaryButton } from '@/features/shared/components/resource-primitives'; import { formatRelativeTime } from '@/lib/utils/date'; import { isMeaningfulTime } from '@/features/nodes/utils'; const tunnelsQueryKey = ['tunnels']; type FeedbackState = { tone: 'info' | 'success' | 'danger'; message: string; }; function getErrorMessage(error: unknown) { return error instanceof Error ? error.message : '请求失败,请稍后重试。'; } export function TunnelsPage() { const queryClient = useQueryClient(); const [feedback, setFeedback] = useState(null); const [editingTunnel, setEditingTunnel] = useState(null); const [isEditorOpen, setIsEditorOpen] = useState(false); const [deploymentTunnel, setDeploymentTunnel] = useState(null); const [isDeploymentOpen, setIsDeploymentOpen] = useState(false); const tunnelsQuery = useQuery({ queryKey: tunnelsQueryKey, queryFn: getTunnels, refetchInterval: 5000, }); const saveMutation = useMutation({ mutationFn: async (payload: TunnelMutationPayload) => { return editingTunnel ? updateTunnel(editingTunnel.id, payload) : createTunnel(payload); }, onSuccess: async () => { setFeedback({ tone: 'success', message: editingTunnel ? '隧道已更新。' : '隧道已创建。', }); setEditingTunnel(null); setIsEditorOpen(false); await queryClient.invalidateQueries({ queryKey: tunnelsQueryKey }); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const deleteMutation = useMutation({ mutationFn: deleteTunnel, onSuccess: async () => { setFeedback({ tone: 'success', message: '隧道已删除。' }); await queryClient.invalidateQueries({ queryKey: tunnelsQueryKey }); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const rotateTokenMutation = useMutation({ mutationFn: rotateTunnelToken, onSuccess: async () => { setFeedback({ tone: 'success', message: '隧道 Token 已轮转,客户端需要重新部署连接。' }); await queryClient.invalidateQueries({ queryKey: tunnelsQueryKey }); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const tunnels = useMemo(() => tunnelsQuery.data ?? [], [tunnelsQuery.data]); const handleResetEditor = () => { setFeedback(null); setEditingTunnel(null); setIsEditorOpen(false); }; const handleCreate = () => { setFeedback(null); setEditingTunnel(null); setIsEditorOpen(true); }; const handleEdit = (tunnel: TunnelItem) => { setFeedback(null); setEditingTunnel(tunnel); setIsEditorOpen(true); }; const handleDeployment = (tunnel: TunnelItem) => { setFeedback(null); setDeploymentTunnel(tunnel); setIsDeploymentOpen(true); }; const handleDelete = (tunnelId: number, tunnelName: string) => { if ( !window.confirm( `确认删除隧道“${tunnelName}”吗?删除后该隧道的连接将立即中断。`, ) ) { return; } setFeedback(null); deleteMutation.mutate(tunnelId); }; const handleRotateToken = (tunnelId: number, tunnelName: string) => { if ( !window.confirm( `确认重置隧道“${tunnelName}”的 Token 吗?已连接的客户端将被断开,并且需要使用新的 Token 重新部署。`, ) ) { return; } setFeedback(null); rotateTokenMutation.mutate(tunnelId); }; return ( <>
新建 Tunnel } /> {feedback ? ( ) : null} {tunnelsQuery.isLoading ? ( ) : tunnelsQuery.isError ? ( ) : tunnels.length === 0 ? ( ) : (
{tunnels.map((tunnel) => ( ))}
名称 / 隧道ID 状态 版本 / 校验 已连接 Relay 操作

{tunnel.name}

{tunnel.tunnel_id.slice(0, 8)}...

{tunnel.remark ? (

{tunnel.remark}

) : null}
{tunnel.status === 'online' && isMeaningfulTime(tunnel.last_seen_at) ? (

活跃:{formatRelativeTime(tunnel.last_seen_at)}

) : null}

版本:{tunnel.current_version || '未知'}

{tunnel.current_checksum ? `签名:${tunnel.current_checksum.slice(0, 12)}...` : '签名:未知'}

{tunnel.connected_relays && tunnel.connected_relays.length > 0 ? (
{tunnel.connected_relays.map((relay, index) => (

{relay}

))}
) : (

暂无

)}
)}
{ setFeedback(null); saveMutation.mutate(payload); }} /> setIsDeploymentOpen(false)} /> ); }