Commit Graph

7 Commits

Author SHA1 Message Date
13b557a742 跨端: 品牌蓝深色下没提亮(24 处字/图标看不见)+ 补深色可读性设备判据
## 一、真 bug:WebUI 深色下把品牌蓝**提亮**了,这边没有

WebUI 的强调色是**双通道**(`tailwind.config.js` 的 `backgroundColor`
/`textColor` 覆盖 + `index.css` 两段定义):

| 通道 | 用途 | 浅色 | 深色 |
|---|---|---|---|
| `--s-blue-600` | **实心按钮底** | `37 99 235` | `37 99 235`(**同值**)|
| `--c-blue-600` | 内容/交互的**蓝字与图标** | `37 99 235` | **`128 175 249`** |

`index.css:353` 写了理由:主按钮底跟着变「会让主按钮在深色页面上
失去『这是主操作』的视觉重量」;而蓝字必须提亮,否则深底上读不动。

鸿蒙只有一个 `Theme.accent = '#2563EB'` ⇒ 24 处字/图标在深色下
对比度 **2.61:1**(设备实测:管理页返回箭头 `‹`),低于 WCAG 图形下限 3:1。

**取证方式**:在跑着的 WebUI 上用 CDP 读**计算样式**(不是读 CSS 源)——
浅色 `37 99 235` / 深色 `128 175 249`,实测确认。

## 二、修法:加前景专用的深色档 + 唯一入口

- `Theme.accentDark = '#80AFF9'`(= WebUI `.dark --c-blue-600`)
- `Theme.accentFor(dark?)` 作为**前景**唯一入口
- **当背景的 20 处保持 `Theme.accent` 不动**(跟 WebUI 的 `--s-*` 一致)

24 处 `.fontColor/.iconColor(Theme.accent)` → `Theme.accentFor()`。

## 三、顺手修掉「转述一层就会漏」这个结构问题

`accentSoftFor(isDark)` 原本的约定是"页面算好深浅色传进来"。
给 `accentFor` 做准备时一数:**8 处**直接用了 `Theme.accentSoft`(没走入口)
—— 约定**已经漏了**,而漏掉的症状正是上一轮那个"深色下白底卡片刺眼"。

于是把两个 `For()` 的参数都改成**可选**:`AppStorage` 是 ArkTS 全局键值存储,
静态类可以直接读(原来"拿不到 Context"的理由不成立)。
⇒ `Theme.isDarkNow()` 成为唯一判断点,调用方不必再各自转述。

## 四、设备判据:深色可读性**扫一屏**

原来只有悬浮球那一条(单个点)。这个 bug 类一天撞到**两批**(12 处 + 24 处),
逐处写判据追不上 ⇒ 改成把当前页所有小段文字都量一遍对比度。

三个实现要点(第一版全踩了,都写进注释):
- **不能只取中心一个像素**:中心多半落在笔画之间 ⇒ 读到的是底色,
  报出一片 ratio=1.00 的假红。改成**框内网格扫描取极值**(最亮=底/最暗=墨)。
- **整屏解码一次**:每点 spawn 一次 ffmpeg 太慢 ⇒ 新增
  `readPixels()`(157ms 解整屏,比逐点快三个数量级)。
- **只判"有真实墨迹"的框**(`hi.L - lo.L >= 0.02`),否则跳过而不是判红。

实测:修前 1 处低对比(2.61:1),修后 **40 段文字全部 ≥3:1**。

## 五、判据自身的三个修正

- `accentSoftFor(dark:)` 的签名断言跟着放宽成 `dark?`,并**补上 `accentFor` 的**。
- **孤儿令牌判据从"一跳"改成"走整条链"**:`KEY_IS_DARK` ← `isDarkNow()`
  ← `accentFor()` ← 24 处页面。只查一跳时它假红 ——
  ★ **"有没有人用"是可达性问题,不是邻接问题**;加中间层(抽 `For()` 入口)
  恰恰是我们鼓励的写法,而旧判据会因此假红。
- 手写色登记表补 `accentDark` 一行理由。

## 六、`baseline.sha` 重算(先核过不是残留)

三个文件哈希对不上。逐个 `git diff --quiet HEAD -- <f>` 取证:
- `AdminUsersPage.ets` / `SettingsPage.ets` —— 本次**有意编辑**;
- `api/AppearanceApi.ets` —— **与 HEAD 逐字节相同** ⇒ 底本取完后被**合法改过**
  (提交 `f811c98`),属 `stale` 不是 `residue`。

按该文件自己那条纪律(「重算必须是一次有记录的动作」)在文件里记了理由。

`run-all.mjs` → `checks=514 pass=514 fail=0 skip=0 red=0 broken=0 unreported=0`。
2026-09-19 20:05:33 +08:00
0e5eec61bd 跨端: 登录页那个"emoji"是 Unicode 符号 —— 顺手修好图标贴左上角(共 4 处)
用户两条反馈,都是**看着界面**报出来的,而编译器与所有既有判据全绿:
①「为什么登陆页不是app图标,而是一个emojy?」
②「你自己看看那个图标的位置正常吗?」

## ① `Text('✉')` 被系统渲染成彩色 emoji

登录页的品牌标识原先写的是 `Text('✉')`(Unicode U+2709)。HarmonyOS 字体链里有
**彩色 emoji 字体**,U+2709 自带 emoji 字形 ⇒ 渲染成一枚黄白色风信封 emoji:

- `.fontColor(Theme.accent)` 对彩色 emoji **无效**(界面显示的是 emoji 自带颜色);
- 与底栏/侧栏那些 `AmIcon` 线描图标不是同一套视觉语言;
- 实测截图硬证:大屏下那枚 emoji 比旁边的文字还显眼。

而本仓**早就有** `ICON_PATHS.brandMark`(就是 App 图标上那个信封,专为品牌标识画的)。
换成 `AmIcon({ iconName: 'brandMark' })` 即可 —— 走 `Path.stroke()`,跟主题色走。

## ② 图标贴在卡片左上角(量出来的)

`AmIcon` 内部那个 `Stack` 是 `iconSize` 那么大、**默认靠左上**排版。调用方写
`AmIcon({…}).width(48).height(48)` 想要个大点的可上色盒子时,**外层盒子变大、图标不动**。

实测(三折叠 3.5 密度,`dumpLayout` 读的**实际 bounds**):

    卡片 [1523,521][1661,659]  138×138px
    图标 [1526,524][1589,587]   63×63px   ← 左边距/上边距都只有 3px
    ⇒ 图标中心偏 10vp

**不是一处**:全仓扫出 4 个同样写法。修法是套一层
`Stack({ alignContent: Alignment.Center })`(仓里回复球与悬浮加号本来就是这么写的,
所以它们一直是对的):

| 位置 | 图标 | 盒子 | 原状态 |
|---|---|---|---|
| 登录页品牌卡 | brandMark 24 | 48×48 | 贴左上 ✗ |
| 用户管理刷新键 | repeat 20 | 40×40 | 贴左上 ✗ |
| 联系人视图切换 | cardView 18 | 40×40 | 贴左上 ✗ |
| 收件箱组头箭头 | chevronRight 12 | 20 槽位 | 贴左 ✗ |
| 回复球 / 悬浮加号 | chatBubble / compose | 56×56 | 本来就对(尺寸在外层 Button 上) |

修后实测:偏移 **−34.5px → 0.5px**(0.14vp,亚像素级)。

## 判据(harmony-arkts 3 → 5 条)

- **图标不许用 Unicode 符号充当**:扫 `Text('…')` 里单个符号的情况。
  ★ 只框 U+2600–U+27BF / U+2B00–U+2BFF / U+FE0F,**刻意不含基本箭头段**(U+2190–U+21FF)——
  `→` 在正文里是标点不是图标,框进来会误伤大量正常文案。
  (我第一版把箭头段也框了,结果自检自己先红 —— 断言写错就是写错,不靠放宽它来「修」。)
- **尺寸不许直接链在 `AmIcon` 上**:把"图标贴左上角"这个坑的**形状**钉住,
  并自检"套了 `Stack` 的正确写法不许被误判"。

**两个变异方向都跑过**:放回 `Text('✉')` ⇒ 红 ✓;给 `AmIcon` 直接加 `.width(48)` ⇒ 红 ✓。
2026-09-18 11:47:51 +08:00
a87a88ea2a 跨端: 全屏是**窗口级**的 —— 光给 MainPage 让位,等于把黑边换成顶栏压字
上一提交(cac026e)把黑边消掉了,但**只给 `MainPage` 加了避让**。
实测截图硬证:写邮件页的「取消」与时钟「09:49」重叠、「发送」与 wifi/电量图标重叠。

## 根因:`setWindowLayoutFullScreen(true)` 不只作用于当前页

它是**窗口级**的:一旦设上,这个窗口里**所有**用 `router.pushUrl` 推上来的页
(写邮件/会话/收件箱/邮件详情/用户管理)都从 y=0 开始画。

所以上次那个错与更早那次(6861934 只删全屏不留避让)**同源**:
都是"同一件事只做了一半"。上次少的是**步骤**,这次少的是**页面**。

## 改法:把"消费避让"变成每个 @Entry 页都得做的事

- `model/WindowInsets.ts` 加 `topInset(insets)`:取 0 时(未全屏/取不到)
  表达式的值与旧代码**逐字相同** ⇒ 没全屏的环境行为不变,不会把谁顶下去。
- 五个页各按自己的形状让位:
  - 固定 56vp 顶栏(写邮件/会话/收件箱/用户管理):
    `height(56 + topInset(...))` **与** `padding(… top: topInset(...))` 一起加 ——
    只加 padding 会把固定的 56 切掉 39(按钮压扁),只加 height 则内容仍贴 y=0。
  - 满高容器(邮件详情):`padding({ top })` 加在 `@Entry` 包装层。
    ★ **不能加在 `MailDetailView` 里面**:它同时被 `MainPage` 的 Navigation 内嵌复用,
      而那层已经让过位了 —— 加在里面就变成让两次(39vp 变 78vp)。
      这类"同一组件两种入口"的坑与"悬浮加号要放在 Navigation 内部"同源:
      **让位的量取决于它被挂在哪一层**。

## 判据(harmony-window 8 → 9 条)

接线⑤ 枚举**所有** `@Entry` 页并要求它们消费避让 —— 口径是
"有人在窗口上开了全屏 ⇒ 每个 @Entry 页都得让",而不是"检查 MainPage 做了没有"。
后者在新增一个推上来的页时会静默逃掉,而"新增一个页"正是最常发生的事。

豁免要带**可机器复核**的理由(不再是"这个页先不管"):
- `Index.ets`:DevEco 模板欢迎页,不在 `main_pages.json` 流程里;
- `LoginPage.ets`:根容器 `.align(Alignment.Center)` ⇒ 结构上碰不到 y=0。
  但"居中"是可能被改掉的性质 ⇒ 判据**断言那个居中写法仍然存在**,
  谁把它改成贴顶,这条先红,逼他回来重新想这个页要不要避让。

**变异自检两个方向都跑过**:
- 把 ComposePage 避让整个拿掉(重演"只给 MainPage 加")⇒ 红 ✓
- 只加 padding 不加 height(会压扁按钮)⇒ 红 ✓

★ 期间还修掉两处"判据锚在当时的字符串上"(不是放宽,是它把"加一个正当的避让"
  与"真犯那个错"判得一模一样):
  - `harmony-nav` ④ 的留白断言、`harmony-widescreen` ④ 的 navReserve 断言,
    都改成剥注释后验**形状与不变量**,而不是写死字面表达式。改完变异仍咬得住。
2026-09-18 10:34:00 +08:00
686193458a fix(harmony): 底栏黑带 / 联系人点不开 / 我的页不守导航 / 顶栏硬截断
用户(2026-09-17)连报五条:「一点动画都没有」「底部那个黑条是啥意思」
「日历页面、我的页面、联系人页面哪个跟 webui 对齐了」「联系人页面连点都点不开」
「你的顶栏为什么还是硬截断而不是渐变」。逐条查证后修:

── ① 底部黑条 = 系统导航栏区域,不是我们画的 ──
根因:窗口默认给系统导航条留位,那块在深色下是黑的;界面于是看起来底下多一条黑带。
修法:`setWindowSystemBarEnable(['status'])` 隐藏系统导航条(底部那条本就是我们自绘的
悬浮玻璃条),内容铺满全高。
★ 刻意**不用** `setWindowLayoutFullScreen(true)`:实测试过,它也能消掉黑带,但会连
状态栏区域一起吃进布局,页签栏被时钟/电量盖住(截图硬证「07:43」与「收件箱」重叠)。

── ② 联系人点不开(用户原话「连点都点不开」)──
根因:`ContactItem` / `WorkCard` **根本没有 onClick** —— 卡片画出来了,
但没有任何点击路径。当时判据只钉了"字段与 WebUI 一致",没钉"点了会发生什么"。
修法:点卡片打开那条会话的邮件列表,就地在**本窗格内**展示(新增 `SessionMailsView`
+ `GET /sessions/{id}/mails`),底部导航保留;每封可再点进详情。

── ③ 我的页不遵守导航规则 ──
根因:点底栏「我的」走 `pushUrl('pages/SettingsPage')` 推**独立 @Entry 页** ⇒
底部导航整条消失,要按返回才能再切窗格。WebUI 里 `account` 只是一个 `viewMode`,
与收件箱同级、导航常驻。
修法:`SettingsPage` → `SettingsPane`(去掉 @Entry 与返回键,从 main_pages.json 摘除),
作为**第 4 个内容窗格**挂到 `currentIndex === 3`;`NAV_CONTENT_COUNT` 3 → 4,
`NAV_ITEMS` 第 4 项去掉 `route`;拨动动画包 `getUIContext().animateTo`。
实测:`我的` 页底部导航可见,可滚到「退出登录」「管理」。

── ④ 顶栏硬截断 ──
根因:`Scroll` 与 `List` 都能挂 `fadingEdge`(同在 `ScrollableCommonMethod` 上),
但**只有 MainPage 的 List 挂了** —— MailDetailPage / CalendarPage / SettingsPage /
AdminUsersPage / SessionsPage / InboxPage 的滚动容器全漏。同一次滚动里
列表是渐隐、正文是硬切,看起来就是"顶栏没做渐变"。
修法:13 个容器全部补齐(LoginPage 例外:它的 Scroll 是整页根、不是列表,
写了理由)。实测:视口上沿被切断的文字最暗 170–196,而下方不透明正文是 **19**。

── ⑤ 一点动画都没有 ──
实测确认改造前全仓 `animateTo` / `transition` / `animation` **一次都没用过**。
补:Theme 加令牌(durFast 120 / durBase 180 / easeOutSoft = cubic-bezier(0.22,1,0.36,1),
与 WebUI `--dur-*`/`--ease-out-soft` 同值同曲线);切窗格包 `animateTo`;
导航项选中变色加 `.animation`。

判据(218 passed / 0 failed):
· 新增「渐隐覆盖**所有**页面滚动容器」:按文件枚举 + 计数配平,
  并自检扫到的容器总数(防正则写坏 ⇒ 全绿)。**这条一写就抓到 LoginPage**。
· ①/②/⑤ 与 headless 那几条按新事实改写:内容窗格 3→4、
  `normalizeNavIndex(3)` 归一到自己、导航项里**禁止**再出现 `pushUrl`
  (那正是导航消失的形状)。
· 修掉自己引入的两处判据缺陷:`itemCode` 未定义;以及一条**窗口式断言**
  (从内容层 `}` 往后扫,靠数花括号定界,而注释里就有大量 `.padding({`,
  切片长到 1700+ 字符一路扫进 `onAreaChange` ⇒ 假红),改为纯负向断言。

未验:真机观感;横屏 Auto Split 双栏;日历页深度对齐(本轮只补了渐隐)。
2026-09-17 21:15:11 +08:00
cfe7808af4 ke: 鸿蒙 UI 整改:emoji→AmIcon 图标、宽屏侧栏、Md 第三方库、通知可配置项
- common/Icons.ets(生成器产出):43 枚与 WebUI icons.tsx 同几何的 Path 图标
- NavItems.ts:icon(emoji) → iconKey,与 WebUI 导航同图标
- 全页面 emoji 清除(空态改单行灰字,按钮/头像/刷新等换 AmIcon)
- MailDetailPage 接入 @luvi/lv-markdown-in(第三方,替代手写解析器)
- WideSidebar + MainPage isWide(≥768vp):宽屏侧栏 60vp 图标轨、藏底部条、onAreaChange 断点
- 通知=可配置项(默认关):PushService.isEnabled/setEnabled + reportToken 最前 gate (关=不取token/不弹权限/不打网关)+ SettingsPage Toggle + 服务端通道状态提示
- EntryAbility.onCreate 解析 want:冷启点通知也能路由(原来只有 onNewWant)
- 判据:harmony-widescreen(4) + harmony-push-optin(4) 新增;harmony-nav 同步 iconKey/条件 padding
2026-09-16 07:46:52 +08:00
7b16fec61c fix(harmony): AdminUsersPage 的 Chip 参数放宽到 ResourceColor —— 三个编译期错误(不是警告)
`Chip(text: string, bg: string, fg: string)` 收不下 `Theme.surfaceMuted` /
`Theme.textSubtle`(它们是 `$r('sys.color.*')` ⇒ `Resource`)。于是
`user.role === 'admin' ? Theme.chipNeutralBg : Theme.surfaceMuted` 这类三目
在三处报:

  Argument of type 'string | Resource' is not assignable to parameter of type 'string'
  Argument of type 'Resource' is not assignable to parameter of type 'string'
  (AdminUsersPage.ets:353 / 357 / 368)

`fontColor` / `backgroundColor` 本来就收 `ResourceColor`,所以放宽参数类型即可 ——
不必把系统语义色抄成字符串(那正是 Theme.ets 文件头要避免的事)。

归属与复核(2026-09-15):
- 这三条在 `474cada` / `b806a05` / `7f4fa26` 上**一直红着**,是**提交树里**的错误,
  不是任何人的在飞改动。复核方式:`git show 7f4fa26:…/AdminUsersPage.ets | grep -n 'Chip(text'`
  ⇒ `bg: string`;该文件自 `474cada` 起未被改过(`git log -- <file>`)。
- 同一批的另两条(MainPage.ets 的 `arkts-no-misplaced-imports`)由**别人**在写,
  我没有 stage / 没有改那份文件。
- 实测:修前 `hvigorw assembleHap` = BUILD FAILED(6 个 arkts 错误,
  含 MainPage 两条 misplaced-imports);MainPage 那份修好后本笔使整个构建
  BUILD SUCCESSFUL(本轮实测)。
2026-09-15 11:33:27 +08:00
474cadaf54 跨端: harmony 管理页(用户管理)+ P4c 壁纸上传入口 —— 「功能做全再交付」的两块
pi 的交付清单里缺的两块(`docs/GUI-PLAN-HARMONY.md` 原先把管理后台划在首版之外,
用户明确要求「功能做全再给我」之后收进来)。

标 `跨端:` 是因为本次的判据落在 `client/electron/test/`(鸿蒙的判据目录一向量在那里),
代码本体全在 `client/harmony/`。

## 管理页(用户管理)

- `pages/AdminUsersPage.ets`:新建 / 编辑(显示名·角色·白名单)/ 启停 / 重置密码。
  排布照 `AdminUsersPage.tsx`,包括「受限」徽标的口径(普通用户且白名单非空才显示)、
  最后登录缺席与空串都显示「从未登录」、管理员对白名单两项忽略。
- 入口在设置页底部,**仅管理员可见**(`role === 'admin'` 严格相等,与 `App.tsx` 同口径)。
  读不到身份时**不**显示也不报错(乐观放行会让每个普通用户看到一个点进去 403 的入口)。
- `api/AdminApi.ets` + `model/AdminUsers.ts`(纯逻辑,零 import ⇒ 判据能真跑)。
- 启停**只发 status 一个字段** —— 服务端是部分更新,多发字段会把显示名与白名单一起改掉。
- `model/Models.ets` 补管理端 DTO;`main_pages.json` 注册路由。

## P4c 壁纸上传

- `model/ImagePrep.ts`:阈值与两档策略(2560/0.85 → 1280/0.78,入口 20MB,压后上限 3.5MiB)。
  **一处有意不对齐 WebUI** 并写明理由:WebUI 卡 data-URL 长度(含 base64 膨胀),
  鸿蒙内存直传 ArrayBuffer,卡的是字节数。
- `common/BackgroundPicker.ets`:不设 / 预设 / 自定义图片 + 浓度与模糊滑杆。
  上传链:picker → 判可不可以 → 逐档按 desiredSize 解码压缩 → 上传 → **请页面以服务端为准重新同步**。
  失败**必带原因**(服务端 415/413 文案原样透出)。
- `ApiClient.uploadBytes`:MultiFormData.data 收 ArrayBuffer(核了 SDK,since 11;本工程 23)
  ⇒ 内存直传,不需要 base64、也不需要临时文件。
- 用户取消选图**不算失败**,什么都不说。

## 顺带修掉的两处真问题(都是变异测试逼出来的)

1. **压缩循环的第二档此前是死代码**:循环里的 break 与循环外那句 shouldRetryWithActual
   互相抵消 —— 把循环里那处改成 `if (true)`(永远只压一档)整套判据照样全绿。
   收成一处判定(overLimit),循环外只读结论。
2. **壁纸的模糊档一直是「只写不读」**(计划文档 §7.12 登记过):滑杆能拖、值能存、
   blurStyleFor 也写了,就是**没有调用点**,壁纸一点没糊。本次补上调用点
   (壁纸层 .blur(px) = 图片内容模糊;导航条材质由 blurStyleFor 映射)。
   同时按 §7.12 的原承诺更新了那一行。

## 一并修正的旧判据(都是"太宽/太窄",不是放宽标准)

- 「模糊归属」:原文「壁纸层不许有**任何**模糊调用」把**图片内容模糊**与**面板材质**
  混为一谈(WebUI 侧核实:.app-backdrop 的 filter 与它之上那层的 backdrop-filter
  是两个不同的量)⇒ 改成按两种模糊分别钉。
- 「bgBlur 只写不读,消费侧必须为 0」:值不再成立,**形状保留**(逐文件登记 + 计数 + 理由),
  标题与断言里的假话一并改掉。
- isDarkMode 那条 `/dark\s*\)/` 断的是**参数顺序**(加一个入参就误红)⇒ 改成"dark 在实参里"。
- 导航材质三处断言原本钉 `Theme.navMaterial` 字面量 ⇒ 改成钉新的映射写法。

## 判据

新增 `harmony-admin.test.mjs`(22 条)、`harmony-imageprep.test.mjs`(29 条);
`harmony-presets.test.mjs` 加 1 条(模糊档搬运与归一,含 -0 那个洞)。
全量 203 条:**201 通过**,2 条失败为**改动前就红**的既有项
(BUILD_INFO 比对、词表↔余额)—— 用 stash 对照验证过。

两个新判据文件上跑了 **48 个变异体,全部被抓**(含"接线"类:删掉「受限」徽标、
组件自己宣布成功、release 不 await、按原图尺寸解码…),
其中 2 个变异体**红不了**,因此又补了 5 条判据(纯逻辑接线、退档判定只有一处、
两档都超限必拒、解码尺寸用的是目标尺寸而非原图尺寸、模糊档搬运)。
(数字口径:按 runner 的真实条件"锚点恰好命中 1 次才算跑过"统计;
另有 4 条锚点不命中、根本没跑,不算在这 48 里。我第一次写的是"40"——
凭记忆累加的,错了,已更正。)

**未验**:本机无设备/无模拟器 ⇒ 全部观感未验(管理页排版、滑杆手感、模糊在真机上的
实际档位观感)。代码齐 ≠ 真机验过。
2026-09-15 11:03:22 +08:00