feat(webui): 导航合并 + 悬浮玻璃 + 页面动画 + 控件档透明度 + 圆角无条件生效
用户四封信(同一线索)提的六件事,都在这里: ## ① 导航合并(「导航栏的内容有点多了」) 收件箱/发件箱/授权 → 一项「通信」,进去由**内部页签**区分(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()` 会把别人的标签页一起关掉)。
This commit is contained in:
108
client/electron/test/nav-merge.test.mjs
Normal file
108
client/electron/test/nav-merge.test.mjs
Normal file
@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 导航重构的判据(2026-09-14 用户三条要求)。
|
||||
*
|
||||
* 用户原话:
|
||||
* ①「导航栏的内容有点多了,收件发件授权改为一个导航项,通过内部导航区分,
|
||||
* 然后新建作为他们内部的一个悬浮的圆形加号,这样导航项就只剩通信,日历,联系人」
|
||||
* ②「将管理和我的合并,管理员视角我的页面拉到最下面有一个管理」
|
||||
* ③「复选框和地址猜测项的透明度问题还没改,他们才是真正需要拉低透明度的地方」
|
||||
*
|
||||
* 为什么写文件级判据:改完跑老套件 254 条**全绿**,因为它一条都没测导航结构 ——
|
||||
* 我差点把"没红的测试"当成"改对了"。结构类改动必须自己带判据。
|
||||
*/
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const read = (...p) => readFileSync(join(HERE, '..', 'src', ...p), 'utf8');
|
||||
|
||||
const sidebar = read('components', 'Sidebar.tsx');
|
||||
const narrow = read('components', 'NarrowNav.tsx');
|
||||
const commTabs = read('components', 'CommTabs.tsx');
|
||||
const app = read('App.tsx');
|
||||
const account = read('components', 'AccountPage.tsx');
|
||||
const uiStore = read('stores', 'uiStore.ts');
|
||||
const css = read('index.css');
|
||||
const mailView = read('components', 'MailView.tsx');
|
||||
const addr = read('components', 'AddressInput.tsx');
|
||||
|
||||
test('① 导航只剩 通信/日历/联系人 三项(桌面与窄屏都要)', () => {
|
||||
for (const [name, src] of [['Sidebar', sidebar], ['NarrowNav', narrow]]) {
|
||||
// 三项都在
|
||||
assert.match(src, /短:\s*'通信'|short:\s*'通信'/, `${name} 缺「通信」`);
|
||||
assert.match(src, /'日历'/, `${name} 缺「日历」`);
|
||||
assert.match(src, /'联系人'|'联系'/, `${name} 缺「联系人」`);
|
||||
// 旧的独立项必须消失
|
||||
for (const gone of ['收件', '发件', '授权请求', '用户管理']) {
|
||||
assert.ok(!src.includes(`'${gone}'`), `${name} 里还留着旧的独立导航项「${gone}」`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('① 「通信」的选中态覆盖三个子页签,点击回到上次那个', () => {
|
||||
assert.match(sidebar, /modes:\s*\['inbox',\s*'sent',\s*'permissions'\]/);
|
||||
assert.match(narrow, /modes:\s*\['inbox',\s*'sent',\s*'permissions'\]/);
|
||||
assert.match(sidebar, /setViewMode\(target \|\| commTab/, '桌面点通信要回 commTab');
|
||||
assert.match(narrow, /setViewMode\(isComm \? commTab : mode\)/, '窄屏点通信要回 commTab');
|
||||
});
|
||||
|
||||
test('① 内部导航(CommTabs)存在、挂在通信页、且带徽标', () => {
|
||||
assert.match(commTabs, /data-testid="comm-tabs"/);
|
||||
assert.match(commTabs, /'收件箱'[\s\S]*'发件箱'[\s\S]*'授权'/, '三个内部页签');
|
||||
assert.match(commTabs, /pendingPerms/, '授权徽标不能因为合并而消失');
|
||||
assert.match(app, /<CommTabs \/>/, 'App 里要渲染内部导航');
|
||||
assert.match(app, /const isComm = viewMode === 'inbox'/, '只挂在通信三个页签下');
|
||||
});
|
||||
|
||||
test('① 新建是悬浮圆形加号,且不再是导航项', () => {
|
||||
assert.match(commTabs, /data-testid="compose-fab"/);
|
||||
assert.match(commTabs, /rounded-full/, '必须是圆的(用户点名"圆形加号")');
|
||||
assert.match(commTabs, /absolute bottom-4 right-4/, '悬浮在列表栏右下角');
|
||||
assert.match(app, /<ComposeFab \/>/, 'App 里要渲染它');
|
||||
assert.ok(!/新建<\/span>/.test(sidebar), '桌面侧栏不该再有为"新建"的导航文字');
|
||||
assert.ok(!/新建<\/span>/.test(narrow), '窄屏导航不该再有"新建"');
|
||||
});
|
||||
|
||||
test('② 管理与我的合并:入口在「我的」最下面,且仅管理员可见', () => {
|
||||
assert.match(account, /user\?\.role === 'admin' &&/, '必须是条件渲染而不是禁用');
|
||||
const logout = account.indexOf('退出登录');
|
||||
const adminEntry = account.indexOf("setViewMode('admin')");
|
||||
assert.ok(logout > 0 && adminEntry > logout, '管理入口要在「登录状态」之后(=页面最下面)');
|
||||
assert.ok(!/mode: 'admin'/.test(sidebar), '侧栏不该再有独立的 admin 导航项');
|
||||
assert.ok(!/mode: 'admin'/.test(narrow), '窄屏不该再有独立的 admin 导航项');
|
||||
});
|
||||
|
||||
test('② uiStore 记住通信子页签,且 reset 能清掉', () => {
|
||||
assert.match(uiStore, /commTab: CommTab/);
|
||||
assert.match(uiStore, /setCommTab/);
|
||||
assert.match(uiStore, /commTab: mode as CommTab/, 'setViewMode 要顺手记住');
|
||||
assert.match(uiStore, /reset:[\s\S]{0,120}commTab: 'inbox'/, '登出后要复位');
|
||||
});
|
||||
|
||||
test('③ 复选框与地址建议用"控件档"透度(比正文面更透)', () => {
|
||||
assert.match(mailView, /glass-control/, '授权多选胶囊');
|
||||
assert.match(addr, /glass-control/, '地址建议菜单');
|
||||
// 两处都不能再是 bg-white:我的 .bg-white 接管会把它们拉回正文面那一档
|
||||
const chip = mailView.match(/glass-control[^']*/);
|
||||
assert.ok(chip, 'chip 类名');
|
||||
assert.ok(!/glass-control[^']*bg-white/.test(mailView), '不能再同时写 bg-white');
|
||||
assert.ok(!/glass-control[^"]*bg-white/.test(addr), '建议菜单不能再同时写 bg-white');
|
||||
// 三档透度必须真的递减(正文 > 嵌套 > 控件)
|
||||
const num = re => Number(css.match(re)[1]);
|
||||
const big = num(/--bg-glass:\s*(0\.\d+)/);
|
||||
const inner = num(/--bg-glass-inner:\s*(0\.\d+)/);
|
||||
const ctrl = num(/--bg-glass-control:\s*(0\.\d+)/);
|
||||
assert.ok(big > inner && inner > ctrl, `三档应递减:${big} > ${inner} > ${ctrl}`);
|
||||
assert.ok(ctrl <= 0.6, `控件档要明显更透(当前 ${ctrl})`);
|
||||
});
|
||||
|
||||
test('★ 判据自检:拿重构前的写法喂进来必须判红', () => {
|
||||
const oldSidebar = `{ short: '收件', mode: 'inbox', Icon: InboxIcon },
|
||||
{ short: '授权', title: '授权请求', mode: 'permissions', Icon: ShieldIcon }`;
|
||||
assert.ok(oldSidebar.includes("'授权请求'"), '旧写法能被本判据的"必须消失"条款命中');
|
||||
const oldChip = "'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'";
|
||||
assert.ok(!/glass-control/.test(oldChip), '旧胶囊写法不含 glass-control ⇒ 会判红');
|
||||
});
|
||||
Reference in New Issue
Block a user