Переглянути джерело

完成webui时间同步page3页面的所有功能开发

niujiuru 1 місяць тому
батько
коміт
4fd7ef3518

+ 106 - 0
web-ui.service/page3_handler.go

@@ -0,0 +1,106 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"os"
+	"os/exec"
+	"strconv"
+	"strings"
+	"time"
+)
+
+func downloadTimeLog(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodGet {
+		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+		return
+	}
+
+	filename := fmt.Sprintf("time.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/timesyncd.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 restartTimeService(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-timesyncd.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 timeStatusHandler(w http.ResponseWriter, r *http.Request) {
+	var t struct {
+		SystemTime string `json:"system_time"`
+		TimeSource string `json:"time_source"`
+		TimeSynced string `json:"time_synced"`
+	}
+
+	_ = callRPCResult(r.Context(), 7001, "core.getTimeSyncStatus", nil, &t)
+
+	sync := noValue
+	if t.TimeSynced == "true" {
+		sync = "已同步"
+	}
+
+	boot, uptime := noValue, noValue
+	if b, err := os.ReadFile("/proc/uptime"); err == nil {
+		if fields := strings.Fields(string(b)); len(fields) > 0 {
+			if sec, err := strconv.ParseFloat(fields[0], 64); err == nil {
+				d := time.Duration(sec) * time.Second
+				boot = time.Now().Add(-d).Format("2006-01-02 15:04:05")
+				day := int(d.Hours() / 24)
+				if day > 0 {
+					uptime = fmt.Sprintf("%d天 %02d:%02d:%02d",
+						int(d.Hours()/24), int(d.Hours())%24, int(d.Minutes())%60, int(d.Seconds())%60)
+				} else {
+					uptime = fmt.Sprintf("%02d:%02d:%02d",
+						int(d.Hours())%24, int(d.Minutes())%60, int(d.Seconds())%60)
+				}
+			}
+		}
+	}
+
+	w.Header().Set("Content-Type", "application/json; charset=utf-8")
+	json.NewEncoder(w).Encode(map[string]string{
+		"var1": sync,
+		"var2": strings.ToUpper(t.TimeSource),
+		"var3": t.SystemTime,
+		"var4": boot,
+		"var5": uptime,
+	})
+}

+ 4 - 4
web-ui.service/web/pages/page2.html

@@ -68,7 +68,7 @@
           <div class="card-header">
             <h2>联网服务</h2>
             <div class="log-actions">
-              <select class="log-level-select" id="logLevel" title="当前联网日志级别" onchange="setNetLogLevel()">
+              <select class="log-level-select" id="logLevel" title="当前服务日志级别" onchange="setNetLogLevel()">
                 <option value="trace">trace</option>
                 <option value="debug">debug</option>
                 <option value="info" selected>info</option>
@@ -76,11 +76,11 @@
                 <option value="fatal">fatal</option>
               </select>
               <span style="margin:0 1px;"></span>
-              <button class="btn" title="复制当前联网日志" onclick="copyLog('text4')">复制</button>
+              <button class="btn" title="复制当前屏显日志" onclick="copyLog('text4')">复制</button>
               <span style="margin:0 1px;"></span>
-              <button class="btn" title="清屏当前联网日志" onclick="clearLog('text4')">清屏</button>
+              <button class="btn" title="清除当前屏显日志" onclick="clearLog('text4')">清除</button>
               <span style="margin:0 1px;"></span>
-              <button class="btn" title="下载联网日志文件" onclick="downloadNetLogFile()">下载</button>
+              <button class="btn" title="下载服务日志文件" onclick="downloadNetLogFile()">下载</button>
               <span style="margin:0 1px;"></span>
               <button class="btn" title="点击重启联网服务" onclick="restartNetService()">重启</button>
             </div>

+ 50 - 6
web-ui.service/web/pages/page3.html

@@ -11,12 +11,56 @@
 
 <body>
 <div class="app">
-  <div class="card">
-    <div class="icon">🚧</div>
-      <h1>页面3</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 class="status-row">
+        <span class="label">开机时间</span>
+        <span class="value" id="var4">--</span>
+      </div>
+      <div class="status-row">
+        <span class="label">开机时长</span>
+        <span class="value" id="var5">--</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="setTimeLogLevel()">
+              <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="downloadTimeLogFile()">下载</button>
+            <span style="margin:0 1px;"></span>
+            <button class="btn" title="点击重启时间服务" onclick="restartTimeService()">重启</button>
+          </div>
+        </div>
+        <pre class="output" id="text1">自动实时获取数据...</pre>
+      </div>
+    </div>
+  </main>
 </div>
 </body>
 

+ 80 - 7
web-ui.service/web/static/css/app.css

@@ -127,13 +127,40 @@ body{
 }
 
 /* 页风格 */
-.card{
-    background:#0f172a;
-    border:1px solid #1f2937;
-    border-radius:4px;
-    padding:20px;
-    margin-bottom:15px;
-    box-shadow:0 0 20px rgba(0,0,0,0.25);
+.card {
+    background: #0f172a;
+    border: 1px solid #1f2937;
+    border-radius: 4px;
+    padding: 12px;
+    margin-bottom: 0;
+}
+
+.card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 12px;
+}
+
+.card-header h2 {
+    font-size: 14px;
+    font-weight: 500;
+    color: #e2e8f0;
+    margin: 0;
+}
+
+.output {
+    background: #020617;
+    border: 1px solid #1f2937;
+    border-radius: 4px;
+    padding: 8px;
+    font-size: 12px;
+    font-family: ui-monospace, Consolas, monospace;
+    color: #cbd5e1;
+    overflow: auto;
+    white-space: pre-wrap;
+    word-break: break-all;
+    line-height: 1.6;
 }
 
 h1{
@@ -147,6 +174,52 @@ p{
     color:#94a3b8;
 }
 
+/* 日志区 */
+.log-card {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    min-height: 0;
+}
+
+.log-card .output {
+    flex: 1;
+    min-height: 0;
+}
+
+.log-actions {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+}
+
+.log-level-select {
+    background: #0f172a;
+    border: 1px solid rgba(148, 163, 184, 0.25);
+    border-radius: 4px;
+    color: #e5e7eb;
+    font-size: 14px;
+    padding: 6px 8px;
+    height: 30px;
+    min-width: 80px;
+    cursor: pointer;
+    outline: none;
+    -webkit-appearance: none;
+    appearance: none;
+}
+
+.log-level-select:hover {
+    border-color: rgba(148, 163, 184, 0.45);
+}
+
+.log-level-select:focus {
+    border-color: #3b82f6;
+}
+
+.log-card .output {
+    padding-bottom: 32px;
+}
+
 /* 日志色 */
 .log-trace {
     color:#38bdf8;

+ 0 - 83
web-ui.service/web/static/css/page2.css

@@ -80,86 +80,3 @@
     flex-direction: column;
     min-width: 0;
 }
-
-.log-card {
-    flex: 1;
-    display: flex;
-    flex-direction: column;
-    min-height: 0;
-}
-
-.log-card .output {
-    flex: 1;
-    min-height: 0;
-}
-
-.log-actions {
-    display: flex;
-    align-items: center;
-    gap: 10px;
-}
-
-.log-level-select {
-    background: #0f172a;
-    border: 1px solid rgba(148, 163, 184, 0.25);
-    border-radius: 4px;
-    color: #e5e7eb;
-    font-size: 14px;
-    padding: 6px 8px;
-    height: 30px;
-    min-width: 80px;
-    cursor: pointer;
-    outline: none;
-    -webkit-appearance: none;
-    appearance: none;
-}
-
-.log-level-select:hover {
-    border-color: rgba(148, 163, 184, 0.45);
-}
-
-.log-level-select:focus {
-    border-color: #3b82f6;
-}
-
-/* 卡片基础样式 */
-.card {
-    background: #0f172a;
-    border: 1px solid #1f2937;
-    border-radius: 4px;
-    padding: 12px;
-    margin-bottom: 0;
-}
-
-.card-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    margin-bottom: 12px;
-}
-
-.card-header h2 {
-    font-size: 14px;
-    font-weight: 500;
-    color: #e2e8f0;
-    margin: 0;
-}
-
-/* 输出信息区域 */
-.output {
-    background: #020617;
-    border: 1px solid #1f2937;
-    border-radius: 4px;
-    padding: 8px;
-    font-size: 12px;
-    font-family: ui-monospace, Consolas, monospace;
-    color: #cbd5e1;
-    overflow: auto;
-    white-space: pre-wrap;
-    word-break: break-all;
-    line-height: 1.6;
-}
-
-.log-card .output {
-    padding-bottom: 32px;
-}

+ 42 - 1
web-ui.service/web/static/css/page3.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;
+}

+ 165 - 0
web-ui.service/web/static/js/page3.js

@@ -1,5 +1,170 @@
+let timeLogLevel = "";
+function setTimeLogLevel() {
+  const level = logLevel.value;
+
+  fetch("/api/time/loglevel", {
+    method: "POST",
+    headers: {"Content-Type": "application/json"},
+    body: JSON.stringify({level})
+  })
+  .then(async r => {
+    if (!r.ok) throw new Error(await r.text());
+    timeLogLevel = level;
+  })
+  .catch(e => {
+    logLevel.value = timeLogLevel;
+  });
+}
+
+function getTimeLogLevel() {
+  fetch("/api/time/loglevel")
+  .then(async r => {
+    if (!r.ok) throw new Error(await r.text());
+    return r.json();
+  })
+  .then(d => {
+    timeLogLevel = d.log_level;
+    logLevel.value = d.log_level;
+  })
+  .catch(e => {
+    timeLogLevel = "";
+    logLevel.value = "";
+  });
+}
+
+function downloadTimeLogFile() {
+  const btn = event.currentTarget;
+  btn.disabled = true;
+
+  fetch("/api/time/logfile")
+  .then(async r => {
+    if (!r.ok) {
+      throw new Error("HTTP " + r.status);
+    }
+
+    const disposition = r.headers.get("Content-Disposition");
+    let filename = "time.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 restartTimeService() {
+  if (!confirm("确定重启时间服务吗?")) {
+    return;
+  }
+
+  const btn = event.currentTarget;
+  btn.disabled = true;
+
+  fetch("/api/time/restart", {
+    method: "POST"
+  })
+  .then(async r => {
+    if (!r.ok) {
+      throw new Error(await r.text());
+    }
+    logLevel.value = "info";
+    timeLogLevel = logLevel.value;
+    alert("重启成功");
+  })
+  .catch(e => {
+    console.error("restart failed:", e);
+    alert("重启失败: " + e.message);
+  })
+  .finally(() => {
+    btn.disabled = false;
+  });
+}
+
+let timeStatusTimer = null;
+function getTimeStatus() {
+  fetch("/api/time/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 || "--";
+      document.getElementById("var4").textContent = data.var4 || "--";
+      document.getElementById("var5").textContent = data.var5 || "--";
+    })
+    .catch(e => {
+      console.error("time status failed:", e);
+
+      document.getElementById("var1").textContent = "--";
+      document.getElementById("var2").textContent = "--";
+      document.getElementById("var3").textContent = "--";
+      document.getElementById("var4").textContent = "--";
+      document.getElementById("var5").textContent = "--";
+    });
+}
+
+function startTimeStatusRefresh() {
+  if (timeStatusTimer) {
+    return;
+  }
+
+  getTimeStatus();
+
+  timeStatusTimer = setInterval(() => {
+    getTimeStatus();
+  }, 1000);
+}
+
+function stopTimeStatusRefresh() {
+  if (timeStatusTimer) {
+    clearInterval(timeStatusTimer);
+    timeStatusTimer = null;
+  }
+}
+
 function initPage3() {
+  startTimeStatusRefresh()
+  getTimeLogLevel();
+  connectLog("yfkj-timesyncd.service", "text1");
 }
 
 function exitPage3() {
+  stopTimeStatusRefresh()
+  disconnectLog();
 }

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

@@ -17,7 +17,10 @@ const pageHandlers = {
     init: () => window.initPage2?.(),
     exit: () => window.exitPage2?.()
   },
-  page3: {},
+  page3: {
+    init: () => window.initPage3?.(),
+    exit: () => window.exitPage3?.()
+  },
   page4: {},
   page5: {},
   page6: {},

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

@@ -69,6 +69,26 @@ var routes = []Route{
 	},
 
 	{
+		"/api/time/loglevel",
+		serviceLogLevelHandler(7001),
+	},
+
+	{
+		"/api/time/logfile",
+		downloadTimeLog,
+	},
+
+	{
+		"/api/time/restart",
+		restartTimeService,
+	},
+
+	{
+		"/api/time/status",
+		timeStatusHandler,
+	},
+
+	{
 		"/api/ssh/ws",
 		sshWSHandler,
 	},