鸿蒙原生应用实战(四):收藏页面与底部导航实现——状态管理与跨页面交互
鸿蒙原生应用实战(四):收藏页面与底部导航实现——状态管理与跨页面交互
前言
前三章我们完成了首页、诗词库、详情页和作者天地四个页面的开发。本章将完成最后一个核心页面——收藏页面,并实现全局底部导航栏的整合。
收藏页面的实现涉及了数据筛选、编辑模式、分类分组等复杂交互,是检验 ArkTS 状态管理能力的试金石。
一、收藏页面(CollectionPage.ets)
1.1 页面布局总览
┌──────────────────────────┐ │ 我的收藏 编辑 返回 │ ← 顶部栏 ├──────────────────────────┤ │ ⭐ 6首 │ ← 收藏统计 │ 共收藏诗词 │ ├──────────────────────────┤ │ 收藏分类 │ │ 📚全部 📜唐诗 🌸宋词 │ ← 横向分类标签 │ 🎭元曲 📗诗经 🌙五代词 │ ├──────────────────────────┤ │ 📖 静夜思 五言绝句 │ │ 唐 · 李白 │ ← 收藏卡片 │ "思乡名篇,百读不厌" │ │ 收藏于 2025-06-15 │ ├──────────────────────────┤ │ 📖 水调歌头 词 │ │ 宋 · 苏轼 │ │ "中秋绝唱,意境超然" │ │ 收藏于 2025-06-14 │ ├──────────────────────────┤ │ ...(共 6 条) │ ├──────────────────────────┤ │ 🏠首页 📚诗词库 👤作者 ⭐收藏 │ └──────────────────────────┘1.2 数据结构
收藏数据包含诗词基本信息、收藏日期和用户笔记:
interfaceCollectionItem{id:number;title:string;author:string;dynasty:string;type:string;dateAdded:string;// 收藏日期notes:string;// 用户笔记}// 收藏分类分组interfaceCollectionGroup{name:string;icon:string;count:number;}1.3 收藏分类分组
我们将收藏的诗词按朝代分类,每个分类显示对应的 emoji 和计数:
constgroups:CollectionGroup[]=[{name:'唐诗',icon:'📜',count:1},{name:'宋词',icon:'🌸',count:3},{name:'元曲',icon:'🎭',count:1},{name:'诗经',icon:'📗',count:1},{name:'五代词',icon:'🌙',count:1}];1.4 数据过滤实现
收藏页面的数据过滤同样使用@State + @Watch模式:
@State@Watch('onGroupChange')selectedGroup:string='';@StateeditMode:boolean=false;@StatefilteredList:CollectionItem[]=myCollections;onGroupChange():void{if(this.selectedGroup===''){this.filteredList=myCollections;return;}constdynastyMap:Record<string,string>={'唐诗':'唐','宋词':'宋','元曲':'元','诗经':'先秦','五代词':'五代'};consttargetDynasty:string=dynastyMap[this.selectedGroup]||'';this.filteredList=myCollections.filter((c:CollectionItem)=>c.dynasty===targetDynasty);}1.5 分类标签高亮
分类标签使用@State驱动的条件样式:
// "全部"标签Column(){Text('📚').fontSize(28)Text('全部').fontSize(11).fontColor(this.selectedGroup===''?$r('app.color.accent_purple'):$r('app.color.text_primary')).fontWeight(this.selectedGroup===''?FontWeight.Bold:FontWeight.Normal)Text(myCollections.length.toString()).fontSize(10).fontColor($r('app.color.text_secondary'))}.width(60).alignItems(HorizontalAlign.Center).onClick(()=>{this.selectedGroup='';}).backgroundColor(this.selectedGroup===''?$r('app.color.accent_purple')+'10':Color.Transparent).borderRadius(12)// 各分类标签ForEach(groups,(g:CollectionGroup)=>{Column(){Text(g.icon).fontSize(28)Text(g.name).fontSize(11).fontColor(this.selectedGroup===g.name?$r('app.color.accent_purple'):$r('app.color.text_primary'))Text(g.count.toString()+'首').fontSize(10).fontColor($r('app.color.text_secondary'))}.onClick(()=>{this.selectedGroup=g.name;}).backgroundColor(this.selectedGroup===g.name?$r('app.color.accent_purple')+'10':Color.Transparent)},(g:CollectionGroup)=>g.name)1.6 收藏卡片设计
每条收藏记录展示完整信息,包含用户笔记(斜体显示):
@BuildercreateCollectionCard(item:CollectionItem){Row(){// 编辑模式下的选中框if(this.editMode){Circle().width(22).height(22).stroke($r('app.color.accent_purple')).strokeWidth(2).fill(Color.Transparent).margin({right:10})}Column(){Text('📖').fontSize(28)}Column(){// 标题 + 类型标签Row(){Text(item.title).fontSize(17).fontWeight(FontWeight.Bold)Text(item.type).fontSize(10).fontColor($r('app.color.accent_purple')).padding({left:6,right:6,top:2,bottom:2}).backgroundColor($r('app.color.accent_purple')+'15').borderRadius(4).margin({left:8})}Text(item.dynasty+' · '+item.author).fontSize(12).fontColor($r('app.color.text_secondary'))// 用户笔记(斜体显示)Text(item.notes).fontSize(13).fontColor($r('app.color.text_primary')).fontStyle(FontStyle.Italic).maxLines(1).textOverflow({overflow:TextOverflow.Ellipsis})Text('收藏于 '+item.dateAdded).fontSize(11).fontColor($r('app.color.text_secondary'))}.layoutWeight(1).padding({left:12})if(!this.editMode){Text('>').fontSize(18).fontColor($r('app.color.text_secondary'))}}.width('100%').padding(14).backgroundColor($r('app.color.bg_card')).borderRadius(12).onClick(()=>{if(!this.editMode){router.pushUrl({url:'pages/PoemDetailPage',params:{poemId:item.id}});}})}1.7 空状态处理
当筛选结果为空时,展示友好的空状态提示:
if(this.filteredList.length===0){Column(){Text('📭').fontSize(48)Text('暂无收藏').fontSize(16).fontColor($r('app.color.text_secondary')).margin({top:12})}.width('100%').height(200).alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)}二、编辑模式实现
收藏页面支持编辑模式,点击顶部"编辑"按钮切换:
Row(){Text('我的收藏').fontSize(24).fontWeight(FontWeight.Bold)Blank()Text(this.editMode?'完成':'编辑').fontSize(14).fontColor($r('app.color.accent_purple')).onClick(()=>{this.editMode=!this.editMode;})}编辑模式下,每个卡片左侧显示圆形选中框,方便用户批量操作。
三、底部导航栏
3.1 统一设计
所有 5 个页面底部共享同一套导航栏,包含 4 个 Tab:首页、诗词库、作者、收藏。
导航栏放在页面的最底层,使用shadow属性创建阴影效果:
Row(){this.navItem('🏠','首页','home',activePage,'pages/Index')this.navItem('📚','诗词库','list',activePage,'pages/PoemListPage')this.navItem('👤','作者','author',activePage,'pages/AuthorPage')this.navItem('⭐','收藏','collection',activePage,'pages/CollectionPage')}.width('100%').height(60).backgroundColor($r('app.color.bg_card')).padding({top:6,bottom:6}).shadow({radius:8,color:'#15000000',offsetX:0,offsetY:-2// 向上投影,浮在页面上方})3.2 Tab 高亮逻辑
当前页面对应的 Tab 使用主题色,其他 Tab 使用灰色:
Text(label).fontSize(10).fontColor(page===activePage?$r('app.color.accent_purple'):$r('app.color.text_secondary'))3.3 Tab 点击跳转
点击非当前 Tab 时触发页面跳转,点击当前 Tab 不做任何操作:
.onClick(()=>{if(page!==activePage){router.pushUrl({url:route});}})四、ArkTS 中的响应式数据绑定
4.1 @State 装饰器
@State是 ArkTS 中最基础的响应式装饰器,被修饰的变量变化时会触发 UI 重新渲染:
@Componentstruct CollectionPage{@StateeditMode:boolean=false;@StatefilteredList:CollectionItem[]=myCollections;// ...}4.2 @Watch 装饰器
@Watch用于监听@State变量的变化,执行副作用逻辑:
@State@Watch('onGroupChange')selectedGroup:string='';关键点:@Watch必须直接修饰在@State变量上,不能单独使用。当selectedGroup变化时,onGroupChange方法自动被调用。
4.3 状态管理的完整流程
用户交互(点击分类标签) │ ▼ this.selectedGroup = '宋词' │ ├─→ UI 自动重渲染(分类标签高亮变化) │ └─→ @Watch 触发 onGroupChange() │ ▼ 执行过滤逻辑 │ ▼ this.filteredList = [...] │ └─→ UI 自动重渲染(收藏列表更新)五、运行错误修复:get 访问器问题
在实际运行中,我们遇到了一个严重的运行时错误:
TypeError: Cannot read property length of undefined错误根因:在 ArkTS 的动态模式下(arkTSMode: dynamic),get访问器的返回值在模板绑定中无法被正确识别为响应式数据。当在build()方法中使用this.filteredCollections.length或ForEach(this.filteredCollections, ...)时,this.filteredCollections返回的是undefined。
解决方案:统一使用@State+@Watch替代get访问器:
| 页面 | 原方案 | 修复方案 |
|---|---|---|
CollectionPage | get filteredCollections() | @State filteredList+@Watch('onGroupChange') |
PoemListPage | get filteredPoems() | @State filteredList+@Watch('onFilterChange') |
AuthorPage | get filteredAuthors() | @State filteredAuthorsList+@Watch('onAuthorFilterChange') |
PoemDetailPage | get poemData() | @State poemData+@Watch('onPoemIdChange') |
这个修复经验非常重要——在 API 23 的 ArkTS 动态模式下,凡是需要在模板中使用的计算数据,都应该用 @State 存储,用 @Watch 触发更新,而不是依赖get访问器。
小结
本章完成了收藏页面的开发,实现了:
- 收藏数据的分组分类展示
- 分类筛选与高亮交互
- 编辑模式切换
- 全局底部导航栏的统一设计
- @State + @Watch 响应式数据管理的最佳实践
- get 访问器在动态模式下的问题与修复
至此,应用的所有 5 个页面已经开发完毕。下一章将总结整个开发过程中的编译错误修复和调试经验。
【系列目录】
- (一)项目初始化与架构设计
- (二)首页与诗词库页面开发
- (三)诗词详情与作者天地页面开发
- (四)收藏页面与底部导航实现 ← 本文
- (五)编译调试与问题修复经验
