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

上传webui框架代码设计

niujiuru 3 hónapja
szülő
commit
c965016f7a

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

@@ -1 +1,15 @@
 package main
+
+import "log"
+
+func main() {
+	webUI, err := NewWebUI(":80")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	log.Println("Server started at http://0.0.0.0:80")
+	if err := webUI.Start(); err != nil {
+		log.Fatal(err)
+	}
+}

+ 43 - 0
web-ui.service/web/index.html

@@ -0,0 +1,43 @@
+<!doctype html>
+<html lang="zh-CN">
+
+<head>
+<meta charset="utf-8">
+<title>河南云飞科技设备管理平台</title>
+<link rel="stylesheet" href="static/css/app.css">
+</head>
+
+<body>
+<div class="app">
+   <header class="topbar">
+    <div class="device">
+      <span class="icon">🆔</span>
+      <span id="imei-value">--</span>
+    </div>
+    <div class="actions">
+      <button class="logout-btn" onclick="logout()">退出</button>
+    </div>
+  </header>
+
+  <div class="layout">
+    <aside class="sidebar">
+      <div class="menu-item" data-page="page1">📊 系统信息</div>
+      <div class="menu-item" data-page="page2">🌐 网络连接</div>
+      <div class="menu-item" data-page="page3">🕒 时间同步</div>
+      <div class="menu-item" data-page="page4">🌍 定位服务</div>
+      <div class="menu-item" data-page="page5">📷 相机操作</div>
+      <div class="menu-item" data-page="page6">📦 应用管理</div>
+      <div class="menu-item" data-page="page7">🧪 一键自检</div>
+      <div class="menu-item" data-page="page8">🖥️ 命令终端</div>
+    </aside>
+
+    <main id="view" class="view">
+    <!-- 动态加载页面 -->
+    </main>
+  </div>
+</div>
+<script src="static/js/router.js"></script>
+<script src="static/js/app.js"></script>
+</body>
+
+</html>

+ 39 - 0
web-ui.service/web/login.html

@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>河南云飞科技设备管理平台</title>
+<link rel="stylesheet" href="static/css/login.css">
+</head>
+
+<body>
+<div class="login-box">
+    <div class="logo"> 🌾 </div>
+
+    <h1>河南云飞科技设备管理平台</h1>
+    <div class="subtitle">
+        科技赋能农业,数据驱动未来
+    </div>
+
+    <form id="loginForm">
+        <input id="username"
+            type="text"
+            placeholder="账号"
+            required>
+        <input id="password"
+            type="password"
+            placeholder="密码"
+            required>
+        <button type="submit">
+            登录
+        </button>
+    </form>
+
+    <div id="msg"></div>
+</div>
+<script src="static/js/login.js"></script>
+</body>
+
+</html>

+ 18 - 0
web-ui.service/web/pages/page1.html

@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="zh-CN">
+
+<head>
+<meta charset="utf-8">
+<title>系统信息</title>
+</head>
+
+<body>
+    <div class="card">
+      <div class="icon">🚧</div>
+        <h1>页面1</h1>
+        <h1>敬请期待</h1>
+        <p class="subtitle">该功能正在开发中,敬请期待...</p>
+    </div>
+</body>
+
+</html>

+ 18 - 0
web-ui.service/web/pages/page2.html

@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="zh-CN">
+
+<head>
+<meta charset="utf-8">
+<title>网络连接</title>
+</head>
+
+<body>
+    <div class="card">
+      <div class="icon">🚧</div>
+        <h1>页面2</h1>
+        <h1>敬请期待</h1>
+        <p class="subtitle">该功能正在开发中,敬请期待...</p>
+    </div>
+</body>
+
+</html>

+ 18 - 0
web-ui.service/web/pages/page3.html

@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="zh-CN">
+
+<head>
+<meta charset="utf-8">
+<title>时间同步</title>
+</head>
+
+<body>
+    <div class="card">
+      <div class="icon">🚧</div>
+        <h1>页面3</h1>
+        <h1>敬请期待</h1>
+        <p class="subtitle">该功能正在开发中,敬请期待...</p>
+    </div>
+</body>
+
+</html>

+ 18 - 0
web-ui.service/web/pages/page4.html

@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="zh-CN">
+
+<head>
+<meta charset="utf-8">
+<title>定位服务</title>
+</head>
+
+<body>
+    <div class="card">
+      <div class="icon">🚧</div>
+        <h1>页面4</h1>
+        <h1>敬请期待</h1>
+        <p class="subtitle">该功能正在开发中,敬请期待...</p>
+    </div>
+</body>
+
+</html>

+ 18 - 0
web-ui.service/web/pages/page5.html

@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="zh-CN">
+
+<head>
+<meta charset="utf-8">
+<title>相机操作</title>
+</head>
+
+<body>
+    <div class="card">
+      <div class="icon">🚧</div>
+        <h1>页面5</h1>
+        <h1>敬请期待</h1>
+        <p class="subtitle">该功能正在开发中,敬请期待...</p>
+    </div>
+</body>
+
+</html>

+ 18 - 0
web-ui.service/web/pages/page6.html

@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="zh-CN">
+
+<head>
+<meta charset="utf-8">
+<title>应用管理</title>
+</head>
+
+<body>
+    <div class="card">
+      <div class="icon">🚧</div>
+        <h1>页面6</h1>
+        <h1>敬请期待</h1>
+        <p class="subtitle">该功能正在开发中,敬请期待...</p>
+    </div>
+</body>
+
+</html>

+ 18 - 0
web-ui.service/web/pages/page7.html

@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="zh-CN">
+
+<head>
+<meta charset="utf-8">
+<title>一键自检</title>
+</head>
+
+<body>
+    <div class="card">
+      <div class="icon">🚧</div>
+        <h1>页面7</h1>
+        <h1>敬请期待</h1>
+        <p class="subtitle">该功能正在开发中,敬请期待...</p>
+    </div>
+</body>
+
+</html>

+ 18 - 0
web-ui.service/web/pages/page8.html

@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="zh-CN">
+
+<head>
+<meta charset="utf-8">
+<title>命令终端</title>
+</head>
+
+<body>
+    <div class="card">
+      <div class="icon">🚧</div>
+        <h1>页面8</h1>
+        <h1>敬请期待</h1>
+        <p class="subtitle">该功能正在开发中,敬请期待...</p>
+    </div>
+</body>
+
+</html>

+ 154 - 0
web-ui.service/web/static/css/app.css

@@ -0,0 +1,154 @@
+*{
+    margin:0;
+    padding:0;
+    box-sizing:border-box;
+}
+
+body{
+    height:100vh;
+    font-family:"Segoe UI","Microsoft YaHei",sans-serif;
+    background:#0b1220;
+    color:#e2e8f0;
+}
+
+/* 主布局 */
+.app{
+    display:flex;
+    flex-direction:column;
+    height:100vh;
+}
+
+/* 标题栏 */
+.topbar{
+    height:52px;
+    display:flex;
+    justify-content:space-between;
+    align-items:center;
+    padding:0 16px;
+
+    background:linear-gradient(180deg,#1f2937,#111827);
+    border-bottom:1px solid rgba(148,163,184,0.18);
+
+    color:#e2e8f0;
+    font-size:14px;
+    letter-spacing:0.5px;
+    font-weight:500;
+
+    box-shadow:0 1px 0 rgba(0,0,0,0.4);
+}
+
+.device{
+    font-family: ui-monospace, monospace;
+    display:flex;
+    align-items:center;
+    gap:8px;
+    font-size:15px;
+    font-weight:500
+}
+
+.device .icon{
+    display:inline-block;
+    transform: translate(2px, -1px);
+}
+
+.actions{
+    display:flex;
+    align-items:center;
+    gap:10px;
+}
+
+.logout-btn{
+    background:rgba(239,68,68,0.08);
+    border:1px solid rgba(239,68,68,0.35);
+    color:#f87171;
+    padding:6px 12px;
+    display:flex;
+    border-radius:6px;
+    font-size:12px;
+    cursor:pointer;
+    transition:0.2s;
+}
+
+.logout-btn:hover{
+    background:#ef4444;
+    border-color:#ef4444;
+    color:#fff;
+}
+
+/* 次布局 */
+.layout{
+    display:flex;
+    flex:1;
+    overflow:hidden;
+}
+
+/* 左侧栏 */
+.sidebar{
+    width:240px;
+    height:100%;
+
+    background:#0f172a;
+    border-right:1px solid rgba(148,163,184,0.12);
+
+    display:flex;
+    flex-direction:column;
+}
+
+.menu{
+    padding:10px;
+    display:flex;
+    flex-direction:column;
+    gap:6px;
+}
+
+.menu-item{
+    display:flex;
+    align-items:center;
+    gap:10px;
+    padding:14px 16px;
+    border-radius:10px;
+    cursor:pointer;
+    color:#94a3b8;
+    font-size:15px;
+    font-weight:500;
+    transition:0.2s;
+}
+
+.menu-item:hover{
+    background:#1e293b;
+    color:white;
+}
+
+.menu-item.active{
+    background:#2563eb;
+    color:white;
+}
+
+/* 页容器 */
+.view{
+    flex:1;
+    padding:20px;
+    overflow:auto;
+    background:#0f172a;
+}
+
+/* 页风格 */
+.card{
+    background:#0f172a;
+    border:1px solid #1f2937;
+    border-radius:10px;
+    padding:20px;
+    margin-bottom:15px;
+    box-shadow:0 0 20px rgba(0,0,0,0.25);
+}
+
+h1{
+    font-size:20px;
+    margin-bottom:10px;
+    color:#e2e8f0;
+}
+
+p{
+    font-size:15px;
+    color:#94a3b8;
+}

+ 82 - 0
web-ui.service/web/static/css/login.css

@@ -0,0 +1,82 @@
+*{
+    margin:0;
+    padding:0;
+    box-sizing:border-box;
+}
+
+body{
+    height:100vh;
+    display:flex;
+    justify-content:center;
+    align-items:center;
+    background:#0f172a;
+    font-family:
+        "Segoe UI",
+        "Microsoft YaHei",
+        sans-serif;
+}
+
+.login-box{
+    width:420px;
+    background:#1e293b;
+    border-radius:12px;
+    padding:40px;
+    box-shadow:
+        0 0 30px rgba(0,0,0,0.4);
+}
+
+.logo{
+    text-align:center;
+    font-size:48px;
+    margin-bottom:15px;
+}
+
+h1{
+    color:white;
+    text-align:center;
+    font-size:26px;
+    margin-bottom:10px;
+}
+
+.subtitle{
+    text-align:center;
+    color:#94a3b8;
+    margin-bottom:30px;
+}
+
+input{
+    width:100%;
+    height:45px;
+    margin-bottom:15px;
+    border:none;
+    border-radius:8px;
+    padding-left:15px;
+    background:#334155;
+    color:white;
+    font-size:15px;
+}
+
+input:focus{
+    outline:none;
+}
+
+button{
+    width:100%;
+    height:45px;
+    border:none;
+    border-radius:8px;
+    cursor:pointer;
+    font-size:16px;
+    background:#2563eb;
+    color:white;
+}
+
+button:hover{
+    opacity:0.9;
+}
+
+#msg{
+    margin-top:15px;
+    text-align:center;
+    color:#ef4444;
+}

+ 34 - 0
web-ui.service/web/static/js/app.js

@@ -0,0 +1,34 @@
+document.addEventListener("DOMContentLoaded", () => {
+  const items = document.querySelectorAll(".menu-item");
+
+  function setActive(name) {
+    items.forEach(i => {
+      i.classList.toggle("active", i.dataset.page === name);
+    });
+  }
+
+  function bindMenu() {
+    items.forEach(item => {
+      item.addEventListener("click", () => {
+        const page = item.dataset.page;
+        setActive(page);
+        loadPage(page);
+      });
+    });
+  }
+
+  bindMenu();
+  setActive("page1");
+  loadPage("page1");
+});
+
+function setIMEI(imei) {
+  const el = document.getElementById("imei-value");
+  if (el) {
+    el.textContent = imei;
+  }
+}
+
+function logout() {
+  window.location.href = "/logout";
+}

+ 36 - 0
web-ui.service/web/static/js/login.js

@@ -0,0 +1,36 @@
+function initLogin() {
+    const form = document.getElementById("loginForm");
+    form.addEventListener("submit", loginHandler);
+}
+
+function showMsg(text, isError = true) {
+    const msg = document.getElementById("msg");
+    msg.innerText = text;
+    msg.style.color = isError ? "red" : "green";
+}
+
+async function loginHandler(e) {
+    e.preventDefault();
+
+    const username = document.getElementById("username")?.value || "";
+    const password = document.getElementById("password")?.value || "";
+
+    const res = await fetch("/login", {
+        method: "POST",
+        headers: {
+            "Content-Type": "application/json"
+        },
+        credentials: "include",
+        body: JSON.stringify({ username, password })
+    });
+
+    const data = await res.json();
+
+    if (data.success) {
+        window.location.href = "/";
+    } else {
+        showMsg("用户名或密码错误");
+    }
+}
+
+initLogin();

+ 38 - 0
web-ui.service/web/static/js/router.js

@@ -0,0 +1,38 @@
+const routes = {
+  page1: "pages/page1.html",
+  page2: "pages/page2.html",
+  page3: "pages/page3.html",
+  page4: "pages/page4.html",
+  page5: "pages/page5.html",
+  page6: "pages/page6.html",
+  page7: "pages/page7.html",
+  page8: "pages/page8.html",
+};
+
+async function loadPage(name){
+  const view = document.getElementById("view");
+
+  const url = routes[name];
+
+  if(!url){
+    view.innerHTML = "<div class='card'>无法找到页面</div>";
+    return;
+  }
+
+  try{
+    const res = await fetch(url, {
+      credentials: "include"
+    });
+
+    const html = await res.text();
+
+    if (html.includes("login.html") || html.includes("登录")) {
+      window.location.href = "/";
+      return;
+    }
+
+    view.innerHTML = html;
+  }catch(e){
+    view.innerHTML = "<div class='card'>页面加载失败</div>";
+  }
+}

+ 110 - 0
web-ui.service/web_handlers.go

@@ -0,0 +1,110 @@
+package main
+
+import (
+	"encoding/json"
+	"io/fs"
+	"net/http"
+	"strings"
+)
+
+type LoginReq struct {
+	Username string `json:"username"`
+	Password string `json:"password"`
+}
+
+type LoginResp struct {
+	Success bool `json:"success"`
+}
+
+func loginHandler(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodPost {
+		w.WriteHeader(http.StatusMethodNotAllowed)
+		return
+	}
+
+	var req LoginReq
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		w.WriteHeader(http.StatusBadRequest)
+		return
+	}
+
+	ok := req.Username == "admin" && req.Password == "admin123456"
+
+	if ok {
+		sessionID := createSession()
+		addSession(sessionID)
+
+		http.SetCookie(w, &http.Cookie{
+			Name:     "session_id",
+			Value:    sessionID,
+			Path:     "/",
+			HttpOnly: true,
+			SameSite: http.SameSiteStrictMode,
+		})
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	json.NewEncoder(w).Encode(LoginResp{Success: ok})
+}
+
+func logoutHandler(w http.ResponseWriter, r *http.Request) {
+	if cookie, err := r.Cookie("session_id"); err == nil {
+		sessionMu.Lock()
+		delete(sessions, cookie.Value)
+		sessionMu.Unlock()
+	}
+
+	http.SetCookie(w, &http.Cookie{
+		Name:   "session_id",
+		Value:  "",
+		Path:   "/",
+		MaxAge: -1,
+	})
+
+	http.Redirect(w, r, "/", http.StatusFound)
+}
+
+func (ui *WebUI) rootHandler(w http.ResponseWriter, r *http.Request) {
+	path := r.URL.Path
+
+	w.Header().Set("Content-Type", "text/html; charset=utf-8")
+	w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
+	w.Header().Set("Pragma", "no-cache")
+	w.Header().Set("Expires", "0")
+
+	// 1, 未登录时, 只能看登录页
+	if !checkSession(r) {
+		data, err := fs.ReadFile(ui.fs, "login.html")
+		if err != nil {
+			http.NotFound(w, r)
+		} else {
+			w.Write(data)
+		}
+		return
+	}
+
+	// 2, 登录成功, 展示导航首页
+	if path == "/" {
+		data, err := fs.ReadFile(ui.fs, "index.html")
+		if err != nil {
+			http.NotFound(w, r)
+		} else {
+			w.Write(data)
+		}
+		return
+	}
+
+	// 3, 在网站内跳转其它功能页
+	if strings.HasPrefix(path, "/pages/") { // path 形如 "/pages/page1.html"
+		data, err := fs.ReadFile(ui.fs, path[1:])
+		if err != nil {
+			http.NotFound(w, r)
+		} else {
+			w.Write(data)
+		}
+		return
+	}
+
+	// 4, 访问其它不认识的页面时
+	http.NotFound(w, r)
+}

+ 77 - 0
web-ui.service/web_session.go

@@ -0,0 +1,77 @@
+package main
+
+import (
+	"crypto/rand"
+	"encoding/hex"
+	"net/http"
+	"sync"
+	"time"
+)
+
+type sessionInfo struct {
+	last time.Time
+}
+
+var (
+	sessionMu sync.Mutex
+	sessions  = make(map[string]*sessionInfo)
+)
+
+// 默认5分钟无任何页面点击操作时, 超时
+const idleTimeout = 5 * time.Minute
+
+func createSession() string {
+	buf := make([]byte, 32)
+	rand.Read(buf)
+	return hex.EncodeToString(buf)
+}
+
+func addSession(id string) {
+	sessionMu.Lock()
+	sessions[id] = &sessionInfo{last: time.Now()}
+	sessionMu.Unlock()
+}
+
+func hasSession(id string) bool {
+	now := time.Now()
+
+	sessionMu.Lock()
+	defer sessionMu.Unlock()
+
+	s, ok := sessions[id]
+	if !ok {
+		return false
+	}
+
+	if now.Sub(s.last) > idleTimeout {
+		delete(sessions, id)
+		return false
+	}
+
+	s.last = now
+	return true
+}
+
+func checkSession(r *http.Request) bool {
+	c, err := r.Cookie("session_id")
+	if err != nil {
+		return false
+	}
+	return hasSession(c.Value)
+}
+
+func sessionCleaner() {
+	ticker := time.NewTicker(3 * time.Minute)
+	defer ticker.Stop()
+
+	for range ticker.C {
+		now := time.Now()
+		sessionMu.Lock()
+		for k, s := range sessions {
+			if now.Sub(s.last) > idleTimeout {
+				delete(sessions, k)
+			}
+		}
+		sessionMu.Unlock()
+	}
+}

+ 40 - 0
web-ui.service/web_ui.go

@@ -0,0 +1,40 @@
+package main
+
+import (
+	"embed"
+	"io/fs"
+	"net/http"
+)
+
+//go:embed web/*
+var webRootFS embed.FS
+
+type WebUI struct {
+	fs   fs.FS
+	addr string
+	mux  *http.ServeMux
+}
+
+func NewWebUI(addr string) (*WebUI, error) {
+	sub, err := fs.Sub(webRootFS, "web")
+	if err != nil {
+		return nil, err
+	}
+
+	mux := http.NewServeMux()
+
+	fs := http.FileServer(http.FS(sub))
+	mux.Handle("/static/css/", fs) //////////// 页面样式
+	mux.Handle("/static/js/", fs)  //////////// 页面脚本
+
+	mux.HandleFunc("/login", loginHandler)   // 登录接口
+	mux.HandleFunc("/logout", logoutHandler) // 注销接口
+
+	return &WebUI{fs: sub, addr: addr, mux: mux}, nil
+}
+
+func (ui *WebUI) Start() error {
+	go sessionCleaner()
+	ui.mux.HandleFunc("/", ui.rootHandler)
+	return http.ListenAndServe(ui.addr, ui.mux)
+}