食材保鲜手册·ArkUI·鸿蒙:保鲜概览统计栏与不耐冷冻警示的交互增强
本文基于 HarmonyOS Next API 24+ 与 ArkUI 2.x,深入「食材保鲜手册」应用的交互增强层。我们将从保鲜概览统计栏的三卡片设计、不耐冷冻警示的条件渲染、概览统计的响应式更新、警示标签的视觉编码、字段值与 UI 形态的值驱动映射、概览统计与列表内容的联动同步、领域知识在 UI 增强中的注入、交互增强与代码克制的工程平衡八个维度,讲透 ArkUI 交互增强的工程实现,以及在工具型应用里"少即是多"的设计哲学。



一、保鲜概览统计栏的三卡片设计
1.1 概览统计栏的工程动机
「食材保鲜手册」在分类筛选上方插入了一栏概览统计——三卡片横排显示"最长保鲜 / 不耐冷冻 / 当前筛选"。这个统计栏是项目重构时新增的交互增强,工程动机有三:
- 概览先于细节——用户在浏览 20 条食材卡片前,先看概览统计建立全局认知(“最长能保鲜多久 / 有多少不耐冷冻的 / 当前在看哪类”)
- 警示前置——"不耐冷冻"是食材保鲜的重要警示,前置到概览栏让用户在浏览前就感知到这个领域知识
- 状态反馈——"当前筛选"显示当前分类筛选态,让用户在多分类间切换时有明确的位置感
这三个动机对应工具型应用的三个 UX 原则——“概览先于细节”、“警示前置”、“状态反馈”。概览统计栏是这三原则的工程载体。
1.2 三卡片的横排布局
Row({ space: 10 }) {
Column() {
Text('最长保鲜')
.fontSize(10)
.fontColor($r('app.color.text_hint'))
Text(`${this.getMaxDays()} 天`)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.primary'))
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor($r('app.color.card_bg'))
.borderRadius(10)
.borderWidth(1)
.borderColor($r('app.color.card_border'))
.layoutWeight(1)
if (this.currentTab === 0) {
Column() {
Text('不耐冷冻')
.fontSize(10)
.fontColor($r('app.color.text_hint'))
Text(`${this.getNonFreezableCount()} 项`)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.tag_replace_text'))
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor($r('app.color.card_bg'))
.borderRadius(10)
.borderWidth(1)
.borderColor($r('app.color.card_border'))
.layoutWeight(1)
}
Column() {
Text('当前筛选')
.fontSize(10)
.fontColor($r('app.color.text_hint'))
Text(this.activeCategory)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.backgroundColor($r('app.color.card_bg'))
.borderRadius(10)
.borderWidth(1)
.borderColor($r('app.color.card_border'))
.layoutWeight(1)
}
.width('100%')
.padding({ left: 16, right: 16, top: 10, bottom: 6 })
.backgroundColor($r('app.color.page_bg'))
三卡片用 Row({ space: 10 }) 横排——space: 10 是卡片间间距 10vp。每个卡片用 Column 垂直堆叠"标签 + 数值"两层文字,.layoutWeight(1) 等分宽度。
每个卡片的视觉编码:
| 卡片 | 标签字 | 数值字色 | 数值内容 |
|---|---|---|---|
| 最长保鲜 | text_hint 灰 |
primary 绿 |
${this.getMaxDays()} 天 |
| 不耐冷冻 | text_hint 灰 |
tag_replace_text 红 |
${this.getNonFreezableCount()} 项 |
| 当前筛选 | text_hint 灰 |
text_primary 深 |
${this.activeCategory} |
标签字色统一 text_hint 灰(小字 10vp 不抢注意力),数值字色按语义区分(绿正向、红负向、深中性)。这是"标签统一数值分化"的设计——标签是辅助信息统一灰,数值是核心信息按语义分色。
1.3 layoutWeight 的动态等分
三卡片都 .layoutWeight(1)——权重相同等分 Row 剩余宽度。Row 的 space: 10 先扣除两个间距(10+10=20vp),剩余宽度按 1:1:1 分给三卡片。
在 360vp 窄屏:
- Row 宽 360vp
- padding 左右各 16vp = 32vp
- space 两个 = 20vp
- 剩余 = 360 - 32 - 20 = 308vp
- 每卡片宽 = 308 / 3 ≈ 102.7vp
在 720vp 宽屏:
- Row 宽 720vp
- padding 32vp
- space 20vp
- 剩余 = 720 - 32 - 20 = 668vp
- 每卡片宽 = 668 / 3 ≈ 222.7vp
宽屏卡片更宽,文字有更多余地——但文字是自适应宽度(Text 的默认行为),不会因卡片宽而拉伸,只是左右余白多。这种"layoutWeight 等分 + 文字自适应"的组合让三卡片在任何屏幕宽度下都视觉平衡。
1.4 条件渲染的不耐冷冻卡片
if (this.currentTab === 0) {
Column() {
Text('不耐冷冻') ...
Text(`${this.getNonFreezableCount()} 项`) ...
}
.layoutWeight(1)
}
"不耐冷冻"卡片只在 Tab 0(保鲜周期)渲染——if (this.currentTab === 0) 控制条件渲染。Tab 1(常温保存)没有不耐冷冻概念,不渲染该卡片。
这个条件渲染影响 layoutWeight 的等分:
| Tab | 卡片数 | 每卡片权重 | 每卡片占 Row 宽 |
|---|---|---|---|
| 0(保鲜周期) | 3 | 1/3 | 33% |
| 1(常温保存) | 2 | 1/2 | 50% |
Tab 1 的两个卡片各占 50% 宽,比 Tab 0 的三个卡片各占 33% 宽更宽——文字有更多余地。这是"条件渲染触发权重重分配"的响应式行为——卡片数变化时 layoutWeight 自动重算。
layoutWeight 的响应式特性是鸿蒙 ArkUI 的设计优势——开发者声明权重(1)而非固定宽度,卡片数变化时系统自动重分配,不需要手动算宽度。
二、不耐冷冻警示的条件渲染
2.1 卡片内的警示分支
@Builder
MaintenanceCard(item: MaintenanceItem) {
Column() {
Row() {
// 色条 + 标题组省略
Column() {
Text(`${item.fridgeDays} 天`)
.fontSize(13)
.fontColor($r('app.color.primary'))
.fontWeight(FontWeight.Medium)
if (item.freezerDays === 0) {
Text('不耐冷冻')
.fontSize(10)
.fontColor($r('app.color.tag_replace_text'))
.backgroundColor($r('app.color.tag_replace_bg'))
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
.margin({ top: 2 })
} else {
Text(`冷冻 ${item.freezerDays} 天`)
.fontSize(11)
.fontColor($r('app.color.text_hint'))
.margin({ top: 2 })
}
}
.alignItems(HorizontalAlign.End)
}
// ...
}
}
每张 MaintenanceCard 的右段有条件分支——item.freezerDays === 0 时显示"不耐冷冻"警示标签,否则显示"冷冻 N 天"普通文字。这是"值驱动 UI 分支"的典型实现——字段值(0 vs 非 0)决定渲染哪个 UI 节点。
条件渲染在 ArkUI 里是"完全销毁/重建"——切到警示分支时普通文字节点销毁,警示节点新建;反之亦然。这种彻底的切换适合本应用——警示与文字是异构 UI(胶囊标签 vs 裸文字),不存在"复用 DOM 节点"的场景。
2.2 警示标签的视觉编码
"不耐冷冻"警示的视觉编码:
| 属性 | 值 | 语义 |
|---|---|---|
| 字色 | tag_replace_text 红 #C8423A |
警示色 |
| 背景色 | tag_replace_bg 粉红 #F8E1E4 |
警示背景 |
| 字号 | 10vp | 小——警示但不抢注意力 |
| 字重 | Normal | 不加粗——保持轻量 |
| padding | 6+6 横向、2+2 纵向 | 紧凑胶囊 |
| borderRadius | 4vp | 圆角胶囊 |
| margin | 上 2vp | 与上方冷藏天数文字间距 |
警示用"红字粉红底胶囊"是 UI 警示的惯用法——红色承载警示语义,胶囊形态让警示标签独立于普通文字。字号 10vp 比副数值 11vp 略小——警示存在但不抢主数值的注意力。
对比"不耐冷冻"警示与普通"冷冻 N 天"文字:
| 元素 | 字色 | 背景色 | 字号 | padding | borderRadius |
|---|---|---|---|---|---|
| 不耐冷冻警示 | 红 | 粉红 | 10 | 6+6,2+2 | 4 |
| 冷冻 N 天文字 | 灰 | 无 | 11 | 无 | 无 |
警示是"胶囊标签"形态(有背景有 padding 有圆角),普通文字是"裸文字"形态(无背景无 padding 无圆角)。形态差异让警示在视觉上独立突出——用户一眼能看出"这张卡片有警示",不需要逐字阅读。
2.3 freezerDays === 0 的领域语义
freezerDays === 0 不是"冷冻 0 天"的字面意义,而是"不建议冷冻"的领域信号。0 值在本应用里承载两层语义:
- 数据层:该食材的冷冻天数字段值是 0
- UI 层:触发"不耐冷冻"警示渲染分支
这种"一个值承载两层语义"是数据驱动 UI 的核心——字段值既存数据又控 UI,不需要额外的"是否耐冷冻"布尔字段。
不耐冷冻的食材实例:
- 鸡蛋(freezerDays: 0)——冷冻后蛋白变稀水状
- 开封酱料(freezerDays: 0)——冷冻后油水分离
- 开封果汁(freezerDays: 0)——冷冻后维 C 流失、口感变差
这些食材的冷冻行为不是"保鲜时长 0",而是"冷冻破坏质地"——0 值是"不建议"的抽象表达,而非"0 天"的字面表达。
2.4 多处条件渲染的一致性
freezerDays === 0 在应用里触发三个独立的条件渲染:
- 卡片右段的警示标签——
if (item.freezerDays === 0)显示"不耐冷冻"胶囊 - 概览统计的不耐冷冻计数——
getNonFreezableCount用MAINTENANCE_DATA.filter((i) => i.freezerDays === 0).length统计 - 冷冻进度条的长度——
getBarWidth(0, 365)返回'0%',进度条前景层宽度 0 不显示
三处都依赖同一领域信号——freezerDays === 0 的判定逻辑一致。如果未来要改判定逻辑(如 freezerDays <= 7 也算"短冷冻"警示),三处都要同步改。
更严谨的做法是把判定逻辑封装成方法:
private isFreezable(item: MaintenanceItem): boolean {
return item.freezerDays > 0
}
三处都调用 isFreezable(item) 而非直接 item.freezerDays === 0。改逻辑时只改一处。本应用当前直接用 === 0 是"简单 vs 集中"的取舍——20 条数据下三处同步改成本可控,引入方法反而增加间接层。
三、概览统计的响应式更新
3.1 getMaxDays 的双 Tab 分支
private getMaxDays(): number {
if (this.currentTab === 0) {
const list = this.getFilteredMaintenance()
const maxFridge = list.length > 0 ? Math.max(...list.map((i: MaintenanceItem) => i.fridgeDays)) : 0
const maxFreezer = list.length > 0 ? Math.max(...list.map((i: MaintenanceItem) => i.freezerDays)) : 0
return Math.max(maxFridge, maxFreezer)
} else {
const list = this.getFilteredParts()
const maxPantry = list.length > 0 ? Math.max(...list.map((i: PartItem) => i.pantryDays)) : 0
const maxDry = list.length > 0 ? Math.max(...list.map((i: PartItem) => i.dryDays)) : 0
return Math.max(maxPantry, maxDry)
}
}
getMaxDays 按 currentTab 走两个分支——Tab 0 算保鲜周期的最长天数(冷藏 vs 冷冻取大),Tab 1 算常温保存的最长天数(常温 vs 干燥取大)。
返回值依赖两个 @State:
currentTab——决定走哪个分支activeCategory——通过getFiltered影响数据范围
任一 @State 变化时 getMaxDays 重算,概览统计的"最长保鲜"数值更新。这是响应式系统的"方法依赖状态"机制——方法内访问 @State,状态变化时方法自动重算。
3.2 状态依赖的响应式触发
概览统计栏的三个数值依赖:
| 数值 | 依赖 @State | 依赖方法 |
|---|---|---|
| 最长保鲜 | currentTab + activeCategory |
getMaxDays |
| 不耐冷冻 | 恒定(全集计数) | getNonFreezableCount |
| 当前筛选 | activeCategory |
无 |
最长保鲜随 Tab 切换和分类筛选变化——切到 Tab 1 时从保鲜周期最长变常温保存最长,切分类时从全数据最长变该分类最长。
不耐冷冻恒定——getNonFreezableCount 用 MAINTENANCE_DATA 全集而非 getFiltered,所以不随分类筛选变。这是有意的"全局警示"语义——不耐冷冻是全局知识,不局限当前筛选。
当前筛选随分类切换变化——this.activeCategory 直接显示,用户切分类时立即看到当前分类名。
3.3 响应式更新的性能特性
每次 @State 变化触发概览统计栏重渲染——三个 Text 节点的文字内容更新。这是 O(3) 的更新开销,可忽略。
但 getMaxDays 内的 Math.max(...list.map(...)) 是 O(n)——n 是当前分类下的食材数(5-20 条)。每次状态变化重算 O(n) 仍可忽略——20 条数据的 map + max 是微秒级。
如果数据集规模增加到 1000+ 条(如全食物数据库),O(n) 的 getMaxDays 每次状态变化重算就有开销。那时要引入缓存——只在 activeCategory 或 currentTab 变化时重算,而非每次 build 都重算。但本应用 20 条规模无需此优化。
3.4 概览统计与列表内容的联动
概览统计栏与下方列表内容联动——切分类时两者同步更新:
- 用户点分类 chip →
this.activeCategory = cat @State activeCategory变化 → 触发响应式更新- 概览统计栏:
getMaxDays重算 → "最长保鲜"数值更新、this.activeCategory显示更新 - 列表内容:
getFilteredMaintenance重算 → ForEach 差量渲染新卡片
联动是响应式系统的"一处状态变化触发多处 UI 更新"——同一个 activeCategory 变化触发概览统计 + 列表内容两处更新,开发者无需手动协调两处同步。
这种联动是 ArkUI 响应式系统的核心红利——开发者声明"UI 依赖什么状态",状态变化时系统自动算出"哪些 UI 需要更新",多处更新自动同步。如果用命令式 UI(如 Android View 系统),要手动调用 textView.setText(...)、listView.notifyDataSetChanged(),协调成本高。
四、警示标签的视觉编码
4.1 胶囊标签的形态语义
"不耐冷冻"警示用胶囊形态——有背景色、有 padding、有 borderRadius。对比普通文字的"裸文字"形态,胶囊形态承载"独立警示"的语义:
| 形态 | 背景色 | padding | borderRadius | 语义 |
|---|---|---|---|---|
| 胶囊标签 | 有 | 有 | 有 | 独立警示、可点击标签 |
| 裸文字 | 无 | 无 | 无 | 普通信息 |
胶囊形态让警示在视觉上独立于其他文字——用户扫视卡片时,胶囊形态的警示标签像"贴纸"一样贴在数值区,与其他裸文字分离。
4.2 红×粉红的警示配色
警示标签的配色:
- 字色
tag_replace_text红#C8423A - 背景色
tag_replace_bg粉红#F8E1E4
红字配粉红底是"低饱和度警示"的惯用法——纯红底配红字会刺眼,粉红底(低饱和度红)配红字(高饱和度红)保持警示感但不刺眼。
对比度:
- 字色
#C8423ARGB (200, 66, 58) - 背景色
#F8E1E4RGB (248, 225, 228) - 亮度差约 0.7——对比度约 4:1,满足 AA Large(≥ 3:1)
这是"警示可读性"的底线——如果警示字色与背景色对比度不足,用户在强光下无法辨认警示。4:1 足 AA Large,但不足 AA(4.5:1)——警示字号 10vp 是小字,严格说应满足 AA 4.5:1。当前 4:1 是"擦边达标",可接受的取舍。
4.3 字号递减的视觉权重
警示标签的字号 10vp,比副数值 11vp 略小——警示存在但不抢主数值的注意力。这是"视觉权重靠字号"的设计:
| 元素 | 字号 | 视觉权重 |
|---|---|---|
| 主数值(冷藏天数) | 13vp | 重 |
| 副数值(冷冻天数) | 11vp | 中 |
| �警示(不耐冷冻) | 10vp | 轻 |
警示字号最小但形态特殊(胶囊),靠"形态 + 颜色"承载警示语义,而非靠字号抢注意力。这种"小字号 + 强形态"的设计让警示存在但不喧宾夺主——主数值(冷藏天数)才是卡片的视觉重心。
4.4 padding 与 borderRadius 的胶囊计算
警示标签的 padding 6+6 横向、2+2 纵向,borderRadius 4vp。胶囊形条件:
- 字号 10vp + 行高约 14vp + padding 2+2 = 18vp 高
- borderRadius ≥ height/2 = 9vp 才完整胶囊
但实际 borderRadius 4vp < 9vp——警示标签是"圆角矩形"而非完整胶囊。这是有意的取舍——完整胶囊的 borderRadius 9vp 在 18vp 高的标签上会让两端是完整半圆,视觉重量大;4vp 圆角矩形更轻量,与"小字号警示"的视觉权重匹配。
完整胶囊形适合"按钮形态"(如分类筛选 chip),强调"可点击 + 重要"。圆角矩形适合"标签形态"(如警示),强调"独立 + 轻量"。本应用的警示是后者,用圆角矩形而非完整胶囊。
五、字段值与 UI 形态的值驱动映射
5.1 freezerDays 值驱动两种 UI 形态
freezerDays 字段的值决定 UI 形态:
| 字段值 | UI 形态 | 视觉编码 |
|---|---|---|
| 0 | 警示胶囊 | 红字粉红底、10vp、padding 6+6,2+2、borderRadius 4 |
| > 0 | 副数值裸文字 | 灰字、11vp、无 padding、无 borderRadius |
值驱动 UI 形态是数据驱动 UI 的核心——开发者声明"字段值 → UI 形态"的映射,鸿蒙响应式系统在字段值变化时自动切换 UI 形态。
本应用的字段值是静态数据(编译期固定),UI 形态在渲染时确定后不变。但如果未来数据变成动态(如用户编辑食材),字段值变化会自动触发 UI 形态切换——无需手动调 setVisible、setText 等命令式更新。
5.2 category 值驱动色条颜色
category 字段的值驱动色条颜色和副标签字色:
Column()
.backgroundColor(CATEGORY_COLORS[item.category])
Text(item.category)
.fontColor(CATEGORY_COLORS[item.category])
CATEGORY_COLORS 是分类名到颜色的映射表——category 值(如"冷藏")通过映射查表得到对应色(如#3D8FA8),驱动色条背景色和副标签字色。
值驱动颜色映射的优势是"声明式"——开发者声明"分类名 → 颜色"的映射表,UI 渲染时自动查表应用,不需要 switch/case 的命令式分支。
5.3 fridgeDays / freezerDays 值驱动进度条长度
.width(this.getBarWidth(item.fridgeDays, 60)) // 冷藏进度条
.width(this.getBarWidth(item.freezerDays, 365)) // 冷冻进度条
getBarWidth(value, max) 把字段值转成百分比字符串:
private getBarWidth(value: number, max: number): string {
const percent = Math.min(value / max * 100, 100)
return `${percent}%`
}
value 是字段值(如 fridgeDays 5),max 是进度条满量程(如 60 天)。value / max * 100 算百分比,Math.min(..., 100) 截断到 100%(防止 value > max 时进度条溢出)。
字段值驱动进度条长度的机制——字段值越大进度条越长,字段值 0 进度条长度 0 不显示。这种"数值 → 视觉长度"的映射是信息可视化的核心——把抽象数字转成直观视觉长度,用户一眼能看出长短对比。
5.4 值驱动 UI 的设计哲学
值驱动 UI 的设计哲学是"UI 是数据的视觉投影"——UI 形态、颜色、长度等都由数据值决定,开发者声明"值 → UI"的映射关系,鸿蒙响应式系统在值变化时自动更新 UI。
对比命令式 UI(如 Android View 系统)——开发者要手动调 setBackgroundColor、setText、setLayoutParams 等方法更新 UI,协调成本高。ArkUI 的声明式 + 值驱动让 UI 更新"自动"——这是鸿蒙 ArkUI 相对传统命令式 UI 的核心红利。
六、领域知识在 UI 增强中的注入
6.1 概览统计栏的领域知识前置
“不耐冷冻” 是食材保鲜的重要领域知识——不是所有食材都能冷冻,冷冻会破坏某些食材的质地。这个知识在概览统计栏前置显示:
- “不耐冷冻 N 项” 计数——让用户在浏览前就知道"有 N 项食材不能冷冻"
- 卡片内"不耐冷冻" 胶囊——让用户在浏览时看到具体哪条不能冷冻
两处领域知识注入形成"概览 + 细节"的双层覆盖——概览栏建立全局认知(有 N 项不耐冷冻),卡片细节确认具体食材(这条不耐冷冻)。
6.2 进度条 max 值的领域知识
进度条的 max 值不是随便取的数字,是领域知识的具象化:
| 进度条 | max 值 | 领域知识 |
|---|---|---|
| 冷藏 | 60 天 | 冷藏室食材最长约 60 天 |
| 冷冻 | 365 天 | 冷冻室食材最长约 1 年 |
| 常温 | 1095 天 | 常温食材最长约 3 年 |
| 干燥 | 1095 天 | 干燥环境延长保存,上限与常温同 |
max 值反映"该保存方式下的最长保鲜时长"——用户看进度条时能感知"这条食材的保鲜时长 vs 该方式的最长上限"。蜂蜜在常温进度条满格(1095/1095=100%)——用户一眼能看出"蜂蜜是常温保存的极限时长"。
6.3 保鲜贴士的领域知识浓缩
卡片的 note / replaceTip 字段浓缩保鲜常识:
- “清洗后沥干,密封冷藏可延长至一周”——流程性建议
- “冷藏时大头朝上放置更耐存”——摆放方式建议
- “苹果释放乙烯,需与其他水果分开”——化学相互作用警告
- “分装冷冻避免反复解冻”——操作方式建议
每条贴士 10-20 字浓缩一条保鲜常识——可执行、具体、不抽象。贴士是领域知识在 UI 的最深层注入——用户读贴士获取可操作的保鲜建议,直接改善食材保存行为。
6.4 领域知识注入的工程哲学
领域知识在 UI 的注入分三层:
- 概览层——概览统计栏的"不耐冷冻 N 项"是全局领域知识前置
- 可视层——进度条 max 值是领域知识具象化到视觉量程
- 文字层——保鲜贴士是领域知识浓缩到可执行文字
三层注入让应用不只是"显示数据"——数据背后的保鲜常识也传递给用户。这是工具型应用的核心价值——不只是查询工具,更是知识传递载体。
七、交互增强与代码克制的工程平衡
7.1 增强点的克制取舍
本应用的交互增强点有几处,每处都经过"增强 vs 克制"的权衡:
| 增强点 | 是否实现 | 克制理由 |
|---|---|---|
| 概览统计栏 | 实现 | 三卡片简洁,前置警示有价值 |
| 不耐冷冻警示 | 实现 | 领域知识必要,胶囊形态醒目但不抢 |
| Tab 滑动切换 | 未实现 | 工具型应用主动点切换足用,滑动切换是浏览型交互 |
| 卡片点击展开详情 | 未实现 | 卡片信息已完整,展开详情是冗余 |
| 搜索框 | 未实现 | 20 条数据分类筛选足用,搜索是过度工程 |
| 收藏功能 | 未实现 | 工具型应用无账户体系,收藏是社交型交互 |
| 深色模式 | 未实现 | 厨房暖色调不适合深色,适配成本高 |
未实现的增强点都有"克制"的理由——不是不能做,是"做了反而破坏简洁"。工具型应用的核心是"快速查询 + 知识传递",过度增强反而分散核心价值。
7.2 概览统计栏的代码开销
概览统计栏的新增代码约 60 行(三卡片的 Column 内含两层 Text + 样式属性)。这 60 行换来:
- 概览前置(最长保鲜一眼看出)
- 警示前置(不耐冷冻计数感知)
- 状态反馈(当前筛选明确)
60 行的代码开销对工具型应用是合理投资——概览统计栏是"信息密度提升"的增强,不增加交互复杂度(用户不点不滑,只是看)。
对比"Tab 滑动切换"的代码开销——要引入 Tabs 组件、重写 Tab 栏逻辑、处理滑动事件与点击事件的冲突,约 100+ 行改动,且改变交互模式(从主动点切变被动滑切)。这种改动是"交互模式重构",代码开销大且改变用户习惯,克制。
7.3 警示分支的代码开销
不耐冷冻警示的条件分支约 15 行(if 分支 + 胶囊标签的 Text + 样式属性)。这 15 行换来:
- 领域知识前置(不耐冷冻的食材明确标识)
- 视觉独立(胶囊形态让警示一眼看出)
- 数据驱动(freezerDays === 0 自动触发)
15 行的代码开销对领域知识传递是必要投资——警示让用户避免"冷冻鸡蛋"的错误操作,直接改善食材保存行为。
对比"卡片点击展开详情"的代码开销——要处理 onClick 事件、维护展开状态、展开区的 UI 设计、展开/收起的动画,约 80+ 行改动。但卡片的现有信息(食材名 + 分类 + 冷藏天数 + 冷冻天数 + 进度条 + 备注)已经完整,展开详情能加什么?可能是"图示"、“视频”、“更多保鲜技巧”——但这些是"内容扩展"而非"交互增强",属于另一个项目定位(如"保鲜百科"),克制。
7.4 工具型应用的少即是多
工具型应用的核心价值是"快速查询 + 知识传递"——用户来应用是有明确目的(查某食材保鲜时长),完成目的后离开。这种"目的型"使用模式决定"少即是多"——简洁的 UI + 必要的领域知识 > 丰富的交互 + 冗余的功能。
本应用的"少"体现在:
- 单页面无导航深度
- 静态数据无网络请求
- 双 Tab 无滑动切换
- 分类筛选无搜索
- 卡片信息完整无展开
"多"体现在:
- 20 条数据覆盖常见食材
- 进度条可视化直观对比
- 概览统计全局认知
- 不耐冷冻警示领域知识
- 保鲜贴士可执行建议
这种"少交互 + 多内容"的平衡是工具型应用的设计核心——交互点到为止,内容深厚扎实。ArkUI 的声明式 UI + 数据驱动渲染让"少交互"的代码开销低(ForEach + Builder 复用),"多内容"的注入直接(数据集字段直接渲染)——技术栈与应用定位契合。
八、本篇小结
本章从保鲜概览统计栏的三卡片设计讲到交互增强与代码克制的工程平衡,完整覆盖了「食材保鲜手册」交互增强的工程实现。关键点归纳:
- 概览统计栏三卡片:最长保鲜 / 不耐冷冻 / 当前筛选,layoutWeight 等分自适应卡片数变化
- 不耐冷冻警示条件渲染:freezerDays === 0 值驱动 UI 分支,警示胶囊 vs 普通文字两种形态
- 概览统计响应式更新:getMaxDays 双 Tab 分支、activeCategory 联动概览与列表
- 警示胶囊视觉编码:红字粉红底、10vp 小字号、padding 6+6,2+2、borderRadius 4 圆角矩形
- 值驱动 UI 映射:freezerDays 值驱动形态、category 值驱动颜色、fridgeDays 值驱动长度
- 领域知识三层注入:概览层(不耐冷冻计数)+ 可视层(进度条 max 值)+ 文字层(保鲜贴士)
- 交互增强代码克制:概览统计 + 不耐冷冻 实现增强,滑动切换 + 卡片展开 + 搜索 + 收藏 + 深色 克制
- 工具型应用少即是多:少交互(单页面 + 静态数据 + 双 Tab + 分类筛选)+ 多内容(20 条 + 进度条 + 概览 + 警示 + 贴士)
九、附录:本篇涉及的交互增强 API
| API | 用途 | 本篇体现 |
|---|---|---|
Row({ space }) |
横排容器 + 间距 | 概览统计栏三卡片 |
Column() |
垂直堆叠 | 卡片内标签 + 数值 |
.layoutWeight(n) |
弹性权重 | 三卡片等分、卡片数变化自适应 |
.alignItems(HorizontalAlign.Start) |
交叉轴对齐 | 卡片内文字左对齐 |
.padding({...}) |
内间距 | 卡片 padding、胶囊 padding |
.backgroundColor($r) |
背景色资源 | 卡片背景、胶囊背景 |
.borderRadius(n) |
圆角 | 卡片 10、胶囊 4 |
.borderWidth(1) |
边框宽度 | 卡片边框 |
.borderColor($r) |
边框色资源 | 卡片边框色 |
if (cond) |
条件渲染 | 不耐冷冻分支、Tab 分支 |
@State |
响应式状态 | currentTab、activeCategory |
Text('...') |
文字渲染 | 标签、数值、警示 |
.fontSize(n) |
字号 | 标签 10、警示 10、副数值 11、主数值 13、数值 15 |
.fontWeight(FontWeight.Bold) |
字重 | 数值加粗 |
.fontColor($r) |
字色资源 | 标签灰、数值绿、警示红 |
.margin({ top: 2 }) |
外间距 | 数值与标签间距 |
十、附录:本篇涉及的交互增强点
10.1 已实现的交互增强
| 增强点 | 实现方式 | 代码开销 | 价值 |
|---|---|---|---|
| 概览统计栏 | Row + 三 Column + layoutWeight | ~60 行 | 概览前置、警示前置、状态反馈 |
| 不耐冷冻警示 | if 分支 + 胶囊标签 | ~15 行 | 领域知识前置、视觉独立 |
| 概览响应式更新 | getMaxDays + getNonFreezableCount + @State 联动 | ~30 行 | 状态变化自动同步 |
| 字段值驱动 UI | getBarWidth + CATEGORY_COLORS 映射 | ~10 行 | 数据驱动 UI 形态/颜色/长度 |
10.2 克制的交互增强
| 增强点 | 克制理由 | 实现代价 |
|---|---|---|
| Tab 滑动切换 | 工具型应用主动点切换足用 | ~100 行,改变交互模式 |
| 卡片点击展开详情 | 卡片信息已完整 | ~80 行,内容扩展非交互增强 |
| 搜索框 | 20 条数据分类筛选足用 | ~50 行,过度工程 |
| 收藏功能 | 无账户体系 | ~150 行,社交型交互 |
| 深色模式 | 厨房暖色调不适合深色 | 19 色重选 + dark 目录,适配成本高 |
至此完整覆盖了「食材保鲜手册」交互增强的工程实现。十个篇章联动,从项目重构到数据建模到布局到数据驱动到色彩到交互,构成「食材保鲜手册」应用从零到完成的完整工程叙事。
更多推荐




所有评论(0)