Explorar o código

1, 完成page4页面开发; 2, 优化迭代代码, 新增后端调试打印日志

niujiuru hai 1 mes
pai
achega
f404dd6ae7

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

@@ -40,7 +40,7 @@ func (p *program) Start(s service.Service) error {
 	baseapp.Logger.Infof("[%s] webui starting on http :8080 ...", p.name)
 	go func() {
 		if err := ui.Start(); err != nil {
-			baseapp.Logger.Errorf("[%s] webui stopped: %v\n!!", p.name, err)
+			baseapp.Logger.Warnf("[%s] webui stopped: %v\n!", p.name, err)
 			os.Exit(1)
 		}
 	}()

+ 88 - 0
web-ui.service/page4_handler.go

@@ -0,0 +1,88 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"os"
+	"os/exec"
+	"time"
+)
+
+func downloadGnssLog(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodGet {
+		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+		return
+	}
+
+	filename := fmt.Sprintf("gnss.log.%s.tar.gz", time.Now().Format("20060102150405"))
+	tmpFile := fmt.Sprintf("/tmp/%d.tar.gz", time.Now().UnixNano())
+
+	defer os.Remove(tmpFile)
+
+	cmd := exec.Command(
+		"tar",
+		"--warning=no-file-changed",
+		"-czf",
+		tmpFile,
+		"-C",
+		"/opt/yfkj/gnss.service/log",
+		".",
+	)
+
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		if exitErr, ok := err.(*exec.ExitError); !ok || exitErr.ExitCode() != 1 {
+			http.Error(w, string(out), http.StatusInternalServerError)
+			return
+		}
+	}
+
+	w.Header().Set("Content-Type", "application/gzip")
+	w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
+
+	http.ServeFile(w, r, tmpFile)
+}
+
+func restartGnssService(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodPost {
+		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+		return
+	}
+
+	cmd := exec.Command("systemctl", "restart", "yfkj-gnss.service")
+
+	if out, err := cmd.CombinedOutput(); err != nil {
+		http.Error(w, string(out)+err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	w.Write([]byte(`{"status":"ok"}`))
+}
+
+func gnssStatusHandler(w http.ResponseWriter, r *http.Request) {
+	var pos struct {
+		Lat string `json:"lat"`
+		Lon string `json:"lon"`
+	}
+
+	callErr := callRPCResult(r.Context(), 7002, "core.getPositionInfo", nil, &pos)
+
+	status := noValue
+	lat := noValue
+	lon := noValue
+
+	if callErr == nil {
+		status = "正常"
+		lat = pos.Lat
+		lon = pos.Lon
+	}
+
+	w.Header().Set("Content-Type", "application/json; charset=utf-8")
+	json.NewEncoder(w).Encode(map[string]string{
+		"var1": status,
+		"var2": lat,
+		"var3": lon,
+	})
+}

+ 6 - 0
web-ui.service/page8_handler.go

@@ -8,6 +8,7 @@ import (
 
 	"github.com/creack/pty"
 	"github.com/gorilla/websocket"
+	"hnyfkj.com.cn/rtu/linux/baseapp"
 )
 
 var sshUpgrader = websocket.Upgrader{
@@ -37,11 +38,16 @@ func sshWSHandler(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	if true {
+		baseapp.Logger.Debugf("[SSH终端打开]")
+	}
+
 	defer func() {
 		ptmx.Close()
 		if cmd.Process != nil {
 			syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
 		}
+		baseapp.Logger.Debugf("[SSH终端关闭]")
 	}()
 
 	pty.Setsize(ptmx, &pty.Winsize{Cols: 120, Rows: 40})

+ 42 - 6
web-ui.service/web/pages/page4.html

@@ -11,12 +11,48 @@
 
 <body>
 <div class="app">
-  <div class="card">
-    <div class="icon">🚧</div>
-      <h1>页面4</h1>
-      <h1>敬请期待</h1>
-    <p class="subtitle">该功能正在开发中,敬请期待...</p>
-  </div>
+  <main class="view page-time">
+    <div class="status-bar">
+      <div class="status-row">
+        <span class="label">定位服务</span>
+        <span class="value" id="var1">--</span>
+      </div>
+      <div class="status-row">
+        <span class="label">定位经度</span>
+        <span class="value" id="var2">--</span>
+      </div>
+      <div class="status-row">
+        <span class="label">定位维度</span>
+        <span class="value" id="var3">--</span>
+      </div>
+    </div>
+
+    <div class="full-pane">
+      <div class="card log-card">
+        <div class="card-header">
+          <h2>时间服务</h2>
+          <div class="log-actions">
+            <select class="log-level-select" id="logLevel" title="当前服务日志级别" onchange="setGnssLogLevel()">
+              <option value="trace">trace</option>
+              <option value="debug">debug</option>
+              <option value="info" selected>info</option>
+              <option value="error">error</option>
+              <option value="fatal">fatal</option>
+            </select>
+            <span style="margin:0 1px;"></span>
+            <button class="btn" title="复制当前屏显日志" onclick="copyLog('text1')">复制</button>
+            <span style="margin:0 1px;"></span>
+            <button class="btn" title="清除当前屏显日志" onclick="clearLog('text1')">清除</button>
+            <span style="margin:0 1px;"></span>
+            <button class="btn" title="下载服务日志文件" onclick="downloadGnssLogFile()">下载</button>
+            <span style="margin:0 1px;"></span>
+            <button class="btn" title="点击重启时间服务" onclick="restartGnssService()">重启</button>
+          </div>
+        </div>
+        <pre class="output" id="text1">自动实时获取数据...</pre>
+      </div>
+    </div>
+  </main>
 </div>
 </body>
 

+ 42 - 1
web-ui.service/web/static/css/page4.css

@@ -1 +1,42 @@
-/* reserved */
+/* 页面整体布局 */
+.page-time {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+    overflow: hidden;
+}
+
+/* 顶部的状态栏 */
+.status-bar {
+    display: flex;
+    flex-direction: column;
+    padding: 8px 12px;
+    background: linear-gradient(180deg, #0f172a, #0b1220);
+    gap: 6px;
+    border: 1px solid rgba(148,163,184,0.15);
+    border-radius: 4px;
+    flex-shrink: 0;
+}
+
+.status-row {
+    display: flex;
+    align-items: center;
+    gap: 14px;
+}
+
+.status-row .label {
+    font-size: 14px;
+}
+
+.status-row .value {
+    font-size: 14px;
+    font-family: ui-monospace, Consolas, monospace;
+}
+
+/* 下方全屏区域 */
+.full-pane {
+    flex: 1;
+    display: flex;
+    min-height: 0;
+    overflow: hidden;
+}

+ 162 - 0
web-ui.service/web/static/js/page4.js

@@ -1,5 +1,167 @@
+let gnssLogLevel = "";
+function setGnssLogLevel() {
+  const level = logLevel.value;
+
+  fetch("/api/gnss/loglevel", {
+    method: "POST",
+    headers: {"Content-Type": "application/json"},
+    body: JSON.stringify({level})
+  })
+  .then(async r => {
+    if (!r.ok) throw new Error(await r.text());
+    gnssLogLevel = level;
+  })
+  .catch(e => {
+    logLevel.value = gnssLogLevel;
+  });
+}
+
+function getGnssLogLevel() {
+  fetch("/api/gnss/loglevel")
+  .then(async r => {
+    if (!r.ok) throw new Error(await r.text());
+    return r.json();
+  })
+  .then(d => {
+    gnssLogLevel = d.log_level;
+    logLevel.value = d.log_level;
+  })
+  .catch(e => {
+    gnssLogLevel = "";
+    logLevel.value = "";
+  });
+}
+
+function downloadGnssLogFile() {
+  const btn = event.currentTarget;
+  btn.disabled = true;
+
+  fetch("/api/gnss/logfile")
+  .then(async r => {
+    if (!r.ok) {
+      throw new Error("HTTP " + r.status);
+    }
+
+    const disposition = r.headers.get("Content-Disposition");
+    let filename = "gnss.log.tar.gz";
+    if (disposition) {
+      const match = disposition.match(/filename="?([^";]+)"?/);
+      if (match) {
+        filename = match[1];
+      }
+    }
+
+    return {
+      blob: await r.blob(),
+      filename
+    };
+  })
+  .then(({ blob, filename }) => {
+    const url = URL.createObjectURL(blob);
+
+    const a = document.createElement("a");
+    a.href = url;
+    a.download = filename;
+
+    document.body.appendChild(a);
+    a.click();
+    a.remove();
+
+    setTimeout(() => {
+      URL.revokeObjectURL(url);
+    }, 1000);
+
+    alert("下载成功");
+  })
+  .catch(e => {
+    console.error("download failed:", e);
+    alert("下载失败: " + e.message);
+  })
+  .finally(() => {
+    btn.disabled = false;
+    btn.textContent = "下载";
+  });
+}
+
+function restartGnssService() {
+  if (!confirm("确定重启时间服务吗?")) {
+    return;
+  }
+
+  const btn = event.currentTarget;
+  btn.disabled = true;
+
+  fetch("/api/gnss/restart", {
+    method: "POST"
+  })
+  .then(async r => {
+    if (!r.ok) {
+      throw new Error(await r.text());
+    }
+    logLevel.value = "info";
+    gnssLogLevel = logLevel.value;
+    alert("重启成功");
+  })
+  .catch(e => {
+    console.error("restart failed:", e);
+    alert("重启失败: " + e.message);
+  })
+  .finally(() => {
+    btn.disabled = false;
+  });
+}
+
+let gnssStatusTimer = null;
+function getGnssStatus() {
+  console.log("getGnssStatus");
+  fetch("/api/gnss/status")
+    .then(async r => {
+      if (!r.ok) {
+        throw new Error("HTTP " + r.status);
+      }
+
+      return r.json();
+    })
+    .then(data => {
+      document.getElementById("var1").textContent = data.var1 || "--";
+      document.getElementById("var2").textContent = data.var2 || "--";
+      document.getElementById("var3").textContent = data.var3 || "--";
+    })
+    .catch(e => {
+      console.error("gnss status failed:", e);
+
+      document.getElementById("var1").textContent = "--";
+      document.getElementById("var2").textContent = "--";
+      document.getElementById("var3").textContent = "--";
+    });
+}
+
+function startGnssStatusRefresh() {
+  if (gnssStatusTimer) {
+    return;
+  }
+
+  getGnssStatus();
+
+  gnssStatusTimer = setInterval(() => {
+    getGnssStatus();
+  }, 5000);
+}
+
+function stopGnssStatusRefresh() {
+  if (gnssStatusTimer) {
+    clearInterval(gnssStatusTimer);
+    gnssStatusTimer = null;
+  }
+}
+
 function initPage4() {
+  startGnssStatusRefresh()
+  getGnssLogLevel();
+  connectLog("yfkj-gnss.service", "text1");
 }
 
 function exitPage4() {
+  stopGnssStatusRefresh()
+  disconnectLog();
 }

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

@@ -21,7 +21,10 @@ const pageHandlers = {
     init: () => window.initPage3?.(),
     exit: () => window.exitPage3?.()
   },
-  page4: {},
+  page4: {
+    init: () => window.initPage4?.(),
+    exit: () => window.exitPage4?.()
+  },
   page5: {},
   page6: {},
   page7: {},

+ 32 - 3
web-ui.service/web_handler.go

@@ -11,6 +11,7 @@ import (
 	"os/exec"
 	"strings"
 
+	"hnyfkj.com.cn/rtu/linux/baseapp"
 	"hnyfkj.com.cn/rtu/linux/utils/jsonrpc2"
 )
 
@@ -189,11 +190,17 @@ func serviceLogHandler(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	if true {
+		baseapp.Logger.Debugf("[服务日志流启动] unit=%s", unit)
+	}
+
 	defer func() {
 		if cmd.Process != nil {
 			cmd.Process.Kill()
 		}
 		cmd.Wait()
+
+		baseapp.Logger.Debugf("[服务日志流关闭] unit=%s", unit)
 	}()
 
 	scanner := bufio.NewScanner(stdout)
@@ -214,11 +221,17 @@ func serviceLogHandler(w http.ResponseWriter, r *http.Request) {
 }
 
 func runShellAndWrite(w http.ResponseWriter, cmd string) {
+	baseapp.Logger.Debugf("[执行命令] %s", cmd)
+
 	out, err := exec.Command("sh", "-c", cmd).CombinedOutput()
 	if err != nil {
-		http.Error(w, err.Error()+"\n"+string(out), http.StatusInternalServerError)
+		baseapp.Logger.Errorf("[命令失败] %s err=%v", cmd, err)
+
+		http.Error(w, err.Error()+"\n"+string(out),
+			http.StatusInternalServerError)
 		return
 	}
+
 	w.Write(out)
 }
 
@@ -235,8 +248,14 @@ func netDNS(w http.ResponseWriter, r *http.Request) {
 }
 
 func callRPCResult(ctx context.Context, port int, method string, params any, result any) error {
-	client, err := jsonrpc2.NewRPCClient(fmt.Sprintf("http://127.0.0.1:%d/rpc", port))
+	url := fmt.Sprintf("http://127.0.0.1:%d/rpc", port)
+
+	req, _ := json.Marshal(params)
+	baseapp.Logger.Debugf("[接收RPC请求] %s %s params=%s\n", url, method, string(req))
+
+	client, err := jsonrpc2.NewRPCClient(url)
 	if err != nil {
+		baseapp.Logger.Errorf("[执行RPC错误] %s err=%v\n", method, err)
 		return err
 	}
 
@@ -245,11 +264,18 @@ func callRPCResult(ctx context.Context, port int, method string, params any, res
 		return err
 	}
 
+	baseapp.Logger.Debugf("[发送RPC应答] %s result=%s\n", method, string(resp.Result))
+
 	return json.Unmarshal(resp.Result, result)
 }
 
 func callRPCResponse(w http.ResponseWriter, r *http.Request, port int, method string, params any) {
-	client, err := jsonrpc2.NewRPCClient(fmt.Sprintf("http://127.0.0.1:%d/rpc", port))
+	url := fmt.Sprintf("http://127.0.0.1:%d/rpc", port)
+
+	req, _ := json.Marshal(params)
+	baseapp.Logger.Debugf("[接收RPC请求] %s %s params=%s\n", url, method, string(req))
+
+	client, err := jsonrpc2.NewRPCClient(url)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -257,10 +283,13 @@ func callRPCResponse(w http.ResponseWriter, r *http.Request, port int, method st
 
 	resp, err := client.Call(r.Context(), method, params)
 	if err != nil {
+		baseapp.Logger.Errorf("[执行RPC错误] %s err=%v\n", method, err)
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
 
+	baseapp.Logger.Debugf("[发送RPC应答] %s result=%s\n", method, string(resp.Result))
+
 	w.Header().Set("Content-Type", "application/json; charset=utf-8")
 	json.NewEncoder(w).Encode(resp.Result)
 }

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

@@ -89,6 +89,26 @@ var routes = []Route{
 	},
 
 	{
+		"/api/gnss/loglevel",
+		serviceLogLevelHandler(7002),
+	},
+
+	{
+		"/api/gnss/logfile",
+		downloadGnssLog,
+	},
+
+	{
+		"/api/gnss/restart",
+		restartGnssService,
+	},
+
+	{
+		"/api/gnss/status",
+		gnssStatusHandler,
+	},
+
+	{
 		"/api/ssh/ws",
 		sshWSHandler,
 	},