
09数据持久化只是第一步将数据以优雅的交互形式呈现给用户才是最终目标。本文将围绕TodoView.ets讲解如何使用 ArkUI 的ListForEach渲染待办列表、swipeAction实现滑动删除、CustomImageToggle处理完成状态以及空状态、计时器、诗词阅读等辅助功能的集成。1. TodoView 组件概览Componentexportstruct TodoView{privatepageContext:PageContextAppStorage.get(pageContext)asPageContext;StorageProp(GlobalInfoModel)globalInfoModel:GlobalInfoModelAppStorage.get(GlobalInfoModel)!;Statearr:TodoItem[][]privatetodoDb:TodoDatabasenewTodoDatabase(getContext());StateisTodoEmpty:booleanfalse// ... 其他状态}核心状态State arr: TodoItem[]待办事项数组驱动列表渲染。StorageProp(GlobalInfoModel)全局设备信息获取状态栏高度等参数。todoDb数据库实例负责 CRUD 操作。isTodoEmpty控制空状态与列表状态的切换。2. 初始化数据库与示例数据asyncaboutToAppear():Promisevoid{awaitthis.todoDb.initialize();letisFirstPreferencesUtil.getBooleanSync(isFirst,true)if(isFirst){this.initDemoTodoList();}else{this.arrawaitthis.todoDb.getAllTodos();}this.getPoem()font.registerFont({familyName:kaiti,familySrc:$rawfile(font/kaiti.ttf)})}初始化流程初始化数据库——await this.todoDb.initialize()。判断首次启动——通过PreferencesUtil读取标记。首次启动调用initDemoTodoList()添加示例数据。非首次启动从数据库加载所有待办事项。注册楷体字体——font.registerFont注册自定义字体供诗词显示使用。2.1 示例数据privateinitDemoTodoList(){this.arr.push({id:1,time:08:00,content:(示例)晨读,isCompleted:true,date:,dayOfWeek:});this.arr.push({id:2,time:09:00,content:(示例)吃早饭,isCompleted:false,date:,dayOfWeek:});this.arr.push({id:3,time:10:00,content:(示例)语文作业,isCompleted:false,date:,dayOfWeek:});}首次用户看到三条示例待办帮助他们快速理解应用功能。3. List ForEach 渲染待办列表3.1 空状态与列表状态切换if(this.isTodoEmpty){this.buildEmptyList()}else{this.buildTodoList()}3.2 空状态展示BuilderbuildEmptyList(){Column({space:10}){Text(当前暂无待办事项).fontColor($r(app.color.color_sub_text)).fontSize(12)Button(添加).height(36).fontSize(14).backgroundColor($r(app.color.app_primary)).fontColor($r(app.color.color_card)).borderRadius(25).onClick((){this.isShowTasktrue;});}.backgroundColor($r(app.color.color_card)).layoutWeight(1).justifyContent(FlexAlign.Center).width(100%).margin({bottom:40}).borderRadius(12)}当列表为空时显示提示文字和「添加」按钮引导用户创建第一条待办。3.3 列表渲染BuilderbuildTodoList(){List({space:12,scroller:this.scrollerForList}){ForEach(this.arr,(item:TodoItem,index:number){ListItem(){Row(){Column(){Text(item.content).layoutWeight(1).fontWeight(FontWeight.Bold)Row({space:6}){Image($r(app.media.ic_time_icon)).width(18);Text(item.time).fontColor($r(app.color.color_text_light));};}.layoutWeight(1).alignItems(HorizontalAlign.Start);CustomImageToggle({isOn:item.isCompleted,onToggleChange:(value){// 完成状态切换逻辑}});}.width(100%).height(100%);}.swipeAction({end:{builder:(){this.itemEnd(index);},actionAreaDistance:40,}}).height(80).padding({left:12,top:5,bottom:5,right:12}).borderRadius(12).backgroundColor($r(app.color.color_card));},(item:TodoItem)item.id?.toString());}.listDirection(Axis.Vertical).scrollBar(BarState.Off).friction(0.6).margin({bottom:40}).width(100%)}关键组件解析List可滚动列表容器space: 12设置列表项间距。ForEach渲染列表数据第三个参数是键值生成函数(item) item.id?.toString()用于高效 diff 更新。ListItem列表项容器支持swipeAction滑动操作。scrollerForListScroller控制器支持编程式滚动。ForEach 键值必须为每个列表项提供唯一键值否则可能导致列表更新异常。本项目使用item.id?.toString()作为键值。4. swipeAction滑动删除4.1 滑动操作配置.swipeAction({end:{builder:(){this.itemEnd(index);},actionAreaDistance:40,}})end向左滑动后显示的操作区域尾部。builder滑动操作区域的 UI 构建器。actionAreaDistance: 40滑动操作区域的宽度。4.2 删除按钮构建BuilderitemEnd(index:number){Row(){Button(删除).borderRadius(5).backgroundColor($r(app.color.app_primary)).onClick(async(){if(!this.arr[index].date){this.arr.splice(index,1)}else{this.todoDb.deleteTodo(this.arr[index].id??0).then((isDelete:boolean){if(isDelete){this.arr.splice(index,1)}})}if(this.arr.length0){this.isTodoEmptytrue}})}.margin({left:7})}删除逻辑说明判断数据来源如果item.date为空说明是示例数据未入库直接从数组移除。数据库中的数据先调用todoDb.deleteTodo()删除成功后再从数组移除。空列表检测删除后如果数组为空切换到空状态视图。5. CustomImageToggle完成状态切换CustomImageToggle({isOn:item.isCompleted,onToggleChange:(value){letcurrentBonusPreferencesUtil.getNumberSync(AgentConstant.SP_BONUS,0)if(!valuecurrentBonus0){item.isCompletedtrue;return;}item.isCompletedvalue;PreferencesUtil.putSync(AgentConstant.SP_BONUS,value?currentBonus1:currentBonus-1)this.todoDb.updateTodo(item.id!!,item)this.finishVisiblethis.arr.every(itemitem.isCompletedtrue);}});完成状态切换涉及积分bonus系统获取当前积分从PreferencesUtil读取SP_BONUS。取消完成的保护逻辑如果积分为 0不允许取消完成状态防止积分变为负数。更新积分完成 1取消完成 -1通过PreferencesUtil.putSync持久化。更新数据库调用todoDb.updateTodo()保存完成状态。全完成检测如果所有待办都已完成显示庆祝弹窗finishVisible true。6. 添加待办半模态弹窗.bindSheet($$this.isShowTask,this.todoAddBuilder(),{blurStyle:BlurStyle.Thick,dragBar:true,title:this.DialogTitle(今日任务,添加,async(){if(StrUtil.isEmpty(this.value)){ToastUtil.showToast(任务内容不能为空)return}if(StrUtil.isEmpty(this.startTime)){ToastUtil.showToast(开始时间不能为空)return}if(StrUtil.isEmpty(this.stopTime)){ToastUtil.showToast(结束时间不能为空)return}constnewTodo:TodoItem{date:DateUtil.getTodayStr(yyyy-MM-dd),dayOfWeek:周DateUtil.getWeekDay(DateUtil.getTodayStr(HH-mm-ss)),time:${this.startTime}-${this.stopTime},content:this.value,isCompleted:false};consttodoIdawaitthis.todoDb.addTodo(newTodo);PreferencesUtil.putSync(isFirst,false);this.arr[]this.arrawaitthis.todoDb.getAllTodos();this.startTimethis.stopTimethis.isShowTaskfalsethis.isTodoEmptyfalse}),height:this.sheetHeight,keyboardAvoidMode:SheetKeyboardAvoidMode.TRANSLATE_AND_RESIZE})**半模态bindSheet**关键点$$this.isShowTask双向绑定显示状态$$语法允许框架内部修改该值如用户下滑关闭。blurStyle: BlurStyle.Thick背景毛玻璃效果。dragBar: true显示拖拽条用户可下滑关闭。keyboardAvoidMode键盘弹出时的避让模式。添加待办的表单包含任务名称、开始时间和结束时间通过IBestField和IBestCell组件实现输入和时间选择。6.1 刷新数据的方式this.arr[]this.arrawaitthis.todoDb.getAllTodos();先清空数组再重新赋值确保State能检测到变化并触发 UI 刷新。直接使用this.arr await ...赋值也是可行的但清空后赋值的模式在某些场景下更可靠。7. 功能入口IconText 组件Flex({justifyContent:FlexAlign.SpaceBetween}){IconText({icon:$r(app.media.ic_func_stroke),txt:每日诗词,fontColor:$r(app.color.color_function_txt_green),bgColor:$r(app.color.color_function_green),onFuncClick:async(){this.userIdUserInfoManager.getUserInfo()?.unionID??if(StrUtil.isBlank(this.userId)){this.login()return}awaitthis.getPoem();if(this.poemInfo){this.pageContext.openPage({param:this.poemInfo,routerName:PoemPage,onReturn:async(data){this.getPoem()}},true);}}}).width(45%)IconText({icon:$r(app.media.ic_func_speak),txt:练字字帖,fontColor:$r(app.color.color_function_txt_purple),bgColor:$r(app.color.color_function_blue),onFuncClick:(){this.pageContext.openPage({routerName:CopyPage},true);}}).width(45%)}.width(100%)IconText是项目封装的图标文字入口组件每个功能入口有独立的配色方案。点击「每日诗词」时需要登录验证而「练字字帖」则直接跳转。8. TimerComponent番茄钟计时器TimerComponent({totalTime:this.settingTime,onTimeUp:this.onTimerFinished,onTimeChange:this.onTimeChanged,controller:this.timerController}).onClick((){showDialogTime({titleBarAttribute:{titleText:请选择计时时间},timeAttribute:{timeType:TimeDialogType.HMS,startHours:0,startMinutes:0,startSeconds:0,selectTime:00-05-00,endHours:23,endMinutes:59,endSeconds:59,},timeConfirmClick:(date){this.settingTime(Number(date.getHours())||0)*3600(Number(date.getMinutes())||0)*60(Number(date.getSeconds())||0)},})})计时器默认 25 分钟番茄钟点击后弹出时间选择对话框用户可自定义时长。计时结束时触发onTimerFinished播放提醒铃声。9. 诗词阅读与字体注册9.1 楷体字体注册font.registerFont({familyName:kaiti,familySrc:$rawfile(font/kaiti.ttf)})通过font.registerFont注册自定义字体文件注册后可在Text组件中使用.fontFamily(kaiti)指定字体。9.2 诗词显示与朗读Column({space:12}){Text(this.poem).fontFamily(kaiti).fontWeight(FontWeight.Bold).textAlign(TextAlign.Center).lineSpacing(LengthMetrics.fp(18))}.mainCardStyle().onClick((){PoemReader.play(this.poem);}).fontFamily(kaiti)使用注册的楷体字体营造传统文化氛围。PoemReader.play点击诗词卡片触发朗读基于 TTS文本转语音能力。10. Scroll 容器配置Scroll(this.scroller){Column({space:10}){// 标题行// 倒计时 诗词// 功能入口// 待办列表}}.height(100%).width(100%).friction(0.6).align(Alignment.Top).scrollBar(BarState.Off).edgeEffect(EdgeEffect.Spring)属性说明.friction(0.6)滚动摩擦系数小于 1 更灵敏.scrollBar(BarState.Off)隐藏滚动条.edgeEffect(EdgeEffect.Spring)边界回弹效果提升手感11. 总结知识点说明ListForEach可滚动列表渲染ForEach 需提供唯一键值swipeAction滑动操作end配置左滑删除CustomImageToggle自定义开关组件处理完成状态与积分联动bindSheet半模态弹窗$$双向绑定显示状态PreferencesUtil轻量偏好存储保存积分和首次启动标记font.registerFont注册自定义字体$rawfile引用 rawfile 资源TimerComponent番茄钟计时器时间选择对话框集成PoemReader.playTTS 诗词朗读IconText功能入口组件带图标、文字和配色ScrollEdgeEffect.Spring弹性滚动效果空状态切换条件渲染空列表 vs 数据列表数据刷新模式清空数组 → 重新查询赋值确保 State 触发更新待办列表看似是一个简单的 CRUD 页面实则涉及列表渲染、手势交互、状态管理、数据持久化、自定义字体、计时器、TTS 朗读等多个技术点的综合运用。理解这些模式的组合方式是构建复杂交互页面的基础。