feat(harmony)★★: 背景档与预设改用官方 Chip,删掉重复的同步指示器
承接 2026-09-29 的反馈,本批改动是那一轮工作的收尾。
## 1. 自绘 Text → 官方 `Chip`
背景档与预设两排原来是**自绘 `Text`**,而且与设置页主题那排
**圆角/内边距不一致**(预设那排 `borderRadius(4)`,另两处是
`radiusControl`)。改用 `import { Chip, AccessibilitySelectedType,
LengthMetrics } from '@kit.ArkUI'`。
## 2. 删掉 `@Prop statusText`(一个数两个指示器)
它原来是「外观」卡右上角那个「已同步」的**第二份**:页面把同一个
`statusLabel(this.appearanceStatus)` 既画在卡片级标题行、又透传进来
画在「背景」小标题右边 ⇒ 同一屏两个「已同步」,永远同值。
删掉透传这一份,值只从页面进、只画一处。
## 3. Surface.ets(32 行)/ Theme.ets(51 行)/ ContactsTab.ets(21 行)
配套:Chip 的尺寸档、主题色取用、联系人页的调用点。
## 判据
- `test/harmony-appearance.test.mjs`:外观判据随实现更新
- `test/mutants/baseline.sha`:变异体自证底本同步
### ⚠️ 设备端判据本机跑不了(不是代码问题)
node --test test/harmony-appearance.test.mjs
pass 26 / fail 3
失败的 3 条**全部**是设备端断言:
★ 设备:打开背景开关后,壁纸上真的出现了
AssertionError: 要能拉起我们的应用并等到它到前台
★ 设备:窗格内容不得超出屏幕
AssertionError: 要能拉起应用
★ 设备:切换主题后界面真的整体换色了
AssertionError: 设备连续 10 轮拉不起应用(本条已连续 3 轮没真跑)
即**本机没有接 Harmony 设备**,不是断言挂在代码上。
判据文本本身断言「设备连续 10 轮拉不起」——它自己在如实报告
「这条从未在真机上跑过」。**需要真机补验**,这里不假装它通过了。
## docs
API.md / GUI-PLAN-HARMONY.md / PLAN.md 同步上述改动。
This commit is contained in:
@ -32,6 +32,12 @@ import { BusinessError } from '@kit.BasicServicesKit';
|
||||
import { ApiClient } from '../api/ApiClient';
|
||||
import { AppearanceApi } from '../api/AppearanceApi';
|
||||
import { Theme } from '../common/Theme';
|
||||
/*
|
||||
* ★ 2026-09-29 新增:官方 `Chip`(用户:「尽量使用官方组件而不是自绘」)。
|
||||
* 背景档与预设两排原来是自绘 `Text`,且与设置页主题那排**圆角/内边距各不相同**
|
||||
* (预设那排是 `borderRadius(4)`,另两处是 `radiusControl`)—— 见下方注释。
|
||||
*/
|
||||
import { Chip, AccessibilitySelectedType, LengthMetrics } from '@kit.ArkUI';
|
||||
import { PRESET_IDS, presetLabel, normalizePreset } from '../model/Wallpaper';
|
||||
import {
|
||||
CompressPlan,
|
||||
@ -81,8 +87,16 @@ export struct BackgroundPicker {
|
||||
@Link bgDim: number;
|
||||
/** 模糊 */
|
||||
@Link bgBlur: number;
|
||||
/** 服务端状态文案(只读透传:组件不解释它) */
|
||||
@Prop statusText: string = '';
|
||||
/*
|
||||
* ★ 2026-09-29 **删掉 `@Prop statusText`**(用户:「一个数两个指示器」的同类问题)。
|
||||
*
|
||||
* 它原来是「外观」卡右上角那个「已同步」的**第二份**:页面把同一个
|
||||
* `statusLabel(this.appearanceStatus)` 既画在卡片级标题行、又透传进来
|
||||
* 画在「背景」小标题右边 ⇒ 同一屏两个「已同步」,永远同值。
|
||||
*
|
||||
* 同步状态描述的是**外观整体**(主题 + 背景 + 浓度 + 模糊一起推的),
|
||||
* 不属于"背景"这一段,所以它留在页面卡片级那一处,组件不再复述。
|
||||
*/
|
||||
/**
|
||||
* **用户**改了背景四值 ⇒ 通知页面推服务端。
|
||||
*
|
||||
@ -257,37 +271,87 @@ export struct BackgroundPicker {
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
Row() {
|
||||
Text('背景').fontSize(Theme.fontBody).fontColor(Theme.textPrimary).layoutWeight(1)
|
||||
Text(this.statusText).fontSize(Theme.fontTiny).fontColor(Theme.textSubtleFor())
|
||||
}
|
||||
.width('100%')
|
||||
/*
|
||||
* 小标题降到 11px muted(原来是 `fontBody` 14px 不加粗):
|
||||
* 它与父级「外观」的 14 Bold 几乎同重,父子分不开(设备截图可见)。
|
||||
* 与设置页 `SectionTitle()` 同一套量级 —— 全仓"小标题"只此一种样子。
|
||||
*/
|
||||
Text('背景').fontSize(Theme.fontTiny).fontColor(Theme.textMuted)
|
||||
.width('100%').margin({ bottom: 8 })
|
||||
|
||||
Row() {
|
||||
/*
|
||||
* ★★ 2026-09-29 改:**自绘 chip → 官方 `Chip`**(用户:「尽量使用官方组件而不是自绘」)。
|
||||
*
|
||||
* ── 改前的「杂乱」到底在哪 ──
|
||||
* 同一个屏幕上三处自绘 chip,**圆角与内边距互不相同**:
|
||||
* · 这里(背景档) `radiusControl` + padding 12/6
|
||||
* · 预设那排 `borderRadius(4)` + padding 8/4 ← 圆角只有上面一半
|
||||
* · 设置页主题那排 `radiusControl` + padding 12/6
|
||||
* 设备 dump 也量得出来:背景档 chip 高 95px、预设 chip 高 73px。
|
||||
* 同类控件三种长相,谁也不比谁"对"—— 这就是用户说的「杂乱」。
|
||||
*
|
||||
* ── 改后 ──
|
||||
* 全部走官方 `Chip`,尺寸/圆角/内边距由组件按系统规范给,
|
||||
* 三处从此**同一套外观**。选中态用 `activated` + `activatedFontColor`,
|
||||
* 读屏软件按单选播报(`AccessibilitySelectedType.SELECTED` ——
|
||||
* 枚举成员叫 SELECTED 而不是 RADIO,见设置页那处的说明)。
|
||||
*
|
||||
* ★ 说清楚这次改的**只是形状**,不是配色:原来的三元
|
||||
* `.fontColor(选中 ? Theme.accentFg : Theme.textMuted)` 是**对的**
|
||||
* (选中态用的就是不翻转的 `accentFg`,没犯 2026-09-19 那个
|
||||
* "面色当墨水"的错)。`activatedFontColor: Theme.accentFg` 是把它
|
||||
* 原样搬到官方组件的参数上,语义不变 —— 别把这次改写成"修了对比度"。
|
||||
* 未选中态的字色由 `textMuted` 提到 `textPrimary`:官方 Chip 的
|
||||
* 未选中底是 `surfaceMuted`(比原来那张更实),配 `textMuted` 偏灰。
|
||||
*/
|
||||
Row({ space: 8 }) {
|
||||
ForEach(KIND_OPTIONS, (opt: KindOption) => {
|
||||
Text(opt.label)
|
||||
.fontSize(Theme.fontSmall)
|
||||
.fontColor(this.bgKind === opt.value ? Theme.accentFg : Theme.textMuted)
|
||||
.backgroundColor(this.bgKind === opt.value ? Theme.accent : Theme.surfaceMuted)
|
||||
.borderRadius(Theme.radiusControl)
|
||||
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
|
||||
.margin({ right: 8 })
|
||||
.onClick(() => { this.bgKind = opt.value; this.emitUserChange(); })
|
||||
Chip({
|
||||
label: {
|
||||
text: opt.label,
|
||||
fontSize: Theme.fontSmall,
|
||||
fontColor: Theme.textPrimary,
|
||||
activatedFontColor: Theme.accentFg
|
||||
},
|
||||
activated: this.bgKind === opt.value,
|
||||
backgroundColor: Theme.surfaceMuted,
|
||||
activatedBackgroundColor: Theme.accent,
|
||||
borderRadius: Theme.radiusControl,
|
||||
/*
|
||||
* ★ `allowClose: false` 必需(官方默认 **true**)—— 不关掉
|
||||
* 每个 chip 右边会长一个 ✕ 删除图标。详见设置页那处的长注释。
|
||||
*/
|
||||
allowClose: false,
|
||||
accessibilitySelectedType: AccessibilitySelectedType.SELECTED,
|
||||
onClicked: () => { this.bgKind = opt.value; this.emitUserChange(); }
|
||||
})
|
||||
}, (opt: KindOption) => opt.value)
|
||||
}
|
||||
.width('100%').margin({ top: 10 })
|
||||
|
||||
if (this.bgKind === 'preset') {
|
||||
Flex({ wrap: FlexWrap.Wrap }) {
|
||||
/*
|
||||
* `Flex` 的间距走 `space: { cross: LengthMetrics.vp(6) }` ——
|
||||
* 同样是因为 `Chip()` 返回 void、链不了 `.margin()`(见上面那段)。
|
||||
* 原来这里是给每个 `Text` 手写 `.margin({ right: 6, top: 6 })`。
|
||||
*/
|
||||
Flex({ wrap: FlexWrap.Wrap, space: { cross: LengthMetrics.vp(6) } }) {
|
||||
ForEach(PRESET_IDS, (pid: string) => {
|
||||
Text(presetLabel(pid))
|
||||
.fontSize(Theme.fontTiny)
|
||||
.fontColor(this.bgPresetId === pid ? Theme.accentFg : Theme.textMuted)
|
||||
.backgroundColor(this.bgPresetId === pid ? Theme.accent : Theme.surfaceMuted)
|
||||
.borderRadius(4)
|
||||
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
|
||||
.margin({ right: 6, top: 6 })
|
||||
.onClick(() => { this.bgPresetId = normalizePreset(pid); this.emitUserChange(); })
|
||||
Chip({
|
||||
label: {
|
||||
text: presetLabel(pid),
|
||||
fontSize: Theme.fontTiny,
|
||||
fontColor: Theme.textPrimary,
|
||||
activatedFontColor: Theme.accentFg
|
||||
},
|
||||
activated: this.bgPresetId === pid,
|
||||
backgroundColor: Theme.surfaceMuted,
|
||||
activatedBackgroundColor: Theme.accent,
|
||||
borderRadius: Theme.radiusControl,
|
||||
allowClose: false,
|
||||
accessibilitySelectedType: AccessibilitySelectedType.SELECTED,
|
||||
onClicked: () => { this.bgPresetId = normalizePreset(pid); this.emitUserChange(); }
|
||||
})
|
||||
}, (pid: string) => pid)
|
||||
}
|
||||
.width('100%').margin({ top: 8 })
|
||||
|
||||
@ -304,7 +304,9 @@ export struct GlassCard {
|
||||
.backgroundColor(this.active ? Color.Transparent : Theme.surface)
|
||||
.backgroundBlurStyle(this.active ? Theme.cardMaterial : BlurStyle.NONE)
|
||||
.borderRadius(this.radius)
|
||||
.border({ width: this.hairline ? 1 : 0, color: Theme.border })
|
||||
/* ★ 2026-09-28:轮廓走 `Theme.hairline`(系统玻璃材质轮廓色),
|
||||
与 GlassCardModifier 同一档 —— 见那处注释的实测数据。 */
|
||||
.border({ width: this.hairline ? 1 : 0, color: Theme.hairline })
|
||||
.clip(true)
|
||||
}
|
||||
}
|
||||
@ -468,8 +470,32 @@ export class GlassCardModifier implements AttributeModifier<CommonAttribute> {
|
||||
* `backgroundColor`)。WebUI 的卡片正是这两件 —— 它没有投影仍然读得出是"一块卡"。
|
||||
* 投影留给**真正悬浮在内容之上**的层(底部导航条),那里它才有信息意义。
|
||||
*/
|
||||
/* 发丝描边:用系统分隔线色(深浅由系统给),不是手写 alpha */
|
||||
instance.border({ width: 1, color: Theme.border });
|
||||
/*
|
||||
* 发丝描边:**卡片轮廓专用**色(`Theme.hairline`,系统
|
||||
* `glass_material_outline_primary`)。
|
||||
*
|
||||
* ★★ 2026-09-28 修(用户:「没有 webui 精致美观…十分杂乱」)。
|
||||
* 原来这里用 `Theme.border`(`ohos_id_color_list_separator`)——
|
||||
* 那是**行间分隔线**的色,职责是"分开两行同底色文本",所以必须够重;
|
||||
* 而卡片轮廓压在壁纸/异色上,只需要"一条若有若无的边"。
|
||||
*
|
||||
* 设备实测(模拟器 1256×2760,同一张列表头卡 y=440):
|
||||
* 改前(list_separator) rgb(204,204,204) → 亮度 202
|
||||
* 改后(本令牌) 实测亮度 236
|
||||
* WebUI `--hairline-fg` 压白 = rgb(238,239,240)
|
||||
* ⇒ 原来比 WebUI **深约 34 级(约 3 倍)**。
|
||||
*
|
||||
* 一屏十几张卡各描一圈重边,就是"杂乱"里**边框**那一半的来源。
|
||||
* 白纱与 alpha 本来就是对的(framework 布局转储实测 `#C7FFFFFF` = 0.78,
|
||||
* 与 WebUI `--glass-card-wall-a` 逐位一致)——**只有描边这一件用错了档位**。
|
||||
*
|
||||
* ★ 为什么是系统色:我第一版照 WebUI 自己写了两个半透明值,判据当场报红
|
||||
* 三条,逼出一个更好的答案 —— SDK 的 `id_defined.json` 里有
|
||||
* `glass_material_outline_primary`("玻璃材质轮廓"),正是这一档,
|
||||
* 深浅两套值由系统给。**不自己造系统已有的值**,这是"用官方件而非自绘"
|
||||
* 在**令牌层**的对应物。完整推理见 `Theme.hairline` 的注释。
|
||||
*/
|
||||
instance.border({ width: 1, color: Theme.hairline });
|
||||
} else {
|
||||
/* 壁纸关着时不铺任何材质(退了才不浪费一次模糊) */
|
||||
instance.backgroundBlurStyle(BlurStyle.NONE);
|
||||
|
||||
@ -107,6 +107,57 @@ export class Theme {
|
||||
static readonly textSubtleLight: string = '#6B7280'; // WebUI gray-400,压白底 4.83:1
|
||||
static readonly textSubtleDark: string = '#8A92A1'; // WebUI .dark gray-400,压深卡片 5.51:1
|
||||
|
||||
/*
|
||||
* 发丝描边(**卡片轮廓**专用)—— **走系统材质轮廓色**,不自己写。
|
||||
*
|
||||
* ★★ 2026-09-28 新增(用户:「鸿蒙 app 样式…没有 webui 精致美观」)。
|
||||
*
|
||||
* ── 病因(**实测**,不是推断)──
|
||||
* 卡片轮廓此前一律用 `Theme.border`
|
||||
* (`sys.color.ohos_id_color_list_separator`)—— 那是**列表行之间**的
|
||||
* 分隔线色,不是"卡片轮廓"色。两者职责不同,而 list_separator 明显更重:
|
||||
*
|
||||
* 设备实测(模拟器 1256×2760 横剖,同一张列表头卡 y=440):
|
||||
* 改前(list_separator) = rgb(204,204,204) → 亮度 202
|
||||
* 改后(本令牌) = 实测亮度 236
|
||||
* WebUI `--hairline-fg` = rgb(15 23 42 / 0.07) 压白 = rgb(238,239,240)
|
||||
* ⇒ 原来比 WebUI **深约 34 级(约 3 倍)**,一屏十几张卡就是
|
||||
* 十几道重描边 —— 这是"杂乱"里**边框**那一半的来源。
|
||||
*
|
||||
* 注意:玻璃卡的三件套"白纱 + 发丝边 + 层次"里,**白纱与 alpha 本来就是对的**
|
||||
* (framework 布局转储实测容器 `#C7FFFFFF` = 0.78,与 WebUI
|
||||
* `--glass-card-wall-a: 0.78` 逐位一致)—— 只有描边这一件用错了档位。
|
||||
*
|
||||
* ── 为什么是系统色,而不是自己写 ----
|
||||
*
|
||||
* 第一版我照 WebUI 写了 `hairlineLight = #120F172A` / `hairlineDark = #17FFFFFF`
|
||||
* (逐字对齐 `--hairline-fg` 的两个值)。**判据当场报红三条**:
|
||||
* · `A2` —— 新写死的色没登记;
|
||||
* · `B` —— 8 位半透明色属于"手写玻璃/手写遮罩",只允许 glassCard 那两档;
|
||||
* · 遮罩那条 —— 同一原因。
|
||||
*
|
||||
* B 条那条注释把问题问得很准:「加第三个半透明色 ⇒ 先回答
|
||||
* **系统为什么没有对应物**」。我照做了,去 SDK 的 `id_defined.json`
|
||||
* (7826 条系统资源,唯一权威表)里搜,**找到了对应物**:
|
||||
*
|
||||
* glass_material_outline_primary ← 「玻璃材质轮廓」(主)
|
||||
* glass_material_outline_secondary ← (次)
|
||||
*
|
||||
* 这正是"玻璃卡的边"这一档,系统连深浅两套值一起给 —— 所以**不该自己写**。
|
||||
* 这也正是用户那句「尽量使用官方组件而不是自绘」在**令牌层**的对应物:
|
||||
* 自绘的边界不是"画了个控件",而是"造了一个系统已有的值"。
|
||||
*
|
||||
* ⇒ `Theme.hairline` 是**系统色**,深浅由系统给,不登记 `SELF_OWNED_COLORS`
|
||||
* (登记册管的是"自己写的色",这个是系统色)。
|
||||
*
|
||||
* ★ 与 `Theme.border` 的分工(**不是替换关系**,两者都留着):
|
||||
* · `Theme.border` = 行间分隔线(Divider、列表行之间、页签条下沿)
|
||||
* —— 需要它够重,才能分开同底色的两行;
|
||||
* · `Theme.hairline` = 卡片/面板**轮廓** —— 压在壁纸或异色上,
|
||||
* 只需要"一条若有若无的边"。
|
||||
*/
|
||||
static readonly hairline: Resource = $r('sys.color.glass_material_outline_primary');
|
||||
|
||||
/*
|
||||
* ──────── 三级文字在**深色**下的可读性下限(2026-09-19 加)────────
|
||||
*
|
||||
|
||||
@ -497,12 +497,26 @@ export struct ContactsTab {
|
||||
ListItem() {
|
||||
Column() {
|
||||
Row() {
|
||||
/*
|
||||
* ★ 2026-09-28:未读 6px 圆点(与收件箱组头同一做法,
|
||||
* 对齐 WebUI `w-1.5 h-1.5 rounded-full`);
|
||||
* 时间 10 → 11px(10px 与同列表的 11/14 不成体系)。
|
||||
* 原来只有**加粗**这一条未读线索,在密集列表里扫不出来。
|
||||
*/
|
||||
if (m.status === 'unread') {
|
||||
Text('')
|
||||
.width(6).height(6).borderRadius(3)
|
||||
.backgroundColor(Theme.accent)
|
||||
.margin({ right: 6 })
|
||||
}
|
||||
Text(m.from_name.length > 0 ? m.from_name : '—')
|
||||
.fontSize(12).fontColor(Theme.textPrimary)
|
||||
.fontSize(12)
|
||||
.fontWeight(m.status === 'unread' ? FontWeight.Bold : FontWeight.Normal)
|
||||
.fontColor(Theme.textPrimary)
|
||||
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Blank()
|
||||
Text(compactMailTime(m.created_at))
|
||||
.fontSize(10).fontColor(Theme.textSubtleFor())
|
||||
.fontSize(11).fontColor(Theme.textSubtleFor())
|
||||
}
|
||||
.width('100%')
|
||||
Text(m.subject)
|
||||
@ -520,7 +534,8 @@ export struct ContactsTab {
|
||||
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
|
||||
.attributeModifier(GlassCardModifier.of(this.bgActive))
|
||||
.borderRadius(Theme.radiusCard)
|
||||
.border({ width: 1, color: Theme.border })
|
||||
/* ★ 2026-09-28:描边只在未读时给(同收件箱组头,见该处注释)。 */
|
||||
.border({ width: 1, color: m.status === 'unread' ? Theme.accentEdgeFor() : Color.Transparent })
|
||||
}
|
||||
.width('100%')
|
||||
.attributeModifier(PressEffectModifier.of())
|
||||
|
||||
Reference in New Issue
Block a user