Pārlūkot izejas kodu

feat: add nginx detail status

Akino 3 nedēļas atpakaļ
vecāks
revīzija
4e8346f04e

+ 4 - 0
api/nginx/router.go

@@ -13,6 +13,10 @@ func InitRouter(r *gin.RouterGroup) {
 	r.POST("nginx/restart", Restart)
 	r.POST("nginx/test", Test)
 	r.GET("nginx/status", Status)
+	// 获取 Nginx 详细状态信息,包括连接数、进程信息等(Issue #850)
+	r.GET("nginx/detailed_status", GetDetailedStatus)
+	// 使用SSE推送Nginx详细状态信息
+	r.GET("nginx/detailed_status/stream", StreamDetailedStatus)
 	r.POST("nginx_log", nginx_log.GetNginxLogPage)
 	r.GET("nginx/directives", GetDirectives)
 }

+ 452 - 0
api/nginx/status.go

@@ -0,0 +1,452 @@
+// GetDetailedStatus API 实现
+// 该功能用于解决 Issue #850,提供类似宝塔面板的 Nginx 负载监控功能
+// 返回详细的 Nginx 状态信息,包括请求统计、连接数、工作进程等数据
+package nginx
+
+import (
+	"fmt"
+	"io"
+	"math"
+	"net/http"
+	"os"
+	"os/exec"
+	"regexp"
+	"runtime"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/gin-gonic/gin"
+	"github.com/shirou/gopsutil/v4/process"
+	"github.com/uozi-tech/cosy/logger"
+)
+
+// NginxPerformanceInfo 存储 Nginx 性能相关信息
+type NginxPerformanceInfo struct {
+	// 基本状态信息
+	Active   int `json:"active"`   // 活动连接数
+	Accepts  int `json:"accepts"`  // 总握手次数
+	Handled  int `json:"handled"`  // 总连接次数
+	Requests int `json:"requests"` // 总请求数
+	Reading  int `json:"reading"`  // 读取客户端请求数
+	Writing  int `json:"writing"`  // 响应数
+	Waiting  int `json:"waiting"`  // 驻留进程(等待请求)
+
+	// 进程相关信息
+	Workers     int     `json:"workers"`      // 工作进程数
+	Master      int     `json:"master"`       // 主进程数
+	Cache       int     `json:"cache"`        // 缓存管理进程数
+	Other       int     `json:"other"`        // 其他Nginx相关进程数
+	CPUUsage    float64 `json:"cpu_usage"`    // CPU 使用率
+	MemoryUsage float64 `json:"memory_usage"` // 内存使用率(MB)
+
+	// 配置信息
+	WorkerProcesses   int `json:"worker_processes"`   // worker_processes 配置
+	WorkerConnections int `json:"worker_connections"` // worker_connections 配置
+}
+
+// GetDetailedStatus 获取 Nginx 详细状态信息
+func GetDetailedStatus(c *gin.Context) {
+	// 检查 Nginx 是否运行
+	pidPath := nginx.GetPIDPath()
+	running := true
+	if fileInfo, err := os.Stat(pidPath); err != nil || fileInfo.Size() == 0 {
+		running = false
+		c.JSON(http.StatusOK, gin.H{
+			"running": false,
+			"message": "Nginx is not running",
+		})
+		return
+	}
+
+	// 获取 stub_status 模块数据
+	stubStatusInfo, err := getStubStatusInfo()
+	if err != nil {
+		logger.Warn("Failed to get stub_status info:", err)
+	}
+
+	// 获取进程信息
+	processInfo, err := getNginxProcessInfo()
+	if err != nil {
+		logger.Warn("Failed to get process info:", err)
+	}
+
+	// 获取配置信息
+	configInfo, err := getNginxConfigInfo()
+	if err != nil {
+		logger.Warn("Failed to get config info:", err)
+	}
+
+	// 组合所有信息
+	info := NginxPerformanceInfo{
+		Active:            stubStatusInfo["active"],
+		Accepts:           stubStatusInfo["accepts"],
+		Handled:           stubStatusInfo["handled"],
+		Requests:          stubStatusInfo["requests"],
+		Reading:           stubStatusInfo["reading"],
+		Writing:           stubStatusInfo["writing"],
+		Waiting:           stubStatusInfo["waiting"],
+		Workers:           processInfo["workers"].(int),
+		Master:            processInfo["master"].(int),
+		Cache:             processInfo["cache"].(int),
+		Other:             processInfo["other"].(int),
+		CPUUsage:          processInfo["cpu_usage"].(float64),
+		MemoryUsage:       processInfo["memory_usage"].(float64),
+		WorkerProcesses:   configInfo["worker_processes"],
+		WorkerConnections: configInfo["worker_connections"],
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"running": running,
+		"info":    info,
+	})
+}
+
+// StreamDetailedStatus 使用 SSE 流式推送 Nginx 详细状态信息
+func StreamDetailedStatus(c *gin.Context) {
+	// 设置 SSE 的响应头
+	c.Header("Content-Type", "text/event-stream")
+	c.Header("Cache-Control", "no-cache")
+	c.Header("Connection", "keep-alive")
+	c.Header("Access-Control-Allow-Origin", "*")
+
+	// 创建上下文,当客户端断开连接时取消
+	ctx := c.Request.Context()
+
+	// 为防止 goroutine 泄漏,创建一个计时器通道
+	ticker := time.NewTicker(5 * time.Second)
+	defer ticker.Stop()
+
+	// 立即发送一次初始数据
+	sendPerformanceData(c)
+
+	// 使用 goroutine 定期发送数据
+	for {
+		select {
+		case <-ticker.C:
+			// 发送性能数据
+			if err := sendPerformanceData(c); err != nil {
+				logger.Warn("Error sending SSE data:", err)
+				return
+			}
+		case <-ctx.Done():
+			// 客户端断开连接或请求被取消
+			logger.Debug("Client closed connection")
+			return
+		}
+	}
+}
+
+// sendPerformanceData 发送一次性能数据
+func sendPerformanceData(c *gin.Context) error {
+	// 检查 Nginx 是否运行
+	pidPath := nginx.GetPIDPath()
+	running := true
+	if fileInfo, err := os.Stat(pidPath); err != nil || fileInfo.Size() == 0 {
+		running = false
+		// 发送 Nginx 未运行的状态
+		c.SSEvent("message", gin.H{
+			"running": false,
+			"message": "Nginx is not running",
+		})
+		// 刷新缓冲区,确保数据立即发送
+		c.Writer.Flush()
+		return nil
+	}
+
+	// 获取性能数据
+	stubStatusInfo, err := getStubStatusInfo()
+	if err != nil {
+		logger.Warn("Failed to get stub_status info:", err)
+	}
+
+	processInfo, err := getNginxProcessInfo()
+	if err != nil {
+		logger.Warn("Failed to get process info:", err)
+	}
+
+	configInfo, err := getNginxConfigInfo()
+	if err != nil {
+		logger.Warn("Failed to get config info:", err)
+	}
+
+	// 组合所有信息
+	info := NginxPerformanceInfo{
+		Active:            stubStatusInfo["active"],
+		Accepts:           stubStatusInfo["accepts"],
+		Handled:           stubStatusInfo["handled"],
+		Requests:          stubStatusInfo["requests"],
+		Reading:           stubStatusInfo["reading"],
+		Writing:           stubStatusInfo["writing"],
+		Waiting:           stubStatusInfo["waiting"],
+		Workers:           processInfo["workers"].(int),
+		Master:            processInfo["master"].(int),
+		Cache:             processInfo["cache"].(int),
+		Other:             processInfo["other"].(int),
+		CPUUsage:          processInfo["cpu_usage"].(float64),
+		MemoryUsage:       processInfo["memory_usage"].(float64),
+		WorkerProcesses:   configInfo["worker_processes"],
+		WorkerConnections: configInfo["worker_connections"],
+	}
+
+	// 发送 SSE 事件
+	c.SSEvent("message", gin.H{
+		"running": running,
+		"info":    info,
+	})
+
+	// 刷新缓冲区,确保数据立即发送
+	c.Writer.Flush()
+	return nil
+}
+
+// 获取 stub_status 模块数据
+func getStubStatusInfo() (map[string]int, error) {
+	result := map[string]int{
+		"active": 0, "accepts": 0, "handled": 0, "requests": 0,
+		"reading": 0, "writing": 0, "waiting": 0,
+	}
+
+	// 默认尝试访问 stub_status 页面
+	statusURL := "http://localhost/stub_status"
+
+	// 创建 HTTP 客户端
+	client := &http.Client{
+		Timeout: 5 * time.Second,
+	}
+
+	// 发送请求获取 stub_status 数据
+	resp, err := client.Get(statusURL)
+	if err != nil {
+		return result, fmt.Errorf("failed to get stub status: %v", err)
+	}
+	defer resp.Body.Close()
+
+	// 读取响应内容
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return result, fmt.Errorf("failed to read response body: %v", err)
+	}
+
+	// 解析响应内容
+	statusContent := string(body)
+
+	// 匹配活动连接数
+	activeRe := regexp.MustCompile(`Active connections:\s+(\d+)`)
+	if matches := activeRe.FindStringSubmatch(statusContent); len(matches) > 1 {
+		result["active"], _ = strconv.Atoi(matches[1])
+	}
+
+	// 匹配请求统计信息
+	serverRe := regexp.MustCompile(`(\d+)\s+(\d+)\s+(\d+)`)
+	if matches := serverRe.FindStringSubmatch(statusContent); len(matches) > 3 {
+		result["accepts"], _ = strconv.Atoi(matches[1])
+		result["handled"], _ = strconv.Atoi(matches[2])
+		result["requests"], _ = strconv.Atoi(matches[3])
+	}
+
+	// 匹配读写等待数
+	connRe := regexp.MustCompile(`Reading:\s+(\d+)\s+Writing:\s+(\d+)\s+Waiting:\s+(\d+)`)
+	if matches := connRe.FindStringSubmatch(statusContent); len(matches) > 3 {
+		result["reading"], _ = strconv.Atoi(matches[1])
+		result["writing"], _ = strconv.Atoi(matches[2])
+		result["waiting"], _ = strconv.Atoi(matches[3])
+	}
+
+	return result, nil
+}
+
+// 获取 Nginx 进程信息
+func getNginxProcessInfo() (map[string]interface{}, error) {
+	result := map[string]interface{}{
+		"workers":      0,
+		"master":       0,
+		"cache":        0,
+		"other":        0,
+		"cpu_usage":    0.0,
+		"memory_usage": 0.0,
+	}
+
+	// 查找所有 Nginx 进程
+	processes, err := process.Processes()
+	if err != nil {
+		return result, fmt.Errorf("failed to get processes: %v", err)
+	}
+
+	totalMemory := 0.0
+	workerCount := 0
+	masterCount := 0
+	cacheCount := 0
+	otherCount := 0
+	nginxProcesses := []*process.Process{}
+
+	// 获取系统CPU核心数
+	numCPU := runtime.NumCPU()
+
+	// 获取Nginx主进程的PID
+	var masterPID int32 = -1
+	for _, p := range processes {
+		name, err := p.Name()
+		if err != nil {
+			continue
+		}
+
+		cmdline, err := p.Cmdline()
+		if err != nil {
+			continue
+		}
+
+		// 检查是否是Nginx主进程
+		if strings.Contains(strings.ToLower(name), "nginx") &&
+			(strings.Contains(cmdline, "master process") ||
+				!strings.Contains(cmdline, "worker process")) &&
+			p.Pid > 0 {
+			masterPID = p.Pid
+			masterCount++
+			nginxProcesses = append(nginxProcesses, p)
+
+			// 获取内存使用情况 - 使用RSS代替
+			// 注意:理想情况下我们应该使用USS(仅包含进程独占内存),但gopsutil不直接支持
+			mem, err := p.MemoryInfo()
+			if err == nil && mem != nil {
+				// 转换为 MB
+				memoryUsage := float64(mem.RSS) / 1024 / 1024
+				totalMemory += memoryUsage
+				logger.Debug("Master进程内存使用(MB):", memoryUsage)
+			}
+
+			break
+		}
+	}
+
+	// 遍历所有进程,区分工作进程和其他Nginx进程
+	for _, p := range processes {
+		if p.Pid == masterPID {
+			continue // 已经计算过主进程
+		}
+
+		name, err := p.Name()
+		if err != nil {
+			continue
+		}
+
+		// 只处理Nginx相关进程
+		if !strings.Contains(strings.ToLower(name), "nginx") {
+			continue
+		}
+
+		// 添加到Nginx进程列表
+		nginxProcesses = append(nginxProcesses, p)
+
+		// 获取父进程PID
+		ppid, err := p.Ppid()
+		if err != nil {
+			continue
+		}
+
+		cmdline, err := p.Cmdline()
+		if err != nil {
+			continue
+		}
+
+		// 获取内存使用情况 - 使用RSS代替
+		// 注意:理想情况下我们应该使用USS(仅包含进程独占内存),但gopsutil不直接支持
+		mem, err := p.MemoryInfo()
+		if err == nil && mem != nil {
+			// 转换为 MB
+			memoryUsage := float64(mem.RSS) / 1024 / 1024
+			totalMemory += memoryUsage
+		}
+
+		// 区分工作进程、缓存进程和其他进程
+		if ppid == masterPID || strings.Contains(cmdline, "worker process") {
+			workerCount++
+		} else if strings.Contains(cmdline, "cache") {
+			cacheCount++
+		} else {
+			otherCount++
+		}
+	}
+
+	// 重新计算CPU使用率,更接近top命令的计算方式
+	// 首先进行初始CPU时间测量
+	times1 := make(map[int32]float64)
+	for _, p := range nginxProcesses {
+		times, err := p.Times()
+		if err == nil {
+			// CPU时间 = 用户时间 + 系统时间
+			times1[p.Pid] = times.User + times.System
+		}
+	}
+
+	// 等待一小段时间
+	time.Sleep(100 * time.Millisecond)
+
+	// 再次测量CPU时间
+	totalCPUPercent := 0.0
+	for _, p := range nginxProcesses {
+		times, err := p.Times()
+		if err != nil {
+			continue
+		}
+
+		// 计算CPU时间差
+		currentTotal := times.User + times.System
+		if previousTotal, ok := times1[p.Pid]; ok {
+			// 计算这段时间内的CPU使用百分比(考虑多核)
+			cpuDelta := currentTotal - previousTotal
+			// 计算每秒CPU使用率(考虑采样时间)
+			cpuPercent := (cpuDelta / 0.1) * 100.0 / float64(numCPU)
+			totalCPUPercent += cpuPercent
+		}
+	}
+
+	// 四舍五入到整数,更符合top显示方式
+	totalCPUPercent = math.Round(totalCPUPercent)
+
+	// 四舍五入内存使用量到两位小数
+	totalMemory = math.Round(totalMemory*100) / 100
+
+	result["workers"] = workerCount
+	result["master"] = masterCount
+	result["cache"] = cacheCount
+	result["other"] = otherCount
+	result["cpu_usage"] = totalCPUPercent
+	result["memory_usage"] = totalMemory
+
+	return result, nil
+}
+
+// 获取 Nginx 配置信息
+func getNginxConfigInfo() (map[string]int, error) {
+	result := map[string]int{
+		"worker_processes":   1,
+		"worker_connections": 1024,
+	}
+
+	// 获取 worker_processes 配置
+	cmd := exec.Command("nginx", "-T")
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		return result, fmt.Errorf("failed to get nginx config: %v", err)
+	}
+
+	// 解析 worker_processes
+	wpRe := regexp.MustCompile(`worker_processes\s+(\d+|auto);`)
+	if matches := wpRe.FindStringSubmatch(string(output)); len(matches) > 1 {
+		if matches[1] == "auto" {
+			result["worker_processes"] = runtime.NumCPU()
+		} else {
+			result["worker_processes"], _ = strconv.Atoi(matches[1])
+		}
+	}
+
+	// 解析 worker_connections
+	wcRe := regexp.MustCompile(`worker_connections\s+(\d+);`)
+	if matches := wcRe.FindStringSubmatch(string(output)); len(matches) > 1 {
+		result["worker_connections"], _ = strconv.Atoi(matches[1])
+	}
+
+	return result, nil
+}

+ 29 - 0
app/src/api/ngx.ts

@@ -35,6 +35,24 @@ export interface NgxLocation {
 
 export type DirectiveMap = Record<string, { links: string[] }>
 
+export interface NginxPerformanceInfo {
+  active: number // 活动连接数
+  accepts: number // 总握手次数
+  handled: number // 总连接次数
+  requests: number // 总请求数
+  reading: number // 读取客户端请求数
+  writing: number // 响应数
+  waiting: number // 驻留进程(等待请求)
+  workers: number // 工作进程数
+  master: number // 主进程数
+  cache: number // 缓存管理进程数
+  other: number // 其他Nginx相关进程数
+  cpu_usage: number // CPU 使用率
+  memory_usage: number // 内存使用率(MB)
+  worker_processes: number // worker_processes 配置
+  worker_connections: number // worker_connections 配置
+}
+
 const ngx = {
   build_config(ngxConfig: NgxConfig) {
     return http.post('/ngx/build_config', ngxConfig)
@@ -52,6 +70,17 @@ const ngx = {
     return http.get('/nginx/status')
   },
 
+  detailed_status(): Promise<{ running: boolean, info: NginxPerformanceInfo }> {
+    return http.get('/nginx/detailed_status')
+  },
+
+  // 创建SSE连接获取实时Nginx性能数据
+  create_detailed_status_stream(): EventSource {
+    const baseUrl = import.meta.env.VITE_API_URL || ''
+    const url = `${baseUrl}/api/nginx/detailed_status/stream`
+    return new EventSource(url)
+  },
+
   reload() {
     return http.post('/nginx/reload')
   },

+ 17 - 35
app/src/components/EnvGroupTabs/EnvGroupTabs.vue

@@ -2,9 +2,9 @@
 import type { EnvGroup } from '@/api/env_group'
 import type { Environment } from '@/api/environment'
 import nodeApi from '@/api/node'
+import { useSSE } from '@/composables/useSSE'
 import { useUserStore } from '@/pinia'
 import { message } from 'ant-design-vue'
-import { SSE } from 'sse.js'
 
 const props = defineProps<{
   envGroups: EnvGroup[]
@@ -15,57 +15,39 @@ const { token } = storeToRefs(useUserStore())
 
 const environments = ref<Environment[]>([])
 const environmentsMap = ref<Record<number, Environment>>({})
-const sse = shallowRef<SSE>()
 const loading = ref({
   reload: false,
   restart: false,
 })
 
+// 使用SSE composable
+const { connect, disconnect } = useSSE()
+
 // Get node data when tab is not 'All'
 watch(modelValue, newVal => {
   if (newVal && newVal !== 0) {
     connectSSE()
   }
   else {
-    disconnectSSE()
+    disconnect()
   }
 }, { immediate: true })
 
-onUnmounted(() => {
-  disconnectSSE()
-})
-
 function connectSSE() {
-  disconnectSSE()
-
-  const s = new SSE('api/environments/enabled', {
-    headers: {
-      Authorization: token.value,
+  connect({
+    url: 'api/environments/enabled',
+    token: token.value,
+    onMessage: data => {
+      environments.value = data
+      environmentsMap.value = environments.value.reduce((acc, node) => {
+        acc[node.id] = node
+        return acc
+      }, {} as Record<number, Environment>)
+    },
+    onError: () => {
+      // 错误处理已由useSSE内部实现自动重连
     },
   })
-
-  s.onmessage = e => {
-    environments.value = JSON.parse(e.data)
-    environmentsMap.value = environments.value.reduce((acc, node) => {
-      acc[node.id] = node
-      return acc
-    }, {} as Record<number, Environment>)
-  }
-
-  s.onerror = () => {
-    setTimeout(() => {
-      connectSSE()
-    }, 5000)
-  }
-
-  sse.value = s
-}
-
-function disconnectSSE() {
-  if (sse.value) {
-    sse.value.close()
-    sse.value = undefined
-  }
 }
 
 // Get the current Node Group data

+ 53 - 0
app/src/composables/useNginxPerformance.ts

@@ -0,0 +1,53 @@
+import type { NginxPerformanceInfo } from '@/api/ngx'
+import ngx from '@/api/ngx'
+import { computed, ref } from 'vue'
+
+export function useNginxPerformance() {
+  const loading = ref(true)
+  const nginxInfo = ref<NginxPerformanceInfo>()
+  const error = ref<string>('')
+  const lastUpdateTime = ref(new Date())
+
+  // 更新刷新时间
+  function updateLastUpdateTime() {
+    lastUpdateTime.value = new Date()
+  }
+
+  // 格式化上次更新时间
+  const formattedUpdateTime = computed(() => {
+    return lastUpdateTime.value.toLocaleTimeString()
+  })
+
+  // 获取Nginx状态数据
+  async function fetchInitialData() {
+    loading.value = true
+    error.value = ''
+
+    try {
+      const result = await ngx.detailed_status()
+      nginxInfo.value = result.info
+      updateLastUpdateTime()
+    }
+    catch (e) {
+      if (e instanceof Error) {
+        error.value = e.message
+      }
+      else {
+        error.value = $gettext('Get data failed')
+      }
+    }
+    finally {
+      loading.value = false
+    }
+  }
+
+  return {
+    loading,
+    nginxInfo,
+    error,
+    lastUpdateTime,
+    formattedUpdateTime,
+    updateLastUpdateTime,
+    fetchInitialData,
+  }
+}

+ 194 - 0
app/src/composables/usePerformanceMetrics.ts

@@ -0,0 +1,194 @@
+import type { NginxPerformanceInfo } from '@/api/ngx'
+import type { Ref } from 'vue'
+import { computed } from 'vue'
+
+export function usePerformanceMetrics(nginxInfo: Ref<NginxPerformanceInfo | undefined>) {
+  // 格式化数值为可读性更好的形式
+  function formatNumber(num: number): string {
+    if (num >= 1000000) {
+      return `${(num / 1000000).toFixed(2)}M`
+    }
+    else if (num >= 1000) {
+      return `${(num / 1000).toFixed(2)}K`
+    }
+    return num.toString()
+  }
+
+  // 活跃连接百分比
+  const activeConnectionsPercent = computed(() => {
+    if (!nginxInfo.value) {
+      return 0
+    }
+    const maxConnections = nginxInfo.value.worker_connections * nginxInfo.value.worker_processes
+    return Number(((nginxInfo.value.active / maxConnections) * 100).toFixed(2))
+  })
+
+  // 工作进程使用百分比
+  const workerProcessesPercent = computed(() => {
+    if (!nginxInfo.value) {
+      return 0
+    }
+    return Number(((nginxInfo.value.workers / nginxInfo.value.worker_processes) * 100).toFixed(2))
+  })
+
+  // 每连接请求数
+  const requestsPerConnection = computed(() => {
+    if (!nginxInfo.value || nginxInfo.value.handled === 0) {
+      return 0
+    }
+    return (nginxInfo.value.requests / nginxInfo.value.handled).toFixed(2)
+  })
+
+  // 最大每秒请求数
+  const maxRPS = computed(() => {
+    if (!nginxInfo.value) {
+      return 0
+    }
+    return nginxInfo.value.worker_processes * nginxInfo.value.worker_connections
+  })
+
+  // 进程构成数据
+  const processTypeData = computed(() => {
+    if (!nginxInfo.value) {
+      return []
+    }
+
+    return [
+      { type: $gettext('Worker Processes'), value: nginxInfo.value.workers, color: '#1890ff' },
+      { type: $gettext('Master Process'), value: nginxInfo.value.master, color: '#52c41a' },
+      { type: $gettext('Cache Processes'), value: nginxInfo.value.cache, color: '#faad14' },
+      { type: $gettext('Other Processes'), value: nginxInfo.value.other, color: '#f5222d' },
+    ]
+  })
+
+  // 资源利用率
+  const resourceUtilization = computed(() => {
+    if (!nginxInfo.value) {
+      return 0
+    }
+
+    const cpuFactor = Math.min(nginxInfo.value.cpu_usage / 100, 1)
+    const maxConnections = nginxInfo.value.worker_connections * nginxInfo.value.worker_processes
+    const connectionFactor = Math.min(nginxInfo.value.active / maxConnections, 1)
+
+    return Math.round((cpuFactor * 0.5 + connectionFactor * 0.5) * 100)
+  })
+
+  // 表格数据
+  const statusData = computed(() => {
+    if (!nginxInfo.value) {
+      return []
+    }
+
+    return [
+      {
+        key: '1',
+        name: $gettext('Active connections'),
+        value: formatNumber(nginxInfo.value.active),
+      },
+      {
+        key: '2',
+        name: $gettext('Total handshakes'),
+        value: formatNumber(nginxInfo.value.accepts),
+      },
+      {
+        key: '3',
+        name: $gettext('Total connections'),
+        value: formatNumber(nginxInfo.value.handled),
+      },
+      {
+        key: '4',
+        name: $gettext('Total requests'),
+        value: formatNumber(nginxInfo.value.requests),
+      },
+      {
+        key: '5',
+        name: $gettext('Read requests'),
+        value: nginxInfo.value.reading,
+      },
+      {
+        key: '6',
+        name: $gettext('Responses'),
+        value: nginxInfo.value.writing,
+      },
+      {
+        key: '7',
+        name: $gettext('Waiting processes'),
+        value: nginxInfo.value.waiting,
+      },
+    ]
+  })
+
+  // 工作进程数据
+  const workerData = computed(() => {
+    if (!nginxInfo.value) {
+      return []
+    }
+
+    return [
+      {
+        key: '1',
+        name: $gettext('Number of worker processes'),
+        value: nginxInfo.value.workers,
+      },
+      {
+        key: '2',
+        name: $gettext('Master process'),
+        value: nginxInfo.value.master,
+      },
+      {
+        key: '3',
+        name: $gettext('Cache manager processes'),
+        value: nginxInfo.value.cache,
+      },
+      {
+        key: '4',
+        name: $gettext('Other Nginx processes'),
+        value: nginxInfo.value.other,
+      },
+      {
+        key: '5',
+        name: $gettext('Nginx CPU usage rate'),
+        value: `${nginxInfo.value.cpu_usage.toFixed(2)}%`,
+      },
+      {
+        key: '6',
+        name: $gettext('Nginx Memory usage'),
+        value: `${nginxInfo.value.memory_usage.toFixed(2)} MB`,
+      },
+    ]
+  })
+
+  // 配置数据
+  const configData = computed(() => {
+    if (!nginxInfo.value) {
+      return []
+    }
+
+    return [
+      {
+        key: '1',
+        name: $gettext('Number of worker processes'),
+        value: nginxInfo.value.worker_processes,
+      },
+      {
+        key: '2',
+        name: $gettext('Maximum number of connections per worker process'),
+        value: nginxInfo.value.worker_connections,
+      },
+    ]
+  })
+
+  return {
+    formatNumber,
+    activeConnectionsPercent,
+    workerProcessesPercent,
+    requestsPerConnection,
+    maxRPS,
+    processTypeData,
+    resourceUtilization,
+    statusData,
+    workerData,
+    configData,
+  }
+}

+ 91 - 0
app/src/composables/useSSE.ts

@@ -0,0 +1,91 @@
+import type { SSEvent } from 'sse.js'
+import { SSE } from 'sse.js'
+import { onUnmounted, shallowRef } from 'vue'
+
+export interface SSEOptions {
+  url: string
+  token: string
+  onMessage?: (data: any) => void
+  onError?: () => void
+  parseData?: boolean
+  reconnectInterval?: number
+}
+
+/**
+ * SSE 连接 Composable
+ * 提供创建、管理和自动清理 SSE 连接的能力
+ */
+export function useSSE() {
+  const sseInstance = shallowRef<SSE>()
+
+  /**
+   * 连接 SSE 服务
+   */
+  function connect(options: SSEOptions) {
+    disconnect()
+
+    const {
+      url,
+      token,
+      onMessage,
+      onError,
+      parseData = true,
+      reconnectInterval = 5000,
+    } = options
+
+    const sse = new SSE(url, {
+      headers: {
+        Authorization: token,
+      },
+    })
+
+    // 处理消息
+    sse.onmessage = (e: SSEvent) => {
+      if (!e.data) {
+        return
+      }
+
+      try {
+        const parsedData = parseData ? JSON.parse(e.data) : e.data
+        onMessage?.(parsedData)
+      }
+      catch (error) {
+        console.error('Error parsing SSE message:', error)
+      }
+    }
+
+    // 处理错误并重连
+    sse.onerror = () => {
+      onError?.()
+
+      // 重连逻辑
+      setTimeout(() => {
+        connect(options)
+      }, reconnectInterval)
+    }
+
+    sseInstance.value = sse
+    return sse
+  }
+
+  /**
+   * 断开 SSE 连接
+   */
+  function disconnect() {
+    if (sseInstance.value) {
+      sseInstance.value.close()
+      sseInstance.value = undefined
+    }
+  }
+
+  // 组件卸载时自动断开连接
+  onUnmounted(() => {
+    disconnect()
+  })
+
+  return {
+    connect,
+    disconnect,
+    sseInstance,
+  }
+}

+ 2 - 2
app/src/lib/http/interceptors.ts

@@ -121,7 +121,7 @@ export function setupResponseInterceptor() {
       const otpModal = use2FAModal()
 
       // Handle authentication errors
-      if (error.response) {
+      if (error?.response) {
         switch (error.response.status) {
           case 401:
             secureSessionId.value = ''
@@ -135,7 +135,7 @@ export function setupResponseInterceptor() {
       }
 
       // Handle JSON error that comes back as Blob for blob request type
-      if (error.response?.data instanceof Blob && error.response.data.type === 'application/json') {
+      if (error?.response?.data instanceof Blob && error?.response?.data?.type === 'application/json') {
         try {
           const text = await error.response.data.text()
           error.response.data = JSON.parse(text)

+ 19 - 1
app/src/routes/modules/dashboard.ts

@@ -4,11 +4,29 @@ import { HomeOutlined } from '@ant-design/icons-vue'
 export const dashboardRoutes: RouteRecordRaw[] = [
   {
     path: 'dashboard',
-    component: () => import('@/views/dashboard/DashBoard.vue'),
+    redirect: '/dashboard/server',
     name: 'Dashboard',
     meta: {
       name: () => $gettext('Dashboard'),
       icon: HomeOutlined,
     },
+    children: [
+      {
+        path: 'server',
+        component: () => import('@/views/dashboard/ServerDashBoard.vue'),
+        name: 'Server',
+        meta: {
+          name: () => $gettext('Server'),
+        },
+      },
+      {
+        path: 'nginx',
+        component: () => import('@/views/dashboard/NginxDashBoard.vue'),
+        name: 'NginxPerformance',
+        meta: {
+          name: () => $gettext('Nginx'),
+        },
+      },
+    ],
   },
 ]

+ 1 - 1
app/src/version.json

@@ -1 +1 @@
-{"version":"2.0.0-rc.5","build_id":12,"total_build":406}
+{"version":"2.0.0-rc.5","build_id":13,"total_build":407}

+ 153 - 0
app/src/views/dashboard/NginxDashBoard.vue

@@ -0,0 +1,153 @@
+<script setup lang="ts">
+import { useNginxPerformance } from '@/composables/useNginxPerformance'
+import { useSSE } from '@/composables/useSSE'
+import { NginxStatus } from '@/constants'
+import { useUserStore } from '@/pinia'
+import { useGlobalStore } from '@/pinia/moudule/global'
+import { ClockCircleOutlined, ReloadOutlined } from '@ant-design/icons-vue'
+import { storeToRefs } from 'pinia'
+import ConnectionMetricsCard from './components/ConnectionMetricsCard.vue'
+import PerformanceStatisticsCard from './components/PerformanceStatisticsCard.vue'
+import PerformanceTablesCard from './components/PerformanceTablesCard.vue'
+import ProcessDistributionCard from './components/ProcessDistributionCard.vue'
+import ResourceUsageCard from './components/ResourceUsageCard.vue'
+
+// 全局状态
+const global = useGlobalStore()
+const { nginxStatus: status } = storeToRefs(global)
+const { token } = storeToRefs(useUserStore())
+
+// 使用性能数据composable
+const {
+  loading,
+  nginxInfo,
+  error,
+  formattedUpdateTime,
+  updateLastUpdateTime,
+  fetchInitialData,
+} = useNginxPerformance()
+
+// SSE 连接
+const { connect, disconnect } = useSSE()
+
+// 连接SSE
+function connectSSE() {
+  disconnect()
+  loading.value = true
+
+  connect({
+    url: 'api/nginx/detailed_status/stream',
+    token: token.value,
+    onMessage: data => {
+      loading.value = false
+
+      if (data.running) {
+        nginxInfo.value = data.info
+        updateLastUpdateTime()
+      }
+      else {
+        error.value = data.message || $gettext('Nginx is not running')
+      }
+    },
+    onError: () => {
+      error.value = $gettext('Connection error, trying to reconnect...')
+
+      // 如果连接失败,尝试使用传统方式获取数据
+      setTimeout(() => {
+        fetchInitialData()
+      }, 5000)
+    },
+  })
+}
+
+// 手动刷新数据
+function refreshData() {
+  fetchInitialData().then(connectSSE)
+}
+
+// 组件挂载时初始化连接
+onMounted(() => {
+  fetchInitialData().then(connectSSE)
+})
+</script>
+
+<template>
+  <div>
+    <!-- 顶部操作栏 -->
+    <div class="mb-4 mx-6 md:mx-0 flex flex-wrap justify-between items-center">
+      <div class="flex items-center">
+        <ABadge :status="status === NginxStatus.Running ? 'success' : 'error'" />
+        <span class="font-medium">{{ status === NginxStatus.Running ? $gettext('Nginx is running') : $gettext('Nginx is not running') }}</span>
+      </div>
+      <div class="flex items-center">
+        <ClockCircleOutlined class="mr-1 text-gray-500" />
+        <span class="mr-4 text-gray-500 text-sm text-nowrap">{{ $gettext('Last update') }}: {{ formattedUpdateTime }}</span>
+        <AButton type="text" size="small" :loading="loading" @click="refreshData">
+          <template #icon>
+            <ReloadOutlined />
+          </template>
+        </AButton>
+      </div>
+    </div>
+
+    <!-- Nginx 状态提示 -->
+    <AAlert
+      v-if="status !== NginxStatus.Running"
+      class="mb-4"
+      type="warning"
+      show-icon
+      :message="$gettext('Nginx is not running')"
+      :description="$gettext('Cannot get performance data in this state')"
+    />
+
+    <!-- 错误提示 -->
+    <AAlert
+      v-if="error"
+      class="mb-4"
+      type="error"
+      show-icon
+      :message="$gettext('Get data failed')"
+      :description="error"
+    />
+
+    <!-- 加载中状态 -->
+    <ASpin :spinning="loading" :tip="$gettext('Loading data...')">
+      <div v-if="!nginxInfo && !loading && !error" class="text-center py-8">
+        <AEmpty :description="$gettext('No data')" />
+      </div>
+
+      <div v-if="nginxInfo" class="performance-dashboard">
+        <!-- 顶部性能指标卡片 -->
+        <ARow :gutter="[16, 16]" class="mb-4">
+          <ACol :span="24">
+            <ACard :title="$gettext('Performance Metrics')" :bordered="false">
+              <PerformanceStatisticsCard :nginx-info="nginxInfo" />
+            </ACard>
+          </ACol>
+        </ARow>
+
+        <!-- 指标卡片 -->
+        <ConnectionMetricsCard :nginx-info="nginxInfo" class="mb-4" />
+
+        <!-- 资源监控 -->
+        <ARow :gutter="[16, 16]" class="mb-4">
+          <!-- CPU和内存使用 -->
+          <ACol :xs="24" :md="12">
+            <ResourceUsageCard :nginx-info="nginxInfo" />
+          </ACol>
+          <!-- 进程分布 -->
+          <ACol :xs="24" :md="12">
+            <ProcessDistributionCard :nginx-info="nginxInfo" />
+          </ACol>
+        </ARow>
+
+        <!-- 性能指标表格 -->
+        <ARow :gutter="[16, 16]" class="mb-4">
+          <ACol :span="24">
+            <PerformanceTablesCard :nginx-info="nginxInfo" />
+          </ACol>
+        </ARow>
+      </div>
+    </ASpin>
+  </div>
+</template>

+ 0 - 0
app/src/views/dashboard/DashBoard.vue → app/src/views/dashboard/ServerDashBoard.vue


+ 139 - 0
app/src/views/dashboard/components/ConnectionMetricsCard.vue

@@ -0,0 +1,139 @@
+<script setup lang="ts">
+import type { NginxPerformanceInfo } from '@/api/ngx'
+import { InfoCircleOutlined } from '@ant-design/icons-vue'
+import { computed, defineProps } from 'vue'
+
+const props = defineProps<{
+  nginxInfo: NginxPerformanceInfo
+}>()
+
+// 活跃连接百分比
+const activeConnectionsPercent = computed(() => {
+  const maxConnections = props.nginxInfo.worker_connections * props.nginxInfo.worker_processes
+  return Number(((props.nginxInfo.active / maxConnections) * 100).toFixed(2))
+})
+
+// 工作进程使用百分比
+const workerProcessesPercent = computed(() => {
+  return Number(((props.nginxInfo.workers / props.nginxInfo.worker_processes) * 100).toFixed(2))
+})
+
+// 每连接请求数
+const requestsPerConnection = computed(() => {
+  if (props.nginxInfo.handled === 0) {
+    return '0'
+  }
+  return (props.nginxInfo.requests / props.nginxInfo.handled).toFixed(2)
+})
+
+// 格式化数值
+function formatNumber(num: number): string {
+  if (num >= 1000000) {
+    return `${(num / 1000000).toFixed(2)}M`
+  }
+  else if (num >= 1000) {
+    return `${(num / 1000).toFixed(2)}K`
+  }
+  return num.toString()
+}
+</script>
+
+<template>
+  <ARow :gutter="[16, 16]">
+    <!-- 当前活跃连接 -->
+    <ACol :xs="24" :sm="12" :md="12" :lg="6">
+      <ACard class="h-full" :bordered="false" :body-style="{ padding: '20px', height: '100%' }">
+        <div class="flex flex-col h-full">
+          <div class="mb-2 text-gray-500 font-medium truncate">
+            {{ $gettext('Current active connections') }}
+          </div>
+          <div class="flex items-baseline mb-2">
+            <span class="text-2xl font-bold mr-2">{{ nginxInfo.active }}</span>
+            <span class="text-gray-500 text-sm">/ {{ nginxInfo.worker_connections * nginxInfo.worker_processes }}</span>
+          </div>
+          <AProgress
+            :percent="activeConnectionsPercent"
+            :format="percent => `${percent?.toFixed(2)}%`"
+            :status="activeConnectionsPercent > 80 ? 'exception' : 'normal'"
+            size="small"
+          />
+        </div>
+      </ACard>
+    </ACol>
+
+    <!-- 工作进程 -->
+    <ACol :xs="24" :sm="12" :md="12" :lg="6">
+      <ACard class="h-full" :bordered="false" :body-style="{ padding: '20px', height: '100%' }">
+        <div class="flex flex-col h-full">
+          <div class="mb-2 text-gray-500 font-medium truncate">
+            {{ $gettext('Worker Processes') }}
+          </div>
+          <div class="flex items-baseline mb-2">
+            <span class="text-2xl font-bold mr-2">{{ nginxInfo.workers }}</span>
+            <span class="text-gray-500 text-sm">/ {{ nginxInfo.worker_processes }}</span>
+          </div>
+          <AProgress
+            :percent="workerProcessesPercent"
+            size="small"
+            status="active"
+          />
+          <div class="mt-2 text-xs text-gray-500 overflow-hidden text-ellipsis">
+            {{ $gettext('Total Nginx processes') }}: {{ nginxInfo.workers + nginxInfo.master + nginxInfo.cache + nginxInfo.other }}
+            <Tooltip :title="$gettext('Includes master process, worker processes, cache processes, and other Nginx processes')">
+              <InfoCircleOutlined class="ml-1" />
+            </Tooltip>
+          </div>
+        </div>
+      </ACard>
+    </ACol>
+
+    <!-- 每连接请求数 -->
+    <ACol :xs="24" :sm="12" :md="12" :lg="6">
+      <ACard class="h-full" :bordered="false" :body-style="{ padding: '20px', height: '100%' }">
+        <div class="flex flex-col h-full justify-between">
+          <div>
+            <div class="mb-2 text-gray-500 font-medium truncate">
+              {{ $gettext('Requests per connection') }}
+            </div>
+            <div class="flex items-baseline mb-2">
+              <span class="text-2xl font-bold">{{ requestsPerConnection }}</span>
+              <Tooltip :title="$gettext('The average number of requests per connection, the higher the value, the higher the connection reuse efficiency')">
+                <InfoCircleOutlined class="ml-2 text-gray-500" />
+              </Tooltip>
+            </div>
+          </div>
+          <div>
+            <div class="text-xs text-gray-500 mb-1 truncate">
+              {{ $gettext('Total requests') }}: {{ formatNumber(nginxInfo.requests) }}
+            </div>
+            <div class="text-xs text-gray-500 truncate">
+              {{ $gettext('Total connections') }}: {{ formatNumber(nginxInfo.handled) }}
+            </div>
+          </div>
+        </div>
+      </ACard>
+    </ACol>
+
+    <!-- 资源利用率 -->
+    <ACol :xs="24" :sm="12" :md="12" :lg="6">
+      <ACard class="h-full" :bordered="false" :body-style="{ padding: '20px', height: '100%' }">
+        <div class="flex flex-col h-full justify-between">
+          <div class="mb-2 text-gray-500 font-medium truncate">
+            {{ $gettext('Resource Utilization') }}
+          </div>
+          <div class="flex items-center justify-center flex-grow">
+            <AProgress
+              type="dashboard"
+              :percent="Math.round((Math.min(nginxInfo.cpu_usage / 100, 1) * 0.5 + Math.min(nginxInfo.active / (nginxInfo.worker_connections * nginxInfo.worker_processes), 1) * 0.5) * 100)"
+              :width="80"
+              status="active"
+            />
+          </div>
+          <div class="mt-2 text-xs text-gray-500 text-center overflow-hidden text-ellipsis">
+            {{ $gettext('Based on CPU usage and connection usage') }}
+          </div>
+        </div>
+      </ACard>
+    </ACol>
+  </ARow>
+</template>

+ 102 - 0
app/src/views/dashboard/components/PerformanceStatisticsCard.vue

@@ -0,0 +1,102 @@
+<script setup lang="ts">
+import type { NginxPerformanceInfo } from '@/api/ngx'
+import {
+  ApiOutlined,
+  CloudServerOutlined,
+  DashboardOutlined,
+  InfoCircleOutlined,
+  ThunderboltOutlined,
+} from '@ant-design/icons-vue'
+import { computed, defineProps } from 'vue'
+
+const props = defineProps<{
+  nginxInfo: NginxPerformanceInfo
+}>()
+
+// 计算连接效率 - 每连接的请求数
+const requestsPerConnection = computed(() => {
+  if (props.nginxInfo.handled === 0) {
+    return '0'
+  }
+  return (props.nginxInfo.requests / props.nginxInfo.handled).toFixed(2)
+})
+
+// 估算最大每秒请求数
+const maxRPS = computed(() => {
+  return props.nginxInfo.worker_processes * props.nginxInfo.worker_connections
+})
+</script>
+
+<template>
+  <ARow :gutter="[16, 24]">
+    <!-- 最大RPS -->
+    <ACol :xs="24" :sm="12" :md="8" :lg="6">
+      <AStatistic
+        :value="maxRPS"
+        :value-style="{ color: '#1890ff', fontSize: '24px' }"
+      >
+        <template #prefix>
+          <ThunderboltOutlined />
+        </template>
+        <template #title>
+          {{ $gettext('Max Requests Per Second') }}
+          <Tooltip :title="$gettext('Calculated based on worker_processes * worker_connections. Actual performance depends on hardware, configuration, and workload')">
+            <InfoCircleOutlined class="ml-1 text-gray-500" />
+          </Tooltip>
+        </template>
+      </AStatistic>
+      <div class="text-xs text-gray-500 mt-1">
+        worker_processes ({{ nginxInfo.worker_processes }}) × worker_connections ({{ nginxInfo.worker_connections }})
+      </div>
+    </ACol>
+
+    <!-- 最大并发连接 -->
+    <ACol :xs="24" :sm="12" :md="8" :lg="6">
+      <AStatistic
+        :title="$gettext('Max Concurrent Connections')"
+        :value="nginxInfo.worker_processes * nginxInfo.worker_connections"
+        :value-style="{ color: '#52c41a', fontSize: '24px' }"
+      >
+        <template #prefix>
+          <ApiOutlined />
+        </template>
+      </AStatistic>
+      <div class="text-xs text-gray-500 mt-1">
+        {{ $gettext('Current usage') }}: {{ ((nginxInfo.active / (nginxInfo.worker_processes * nginxInfo.worker_connections)) * 100).toFixed(2) }}%
+      </div>
+    </ACol>
+
+    <!-- 每连接请求数 -->
+    <ACol :xs="24" :sm="12" :md="8" :lg="6">
+      <AStatistic
+        :title="$gettext('Requests Per Connection')"
+        :value="requestsPerConnection"
+        :precision="2"
+        :value-style="{ color: '#f5222d', fontSize: '24px' }"
+      >
+        <template #prefix>
+          <DashboardOutlined />
+        </template>
+      </AStatistic>
+      <div class="text-xs text-gray-500 mt-1">
+        {{ $gettext('Higher value means better connection reuse') }}
+      </div>
+    </ACol>
+
+    <!-- Nginx进程总数 -->
+    <ACol :xs="24" :sm="12" :md="8" :lg="6">
+      <AStatistic
+        :title="$gettext('Total Nginx Processes')"
+        :value="nginxInfo.workers + nginxInfo.master + nginxInfo.cache + nginxInfo.other"
+        :value-style="{ color: '#722ed1', fontSize: '24px' }"
+      >
+        <template #prefix>
+          <CloudServerOutlined />
+        </template>
+      </AStatistic>
+      <div class="text-xs text-gray-500 mt-1">
+        {{ $gettext('Workers') }}: {{ nginxInfo.workers }}, {{ $gettext('Master') }}: {{ nginxInfo.master }}, {{ $gettext('Others') }}: {{ nginxInfo.cache + nginxInfo.other }}
+      </div>
+    </ACol>
+  </ARow>
+</template>

+ 211 - 0
app/src/views/dashboard/components/PerformanceTablesCard.vue

@@ -0,0 +1,211 @@
+<script setup lang="ts">
+import type { NginxPerformanceInfo } from '@/api/ngx'
+import type { TableColumnType } from 'ant-design-vue'
+import { InfoCircleOutlined } from '@ant-design/icons-vue'
+import { computed, defineProps, ref } from 'vue'
+
+const props = defineProps<{
+  nginxInfo: NginxPerformanceInfo
+}>()
+
+const activeTabKey = ref('status')
+
+// 表格列定义
+const columns: TableColumnType[] = [
+  {
+    title: $gettext('Indicator'),
+    dataIndex: 'name',
+    key: 'name',
+    width: '30%',
+  },
+  {
+    title: $gettext('Value'),
+    dataIndex: 'value',
+    key: 'value',
+  },
+]
+
+// 格式化数值
+function formatNumber(num: number): string {
+  if (num >= 1000000) {
+    return `${(num / 1000000).toFixed(2)}M`
+  }
+  else if (num >= 1000) {
+    return `${(num / 1000).toFixed(2)}K`
+  }
+  return num.toString()
+}
+
+// 状态数据
+const statusData = computed(() => {
+  return [
+    {
+      key: '1',
+      name: $gettext('Active connections'),
+      value: formatNumber(props.nginxInfo.active),
+    },
+    {
+      key: '2',
+      name: $gettext('Total handshakes'),
+      value: formatNumber(props.nginxInfo.accepts),
+    },
+    {
+      key: '3',
+      name: $gettext('Total connections'),
+      value: formatNumber(props.nginxInfo.handled),
+    },
+    {
+      key: '4',
+      name: $gettext('Total requests'),
+      value: formatNumber(props.nginxInfo.requests),
+    },
+    {
+      key: '5',
+      name: $gettext('Read requests'),
+      value: props.nginxInfo.reading,
+    },
+    {
+      key: '6',
+      name: $gettext('Responses'),
+      value: props.nginxInfo.writing,
+    },
+    {
+      key: '7',
+      name: $gettext('Waiting processes'),
+      value: props.nginxInfo.waiting,
+    },
+  ]
+})
+
+// 工作进程数据
+const workerData = computed(() => {
+  return [
+    {
+      key: '1',
+      name: $gettext('Number of worker processes'),
+      value: props.nginxInfo.workers,
+    },
+    {
+      key: '2',
+      name: $gettext('Master process'),
+      value: props.nginxInfo.master,
+    },
+    {
+      key: '3',
+      name: $gettext('Cache manager processes'),
+      value: props.nginxInfo.cache,
+    },
+    {
+      key: '4',
+      name: $gettext('Other Nginx processes'),
+      value: props.nginxInfo.other,
+    },
+    {
+      key: '5',
+      name: $gettext('Nginx CPU usage rate'),
+      value: `${props.nginxInfo.cpu_usage.toFixed(2)}%`,
+    },
+    {
+      key: '6',
+      name: $gettext('Nginx Memory usage'),
+      value: `${props.nginxInfo.memory_usage.toFixed(2)} MB`,
+    },
+  ]
+})
+
+// 配置数据
+const configData = computed(() => {
+  return [
+    {
+      key: '1',
+      name: $gettext('Number of worker processes'),
+      value: props.nginxInfo.worker_processes,
+    },
+    {
+      key: '2',
+      name: $gettext('Maximum number of connections per worker process'),
+      value: props.nginxInfo.worker_connections,
+    },
+  ]
+})
+
+// 最大每秒请求数
+const maxRPS = computed(() => {
+  return props.nginxInfo.worker_processes * props.nginxInfo.worker_connections
+})
+</script>
+
+<template>
+  <ACard :bordered="false">
+    <ATabs v-model:active-key="activeTabKey">
+      <!-- 请求统计 -->
+      <ATabPane key="status" :tab="$gettext('Request statistics')">
+        <div class="overflow-x-auto">
+          <ATable
+            :columns="columns"
+            :data-source="statusData"
+            :pagination="false"
+            size="middle"
+            :scroll="{ x: '100%' }"
+          />
+        </div>
+      </ATabPane>
+
+      <!-- 进程信息 -->
+      <ATabPane key="workers" :tab="$gettext('Process information')">
+        <div class="overflow-x-auto">
+          <ATable
+            :columns="columns"
+            :data-source="workerData"
+            :pagination="false"
+            size="middle"
+            :scroll="{ x: '100%' }"
+          />
+        </div>
+      </ATabPane>
+
+      <!-- 配置信息 -->
+      <ATabPane key="config" :tab="$gettext('Configuration information')">
+        <div class="overflow-x-auto">
+          <ATable
+            :columns="columns"
+            :data-source="configData"
+            :pagination="false"
+            size="middle"
+            :scroll="{ x: '100%' }"
+          />
+        </div>
+        <div class="mt-4">
+          <AAlert type="info" show-icon>
+            <template #message>
+              {{ $gettext('Nginx theoretical maximum performance') }}
+            </template>
+            <template #description>
+              <p>
+                {{ $gettext('Theoretical maximum concurrent connections:') }}
+                <strong>{{ nginxInfo.worker_processes * nginxInfo.worker_connections }}</strong>
+              </p>
+              <p>
+                {{ $gettext('Theoretical maximum RPS (Requests Per Second):') }}
+                <strong>{{ maxRPS }}</strong>
+                <ATooltip :title="$gettext('Calculated based on worker_processes * worker_connections. Actual performance depends on hardware, configuration, and workload')">
+                  <InfoCircleOutlined class="ml-1 text-gray-500" />
+                </ATooltip>
+              </p>
+              <p>
+                {{ $gettext('Maximum worker process number:') }}
+                <strong>{{ nginxInfo.worker_processes }}</strong>
+                <span class="text-gray-500 text-xs ml-2">
+                  {{ nginxInfo.worker_processes === nginxInfo.workers ? $gettext('auto = CPU cores') : $gettext('manually set') }}
+                </span>
+              </p>
+              <p class="mb-0">
+                {{ $gettext('Tips: You can increase the concurrency processing capacity by increasing worker_processes or worker_connections') }}
+              </p>
+            </template>
+          </AAlert>
+        </div>
+      </ATabPane>
+    </ATabs>
+  </ACard>
+</template>

+ 53 - 0
app/src/views/dashboard/components/ProcessDistributionCard.vue

@@ -0,0 +1,53 @@
+<script setup lang="ts">
+import type { NginxPerformanceInfo } from '@/api/ngx'
+import { computed, defineProps } from 'vue'
+
+const props = defineProps<{
+  nginxInfo: NginxPerformanceInfo
+}>()
+
+// 进程构成数据
+const processTypeData = computed(() => {
+  return [
+    { type: $gettext('Worker Processes'), value: props.nginxInfo.workers, color: '#1890ff' },
+    { type: $gettext('Master Process'), value: props.nginxInfo.master, color: '#52c41a' },
+    { type: $gettext('Cache Processes'), value: props.nginxInfo.cache, color: '#faad14' },
+    { type: $gettext('Other Processes'), value: props.nginxInfo.other, color: '#f5222d' },
+  ]
+})
+
+// 总进程数
+const totalProcesses = computed(() => {
+  return props.nginxInfo.workers + props.nginxInfo.master + props.nginxInfo.cache + props.nginxInfo.other
+})
+</script>
+
+<template>
+  <ACard :title="$gettext('Process Distribution')" :bordered="false" class="h-full" :body-style="{ height: 'calc(100% - 58px)' }">
+    <div class="process-distribution h-full flex flex-col justify-between">
+      <div>
+        <div v-for="(item, index) in processTypeData" :key="index" class="mb-3">
+          <div class="flex items-center">
+            <div class="w-3 h-3 rounded-full mr-2" :style="{ backgroundColor: item.color }" />
+            <div class="flex-grow truncate">
+              {{ item.type }}
+            </div>
+            <div class="font-medium w-8 text-right">
+              {{ item.value }}
+            </div>
+          </div>
+          <AProgress
+            :percent="totalProcesses === 0 ? 0 : (item.value / totalProcesses) * 100"
+            :stroke-color="item.color"
+            size="small"
+            :show-info="false"
+          />
+        </div>
+      </div>
+      <div class="mt-auto text-xs text-gray-500 truncate">
+        {{ $gettext('Actual worker to configured ratio') }}:
+        <span class="font-medium">{{ nginxInfo.workers }} / {{ nginxInfo.worker_processes }}</span>
+      </div>
+    </div>
+  </ACard>
+</template>

+ 87 - 0
app/src/views/dashboard/components/ResourceUsageCard.vue

@@ -0,0 +1,87 @@
+<script setup lang="ts">
+import type { NginxPerformanceInfo } from '@/api/ngx'
+import {
+  FundProjectionScreenOutlined,
+  InfoCircleOutlined,
+  ThunderboltOutlined,
+} from '@ant-design/icons-vue'
+import { computed, defineProps } from 'vue'
+
+const props = defineProps<{
+  nginxInfo: NginxPerformanceInfo
+}>()
+
+// 资源利用率
+const resourceUtilization = computed(() => {
+  const cpuFactor = Math.min(props.nginxInfo.cpu_usage / 100, 1)
+  const maxConnections = props.nginxInfo.worker_connections * props.nginxInfo.worker_processes
+  const connectionFactor = Math.min(props.nginxInfo.active / maxConnections, 1)
+
+  return Math.round((cpuFactor * 0.5 + connectionFactor * 0.5) * 100)
+})
+</script>
+
+<template>
+  <ACard :title="$gettext('Resource Usage of Nginx')" :bordered="false" class="h-full" :body-style="{ padding: '16px', height: 'calc(100% - 58px)' }">
+    <div class="flex flex-col h-full">
+      <!-- CPU使用率 -->
+      <ARow :gutter="[16, 8]" class="mb-2">
+        <ACol :span="24">
+          <div class="flex items-center">
+            <ThunderboltOutlined class="text-lg mr-2" :style="{ color: nginxInfo.cpu_usage > 80 ? '#cf1322' : '#3f8600' }" />
+            <div class="text-base font-medium">
+              {{ $gettext('CPU Usage') }}: <span :style="{ color: nginxInfo.cpu_usage > 80 ? '#cf1322' : '#3f8600' }">{{ nginxInfo.cpu_usage.toFixed(2) }}%</span>
+            </div>
+          </div>
+          <AProgress
+            :percent="Math.min(nginxInfo.cpu_usage, 100)"
+            :format="percent => `${percent?.toFixed(2)}%`"
+            :status="nginxInfo.cpu_usage > 80 ? 'exception' : 'active'"
+            size="small"
+            class="mt-1"
+            :show-info="false"
+          />
+          <div v-if="nginxInfo.cpu_usage > 50" class="text-xs text-orange-500 mt-1">
+            {{ $gettext('CPU usage is relatively high, consider optimizing Nginx configuration') }}
+          </div>
+        </ACol>
+      </ARow>
+
+      <!-- 内存使用 -->
+      <ARow :gutter="[16, 8]" class="mb-2">
+        <ACol :span="24">
+          <div class="flex items-center">
+            <div class="text-blue-500 text-lg mr-2 flex items-center">
+              <FundProjectionScreenOutlined />
+            </div>
+            <div class="text-base font-medium">
+              {{ $gettext('Memory Usage(RSS)') }}: <span class="text-blue-500">{{ nginxInfo.memory_usage.toFixed(2) }} MB</span>
+            </div>
+            <ATooltip :title="$gettext('Resident Set Size: Actual memory resident in physical memory, including all shared library memory, which will be repeated calculated for multiple processes')">
+              <InfoCircleOutlined class="ml-1 text-gray-500" />
+            </ATooltip>
+          </div>
+        </ACol>
+      </ARow>
+
+      <div class="mt-1 flex justify-between text-xs text-gray-500">
+        {{ $gettext('Per worker memory') }}: {{ (nginxInfo.memory_usage / (nginxInfo.workers || 1)).toFixed(2) }} MB
+      </div>
+
+      <!-- 系统负载 -->
+      <div class="mt-4 text-xs text-gray-500 border-t border-gray-100 pt-2">
+        <div class="flex justify-between mb-1">
+          <span>{{ $gettext('System load') }}</span>
+          <span class="font-medium">{{ resourceUtilization }}%</span>
+        </div>
+        <AProgress
+          :percent="resourceUtilization"
+          size="small"
+          :status="resourceUtilization > 80 ? 'exception' : 'active'"
+          :stroke-color="resourceUtilization > 80 ? '#ff4d4f' : resourceUtilization > 50 ? '#faad14' : '#52c41a'"
+          :show-info="false"
+        />
+      </div>
+    </div>
+  </ACard>
+</template>