Files
liuzhuohua b7ca9c89e8 feat(i18n/layout): 全站中英双语 + 四轮排版修复收官 + 修 boot_id 重启检测
v1.0.10 两件大事:

1. 界面中英双语(英文界面无中文残留)
   - 新增 web/i18n.js(约 4200 条词条),零依赖零构建
   - 后端下发中文数据走 bt4() 查表翻译;独立模块(upstream/netdetail/
     realtime/update)纳入翻译并注册切语言重绘钩子
   - 后端「状态/聚合」接口只回稳定枚举+原始数值,不下发中文文案
   - 顺带修一个真实 XSS:顶层 const 不挂 window,独立脚本 esc 静默退化成零转义

2. 修掉「文字竖着排/错位」(多分辨率反复暴露)
   - 单元格 min-content 只有 1 字宽 → word-break:keep-all + 表头 nowrap
   - 依赖卡片正文被标签/按钮挤成 27px → .dep-body{flex:1 1 200px} + flex-wrap
   - 裸 flex:1(下限为0)给正文 → 一律改 flex:1 1 <px>
   - .dep-item 横向容器直接塞块级子项 → 退回块级
   - 关键陷阱:auto-fill 布局屏越宽列数越多单卡越窄,窄屏反而测不出来,
     真机审计推进到「逐视图点按钮 + 1920/2560 宽视口 + 中英双语」
   - 守卫 t-layout.py 扩到 35 条,12 处反向验证全红

3. 修真缺陷:_link_since() 重启检测被误改成 if False:(注释还写着「已修」),
   uptime 归零后相减得负数 → 恢复比对 boot_id

附带:新增 25 个检查器(t-*.py)、排版审计工具链(shot-*/ocr-*/live-*)、
版本号兜底值同步到 1.0.10、.gitignore 收纳 i18n/排版排查的一次性脚本。
2026-10-10 04:50:32 +08:00

174 lines
9.6 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="light">
<title data-i18n="common.ui.pageTitle">Drouter · Debian 13 主路由管理</title>
<link rel="icon" href="/logo.svg" type="image/svg+xml">
<link rel="stylesheet" href="/app.css">
<link rel="stylesheet" href="/update.css">
<!-- 主题之家(#12):由后端根据「当前生效主题」动态生成,覆盖 app.css 里的 CSS 变量。
文件不存在时后端返回空样式,页面自动回落到内置默认配色。 -->
<link rel="stylesheet" href="/theme.css" id="theme-css">
</head>
<body>
<!-- ===================== Logo 定义(可被各处 <use> 引用) ===================== -->
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<defs>
<!-- Drouter 标识:外圈 = 数据包环路;内部 = 路由跳转路径(三个节点 + 方向箭头)
底部横线 = 网口/接地;整体像一枚沿环流转的路由符号 -->
<symbol id="drouter-logo" viewBox="0 0 48 48">
<circle cx="24" cy="24" r="21" fill="none" stroke="currentColor" stroke-width="2.4"
stroke-linecap="round" stroke-dasharray="96 36"
transform="rotate(-42 24 24)" opacity=".55"/>
<path d="M11 31.5 20.5 22 27 27.5 37 15.5" fill="none" stroke="currentColor"
stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="11" cy="31.5" r="3.1" fill="currentColor"/>
<circle cx="20.5" cy="22" r="2.4" fill="currentColor"/>
<circle cx="27" cy="27.5" r="2.4" fill="currentColor"/>
<path d="M33.2 12.6h4.6v4.6" fill="none" stroke="currentColor"
stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 38.5h24" fill="none" stroke="currentColor" stroke-width="2.4"
stroke-linecap="round" opacity=".45"/>
</symbol>
</defs>
</svg>
<div id="login" class="login-wrap">
<div class="login-glow" aria-hidden="true"></div>
<div class="login-box">
<div class="login-logo"><svg class="lg-mark" width="56" height="56"><use href="#drouter-logo"/></svg></div>
<h1>Drouter</h1>
<p class="sub" data-i18n="common.ui.localConsole">Debian 13 主路由 · 本地管理台</p>
<label><span data-i18n="common.ui.adminUser">管理员用户名</span><input id="lg-user" autocomplete="username" value="admin"></label>
<label><span data-i18n="common.ui.password">密码</span><input id="lg-pass" type="password" autocomplete="current-password"></label>
<button id="lg-btn" class="primary" data-i18n="common.ui.login">登 录</button>
<div id="lg-msg" class="msg"></div>
<p class="hint" data-i18n="common.ui.firstHint">首次使用默认账号 admin / admin123,登录后请立即修改密码。</p>
</div>
</div>
<div id="app" class="hidden">
<aside class="sidebar" id="sidebar">
<!-- 左上角 logo 回概述页。
用 <a> 而不是 <div onclick>:Tab 能聚焦、Enter 能触发、读屏会念
「链接」。role/button + tabindex 只能骗过一部分辅助工具,语义标签
是免费的。href="#" 会被下面的 click preventDefault 吃掉。 -->
<a class="brand brand-home" href="#dash" id="brand-home" data-i18n-attr="title:common.ui.backDash" title="返回系统概览">
<svg class="brand-mark" width="30" height="30" aria-hidden="true"><use href="#drouter-logo"/></svg>
<div class="brand-txt">
<b>Drouter</b>
<span id="host-tag"></span>
</div>
</a>
<div class="nav-search">
<input id="nav-filter" data-i18n-attr="placeholder:nav.search"
placeholder="搜索菜单…" autocomplete="off">
</div>
<nav id="nav"></nav>
<div class="side-foot">
<div id="vnc-dot" class="dot-line"></div>
<!-- 左下角退出:显示当前登录账号,点一下即登出 -->
<button id="btn-logout" class="side-logout" data-i18n-attr="title:退出系统, aria-label:退出系统"
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<path d="M16 17l5-5-5-5"/><path d="M21 12H9"/>
</svg>
<span class="sl-txt" data-i18n="退出系统">退出系统</span>
</button>
</div>
</aside>
<!-- 手机端抽屉遮罩(#14):点它可关闭菜单;桌面端由 CSS 隐藏 -->
<div class="m-mask" id="m-mask" aria-hidden="true"></div>
<main>
<header class="topbar">
<div class="tb-left">
<button id="btn-side" class="icon-btn" data-i18n-attr="title:common.ui.toggleMenu, aria-label:common.ui.toggleMenuAria" title="折叠/展开菜单" aria-label="折叠或展开菜单">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
<h2 id="page-title">概览</h2>
<!-- 本页模块的状态灯(#5):由主脚本的 paintPageLight() 填。
只在这个页面确实对应一个受监控模块时才出现,其余页面留空。 -->
<span id="page-health" class="page-health" hidden></span>
</div>
<div class="tb-right">
<!-- 语言切换(1.0.10 新增)。i18n.js 挂载时会给它填当前语言文字,
点击调用 window.i18n.toggle()。开关放在刷新按钮旁边,
因为它和刷新一样是「作用于整个面板」的动作。 -->
<button id="btn-lang" class="icon-btn" data-i18n-attr="title:app.langToZh,aria-label:app.langToZh"
title="切换到中文" aria-label="切换到中文">
<span id="lang-label">中</span>
</button>
<!-- 右上角刷新:重新加载当前页面数据(旋转动画表示正在刷新) -->
<button id="btn-refresh" class="icon-btn" data-i18n-attr="title:common.ui.refreshPage, aria-label:common.ui.refreshPage" title="刷新当前页面" aria-label="刷新当前页面">
<svg id="btn-refresh-ico" width="17" height="17" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/>
</svg>
</button>
<div id="clock-box" class="clockbox" data-i18n-attr="title:common.ui.switchClock" title="点击切换 12/24 小时制">
<span id="clock-time" class="ck-time">--:--:--</span>
<span id="clock-date" class="ck-date" data-i18n="common.ui.loading">加载中…</span>
<span id="clock-lunar" class="ck-lunar"></span>
</div>
</div>
</header>
<div id="newiface-tip" class="banner hidden"></div>
<section id="view" class="view"></section>
<div id="actionbar" class="actionbar hidden">
<div id="ab-msg" class="ab-msg"></div>
<div class="ab-btns">
<button id="ab-save" class="ghost" data-i18n="common.ui.saveOnly">仅保存</button>
<button id="ab-apply" class="primary" data-i18n="common.ui.saveApply">保存并应用</button>
</div>
</div>
<footer class="site-footer">
<span data-i18n="common.ui.footer">Drouter · Debian 13 主路由管理系统</span>
<span class="sep">·</span>
<span>火麒麟 (ajeef)</span>
<span class="sep">·</span>
<a href="https://github.com/liuzhuohua" target="_blank" rel="noopener noreferrer">https://github.com/liuzhuohua</a>
<span class="sep">·</span>
<span data-i18n="common.ui.mit">遵循 MIT 开源协议</span>
<span class="sep">·</span>
<span class="site-ver" id="app-ver" data-i18n-attr="title:common.ui.curVer" title="当前安装的版本">v__APP_VERSION__</span>
</footer>
</main>
</div>
<div id="toast" class="toast hidden"></div>
<div id="modal" class="modal hidden"><div class="modal-box" id="modal-box"></div></div>
<!-- ⚠️⚠️ i18n 框架**必须排在 app.js 之前**(2026-10-05 修:顺序写反了,
整站登录失效)。app.js 的**顶层**就有 t() 调用 —— 左侧导航的
`g: t('概览')` 之类是模块加载时求值的常量,i18n.js 还没加载就
ReferenceError: t is not defined,于是 app.js 整个挂掉,
登录按钮的 onclick 根本没绑上(用户反馈「输入密码后网页无反应」)。
而下面的 update.js / upstream.js / netdetail.js / realtime.js
必须在 app.js **之后** —— 它们要用 app.js 暴露的 api()/esc()。
两组顺序要求相反,注意别再弄反。 -->
<script src="/i18n.js"></script>
<script src="/app.js"></script>
<!-- 版本检测 / 一键更新。刻意**独立于 app.js**:
app.js 是 690KB 的主面板 SPA,这个模块只服务概览页的一个卡片。
分开的好处是它可以被单独缓存/替换,而不必让整个 SPA 失效。 -->
<script src="/update.js"></script>
<!-- 上游链路详情(IPv4/IPv6 协议 + DHCPv6 统计)。独立文件:
每个请求要 fork 多个 ip/resolvectl 命令,刷新节奏也与主面板不同,
混进 app.js 会让首屏背不必要的重量。必须在 app.js 之后
—— 要用它暴露的 api() 与 __drouterEsc。 -->
<script src="/upstream.js"></script>
<!-- 网络明细(邻居表 / 路由表 / IPv6 规则)。15 秒刷新一次,理由同上。 -->
<script src="/netdetail.js"></script>
<!-- 实时柱状流动图(负载 / 带宽 / 连接)。canvas 绘制、3 秒采样,
独立成文件避免给首屏再加重量。 -->
<script src="/realtime.js"></script>
</body>
</html>