从零设计车辆保养手册应用:ArkUI 组件化实战
本文基于 HarmonyOS Next API 24+,完整复盘「车辆保养手册」应用从需求拆解到组件化落地的全过程。我们将以一个真实的双标签查询应用为案例,讲透 ArkUI 声明式 UI 在工程中的组织方式、组件边界划分、状态驱动的数据流,以及把"一张静态表"打磨成"可交互、可筛选、可可视化的产品页"的关键细节。




一、应用定位与需求拆解
1.1 应用定位
车辆保养手册——一个面向私家车主的轻量查询工具,解决两个高频疑问:
- 这辆车到什么时候该换什么?(保养周期)
- 这些大件能用多久、该不该提前换?(配件使用年限)
它不追求覆盖所有车型与原厂规范,而是提供一份基于行业通用经验、可快速查阅的"基线手册"。用户真正的痛点不是缺少数据,而是数据散落在保养手册、论坛帖子、4S 店话术中,缺少一个**一眼能看懂、能按分类筛选、能直观感知"还剩多少"**的载体。
1.2 信息架构
应用最终采用「双标签 + 分类筛选」的三段式信息架构:
| 层级 | 内容 | 数量 |
|---|---|---|
| 顶层 Tab | 保养周期 / 配件年限 | 2 |
| 分类筛选 | 全部、发动机、底盘、电气、车身 | 5 |
| 保养项目 | 机油、滤芯、皮带、变速箱油、刹车片、轮胎、雨刮、玻璃水… | 20 |
| 配件项目 | 发动机总成、发电机、蓄电池、压缩机、三元催化、减震器、ECU… | 20 |
顶层 Tab 切换「周期查询」与「寿命查询」两个心智模型;分类筛选把 20 条数据切成可管理的子集;每条数据用一张卡片承载,卡片内同时呈现数值、比例条、分类色、专业备注。
1.3 为什么不做成"一大张表"
把 40 条数据塞进一个长表格,是看起来最快但体验最差的方案。原因有三:
- 纵向滚动无锚点:用户想查"电气类"时,要在 40 行里肉眼跳着找,分类边界靠"边框颜色"区分,信噪比低。
- 信息密度不可控:表格天然追求"列对齐",但保养周期(5000 公里 / 6 个月)与配件寿命(15 年 / 60 万公里)量纲不同,强行对齐会让数字列参差不齐。
- 缺少"还剩多少"的直觉:表格只能给数字,无法给比例。用户看到
80000和60000并排,不会立刻意识到前者比后者长了 33%。
因此最终选择「卡片流 + 比例条」:每张卡片是一个自洽的信息块,比例条把数字转换成视觉长度,分类色把抽象类别转换成颜色锚点。
二、工程结构与模块边界
2.1 目录结构
doubleinfo/
├── AppScope/
│ └: ...
│ └: ...
│ └: ...
│ └: ...
│ └: ...
│ └: ...
│ └: ...
│ └: ...
│ └: ...
│ ┌─run/ ...
│ ├─ets/
│ │ ├─entryability/ EntryAbility.ets
│ │ ├─pages/ Index.ets ← 唯一页面
│ │ └: ...
│ └: ...
│ └: ...
│ └: ...
│ └: ...
│ └: ...
│ └: ...
│ └: ...
│ └: ...
└─<s/ ...
应用只保留一个 Index.ets 页面,所有交互在单页内完成。这符合"工具型轻应用"的定位——不需要路由跳转、不需要多页面栈、不需要跨页面参数传递。HarmonyOS 的路由栈对一个查询手册是过度设计。
2.2 ArkUI 的组件边界
ArkUI 声明式 UI 的最小复用单元是 @Builder 方法。与 React 的函数组件、Vue 的单文件组件不同,ArkUI 的 @Builder 不是一个独立作用域的组件实例,而是宿主 struct 的渲染片段——它共享宿主的 @State,没有独立生命周期,没有独立 props 校验,传参靠方法参数。
这个特性决定了组件边界划分策略:
- 不抽离过度细的子组件:ArkUI 的
@Builder没有独立状态,过度拆分反而要把状态一层层透传,得不偿失。 - 抽离"渲染形状稳定、数据流入清晰"的片段:例如
MaintenanceCard、PartCard、LegendDot三张卡片,形状固定、数据单一,适合抽成@Builder。 - 保留"控制流"在主
build():Tab 切换、分类切换、列表渲染的控制流留在build(),让状态流转一目了然。
最终 Index struct 内的组件分解:
| @Builder | 职责 | 入参 | 复用次数 |
|---|---|---|---|
MaintenanceCard(item) |
渲染一条保养项目卡片 | MaintenanceItem |
20(按筛选动态) |
PartCard(item) |
渲染一条配件年限卡片 | PartItem |
20(按筛选动态) |
LegendDot(color, label) |
底部图例的色点+文字 | Resource, string |
2 |
2.3 为什么不拆成多个 struct
ArkUI 完全支持 @Component struct Child { ... } 拆分。本项目没有拆,原因有三:
- 状态集中:只有两个
@State(currentTab、activeCategory),拆出去要写一堆@Prop/@Link,徒增样板。 - 数据共享:分类筛选状态同时影响两个 Tab,若拆成两个子 struct,筛选状态要在父级持有并通过
@Link双向绑定,反而比当前集中式难追踪。 - 渲染规模可控:40 条数据、3 个 Builder,单 struct 完全胜任。ArkUI 的
struct本身不背"重量级"包袱,拆不拆只看"是否需要独立状态"。
这给 ArkUI 工程的一个重要原则做了注脚:组件拆分不是为了"代码短",而是为了"状态清晰"。当一个片段没有独立状态、且被复用次数有限时,@Builder 比子 struct 更合适。
三、状态建模
3.1 状态变量选择
应用只有两个状态:
@State currentTab: number = 0 // 0=保养周期,1=配件年限
@State activeCategory: string = '全部' // 当前选中的分类筛选
这两个状态都是用户交互的直接产物,且都会引发 UI 重渲染:
currentTab改变 → 顶部 Tab 高亮迁移、内容列表整体切换、右上角计数变化activeCategory改变 → 分类筛选 chip 高亮迁移、列表内容过滤、右上角计数变化
ArkUI 的 @State 装饰器会追踪变量赋值,自动触发依赖该变量的 build() 片段重渲染。这里两个状态都是原始类型(number / string),赋值即触发,无需 @Observed / @ObjectLink 这类深层观测。
3.2 静态数据为什么不加 @State
const MAINTENANCE_DATA: MaintenanceItem[] = [ ... ] // 模块级 const
const PARTS_DATA: PartItem[] = [ ... ] // 模块级 const
const CATEGORY_COLORS: Record<string, string> = { ... } // 模块级 const
三个数据集都是编译期常量:内容不变、不需要响应式、不需要观测。把它们放在模块顶层而不是 struct 内部,有三个好处:
- 不占用组件实例内存:每次
Index实例化时,不会重新创建一份 40 条数据的数组。 - 语义清晰:
const一眼看出"这是不可变数据"。 - 符合 ArkUI 的渲染约定:
@State是给"会变"的数据用的,给"不会变"的数据加@State不仅浪费观测开销,还会让阅读者误以为它会被改写。
3.3 派生数据用方法而不是计算属性
ArkTS 在 struct 内不支持 getter 形式的计算属性(这是与 TypeScript 的一个关键差异),派生数据用方法表达:
private getFilteredMaintenance(): MaintenanceItem[] {
if (this.activeCategory === '全部') {
return MAINTENANCE_DATA
}
return MAINTENANCE_DATA.filter((item: MaintenanceItem) => item.category === this.activeCategory)
}
private getFilteredParts(): PartItem[] {
if (this.activeCategory === '全部') {
return PARTS_DATA
}
return PARTS_DATA.filter((item: PartItem) => item.category === this.activeCategory)
}
这两个方法在 build() 中被调用,每次重渲染都会执行。对 20 条数据来说,filter 一次的开销可以忽略(微秒级),完全不需要做 memo 缓存。
如果数据量上千,可以考虑把过滤结果缓存为 @State,在 activeCategory 变化的回调里手动重算。但本项目的数据规模,过滤本身就是最省事的方案。
3.4 比例条宽度的实时计算
private getBarWidth(value: number, max: number): string {
const percent = Math.min(value / max * 100, 100)
return `${percent}%`
}
这是另一个"派生数据用方法"的例子。比例条宽度 = 当前值 / 满量程 × 100%,封顶 100%。
注意三个细节:
- 返回字符串而不是 number:ArkUI 的
.width()接受string | number | Resource,传'80%'是百分比、传80是 vp、传$r('...')是资源引用。这里要百分比,必须返回字符串。 Math.min封顶:当value > max(例如某项目间隔 10 万公里、满量程 10 万)时,直接返回超过 100% 的宽度会被 ArkUI 截断,但显式封顶更安全,避免某些边界场景的渲染异常。- 满量程是硬编码常量:保养周期满量程 100000 公里 / 72 个月,配件寿命满量程 15 年 / 60 万公里。这是根据数据集最大值选定的"标尺"。如果数据会动态变化,满量程应该也用方法计算;静态数据集里直接硬编码更直观。
四、主页面骨架解析
4.1 整体布局结构
build() {
Column() {
// 1. 顶部标题栏(应用名 + 副标题 + 项目计数)
Row() { ... }
// 2. Tab 栏(保养周期 / 配件年限)
Row() { ... }
// 3. 分类筛选(横向滚动 chip 列)
Scroll() { Row() { ForEach(...) } }
// 4. 内容列表(纵向滚动卡片流)
Scroll() {
Column() {
if (this.currentTab === 0) {
ForEach(this.getFilteredMaintenance(), (item: MaintenanceItem) => {
this.MaintenanceCard(item)
})
} else {
ForEach(this.getFilteredParts(), (item: PartItem) => {
this.PartCard(item)
})
}
// 底部图例
Row({ space: 16 }) { ... }
}
}.layoutWeight(1)
}
.height('100%')
.width('100%')
}
整个页面是一个垂直 Column,自上而下四段:标题、Tab、筛选、列表。其中只有第 4 段(内容列表)通过 .layoutWeight(1) 占满剩余高度,前三段都是内容自适应高度。
这种"顶部固定、中部固定、底部填充"的布局是工具型应用的标准范式——头部信息一眼扫过、筛选控件触手可及、列表区把所有剩余空间让给内容。
4.2 ArkUI 的布局权重
.layoutWeight(1) 是 ArkUI 的弹性布局权重。在 Column 里,所有子元素默认按内容高度排列;剩余空间按 layoutWeight 比例分配。这里只有内容列表设了 layoutWeight(1),意味着前三段按内容高度固定后,剩下的高度全部给列表区。
如果 Tab 栏也要分一份剩余空间(比如让 Tab 占 1/4、列表占 3/4),可以分别设 .layoutWeight(1) 和 .layoutWeight(3)。本项目不需要——前三段内容固定,列表区独占剩余空间最合理。
4.3 双重滚动隔离
页面里有两条滚动轴:
- 分类筛选:
Scroll().scrollable(ScrollDirection.Horizontal),横向滚动,scrollBar(BarState.Off)隐藏滚动条 - 内容列表:
Scroll().scrollable(ScrollDirection.Vertical)(默认方向),scrollBar(BarState.Auto)自动显隐
两个 Scroll 互相独立,各自管理自己的视口与滚动状态。这是 ArkUI 的滚动隔离机制——每个 Scroll 组件是一个独立的滚动容器,不会冒泡到父级。
为什么分类筛选要横向滚动?5 个分类在窄屏(如 360vp)一字排开会有点挤,但又不至于要换行。横滚让 chip 列保持一行视觉连贯,同时不抢占内容区高度。scrollBar(BarState.Off) 是关键——5 个 chip 的横滚距离很短,显示滚动条反而干扰。
五、顶部标题栏的设计取舍
5.1 三段式标题结构
Row() {
Column() {
Text('车辆保养手册')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Text('保养周期 · 配件使用年限')
.fontSize(12)
.fontColor($r('app.color.text_hint'))
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
Blank()
Text(this.currentTab === 0 ?
`${this.getFilteredMaintenance().length}项` :
`${this.getFilteredParts().length}项`)
.fontSize(12)
.fontColor($r('app.color.primary'))
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.backgroundColor($r('app.color.tag_normal_bg'))
.borderRadius(10)
}
.width('100%')
.padding({ left: 16, right: 16, top: 14, bottom: 10 })
.backgroundColor($r('app.color.header_bg'))
标题栏是一个 Row,左中右三段:
- 左段:应用名 + 副标题,垂直堆叠,靠
HorizontalAlign.Start顶到左边 - 中段:
Blank()弹性占位,把左右两段推开 - 右段:项目计数 chip,背景浅蓝、文字深蓝、圆角胶囊
Blank() 是 ArkUI 的弹性空白组件,在 Row 里会吃掉所有剩余宽度。这是 ArkUI 实现"两端对齐、中间留白"的惯用手法,相当于 Flexbox 的 flex: 1 中段。
5.2 项目计数的实时性
右段 chip 显示当前 Tab + 当前分类下的项目数:
`${this.getFilteredMaintenance().length}项`
`${this.getFilteredParts().length}项`
注意这里重新调用了 getFilteredMaintenance() / getFilteredParts(),而不是缓存某个变量。在 ArkUI 的响应式模型里,build() 重渲染时会重新执行所有表达式,因此这个 chip 会自动跟着 currentTab 和 activeCategory 同步更新。
这种"在 build() 里直接调方法取派生数据"的写法,在 ArkUI 里是安全的——重渲染的频率受 @State 变化驱动,每次变化都会重新执行整个 build(),不存在"缓存陈旧"的问题。代价是每次重渲染都会重算一次过滤;对 20 条数据来说,重算开销可以忽略。
5.3 副标题用 · 而不是空格
副标题 '保养周期 · 配件使用年限' 用了中文间隔号 · 而不是空格或斜杠。这是一个细节取舍:
- 用空格
'保养周期 配件使用年限':视觉太散,两个词像没分隔 - 用斜杠
'保养周期/配件使用年限':太"技术感",像文件路径 - 用
·:中文阅读节奏自然,视觉重量适中,是中文 UI 副标题的常用分隔符
这种细节看似无关紧要,但在工具型应用里,副标题是用户每次打开都会扫一眼的"定位锚点",分隔符的视觉重量直接影响信息密度感。
六、Tab 栏的声明式实现
6.1 不用 Tabs 组件,自建 Row
ArkUI 提供了 Tabs / TabContent 一套官方 Tab 组件,但本项目选择用 Row + ForEach + Text 自建。原因有四:
- 样式定制简单:官方
Tabs的指示器(下划线)样式、间距、字号都要绕一圈TabBarMode/TabWidthMode参数,自建Row直接调样式 API 更直接。 - 不需要滑动切换:官方
Tabs默认支持左右滑动切换 Tab,但本应用只有两个 Tab,滑动切换反而容易误触。自建Row只有点击切换,行为更可控。 - 不需要内容联动:官方
Tabs把 Tab 标签和内容TabContent绑定,切换 Tab 时自动切换内容。但本应用的内容切换依赖currentTab状态驱动,自建 Tab 标签和内容的关系更灵活。 - 视觉指示器不需要:本应用 Tab 栏下方是一条 1vp 的全宽分隔线(
.borderWidth({ bottom: 1 })),不是跟随选中 Tab 移动的下划线指示器。官方Tabs的指示器是移动的,要改成固定分隔线反而要覆盖样式。
6.2 ForEach 渲染 Tab 标签
Row() {
ForEach(this.tabs, (tab: string, index?: number) => {
Text(tab)
.fontSize(15)
.fontWeight(this.currentTab === index ? FontWeight.Bold : FontWeight.Normal)
.fontColor(this.currentTab === index ? $r('app.color.primary') : $r('app.color.text_hint'))
.padding({ left: 12, right: 12, top: 8, bottom: 8 })
.onClick(() => {
this.currentTab = index as number
})
if (index === 0) {
Text('·')
.fontColor($r('app.color.divider'))
.margin({ left: 6, right: 6 })
}
})
}
.width('100%')
.justifyContent(FlexAlign.Center)
.borderWidth({ bottom: 1 })
.borderColor($r('app.color.divider'))
.backgroundColor($r('app.color.header_bg'))
ForEach 是 ArkUI 的列表渲染装饰器,第二个参数是回调 (item, index?) => UI 片段。注意 index 是可选参数——ArkUI 的类型签名如此,使用时要 index as number 显式收窄。
6.3 ArkUI build() 内的 if 是控制流,不是条件渲染
这段代码里有一个关键细节:
if (index === 0) {
Text('·')
.fontColor($r('app.color.divider'))
.margin({ left: 6, right: 6 })
}
在 ArkUI 的 build() 内,if 是合法的控制流语法,不是 React 风格的"条件渲染节点"。这里的 if 决定的是渲染时是否生成这个 Text 节点,而不是运行时切换。
更准确地说:ArkUI 的 build() 在每次重渲染时都会重新执行一遍,if 语句每次都会重新判断。如果 index === 0,就生成一个 Text('·');否则不生成。这是 ArkUI 声明式 UI 的核心机制——build() 不是一次性构造,而是每次状态变化都重新执行的表达式。
6.4 中间分隔符 · 的作用
两个 Tab 标签之间用 Text('·') 分隔,颜色用 app.color.divider(很浅的灰)。这是一种视觉过渡:两个 Tab 文字之间如果直接挨着,会显得"挤";如果用 Blank() 拉开,又会显得"散"。一个浅灰的 · 既提供了视觉分隔,又不抢注意力。
只在 index === 0 时渲染分隔符,意味着第一个 Tab 后面有 ·,第二个 Tab 后面没有——正好形成 [Tab1 · Tab2] 的对称结构。
七、分类筛选的横向滚动实现
7.1 Scroll + Row + ForEach 的标准三件套
Scroll() {
Row({ space: 8 }) {
ForEach(this.categories, (cat: string) => {
Text(cat)
.fontSize(13)
.fontColor(this.activeCategory === cat ? '#FFFFFF' : $r('app.color.text_secondary'))
.backgroundColor(this.activeCategory === cat ? $r('app.color.primary') : $r('app.color.card_bg'))
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(14)
.borderWidth(1)
.borderColor(this.activeCategory === cat ? $r('app.color.primary') : $r('app.color.card_border'))
.onClick(() => {
this.activeCategory = cat
})
})
}
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.backgroundColor($r('app.color.page_bg'))
外层 Scroll 提供横向滚动能力,内层 Row({ space: 8 }) 用 space: 8 让 chip 之间有 8vp 间距,ForEach 渲染 5 个分类 chip。
7.2 chip 的选中态样式切换
每个 chip 是一个 Text,选中态与未选中态的样式差异:
| 属性 | 选中态 | 未选中态 |
|---|---|---|
| 字色 | #FFFFFF(白) |
text_secondary(深灰) |
| 背景 | primary(蓝) |
card_bg(白) |
| 边框 | primary(蓝) |
card_border(浅灰) |
选中态用"实色填充"——蓝底白字蓝框,视觉重量大;未选中态用"线框"——白底深字浅灰框,视觉重量小。这种对比让用户一眼能定位当前筛选。
样式切换靠三元表达式 this.activeCategory === cat ? X : Y,每个属性都要写一次。ArkUI 没有类似 CSS class 的样式批量切换机制,所有样式都是属性级 API。这是 ArkUI 与 Web CSS 的一个根本差异——ArkUI 不追求"样式与结构分离",而是把样式作为组件属性的延伸。
7.3 BorderRadius 14 的视觉计算
borderRadius(14) 看似随手取的数字,实际经过计算:chip 的 padding 是 { top: 6, bottom: 6 },文字 fontSize 13,所以 chip 总高度 = 13 + 6×2 = 25vp。要做成"完全圆角的胶囊",border-radius 应该 ≥ 高度/2 = 12.5vp。取 14vp 略大于 12.5,确保两端是完整的半圆,不会留出"方角"。
这是 ArkUI 圆角取值的一个常见心法:胶囊形 = borderRadius ≥ height/2。如果 borderRadius < height/2,两端会保留一段直边,视觉上变成"圆角矩形"而不是"胶囊"。
八、内容列表的条件切换
8.1 if/else 控制内容渲染
Scroll() {
Column({ space: 10 }) {
if (this.currentTab === 0) {
ForEach(this.getFilteredMaintenance(), (item: MaintenanceItem) => {
this.MaintenanceCard(item)
})
} else {
ForEach(this.getFilteredParts(), (item: PartItem) => {
this.PartCard(item)
})
}
// 底部图例
Row({ space: 16 }) {
this.LegendDot($r('app.color.primary'), '里程指标')
this.LegendDot($r('app.color.accent'), '时间指标')
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding({ top: 8, bottom: 24 })
}
.padding({ left: 12, right: 12, top: 4, bottom: 4 })
}
.layoutWeight(1)
.scrollBar(BarState.Auto)
.backgroundColor($r('app.color.page_bg'))
if (this.currentTab === 0) { ... } else { ... } 是 ArkUI build() 内的标准条件渲染。两个分支各渲染一个 ForEach,分别用 MaintenanceCard 和 PartCard 两种 Builder。
8.2 ArkUI ForEach 的键值
ForEach 的完整签名是 ForEach(arr, itemGenerator, keyGenerator?)。本项目没有显式传 keyGenerator,意味着 ArkUI 会用默认的索引键。
对本项目来说这是安全的——数据集是静态常量,没有插入/删除/重排序,每次重渲染的数组顺序稳定,索引键不会错位。
但如果数据会动态变化(比如支持拖拽排序、远端拉取后插入新项),就必须显式传 keyGenerator:
ForEach(this.getFilteredMaintenance(),
(item: MaintenanceItem) => { this.MaintenanceCard(item) },
(item: MaintenanceItem) => item.name // 用项目名做唯一键
)
键值稳定是 ArkUI ForEach 性能与状态保持的关键。本项目静态数据用默认索引键没问题,但工程化时这是个要留意的点。
8.3 底部图例的 Builder 复用
@Builder
LegendDot(color: Resource, label: string) {
Row({ space: 4 }) {
Column()
.width(8)
.height(8)
.backgroundColor(color)
.borderRadius(4)
Text(label)
.fontSize(11)
.fontColor($r('app.color.text_hint'))
}
}
图例是一个"色点 + 文字"的小复合单元,被复用 2 次(里程指标 / 时间指标)。这种"形状稳定、数据流入简单"的片段最适合抽成 @Builder。
注意入参类型 Resource——这是 ArkUI 资源引用的类型。$r('app.color.primary') 返回的就是 Resource。把 Resource 当作参数传给 @Builder,是 ArkUI 资源在组件层透传的标准方式。
九、本篇小结
本章从需求拆解到主页面骨架,铺陈了"车辆保养手册"应用的组件化决策路径。关键取舍可以归纳成五条:
- 信息架构优先:先想清楚"用户怎么查",再决定"代码怎么写"。双 Tab + 分类筛选的三段式架构,是从"40 条数据怎么让用户一眼找到"反推出来的。
- 状态最小化:只有两个
@State,分别管 Tab 切换和分类筛选。派生数据用方法算,不冗余成状态。 - 组件边界按"是否需要独立状态"划:不需要独立状态的复用片段用
@Builder,需要独立状态的才用子struct。 - ArkUI 的 if 是控制流不是节点切换:
build()每次重渲染都会重新执行if,这是声明式 UI 的核心机制,不是 React 的虚拟 DOM diff。 - 样式是属性的延伸不是独立的层:ArkUI 不追求样式与结构分离,每个样式都是组件属性 API,选中态切换靠三元表达式逐属性写。
下一篇我们会深入数据建模:从 MaintenanceItem 和 PartItem 两个接口设计,到 40 条数据的静态建模,再到 ArkTS 类型系统在工程中的实际取舍。
十、附录:本篇涉及的关键 ArkUI API
| API | 用途 | 出现位置 |
|---|---|---|
@State |
响应式状态变量 | currentTab, activeCategory |
@Builder |
复用渲染片段 | MaintenanceCard, PartCard, LegendDot |
ForEach |
数组列表渲染 | Tab 栏、分类筛选、内容列表 |
Scroll |
滚动容器 | 分类筛选(横滚)、内容列表(纵滚) |
Row / Column |
布局容器 | 全页面骨架 |
Blank() |
弹性空白 | 标题栏中段 |
.layoutWeight(n) |
弹性布局权重 | 内容列表占满剩余高度 |
.scrollable(dir) |
滚动方向 | Horizontal / Vertical |
.scrollBar(state) |
滚动条显隐 | Off / Auto |
$r('app.color.X') |
资源引用 | 全页面颜色取值 |
FontWeight.Bold / .Normal / .Medium |
字重 | 标题、Tab 选中态、卡片标题 |
十一、附录:本篇涉及的 ArkTS 与 TS 差异
| 特性 | ArkTS | TypeScript | 本篇体现 |
|---|---|---|---|
| struct 内 getter | 不支持 | 支持 | 派生数据用方法 getFilteredMaintenance() |
| ForEach index 类型 | number?(可选) |
不适用(不是语言特性) | 用 index as number 收窄 |
@State 装饰器 |
必须用 | 不存在 | 状态变量声明 |
@Builder 装饰器 |
必须用 | 不存在 | 复用渲染片段 |
Resource 类型 |
内置 | 不存在 | 资源引用透传给 @Builder |
build() 内 if |
控制流,每次重渲染执行 | 不适用 | Tab 内容切换、分隔符渲染 |
至此完整覆盖了"从零设计到主页面骨架"的全过程。下一篇将进入数据建模的细节战场。
更多推荐


所有评论(0)