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

141 lines
6.0 KiB
JavaScript
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.
// 手机适配(#14)专项检查
// 1) 静态要素:viewport、抽屉遮罩、断点规则、抽屉开关逻辑、表格包裹逻辑
// 2) 功能验证:用最小 DOM 桩跑一遍 wrapTables,确认真的包出了 .tw 且不会重复包
'use strict';
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const APP = path.join(__dirname, '..', 'web', 'app.js');
const CSS = path.join(__dirname, '..', 'web', 'app.css');
const HTML = path.join(__dirname, '..', 'web', 'index.html');
const code = fs.readFileSync(APP, 'utf8');
const css = fs.readFileSync(CSS, 'utf8');
const html = fs.readFileSync(HTML, 'utf8');
let PASS = 0, FAIL = 0;
const MSG = [];
function has(name, cond, extra) {
if (cond) PASS++;
else { FAIL++; MSG.push(' ✘ ' + name + (extra ? ' → ' + extra : '')); }
}
// ---------------- 1. 静态要素 ----------------
has('index.html 有 viewport', /<meta\s+name="viewport"/.test(html));
has('index.html 有抽屉遮罩 #m-mask', /id="m-mask"/.test(html));
has('index.html 有侧栏开关 #btn-side', /id="btn-side"/.test(html));
has('CSS 有 .tw 横向滚动容器', /\.tw\{[^}]*overflow-x:auto/.test(css));
has('CSS 有 m-mask 遮罩样式', /\.m-mask\{/.test(css));
has('CSS 有 760px 断点', /@media \(?max-width:760px\)?/.test(css));
has('CSS 抽屉滑入规则', /#app\.m-drawer \.sidebar\{[^}]*transform:translateX\(0\)/.test(css));
has('CSS 侧栏在窄屏为 fixed 抽屉',
/@media \(?max-width:760px\)?\{[\s\S]*?\.sidebar\{[^}]*position:fixed/.test(css));
has('CSS 窄屏输入框 16px(防 iOS 缩放)',
/@media \(?max-width:760px\)?\{[\s\S]*?input,select,textarea\{font-size:16px\}/.test(css));
has('CSS 有安全区适配', /env\(safe-area-inset/.test(css));
has('CSS 弹窗窄屏适配', /\.modal-box\{[^}]*margin:0 12px/.test(css));
has('JS 有 isNarrow 判定', /const isNarrow\s*=/.test(code));
has('JS 有 closeDrawer', /function closeDrawer\(/.test(code));
has('JS 切换 m-drawer', /classList\.toggle\('m-drawer'\)/.test(code));
has('JS go() 里会关抽屉', /function go\([\s\S]{0,200}?closeDrawer\(\)/.test(code));
has('JS 遮罩点击关闭', /mMask\.onclick\s*=\s*closeDrawer/.test(code));
has('JS Esc 关闭抽屉', /e\.key === 'Escape'\) closeDrawer\(\)/.test(code));
has('JS 有 wrapTables', /function wrapTables\(/.test(code));
has('JS 用 MutationObserver 兜异步表格', /new MutationObserver\(/.test(code));
has('JS 窗口变宽会清抽屉', /mq\.matches\) closeDrawer\(\)/.test(code));
// ---------------- 2. wrapTables 功能验证 ----------------
// 最小 DOM 桩:只要能表达「节点 / 父节点 / class 列表 / 插入」即可
function mk(tag, cls) {
const el = {
tag, children: [], parentNode: null,
classList: {
_s: new Set(cls ? [cls] : []),
add(c) { this._s.add(c); }, contains(c) { return this._s.has(c); },
},
insertBefore(node, ref) {
const i = this.children.indexOf(ref);
node.parentNode = this;
if (i < 0) this.children.push(node); else this.children.splice(i, 0, node);
return node;
},
appendChild(node) {
// 从原父节点摘下,再挂到新父节点
if (node.parentNode) {
const j = node.parentNode.children.indexOf(node);
if (j >= 0) node.parentNode.children.splice(j, 1);
}
node.parentNode = this;
this.children.push(node);
return node;
},
querySelectorAll(sel) {
const out = [];
(function walk(n) {
n.children.forEach(c => {
if (sel === 'table' && c.tag === 'table') out.push(c);
walk(c);
});
})(this);
return out;
},
};
// 真实 DOM 里 el.className = 'tw' 会同步反映到 classList,
// 桩必须复刻这个联动,否则 wrapTables 的「已包过」判断永远失效。
Object.defineProperty(el, 'className', {
get() { return Array.from(el.classList._s).join(' '); },
set(v) { el.classList._s = new Set(String(v).split(/\s+/).filter(Boolean)); },
});
return el;
}
const documentStub = { createElement: t => mk(t) };
const sandbox = { document: documentStub, console };
// 1.0.10 起 app.js 的文案走 i18n 的 t()。本检查器**不加载** i18n.js
// (它要 DOM + localStorage),给一个恒等兜底,保证渲染流程能跑完。
// ⛔ 刻意不加载真 i18n.js —— 那会让本检查器依赖字典,掩盖真正的渲染问题。
sandbox.t = k => k;
sandbox.i18n = {
t: k => k, setLang() {}, getLang: () => 'zh-CN', toggle() {},
onChange() {}, applyDom() {}, dict: {}, register() {}, rerenderAll() {},
};
vm.createContext(sandbox);
// 只抽取 wrapTables 函数体,避免牵扯整个 app.js
const m = code.match(/function wrapTables\(root\) \{[\s\S]*?\n\}/);
has('能抽到 wrapTables 源码', !!m);
if (m) {
vm.runInContext(m[0], sandbox);
const root = mk('div');
const holder = mk('div', 'card');
const t1 = mk('table'), t2 = mk('table');
root.appendChild(holder); holder.appendChild(t1); holder.appendChild(t2);
// 第一次:两个表都该被包上一层 .tw
sandbox.wrapTables(root);
const wrapped = holder.children.filter(c => c.classList.contains('tw'));
has('首次包裹:两个表格都套上 .tw', wrapped.length === 2, wrapped.length);
has('包裹后 table 的父节点是 .tw',
wrapped.length === 2 && wrapped.every(w => w.children.length === 1 && w.children[0].tag === 'table'));
// 第二次:幂等,不能重复套(否则每次刷新 DOM 都多一层)
sandbox.wrapTables(root);
const wrapped2 = holder.children.filter(c => c.classList.contains('tw'));
has('二次包裹:幂等不重复', wrapped2.length === 2, wrapped2.length);
has('二次包裹后结构仍是一层',
wrapped2.every(w => w.children.length === 1 && w.children[0].tag === 'table'));
// 空树不应报错
let threw = false;
try { sandbox.wrapTables(mk('div')); } catch (e) { threw = true; }
has('空容器不抛异常', !threw);
}
console.log('='.repeat(60));
console.log(`手机适配检查:通过 ${PASS}|失败 ${FAIL}`);
if (FAIL) { console.log('-'.repeat(60)); MSG.forEach(x => console.log(x)); }
process.exit(FAIL ? 1 : 0);