Files
drouter/devtools/check-mobile.js
T
liuzhuohua 64df3cf331 feat: Drouter 1.0.0 —— Debian 13 软路由管理系统
一套跑在 Debian 13 (trixie) 上的软路由管理系统,提供 43 个功能页面的
Web 管理面板:WAN 接入、PPPoE 多拨、DHCP/DNS、IPv6、防火墙、QoS 限速、
访问控制、DDNS、文件共享、Docker 管理等。

核心设计原则:不重新实现任何协议
  PPPoE 交给 ppp,DHCP 交给 dnsmasq,IPv6 RA 交给 radvd,NAT 交给 nftables。
  Web 面板只做三件事:渲染原生配置文件 → 语法预检 → 原子写入并 reload。
  面板删掉后,系统仍是一台配置完好的 Debian 路由器。

安全默认值
  · 安装后不接管网络:不改默认路由、不开 IP 转发、不启动 DHCP 服务
  · 所有改网络的动作都需要用户在面板上亲手点「应用」
  · 避开 5900 端口,不触碰 VNC / X 会话
  · 配置写入前先做语法预检,失败则回滚

架构
  · backend/  Python 3 标准库,零第三方依赖(9 个模块)
  · web/      原生 JS/CSS,无构建步骤
  · 常驻 helper 通过 Unix socket 提供 75 个动作,socket 不可用时回退 subprocess
  · 鉴权走 X-Token 请求头

打包
  · packaging/build-deb.sh            → drouter_1.0.0_all.deb(含 6 个 systemd 单元)
  · scripts/make-offline-deps.sh      → 236 个依赖 .deb + 本地仓库索引
  · packaging/build-offline-bundle.sh → 自包含离线包,目标机零外网可装
  · packaging/build-docker.sh         → 容器镜像(体验用)

测试
  40 套单元测试 + 3 个前端检查脚本(菜单映射 / 离线渲染 / 手机适配),
  全部挂在 devtools/sync.sh 的预检链里。

许可:MIT
2026-09-30 19:26:24 +08:00

133 lines
5.6 KiB
JavaScript
Raw 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 };
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);