'use client'; import Link from 'next/link'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMemo, useState } from 'react'; import { useForm, useWatch } from 'react-hook-form'; import { z } from 'zod'; 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 { AppModal } from '@/components/ui/app-modal'; import { AppCard } from '@/components/ui/app-card'; import { StatusBadge } from '@/components/ui/status-badge'; import { createNode, deleteNode, getNodes, updateNode, } from '@/features/nodes/api/nodes'; import type { NodeMutationPayload } from '@/features/nodes/types'; import { DangerButton, PrimaryButton, ResourceField, ResourceInput, SecondaryButton, ToggleField, } from '@/features/shared/components/resource-primitives'; import { formatDateTime, formatRelativeTime } from '@/lib/utils/date'; import { getApplyLabel, getApplyVariant, getNodeStatusLabel, getNodeStatusVariant, isMeaningfulTime, } from '@/features/nodes/utils'; const nodesQueryKey = ['nodes']; const nodeSchema = z.object({ name: z .string() .trim() .min(1, '请输入节点名') .max(128, '节点名不能超过 128 个字符'), auto_update_enabled: z.boolean(), }); type NodeFormValues = z.infer; type FeedbackState = { tone: 'info' | 'success' | 'danger'; message: string; }; const defaultValues: NodeFormValues = { name: '', auto_update_enabled: false, }; function getErrorMessage(error: unknown) { return error instanceof Error ? error.message : '请求失败,请稍后重试。'; } function toPayload(values: NodeFormValues): NodeMutationPayload { return { name: values.name.trim(), auto_update_enabled: values.auto_update_enabled, }; } export function NodesPage() { const queryClient = useQueryClient(); const [feedback, setFeedback] = useState(null); const [editingNodeId, setEditingNodeId] = useState(null); const [isEditorOpen, setIsEditorOpen] = useState(false); const form = useForm({ resolver: zodResolver(nodeSchema), defaultValues, }); const watchedAutoUpdate = useWatch({ control: form.control, name: 'auto_update_enabled', }); const nodesQuery = useQuery({ queryKey: nodesQueryKey, queryFn: getNodes, refetchInterval: 5000, }); const saveMutation = useMutation({ mutationFn: async (values: NodeFormValues) => { const payload = toPayload(values); return editingNodeId ? updateNode(editingNodeId, payload) : createNode(payload); }, onSuccess: async () => { setFeedback({ tone: 'success', message: editingNodeId ? '节点已更新。' : '节点已创建。', }); setEditingNodeId(null); setIsEditorOpen(false); form.reset(defaultValues); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const deleteMutation = useMutation({ mutationFn: deleteNode, onSuccess: async () => { setFeedback({ tone: 'success', message: '节点已删除。' }); setEditingNodeId(null); form.reset(defaultValues); await queryClient.invalidateQueries({ queryKey: nodesQueryKey }); }, onError: (error) => { setFeedback({ tone: 'danger', message: getErrorMessage(error) }); }, }); const nodes = useMemo(() => nodesQuery.data ?? [], [nodesQuery.data]); const handleReset = () => { setFeedback(null); setEditingNodeId(null); setIsEditorOpen(false); form.reset(defaultValues); }; const handleCreate = () => { setFeedback(null); setEditingNodeId(null); form.reset(defaultValues); setIsEditorOpen(true); }; const handleEdit = ( nodeId: number, name: string, autoUpdateEnabled: boolean, ) => { setFeedback(null); setEditingNodeId(nodeId); form.reset({ name, auto_update_enabled: autoUpdateEnabled, }); setIsEditorOpen(true); }; const handleDelete = (nodeId: number, nodeName: string) => { if ( !window.confirm( `确认删除节点“${nodeName}”吗?删除后该节点需要重新创建并重新接入。`, ) ) { return; } setFeedback(null); deleteMutation.mutate(nodeId); }; const handleSubmit = form.handleSubmit((values) => { setFeedback(null); saveMutation.mutate(values); }); return ( <>
新增节点 应用记录 } /> {feedback ? ( ) : null} void queryClient.invalidateQueries({ queryKey: nodesQueryKey }) } > 立即刷新 } > {nodesQuery.isLoading ? ( ) : nodesQuery.isError ? ( ) : nodes.length === 0 ? ( ) : (
{nodes.map((node) => ( ))}
节点 状态 Agent / Nginx 当前版本 最近应用 最近心跳 操作

{node.name}

IP:{node.ip || 'null'}

{node.agent_version || 'unknown'} /{' '} {node.nginx_version || 'unknown'} {node.current_version || '未应用'}
{isMeaningfulTime(node.last_seen_at) ? `${formatRelativeTime( node.last_seen_at, )} · ${formatDateTime(node.last_seen_at)}` : '暂无'}
详情 handleEdit( node.id, node.name, node.auto_update_enabled, ) } className="px-3 py-2 text-xs" > 编辑 handleDelete(node.id, node.name)} disabled={deleteMutation.isPending} className="px-3 py-2 text-xs" > 删除
)}
取消 {saveMutation.isPending ? '保存中...' : editingNodeId ? '保存修改' : '新增节点'} } >
form.setValue('auto_update_enabled', checked, { shouldDirty: true, shouldValidate: true, }) } />
); }