From c00ead9aa0adbf1b4a22e4a6ed1da766d7a63202 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 19 Jul 2026 15:01:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(nodes):=20=E8=B0=83=E6=95=B4=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E8=AF=A6=E6=83=85=20Tab=20=E5=B9=B6=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=BE=B9=E7=BC=98=E9=83=A8=E7=BD=B2=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将数据看板并入概览,运行状态与配置合并为状态与部署; 边缘节点支持自动填充 Server URL 与 Agent Token 的 Docker 部署卡片。 --- docs/changelog/index.md | 8 ++-- .../nodes/components/edge-node-detail.tsx | 12 +++--- .../nodes/components/install-command.tsx | 34 +++++++++++---- .../nodes/components/node-detail-shell.tsx | 41 ++++++++++--------- .../app/(main)/nodes/components/node-utils.ts | 19 +++++++++ .../nodes/components/relay-node-detail.tsx | 9 +--- .../nodes/components/tunnel-node-detail.tsx | 13 ++---- 7 files changed, 81 insertions(+), 55 deletions(-) diff --git a/docs/changelog/index.md b/docs/changelog/index.md index 7304b1f4..4ec2e39e 100644 --- a/docs/changelog/index.md +++ b/docs/changelog/index.md @@ -11,11 +11,12 @@ sidebar: false ## 重大变更 > [!IMPORTANT] -> +> > 3.1.2 版本更新了 CLickHouse 部署配置。 -> +> > 3.0.0 版本为 Wavelet 平台迁移与架构重构版本,涉及数据库表结构、环境变量以及前后端底层架构的重大变更。请务必在升级前备份数据库,并且更新到 V2.3.4。 > 目前已知的兼容性问题: +> > - Pages 无法迁移, 升级前请先手动下载并备份 Pages 静态站点的 ZIP 包,升级后重新创建。 > - 性能调优参数重置, 升级后请重新配置 @@ -28,6 +29,7 @@ sidebar: false ### 改进 +- 节点详情页 Tab 调整为「概览」与「状态与部署」:原数据看板并入概览;运行状态与配置信息并入状态与部署;边缘节点新增可自动填充 Server URL 与 Agent Token 的 Docker 部署命令卡片。 - WAF 规则编辑器支持为节点自定义显示名称,并从节点库拖放到画布指定位置添加节点。 - WAF 规则画布支持右键删除节点或连线,并屏蔽浏览器默认右键菜单。 - WAF 规则编辑器支持一键格式化布局,按流程层次自动整理节点位置。 @@ -90,7 +92,6 @@ sidebar: false - 移除了 WAF 旧版固定名单与人机验证配置;升级后请在发布前使用新的可视化规则重新编排防护策略。 - ## [v3.2.0] - 2026-07-12 ### 新增 @@ -177,7 +178,6 @@ sidebar: false - 网站管理拆分为域名、路由、静态托管、WAF 和缓存等独立能力,更适合维护复杂站点配置。 - Tunnel 节点统一纳入节点管理,配置发布和运行状态查看更加一致。 - ## [v2.3.4] - 2026-06-17 ### 变更 diff --git a/frontend/app/(main)/nodes/components/edge-node-detail.tsx b/frontend/app/(main)/nodes/components/edge-node-detail.tsx index ce24e72b..436c729e 100644 --- a/frontend/app/(main)/nodes/components/edge-node-detail.tsx +++ b/frontend/app/(main)/nodes/components/edge-node-detail.tsx @@ -37,6 +37,7 @@ import type { import { NodeService } from '@/lib/services/openflare'; import { AgentUpdateDialog } from './agent-update-dialog'; +import { InstallCommand } from './install-command'; import { NodeDetailShell } from './node-detail-shell'; import { NodeErrorBanner, @@ -199,7 +200,7 @@ export function EdgeNodeDetail({ node }: { node: NodeItem }) { ); - const overviewTab = ( + const manageTab = (
{node.last_error ? : null} @@ -277,11 +278,9 @@ export function EdgeNodeDetail({ node }: { node: NodeItem }) {
- - ); - const manageTab = ( -
+ + = { + edge: { + title: '边缘节点部署', + description: '使用 Agent Token 将边缘节点接入控制端。', + tokenLabel: 'Agent Token', + dockerLabel: 'Docker 容器部署', + }, relay: { title: '中继部署命令', description: '使用 Discovery Token 将 frps 中继节点接入控制端。', @@ -79,7 +86,7 @@ export function InstallCommand({ const normalizedServerUrl = getServerUrl(serverUrl); const scriptCommand = useMemo(() => { - if (!normalizedServerUrl || !node.access_token) { + if (!normalizedServerUrl || !node.access_token || variant === 'edge') { return ''; } return variant === 'relay' @@ -91,6 +98,13 @@ export function InstallCommand({ if (!normalizedServerUrl || !node.access_token) { return ''; } + if (variant === 'edge') { + return buildEdgeDockerInstallCommand( + normalizedServerUrl, + node.access_token, + serverVersion, + ); + } return variant === 'relay' ? buildRelayDockerInstallCommand( normalizedServerUrl, @@ -190,12 +204,14 @@ export function InstallCommand({

) : ( <> -
-

{meta.scriptLabel}

-
-                {scriptCommand}
-              
-
+ {scriptCommand && meta.scriptLabel ? ( +
+

{meta.scriptLabel}

+
+                  {scriptCommand}
+                
+
+ ) : null}

{meta.dockerLabel}

diff --git a/frontend/app/(main)/nodes/components/node-detail-shell.tsx b/frontend/app/(main)/nodes/components/node-detail-shell.tsx
index 0e278d1c..8e0849ca 100644
--- a/frontend/app/(main)/nodes/components/node-detail-shell.tsx
+++ b/frontend/app/(main)/nodes/components/node-detail-shell.tsx
@@ -14,7 +14,7 @@ import { NodeKpiCard } from './node-detail-primitives';
 import { NodeStatusBadge } from './node-status-badge';
 import type { StatusTone } from './node-utils';
 
-export type NodeDetailTabId = 'overview' | 'dashboard' | 'manage';
+export type NodeDetailTabId = 'overview' | 'manage';
 
 export type NodeDetailTabConfig = {
   id: NodeDetailTabId;
@@ -33,18 +33,20 @@ const TAB_CONFIGS: NodeDetailTabConfig[] = [
     id: 'overview',
     label: '概览',
   },
-  {
-    id: 'dashboard',
-    label: '数据看板',
-  },
   {
     id: 'manage',
-    label: '配置与部署',
+    label: '状态与部署',
   },
 ];
 
-function isValidTab(value: string | null): value is NodeDetailTabId {
-  return value === 'overview' || value === 'dashboard' || value === 'manage';
+function resolveTab(value: string | null): NodeDetailTabId | null {
+  if (value === 'overview' || value === 'dashboard') {
+    return 'overview';
+  }
+  if (value === 'manage') {
+    return 'manage';
+  }
+  return null;
 }
 
 export function NodeDetailShell({
@@ -55,7 +57,6 @@ export function NodeDetailShell({
   actions,
   kpis,
   overview,
-  dashboard,
   manage,
   defaultTab = 'overview',
 }: {
@@ -66,7 +67,6 @@ export function NodeDetailShell({
   actions: ReactNode;
   kpis: NodeDetailKpi[];
   overview: ReactNode;
-  dashboard: ReactNode;
   manage: ReactNode;
   defaultTab?: NodeDetailTabId;
 }) {
@@ -76,7 +76,7 @@ export function NodeDetailShell({
 
   const activeTab = useMemo(() => {
     const tab = searchParams.get('tab');
-    return isValidTab(tab) ? tab : defaultTab;
+    return resolveTab(tab) ?? defaultTab;
   }, [defaultTab, searchParams]);
 
   const setActiveTab = useCallback(
@@ -88,6 +88,16 @@ export function NodeDetailShell({
     [pathname, router, searchParams],
   );
 
+  const handleTabChange = useCallback(
+    (value: string) => {
+      const tab = resolveTab(value);
+      if (tab) {
+        setActiveTab(tab);
+      }
+    },
+    [setActiveTab],
+  );
+
   return (
     
@@ -144,11 +154,7 @@ export function NodeDetailShell({ { - if (isValidTab(value)) { - setActiveTab(value); - } - }} + onValueChange={handleTabChange} className='w-full gap-0' >
@@ -174,9 +180,6 @@ export function NodeDetailShell({ {activeTab === 'overview' ? overview : null} - - {activeTab === 'dashboard' ? dashboard : null} - {activeTab === 'manage' ? manage : null} diff --git a/frontend/app/(main)/nodes/components/node-utils.ts b/frontend/app/(main)/nodes/components/node-utils.ts index 1c29c8d7..4f33a9ef 100644 --- a/frontend/app/(main)/nodes/components/node-utils.ts +++ b/frontend/app/(main)/nodes/components/node-utils.ts @@ -138,6 +138,25 @@ export function getImageTag(version?: string): string { return 'latest'; } +export function buildEdgeDockerInstallCommand( + serverUrl: string, + agentToken: string, + version?: string, +) { + const tag = getImageTag(version); + const image = `ghcr.io/rain-kl/openflare-agent:${tag}`; + + return [ + `docker pull ${image}`, + `docker rm -f openflare-agent 2>/dev/null || true`, + `docker run -d --name openflare-agent --restart unless-stopped \\`, + ` -p 80:80 -p 443:443/tcp -p 443:443/udp \\`, + ` -e OPENFLARE_SERVER_URL=${serverUrl} \\`, + ` -e OPENFLARE_AGENT_TOKEN=${agentToken} \\`, + ` ${image}`, + ].join('\n'); +} + export function buildRelayInstallCommand( serverUrl: string, discoveryToken: string, diff --git a/frontend/app/(main)/nodes/components/relay-node-detail.tsx b/frontend/app/(main)/nodes/components/relay-node-detail.tsx index 8299a6fe..d50ef612 100644 --- a/frontend/app/(main)/nodes/components/relay-node-detail.tsx +++ b/frontend/app/(main)/nodes/components/relay-node-detail.tsx @@ -278,7 +278,7 @@ export function RelayNodeDetail({ node }: { node: NodeItem }) { ); - const overviewTab = ( + const manageTab = (
{node.last_error ? : null} @@ -359,11 +359,7 @@ export function RelayNodeDetail({ node }: { node: NodeItem }) {
-
- ); - const manageTab = ( -
); - const overviewTab = ( + const manageTab = (
{node.last_error ? : null} @@ -271,11 +271,9 @@ export function TunnelNodeDetail({ node }: { node: NodeItem }) {
-
- ); - const manageTab = ( -
+ +
@@ -294,8 +292,6 @@ export function TunnelNodeDetail({ node }: { node: NodeItem }) {
- -
); @@ -338,8 +334,7 @@ export function TunnelNodeDetail({ node }: { node: NodeItem }) { icon: Activity, }, ]} - overview={overviewTab} - dashboard={ + overview={