From 2b8636300755dec1633bf468382430ca975f0d2a Mon Sep 17 00:00:00 2001 From: Awuqing <3184394176@qq.com> Date: Sun, 19 Apr 2026 16:48:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD:=20NodesPage=20=E9=9B=86?= =?UTF-8?q?=E6=88=90=E4=B8=80=E9=94=AE=E9=83=A8=E7=BD=B2=20Wizard=20?= =?UTF-8?q?=E4=B8=8E=E6=93=8D=E4=BD=9C=E5=88=97=E6=94=B9=E9=80=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/pages/nodes/NodesPage.tsx | 229 ++++++++++++------------------ 1 file changed, 87 insertions(+), 142 deletions(-) diff --git a/web/src/pages/nodes/NodesPage.tsx b/web/src/pages/nodes/NodesPage.tsx index 44e6688..0b93cf1 100644 --- a/web/src/pages/nodes/NodesPage.tsx +++ b/web/src/pages/nodes/NodesPage.tsx @@ -1,23 +1,26 @@ import React, { useEffect, useState, useCallback } from 'react' import { - Table, Button, Space, Tag, Typography, PageHeader, Modal, Input, Message, Badge, Popconfirm, Card, Descriptions, Empty + Table, Button, Space, Tag, Typography, PageHeader, Modal, Input, Message, Badge, Popconfirm, Card, + Empty, Dropdown, Menu, } from '@arco-design/web-react' import { - IconPlus, IconDelete, IconDesktop, IconCloudDownload, IconEdit + IconPlus, IconDelete, IconDesktop, IconCloudDownload, IconEdit, IconMore, } from '@arco-design/web-react/icon' import type { NodeSummary } from '../../types/nodes' -import { listNodes, createNode, deleteNode, updateNode } from '../../services/nodes' +import { listNodes, deleteNode, updateNode, rotateNodeToken } from '../../services/nodes' +import { fetchSystemInfo } from '../../services/system' +import { AgentInstallWizard } from './AgentInstallWizard' -const { Title, Text } = Typography +const { Text } = Typography export default function NodesPage() { const [nodes, setNodes] = useState([]) const [loading, setLoading] = useState(false) - const [createVisible, setCreateVisible] = useState(false) - const [newNodeName, setNewNodeName] = useState('') - const [newToken, setNewToken] = useState('') - // 编辑状态 + const [wizardVisible, setWizardVisible] = useState(false) + const [wizardFixedNode, setWizardFixedNode] = useState<{ id: number; name: string } | undefined>() + const [masterVersion, setMasterVersion] = useState('latest') + const [editVisible, setEditVisible] = useState(false) const [editNode, setEditNode] = useState(null) const [editName, setEditName] = useState('') @@ -34,22 +37,13 @@ export default function NodesPage() { } }, []) - useEffect(() => { fetchNodes() }, [fetchNodes]) - - const handleCreate = async () => { - if (!newNodeName.trim()) { - Message.warning('请输入节点名称') - return - } - try { - const result = await createNode(newNodeName.trim()) - setNewToken(result.token) - Message.success('节点创建成功') - fetchNodes() - } catch { - Message.error('创建节点失败') - } - } + useEffect(() => { + fetchNodes() + // 取 Master 版本号作为 Wizard agentVersion 默认值 + fetchSystemInfo().then((info) => { + if (info?.version) setMasterVersion(info.version) + }).catch(() => {}) + }, [fetchNodes]) const handleDelete = async (id: number) => { try { @@ -76,10 +70,30 @@ export default function NodesPage() { } } + const handleRotate = async (record: NodeSummary) => { + try { + const { newToken } = await rotateNodeToken(record.id) + Modal.success({ + title: 'Token 已轮换', + content: ( +
+ + 新 Token(24 小时内新旧 Token 均可认证,便于滚动替换): + + + {newToken} + +
+ ), + }) + } catch { + Message.error('轮换 Token 失败') + } + } + const columns = [ { - title: '节点名称', - dataIndex: 'name', + title: '节点名称', dataIndex: 'name', render: (name: string, record: NodeSummary) => ( {record.isLocal ? : } @@ -89,60 +103,48 @@ export default function NodesPage() { ), }, { - title: '状态', - dataIndex: 'status', - width: 100, - render: (status: string) => { - if (status === 'online') return - return - }, + title: '状态', dataIndex: 'status', width: 100, + render: (status: string) => status === 'online' + ? + : , }, + { title: '主机名', dataIndex: 'hostname', render: (v: string) => v || '-' }, + { title: 'IP 地址', dataIndex: 'ipAddress', render: (v: string) => v || '-' }, { - title: '主机名', - dataIndex: 'hostname', - render: (v: string) => v || '-', + title: '系统', dataIndex: 'os', width: 120, + render: (_: string, record: NodeSummary) => record.os + ? {record.os}/{record.arch} : '-', }, + { title: 'Agent 版本', dataIndex: 'agentVersion', width: 100, render: (v: string) => v || '-' }, { - title: 'IP 地址', - dataIndex: 'ipAddress', - render: (v: string) => v || '-', - }, - { - title: '系统', - dataIndex: 'os', - width: 120, - render: (_: string, record: NodeSummary) => { - if (!record.os) return '-' - return {record.os}/{record.arch} - }, - }, - { - title: 'Agent 版本', - dataIndex: 'agentVersion', - width: 100, - render: (v: string) => v || '-', - }, - { - title: '最后活跃', - dataIndex: 'lastSeen', - width: 170, + title: '最后活跃', dataIndex: 'lastSeen', width: 170, render: (v: string) => v ? new Date(v).toLocaleString('zh-CN') : '-', }, { - title: '操作', - width: 120, + title: '操作', width: 180, render: (_: unknown, record: NodeSummary) => ( - } /> - } - /> +
} /> - {/* 添加节点弹窗 */} - setCreateVisible(false)} - style={{ width: 640 }} - footer={newToken ? ( - - ) : undefined} - onOk={handleCreate} - okText="创建" - > - {!newToken ? ( - - ) : ( -
- {newToken} }, - ]} /> -
- - 令牌仅显示一次,请妥善保存。将其配置到远程服务器后,Agent 会自动连接 Master。 - -
-
- Agent 部署步骤 -
    -
  1. 把 BackupX 二进制上传到目标服务器(与 Master 同一个可执行文件)
  2. -
  3. 通过以下命令启动 Agent(替换 MASTER_URL):
  4. -
-
- - {`backupx agent --master ${window.location.origin} --token ${newToken}`} - -
- - 或使用配置文件 / 环境变量:
- BACKUPX_AGENT_MASTER={window.location.origin}
- BACKUPX_AGENT_TOKEN={newToken} -
-
-
- )} -
+ setWizardVisible(false)} + onSuccess={fetchNodes} + masterVersion={masterVersion} + fixedNode={wizardFixedNode} + /> - {/* 编辑节点弹窗 */} - setEditVisible(false)} - onOk={handleEdit} - okText="保存" - cancelText="取消" - > + setEditVisible(false)} onOk={handleEdit} + okText="保存" cancelText="取消">
节点名称
- +
)