fix(mobile): 修移动端顶栏塌陷,窄屏默认卡片视图

问题(用户报告移动端异常):窄屏下顶栏统计挤成竖排并与标题重叠,
表格在手机上必须横向拖动,固定装饰在小屏上可能溢出或压住内容。

修复:
- 顶栏结构拆为「标题 / 统计(.stats) / 控件(.controls)」三段;
  ≤820px 改为纵向三行,统计均分视口宽度,页脚改纵向堆叠
- ≤700px 且用户未显式选择视图时自动用卡片(横竖屏切换时重算,
  显式选择仍尊重 localStorage)
- ≤820px(及横屏手机)撤掉立绘、公路车、光环水印等固定装饰

验证:
- 390x844 / 360x640 / 414x896 / 844x390 四种视口:0 横向溢出,顶栏三行
- verify-views 新增移动端 7 条断言(22/22 通过)
- 四套合计 81 项全通过(17+22+20+22)
This commit is contained in:
tamakyi
2026-10-13 20:38:39 +08:00
parent 5b417c84f4
commit f0e5d91171
15 changed files with 523 additions and 442 deletions
+37
View File
@@ -138,6 +138,43 @@ async function main() {
fs.writeFileSync(CONFIG, raw); // 还原
}
// ---------- H. 移动端(窄屏) ----------
console.log('\n[H] 移动端窄屏');
const mCtx = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, deviceScaleFactor: 2 });
const mp = await mCtx.newPage();
const mErrors = [];
mp.on('pageerror', (e) => mErrors.push(e.message));
// 不带 ?view=:窄屏下应自动落到卡片视图(表格在手机上必须横向拖动)
await mp.goto(`${BASE}/?source=api`, { waitUntil: 'domcontentloaded' });
await mp.waitForSelector('.card', { timeout: 20000 });
await mp.waitForTimeout(1500);
const mob = await mp.evaluate(() => {
const top = (sel) => { const el = document.querySelector(sel); return el ? Math.round(el.getBoundingClientRect().top) : -1; };
const statsW = Math.round(document.querySelector('.stats')?.getBoundingClientRect().width ?? 0);
const lastFooter = [...document.querySelectorAll('.footer-item')].pop();
return {
view: document.body.dataset.view,
overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth,
brandTop: top('.brand'), statsTop: top('.stats'), controlsTop: top('.controls'),
statsW, vw: window.innerWidth,
artwork: getComputedStyle(document.querySelector('.artwork') ?? document.body).display,
beforeDisplay: getComputedStyle(document.body, '::before').display,
afterDisplay: getComputedStyle(document.body, '::after').display,
footerRight: lastFooter ? Math.round(lastFooter.getBoundingClientRect().right) : -1,
cards: document.querySelectorAll('.card').length,
tabsWrapped: getComputedStyle(document.querySelector('.topbar')).flexDirection,
};
});
check('窄屏默认使用卡片视图', mob.view === 'cards', `data-view=${mob.view}`);
check('窄屏无横向溢出', mob.overflow <= 0, `溢出 ${mob.overflow}px`);
check('顶栏在窄屏改为纵向三行(标题/统计/控件)', mob.brandTop < mob.statsTop && mob.statsTop < mob.controlsTop && mob.tabsWrapped === 'column', `top: ${mob.brandTop}/${mob.statsTop}/${mob.controlsTop}, flex=${mob.tabsWrapped}`);
check('统计行铺满视口宽度', Math.abs(mob.statsW - mob.vw) <= 32, `stats=${mob.statsW} 视口=${mob.vw}`);
check('窄屏撤掉立绘与固定装饰', mob.artwork === 'none' && mob.beforeDisplay === 'none' && mob.afterDisplay === 'none', `立绘=${mob.artwork} 左装饰=${mob.beforeDisplay} 光环水印=${mob.afterDisplay}`);
check('页脚内容不被裁切', mob.footerRight > 0 && mob.footerRight <= mob.vw, `最后一项右边界 ${mob.footerRight} / 视口 ${mob.vw}`);
check('窄屏渲染无 JS 错误', mErrors.length === 0, mErrors.join(' | ') || '无');
await mp.screenshot({ path: path.join(__dirname, '..', 'docs', 'screenshot-mobile.png'), fullPage: true });
await mCtx.close();
// 429 是刷新限流保护在起作用(快速连续导航各触发一次强制探测),面板已优雅降级为只读数据;
// 这里只把 429 以外的错误视为失败
const hardErrors = errors.filter((e) => !/429/.test(e));
+6 -4
View File
@@ -107,12 +107,14 @@ async function main() {
const pageG = await browser.newPage({ viewport: { width: 1280, height: 900 } });
await pageG.goto(`${BASE}/?source=api&view=table`, { waitUntil: 'domcontentloaded' });
await pageG.waitForSelector('table.monitor-table tbody tr[data-status]', { timeout: 20000 });
await pageG.waitForTimeout(1200);
// 等首轮刷新结束(倒计时在刷新完成时才重置),否则读到的是加载中的旧值,偏差会被算进断言
await pageG.waitForFunction(() => document.getElementById('refresh-btn').textContent === '刷新' && !document.getElementById('refresh-btn').disabled, null, { timeout: 45000 });
const shownText = await pageG.evaluate(() => document.getElementById('countdown').textContent);
const shownSec = Number((shownText.match(/(\d+)/) ?? [])[1]);
// 期望值需在当前时刻重算:面板显示的是「服务端上次更新 + 一个周期 - 现在」
const expectSec = Math.round((updatedMs + cadence * 1000 - Date.now()) / 1000);
check('倒计时对齐服务端下次更新时刻', Math.abs(shownSec - expectSec) <= 4, `面板 ${shownText}(服务端还有约 ${expectSec}s,节奏 ${cadence}s)`);
const shownSec = Number((shownText.match(/(\d+)/) ?? [])[1]);
check('倒计时对齐服务端下次更新时刻', shownSec >= expectSec - 2 && shownSec <= expectSec + 4, `面板 ${shownText}(服务端还有约 ${expectSec}s,节奏 ${cadence}s)`);
// 注:无头浏览器会冻结页面定时器(实测 performance.now() 会在采样间隔内跳跃十几秒),
// 因此不断言「N 秒内递减」,只断言与公式一致、且不退化为整周期重计——这两条不依赖定时器
check('倒计时不会退化为从整周期重新计时', shownSec <= Math.max(5, Math.min(cadence, Number(cfg.settings.refreshInterval) || cadence)), `显示 ${shownSec}s`);
await pageG.close();