Files
MailUI4Agents/client/electron/scripts/gen-background-takeover.mjs
JianFeeeee ababe4ae56 fix(webui): 壁纸被不透明层盖住 —— 玻璃不再层层相乘 + 浅色表面类全部接管
用户报的:「webui 目前在壁纸底上叠了太多不透明层,导致壁纸效果很差,几乎看不出来」,
追问后补充:「不只是玻璃,而是很多界面是不透明的」。两句都成立,是两个叠加的原因。

## ① 玻璃层层相乘

每层面板都吃同一个不透明度 a,两层就是 1-(1-a)²。a=0.82 时两层 0.97、三层 0.995
—— 壁纸在数学上被吃掉,而且每层各做一次 16px 模糊,图案被糊成灰块。
改法:玻璃只出现一次(外层 0.62 + 18px 模糊;嵌套层只留 0.3 色调且不再模糊;
第三层透明)。同时把遮罩 24%→12%、照片自身模糊 8→4px(那张被洗白的锅之一)。

## ② 14 个浅色表面类根本没被接管("很多界面是不透明的"就是这条)

旧规则只接管 white / gray-50 / slate-100,而源码里在用的是
gray-100(24 处)、blue-50(17)、red-50(13)、blue-100(8)、gray-200(8)、green-50(8)…
—— 全是实心的,正好盖住壁纸。

清单改为**从源码用法生成**(`scripts/gen-background-takeover.mjs` → 生成的 CSS,
构建前自动跑),因为手写清单必然烂;判据保证"源码里出现的浅色表面类必须都被接管",
并明确排除页面底(gray-50/slate-100 必须保持**全透明**,不能被改成半透明)。
按钮/徽标(*-600/700、chrome-600/700)**故意保持不透明**:小控件可读性优先
(index.css 里原有注释记着实测 4.46:1 的教训)。

## 判据

- `test/background.test.mjs` 23 条(新增 8 条):玻璃算式(两层 ≤0.85)、外层 ≤0.70、
  嵌套规则存在、**判据自检**(旧值 0.82 必须算得出 >0.95)、表面类覆盖、页面底不得进清单、
  清单非空跑。犯过一次错:第一版把判据追加在 `process.exit()` **之后** ⇒ 根本没执行,
  从"测试数没变"才发现。
- `test/manual/wallpaper-layers-verify.mjs`(真实渲染,自带无头 Chromium):
  用**纯红壁纸读绿通道**测有效不透明度(纯色下 backdrop-filter 不影响读数)。
  面板 p95:**修复前 82% → 修复后 62%**;反向对照(改回旧值)能分辨 ≥10 个百分点;
  深色照片面上面板仍是浅底;关掉壁纸的同坐标对照更实。5/5。
  过程中作废了两个指标:只看绿通道会把深色元素误判成"盖死";"面积占比"类阈值
  (Δ≥8/≥40)在 0.97 时仍会蹭过门槛,饱和而无分辨力 —— 同坐标比值才可信。

## 交付

已部署(网关内嵌 WebUI 重建):线上 CSS 已含 `--bg-glass-inner`、嵌套规则与 14 个接管类。
2026-09-14 08:09:22 +08:00

74 lines
3.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 生成"背景开启时哪些浅色表面类要变半透明"的 CSS。
*
* # 为什么要生成而不是手写
*
* 手写清单必然烂2026-09-13 用户报「很多界面是不透明的,导致壁纸露不出来」,
* 一查代码里在用的浅色表面类有 gray-100(24 处)、blue-50(17)、red-50(13)、
* gray-200(8)、green-50(8)… 而当时的接管清单只有 white / gray-50 / slate-100 ——
* 漏掉的那些全是实心的,正好盖住壁纸。
*
* 所以清单从**源码里实际出现的类**推导:出现什么就接管什么。新增一个
* `bg-purple-50` 而忘了同步,`test/background.test.mjs` 里那条判据会红。
*
* 只接管"表面段"50/100/200它们是面板底、提示条、卡片这类大面积浅色。
* 600/700 那一段是按钮与徽标(小控件、要读得清),**故意保持不透明** —— 见
* index.css 里 chrome-600/700 那段注释(实测可读性从 4.46:1 掉到 AA 以下)。
*/
import { readdir, readFile, writeFile } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const SRC = join(HERE, '..', 'src');
const OUT = join(SRC, 'background-takeover.generated.css');
/** 表面段:浅色大类 + 50/100/200 三档。 */
const SURFACE = /^bg-([a-z]+)-(50|100|200)$/;
/**
* 页面底:**必须完全透明**(背景开启时),不能进这份"半透明"清单。
*
* 它们铺满整屏,给它们 0.3 的不透明度等于在壁纸上盖一层白纱;而 index.css 里
* 已经把它们设成 transparent —— 谁后加载谁生效,生成物在 index.css 之后引入,
* 所以漏了这条排除就会把"完全透明"悄悄改回"半透明"。
*/
const PAGE_BASE = new Set(['bg-gray-50', 'bg-slate-100']);
async function walk(dir) {
const out = [];
for (const e of await readdir(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isDirectory()) out.push(...(await walk(p)));
else if (/\.(tsx?|jsx?)$/.test(e.name)) out.push(p);
}
return out;
}
const found = new Set();
for (const file of await walk(SRC)) {
const text = await readFile(file, 'utf8');
for (const m of text.matchAll(/bg-[a-z]+-\d{2,3}/g)) {
if (SURFACE.test(m[0]) && !PAGE_BASE.has(m[0])) found.add(m[0]);
}
}
const classes = [...found].sort();
const body = classes
.map(
(cls) =>
`html[data-bg='on'] .${cls} {\n` +
` background-color: rgb(var(--c-${cls.slice(3)}) / var(--bg-glass-inner));\n` +
` backdrop-filter: none; /* 外层已经模糊过,嵌套层再糊一次会把图案糊成灰块 */\n` +
` -webkit-backdrop-filter: none;\n}`,
)
.join('\n\n');
await writeFile(
OUT,
`/* 由 scripts/gen-background-takeover.mjs 生成 —— 不要手改。\n` +
` * 背景开启时把源码里在用的浅色表面类改成半透明(清单来自 src/**/*.tsx 的实际用法)。\n` +
` * 共 ${classes.length} 个类:${classes.join(', ')}\n */\n\n${body}\n`,
);
console.log(` 生成 ${classes.length} 个类 → ${OUT}`);