fix: 窄屏实测修复 —— 删抽屉、44px 命中区、触摸设备可见的次要动作
用 playwright 连本机共享 Chromium,在 390px(iPhone 14 Pro)与 320px (iPhone SE)量真实盒子。之前的窄屏适配是「照着规则写对」,实测发现 一个功能性 bug 加五处可用性问题。 ## 抽屉式侧栏遮挡底部导航(真 bug) 抽屉是 `fixed left-0 top-0 bottom-0 z-50`,铺满整个视口高度;底部导航没有 z-index。抽屉打开时点最左那一项「收件」,elementFromPoint 命中的是抽屉里的 SVG,不是导航按钮 —— 按钮在那里、尺寸也够、CSS 规则也没写错,只有命中测试 才能发现。 **删掉抽屉而不是给导航加 z-index**:抽屉装的六项(收件/发件/联系/用户/新建/ 管理)与底部导航完全重复,唯一独有的是退出登录。为一个按钮维护一套 fixed 层级加遮罩不划算,而它还附带「Esc 关不掉」「底层未锁滚」两个毛病。 退出登录移到「我的」页 —— 它与密码、密钥同属「账号自身」,而那页此前根本 没有退出入口。`NavToggle` 与 uiStore 的 navOpen/toggleNav/closeNav 一并删除。 ## 触摸命中区:新增 .tap 详情页那排工具按钮视觉高度只有 15-16px(实测「标记已读」48x16、「对话树」 54x16、「转发」42x16、「抄送」20x15),移动端下限是 44x44。 直接加 padding 会把本来就挤的头部撑散、320px 下换行,因此用居中的透明伪元素 扩大命中区:**视觉一像素不动**。只在 max-width:767px 生效 —— 桌面用鼠标精度 足够,而扩大后的命中区在密排工具栏里会互相重叠,点一个可能命中隔壁那个。 覆盖 MailView / ContactPanel / WorkCard / ModelScopePanel / AdminUsersPage / ComposePage / ThreadView / KeyPanel / QuotaPanel / Attachments / BackButton。 ## 看不见却按得动的按钮:新增 .reveal `opacity-0 group-hover:opacity-100` 在没有 hover 的设备上永远是 opacity:0, **但仍然接收点击** —— 实测联系人列表里 elementFromPoint 命中的就是那个看不见的 「归档」。一个看不见却按得动的破坏性按钮比没有按钮更糟:人以为点的是卡片, 实际归档了一条会话。 改为默认可见,只在 `(hover: hover) and (pointer: fine)` 时隐藏。 单看 hover 会把带触摸板的平板算进去。 ## 对话树 - 缩进随屏宽自适应:固定「每级 20px、上限 8 级」= 最多 160px,320px 屏还要 去掉 px-4 的 32px 与连接线 18px,卡片只剩 110px,发件人一行直接被 truncate 吃掉。窄屏改为每级 10px、上限 5 级 - 补返回出口:原先只有「关闭」。两者语义不同 —— 返回退出整个详情栏回到列表, 关闭只收起树、留在这封邮件上 ## 把实测脚本留进仓库 `web/test/manual/`(`npm run test:narrow` / `test:wide`),不进 npm test —— 要一个跑着的浏览器加一个活的 Gateway。 留着而不是用完即删,是因为结构性断言守不住「按钮实际多大、点下去命中谁」, 而这次最严重的 bug 恰好只有 elementFromPoint 能发现。helper 里两个函数专门 为此:tapTargets() 量 .tap 的真实命中区(伪元素尺寸,不是 boundingBox), hitTest() 验每个元素点下去是否命中自己。 ## 验证 - 窄屏 13 项 + 宽屏 5 项全通过。宽屏回归特意验了两件只该在窄屏生效的事: .tap 伪元素 content 为 none、没有返回按钮 - narrow-layout.test.mjs 从 20 条扩到 28 条,逐条钉住上面每个修复 - 无横向溢出:390px 与 320px 下 scrollWidth === clientWidth - 生产已部署
This commit is contained in:
@ -66,7 +66,7 @@ check('写信页有返回出口', compose.includes('cancelCompose') && compose.i
|
||||
// 注释里提到 md:hidden 是在解释「为什么不用它」,所以先剥掉注释再查。
|
||||
const stripComments = src =>
|
||||
src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
||||
for (const f of ['BackButton', 'NavToggle', 'NarrowOnly']) {
|
||||
for (const f of ['BackButton', 'NarrowOnly']) {
|
||||
const src = read(`../src/components/${f}.tsx`);
|
||||
const code = stripComments(src);
|
||||
check(
|
||||
@ -79,6 +79,61 @@ for (const f of ['BackButton', 'NavToggle', 'NarrowOnly']) {
|
||||
const nav = read('../src/components/NarrowNav.tsx');
|
||||
check('底部导航留了安全区内边距', nav.includes('safe-area-inset-bottom'));
|
||||
|
||||
// 5.1) 抽屉式侧栏已删。
|
||||
// 它装的六项与底部导航完全重复,唯一独有的是退出登录;代价是 z-50 的
|
||||
// fixed 层铺满视口高度,把底部导航最左那一项盖住点不到
|
||||
// (实测 elementFromPoint 命中抽屉里的 SVG)。
|
||||
const app = read('../src/App.tsx');
|
||||
check(
|
||||
'窄屏没有抽屉式侧栏(它曾遮挡底部导航)',
|
||||
!app.includes('navOpen') && !app.includes('bg-black/40')
|
||||
);
|
||||
const ui = read('../src/stores/uiStore.ts');
|
||||
check('uiStore 不再有抽屉状态', !ui.includes('navOpen') && !ui.includes('toggleNav'));
|
||||
|
||||
// 5.2) 退出登录必须还有地方可点 —— 删抽屉时它是唯一的独有入口
|
||||
const account = read('../src/components/AccountPage.tsx');
|
||||
check(
|
||||
'退出登录已移到账号页(窄屏唯一出口)',
|
||||
account.includes('logout') && account.includes('退出登录')
|
||||
);
|
||||
|
||||
// 5.3) 触摸命中区:44x44 是移动端下限,而这些按钮视觉高度只有 15-24px。
|
||||
// .tap 用居中的透明伪元素扩大命中区,视觉尺寸不变。
|
||||
const css = read('../src/index.css');
|
||||
check(
|
||||
'.tap 提供 44px 触摸命中区且只在窄屏生效',
|
||||
/\.tap::after/.test(css) && css.includes('min-width: 44px') &&
|
||||
css.includes('min-height: 44px') && /max-width:\s*767px/.test(css)
|
||||
);
|
||||
// 详情页那排工具按钮是实测最小的一组(「抄送」只有 20x15)
|
||||
const viewSrc = read('../src/components/MailView.tsx');
|
||||
for (const label of ['标记已读', '对话树', '转发']) {
|
||||
const re = new RegExp('className="tap[^"]*"[^>]*>[\\s\\S]{0,120}' + label);
|
||||
check(`详情页「${label}」有 .tap 命中区`, re.test(viewSrc));
|
||||
}
|
||||
|
||||
// 5.4) 悬停才显形的次要动作在触摸设备上必须默认可见。
|
||||
// `opacity-0 group-hover:opacity-100` 在没有 hover 的设备上永远透明,
|
||||
// 却仍然接收点击 —— 一个看不见却按得动的「归档」比没有按钮更糟。
|
||||
check(
|
||||
'.reveal 只在支持悬停的设备上隐藏',
|
||||
css.includes('.reveal') && /@media\s*\(hover:\s*hover\)\s*and\s*\(pointer:\s*fine\)/.test(css)
|
||||
);
|
||||
for (const f of ['ContactPanel', 'WorkCard']) {
|
||||
const src = read(`../src/components/${f}.tsx`);
|
||||
check(
|
||||
`${f} 用 .reveal 而非裸 opacity-0 group-hover`,
|
||||
src.includes('reveal') && !src.includes('opacity-0 group-hover:opacity-100')
|
||||
);
|
||||
}
|
||||
|
||||
// 5.5) 对话树:缩进随屏宽变,且窄屏要有返回出口。
|
||||
// 固定「每级 20px、上限 8 级」在 320px 屏上把卡片压到 110px 可用宽度。
|
||||
const thread = read('../src/components/ThreadView.tsx');
|
||||
check('对话树缩进随屏宽自适应', thread.includes('useIsNarrow') && /narrow \? 10 : 20/.test(thread));
|
||||
check('对话树窄屏有返回出口', thread.includes('<BackButton'));
|
||||
|
||||
// 6) 横向内边距在窄屏收窄(px-6 在 375px 屏上白吃 48px)
|
||||
const wide = ['MailView', 'ComposePage', 'ThreadView', 'AccountPage', 'AdminUsersPage'];
|
||||
for (const f of wide) {
|
||||
|
||||
Reference in New Issue
Block a user