Forráskód Böngészése

编写page1-系统信息页面功能

niujiuru 1 hónapja
szülő
commit
5b91c7bbcc

+ 2 - 0
web-ui.service/main.go

@@ -74,6 +74,8 @@ func main() {
 
 	servicelib.WriteVersionFile(baseapp.EXEC_DIR, Version) //-> for 升级
 
+	initCPUUsage()
+
 	prg := &program{
 		name: "WebUI",
 	}

+ 212 - 0
web-ui.service/page1_handler.go

@@ -0,0 +1,212 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"os"
+	"os/exec"
+	"runtime"
+	"strconv"
+	"strings"
+	"sync"
+)
+
+var lastCPU struct {
+	idle, total uint64
+	mu          sync.Mutex
+}
+
+func initCPUUsage() {
+	getCPUUsage()
+}
+
+func systemInfoHandler(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json; charset=utf-8")
+
+	memPercent, memUsed, memTotal := getMemInfo()
+	diskPercent, diskUsed, diskTotal := getDiskInfo()
+
+	json.NewEncoder(w).Encode(map[string]any{
+		"cpu_usage": getCPUUsage(),
+		"cpu_cores": getCPUCore(),
+		"cpu_freq":  getCPUFreq(),
+
+		"mem_percent": memPercent,
+		"mem_used":    memUsed,
+		"mem_total":   memTotal,
+
+		"disk_percent": diskPercent,
+		"disk_used":    diskUsed,
+		"disk_total":   diskTotal,
+	})
+}
+
+func getCPUUsage() int {
+	lastCPU.mu.Lock()
+	defer lastCPU.mu.Unlock()
+
+	data, err := os.ReadFile("/proc/stat")
+	if err != nil {
+		return 0
+	}
+
+	f := strings.Fields(strings.Split(string(data), "\n")[0])
+	if len(f) < 5 {
+		return 0
+	}
+
+	var idle, total uint64
+
+	for i := 1; i < len(f); i++ {
+		v, err := strconv.ParseUint(f[i], 10, 64)
+		if err != nil {
+			continue
+		}
+
+		total += v
+
+		// idle + iowait
+		if i == 4 || i == 5 {
+			idle += v
+		}
+	}
+
+	if lastCPU.total == 0 {
+		lastCPU.idle = idle
+		lastCPU.total = total
+		return 0
+	}
+
+	dt := total - lastCPU.total
+	di := idle - lastCPU.idle
+
+	lastCPU.idle = idle
+	lastCPU.total = total
+
+	if dt == 0 {
+		return 0
+	}
+
+	return int((dt - di) * 100 / dt)
+}
+
+func getCPUCore() int {
+	return runtime.NumCPU()
+}
+
+func getCPUFreq() string {
+	var total, count int
+
+	for i := 0; ; i++ {
+		data, err := os.ReadFile(fmt.Sprintf("/sys/devices/system/cpu/cpu%d/cpufreq/scaling_cur_freq", i))
+		if err != nil {
+			break
+		}
+
+		freq, err := strconv.Atoi(strings.TrimSpace(string(data)))
+		if err != nil {
+			continue
+		}
+
+		total += freq / 1000
+		count++
+	}
+
+	if count > 0 {
+		return strconv.Itoa(total / count)
+	}
+
+	data, err := os.ReadFile("/proc/cpuinfo")
+	if err == nil {
+		for _, line := range strings.Split(string(data), "\n") {
+			if strings.HasPrefix(line, "cpu MHz") {
+				f := strings.Split(line, ":")
+				if len(f) == 2 {
+					v, err := strconv.ParseFloat(strings.TrimSpace(f[1]), 64)
+					if err == nil {
+						return strconv.Itoa(int(v))
+					}
+				}
+			}
+		}
+	}
+
+	return noValue
+}
+
+func getMemInfo() (int, string, string) {
+	data, err := os.ReadFile("/proc/meminfo")
+	if err != nil {
+		return 0, noValue, noValue
+	}
+
+	var total, avail uint64
+
+	for _, line := range strings.Split(string(data), "\n") {
+		f := strings.Fields(line)
+		if len(f) < 2 {
+			continue
+		}
+
+		switch f[0] {
+		case "MemTotal:":
+			total, _ = strconv.ParseUint(f[1], 10, 64)
+		case "MemAvailable:":
+			avail, _ = strconv.ParseUint(f[1], 10, 64)
+		}
+	}
+
+	if total == 0 {
+		return 0, noValue, noValue
+	}
+
+	used := total - avail
+
+	return int(used * 100 / total),
+		formatSize(used * 1024),
+		formatSize(total * 1024)
+}
+
+func getDiskInfo() (int, string, string) {
+	out, err := exec.Command("df", "-h", "/").Output()
+	if err != nil {
+		return 0, noValue, noValue
+	}
+
+	lines := strings.Split(string(out), "\n")
+	if len(lines) < 2 {
+		return 0, noValue, noValue
+	}
+
+	f := strings.Fields(lines[1])
+	if len(f) < 5 {
+		return 0, noValue, noValue
+	}
+
+	percent, err := strconv.Atoi(strings.TrimSuffix(f[4], "%"))
+	if err != nil {
+		percent = 0
+	}
+
+	return percent, f[2], f[1]
+}
+
+func formatSize(size uint64) string {
+	const (
+		KB = 1024
+		MB = KB * 1024
+		GB = MB * 1024
+	)
+
+	switch {
+	case size >= GB:
+		return fmt.Sprintf("%.1fGB", float64(size)/GB)
+	case size >= MB:
+		return fmt.Sprintf("%.0fMB", float64(size)/MB)
+	case size >= KB:
+		return fmt.Sprintf("%.0fKB", float64(size)/KB)
+	default:
+		return fmt.Sprintf("%dB", size)
+	}
+}

+ 1 - 0
web-ui.service/web/app.html

@@ -48,6 +48,7 @@
     </main>
   </div>
 </div>
+<script src="static/js/echarts.min.js"></script>
 <script src="static/js/xterm.js"></script>
 <script src="static/js/xterm-addon-fit.js"></script>
 <script src="static/js/log-stream.js"></script>

+ 106 - 6
web-ui.service/web/pages/page1.html

@@ -10,12 +10,112 @@
 </head>
 
 <body>
-<div class="app">
-  <div class="card">
-    <div class="icon">🚧</div>
-      <h1>页面1</h1>
-      <h1>敬请期待</h1>
-    <p class="subtitle">该功能正在开发中,敬请期待...</p>
+<div class="page1">
+  <div class="page1-grid-1">
+    <div class="page1-card">
+      <div class="page1-header">🖥️ CPU</div>
+      <div id="cpu-chart" class="page1-chart"></div>
+    </div>
+
+    <div class="page1-card">
+      <div class="page1-header">🧠 内存</div>
+      <div id="mem-chart" class="page1-chart"></div>
+    </div>
+
+    <div class="page1-card">
+      <div class="page1-header">💾 硬盘</div>
+      <div id="disk-chart" class="page1-chart"></div>
+    </div>
+  </div>
+  <div class="page1-grid-2">
+    <div class="page1-card">
+      <div class="page1-header">📈 负载</div>
+      <div class="info-list">
+        <div class="info-item">
+          <span class="info-name">系统01分钟负载</span>
+          <span class="info-value" id="load-1m">--</span>
+        </div>
+
+        <div class="info-item">
+          <span class="info-name">系统05分钟负载</span>
+          <span class="info-value" id="load-1m">--</span>
+        </div>
+
+        <div class="info-item">
+          <span class="info-name">系统15分钟负载</span>
+          <span class="info-value" id="load-1m">--</span>
+        </div>
+      </div>
+    </div>
+
+    <div class="page1-card">
+      <div class="page1-header">📦 云飞</div>
+      <div class="info-list">
+        <div class="info-item">
+          <div class="info-info">
+            <div class="info-name">networkd.service</div>
+            <div class="info-desc">网络管理服务</div>
+          </div>
+          <span class="info-version" id="var1">--</span>
+        </div>
+
+        <div class="info-item">
+          <div class="info-info">
+            <div class="info-name">timesyncd.service</div>
+            <div class="info-desc">系统时间同步</div>
+          </div>
+          <span class="info-version" id="var2">--</span>
+        </div>
+
+        <div class="info-item">
+          <div class="info-info">
+            <div class="info-name">gnss.service</div>
+            <div class="info-desc">位置信息获取</div>
+          </div>
+          <span class="info-version" id="var3">--</span>
+        </div>
+
+        <div class="info-item">
+          <div class="info-info">
+            <div class="info-name">sshd-mqtt-bridge.service</div>
+            <div class="info-desc">远程运维通道</div>
+          </div>
+          <span class="info-version" id="var4">--</span>
+        </div>
+
+        <div class="info-item">
+          <div class="info-info">
+            <div class="info-name">camera-capture.service</div>
+            <div class="info-desc">相机图像采集</div>
+          </div>
+          <span class="info-version" id="var5">--</span>
+        </div>
+
+        <div class="info-item">
+          <div class="info-info">
+            <div class="info-name">app-install.service</div>
+            <div class="info-desc">应用安装卸载</div>
+          </div>
+          <span class="info-version" id="var6">--</span>
+        </div>
+
+        <div class="info-item">
+          <div class="info-info">
+            <div class="info-name">web-ui.service</div>
+            <div class="info-desc">统一配置管理</div>
+          </div>
+          <span class="info-version" id="var7">--</span>
+        </div>
+
+        <div class="info-item">
+          <div class="info-info">
+            <div class="info-name">upgrade.service</div>
+            <div class="info-desc">自动服务升级</div>
+          </div>
+          <span class="info-version", id="var8">--</span>
+        </div>
+      </div>
+    </div>
   </div>
 </div>
 </body>

+ 79 - 1
web-ui.service/web/static/css/page1.css

@@ -1 +1,79 @@
-/* reserved */
+.page1 {
+    width:100%;
+    height:100%;
+    padding:12px;
+}
+
+.page1-grid-1 {
+    width:100%;
+    display:grid;
+    grid-template-columns:repeat(3, minmax(0, 1fr));
+    gap:20px;
+}
+
+.page1-grid-2 {
+    width:100%;
+    display:grid;
+    grid-template-columns:1fr 2fr;
+    gap:20px;
+    margin-top:20px;
+}
+
+.page1-card {
+    width:100%;
+    background:#0f172a;
+    border:1px solid #1f2937;
+    border-radius:4px;
+    padding:12px;
+    overflow:hidden;
+}
+
+.page1-header {
+    height:24px;
+    line-height:24px;
+    margin-bottom:12px;
+    color:#e2e8f0;
+    font-size:14px;
+}
+
+.page1-chart {
+    width:100%;
+    height:260px;
+}
+
+.info-list {
+    width:100%;
+}
+
+.info-item {
+    height:42px;
+    display:flex;
+    align-items:center;
+    border-bottom:1px solid #1f2937;
+}
+
+.info-item:last-child {
+    border-bottom:none;
+}
+
+.info-name {
+    width:200px;
+    flex-shrink:0;
+    color:#cbd5e1;
+    font-size:14px;
+}
+
+.info-desc {
+    flex:1;
+    color:#64748b;
+    font-size:12px;
+}
+
+.info-value {
+    width:80px;
+    margin-left:20px;
+    text-align:right;
+    color:#94a3b8;
+    font-size:14px;
+    font-family:ui-monospace,Consolas,monospace;
+}

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 45 - 0
web-ui.service/web/static/js/echarts.min.js


+ 167 - 2
web-ui.service/web/static/js/page1.js

@@ -1,5 +1,170 @@
-function initPage1() {
+let cpuChart       = null;
+let memChart       = null;
+let diskChart      = null;
+let sysStatusTimer = null;
+
+function getSystemInfo(){
+  fetch("/api/system/info")
+  .then(r=>{
+    if(!r.ok) throw new Error("HTTP "+r.status);
+    return r.json();
+  })
+  .then(d=>{
+    cpuChart.setOption({
+      graphic:[{
+        type:"text",
+        left:"center",
+        top:"75%",
+        style:{
+          text:`${d.cpu_cores||"--"}核 ${d.cpu_freq||"--"}MHz`,
+          fill:"#aaa",
+          fontSize:15
+        }
+      }],
+      series:[{data:[{value:d.cpu_usage}]}]
+    });
+
+    memChart.setOption({
+      graphic:[{
+        type:"text",
+        left:"center",
+        top:"75%",
+        style:{
+          text:`${d.mem_used||"--"} / ${d.mem_total||"--"}`,
+          fill:"#aaa",
+          fontSize:15
+        }
+      }],
+      series:[{data:[{value:d.mem_percent}]}]
+    });
+
+    diskChart.setOption({
+      title:{
+        text:`${d.disk_percent}%`,
+        subtext:`${d.disk_used||"--"} / ${d.disk_total||"--"}`,
+        left:"center",
+        top:"38%",
+        textStyle:{
+          fontSize:38,
+          fontWeight:"bold"
+        },
+        subtextStyle:{
+          fontSize:15
+        }
+      },
+      series:[{
+        data:[
+          {value:d.disk_percent,name:"已使用"},
+          {value:100-d.disk_percent,name:"剩余"}
+        ]
+      }]
+    });
+  })
+  .catch(e=>console.error("system info failed:",e));
+}
+
+function gaugeOption(){
+  return {
+    series:[{
+      type:"gauge",
+      radius:"95%",
+      center:["50%","55%"],
+      min:0,
+      max:100,
+      startAngle:210,
+      endAngle:-30,
+      axisLine:{
+        lineStyle:{
+          width:22
+        }
+      },
+      progress:{
+        show:true,
+        width:22
+      },
+      pointer:{show:false},
+      axisTick:{show:false},
+      splitLine:{show:false},
+      axisLabel:{show:false},
+      detail:{
+        fontSize:42,
+        fontWeight:"bold",
+        offsetCenter:[0,"0%"],
+        formatter:"{value}%"
+      },
+      title:{show:false},
+      data:[{value:0}]
+    }]
+  };
 }
 
-function exitPage1() {
+function startSysStatusRefresh(){
+  if(sysStatusTimer) return;
+
+  getSystemInfo();
+
+  sysStatusTimer = setInterval(() => {
+    getSystemInfo();
+  }, 3000);
+}
+
+function stopSysStatusRefresh(){
+  if(sysStatusTimer){
+    clearInterval(sysStatusTimer);
+    sysStatusTimer=null;
+  }
+}
+
+function initPage1(){
+  cpuChart=echarts.init(document.getElementById("cpu-chart"));
+  memChart=echarts.init(document.getElementById("mem-chart"));
+  diskChart=echarts.init(document.getElementById("disk-chart"));
+
+  cpuChart.setOption(gaugeOption());
+  memChart.setOption(gaugeOption());
+
+  diskChart.setOption({
+    title:{
+      text:"0%",
+      subtext:"-- / --",
+      left:"center",
+      top:"38%",
+      textStyle:{
+        fontSize:38,
+        fontWeight:"bold"
+      },
+      subtextStyle:{
+        fontSize:15
+      }
+    },
+    series:[{
+      type:"pie",
+      radius:["65%","85%"],
+      label:{show:false},
+      data:[
+        {value:0,name:"已使用"},
+        {value:100,name:"剩余"}
+      ]
+    }]
+  });
+
+  startSysStatusRefresh();
+
+  setTimeout(()=>{
+    cpuChart.resize();
+    memChart.resize();
+    diskChart.resize();
+  },500);
+}
+
+function exitPage1(){
+  stopSysStatusRefresh();
+
+  cpuChart?.dispose();
+  memChart?.dispose();
+  diskChart?.dispose();
+
+  cpuChart=null;
+  memChart=null;
+  diskChart=null;
 }

+ 4 - 1
web-ui.service/web/static/js/router.js

@@ -12,7 +12,10 @@ const routes = {
 let currentPage = null;
 
 const pageHandlers = {
-  page1: {},
+  page1: {
+    init: () => window.initPage1?.(),
+    exit: () => window.exitPage1?.()
+  },
   page2: {
     init: () => window.initPage2?.(),
     exit: () => window.exitPage2?.()

+ 1 - 1
web-ui.service/web_handler.go

@@ -172,7 +172,7 @@ func serviceLogHandler(w http.ResponseWriter, r *http.Request) {
 		"-u",
 		unit,
 		"-n",
-		"10",
+		"50",
 		"--no-pager",
 		"-q",
 		"-o",

+ 5 - 0
web-ui.service/web_route.go

@@ -34,6 +34,11 @@ var routes = []Route{
 	},
 
 	{
+		"/api/system/info",
+		systemInfoHandler,
+	},
+
+	{
 		"/api/network/interfaces",
 		netInterfaces,
 	},