Parcourir la source

使用WorkBuddy继续优化, 1: 支持移动端横屏; 2: 去掉不比要的兼容性代码, 使得代码更加清爽

niujiuru il y a 12 heures
Parent
commit
bbac94f456

BIN
build/web-ui


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

@@ -22,7 +22,7 @@
 
 <body>
 <!-- 内联 SVG 图标库: 不依赖任何系统字体, 所有图标由矢量代码绘制, CSS 可控色 -->
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display:none" aria-hidden="true">
+<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
   <symbol id="i-logo" viewBox="0 0 24 24"><path d="M12 21v-8"/><path d="M12 13c0-3.9 3.1-7 7-7 0 3.9-3.1 7-7 7z"/><path d="M12 13c-2.8 0-5-2.2-5-5 2.8 0 5 2.2 5 5z"/><path d="M5 21h14"/></symbol>
   <symbol id="i-chart" viewBox="0 0 24 24"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></symbol>
   <symbol id="i-globe" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></symbol>
@@ -51,7 +51,7 @@
 <div class="app">
    <header class="topbar">
     <div class="device">
-      <span class="icon-box"><svg class="icon"><use href="#i-logo" xlink:href="#i-logo"/></svg></span>
+      <span class="icon-box"><svg class="icon"><use href="#i-logo"/></svg></span>
       <div class="device-info">
         <div class="title-line">
           <span class="title">河南云飞科技RTU设备运维管理平台</span>
@@ -69,21 +69,21 @@
 
   <div class="layout">
     <aside class="sidebar">
-      <div class="menu-item" data-page="page1"><svg class="icon"><use href="#i-chart" xlink:href="#i-chart"/></svg>系统信息</div>
-      <div class="menu-item" data-page="page2"><svg class="icon"><use href="#i-globe" xlink:href="#i-globe"/></svg>网络连接</div>
-      <div class="menu-item" data-page="page3"><svg class="icon"><use href="#i-clock" xlink:href="#i-clock"/></svg>时间同步</div>
-      <div class="menu-item" data-page="page4"><svg class="icon"><use href="#i-pin" xlink:href="#i-pin"/></svg>定位服务</div>
-      <div class="menu-item" data-page="page5"><svg class="icon"><use href="#i-camera" xlink:href="#i-camera"/></svg>相机操作</div>
-      <div class="menu-item" data-page="page6"><svg class="icon"><use href="#i-box" xlink:href="#i-box"/></svg>应用管理</div>
-      <div class="menu-item" data-page="page7"><svg class="icon"><use href="#i-flask" xlink:href="#i-flask"/></svg>一键自检</div>
-      <div class="menu-item" data-page="page8"><svg class="icon"><use href="#i-term" xlink:href="#i-term"/></svg>命令终端</div>
+      <div class="menu-item" data-page="page1"><svg class="icon"><use href="#i-chart"/></svg>系统信息</div>
+      <div class="menu-item" data-page="page2"><svg class="icon"><use href="#i-globe"/></svg>网络连接</div>
+      <div class="menu-item" data-page="page3"><svg class="icon"><use href="#i-clock"/></svg>时间同步</div>
+      <div class="menu-item" data-page="page4"><svg class="icon"><use href="#i-pin"/></svg>定位服务</div>
+      <div class="menu-item" data-page="page5"><svg class="icon"><use href="#i-camera"/></svg>相机操作</div>
+      <div class="menu-item" data-page="page6"><svg class="icon"><use href="#i-box"/></svg>应用管理</div>
+      <div class="menu-item" data-page="page7"><svg class="icon"><use href="#i-flask"/></svg>一键自检</div>
+      <div class="menu-item" data-page="page8"><svg class="icon"><use href="#i-term"/></svg>命令终端</div>
 
       <div class="sidebar-actions">
         <div class="action-item" onclick="reboot()">
-          <svg class="icon"><use href="#i-reboot" xlink:href="#i-reboot"/></svg>重启设备
+          <svg class="icon"><use href="#i-reboot"/></svg>重启设备
         </div>
         <div class="action-item" onclick="logout()">
-          <svg class="icon"><use href="#i-logout" xlink:href="#i-logout"/></svg>退出登录
+          <svg class="icon"><use href="#i-logout"/></svg>退出登录
         </div>
       </div>
     </aside>
@@ -93,7 +93,7 @@
     </main>
   </div>
 </div>
-<script src="static/js/polyfills.js?v={{V}}"></script>
+<script src="static/js/util.js?v={{V}}"></script>
 <script src="static/js/echarts.min.js?v={{V}}"></script>
 <script src="static/js/xterm.js?v={{V}}"></script>
 <script src="static/js/xterm-addon-fit.js?v={{V}}"></script>

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

@@ -38,7 +38,6 @@
 
     <div id="msg"></div>
 </div>
-<script src="static/js/polyfills.js?v={{V}}"></script>
 <script src="static/js/login.js?v={{V}}"></script>
 </body>
 

+ 1 - 1
web-ui.service/web/pages/page8.html

@@ -7,7 +7,7 @@
 </head>
 
 <body>
-<div class="app">
+<div class="app app-terminal">
   <div class="card page8-card">
     <div id="ssh-terminal">
     </div>

+ 89 - 44
web-ui.service/web/static/css/app.css

@@ -238,7 +238,10 @@ body {
     display: flex;
     flex-direction: column;
     gap: 6px;
-    padding: 24px 14px;
+    padding: 22px 12px;
+    /* 兜底: 窗口高度不足时侧栏自身滚动, 不会把底部"重启/退出"裁掉 */
+    overflow-y: auto;
+    overscroll-behavior: contain;
 }
 
 .sidebar-actions {
@@ -254,7 +257,7 @@ body {
     display: flex;
     align-items: center;
     gap: 10px;
-    padding: 13px 14px;
+    padding: 13px 12px;
     border-radius: 6px;
     cursor: pointer;
     color: #94a3b8;
@@ -277,7 +280,7 @@ body {
     display: flex;
     align-items: center;
     gap: 10px;
-    padding: 13px 14px;
+    padding: 13px 12px;
     border-radius: 6px;
     cursor: pointer;
     color: #94a3b8;
@@ -449,39 +452,6 @@ p {
     color: #38bdf8;
 }
 
-/* ================= IE11 兼容兜底 ================= */
-/* 说明: \9 后缀声明仅 IE9-11 生效, 现代浏览器忽略; flex gap IE 不支持, 用相邻 margin 兜底 */
-
-.clock {
-    background: none\9;
-    color: #00e5ff\9;
-}
-
-.loading-box > * + * {
-    margin-top: 12px\9;
-}
-.device > * + * {
-    margin-left: 10px\9;
-}
-.actions > * + * {
-    margin-left: 10px\9;
-}
-.sidebar-actions > * + * {
-    margin-top: 10px\9;
-}
-.menu-item + .menu-item {
-    margin-top: 6px\9;
-}
-.action-item > * + * {
-    margin-left: 10px\9;
-}
-.menu-item > * + * {
-    margin-left: 10px\9;
-}
-.log-actions > * + * {
-    margin-left: 4px\9;
-}
-
 /* ================= 界面美化增强 ================= */
 
 /* 页面背景与卡片形成层次: 视图区更深, 卡片浮起 */
@@ -627,18 +597,26 @@ textarea:focus {
     text-overflow: ellipsis;
 }
 
-/* ================= 移动端适配 (≤768px) ================= */
-@media (max-width: 768px) {
+/* ================= 紧凑布局(手机竖屏 / 手机横屏 / 矮窗口)=================
+   触发条件只做"或"判断, 不叠加 orientation、不设宽度上限, 杜绝"两边都不命中"的盲区:
+   1. 宽度 ≤768px —— 手机竖屏、平板竖屏、窄窗口
+   2. 高度 ≤600px —— 手机横屏(典型 360~430px)、矮窗口
+      依据: 竖向侧栏(8 项菜单 + 底部 2 项操作)实测约需 541px, 加上 64px 顶栏约 605px,
+      视口高度低于 600px 时竖排侧栏必然放不下
+   横屏不再单独成一套规则 —— 横竖屏走完全相同的代码路径, 只重算宽度, 行为可预期 */
+@media (max-width: 768px), (max-height: 600px) {
     /* 顶栏: 单行紧凑布局。
        flex-shrink:0 防止内容超高时标题栏被 flex 压缩裁切(窄屏遮挡问题的根因);
        标题/IMEI 单行省略, 不再换行撑高标题栏;
        env(safe-area-inset-top) 兼容刘海屏/全面屏安全区 */
     .topbar {
         height: auto;
-        min-height: 48px;
+        min-height: 50px;
         flex-shrink: 0;
-        padding: 8px 10px;
-        padding-top: max(8px, env(safe-area-inset-top));
+        /* 内容实测 39px(标题+IMEI 两行), 上下各 5px => 恰好 50px;
+           换成固定 height 会在系统大字号时裁切文字, 所以用 min-height 定下限 */
+        padding: 5px 10px;
+        padding-top: max(5px, env(safe-area-inset-top));
         position: relative;
         z-index: 20;
     }
@@ -688,10 +666,13 @@ textarea:focus {
         font-size: 12px;
     }
 
-    /* 侧栏: 变为顶部横向滚动导航, 固定高度不参与压缩 */
+    /* 侧栏: 变为顶部横向滚动导航, 固定高度不参与压缩。
+       .layout 必须保持 overflow:hidden —— 外壳始终等于视口高度,
+       页面绝不出现"文档级滚动"(移动端地址栏收放时最容易错位的那种滚动),
+       滚动只发生在下面的 #view 里, 这是整个紧凑布局稳定性的关键 */
     .layout {
         flex-direction: column;
-        overflow: visible;
+        overflow: hidden;
     }
 
     .sidebar {
@@ -737,13 +718,77 @@ textarea:focus {
         box-shadow: none;
     }
 
-    /* 内容区: 放开固定高度, 交给整页滚动 */
+    /* 内容区: 全站唯一的滚动容器。
+       flex:1 撑满顶栏与导航之外的剩余高度; min-height:0 让它在内容超高时真的能滚动
+       (缺了它, flex 子项的自动最小高度会把外壳顶高, 又退回文档级滚动) */
     .view {
         padding: 4px;
     }
 
+    #view {
+        flex: 1;
+        min-height: 0;
+        overflow-y: auto;
+        -webkit-overflow-scrolling: touch;
+    }
+
+    /* 页面片段根容器: 高度随内容自然撑开, 不再各自套一层滚动, 避免嵌套滚动的各种怪象 */
     #view .app {
         height: auto;
+        min-height: 0;
+    }
+
+    /* page1 根容器不叫 .app 且固定 100% 高, 同样放开, 让卡片随内容撑开 */
+    #view > .page1 {
+        height: auto;
         min-height: 100%;
     }
+
+    /* 终端页例外: xterm 依赖明确的容器高度计算行列, 必须占满可视区域 */
+    #view > .app-terminal {
+        height: 100%;
+        min-height: 0;
+    }
+
+    /* iOS 聚焦输入类控件时若字号 <16px 会自动放大页面, 统一提到 16px 规避 */
+    .log-level-select {
+        font-size: 16px;
+    }
+}
+
+/* ================= 矮屏微调(手机横屏等, 高度 ≤600px)=================
+   横屏可用高度只有 360~430px, 这里只做"省空间"一件事: 压缩顶栏与导航占用的高度,
+   把空间让给内容。**滚动方式完全不变**——依旧由 #view 内部滚动,
+   与竖屏是同一套逻辑, 所以横竖屏切换不会出现"能显示但不能滚"的割裂状态 */
+@media (max-height: 600px) {
+    .topbar {
+        min-height: 44px;
+        padding-top: max(5px, env(safe-area-inset-top));
+        padding-bottom: 5px;
+        /* 横屏时刘海位于左右两侧 */
+        padding-left: max(10px, env(safe-area-inset-left));
+        padding-right: max(10px, env(safe-area-inset-right));
+    }
+
+    .device .icon-box {
+        width: 24px;
+        height: 24px;
+        border-radius: 5px;
+    }
+
+    .device .icon {
+        width: 14px;
+        height: 14px;
+    }
+
+    .sidebar {
+        padding: 4px 8px;
+        padding-left: max(8px, env(safe-area-inset-left));
+        padding-right: max(8px, env(safe-area-inset-right));
+    }
+
+    .menu-item,
+    .action-item {
+        padding: 6px 10px;
+    }
 }

+ 56 - 1
web-ui.service/web/static/css/login.css

@@ -5,7 +5,9 @@
 }
 
 body {
-    height: 100vh;
+    /* min-height 而非固定 height: 内容高于视口时(如手机横屏)可正常滚动, 顶部不会被裁切 */
+    min-height: 100vh;
+    padding: 16px 0;
     display: flex;
     justify-content: center;
     align-items: center;
@@ -121,3 +123,56 @@ input:focus {
         font-size: 19px;
     }
 }
+
+/* 手机/平板上 iOS 聚焦输入框时若字号 <16px 会自动放大页面, 提到 16px 规避 */
+@media (max-width: 768px) {
+    input {
+        font-size: 16px;
+    }
+}
+
+/* ================= 矮屏适配 (手机横屏 / 小窗口, 高度 ≤520px) ================= */
+@media (max-height: 520px) {
+    body {
+        padding: 10px 0;
+    }
+
+    .login-box {
+        padding: 20px 24px;
+    }
+
+    .logo {
+        margin-bottom: 12px;
+    }
+
+    .logo .icon {
+        width: 48px;
+        height: 48px;
+        padding: 10px;
+        border-radius: 12px;
+    }
+
+    h1 {
+        font-size: 18px;
+        margin-bottom: 6px;
+    }
+
+    .subtitle {
+        margin-bottom: 14px;
+        font-size: 13px;
+    }
+
+    input {
+        height: 40px;
+        margin-bottom: 10px;
+    }
+
+    .login-box .btn {
+        height: 40px;
+    }
+
+    #msg {
+        margin-top: 8px;
+        min-height: 18px;
+    }
+}

+ 0 - 26
web-ui.service/web/static/css/page1.css

@@ -85,32 +85,6 @@
     font-family: ui-monospace, Consolas, monospace;
 }
 
-/* ================= IE11 兼容兜底 ================= */
-/* IE 不支持 grid: 降级为等分 flex; gap 用相邻 margin 兜底 */
-.page1 .page1-grid-1 {
-    display: flex\9;
-}
-.page1 .page1-grid-2 {
-    display: flex\9;
-}
-.page1 .page1-grid-1 > * {
-    flex: 1 1 0%\9;
-    min-width: 0\9;
-}
-.page1 .page1-grid-2 > * {
-    flex: 1 1 0%\9;
-    min-width: 0\9;
-}
-.page1 .service-card {
-    flex: 2 1 0%\9;
-}
-.page1 .page1-grid-1 > * + * {
-    margin-left: 12px\9;
-}
-.page1 .page1-grid-2 > * + * {
-    margin-left: 12px\9;
-}
-
 /* ================= 移动端适配 (≤768px) ================= */
 @media (max-width: 768px) {
     .page1 {

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

@@ -81,23 +81,6 @@
     min-width: 0;
 }
 
-/* ================= IE11 兼容兜底 (flex gap → 相邻 margin) ================= */
-.page-network > * + * {
-    margin-top: 12px\9;
-}
-.page-network .status-bar > * + * {
-    margin-left: 12px\9;
-}
-.page-network .status-item > * + * {
-    margin-top: 4px\9;
-}
-.page-network .panes > * + * {
-    margin-left: 12px\9;
-}
-.page-network .left-pane > * + * {
-    margin-top: 12px\9;
-}
-
 /* ================= 移动端适配 (≤768px) ================= */
 @media (max-width: 768px) {
     .page-network .status-bar {

+ 0 - 14
web-ui.service/web/static/css/page3.css

@@ -56,20 +56,6 @@
     overflow: hidden;
 }
 
-/* ================= IE11 兼容兜底 (flex gap → 相邻 margin) ================= */
-.page-time > * + * {
-    margin-top: 12px\9;
-}
-.page-time .status-bar > * + * {
-    margin-top: 6px\9;
-}
-.page-time .status-item > * + * {
-    margin-top: 4px\9;
-}
-.page-time .status-row > * + * {
-    margin-left: 12px\9;
-}
-
 /* ================= 移动端适配 (≤768px) ================= */
 @media (max-width: 768px) {
     .page-time .status-bar,

+ 0 - 14
web-ui.service/web/static/css/page4.css

@@ -56,20 +56,6 @@
     overflow: hidden;
 }
 
-/* ================= IE11 兼容兜底 (flex gap → 相邻 margin) ================= */
-.page-gnss > * + * {
-    margin-top: 12px\9;
-}
-.page-gnss .status-bar > * + * {
-    margin-top: 6px\9;
-}
-.page-gnss .status-item > * + * {
-    margin-top: 4px\9;
-}
-.page-gnss .status-row > * + * {
-    margin-left: 12px\9;
-}
-
 /* ================= 移动端适配 (≤768px) ================= */
 @media (max-width: 768px) {
     .page-gnss .status-bar,

+ 0 - 23
web-ui.service/web/static/css/page5.css

@@ -139,29 +139,6 @@
     min-width: 0;
 }
 
-/* ================= IE11 兼容兜底 (flex gap → 相邻 margin) ================= */
-.page-camera > * + * {
-    margin-top: 12px\9;
-}
-.page-camera .page5-top > * + * {
-    margin-left: 12px\9;
-}
-.page-camera .status-bar > * + * {
-    margin-left: 12px\9;
-}
-.page-camera .status-item > * + * {
-    margin-top: 4px\9;
-}
-.page-camera .button-item > * + * {
-    margin-left: 4px\9;
-}
-.page-camera .panes > * + * {
-    margin-left: 12px\9;
-}
-.page-camera .left-pane > * + * {
-    margin-top: 12px\9;
-}
-
 /* ================= 移动端适配 (≤768px) ================= */
 @media (max-width: 768px) {
     .page-camera .status-bar {

+ 0 - 23
web-ui.service/web/static/css/page6.css

@@ -129,29 +129,6 @@
     min-width: 0;
 }
 
-/* ================= IE11 兼容兜底 (flex gap → 相邻 margin) ================= */
-.page-userapp > * + * {
-    margin-top: 12px\9;
-}
-.page-userapp .status-bar > * + * {
-    margin-left: 12px\9;
-}
-.page-userapp .status-item > * + * {
-    margin-top: 4px\9;
-}
-.page-userapp .panes > * + * {
-    margin-left: 12px\9;
-}
-.page-userapp .left-pane > * + * {
-    margin-top: 12px\9;
-}
-.page-userapp .card-body > * + * {
-    margin-top: 12px\9;
-}
-.page-userapp .app-info > * + * {
-    margin-left: 8px\9;
-}
-
 /* ================= 移动端适配 (≤768px) ================= */
 @media (max-width: 768px) {
     .page-userapp .status-bar {

+ 0 - 25
web-ui.service/web/static/css/page7.css

@@ -79,31 +79,6 @@
     color: #ef4444;
 }
 
-/* ================= IE11 兼容兜底 (grid/flex gap → flex + 相邻 margin) ================= */
-.page-health-check {
-    display: block\9;
-    overflow: auto\9;
-}
-.page-health-check > * + * {
-    margin-top: 12px\9;
-}
-.page-health-check .check-actions {
-    display: flex\9;
-}
-.page-health-check .check-actions > * + * {
-    margin-left: 8px\9;
-}
-.page-health-check .check-row {
-    display: flex\9;
-}
-.page-health-check .check-row > div {
-    flex: 1 1 0%\9;
-    min-width: 0\9;
-}
-.page-health-check .check-row > div:last-child {
-    flex: 2 1 0%\9;
-}
-
 /* ================= 移动端适配 (≤768px) ================= */
 @media (max-width: 768px) {
     .page-health-check .check-table {

+ 7 - 2
web-ui.service/web/static/js/app.js

@@ -32,6 +32,10 @@ function setIMEI(imei) {
     }
 }
 
+// IMEI 重试间隔: 失败时从 3s 起翻倍, 上限 60s。
+// 设备早期(IMEI 文件尚未生成)能快速补上, 若接口长期不可用也不会一直每 3 秒空转。
+var imeiRetryDelay = 3000;
+
 function fetchIMEI() {
     fetch("/api/device/imei")
         .then(function (res) {
@@ -45,10 +49,11 @@ function fetchIMEI() {
                 throw new Error("empty imei");
             }
             setIMEI(imei.trim());
+            imeiRetryDelay = 3000; // 成功一次即复位, 后续偶发失败仍快速重试
         })
         .catch(function (e) {
-            console.log("get imei failed, retrying...");
-            setTimeout(fetchIMEI, 3000); // 3秒后继续重试
+            setTimeout(fetchIMEI, imeiRetryDelay);
+            imeiRetryDelay = Math.min(imeiRetryDelay * 2, 60000);
         });
 }
 var sysTimeTimer = null;

+ 20 - 0
web-ui.service/web/static/js/util.js

@@ -0,0 +1,20 @@
+// 通用小工具(被多个页面共用)
+
+/**
+ * 统一文件下载入口。
+ * 现代浏览器统一走 a[download] + ObjectURL,下载完立即回收 URL。
+ * @param {Blob} blob
+ * @param {string} filename
+ */
+function downloadBlobFile(blob, filename) {
+    var url = URL.createObjectURL(blob);
+    var a = document.createElement("a");
+    a.href = url;
+    a.download = filename || "download";
+    document.body.appendChild(a);
+    a.click();
+    setTimeout(function () {
+        document.body.removeChild(a);
+        URL.revokeObjectURL(url);
+    }, 100);
+}

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

@@ -92,6 +92,11 @@ func NewWebUI(addr string) (*WebUI, error) {
 		server: &http.Server{
 			Addr:    addr,
 			Handler: gzipHandler(mux),
+			// 只读请求头阶段设时限: 移动网络掉线/半开连接不会长期占住 goroutine。
+			// 不设 ReadTimeout/WriteTimeout —— 日志流(SSE)与 SSH 终端(WebSocket)是长连接,
+			// 全局写超时会把它们按时掐断。
+			ReadHeaderTimeout: 10 * time.Second,
+			IdleTimeout:       120 * time.Second,
 		},
 	}, nil
 }