鸿蒙|图标 fill 型分族 + 页签条阴影根因修复 + 生成器入库
用户逐条报的观感问题(都在"视觉观感"那一栏,不是功能缺失):
① 侧边栏图标"莫名其妙的加粗"
根因:43 个图标里**只有品牌标 `brandMark` 是 fill 型**
(`fill="currentColor"` + 两个实心 `<circle r=".9">`),
它自己的注释就写着「fill 型,与上面的描边图标集**不同族**,所以不包 Svg」。
而 `AmIcon` 一律 `fill(Transparent) + stroke()` ⇒ 实心块只剩轮廓、
实心眼变成小圆环、圆头端点在小尺寸下糊成一坨。
修法:生成器自动判定两族(`is_fill_family`),产出 `FILL_ICONS` 名单,
fill 族整块填色、不加描边。
② 详情页权限面板"左边被截断"
`PermissionPanel` 挂在详情页外层,根部只有 `padding({top:10})` ——
而正文 `Markdown` 与附件块**各自自带** `left/right:16`。补上同档 16。
③ 顶栏"莫名其妙的底部阴影"
★ 这条查错了两轮,记下来:
· 先以为是窗格投影从半透明玻璃透上来 → 补底边、去材质 —— 都没用;
· 几何取证才定位真因:`InboxTab` 根容器与页签条是 `Column` 里的**兄弟**
且**绘制在后**(页签条 y 142→271,InboxTab y 271→2202),
它的 `PaneModifier` 投影向上扩散 ~30px,正好压住页签条
(观测到的渐变区 y 240→268,完全吻合)。
· 双向验证:关掉所有窗格投影 → 全平(证明确实是投影);
只把 `InboxTab` 换 `plain` → 落差 21→8(证明是**这一层**)。
修法:`PaneModifier` 加 `plain()`(只要背景语义、不投投影)。
WebUI 的 `box-shadow` 只给 `.app-shell > *`(三个**并列**面板),
面板内部不投 —— 这个结构差异就是原因。
★ 同时保留页签条的**悬浮玻璃**(用户 09-20 点名要的):
我中途一度按"跟 webui 同步"把它改成不透明实体面,
那是**读错了**——用户指的是页面结构对齐,而玻璃是他自己定过的;
两者不冲突(玻璃是观感选择,阴影是 bug)。已恢复并留注释。
④ 生成器入库(`client/harmony/tools/gen-icons.py`)
它原先在仓库外(`/root/gotmp`),于是**落后生成物三次提交**没人发现:
我手改 `Icons.ets` 修 fill 图标后重跑它,修改被**冲掉**(退回旧实现)。
现在:路径按 `__file__` 解析(任何检出目录都能跑)、
`is_fill_family` 自动分族、输出段与正确实现逐字一致(diff 为空)。
`Icons.ets` 由它生成,不再手改。
判据:
· `harmony-appearance` 29 条(+1):窗格投影只给并列窗格,
面板内部的子 tab 不得再投。按 **struct 归属**判(第一版计数有洞,
变异①抓不到 —— 改回 `of` 时同文件别处的 `plain` 仍让它绿)。
两个变异都能红:① InboxTab 改回 of;② 让 plain 也投投影。
另修一条既有判据的假红:它数 `PaneModifier.of` 出现次数,
加 `plain` 后误报("让出页面底"的两种合法写法都要算)。
· 套件:harmony-nav 22/22、harmony-appearance 28/28、
harmony-logic 34/34、harmony-contacts 5/5、animation-audit 15/15。
**功能对齐状态**(回答用户"功能对齐没有"):
对着 `docs/DEBTS.json` 逐条核过 —— 鸿蒙侧**没有缺页面、没有缺功能**。
授权栏历史(`harmony-permission-history`)✅ 已做、
详情页转发/改名建议/预算(`harmony-maildetail-missing-three`)✅ 三块全做完。
仅剩两条,都不在鸿蒙:`harmony-p4c-boundary-decls` 卡在"需真人操作系统选图器"、
`permission-expires-at-unused` 缺的是 **WebUI 那一半**。
This commit is contained in:
@ -498,7 +498,21 @@ test('★ 背景画出来了还不够:每个页面要**让出**页面底,否
|
||||
'页面底要让位给壁纸 ⇒ 必须有一个统一的窗格属性集(不是每页各写三元)');
|
||||
assert.match(paneMod, /backgroundColor\(this\.active \? Color\.Transparent : Theme\.pageBg\)/,
|
||||
'PaneModifier 必须在 active 时返回透明(否则"让出"这件事根本没发生)');
|
||||
const yielded = [...main.matchAll(/\.attributeModifier\(PaneModifier\.of\(this\.bgActive\)\)/g)].length;
|
||||
/*
|
||||
* ★★ 2026-09-24 改:判"让出页面底"的**两种入口**,而不是数一种写法。
|
||||
*
|
||||
* 原断言:`\.attributeModifier\(PaneModifier\.of\(this\.bgActive\)\)` 至少出现 5 次。
|
||||
*
|
||||
* 本次加了 `PaneModifier.plain(...)`(同背景语义、**不投投影**)——
|
||||
* 它同样“让出页面底”(`backgroundColor` 那段是共用的),
|
||||
* 而旧断言只数 `of(` ⇒ 当场假红(实测:`实际 1 处`、`fail 1`)。
|
||||
*
|
||||
* 这正是本文件自己反复记过的形状:**判据锚在实现的一种写法上,
|
||||
* 而不是锚在它声称的那件事上**。"让出页面底"的两种合法写法都要算。
|
||||
* (`plain` 的出现理由:同一窗格被多层嵌套时,只有**最外层的并列窗格**
|
||||
* 该投投影,内层用 `plain` —— 见 `Surface.ets` 的 `shadowed` 注释。)
|
||||
*/
|
||||
const yielded = [...main.matchAll(/\.attributeModifier\(PaneModifier\.(?:of|plain)\(this\.bgActive\)\)/g)].length;
|
||||
assert.ok(yielded >= 5, `要让出页面底的页面至少 5 个(通信/联系人 + 三个 pane),实际 ${yielded} 处`);
|
||||
|
||||
// 每个页面都要**收到**这个开关:漏传 = 该页恒为不透明(等于没有让出)
|
||||
@ -554,6 +568,114 @@ test('★ 背景画出来了还不够:每个页面要**让出**页面底,否
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════
|
||||
* 窗格投影:**只有"并列窗格"该投**,面板内部不投
|
||||
* ═══════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 用户 2026-09-24:「顶栏莫名其妙的底部阴影」+「跟 webui 同步」。
|
||||
*
|
||||
* ── 症状 ──
|
||||
* 页签条(收件箱/发件箱/授权)内部从上到下 254→234 **单调递减**一条暗带,
|
||||
* 像素实测贯通整条(x 250→1150 亮度恒 224-227)。
|
||||
*
|
||||
* ── 根因(几何取证,不是猜)──
|
||||
* `InboxTab` 的根容器与页签条是 `Column` 里的**兄弟**,且**绘制在后**
|
||||
* (页签条 y 142→271,InboxTab y 271→2202)。它的 `PaneModifier` 投影向四周扩散
|
||||
* ⇒ 向上扩散的那 ~30px 正好压住页签条(观测到的渐变区 y 240→268,吻合)。
|
||||
*
|
||||
* 实测验证(只把 `InboxTab` 改 `plain`,其余不动):
|
||||
* 修前落差 21(254→233) → 修后落差 8(255→247)✅
|
||||
* 而窗格自己那圈投影(x=230 处 176-180)**仍在** —— 没把该有的层次感一起删掉。
|
||||
*
|
||||
* ── WebUI 为什么没这问题 ──
|
||||
* `box-shadow` 只挂在 `.app-shell > *`(`index.css:980`)——
|
||||
* 那是**三个并列面板**(Sidebar / list / main),**没有嵌套**。
|
||||
* 面板**内部**(列表、卡片)不再投投影。
|
||||
*
|
||||
* ── 为什么不只盯 `InboxTab` 一个名字 ──
|
||||
* 同一个形状在仓里有**多处**(`SentTab`/`PermissionTab`/`ContactsTab` 的 navBar 内容)。
|
||||
* 只钉一个名字的话,下次改另一个 tab 就会把同一条阴影带回来。
|
||||
* 所以判的是**结构不变式**:
|
||||
* · `Navigation` 壳(并列窗格)→ `PaneModifier.of`(带投影);
|
||||
* · 壳**内部**那几层 → `PaneModifier.plain`(不投)。
|
||||
*/
|
||||
test('★ 窗格投影只给并列窗格;面板内部的子 tab 不得再投一次(顶栏阴影的根因)', () => {
|
||||
const surface = read('common/Surface.ets');
|
||||
|
||||
/* 前提:两类工厂都得存在,且 plain 真的不投投影 */
|
||||
assert.match(surface, /static plain\(active: boolean\): PaneModifier/,
|
||||
'要有一个"只要背景语义、不投投影"的入口(面板内部用它)');
|
||||
assert.match(surface, /private shadowed: boolean = true;/,
|
||||
'PaneModifier 要有一个显式开关区分"投/不投",而不是靠调用点自己记');
|
||||
assert.match(surface, /if \(this\.shadowed\) \{\s*instance\.shadow\(Theme\.glassShadow\);\s*\}/,
|
||||
'`plain` 必须**真的**不调 shadow —— 否则这个开关是装饰品');
|
||||
|
||||
/*
|
||||
* 不变式:**子 tab 的根容器**不得带投影(它们就在页签条下方、绘制在后)。
|
||||
*
|
||||
* ★ 判法:先定位每个 modifier 落在哪个 `struct` 里,再要求子 tab 那些 struct
|
||||
* 一律用 `plain`。
|
||||
*
|
||||
* ⚠ 第一版写的是"该文件里 `plainCount >= 1`" —— **变异测不出来**:
|
||||
* 把 `InboxTab` 改回 `of` 后,同文件的 `SentTab`/navBar 那几处 `plain`
|
||||
* 仍然存在 ⇒ 计数 ≥1 照样绿(实测:变异① 没抓到)。
|
||||
* "存在某个正确的" 与 "该正确的那个不许错" 是两件事。
|
||||
*/
|
||||
const CHILD_TABS = ['InboxTab', 'SentTab', 'PermissionTab', 'ContactsTab'];
|
||||
/* 某个偏移落在哪个 struct 里(取它前面最近的那个 `struct X {`) */
|
||||
const structAt = (src, idx) => {
|
||||
const before = src.slice(0, idx);
|
||||
const ms = [...before.matchAll(/(?:^|\n)struct\s+(\w+)\s*\{/g)];
|
||||
return ms.length ? ms[ms.length - 1][1] : '(top)';
|
||||
};
|
||||
for (const [file, label] of [
|
||||
['pages/MainPage.ets', 'CommPage'],
|
||||
['pages/ContactsTab.ets', 'ContactsTab 页'],
|
||||
['pages/PermissionTab.ets', 'PermissionTab'],
|
||||
]) {
|
||||
const src = read(file);
|
||||
const offenders = [];
|
||||
for (const m of src.matchAll(/PaneModifier\.(of|plain)\(this\.bgActive\)/g)) {
|
||||
const owner = structAt(src, m.index);
|
||||
if (m[1] === 'of' && CHILD_TABS.includes(owner)) offenders.push(owner);
|
||||
}
|
||||
assert.deepEqual(offenders, [],
|
||||
`${label}: ${offenders.join('/')} 的根容器用了 \`PaneModifier.of\`(带投影),` +
|
||||
'但它们就在页签条下方且绘制在后 ⇒ 投影向上扩散会压住页签条。' +
|
||||
'要改用 `PaneModifier.plain`(投影只留给 Navigation 那层并列窗格)');
|
||||
}
|
||||
|
||||
/* 同时:并列窗格那层仍要**保留**投影,否则层次感整个丢掉(不是只删不建) */
|
||||
const mainSrc = read('pages/MainPage.ets');
|
||||
const commAt = mainSrc.indexOf('struct CommPage {');
|
||||
assert.ok(commAt >= 0, '要能找到 CommPage');
|
||||
const commBody = mainSrc.slice(commAt, mainSrc.indexOf('\n}\n', commAt));
|
||||
assert.match(commBody, /PaneModifier\.of\(this\.bgActive\)/,
|
||||
'CommPage 的 Navigation(并列窗格)要保留带投影的 `PaneModifier.of` —— ' +
|
||||
'否则窗格浮在壁纸上的层次感没了');
|
||||
|
||||
/* 同一个实例上不得挂两个 .attributeModifier(后者静默覆盖前者,意图丢失) */
|
||||
const main = read('pages/MainPage.ets');
|
||||
const navAt = main.indexOf('Navigation(this.navPathStack)');
|
||||
assert.ok(navAt >= 0, 'CommPage 要有 Navigation');
|
||||
/*
|
||||
* ⚠ 取**属性链**(从 `navDestination(` 到闭合 `}`)而不是".slice(4000)":
|
||||
* 第一版写 4000 字符,跨到了下一个组件的 modifier ⇒ 数出 3 个、假红。
|
||||
* 属性链上有固定的一组锚(navDestination / mode / navBarWidth),
|
||||
* 用它定界比按字符数可靠。
|
||||
*/
|
||||
const chainStart = main.indexOf('.navDestination(', navAt);
|
||||
const chainEnd = main.indexOf('\n }\n}', chainStart);
|
||||
assert.ok(chainStart > navAt && chainEnd > chainStart, '要能找到 Navigation 的属性链边界');
|
||||
const chain = main.slice(chainStart, chainEnd);
|
||||
const navMods = [...chain.matchAll(/^\s*\.attributeModifier\(PaneModifier\./gm)].length;
|
||||
assert.ok(navMods <= 1,
|
||||
`同一个 Navigation 实例上挂了 ${navMods} 个 .attributeModifier —— ` +
|
||||
'`.attributeModifier` 是**单一插槽**,后者会静默覆盖前者,' +
|
||||
'于是"壳有没有投影"取决于书写顺序而不是意图(2026-09-24 已删掉那处重复)');
|
||||
});
|
||||
|
||||
// ───────────── 深色色板:pi 指出这是"机制上确定不同",不是"观感未验" ─────────────
|
||||
|
||||
/** 从 index.css 的某个段(:root 或 .dark)里读出调色板变量的 RGB */
|
||||
|
||||
Reference in New Issue
Block a user