用户:「你自己看看宽屏的侧边栏和webui有哪怕一丁点的相似之处嘛?」
并排截图(WebUI 1100×700 @2x vs 三折叠展开态 3184×2232)之后,差异一眼可见:
| | WebUI | 鸿蒙(改前) |
|---|---|---|
| 文字标签 | **有**(通信/日历/联系) | 没有 |
| 选中态 | **浅蓝底块** | 只换颜色 |
| 「我的」 | 底部头像按钮进入 | 甩给 `onSettings` → **pushUrl 推页** |
| 品牌标颜色 | `#475569` 石板灰 | 品牌蓝 |
| 项间距 | 48px 项 + 4px gap,**贴顶一簇** | `layoutWeight(1)` 等分铺满(395px/项) |
## 根因:`WideSidebar` 里那段"复刻 WebUI"的注释是**编的**
```
* WebUI 的 `Sidebar`(60px 宽)是**纯图标轨**(无 label 文字)……
* 选中态:图标变色(`navFgActive`),**不加背景块、不加指示条、不加文字**
* (用户 2026-09-16:「底部导航栏不允许有文字」⇒ 侧栏同样按纯图标走)
```
两条都错,而且都能在源码里当场证伪:
- `Sidebar.tsx:110` 明明有 `<span className="text-3xs leading-none">{short}</span>`
—— 通信/日历/联系三个标签一直都在;
- `index.css:1590` 的 `.nav-item[data-active='true'] { background-color: … }`
就是底块,而且 CSS 注释**专门**说了侧栏必须有它:
「宽屏侧栏是 48px 宽的竖条,图标底下那一块底色是它**唯一的选中线索**,
所以"只变色"不能无差别推广到所有 `.nav-item`。」
我犯的错是**把底栏那条纪律套到了侧栏上**:用户 2026-09-14 说「底部导航栏选中
对应的文字和图标变色即可」、2026-09-16 说「底部导航栏不允许有文字」——
两句都针对**底部导航栏**,而侧栏是另一种东西(`index.css:1595-1606` 把这个区别
写得很清楚)。更糟的是我把这个错误**写进了判据**(`harmony-widescreen` ②③ 与
`harmony-nav` 的宽屏分支),于是判据锁住的是我编的理由,一路全绿。
## 修
- 侧栏项 = **图标 + 文字标签 + 选中底块**(`navActiveBg` = `--nav-active-bg` #DBEAFE,
判据**直接读 WebUI 的 CSS** 取值,不写死、更不引自己的注释)。
- 品牌标:`navBrandFg` = `#475569`(**像素取证**:2x 截图里品牌标附近最常见的墨色
是 `rgb(71,85,105) ×206` = `--nav-fg-muted`,即中性石板灰,**不是**品牌蓝);
尺寸/圆角按 WebUI `w-10 h-10 rounded-xl`(40×40、圆角 16);点它回收件箱。
- 项**贴顶一簇**(`Column({ space: 4 })` = WebUI 的 `gap-1`),不再 `layoutWeight(1)`。
- 删掉单列的"设置"入口(`onSettings` 回调一并删除)—— 那正是用户 2026-09-17 报过的
「我的页面完全没有遵守 nav 的导航规则」(push 页 ⇒ 侧栏整条消失)。
「我的」由 `ForEach(NAV_CONTENT_ITEMS)` 覆盖(该常量**含第 4 项**,
走 `onSelect(3)` = 窗格,与底栏同一套)。
- 补避让:侧栏原先**完全没有** `topInset` ⇒ 全屏之后品牌标被状态栏时钟压住。
## 判据(并修掉它们锁住的错误)
- `harmony-widescreen` ②③ **重写**:从"纯图标 / 只变色"改成
"有文字标签 / 有选中底块 / 不许留 `onSettings`",并读 WebUI `index.css` 拿真实色值。
- `harmony-nav` 宽屏分支:原来断言「侧栏项**不该有文字**」—— 同一条编造。
改成"图标(Path)画出来了 **且** 文字命中源码 `NAV_ITEMS`"。
- `harmony-nav` 宽屏形状阈值 `boxH > screenH*0.08` 是**错的**:48vp 项在密度 2.875 下
是 138px,而阈值要求 >178px ⇒ 四项全被滤掉(当时"通过"只是因为项被另一个 bug
压成了 39vp)。改成 `*0.04`,并补一条"必须有文字"把**品牌标**(40vp 无文字的可点方块)
排除在外。
**变异测试 3 个方向全咬**:去掉文字标签 ⇒ 红;去掉选中底块 ⇒ 红;Theme 色值写错 ⇒ 红。
★ 顺带记一条**我差点犯的错**:我一度按 density 3.5 换算,算出"60vp 侧栏被压成 49.4vp",
去查 flex 压缩、加 `.flexShrink(0)` —— 全是假的。实测密度是 **2.875**
(`138px ÷ 48vp = 2.875`),侧栏 173px ÷ 2.875 = **60.2vp**,与声明完全一致。
**没有压缩,是我除错了。** 已撤回那笔改动并把口径写进注释。
harmony-widescreen 6/6、harmony-nav 18/18、harmony-window 9/9、harmony-arkts 5/5、
harmony-contacts 5/5、harmony-calendar 30/30、harmony-system-api 5/5、harmony-logic 28/28。
426 lines
23 KiB
Plaintext
426 lines
23 KiB
Plaintext
/*
|
||
* AgentMail 鸿蒙客户端 — 设计令牌
|
||
*
|
||
* ## 这个文件在 2026-09-14 换了一次"来源"(jianf:「鸿蒙要求用系统方案」)
|
||
*
|
||
* 原先这里的每一格都是**手抄 WebUI 的色值**(`#F8FAFC`、`#0F172A`…)。现在分成两类,
|
||
* 分界线是"**系统有没有这份语义**":
|
||
*
|
||
* - **表面 / 文字 / 分隔 / 遮罩 / 圆角 / 材质 —— 交给系统**(`$r('sys.color.*')`、
|
||
* `$r('sys.float.*')`、`BlurStyle.*`)。理由不是"看起来更原生",而是三条实际的:
|
||
* ① 它们会**跟随深色模式**(手抄的值不会,这正是 WebUI 那边"半深不浅"的病根);
|
||
* ② 跟随系统动效/无障碍设置;
|
||
* ③ 少一处会漂移的副本 —— 这些格子的值不再由我们决定,也就不会再和系统打架。
|
||
* - **品牌色与业务语义色 —— 仍然自己写**:`accent = #2563EB` 是**跨客户端身份**
|
||
* (两个客户端是同一个产品),不能退化成随主题/厂商皮肤变的系统强调色;
|
||
* 权限三档(蓝/绿/琥珀)与预算三档(灰/橙/红)在系统里**没有对应物**
|
||
* (系统只有 warning/alert 两个"情绪色"),硬套会把语义丢掉。
|
||
*
|
||
* ## 名字不是猜的
|
||
*
|
||
* `sys.*` 的名字一旦写错,**编译期不报**、只有真机运行到那一行才炸 —— 而本机没有设备。
|
||
* 所以名字全部对着 SDK 自带的系统资源名表核过:
|
||
* `sdk/default/openharmony/toolchains/id_defined.json`(本机 API 26,7826 条)。
|
||
* 而且有一条判据(`client/electron/test/harmony-system-api.test.mjs`)持续盯着这件事:
|
||
* 源码里每个 `$r('sys.<type>.<name>')` 都必须在表里存在且类型相符。
|
||
*
|
||
* ## 保留的取舍
|
||
*
|
||
* - WebUI 的"正文面"是 0.88 不透明的玻璃(字要读得清);鸿蒙用的系统卡片底色也是不透明的,
|
||
* 结论一致:**正文面不透,只有浮在内容之上的那一层用材质**("玻璃只出现在一层")。
|
||
* - 字体大小仍与 WebUI 的 text-2xs/xs/sm 对齐(字号不是"系统方案"要解决的问题,
|
||
* 两边的版式意图是同一套)。
|
||
*/
|
||
|
||
import { curves } from '@kit.ArkUI';
|
||
|
||
export class Theme {
|
||
// ─────────────── 表面:系统语义色 ───────────────
|
||
|
||
/** 页面底色(系统 `ohos_id_color_background`:浅色白/深色深灰,自动跟随主题) */
|
||
static readonly pageBg: Resource = $r('sys.color.ohos_id_color_background');
|
||
/** 承载文字的面 = **列表卡片底色**(对应"每项一张卡/气泡",而不是通栏底色) */
|
||
static readonly surface: Resource = $r('sys.color.ohos_id_color_list_card_bg');
|
||
/** 次级面(分组底、列表行 hover) */
|
||
static readonly surfaceMuted: Resource = $r('sys.color.ohos_id_color_sub_background');
|
||
/** 分隔线 */
|
||
static readonly border: Resource = $r('sys.color.ohos_id_color_list_separator');
|
||
|
||
// ─────────────── 文字:系统语义色(三级) ───────────────
|
||
|
||
static readonly textPrimary: Resource = $r('sys.color.ohos_id_color_text_primary');
|
||
static readonly textMuted: Resource = $r('sys.color.ohos_id_color_text_secondary');
|
||
static readonly textSubtle: Resource = $r('sys.color.ohos_id_color_text_tertiary');
|
||
|
||
// ─────────────── 遮罩与材质:交给系统 ───────────────
|
||
|
||
/**
|
||
* 遮罩(模态/淡出层):系统遮罩色。
|
||
*
|
||
* 这里原来存着 `overlayColor` + `overlayAlpha` 两个常量,再用 `overlay()` 拼成
|
||
* `#AARRGGBB` —— 之所以要"色与透明度分开",是因为**遮罩色必须随主题换向**
|
||
* (浅色主题用白把图案洗淡、深色主题必须换黑,否则浅色照片在深色界面里糊成一块亮斑)。
|
||
* 那件事现在由系统做:一个语义色就够,且换向不会再漏。
|
||
*/
|
||
static readonly overlay: Resource = $r('sys.color.ohos_id_color_mask_regular');
|
||
|
||
/**
|
||
* 壁纸遮盖层的颜色 —— **不是** `overlay`(那是模态遮罩),两者语义不同。
|
||
*
|
||
* pi 2026-09-14 提出的方向性疑问,我按他说的去 SDK 里把值读出来了
|
||
* (`ets/build-tools/ets-loader/sysResource.js` 给 名字→id,
|
||
* `previewer/common/resources/entry/resources.txt` 给 id→值,两张表交叉验证):
|
||
*
|
||
* | 令牌 | 浅色主题 | 深色主题 |
|
||
* |---|---|---|
|
||
* | `ohos_id_color_mask_regular`(= `overlay`) | `#99182431` **深蓝灰** | `#b2000000` 黑 |
|
||
* | `ohos_id_color_background` | `#ffffffff` **白** | `#ff18181a` 近黑 |
|
||
*
|
||
* 也就是说 **`mask_*` 在两套主题下都是深色**(它的 light/regular/thick 是**浓度档**,
|
||
* 不是深浅两套值),用途是**模态遮罩**(弹层背后压暗)。
|
||
* 而 WebUI 的 `--bg-scrim` 在 `:root` 是**白**、`.dark` 才是黑,注释原话是
|
||
* 「浅色下用白把花哨的图案洗淡,深色下用黑压暗」—— 它做的是"**朝页面底色淡化**"。
|
||
*
|
||
* 所以壁纸遮盖若用 `mask`:**浅色主题下会把预设压暗,而 WebUI 是把它洗淡 —— 方向相反**。
|
||
* 这是"机制上确定不同",不是观感。改用**页面底色系**:那才是"朝底色淡化"的系统对应物,
|
||
* 浅色=白底方向、深色=深底方向,自动换向,与 WebUI 是同一个意图。
|
||
*
|
||
* ⚠️ 为什么不干脆复用 `pageBg`(值一样是 `ohos_id_color_background`):
|
||
* 页面底在背景开启时会被换成语义上的**透明**(`bgActive ? Color.Transparent : Theme.pageBg`),
|
||
* 而遮盖层**永远需要一个真实颜色**。两个用途的生命周期不同,共用一个名字迟早坏一头
|
||
* —— 这正是这个仓库撞过四次的模式(`text-white` vs `bg-white`、`--c-*` vs `--s-*`)。
|
||
*/
|
||
static readonly wallpaperScrim: Resource = $r('sys.color.ohos_id_color_background');
|
||
|
||
/**
|
||
* 导航/浮层的材质档次。**不再有 `#B8FFFFFF` / `#B80F172A` 这种手写玻璃 alpha** ——
|
||
* 那两个值等于"我们替系统猜了深色该怎么做",与"用系统方案"直接冲突;
|
||
* 而材质档次是系统给的,深浅两套颜色由系统按主题挑。
|
||
*
|
||
* 用 `COMPONENT_THICK`:贴在界面组件上的一层材质(导航条正属于这一类)。
|
||
*/
|
||
static readonly navMaterial: BlurStyle = BlurStyle.COMPONENT_THICK;
|
||
|
||
// ─────────────── 圆角:系统尺寸 ───────────────
|
||
|
||
/** 卡片圆角:系统"卡片"圆角(不再与 WebUI 的 14vp 绑死 —— 允许各自跟随系统) */
|
||
static readonly radiusCard: Resource = $r('sys.float.ohos_id_corner_radius_card');
|
||
/** 控件圆角:系统"按钮"圆角 */
|
||
static readonly radiusControl: Resource = $r('sys.float.ohos_id_corner_radius_button');
|
||
|
||
/*
|
||
* ★ 2026-09-16:宽屏玻璃面板几何(与 WebUI `app-shell` 的 CSS 变量**同值**)。
|
||
* `--radius-card: 0.875rem` = 14px、`--pane-gap: 10px`(`index.css`)。
|
||
* 写死常量而不是 `$r('sys.*')`:因为 WebUI 是 14/10,而系统的
|
||
* `corner_radius_card` 是 16 —— 宽屏面板必须用 WebUI 的值才能"一比一复刻"。
|
||
* 原有的 `radiusCard`/`radiusControl` 保持系统跟随,不动它们。
|
||
*/
|
||
static readonly glassRadius: number = 14;
|
||
static readonly paneGap: number = 10;
|
||
|
||
/*
|
||
* ─────────────── 手写色**登记表**(改这里要一起改判据) ───────────────
|
||
*
|
||
* 为什么需要这张表:本文件里"自己写的色值"是有理由的(品牌身份 + 系统没有对应物的
|
||
* 业务语义色),但**理由不能靠记忆**。判据(`cross-client-theme.test.mjs` 的
|
||
* A2 条)会枚举本文件里所有 `static readonly X: string = '#……'`,
|
||
* 发现**未登记的名字就判红** —— 于是"新写死一个色"必须显式过一道:
|
||
* 要么登记在这里并写清理由,要么它本来就该走 `$r('sys.*')`。
|
||
*
|
||
* 这条是 pi 读出来的真缺口:只枚举 11 个"必须是系统资源"的名字,挡不住
|
||
* 第 12 个**新加的手写色**(它不在名单里,于是 A/B/裸色值三条都碰不到它)。
|
||
* 「枚举挡实例,类才挡漂移」—— 这次枚举的是**名字**,所以要有名单。
|
||
*
|
||
* 登记项(17 个,名字与判据里的 SELF_OWNED_COLORS 逐字一致 —— 逐个列出而不是缩写,
|
||
* 这样 grep 一个名字就能找到它的理由):
|
||
*
|
||
* 品牌(跨客户端身份,系统给不了):
|
||
* · accent #2563EB 与 WebUI 的 --c-blue-600 逐字一致
|
||
* · accentFg #FFFFFF 品牌底上的文字
|
||
* · accentSoft #EFF6FF 品牌浅底(选中态背景)
|
||
* · accentStrong #1D4ED8 品牌深色变体(选中文字/边框)
|
||
*
|
||
* 业务语义(系统只有 list/warning/alert 这一档,没有"同意/拒绝"):
|
||
* · approve #15803D 同意(绿)
|
||
* · approveBg #F0FDF4 同意底
|
||
* · approveFg #15803D 同意文字
|
||
* · danger #B91C1C 拒绝/危险(红)
|
||
* · dangerBg #FEF2F2 拒绝底
|
||
* · warnBg #FFFBEB 警示底(对应 WebUI --c-amber-50)
|
||
* · warnFg #B45309 警示文字(对应 WebUI --c-amber-700)
|
||
*
|
||
* 档位胶囊(档位是产品语义:系统不认识 plan / workspace / full):
|
||
* · chipNeutralBg #F3F4F6 plan 档底(最宽档但不报警)
|
||
* · chipNeutralFg #5A6270 plan 档文字
|
||
* · chipSpentBg #FEE2E2 预算耗尽底
|
||
* · chipSpentFg #B91C1C 预算耗尽文字
|
||
* · chipWarnBg #FFEDD5 预算紧张底
|
||
* · chipWarnFg #C2410C 预算紧张文字
|
||
*
|
||
* 不在这里的手写色只有一种合法去处:`$r('sys.*')`(跟随系统/深色模式)。
|
||
*/
|
||
|
||
// ─────────────── 品牌色:跨客户端身份,必须自己写 ───────────────
|
||
|
||
/**
|
||
* 品牌蓝(按钮、链接、焦点环)。
|
||
*
|
||
* **不要**为了"用系统方案"把它换成系统的强调色:系统强调色会随主题/厂商皮肤变,
|
||
* 一旦换过去,"两个客户端是同一个产品"这件事就靠不住了。
|
||
* 这是整个文件里**唯一必须与 WebUI 逐字一致**的取值(判据钉住)。
|
||
*/
|
||
static readonly accent: string = '#2563EB';
|
||
static readonly accentFg: string = '#FFFFFF';
|
||
/** 品牌蓝的浅底 / 深前景(与 WebUI 的 blue-50 / blue-700 成对) */
|
||
static readonly accentSoft: string = '#EFF6FF';
|
||
static readonly accentStrong: string = '#1D4ED8';
|
||
|
||
/** 导航文字:未选中 / 选中(对应 --nav-fg-muted / --nav-active-fg) */
|
||
static readonly navFg: Resource = $r('sys.color.ohos_id_color_text_secondary');
|
||
static readonly navFgActive: string = Theme.accentStrong;
|
||
/*
|
||
* 侧栏**选中块的底色**(WebUI `--nav-active-bg: 219 234 254`)。
|
||
*
|
||
* ★ 为什么侧栏有底块、底栏没有(这是两种导航**纪律不同**,不是不一致):
|
||
* `index.css:1595-1606` 的原话:「底部导航的选中态**只换颜色**……
|
||
* 只作用在 `.narrow-nav` 上:宽屏侧栏是 48px 宽的竖条,图标底下那一块底色
|
||
* 是它**唯一的选中线索**,所以"只变色"不能无差别推广到所有 `.nav-item`。」
|
||
* 我 2026-09-18 之前把底栏那条纪律套到了侧栏上,还在 `WideSidebar` 的注释里
|
||
* 写成「WebUI 的 Sidebar 是纯图标轨(无 label 文字)」—— 那是**编的**:
|
||
* `Sidebar.tsx:110` 明明有 `<span className="text-3xs">{short}</span>`,
|
||
* `nav-item[data-active='true']` 也明明有底色。并排截图一眼就能看出来。
|
||
*/
|
||
static readonly navActiveBg: string = '#DBEAFE';
|
||
/*
|
||
* 侧栏**品牌标**的墨色(WebUI `--nav-fg-muted: 71 85 105` = #475569)。
|
||
*
|
||
* ★ 用像素取证过,不是照着色阶猜的:在 2x 缩放的 WebUI 侧栏截图上统计
|
||
* 品牌标附近最常见的墨色 → `rgb(71,85,105) ×206`,正是 `--nav-fg-muted`。
|
||
* 也就是说 WebUI 的品牌标是**中性石板灰**(`.nav-item` 默认色),
|
||
* **不是**品牌蓝。鸿蒙这边原来画成 `Theme.accent`(蓝)—— 并排一看就不同色。
|
||
*
|
||
* 深色主题下 `--nav-fg-muted: 148 158 175`(见 index.css:1559)。
|
||
* 鸿蒙暂未跟:`$r('sys.color.*')` 那套只在系统语义色上跟随,而这个值
|
||
* 是 WebUI 自己定的中性色。这一条**记在案上**,不假装已经跟了。
|
||
*/
|
||
static readonly navBrandFg: string = '#475569';
|
||
|
||
// ─────────────── 业务语义色:系统没有对应物,继续自己写 ───────────────
|
||
|
||
/** 语义色:同意 / 拒绝(对应 WebUI 的 approve/danger) */
|
||
static readonly approve: string = '#15803D';
|
||
static readonly danger: string = '#B91C1C';
|
||
static readonly dangerBg: string = '#FEF2F2';
|
||
static readonly approveBg: string = '#F0FDF4';
|
||
static readonly approveFg: string = '#15803D';
|
||
/** 警示面/前景(对应 WebUI 的 --c-amber-50 / --c-amber-700)—— 权限 full 档、待决策徽标 */
|
||
static readonly warnBg: string = '#FFFBEB';
|
||
static readonly warnFg: string = '#B45309';
|
||
|
||
/**
|
||
* 权限档位 → 徽标底色 / 文字色。
|
||
*
|
||
* 与 WebUI 的 `PermissionChip.tsx` **同一映射**(plan=蓝 / workspace=绿 / full=琥珀),
|
||
* 认不出的档位与空档位按 workspace 处理 —— 映射只有这一处实现,
|
||
* 页面不再各自 if-else 挑颜色。
|
||
*
|
||
* 为什么不用系统情绪色:系统只有 warning/alert 两个,凑不出三档;
|
||
* 而档位是**可被文档与判据按名字引用的枚举标识**(plan/workspace/full),
|
||
* 用情绪色顶替它,界面对了语义丢了。
|
||
*/
|
||
static permBg(mode: string): string {
|
||
if (mode === 'plan') {
|
||
return Theme.accentSoft;
|
||
}
|
||
if (mode === 'full') {
|
||
return Theme.warnBg;
|
||
}
|
||
return Theme.approveBg;
|
||
}
|
||
|
||
static permFg(mode: string): string {
|
||
if (mode === 'plan') {
|
||
return Theme.accentStrong;
|
||
}
|
||
if (mode === 'full') {
|
||
return Theme.warnFg;
|
||
}
|
||
return Theme.approveFg;
|
||
}
|
||
|
||
/** 中性 chip(对应 WebUI 的 --c-gray-100 / --c-gray-500)—— 预算条"还宽裕"档 */
|
||
static readonly chipNeutralBg: string = '#F3F4F6';
|
||
static readonly chipNeutralFg: string = '#5A6270';
|
||
/** 预算用尽(对应 WebUI 的 --c-red-100 / --c-red-700) */
|
||
static readonly chipSpentBg: string = '#FEE2E2';
|
||
static readonly chipSpentFg: string = '#B91C1C';
|
||
/** 预算将尽(对应 WebUI 的 --c-orange-100 / --c-orange-700) */
|
||
static readonly chipWarnBg: string = '#FFEDD5';
|
||
static readonly chipWarnFg: string = '#C2410C';
|
||
|
||
/**
|
||
* 往返预算档位 → chip 底色 / 字色。
|
||
*
|
||
* 与 WebUI 的 `BudgetChip` **同一映射**(`WorkCard.tsx`):
|
||
* 剩 0 = 红(用尽)、剩 ≤1 = 橙(将尽,任务需要人介入)、其余 = 中性灰。
|
||
* 档位本身由 `MailGrouping.ts` 的 `budgetState()` 算(那是可被判据执行的一层),
|
||
* 这里只管"哪个档用什么颜色"。这三个色**不进跨端"取值相同"判据**(业务局部),
|
||
* 但必须进枚举完整性判据(三档齐全、认不出的归一到中性档)。
|
||
*/
|
||
static budgetBg(state: string): string {
|
||
if (state === 'spent') {
|
||
return Theme.chipSpentBg;
|
||
}
|
||
if (state === 'warn') {
|
||
return Theme.chipWarnBg;
|
||
}
|
||
return Theme.chipNeutralBg;
|
||
}
|
||
|
||
static budgetFg(state: string): string {
|
||
if (state === 'spent') {
|
||
return Theme.chipSpentFg;
|
||
}
|
||
if (state === 'warn') {
|
||
return Theme.chipWarnFg;
|
||
}
|
||
return Theme.chipNeutralFg;
|
||
}
|
||
|
||
/** 字体大小(与 WebUI 的 text-2xs/xs/sm 对齐) */
|
||
static readonly fontTiny: number = 11;
|
||
static readonly fontSmall: number = 12;
|
||
static readonly fontBody: number = 14;
|
||
|
||
/*
|
||
* ── 动画令牌(对照 WebUI `index.css`)──
|
||
*
|
||
* 用户(2026-09-17):「一方面一点动画都没有」。
|
||
* 实测确认:改造前全仓 `animateTo` / `transition` / `animation` **一次都没用过** ——
|
||
* 所有页面切换、详情推入、面板展开都是硬切。
|
||
*
|
||
* ★★ 2026-09-18 更正:这里原先写着「三个数与 WebUI **逐字一致**」,**那句是错的**。
|
||
* 它把「令牌存在」当成了「动画用了那个令牌」。实测(`client/electron/src/index.css`):
|
||
*
|
||
* `--dur-base: 180ms` 只用在**壁纸淡入**(:747 `transition: opacity …`)
|
||
* `--ease-out-soft (0.22,1,0.36,1)` 只用在 **transition**(壁纸、控件变色 :1169)
|
||
* 而**所有 @keyframes 动画**用的是 150ms + `cubic-bezier(0.22, 0.61, 0.36, 1)`
|
||
*
|
||
* 全仓 `(0.22,0.61,0.36,1)` 出现 **5 次**(rise-in ×3 / cal-in ×2),
|
||
* `(0.22,1,0.36,1)` 只出现 **1 次**(就是令牌定义处)。**这是两根不同的曲线。**
|
||
* 旧代码把面板入场按 `(0.22,1,0.36,1)` + 180ms 做 ⇒ 比 WebUI **慢 30ms 且曲线偏软**,
|
||
* 而当时的判据(`harmony-nav` "缓动必须是 0.22,1,0.36,1")锚在**令牌**上,
|
||
* 所以这个错它一辈子抓不到。
|
||
*
|
||
* 所以下面分**两组**,各自对齐各自该对齐的东西:
|
||
* 控件类(transition) —— `durFast 120` + `easeOutSoft`
|
||
* 动画类(@keyframes)—— 各自时长 + `easeRise`
|
||
*/
|
||
/** 控件变色(hover / active)—— WebUI `--dur-fast: 120ms` */
|
||
static readonly durFast: number = 120;
|
||
/**
|
||
* 壁纸淡入 —— WebUI `--dur-base: 180ms`。
|
||
*
|
||
* ★ **只用于壁纸/整体淡入,不是面板入场**(面板入场是 `durRise`)。
|
||
* 这个区分是 2026-09-18 查出来的,之前两者被当成同一个数。
|
||
*/
|
||
static readonly durBase: number = 180;
|
||
/** 面板/内容入场 —— WebUI `@keyframes rise-in` 实测 **150ms** */
|
||
static readonly durRise: number = 150;
|
||
/** 弹层入场 —— WebUI `.animate-menu-in` 实测 **140ms** */
|
||
static readonly durMenu: number = 140;
|
||
/** 日历翻月 —— WebUI `.cal-slide-next/prev` 实测 **200ms** */
|
||
static readonly durCal: number = 200;
|
||
|
||
/**
|
||
* **transition** 用的缓动:`cubic-bezier(0.22, 1, 0.36, 1)` = WebUI `--ease-out-soft`。
|
||
*
|
||
* 用 `curves.cubicBezierCurve` 而不是 `Curve.EaseOut` 之类的枚举:
|
||
* 枚举是系统预设的**另一根**曲线,观感与 WebUI 对不上。
|
||
*/
|
||
static readonly easeOutSoft: ICurve = curves.cubicBezierCurve(0.22, 1, 0.36, 1);
|
||
|
||
/**
|
||
* **@keyframes 动画**用的缓动:`cubic-bezier(0.22, 0.61, 0.36, 1)`。
|
||
*
|
||
* ★ 与 `easeOutSoft` **不是同一根曲线**(前者第二段控制点是 0.61,后者是 1)。
|
||
* 两者别互换:WebUI 里 transition 用 `--ease-out-soft`,而所有动画
|
||
* (rise-in / cal-in)硬编码用的是这一根。
|
||
*/
|
||
static readonly easeRise: ICurve = curves.cubicBezierCurve(0.22, 0.61, 0.36, 1);
|
||
|
||
/**
|
||
* 窗格入场时的上浮量(vp)。
|
||
*
|
||
* 4 —— **与 WebUI `@keyframes rise-in` 逐字一致**(`index.css:1208`):
|
||
* `from { opacity: 0; transform: translateY(4px); }`
|
||
* 只上浮 4vp,是"轻轻落位"而不是"整块飞进来"。用户对"闪/跳"敏感
|
||
* (09-14 否掉过整屏淡入),所以这个数**不许**随手调大。
|
||
*/
|
||
static readonly riseInOffset: number = 4;
|
||
|
||
/**
|
||
* 窗格入场过渡:4vp 上浮 + 淡入(对齐 WebUI `@keyframes rise-in`)。
|
||
*
|
||
* 做成方法而不是常量,是因为 `TransitionEffect` 是**有状态的构建器**
|
||
* (`combine`/`animation` 会就地改自身),多处共用同一个实例会互相干扰;
|
||
* 每处调用各拿一个新的。
|
||
*
|
||
* ★ 时长/曲线用 `durRise` + `easeRise`(**150ms + 0.22,0.61,0.36,1**)——
|
||
* 即 WebUI `rise-in` 的**真实取值**。改之前用的是 `durBase(180)` + `easeOutSoft`,
|
||
* 那是令牌值而不是动画值(见上面令牌区的更正说明)。
|
||
*
|
||
* ★ 两边**不对称**:出场用 `durFast`(120ms)比入场快。
|
||
* WebUI 那边 `rise-in` 是对称的(一个 @keyframes 两用)—— 因为它是**逐个元素**
|
||
* 挂载即播,新旧两棵子树不会同时可见。而 ArkUI 这里是 `if/else` 换子树,
|
||
* 两层会**同时半透明地叠着**,同长看起来就是“闪一下”(实测 6 秒取样确认),
|
||
* 所以出场必须更快地让位。这是两端机制不同带来的**必要差异**,不是随手拍数。
|
||
*/
|
||
static paneRiseIn(): TransitionEffect {
|
||
return TransitionEffect.asymmetric(
|
||
TransitionEffect.opacity(0).combine(
|
||
TransitionEffect.translate({ y: Theme.riseInOffset })
|
||
).animation({ duration: Theme.durRise, curve: Theme.easeRise }),
|
||
TransitionEffect.opacity(0).animation({ duration: Theme.durFast, curve: Theme.easeRise })
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 弹层入场:下移 4vp + 缩到 0.985 + 淡入(对齐 WebUI `@keyframes menu-in`)。
|
||
*
|
||
* WebUI `.animate-menu-in` 的注释把适用范围钉得很窄,这里照搬同一个口径:
|
||
* 「只给**真正是弹层**的东西(候选/下拉菜单自己穿)」——
|
||
* 它原本还挂着 `html.view-switch .glass-control`,于是**每次切视图页面上所有
|
||
* 按钮与输入框一起淡入位移**(几十个元素同时动),2026-09-15 被摘掉。
|
||
* 所以本方法**只给弹层**(账号选择器、下拉候选),不要挂到常驻控件上。
|
||
*
|
||
* 入场方向是**从上往下**(`translateY(-4px)` → 0):弹层从触发点的下方展开,
|
||
* 从上方“落”下来。与 `rise-in` 的**上浮**方向相反,别混。
|
||
*/
|
||
static menuIn(): TransitionEffect {
|
||
return TransitionEffect.opacity(0).combine(
|
||
TransitionEffect.translate({ y: -4 })
|
||
).combine(
|
||
TransitionEffect.scale({ x: 0.985, y: 0.985 })
|
||
).animation({ duration: Theme.durMenu, curve: Theme.easeRise });
|
||
}
|
||
|
||
/**
|
||
* 日历翻月的横向滑入(对齐 WebUI `@keyframes cal-in-next/prev`)。
|
||
*
|
||
* `forward = true` → 下一月,从**右侧**滑入(`translateX(12%)`);
|
||
* `forward = false` → 上一月,从**左侧**滑入(`translateX(-12%)`)。
|
||
*
|
||
* 百分比在 ArkUI 的 `translate` 里是**相对元素自身尺寸**的 —— 与 CSS 语义一致,
|
||
* 所以 12% 就是“滑入自身宽度的 12%”,不是拍一个 vp 数(那样在大屏上会变小)。
|
||
*/
|
||
static calendarSlide(forward: boolean): TransitionEffect {
|
||
return TransitionEffect.asymmetric(
|
||
TransitionEffect.opacity(0).combine(
|
||
TransitionEffect.translate({ x: forward ? '12%' : '-12%' })
|
||
).animation({ duration: Theme.durCal, curve: Theme.easeRise }),
|
||
TransitionEffect.opacity(0).animation({ duration: Theme.durFast, curve: Theme.easeRise })
|
||
);
|
||
}
|
||
}
|