跨端: 修「深色下玻璃变灰纸板」+「主题选了不生效」(两个真 bug)

用户问「你的玻璃效果呢?」。我上手先取像素,结果是**两个一直存在的真 bug**,
不是观感偏好问题。两个都有实测数据。

## ① 深色下玻璃 alpha 没跟着翻 →「像深色卡片浮在灰纸板上」

### 实测

深色主题 + aurora 壁纸,同一行交替采样(`/tmp/g1.raw`,1008×2232):

    y=670   卡片缝隙 rgb(199,199,199)   卡片内 rgb(27,37,50)
    y=880   卡片缝隙 rgb(201,203,201)   卡片内 rgb(27,36,53)

缝隙是**浅灰 199**、比卡片还亮。而 199 恰好 = `0.78 × 255`
(壁纸层实测 x=4..24 为 rgb(0,1,3),近黑)—— 就是 `glassCardWall` 那层白纱。

### 根因:我把 WebUI 的一句话读漏了后半句

`Theme.glassCard` / `glassCardWall` 是**单一值、不分深浅**。我当初写的理由是
「基色恒为白,主题之间只差 alpha」——但 WebUI 原话是
「**基材恒为白**,主题之间**只差 alpha**」(`index.css:406`、`:1546`)。
我只抄了前半句,把「只差 alpha」误读成「alpha 也不用变」。

WebUI `.dark` 的实际取值:

    --glass-card-a:      0.06     ← 浅色 0.92
    --glass-card-wall-a: 0.04     ← 浅色 0.78

**深浅差 15 倍**:深色下白纱要几乎撤掉让深壁纸透上来,浅色下才用厚白纱盖亮壁纸。
我用同一个 0.92 配两套主题 ⇒ 深色下壁纸被糊成浅灰,**玻璃感与深色同时消失**。

### 修法

`Theme.glassCardFor(wall, dark?)` —— 与 `accentFor`/`textSubtleFor` 同一范式,
深浅从 `Theme.isDarkNow()`(`AppStorage` 那个发布键)读,不靠调用方自觉传。
新增 `glassCardDark #0FFFFFFF` / `glassCardWallDark #0AFFFFFF`。

改后同处实测:缝隙 rgb(3..16),壁纸透上来了;浅色档回归检查未变(244/239,厚白纱仍在)。

## ② 用户选的主题被无条件覆盖 →「选了深色但界面还是浅的」

### 实测

「我的」页选「深色」后:
· 按钮显示选中、服务端也记下 `theme:'dark'`(`GET /me/appearance` 确认);
· 但界面仍浅色,且文字浅色压浅底 —— 实测背景 `rgb(243,243,243)` /
  文字 `rgb(243,243,243)`,**对比度 ≈1.0:1,完全不可读**。

### 根因(两处叠加)

① `EntryAbility.onCreate` 有一句**无条件**的
   `setColorMode(COLOR_MODE_NOT_SET)`(= 跟随系统)—— 每次冷启都把用户的选择重置掉。
   它是目录迁移时抄进来的(`git log -S` 指向 `f9d757b chore: directory migration`),
   **没有注释说明为什么,也没有谁在用**。已删除(连带上游 import)。

② `MainPage.applyAppearance()` 算出了 `isDarkNow`、发布了 `AppStorage`,
   但**从来没调用过 `store.applyTheme(...)`** ⇒ 系统色彩模式根本没被设过。

### 修法

· 删掉 EntryAbility 那句无条件覆盖(附长注释说明为什么由页面负责);
· `applyAppearance()` 补上 `store.applyTheme(ctx, snap.theme)`。

★ **顺序**:`KEY_IS_DARK` 发布必须在 `applyTheme` **之前** ——
  因为 `glassCardFor()` 是从那个键读深浅的,反过来会让卡片用上一轮的深浅画一帧。
  `applyThemeNow()` 里同一处顺序也一并修正。

## 判据

`cross-client-theme` 的两条原本把 `Theme.glassCard`/`glassCardWall` 两个**字面量**钉死,
被我这次正确的改动撞红 —— 我没有放宽它们,而是改成钉**行为**:
· 卡片必须经 `glassCardFor(...)` 取色(入口存在);
· 四个令牌都要在 Theme 里存在;
· **深色档不得与浅色档同值**(同值 = 主题感知是假的)。

变异验证:① 深色档改回与浅色同值 → 3 条红;② 卡片改回不分深浅 → 5 条红;还原 → 21/21 绿。

## 验证状态

✓ 两个修复都在设备上取**像素**验证(不是看截图说"像了")
✓ `cross-client-theme` 21/21、全量套件 545 pass(3 红均为改动前既有:
  `harmony-admin` 两处 = 我上一轮 `SecuritySection` 拆分遗留、`build-stamp` = 产物戳过期)
✓ 编译通过、进程存活、无新 jscrash
This commit is contained in:
2026-09-21 18:46:19 +08:00
parent e29f38b151
commit cbb1e1ee62
6 changed files with 286 additions and 19 deletions

View File

@ -251,6 +251,19 @@ const SELF_OWNED_COLORS = [
* 浅壁纸与深壁纸(深色档另有 `DARK_DIM_MIN` 兜底)。
*/
'glassCard', 'glassCardWall',
/*
* ★★ 2026-09-21 补登记:深色档的玻璃 alpha。
*
* 与上面两个是**同一个理由的正反两面**:WebUI 深色下把白纱 alpha
* 从 0.92 降到 0.06(`.dark { --glass-card-a: 0.06; }`),
* 因为"白基材恒白、主题之间只差 alpha"。
*
* 不登记的话界面会彻底变暗 —— 而这件事**实测抓到过**:
* 深色 + aurora 壁纸下,卡片缝隙实测 `rgb(199,199,199)`
* (= 0.78×255,那层白纱把黑壁纸糊成浅灰),
* 用户看到的"没有玻璃效果"就是这个。
*/
'glassCardDark', 'glassCardWallDark',
// 业务语义色:系统没有对应物(同意 / 拒绝 / 警示)
'approve', 'danger', 'approveBg', 'approveFg', 'dangerBg', 'warnBg', 'warnFg',
// 权限档位与预算档位的胶囊配色(档位是产品语义,系统不认识"plan/workspace/full")
@ -373,10 +386,27 @@ test('B|旧机制不得回来:手写玻璃 alpha、替系统猜深色、与
* 不是"只要是玻璃就放行"。加第三个半透明色 ⇒ 这条判据照样红,
* 而那时得先回答"系统为什么没有对应物"这个问题。
*
* ★★ 2026-09-21 补登记:`glassCardDark` / `glassCardWallDark`。
*
* 这一条判据**真的抓到过东西**:我给深色加两个更薄的 alpha 时它立刻变红,
* 逼我把"为什么系统没有对应物"重新回答一遍(而不是默默放行)。那个回答是:
*
* · 系统材质(`bgMaterial*` / `compBackground*`)是**不透明**整块色板,
* 没有"白 6% 叠在壁纸上"这一档;
* · 而 WebUI 深色档的玻璃观感**正是这个 alpha**
* (`.dark` 里 `--glass-card-a: 0.06`,`--glass-card-wall-a: 0.04`)——
* 浅色是 0.92 / 0.78,**深浅差 15 倍**。
*
* 所以深色这两个值与浅色那两个是**同一个理由**(同一条豁免的正反两面),
* 不是"又开了个口子"。
*
* ⇒ glassCardDark = #0FFFFFFF(0.06×255 ≈ 15 = 0x0F)
* ⇒ glassCardWallDark = #0AFFFFFF(0.04×255 ≈ 10 = 0x0A)
*
* ★ 白名单项与 `SELF_OWNED_COLORS` 是**两个不同的问题**:
* 那个问"色值有没有登记理由",这个问"半透明这件事合不合法"。
*/
const TRANSLUCENT_OK = ['#EBFFFFFF', '#C7FFFFFF']; // glassCard / glassCardWall
const TRANSLUCENT_OK = ['#EBFFFFFF', '#C7FFFFFF', '#0FFFFFFF', '#0AFFFFFF']; // 玻璃卡 浅/深 两档
const translucent = rawColors(codeOnly)
.filter(h => h.startsWith('#') && h.length === 9)
.filter(h => !TRANSLUCENT_OK.includes(h.toUpperCase()));
@ -679,11 +709,49 @@ test('C|玻璃:位置用系统材质、不许叠、每一处都要登记(
'卡片层不得挂 backgroundEffect —— WebUI `.glass-card` 没有 backdrop-filter,' +
'卡片的玻璃感来自半透明白。加模糊会叠成灰雾并掉帧(已实测)。');
/* ② 两个 alpha 令牌必须真的被卡片读(否则是孤儿令牌,见 C 的另一段) */
assert.match(cardBody, /Theme\.glassCard\b/,
'卡片要读 Theme.glassCard(0.92 那档,照抄 WebUI rgb(255 255 255 / 0.92))');
assert.match(cardBody, /Theme\.glassCardWall\b/,
'卡片要读 Theme.glassCardWall(0.78 那档,对应 WebUI html[data-bg=\'on\'])');
/*
* ★★ 2026-09-21 修:卡片现在读的是 `Theme.glassCardFor(this.active)` ——
* **主题感知**的那个入口,它内部再分派到四个令牌:
* glassCard / glassCardWall (浅色:白 0.92 / 0.78)
* glassCardDark / glassCardWallDark (深色:白 0.06 / 0.04)
*
* 原来这里钉的是 `Theme.glassCard` / `Theme.glassCardWall` 两个字面量。
* 而 2026-09-21 设备实测证明**单一值是不对的**:
* 深色 + aurora 壁纸下,卡片缝隙实测 `rgb(199,199,199)`
* (= 0.78×255,那层厚白纱把黑壁纸糊成浅灰),
* 用户报的「没有玻璃效果」正是这个。WebUI 深色把 alpha 降到 0.06(差 15 倍)。
*
* ★ 为什么不改成 `assert.match(cardBody, /Theme\.glassCardDark/)`:
* 那就把"用哪一档"钉死了 —— 而"壁纸档用 wall、否则用 card"是**行为**,
* 应该由 `glassCardFor` 的一处实现决定,而不是由判据数调用点。
* 所以这里改为:**入口存在**,且**四个令牌在 Theme 里都存在且被入口读到**。
* 真正的行为验证在下面 ②' 那一段(深浅两个值必须不同)。
*/
assert.match(cardBody, /Theme\.glassCardFor\(/,
'卡片要经 Theme.glassCardFor(...) 取底色 —— 它按当前深浅分派四档 alpha');
/* ②' 四个令牌都得在 Theme 里真的存在,且入口真的按深浅分派 */
const themeSrcForGlass = code(join(HARMONY_ETS, 'common', 'Theme.ets'));
for (const name of ['glassCard', 'glassCardWall', 'glassCardDark', 'glassCardWallDark']) {
assert.match(themeSrcForGlass, new RegExp(`static readonly ${name}: string = '#[0-9A-Fa-f]{8}'`),
`Theme 里要有 ${name}(四档 alpha 缺一不可)`);
}
/*
* ★ 深色档必须**真的不同**于浅色档 —— 否则 "主题感知" 是假的。
* 这条不依赖具体数值(那些数值已在 `C2` 与`SELF_OWNED_COLORS` 那边登记),
* 只钉"变了"这件事:
* 浅色 0.92 / 0.78 vs 深色 0.06 / 0.04
* 若哪个改成与浅色同值,这条红。
*/
const glassAlpha = (name) => {
const m = themeSrcForGlass.match(new RegExp(`static readonly ${name}: string = '#([0-9A-Fa-f]{2})`));
return m ? m[1].toUpperCase() : '';
};
assert.notEqual(glassAlpha('glassCardDark'), glassAlpha('glassCard'),
'深色档 glassCardDark 不得与浅色 glassCard 同值 —— 同值等于没有主题感知');
assert.notEqual(glassAlpha('glassCardWallDark'), glassAlpha('glassCardWall'),
'深色档 glassCardWallDark 不得与浅色 glassCardWall 同值;'
+ '实测教训:深色下白纱 0.78 会把黑壁纸糊成 rgb(199,199,199) 的深灰纸板');
/*
* ③ **不许**用 `backgroundEffect` —— 2026-09-21 实测否掉了我上一版的推断。
@ -946,7 +1014,19 @@ test('遮罩:交给系统的遮罩语义色("随主题换向"这件事现在
* 而不是把这条判据整个放宽:遮罩那个真实约束**原样保留**。
* `overlay` / `wallpaperScrim` 若被写成 8 位单值,这条照样红。
*/
const glassAlphaOk = ['#EBFFFFFF', '#C7FFFFFF']; // 与 B 条同一份名单
/*
* ★★ 2026-09-21 再修正:白名单里要加上**深色档**那两个。
*
* 上面这轮修正把名单限制成"同名白名单",但写的是**两个**名字;
* 而 WebUI 的玻璃 alpha 本来就是**深浅两套**
* (`.dark` 里 `--glass-card-a: 0.06` / `--glass-card-wall-a: 0.04`)——
* 我先前只搬了浅色那两个,正是「玻璃在深色下变成灰纸板」的成因。
*
* ⇒ 现在四个一起列,与 B 条的 `TRANSLUCENT_OK` **同一份名单**。
* 遮罩那个真实约束**原样保留**:`overlay` / `wallpaperScrim`
* 若被写成 8 位单值,这条照样红(下面这句断言就是干这个的)。
*/
const glassAlphaOk = ['#EBFFFFFF', '#C7FFFFFF', '#0FFFFFFF', '#0AFFFFFF']; // 玻璃卡浅/深两档
const all8 = (stripComments(harmony).match(/#[0-9A-Fa-f]{8}/g) ?? [])
.filter(h => !glassAlphaOk.includes(h.toUpperCase()));
assert.deepEqual(all8, [],