用户四封信(同一线索)提的六件事,都在这里: ## ① 导航合并(「导航栏的内容有点多了」) 收件箱/发件箱/授权 → 一项「通信」,进去由**内部页签**区分(CommTabs); 新建 → 「通信」页内**悬浮圆形加号**(ComposeFab);导航只剩 通信/日历/联系人; 管理 → 合进「我的」,管理员在页面**最下面**看得到(非管理员不渲染,不是禁用)。 `viewMode` 仍是原来那三个值(几十处调用点不用改),新增 `commTab` 记住上次用的子页签; 通信项的徽标 = 未读 + 待决策(授权信息不能因为合并而消失)。 ## ② 窄屏底部导航:悬浮玻璃(「还是固定底部延伸,没有玻璃效果,也没有悬浮」) 原来是 `border-t bg-chrome-900` 通栏贴底。改成:左右/离底各留 10px、圆角、 深色半透明 + `blur(18px)`、投影,安全区并入下外边距。 ## ③ 页面动画(「页面极度缺少动画,所有页面都是直接出现」) 在 `<html>` 上挂一个短命类触发外壳直接子面板的入场动画。**没有用 key 重挂载**: 面板是 flex 链上的一环,套 wrapper 会改掉 flex 传递(窄屏覆盖层最先坏)。 并且尊重 `prefers-reduced-motion`(关了就一点都不动)。 ## ④ 控件档透明度(「复选框和地址猜测项…他们才是真正需要拉低透明度的地方」) 新增第三档 `--bg-glass-control`(0.5/0.55):授权多选胶囊、地址建议菜单。 三档递减:正文面 0.88 > 嵌套 0.82 > 控件 0.5。 ## ⑤ 打底 vs 透明(上一封「该打底的地方透了、该透的空白处糊了」) 正文面回到 0.88(读得清优先)、空白/页面底透明**且不模糊**、面板模糊降到 10px。 ## ⑥ 圆角与玻璃**无条件**生效(「大面积缺失圆角与玻璃效果…都是硬截断」) 根因:`.app-shell` 的留缝/圆角/投影原先全写在 `html[data-bg='on']` 里 ⇒ **没开壁纸的账号**看到的是硬边不透明面板。现在几何下沉为无条件基线, 壁纸相关的加强仍叠加在上面。窄屏内容面板同样浮起来。 ## 判据 - `test/nav-merge.test.mjs` 8 条(含自检:重构前的写法必须判红)—— 改完跑老套件 254 条**全绿却一条都没测导航结构**,结构改动必须自带判据。 - `test/manual/nav-restructure-verify.mjs`:真浏览器 9 条(导航项数、页签切换真的换内容、 加号 56×56 且 `elementFromPoint` 命中自己、我的页底部管理入口在退出登录之后、 控件档 α=0.5 对正文面 α=0.88 的反向对照)。 - `test/manual/narrow-glass-verify.mjs` 6 条:悬浮几何(三边留缝 10px、圆角 14px、 缝隙处命中的是页面底 ⇒ 真的浮着)、玻璃(α=0.82 + blur18)、触摸高度最小 49px、 动画真的在跑(静止时 `animationName=none`,切换后 =pane-in)且 reduced-motion 下不动。 - `test/background.test.mjs`:把三条**过时判据**(早前那版"越透越好")按现行契约重写 —— 旧判据留着只会把我拽回错误方向。30 条全绿。 ## 顺带 窄屏探针原先只调视口、**没模拟触屏**,于是 `(hover: hover)` 仍为真, 把「悬停才显形的次要动作」误报成透明 —— 差点去"修"一个本来就对的规则。 已改成 hasTouch + isMobile + DPR 的触屏上下文,且收尾只关自己的 context (CDP 连的是共享 Chromium,`browser.close()` 会把别人的标签页一起关掉)。
121 lines
5.2 KiB
JavaScript
121 lines
5.2 KiB
JavaScript
/**
|
||
* 窄屏悬浮玻璃导航 + 视图切换动画的真实验收。
|
||
*
|
||
* 用户两条原话:
|
||
* 「窄屏布局的导航栏还是固定底部延伸,没有玻璃效果,也没有悬浮」
|
||
* 「页面极度缺少动画,所有页面都是直接出现」
|
||
*
|
||
* 判据只量**可观测的东西**:几何(留缝/圆角)、玻璃(半透明 + 模糊)、动画(真的在跑)。
|
||
* 每项都配反向对照,否则"没有动画"和"有动画但没触发"分不开。
|
||
*
|
||
* 用法:AGENTMAIL_DIST=<dist> ADMIN_USER=.. ADMIN_PW=.. node test/manual/narrow-glass-verify.mjs
|
||
*/
|
||
import { openApp } from './narrow-probe-helper.mjs';
|
||
|
||
const PHONE = { width: 390, height: 844 };
|
||
const results = [];
|
||
const record = (name, ok, note) => {
|
||
results.push({ name, ok });
|
||
console.log(` ${ok ? '通过' : '失败'} ${name}${note ? ' — ' + note : ''}`);
|
||
};
|
||
|
||
const { page, ctx } = await openApp(PHONE);
|
||
try {
|
||
await page.waitForSelector('.narrow-nav', { timeout: 25000 });
|
||
await page.waitForTimeout(600);
|
||
|
||
// ── ① 悬浮:留缝 + 圆角 ──
|
||
const geo = await page.evaluate(() => {
|
||
const nav = document.querySelector('.narrow-nav');
|
||
const r = nav.getBoundingClientRect();
|
||
const cs = getComputedStyle(nav);
|
||
const pr = getComputedStyle(document.querySelector('.narrow-shell') || nav.parentElement);
|
||
return {
|
||
left: r.left,
|
||
right: r.right,
|
||
bottom: r.bottom,
|
||
vw: window.innerWidth,
|
||
vh: window.innerHeight,
|
||
radius: cs.borderRadius,
|
||
marginBottom: cs.marginBottom,
|
||
parentPadBottom: pr.paddingBottom,
|
||
pointerAtGap: document.elementFromPoint(r.left - 4, r.top + 4)?.className?.toString().slice(0, 40) || ''
|
||
};
|
||
});
|
||
record(
|
||
'① 导航不再贴底通栏:左右留缝、离底留缝、有圆角',
|
||
geo.left >= 6 && geo.vw - geo.right >= 6 && geo.vh - geo.bottom >= 6 && parseFloat(geo.radius) >= 8,
|
||
`left=${Math.round(geo.left)} right缝=${Math.round(geo.vw - geo.right)} bottom缝=${Math.round(geo.vh - geo.bottom)} radius=${geo.radius}`
|
||
);
|
||
record(
|
||
'① 左边缝露出来的是页面底(说明它真的浮在上面)',
|
||
!/narrow-nav/.test(geo.pointerAtGap),
|
||
`缝隙处命中=${geo.pointerAtGap || '(页面底)'}`
|
||
);
|
||
|
||
// ── ② 玻璃:半透明 + 模糊 ──
|
||
const glass = await page.evaluate(() => {
|
||
const cs = getComputedStyle(document.querySelector('.narrow-nav'));
|
||
const bg = cs.backgroundColor.match(/rgba?\(([^)]+)\)/);
|
||
const a = bg ? (bg[1].split(',').length === 4 ? parseFloat(bg[1].split(',')[3]) : 1) : null;
|
||
const bf = cs.backdropFilter || cs.webkitBackdropFilter || 'none';
|
||
return { alpha: a, blur: bf };
|
||
});
|
||
record(
|
||
'② 导航是玻璃:半透明 + 背景模糊',
|
||
glass.alpha !== null && glass.alpha > 0.4 && glass.alpha < 0.98 && /blur\((\d+)/.test(glass.blur),
|
||
`α=${glass.alpha} backdrop=${glass.blur}`
|
||
);
|
||
|
||
// ── ③ 触摸目标仍然达标(悬浮不能把可点面积改小)──
|
||
const taps = await page.$$eval('.narrow-nav button', els =>
|
||
els.map(e => {
|
||
const r = e.getBoundingClientRect();
|
||
return { w: Math.round(r.width), h: Math.round(r.height), label: e.textContent.trim().slice(0, 4) };
|
||
})
|
||
);
|
||
const small = taps.filter(t => t.h < 44);
|
||
record('③ 每项触摸高度 ≥44px', taps.length >= 4 && small.length === 0,
|
||
`${taps.length} 项,最小 ${Math.min(...taps.map(t => t.h))}px`);
|
||
|
||
// ── ④ 动画:切换页签时面板真的在跑 pane-in ──
|
||
const animBefore = await page.evaluate(
|
||
() => getComputedStyle(document.querySelector('.narrow-shell > *')).animationName
|
||
);
|
||
await page.click('.narrow-nav button:has-text("日历")');
|
||
await page.waitForTimeout(60);
|
||
const during = await page.evaluate(() => {
|
||
const root = document.documentElement;
|
||
const panes = [...document.querySelectorAll('.narrow-shell > *')];
|
||
return {
|
||
hasClass: root.classList.contains('view-switch'),
|
||
names: panes.map(p => getComputedStyle(p).animationName).filter(n => n !== 'none')
|
||
};
|
||
});
|
||
record(
|
||
'④ 切视图时面板在跑入场动画(反向对照:静止时不跑)',
|
||
animBefore === 'none' && during.names.length > 0 && during.names.every(n => n === 'pane-in'),
|
||
`静止 animationName=${animBefore};切换后 class=${during.hasClass} 动画=[${during.names.join(',')}]`
|
||
);
|
||
|
||
// ── ⑤ 动画必须尊重 reduced-motion ──
|
||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||
await page.click('.narrow-nav button:has-text("联系人")');
|
||
await page.waitForTimeout(60);
|
||
const reduced = await page.evaluate(() =>
|
||
[...document.querySelectorAll('.narrow-shell > *')]
|
||
.map(p => getComputedStyle(p).animationName)
|
||
.filter(n => n !== 'none')
|
||
);
|
||
record('⑤ 系统关了动画时一点都不动', reduced.length === 0, `动画=[${reduced.join(',')}]`);
|
||
await page.emulateMedia({ reducedMotion: null });
|
||
} finally {
|
||
await page.close();
|
||
await ctx.close();
|
||
}
|
||
|
||
const failed = results.filter(r => !r.ok);
|
||
console.log(`\n 窄屏悬浮玻璃 + 动画:${results.length - failed.length}/${results.length} 通过`);
|
||
if (failed.length) console.log(' 失败:' + failed.map(f => f.name).join('; '));
|
||
process.exitCode = failed.length ? 1 : 0;
|