fix(webui): 自定义背景完全不可用 —— 「图片」档进不去

现象:点「图片」后背景反而被关掉,上传控件永远不出现 ⇒ 自定义图片在 UI 上
完全不可达(用户看到的正是"自定义背景不正常")。

根因:`normalizeBackground` 把「kind=image 但还没有图片数据」折叠成 `none`
(这条判据本身是对的 —— 读盘时那确实是脏数据),但 store 的 `commit()` 每次
patch 都要过一遍它,于是 `setKind('image')` 这一瞬间就被折叠回去;而上传控件
只在 `kind === 'image'` 下渲染 ⇒ 鸡生蛋问题,用户永远走不到选文件那一步。

修法:给归一化加 `keepEmptyImage`。
  - 读盘(`readStored`)保持严格:空图片状态是脏数据,退回 none。
  - 交互(`commit`)保留瞬态:允许"已选图片档、还没挑文件"这个中间状态存在。
静止态的不变量没有放松,放松的只是正在选图的那一瞬间;`applyBackground` 对
空图片本来就不铺开(不会出现 `url("")`)。

判据(都验过"修复前会红"):
  · 新增 `test/components/BackgroundPicker.test.tsx` —— 点「图片」后选文件控件
    必须出现、选完图背景必须亮、失败必须说原因、选「无」必须能关掉。
    **扰动验证**:把修复撤掉 → 组件 3 红 + store 1 红;恢复 → 22 全绿。
  · `test/stores/background.test.ts` 补 2 条:交互进入图片档要留住 /
    瞬态落盘后重读必须退回 none。

线上验证(真浏览器,部署后):线上 bundle 换成 index-CSFGa8wa.js 后 ——
点「图片」→ `kind=image` 保持 → 上传 16KB 小图与 9MB 大图都成功
(大图压缩到 1790KB)→ 刷新后仍在 → 全程无页面错误。

顺带:应用内**从未出现**过品牌图标。`BrandMarkIcon` 只用在登录页与首启页
(都是登录前界面),登录后的日常界面里一处都没有。侧栏顶端加上品牌标记
(点它回收件箱)。favicon 那条链本来就是好的(3 个 link 都 200、类型正确、
图标内容正确),已在验证中确认。
This commit is contained in:
2026-09-13 08:55:55 +08:00
parent 085e6384a0
commit 5804ba4f63
4 changed files with 144 additions and 6 deletions

View File

@ -10,7 +10,8 @@ import {
UsersIcon,
LogoutIcon,
ShieldIcon,
CalendarIcon
CalendarIcon,
BrandMarkIcon
} from './icons';
import { ConnectionIndicator } from './ConnectionIndicator';
import { ThemeToggleButton } from './ThemePicker';
@ -56,6 +57,24 @@ export default function Sidebar() {
<div className="w-[60px] h-full shrink-0 flex flex-col items-center py-3 gap-1 bg-chrome-900"
style={{ paddingBottom: 'calc(0.75rem + env(safe-area-inset-bottom))' }}
>
{/*
品牌标记放在侧栏顶端。
在放到这里之前,应用内**没有任何一处**品牌标识 —— 登录页与首启页各有一个
BrandMarkIcon但两者都是登录前才看得到的界面日常使用登录后从头到尾
见不到产品图标,只有浏览器页签的 favicon。
点它回收件箱,与邮箱类应用的一致习惯相符(点左上角 logo 回家)。
*/}
<button
type="button"
data-testid="brand-mark"
onClick={() => setViewMode('inbox')}
title="AgentMail"
aria-label="AgentMail 首页"
className="w-10 h-10 shrink-0 mb-1 rounded-xl bg-white/10 text-white flex items-center justify-center hover:bg-white/15 active:bg-white/20 transition-colors"
>
<BrandMarkIcon className="w-5 h-5" />
</button>
{navItems
.filter(n => !n.adminOnly || isAdmin)
.map(({ short, title, mode, Icon }) => {