Pārlūkot izejas kodu

完成page1页面版本切换功能和关停开机自动服务的前端代码

niujiuru 6 dienas atpakaļ
vecāks
revīzija
0c24968fd2

+ 160 - 18
web-ui.service/page1_handler.go

@@ -299,36 +299,36 @@ func formatLoad(v string) string {
 	return strconv.FormatFloat(f, 'f', 2, 64)
 }
 
+var services = []struct {
+	name string
+	dir  string
+	bin  string
+}{
+	{"yfkj-networkd.service", "/opt/yfkj/networkd.service", "networkd"},
+	{"yfkj-timesyncd.service", "/opt/yfkj/timesyncd.service", "timesyncd"},
+	{"yfkj-gnss.service", "/opt/yfkj/gnss.service", "gnss"},
+	{"yfkj-sshd-mqtt-bridge.service", "/opt/yfkj/sshd-mqtt-bridge.service", "sshd-mqtt-bridge"},
+	{"yfkj-camera-capture.service", "/opt/yfkj/camera-capture.service", "camera-capture"},
+	{"yfkj-app-install.service", "/opt/yfkj/app-install.service", "app-install"},
+	{"yfkj-web-ui.service", "/opt/yfkj/web-ui.service", "web-ui"},
+	{"yfkj-upgrade.service", "/opt/yfkj/upgrade.service", "upgrade"},
+}
+
 func serviceStatusHandler(w http.ResponseWriter, r *http.Request) {
 	if r.Method != http.MethodGet {
 		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
 		return
 	}
 
-	services := []struct {
-		name string
-		dir  string
-	}{
-		{"yfkj-networkd.service", "/opt/yfkj/networkd.service"},
-		{"yfkj-timesyncd.service", "/opt/yfkj/timesyncd.service"},
-		{"yfkj-gnss.service", "/opt/yfkj/gnss.service"},
-		{"yfkj-sshd-mqtt-bridge.service", "/opt/yfkj/sshd-mqtt-bridge.service"},
-		{"yfkj-camera-capture.service", "/opt/yfkj/camera-capture.service"},
-		{"yfkj-app-install.service", "/opt/yfkj/app-install.service"},
-		{"yfkj-web-ui.service", "/opt/yfkj/web-ui.service"},
-		{"yfkj-upgrade.service", "/opt/yfkj/upgrade.service"},
-	}
-
 	data := make(map[string]string, len(services)*2)
 
 	for i, s := range services {
 		n := i + 1
 
 		status := noValue
-		if out, err := exec.Command("systemctl", "is-active", s.name).Output(); err == nil {
-			if strings.TrimSpace(string(out)) == "active" {
-				status = "🟢运行中..."
-			}
+		if out, err := exec.Command("systemctl", "is-active", s.name).Output(); err == nil &&
+			strings.TrimSpace(string(out)) == "active" {
+			status = "🟢运行中..."
 		}
 
 		data[fmt.Sprintf("var%d", n)] = status
@@ -343,3 +343,145 @@ func serviceStatusHandler(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	json.NewEncoder(w).Encode(data)
 }
+
+type VersionSwitchReq struct {
+	Service  string `json:"service"`
+	Action   string `json:"action"`
+	Password string `json:"password"`
+}
+
+type VersionResp struct {
+	Status  string `json:"status"`
+	Message string `json:"message,omitempty"`
+}
+
+var versionSwitchLock sync.Mutex
+
+func serviceVersionSwitchHandler(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodPost {
+		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+		return
+	}
+
+	versionSwitchLock.Lock()
+	defer versionSwitchLock.Unlock()
+
+	var req VersionSwitchReq
+	if json.NewDecoder(r.Body).Decode(&req) != nil {
+		json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: "invalid request"})
+		return
+	}
+
+	if req.Password != "yfkj123456" {
+		json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: "输入的密码不正确"})
+		return
+	}
+
+	var cfg struct{ name, dir, bin string }
+	for _, v := range services {
+		if v.name == req.Service {
+			cfg = v
+			break
+		}
+	}
+
+	if cfg.name == "" {
+		json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: "unknown service"})
+		return
+	}
+
+	link := filepath.Join(cfg.dir, cfg.bin)
+
+	oldLink, err := os.Readlink(link)
+	if err != nil {
+		json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: "读取当前版本失败"})
+		return
+	}
+
+	current := filepath.Base(filepath.Dir(oldLink))
+	if current != "a" && current != "b" && current != "c" {
+		json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: "当前版本状态异常"})
+		return
+	}
+	target := ""
+
+	switch req.Action {
+	case "factory":
+		if current == "a" {
+			json.NewEncoder(w).Encode(VersionResp{Status: "ok", Message: "当前已是出厂版本"})
+			return
+		}
+		target = "a"
+	case "previous":
+		switch current {
+		case "b":
+			if info, err := os.Stat(filepath.Join(cfg.dir, "c", cfg.bin)); err == nil && !info.IsDir() && info.Size() > 0 && info.Mode().Perm()&0111 != 0 {
+				target = "c"
+			}
+		case "c":
+			if info, err := os.Stat(filepath.Join(cfg.dir, "b", cfg.bin)); err == nil && !info.IsDir() && info.Size() > 0 && info.Mode().Perm()&0111 != 0 {
+				target = "b"
+			}
+		}
+		if target == "" {
+			json.NewEncoder(w).Encode(VersionResp{
+				Status:  "error",
+				Message: "当前没有可回退的上一版本",
+			})
+			return
+		}
+	default:
+		json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: "invalid action"})
+		return
+	}
+
+	targetBin := filepath.Join(cfg.dir, target, cfg.bin)
+
+	info, err := os.Stat(targetBin)
+	if err != nil || info.IsDir() || info.Size() == 0 || info.Mode().Perm()&0111 == 0 {
+		json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: fmt.Sprintf("目标版本文件异常: %s", target)})
+		return
+	}
+
+	if out, err := exec.Command("systemctl", "stop", req.Service).CombinedOutput(); err != nil {
+		json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: fmt.Sprintf("停止当前服务失败: %s", out)})
+		return
+	}
+
+	tmp := filepath.Join(cfg.dir, cfg.bin+".tmp")
+	os.Remove(tmp)
+
+	if err := os.Symlink(filepath.Join(target, cfg.bin), tmp); err != nil {
+		exec.Command("systemctl", "start", req.Service).Run()
+		json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: err.Error()})
+		return
+	}
+
+	if err := os.Rename(tmp, link); err != nil {
+		os.Remove(tmp)
+		exec.Command("systemctl", "start", req.Service).Run()
+		json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: err.Error()})
+		return
+	}
+
+	if out, err := exec.Command("systemctl", "start", req.Service).CombinedOutput(); err != nil {
+		tmp := filepath.Join(cfg.dir, cfg.bin+".tmp")
+		os.Remove(tmp)
+
+		if e := os.Symlink(oldLink, tmp); e == nil {
+			if e = os.Rename(tmp, link); e != nil {
+				os.Remove(tmp)
+			}
+		}
+
+		exec.Command("systemctl", "start", req.Service).Run()
+
+		json.NewEncoder(w).Encode(VersionResp{
+			Status:  "error",
+			Message: fmt.Sprintf("目标版本启动失败,已恢复旧版本: %s", out),
+		})
+		return
+	}
+
+	json.NewEncoder(w).Encode(VersionResp{Status: "ok"})
+}

+ 2 - 2
web-ui.service/web/app.html

@@ -27,9 +27,9 @@
       <!-- 系统时间 -->
       <span id="sys-time" class="clock">--</span>
       <!-- 退出系统 -->
-      <button class="btn btn-red" title="点击退出管理页面" onclick="logout()">退出</button>
+      <button class="btn btn-red" title="点击退出管理页面" onclick="logout()">退出登录</button>
       <!-- 退出系统 -->
-      <button class="btn btn-red" title="点击重启操作系统" onclick="reboot()">重启</button>
+      <button class="btn btn-red" title="点击重启操作系统" onclick="reboot()">系统重启</button>
     </div>
   </header>
 

+ 16 - 16
web-ui.service/web/pages/page1.html

@@ -73,10 +73,10 @@
           </div>
           <span class="info-value" id="var1">--</span>
           <span class="info-value" id="var11">--</span>
-          <button class="btn" style="margin-right:32px;" onclick="switch1FVersion('factory')">
+          <button class="btn" style="margin-right:32px;" onclick="switchServiceVersion('yfkj-networkd.service', 'factory')">
             恢复出厂版本
           </button>
-          <button class="btn" onclick="switch1PVersion('previous')">
+          <button class="btn" onclick="switchServiceVersion('yfkj-networkd.service', 'previous')">
             回退上一版本
           </button>
         </div>
@@ -88,10 +88,10 @@
           </div>
           <span class="info-value" id="var2">--</span>
           <span class="info-value" id="var22">--</span>
-          <button class="btn" style="margin-right:32px;" onclick="switch2FVersion('factory')">
+          <button class="btn" style="margin-right:32px;" onclick="switchServiceVersion('yfkj-timesyncd.service', 'factory')">
             恢复出厂版本
           </button>
-          <button class="btn" onclick="switch2PVersion('previous')">
+          <button class="btn" onclick="switchServiceVersion('yfkj-timesyncd.service', 'previous')">
             回退上一版本
           </button>
         </div>
@@ -103,10 +103,10 @@
           </div>
           <span class="info-value" id="var3">--</span>
           <span class="info-value" id="var33">--</span>
-          <button class="btn" style="margin-right:32px;" onclick="switch3FVersion('factory')">
+          <button class="btn" style="margin-right:32px;" onclick="switchServiceVersion('yfkj-gnss.service', 'factory')">
             恢复出厂版本
           </button>
-          <button class="btn" onclick="switch3PVersion('previous')">
+          <button class="btn" onclick="switchServiceVersion('yfkj-gnss.service', 'previous')">
             回退上一版本
           </button>
         </div>
@@ -118,10 +118,10 @@
           </div>
           <span class="info-value" id="var4">--</span>
           <span class="info-value" id="var44">--</span>
-          <button class="btn" style="margin-right:32px;" onclick="switch4FVersion('factory')">
+          <button class="btn" style="margin-right:32px;" onclick="switchServiceVersion('yfkj-sshd-mqtt-bridge.service', 'factory')">
             恢复出厂版本
           </button>
-          <button class="btn" onclick="switch4PVersion('previous')">
+          <button class="btn" onclick="switchServiceVersion('yfkj-sshd-mqtt-bridge.service', 'previous')">
             回退上一版本
           </button>
         </div>
@@ -133,10 +133,10 @@
           </div>
           <span class="info-value" id="var5">--</span>
           <span class="info-value" id="var55">--</span>
-          <button class="btn" style="margin-right:32px;" onclick="switch5FVersion('factory')">
+          <button class="btn" style="margin-right:32px;" onclick="switchServiceVersion('yfkj-camera-capture.service', 'factory')">
             恢复出厂版本
           </button>
-          <button class="btn" onclick="switch5PVersion('previous')">
+          <button class="btn" onclick="switchServiceVersion('yfkj-camera-capture.service', 'previous')">
             回退上一版本
           </button>
         </div>
@@ -148,10 +148,10 @@
           </div>
           <span class="info-value" id="var6">--</span>
           <span class="info-value" id="var66">--</span>
-          <button class="btn" style="margin-right:32px;" onclick="switch6FVersion('factory')">
+          <button class="btn" style="margin-right:32px;" onclick="switchServiceVersion('yfkj-app-install.service', 'factory')">
             恢复出厂版本
           </button>
-          <button class="btn" onclick="switch6PVersion('previous')">
+          <button class="btn" onclick="switchServiceVersion('yfkj-app-install.service', 'previous')">
             回退上一版本
           </button>
         </div>
@@ -163,10 +163,10 @@
           </div>
           <span class="info-value" id="var7">--</span>
           <span class="info-value" id="var77">--</span>
-          <button class="btn" style="margin-right:32px;" onclick="switch7FVersion('factory')">
+          <button class="btn" style="margin-right:32px;" onclick="switchServiceVersion('yfkj-web-ui.service', 'factory')">
             恢复出厂版本
           </button>
-          <button class="btn" onclick="switch7PVersion('previous')">
+          <button class="btn" onclick="switchServiceVersion('yfkj-web-ui.service', 'previous')">
             回退上一版本
           </button>
         </div>
@@ -178,10 +178,10 @@
           </div>
           <span class="info-value" id="var8">--</span>
           <span class="info-value" id="var88">--</span>
-          <button class="btn" style="margin-right:32px;" onclick="enableAutoUpgrade">
+          <button class="btn" style="margin-right:32px;" onclick="switchAutoUpgrade('enable')">
             打开自动升级
           </button>
-          <button class="btn" onclick="disableAutoUpgrade('previous')">
+          <button class="btn" onclick="switchAutoUpgrade('disable')">
             关闭自动升级
           </button>
         </div>

+ 84 - 0
web-ui.service/web/static/js/page1.js

@@ -217,6 +217,90 @@ function resizeSysCharts(){
   diskChart?.resize();
 }
 
+const serviceNames = {
+  "yfkj-networkd.service":         "设备自动联网",
+  "yfkj-timesyncd.service":        "系统时间同步",
+  "yfkj-gnss.service":             "地理坐标定位",
+  "yfkj-camera-capture.service":   "相机图像采集",
+  "yfkj-sshd-mqtt-bridge.service": "远程运维通道",
+  "yfkj-app-install.service":      "应用安装卸载",
+  "yfkj-web-ui.service":           "统一配置管理",
+  "yfkj-upgrade.service":          "开机自动升级",
+};
+
+function switchServiceVersion(service, action)
+{
+  let name = serviceNames[service] || service;
+
+  let msg = action === "factory"
+    ? `确认将【${name}】恢复到出厂版本吗?\n\n警告: 切换过程中服务会自动重启,请勿断开电源!`
+    : `确认将【${name}】回退到上一版本吗?\n\n警告: 切换过程中服务会自动重启,请勿断开电源!`;
+
+  if(!confirm(msg)){
+    return;
+  }
+
+  let password = prompt("请输入操作密码:");
+  if(password === null){
+    return;
+  }
+  password = password.trim();
+
+  fetch("/api/service/version/switch", {
+    method:"POST",
+    headers:{
+      "Content-Type":"application/json"
+    },
+    body:JSON.stringify({
+      service:service,
+      action:action,
+      password:password
+    })
+  })
+  .then(r=>r.json())
+  .then(d=>{
+    alert(`【${name}】\n\n${d.status=="ok" ? "✅ 版本切换成功" : "❌ 版本切换失败:" + d.message}`);
+  })
+  .catch(e=>{
+    alert(`【${name}】\n\n❌ 请求失败:${e.message}`);
+  });
+}
+
+function switchAutoUpgrade(action)
+{
+  let msg = action === "enable"
+    ? "确认要打开 【开机自动升级】 吗?"
+    : "确认要关闭 【开机自动升级】 吗?";
+
+  if(!confirm(msg)){
+    return;
+  }
+
+  let password = prompt("请输入操作密码:");
+  if(password === null){
+    return;
+  }
+  password = password.trim();
+
+  fetch("/api/upgrade/on-off", {
+    method:"POST",
+    headers:{
+      "Content-Type":"application/json"
+    },
+    body:JSON.stringify({
+      action:action,
+      password:password
+    })
+  })
+  .then(r=>r.json())
+  .then(d=>{
+    alert(`【开机自动升级】\n\n${d.status=="ok" ? (action=="enable" ? "✅ 已打开" : "✅ 已关闭") : "❌ 操作失败: " + d.message}`);
+  })
+  .catch(e=>{
+    alert(`【开机自动升级】\n\n❌ 请求失败: ${e.message}`);
+  });
+}
+
 function initPage1(){
   page1Active = true;
 

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

@@ -49,6 +49,11 @@ var routes = []Route{
 	},
 
 	{
+		"/api/service/version/switch",
+		serviceVersionSwitchHandler,
+	},
+
+	{
 		"/api/network/interfaces",
 		netInterfaces,
 	},