HarmonyOs应用《重要日》开发第9篇 - 重要日列表页面实现详解
本篇深入剖析 ImportantDayListPage 的完整实现包括页面结构、空状态处理、排序列表、浮动按钮以及响应式数据监听。一、页面功能概述重要日列表页面是应用的第一个 Tab是用户看到的主页面。它的功能包括顶部标题栏统计面板总计/即将到来/已过/本月排序选项栏重要日卡片列表空状态提示右下角浮动添加按钮二、页面结构设计2.1 整体布局┌─────────────────────────────────┐ │ 状态栏安全区域 padding │ ├─────────────────────────────────┤ │ 重要日 │ ← 标题栏 ├─────────────────────────────────┤ │ 总计 3 │ 即将 2 │ 已过 1 │ 本月 1│ ← 统计面板 ├─────────────────────────────────┤ │ 排序: 按日期 ▾ │ ← 排序栏 ├─────────────────────────────────┤ │ ┌───────────────────────────┐ │ │ │ 妈妈生日 30天后 │ │ ← DayCard │ │ 2027年3月15日 每年 │ │ │ └───────────────────────────┘ │ │ ┌───────────────────────────┐ │ │ │ 旅行出发 5天后 │ │ ← DayCard │ │ 2026年7月20日 │ │ │ └───────────────────────────┘ │ │ ┌────┐ │ │ │ │ │ ← FAB │ └────┘ │ └─────────────────────────────────┘2.2 代码实现// pages/ImportantDayListPage.etsComponentV2exportstruct ImportantDayListPage{privatevm:MainViewModelMainViewModel.instance;LocalavoidArea:AvoidAreaAppStorageV2.connect(AvoidArea,()newAvoidArea(0,0))!;LocalsortedDays:ImportantDay[][];aboutToAppear():void{this.refreshList();}Monitor(vm.dayList)onDayListChange():void{this.refreshList();}Monitor(vm.sortType)onSortChange():void{this.refreshList();}privaterefreshList():void{this.sortedDaysthis.vm.getSortedDays();}build(){Stack({alignContent:Alignment.BottomEnd}){Column(){// 标题栏Row(){Text(重要日).fontSize($r(app.float.title_font_size)).fontColor($r(app.color.font_primary)).fontWeight(FontWeight.Bold);}.width(100%).height(56).padding({left:20,right:20}).padding({top:px2vp(this.avoidArea.topRectHeight)});// 统计面板if(this.vm.dayList.length0){Column(){StatPanel();}.width(100%).padding({left:16,right:16,bottom:8});}// 排序栏if(this.vm.dayList.length0){Row(){Text($r(app.string.sort_hint)).fontSize($r(app.string.font_size_12)).fontColor($r(app.color.font_secondary));Text(this.sortTypeLabel()).fontSize($r(app.string.font_size_12)).fontColor($r(app.color.font_primary)).margin({left:4});}.width(100%).padding({left:20,right:20,top:8,bottom:8}).justifyContent(FlexAlign.Start).bindMenu([{value:按日期,action:(){this.vm.setSortType(SortType.BY_DATE);}},{value:按创建时间,action:(){this.vm.setSortType(SortType.BY_CREATED);}},{value:按倒计时,action:(){this.vm.setSortType(SortType.BY_COUNTDOWN);}},]);}// 列表 / 空状态if(this.vm.dayList.length0){this.emptyState();}else{List({space:12}){ForEach(this.sortedDays,(day:ImportantDay){ListItem(){DayCard({day:day})}.onClick((){this.vm.selectedDayIdday.id;router.pushUrl({url:pages/DayDetailPage});});},(day:ImportantDay)day.id);}.width(100%).layoutWeight(1).padding({left:16,right:16,bottom:16}).alignListItem(ListItemAlign.Center);}}.width(100%).height(100%).backgroundColor($r(app.color.page_background));// 浮动添加按钮Button(){Text().fontSize(32).fontColor(Color.White);}.width(56).height(56).borderRadius(28).backgroundColor($r(app.color.icon_checked)).margin({right:20,bottom:20}).onClick((){router.pushUrl({url:pages/AddEditDayPage});});}.width(100%).height(100%);}}三、关键实现细节3.1 响应式数据监听Monitor(vm.dayList)onDayListChange():void{this.refreshList();}Monitor(vm.sortType)onSortChange():void{this.refreshList();}两个独立的Monitorvm.dayList变化时刷新列表添加、删除、更新重要日vm.sortType变化时重新排序用户切换排序方式这种分离确保了不同类型的变化都能触发正确的更新。3.2 排序后的列表副本LocalsortedDays:ImportantDay[][];privaterefreshList():void{this.sortedDaysthis.vm.getSortedDays();}页面维护一个sortedDays副本而非直接使用vm.dayList。原因排序结果需要分离展示避免每次渲染都重新排序ForEach 的 key 生成器需要稳定的列表3.3 条件渲染// 统计面板和排序栏只在有数据时显示if(this.vm.dayList.length0){Column(){StatPanel();}// ...}// 有数据显示列表无数据显示空状态if(this.vm.dayList.length0){this.emptyState();}else{List({space:12}){...}}3.4 排序菜单.bindMenu([{value:按日期,action:(){this.vm.setSortType(SortType.BY_DATE);}},{value:按创建时间,action:(){this.vm.setSortType(SortType.BY_CREATED);}},{value:按倒计时,action:(){this.vm.setSortType(SortType.BY_COUNTDOWN);}},])使用bindMenu绑定弹出菜单。点击排序栏时弹出三个选项选择后调用vm.setSortType()触发Monitor(vm.sortType)自动刷新列表。3.5 排序标签privatesortTypeLabel():string{if(this.vm.sortTypeSortType.BY_DATE)return按日期;elseif(this.vm.sortTypeSortType.BY_CREATED)return按创建时间;elsereturn按倒计时;}3.6 浮动按钮FABStack({alignContent:Alignment.BottomEnd}){Column(){// 主内容}// FABButton(){Text().fontSize(32).fontColor(Color.White);}.width(56).height(56).borderRadius(28).backgroundColor($r(app.color.icon_checked)).margin({right:20,bottom:20}).onClick((){router.pushUrl({url:pages/AddEditDayPage});})}使用Stack的alignContent: Alignment.BottomEnd将 FAB 定位到右下角。FAB 是一个圆形按钮56x56圆角28使用主题蓝色背景。四、空状态设计BuilderemptyState(){Column(){Text().fontSize(64);Text($r(app.string.no_days_title)).fontSize($r(app.string.font_size_20)).fontColor($r(app.color.font_primary)).fontWeight(FontWeight.Medium).margin({top:16});Text($r(app.string.no_days_desc)).fontSize($r(app.string.font_size_14)).fontColor($r(app.color.font_secondary)).margin({top:8});}.width(100%).layoutWeight(1).justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center);}空状态包括一个大号日历 emoji64fp还没有重要日标题点击右下角按钮添加你的第一个重要日描述使用justifyContent(FlexAlign.Center)和layoutWeight(1)让空状态内容垂直居中。五、列表项点击处理ListItem(){DayCard({day:day})}.onClick((){this.vm.selectedDayIdday.id;router.pushUrl({url:pages/DayDetailPage});});点击列表项时将选中 ID 存入 ViewModelvm.selectedDayId day.id跳转到详情页面router.pushUrl详情页面通过vm.selectedDayId获取对应的重要日数据。这种通过 ViewModel 传递选中 ID 的方式避免了 URL 参数传递的限制。六、ForEach 的 key 生成器ForEach(this.sortedDays,(day:ImportantDay){ListItem(){DayCard({day:day})}},(day:ImportantDay)day.id);// ↑ key 生成器使用day.id作为 key确保列表更新时正确复用组件删除/插入操作不会导致全量重绘拖拽排序时能正确追踪元素七、样式设计7.1 颜色方案// color.json{name:page_background,value:#F1F3F5}// 浅灰背景{name:card_background,value:#FFFFFF}// 白色卡片{name:font_primary,value:#E6000000}// 主文字90% 黑{name:font_secondary,value:#99000000}// 次要文字60% 黑{name:icon_checked,value:#007DFF}// 主题蓝7.2 尺寸规范// float.json{name:title_font_size,value:24fp}{name:stat_number_font_size,value:28fp}{name:stat_label_font_size,value:12fp}所有尺寸使用fpfont-size pixel和vpvirtual pixel单位确保不同屏幕密度下的一致性。八、页面生命周期页面创建 │ ├── aboutToAppear() │ └── refreshList() → 获取排序后的列表 │ ├── build() 渲染 │ ├── 标题栏 │ ├── 统计面板条件渲染 │ ├── 排序栏条件渲染 │ ├── 列表 / 空状态 │ └── FAB │ ├── Monitor(vm.dayList) → 数据变化时刷新 ├── Monitor(vm.sortType) → 排序变化时刷新 │ └── aboutToDisappear()页面销毁九、小结ImportantDayListPage 是应用的核心页面通过Monitor实现了完全数据驱动的 UI 更新。Stack 布局实现浮动按钮、条件渲染处理空状态、bindMenu 提供排序选项——这些 ArkUI 组件和 API 的组合使用构建出了一个完整的列表页面体验。