:首页开发与全局数据流设计)
鸿蒙原生应用实战二首页开发与全局数据流设计系列目录第一篇项目搭建与页面架构设计第二篇首页开发与全局数据流设计 ← 当前第三篇笔记详情与编辑页面的路由与CRUD第四篇分类浏览与个人中心的多维数据展示第五篇构建调试、异常处理与HAP发布一、前言上一篇我们搭建了项目骨架并设计了 5 页面的路由架构。本篇将正式进入编码——开发 App 的首页Index.ets它是整个应用的门面承载着笔记列表展示、搜索、分类筛选和新建入口等核心功能。同时我们将设计全局数据流方案让 5 个页面共享同一份笔记数据。二、首页布局架构首页是一个全屏Stack布局包含三层Stack (根容器) ├── Column (主内容层) │ ├── 顶部导航栏 (Row) │ ├── 搜索栏 (Row) │ ├── 分类筛选标签 (Scroll → Row) │ ├── 笔记列表 (List) 或 空状态 (Column) │ └── 底部导航栏 (Row) └── Column (悬浮FAB按钮层) └── 圆形新建按钮使用Stack作为根容器的好处FAB 按钮可以通过.align(Alignment.BottomEnd)轻松定位到右下角不会影响主内容区的布局。三、数据模型与全局状态3.1 定义 Note 接口在 Index.ets 顶部定义数据模型interfaceNote{id:number;title:string;content:string;category:string;// 工作 | 学习 | 生活 | 灵感date:string;// YYYY-MM-DD}3.2 AppStorage 全局状态鸿蒙的AppStorage是应用级的 UI 状态存储跨页面共享。我们用它来存储笔记数组// 写入任意页面AppStorage.setOrCreatestring(notes,JSON.stringify(allNotes));// 读取任意页面letstored:string|undefinedAppStorage.getstring(notes);letnotes:Note[]stored?JSON.parse(stored)asNote[]:[];为什么用 JSON 字符串而不是直接存对象数组因为AppStorage对复杂对象的序列化支持有限存储 JSON 字符串是跨版本最稳定的方式。3.3 预置示例数据为了让 App 第一次启动时有内容可看在getDefaultNotes()中预置 6 条笔记getDefaultNotes():Note[]{letdefaultNotes:Note[][{id:1,title:鸿蒙开发入门指南,content:HarmonyOS 是面向全场景的分布式操作系统...,category:学习,date:2024-12-01},{id:2,title:项目周报 - 第十二周,content:本周完成1. 用户模块接口联调 2. 首页UI重构...,category:工作,date:2024-12-15},// ... 更多示例数据];returndefaultNotes;}整个数据流的生命周期App 启动 → aboutToAppear() → loadNotes() ├── AppStorage 有数据→ 读取 → filterNotes() └── 无数据→ 预置数据 → 存入 AppStorage → filterNotes()四、顶部导航栏实现// 顶部导航栏Row(){Text($r(app.string.page_title_home))// 我的笔记.fontSize($r(app.float.title_font_size)).fontWeight(FontWeight.Bold).fontColor($r(app.color.text_primary))Blank()Image($r(app.media.foreground))// 头像图标.width(28).height(28).borderRadius(14).onClick(()this.goToProfilePage())}.width(100%).padding({left:$r(app.float.page_padding),right:$r(app.float.page_padding)})这里使用了$r()引用资源文件ArkTS 编译时会自动替换为实际值。注意Image这里用了默认资源的占位图实际开发中应替换为自定义头像。五、搜索栏实现搜索栏有点击展开/收起两种状态StateisSearchActive:booleanfalse;StatesearchText:string;// 搜索栏Row(){if(this.isSearchActive){TextInput({placeholder:搜索笔记...,text:this.searchText}).layoutWeight(1).height(40).onChange((value:string)this.onSearchChange(value))}else{Text(搜索笔记...).fontColor($r(app.color.text_tertiary)).layoutWeight(1)}// 搜索图标Image($r(app.media.foreground)).onClick(()this.toggleSearch())}搜索触发过滤逻辑filterNotes():void{letresult:Note[]this.notes;// 分类过滤if(this.selectedCategory!全部){resultresult.filter((note:Note)note.categorythis.selectedCategory);}// 关键词过滤标题正文if(this.searchText.length0){letkeyword:stringthis.searchText.toLowerCase();resultresult.filter((note:Note)note.title.toLowerCase().includes(keyword)||note.content.toLowerCase().includes(keyword));}this.filteredNotesresult;}六、分类筛选标签5 个分类标签用横向滚动的Scroll实现privatecategories:CategoryItem[][{label:全部,key:全部},{label:工作,key:工作},{label:学习,key:学习},{label:生活,key:生活},{label:灵感,key:灵感}];Scroll(){Row(){ForEach(this.categories,(item:CategoryItem){Text(item.label).padding({left:16,right:16,top:6,bottom:6}).backgroundColor(this.selectedCategoryitem.label?$r(app.color.primary)// 选中时蓝色:$r(app.color.card_bg)// 未选中时白色).fontColor(this.selectedCategoryitem.label?Color.White:$r(app.color.text_secondary)).borderRadius(16).onClick(()this.onCategoryChange(item.label))},(item:CategoryItem)item.key)}}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off)ForEach的第三个参数(item) item.key是键值生成器帮助 ArkTS 高效地 diff 和重用组件。七、笔记卡片列表每个笔记卡片包含分类标签、标题、内容预览、日期。List(){ForEach(this.filteredNotes,(note:Note){ListItem(){Column(){// 分类标签Text(note.category).fontColor(this.getCategoryColor(note.category)).border({width:1,color:this.getCategoryColor(note.category)}).borderRadius(4).alignSelf(ItemAlign.Start)// 标题 - 最多1行Text(note.title).fontSize($r(app.float.subtitle_font_size)).fontWeight(FontWeight.Medium).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis})// 预览 - 最多2行Text(note.content).fontSize($r(app.float.small_font_size)).fontColor($r(app.color.text_secondary)).maxLines(2).textOverflow({overflow:TextOverflow.Ellipsis})// 日期Text(note.date).fontSize($r(app.float.tiny_font_size)).fontColor($r(app.color.text_tertiary))}.padding(14).backgroundColor($r(app.color.card_bg)).borderRadius($r(app.float.card_radius))}.onClick(()this.goToNoteDetail(note))},(note:Note)note.id.toString())}空状态处理当过滤结果为空时显示友好的空状态if(this.filteredNotes.length0){// ... 列表}else{Column(){Text(还没有笔记点击下方按钮新建).fontColor($r(app.color.text_tertiary))}.layoutWeight(1).justifyContent(FlexAlign.Center)}八、悬浮新建按钮FAB利用Stack的特性将 FAB 放在主内容层之上Stack(){// 主内容 Column ...Column().width(100%).height(100%)// FABColumn(){/* 加号图标 */}.width(56).height(56).backgroundColor($r(app.color.primary)).borderRadius(28).onClick(()this.goToEditPage()).align(Alignment.BottomEnd).margin({bottom:72,right:24})}注意早期版本我用过.overlay()的 inline builder 方案但 ArkTS 严格模式不允许在.overlay()参数中使用内联 builder。改用Stack.align()是最简洁可靠的方案。九、底部导航栏三个底部 Tab 使用简单的文字 点击跳转当前页面高亮Row(){Text(笔记).fontColor($r(app.color.primary))// 高亮当前页Text(分类).onClick(()this.goToCategoryPage())Text(我的).onClick(()this.goToProfilePage())}.justifyContent(FlexAlign.SpaceAround).backgroundColor($r(app.color.card_bg))十、onPageShow 生命周期鸿蒙的页面生命周期中onPageShow在每次页面可见时调用。我们在首页和分类页都注册了它aboutToAppear():void{this.loadNotes();// 首次加载}onPageShow():void{this.loadNotes();// 从其他页面返回时刷新}这样当用户从编辑页面保存笔记返回后首页会自动刷新列表。十一、ArkTS 严格模式避坑11.1 对象字面量必须有类型// ❌ 错误arkts-no-untyped-obj-literalsprivatecategories[{label:全部,key:全部}];// ✅ 正确定义接口并用类型注解interfaceCategoryItem{label:string;key:string;}privatecategories:CategoryItem[][{label:全部,key:全部}];11.2 数组字面量必须可推断类型// ✅ 正确数组类型明确Statenotes:Note[][];StatefilteredNotes:Note[][];11.3 build() 方法只能有一个根节点// ❌ 错误build 中不能有 let 语句build(){letx1;// 编译错误Column(){}}// ✅ 正确在变量声明处计算或直接内联build(){Column(){}}十二、本篇总结本篇我们完成了✅ 首页的完整布局导航栏、搜索栏、分类标签、笔记列表、FAB✅ 基于 AppStorage 的全局数据流设计5 页面共享数据✅ 搜索分类双重过滤逻辑✅ 空状态处理、生命周期刷新✅ ArkTS 严格模式避坑指南下一篇将进入笔记详情页和编辑页面的开发重点讲解路由参数传递、CRUD 操作和删除确认弹窗。作者AtomCodeGitHub: [项目链接]如果本文对你有帮助请点赞收藏⭐关注➕