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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、应用定位与需求拆解

1.1 应用定位

车辆保养手册——一个面向私家车主的轻量查询工具,解决两个高频疑问:

  • 这辆车到什么时候该换什么?(保养周期)
  • 这些大件能用多久、该不该提前换?(配件使用年限)

它不追求覆盖所有车型与原厂规范,而是提供一份基于行业通用经验、可快速查阅的"基线手册"。用户真正的痛点不是缺少数据,而是数据散落在保养手册、论坛帖子、4S 店话术中,缺少一个**一眼能看懂、能按分类筛选、能直观感知"还剩多少"**的载体。

1.2 信息架构

应用最终采用「双标签 + 分类筛选」的三段式信息架构:

层级 内容 数量
顶层 Tab 保养周期 / 配件年限 2
分类筛选 全部、发动机、底盘、电气、车身 5
保养项目 机油、滤芯、皮带、变速箱油、刹车片、轮胎、雨刮、玻璃水… 20
配件项目 发动机总成、发电机、蓄电池、压缩机、三元催化、减震器、ECU… 20

顶层 Tab 切换「周期查询」与「寿命查询」两个心智模型;分类筛选把 20 条数据切成可管理的子集;每条数据用一张卡片承载,卡片内同时呈现数值、比例条、分类色、专业备注。

1.3 为什么不做成"一大张表"

把 40 条数据塞进一个长表格,是看起来最快但体验最差的方案。原因有三:

  1. 纵向滚动无锚点:用户想查"电气类"时,要在 40 行里肉眼跳着找,分类边界靠"边框颜色"区分,信噪比低。
  2. 信息密度不可控:表格天然追求"列对齐",但保养周期(5000 公里 / 6 个月)与配件寿命(15 年 / 60 万公里)量纲不同,强行对齐会让数字列参差不齐。
  3. 缺少"还剩多少"的直觉:表格只能给数字,无法给比例。用户看到 8000060000 并排,不会立刻意识到前者比后者长了 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 没有独立状态,过度拆分反而要把状态一层层透传,得不偿失。
  • 抽离"渲染形状稳定、数据流入清晰"的片段:例如 MaintenanceCardPartCardLegendDot 三张卡片,形状固定、数据单一,适合抽成 @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 { ... } 拆分。本项目没有拆,原因有三:

  1. 状态集中:只有两个 @StatecurrentTabactiveCategory),拆出去要写一堆 @Prop / @Link,徒增样板。
  2. 数据共享:分类筛选状态同时影响两个 Tab,若拆成两个子 struct,筛选状态要在父级持有并通过 @Link 双向绑定,反而比当前集中式难追踪。
  3. 渲染规模可控: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 内部,有三个好处:

  1. 不占用组件实例内存:每次 Index 实例化时,不会重新创建一份 40 条数据的数组。
  2. 语义清晰const 一眼看出"这是不可变数据"。
  3. 符合 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%。

注意三个细节:

  1. 返回字符串而不是 number:ArkUI 的 .width() 接受 string | number | Resource,传 '80%' 是百分比、传 80 是 vp、传 $r('...') 是资源引用。这里要百分比,必须返回字符串。
  2. Math.min 封顶:当 value > max(例如某项目间隔 10 万公里、满量程 10 万)时,直接返回超过 100% 的宽度会被 ArkUI 截断,但显式封顶更安全,避免某些边界场景的渲染异常。
  3. 满量程是硬编码常量:保养周期满量程 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 会自动跟着 currentTabactiveCategory 同步更新。

这种"在 build() 里直接调方法取派生数据"的写法,在 ArkUI 里是安全的——重渲染的频率受 @State 变化驱动,每次变化都会重新执行整个 build(),不存在"缓存陈旧"的问题。代价是每次重渲染都会重算一次过滤;对 20 条数据来说,重算开销可以忽略。

5.3 副标题用 · 而不是空格

副标题 '保养周期 · 配件使用年限' 用了中文间隔号 · 而不是空格或斜杠。这是一个细节取舍:

  • 用空格 '保养周期 配件使用年限':视觉太散,两个词像没分隔
  • 用斜杠 '保养周期/配件使用年限':太"技术感",像文件路径
  • ·:中文阅读节奏自然,视觉重量适中,是中文 UI 副标题的常用分隔符

这种细节看似无关紧要,但在工具型应用里,副标题是用户每次打开都会扫一眼的"定位锚点",分隔符的视觉重量直接影响信息密度感。

六、Tab 栏的声明式实现

6.1 不用 Tabs 组件,自建 Row

ArkUI 提供了 Tabs / TabContent 一套官方 Tab 组件,但本项目选择用 Row + ForEach + Text 自建。原因有四:

  1. 样式定制简单:官方 Tabs 的指示器(下划线)样式、间距、字号都要绕一圈 TabBarMode / TabWidthMode 参数,自建 Row 直接调样式 API 更直接。
  2. 不需要滑动切换:官方 Tabs 默认支持左右滑动切换 Tab,但本应用只有两个 Tab,滑动切换反而容易误触。自建 Row 只有点击切换,行为更可控。
  3. 不需要内容联动:官方 Tabs 把 Tab 标签和内容 TabContent 绑定,切换 Tab 时自动切换内容。但本应用的内容切换依赖 currentTab 状态驱动,自建 Tab 标签和内容的关系更灵活。
  4. 视觉指示器不需要:本应用 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,分别用 MaintenanceCardPartCard 两种 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 资源在组件层透传的标准方式。

九、本篇小结

本章从需求拆解到主页面骨架,铺陈了"车辆保养手册"应用的组件化决策路径。关键取舍可以归纳成五条:

  1. 信息架构优先:先想清楚"用户怎么查",再决定"代码怎么写"。双 Tab + 分类筛选的三段式架构,是从"40 条数据怎么让用户一眼找到"反推出来的。
  2. 状态最小化:只有两个 @State,分别管 Tab 切换和分类筛选。派生数据用方法算,不冗余成状态。
  3. 组件边界按"是否需要独立状态"划:不需要独立状态的复用片段用 @Builder,需要独立状态的才用子 struct
  4. ArkUI 的 if 是控制流不是节点切换build() 每次重渲染都会重新执行 if,这是声明式 UI 的核心机制,不是 React 的虚拟 DOM diff。
  5. 样式是属性的延伸不是独立的层:ArkUI 不追求样式与结构分离,每个样式都是组件属性 API,选中态切换靠三元表达式逐属性写。

下一篇我们会深入数据建模:从 MaintenanceItemPartItem 两个接口设计,到 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 内容切换、分隔符渲染

至此完整覆盖了"从零设计到主页面骨架"的全过程。下一篇将进入数据建模的细节战场。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐