HarmonyOS 校园应用系列之鸿蒙原生实战:用 ArkUI 搭 AI 单词首页 —— 沉浸适配与多维筛选
HarmonyOS 校园应用系列之鸿蒙原生实战:用 ArkUI 搭 AI 单词首页 —— 沉浸适配与多维筛选
源码位置:
entry/src/main/ets/pages/HomeTab.ets(193 行)
核心主题:沉浸式全屏适配 + Hero 统计卡片 + 快捷操作入口 + 交互式条形图 + 多维筛选列表
技术关键词:@StorageProp、Scroll、linearGradient、ForEach、Stack、@Builder
一、页面概览与沉浸式全屏适配
首页是用户打开应用后看到的第一个界面,它承担着 “全局概览 + 快速导航” 的双重职责。页面从上到下分为五个视觉区域:Hero 统计卡(渐变背景 + 三维指标)、快捷操作行(四宫格图标入口)、本周数据图(柱状趋势)、筛选标签栏(全部/推荐/热门/最新)、推荐项目列表(带进度和标签的卡片流)。
与 05-mistake-book 的橙色主题不同,本应用采用 天蓝色系主题(主色 #0EA5E9),传达出"智能、高效、清爽"的产品调性。这种配色在教育类和学习类 App 中非常常见——蓝色让人联想到"知识海洋",同时不会像红色那样产生焦虑感。
// HomeTab.ets 根布局结构
build() {
Column() {
Scroll() {
Column({ space: 14 }) {
this.HeroCard()
this.QuickActionRow()
this.WeekChartCard()
this.CategoryBar()
this.ItemList()
Blank().height(this.safeBottom + 20)
}
.width('100%').padding({ left: D.pad, right: D.pad })
}
.layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
}
.width('100%').height('100%').backgroundColor(C.bg)
}
根组件是 Column,内部用 Scroll 包裹真正的内容列:layoutWeight(1) 让滚动区占满主区域,scrollBar(BarState.Off) 隐藏滚动条,align(Alignment.Top) 让内容从顶部开始排布。内容列用 space: 14 统一控制卡片间距,左右 padding(D.pad)(16vp)留出页边距,末尾的 Blank().height(this.safeBottom + 20) 为底部导航条让位。这里的 safeTop 和 safeBottom 来自 EntryAbility.ets 中通过 AppStorage.setOrCreate 存储的安全区数值:
// EntryAbility.ets — 安全区存储(节选)
async onWindowStageCreate(windowStage: window.WindowStage): Promise<void> {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, TAG, 'loadContent failed: %{public}s', JSON.stringify(err));
return;
}
try {
const win = windowStage.getMainWindowSync();
win.setWindowLayoutFullScreen(true); // 沉浸式全屏
win.setWindowSystemBarProperties({
statusBarContentColor: '#1C2333',
navigationBarContentColor: '#1C2333'
});
const top = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
const bottom = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
AppStorage.setOrCreate('safeTop', px2vp(top.topRect.height));
AppStorage.setOrCreate('safeBottom', px2vp(bottom.bottomRect.height));
} catch (e) {
hilog.error(DOMAIN, TAG, 'window setup failed: %{public}s', JSON.stringify(e));
}
});
}
setWindowLayoutFullScreen(true) 让应用内容延伸到状态栏和导航栏区域——这就是为什么首页的蓝色渐变 HeroCard 能从屏幕最顶部开始渲染。而 AppStorage 充当了全局状态容器,让所有页面都能通过 @StorageProp 读取安全区数值,无需层层传递 props。


二、HeroCard 渐变统计卡片
HeroCard 是首页的视觉焦点——一个从左上到右下的天蓝色线性渐变背景卡片,内含标题行和三维统计指标:
@Builder
HeroCard() {
Column({ space: 12 }) {
Row({ space: 12 }) {
Text('🏠').fontSize(24)
Column({ space: 4 }) {
Text('智能管理').fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text('高效便捷 · 智能推荐').fontSize(12).fontColor('rgba(255,255,255,0.85)')
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
}.width('100%')
Row() {
ForEach(this.stats, (s: StatItem, idx: number) => {
Column({ space: 2 }) {
Text(s.value).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text(s.label).fontSize(10).fontColor('rgba(255,255,255,0.8)')
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
if (idx < this.stats.length - 1) {
Column().width(1).height(28).backgroundColor('rgba(255,255,255,0.25)')
}
}, (s: StatItem) => s.label)
}.width('100%')
}
.width('100%').padding(18).padding({ top: this.safeTop + 20 })
.borderRadius({ bottomLeft: D.rLg, bottomRight: D.rLg })
.linearGradient({ angle: 135, colors: [[C.primary, 0.0], [C.accent, 1.0]] })
}
几个值得注意的细节:标题行是 🏠 emoji(24fp)加一个双行文字的 Column——主标题"智能管理"20fp 粗体纯白,副标题"高效便捷 · 智能推荐"12fp、85% 不透明度白色;卡片整体 padding(18),但顶部额外叠加 .padding({ top: this.safeTop + 20 }),为状态栏内容让位;borderRadius 只圆了底部两角(D.rLg = 20vp),顶部直角贴住屏幕边缘,形成"渐变从状态栏倾泻而下"的沉浸效果。
渐变配置解读:angle: 135 表示渐变方向为左上→右下(135 度)。颜色数组 [['#0EA5E9', 0], ['#38BDF8', 1]] 定义了从深蓝到浅蓝的过渡——#0EA5E9 是 Theme.ets 中定义的 C.primary,#38BDF8 是其亮色变体。这种同色系渐变比跨色系渐变(如蓝→紫)更稳重,适合工具类 App。
统计指标没有封装成独立的 @Builder,而是通过 ForEach(this.stats, ...) 内联渲染,项与项之间还插入了一条 1vp 宽、28vp 高的半透明白色分隔线:
Row() {
ForEach(this.stats, (s: StatItem, idx: number) => {
Column({ space: 2 }) {
Text(s.value).fontSize(20).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text(s.label).fontSize(10).fontColor('rgba(255,255,255,0.8)')
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
if (idx < this.stats.length - 1) {
Column().width(1).height(28).backgroundColor('rgba(255,255,255,0.25)')
}
}, (s: StatItem) => s.label)
}.width('100%')
注意统计项的文字颜色设计:数值用纯白 #FFFFFF + 粗体 20fp,确保在渐变背景上有足够的对比度;标签用 80% 不透明度的白色 rgba(255,255,255,0.8) + 小字号 10fp,形成清晰的视觉层级。layoutWeight(1) 让三项指标平分宽度,idx < this.stats.length - 1 的判断保证分隔线只出现在项与项之间,末项之后不再画线。
三、QuickActionRow 四宫格快捷入口
统计卡片下方是一个四宫格快捷操作区,每个入口由图标 + 文字组成:
@Builder
QuickActionRow() {
Row() {
ForEach(this.actions, (item: QuickAction) => {
Column({ space: 6 }) {
Row() { Text(item.icon).fontSize(22) }
.width(44).height(44).backgroundColor(C.cardSoft).borderRadius(D.rSm).justifyContent(FlexAlign.Center)
Text(item.title).fontSize(10).fontColor(C.textSub)
}
.layoutWeight(1)
.onClick(() => { promptAction.showToast({ message: item.title }); })
}, (item: QuickAction) => item.title)
}
.width('100%').padding(12).backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke }).justifyContent(FlexAlign.SpaceAround)
}
技术要点:
- 外层 Row 使用
FlexAlign.SpaceAround让四个入口均匀分布 - 每个入口的 Column 使用
layoutWeight(1)平分可用宽度 - 图标是
🔍 ⭐ 📊 🔔四个 emoji 文本(22fp),装在 44×44vp、圆角D.rSm(10vp)、底色C.cardSoft的容器里,达到触控友好的最小推荐尺寸(Apple HIG 建议 44×44pt,Android Material 建议 48×48dp) - 整个区域用白色圆角卡片包裹(
C.card背景 +D.rMd(14vp)圆角 + 1vpC.stroke描边),与上方蓝色 HeroCard 形成色彩对比 - 每个入口点击后通过
promptAction.showToast弹出对应名称的 Toast
这种 “图标 + 文字标签” 的快捷入口模式在移动端极其常见——微信的"我"页、支付宝首页、美团外卖底部都采用了相同的设计语言。它的优势在于:图标提供快速识别,文字消除歧义,两者结合实现零学习成本的操作引导。
从交互设计角度看,四宫格布局的每个入口的触控区域约为 (屏幕宽度/4) × 64vp——在 1280vp 宽度的模拟器上约 320×64vp,远超 Material Design 建议的 48×48dp 最小触控目标。这意味着用户在快速操作时几乎不会误触。
四点五、Scroll 组件与页面滚动行为
首页内容整体包裹在 Scroll 组件中,这是内容型页面的标准做法:
Scroll() {
Column({ space: 14 }) {
this.HeroCard()
this.QuickActionRow()
this.WeekChartCard()
this.CategoryBar()
this.ItemList()
Blank().height(this.safeBottom + 20)
}
.width('100%').padding({ left: D.pad, right: D.pad })
}
.layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
scrollBar(BarState.Off) 隐藏默认滚动条以保持界面简洁;align(Alignment.Top) 约束内容在滚动区域内顶部对齐;内容列末尾的 Blank().height(this.safeBottom + 20) 是"滚动底部安全垫"——既为底部导航条让位,也保证列表最后一项不会被 Tab 栏遮挡。当推荐列表项目增多或屏幕尺寸变小时,内容会自然溢出并滚动,而不是被裁剪。
十一、@StorageProp 的性能优势与使用注意事项
首页通过 @StorageProp('safeTop') 和 @StorageProp('safeBottom') 读取安全区数值。与 @State 不同,@StorageProp 是 单向只读 绑定——页面只能读取 AppStorage 中的值,不能修改它。这种设计确保了安全区数值的 单一数据源(只在 EntryAbility 中写入),避免了多个页面各自修改导致的不一致。
从性能角度看,@StorageProp 使用了 ArkUI 框架内部的优化机制——只有当 AppStorage 中对应 key 的值真正变化时,才会触发组件重新渲染。这与 @State 的变化检测机制相同,但省去了在页面间传递 props 的开销。对于全局配置类数据(主题色、语言、安全区等),AppStorage + @StorageProp/@StorageLink 是 HarmonyOS 推荐的状态管理方案。
四、WeekChartCard 本周数据柱状图
本周数据卡片是首页最具技术含量的组件之一——它使用 Column + ForEach 动态渲染柱状图,无需任何第三方图表库:
@Builder
WeekChartCard() {
Column({ space: 12 }) {
Row() {
Text('📊 本周数据').fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text)
Blank()
Text('30 次').fontSize(13).fontWeight(FontWeight.Bold).fontColor(C.primary)
}.width('100%')
Row({ space: 4 }) {
ForEach(this.weekBars, (b: WeekBar, idx: number) => {
Column({ space: 6 }) {
Column()
.width(18)
.height(Math.max(6, b.value / 7 * 80))
.borderRadius(4)
.linearGradient({
angle: 180,
colors: idx === this.selectedBar
? [[C.primary, 0.0], [C.accent, 1.0]]
: [[C.primarySoft, 0.0], [C.primary, 1.0]]
})
Text(b.day).fontSize(10)
.fontColor(idx === this.selectedBar ? C.primary : C.textDim)
.fontWeight(idx === this.selectedBar ? FontWeight.Bold : FontWeight.Normal)
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
.onClick(() => { this.selectedBar = idx; })
}, (b: WeekBar) => b.day)
}.width('100%')
}
.width('100%').padding(14).backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
}
核心设计决策分析:
1. 高度映射公式 Math.max(6, b.value / 7 * 80):原始数据值范围 2~7,除以 7 再乘 80 得到约 23~80vp 的柱高;Math.max(6, ...) 兜底保证最小 6vp,避免数值极小时柱子完全消失。这个系数是根据容器可用高度和最大值估算的经验值——在实际项目中应该根据容器实际高度动态计算:maxHeight = containerHeight * 0.7(留 30% 给标签和间距),然后 scaleFactor = maxHeight / maxValue。
2. 选中态渐变柱子:每个柱子使用从上到下的线性渐变(angle: 180),当前选中柱(idx === this.selectedBar,默认值 4 即"五")用 C.primary → C.accent 的亮色渐变,其余柱子用 C.primarySoft → C.primary 的柔和渐变。点击任意柱子会更新 selectedBar,并联动星期标签变色加粗——柱状图本身就是可交互的。
3. keyGenerator 使用 b.day:因为星期几(一~日)天然唯一且稳定,用作 key 既简洁又可靠。
4. layoutWeight(1) 让柱子等宽:7 个柱子平分 Row 的宽度,无论屏幕宽窄都自动适配。
五、CategoryBar 多维筛选标签栏
筛选栏提供了四个维度:全部、推荐、热门、最新。当前选中项有蓝色背景 + 白色文字,未选中项是白底灰字带描边:
@Builder
CategoryBar() {
Scroll() {
Row({ space: 8 }) {
ForEach(this.cats, (cat: string, idx: number) => {
Text(cat).fontSize(13)
.fontColor(this.activeCat === idx ? '#FFFFFF' : C.textSub)
.backgroundColor(this.activeCat === idx ? C.primary : C.card)
.borderRadius(D.rSm).border({ width: 1, color: this.activeCat === idx ? C.primary : C.stroke })
.padding({ left: 14, right: 14, top: 8, bottom: 8 })
.onClick(() => { this.activeCat = idx; })
}, (cat: string) => cat)
}
}
.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width('100%')
}
条件样式三元表达式 是这个组件的核心模式——this.activeCat === idx ? A : B 同时控制字体颜色、背景色、边框颜色三个属性。当 activeCat === idx 时呈现"选中态"(蓝底白字蓝框),否则是"默认态"(白底灰字灰框)。
外层横向 Scroll 让标签栏具备横向滚动能力——scrollable(ScrollDirection.Horizontal) 指定滚动方向,scrollBar(BarState.Off) 隐藏滚动条。当前四个标签放得下,但一旦运营侧增加更多分类,无需改任何布局代码就能自然横向滑出屏幕。
keyGenerator 用 cat 文本本身——因为分类名天然唯一且数组是静态常量(不会增删重排),比用索引做 key 更稳定可靠。

六、ItemList 推荐项目列表
推荐列表是首页内容密度最高的区域,每张卡片包含:左侧 emoji 图标、标题行(名称 + 标签)、描述行、进度条 + 百分比:
@Builder
ItemList() {
Column({ space: 10 }) {
Row() {
Text('推荐列表').fontSize(16).fontWeight(FontWeight.Bold).fontColor(C.text)
Blank()
Text('查看全部 ›').fontSize(11).fontColor(C.primary)
}.width('100%')
ForEach(this.items, (item: ListItem) => {
Column({ space: 10 }) {
Row({ space: 12 }) {
Row() { Text(item.emoji).fontSize(28) }
.width(56).height(56).backgroundColor(C.cardSoft).borderRadius(D.rMd).justifyContent(FlexAlign.Center)
Column({ space: 4 }) {
Row() {
Text(item.title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(C.text).layoutWeight(1)
Text(item.tag).fontSize(9).fontColor('#FFFFFF')
.backgroundColor(item.tag === 'HOT' ? C.danger : item.tag === 'NEW' ? C.ok : C.accent)
.padding({ left: 5, right: 5, top: 2, bottom: 2 }).borderRadius(D.rSm)
}.width('100%')
Text(item.desc).fontSize(12).fontColor(C.textDim)
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
}.width('100%')
Row() {
Text('进度').fontSize(10).fontColor(C.textDim)
Stack({ alignContent: Alignment.Start }) {
Column().width('100%').height(5).backgroundColor(C.cardSoft).borderRadius(3)
Column().width(`${item.progress}%`).height(5).borderRadius(3).backgroundColor(C.primary)
}.layoutWeight(1).margin({ left: 8, right: 8 })
Text(`${item.progress}%`).fontSize(10).fontColor(C.primary).fontWeight(FontWeight.Bold)
}.width('100%')
}
.width('100%').padding(12).backgroundColor(C.card).borderRadius(D.rMd)
.border({ width: 1, color: C.stroke })
.onClick(() => { promptAction.showToast({ message: item.title }); })
}, (item: ListItem) => item.id.toString())
}.width('100%')
}
ListItem 接口定义:
interface ListItem { id: number; emoji: string; title: string; desc: string; tag: string; progress: number; }
tag 颜色语义化:标签用 9fp 白字配彩色底——HOT 用 C.danger 红底,NEW 用 C.ok 绿底,其余(如"精选")用 C.accent 蓝底,内边距 5/2、圆角 D.rSm。这种 “颜色=含义” 的设计让用户无需阅读文字就能识别标签类型。
手绘进度条(Stack + Column):进度条没有使用 Progress 组件,而是用 Stack({ alignContent: Alignment.Start }) 叠放两根 Column——底层是 width('100%')、高 5vp 的 C.cardSoft 灰色轨道,顶层是 width(${item.progress}%)、高 5vp 的 C.primary 蓝色填充,两者都带 borderRadius(3) 圆角。alignContent: Alignment.Start 保证填充条从左侧起点生长。这种写法比 Progress 组件更轻量,也能精确控制轨道与填充的圆角和颜色。

七、数据模型与 Mock 数据策略
首页的所有数据都以 interface 先行 + mock 数组 的方式定义:
@State activeCat: number = 0;
@State selectedBar: number = 4;
private cats: string[] = ['全部', '推荐', '热门', '最新'];
private stats: StatItem[] = [
{ value: '128', label: '总数' },
{ value: '32', label: '今日' },
{ value: '86%', label: '完成率' },
];
private weekBars: WeekBar[] = [
{ day: '一', value: 3 }, { day: '二', value: 5 }, { day: '三', value: 2 },
{ day: '四', value: 6 }, { day: '五', value: 4 }, { day: '六', value: 7 },
{ day: '日', value: 3 },
];
private items: ListItem[] = [
{ id: 1, emoji: '📦', title: '热门项目一', desc: '描述信息 · 已参与128人', tag: 'HOT', progress: 75 },
{ id: 2, emoji: '🎯', title: '推荐项目二', desc: '描述信息 · 已参与86人', tag: 'NEW', progress: 45 },
{ id: 3, emoji: '⭐', title: '精选项目三', desc: '描述信息 · 已参与256人', tag: '精选', progress: 90 },
];
值得注意的细节:只有 activeCat 和 selectedBar 两个交互状态是 @State(它们驱动筛选栏和柱状图的选中态切换),其余 mock 数组全部声明为 private 普通成员——静态展示数据不参与状态管理,避免无意义的观察开销。
这种 “声明式数据驱动 UI” 模式的优势在于:
- 类型安全:interface 定义确保编译期就能发现字段名拼写错误
- 易于替换:mock 数据可以一键替换为 API 调用结果,UI 层代码无需改动
- 可预测渲染:ArkUI 的 diff 算法基于数据变化精准更新 DOM,避免全量重绘

八、样式复用体系:Theme.ets 常量类
首页大量使用了 Theme.ets 中定义的 C(颜色)和 D(尺寸)两个常量类来消除重复代码:
// Theme.ets — 颜色与尺寸常量
export class C {
static readonly bg: string = '#F2F5FB';
static readonly card: string = '#FFFFFF';
static readonly cardSoft: string = '#F4F6FB';
static readonly stroke: string = '#EAEEF6';
static readonly text: string = '#1C2333';
static readonly textSub: string = '#5A6483';
static readonly textDim: string = '#9AA3BC';
static readonly primary: string = '#0EA5E9';
static readonly primarySoft: string = '#E0F4FE';
static readonly accent: string = '#38BDF8';
static readonly warn: string = '#FF9F1C';
static readonly danger: string = '#FF5A6E';
static readonly ok: string = '#2BB673';
}
export class D {
static readonly rLg: number = 20;
static readonly rMd: number = 14;
static readonly rSm: number = 10;
static readonly pad: number = 16;
}
C.card + D.rMd + C.stroke 的组合被应用到几乎每张卡片上——一次性设置白色背景、14vp 圆角、1vp 描边。如果没有这两个常量类,每个卡片都需要重复写硬编码的字面量,而且如果设计师要调整圆角或描边颜色,需要修改多处。
常量类 vs @Extend/@Styles 的选择原则:
- 常量类(
class C/class D)只提供值,不绑定组件类型,任何组件的任何属性都能引用,配合三元表达式做条件样式最灵活 @Extend只能用于特定组件类型(如@Extend(Column)只能给 Column 用),但支持属性链@Styles可以用于多种组件,但不支持传参- 本项目选择常量类,因为页面里大量"选中态/默认态"切换需要在字体色、背景色、边框色等多个属性间自由组合颜色值,这是 @Extend 难以覆盖的场景
九、页面布局的层次结构总结
首页的完整组件树如下:
Column (root, 100%×100%, bg)
└── Scroll (layoutWeight 1, scrollBar Off, align Top)
└── Column (space 14, padding left/right D.pad)
├── HeroCard (@Builder) — Column, linearGradient 135°, 底部两角圆 D.rLg
│ ├── Row (🏠 emoji + 标题/副标题 Column)
│ └── Row (ForEach stats × 3 + 分隔线)
├── QuickActionRow (@Builder) — Row, SpaceAround, 白卡
│ └── ForEach actions × 4 — Column (emoji 容器 + 文字)
├── WeekChartCard (@Builder) — Column, 白卡
│ ├── Row (标题 + '30 次')
│ └── Row (ForEach weekBars × 7, 渐变柱 + 星期标签)
├── CategoryBar (@Builder) — 横向 Scroll
│ └── Row (ForEach cats × 4, 条件样式 Text)
├── ItemList (@Builder) — Column
│ ├── Row (标题 + '查看全部 ›')
│ └── ForEach items × 3 — Column 卡片
│ ├── Row (emoji 容器 56×56 + 标题/标签/描述 Column)
│ └── Row ('进度' + Stack 手绘进度条 + 百分比)
└── Blank (height safeBottom + 20)
这个树形结构清晰地展示了 “自顶向下、逐层细化” 的 ArkUI 组件组织方式。每个节点职责单一,嵌套深度控制在 4~5 层以内,保证了代码的可读性和可维护性。
十一、写在最后
首页以 193 行代码实现了功能完整的智能管理仪表盘:沉浸式全屏适配的渐变 HeroCard、四宫格快捷入口、可点击交互的动态柱状图、横向可滚动的多维筛选标签、带手绘进度条的推荐列表。它是学习 ArkUI 声明式 UI 和 @Builder 组件化 的最佳实战案例之一。
特别值得学习的是其 “渐进式信息披露” 设计——页面从上到下按照"总览 → 操作 → 趋势 → 筛选 → 详情"的逻辑顺序排列,符合用户打开 App 时的自然扫视路径。每个区域的视觉权重(通过字号、颜色、间距控制)也精确匹配了其信息重要性。
更多推荐





所有评论(0)