鸿蒙清新卡片式审美:ArkUI 声明式布局搭出待办清单的统计与输入面板
实例:待办事项清单(Todo List)|风格:清新卡片式
一、整体布局结构
页面采用「统计面板 → 输入区 → 状态切换 → 列表 → 底部操作栏」五段式结构,让面板内容丰富但不拥挤:
┌────────────────────────────────────┐ │ ✅ 待办清单 共 15 项 │ ← 标题栏 ├────────────────────────────────────┤ │ ▢ 未完成 12 ✓ 已完成 3 ⚡高优 4 │ ← 统计卡(3 个数字块) ├────────────────────────────────────┤ │ [输入框: 输入新待办…] [分类] [新增] │ ← 输入区 ├────────────────────────────────────┤ │ [全部] [未完成] [已完成] │ ← 状态切换(分段控件) ├────────────────────────────────────┤ │ ┌──────────────────────────────┐ │ │ │ ⬜ 完成周报提交 工作 ⚡ │ │ ← 待办卡片(多张) │ │ 今天 09:30 │ │ │ │ [改] [完成] [删] │ │ │ └──────────────────────────────┘ │ │ ... │ ├────────────────────────────────────┤ │ 已完成 3 项 ▏▏▏▏▏▏▏▏▏▏ 20% 清空 │ ← 底部进度条 + 清空已完成 └────────────────────────────────────┘二、各区块说明
1. 标题栏
- 左侧:
✅ 待办清单标题 + 副标题「优先级排序 · 未完成在前」 - 右侧:刷新按钮(
↻),点击重新查询
2. 统计卡(3 个数字块)
| 数据项 | 数据来源 | 展示样式 |
|---|---|---|
| 未完成 | statistics().pending | 大号蓝色数字 |
| 已完成 | statistics().done | 大号绿色数字 |
| 高优先级 | statistics().high | 大号橙色数字 |
卡片为圆角白底、左右三块均分,数字下方有小字标签。
3. 输入区
| 组件 | 说明 |
|---|---|
| TextInput | 待办内容输入,占位符「输入新待办,回车或点新增…」 |
| Select(分类) | 下拉选择:工作 / 生活 / 学习 / 日常 |
| 优先级选择 | 三选一按钮:低 · 中 · 高 |
| 新增按钮 | 主色填充圆角按钮「+ 新增」 |
4. 状态切换分段控件
三个 Tab:全部 / 未完成 / 已完成,切换后调用queryByCompleted()刷新列表,同时统计卡联动变化。
5. 待办卡片(列表主体)
每张卡片包含:
| 元素 | 样式 |
|---|---|
| 完成勾选圈 | 未完成:空心圆;已完成:绿色实心圆 + 白勾 |
| 标题 | 未完成黑色加粗;已完成灰色 + 删除线 |
| 分类标签 | 小圆角底色标签(工作蓝/生活绿/学习紫/日常灰) |
| 优先级 | 高优显示 ⚡ 橙色角标 |
| 时间 | 灰色小字「MM-DD HH:mm」 |
| 操作按钮 | 「改」打开编辑弹窗、「完成/取消完成」、「删除」 |
已完成卡片整体置灰、透明度 0.6,视觉上弱化。
6. 底部进度条
- 完成率进度条:
已完成/总数百分比 - 右侧「清空已完成」按钮(红色小字),一键删除全部已完成项
三、UI 风格要素
| 风格项 | 取值 |
|---|---|
| 背景色 | #F5F7FA浅灰蓝 |
| 卡片底色 | 白色,圆角 12,浅阴影 |
| 主色 | #3B82F6蓝(按钮/选中态) |
| 成功色 | #22C55E绿(完成态) |
| 警告色 | #F97316橙(高优先级) |
| 删除色 | #EF4444红 |
| 字体 | 默认 HarmonyOS Sans |
四、弹窗交互
- 编辑弹窗:点击「改」弹出底部弹窗,可修改内容、分类、优先级、备注,点「保存」调用
update() - 删除确认:点「删除」弹出 AlertDialog 二次确认,避免误删
- 清空确认:清空已完成前同样弹窗确认
五、空态与加载
| 场景 | 展示 |
|---|---|
| 无任何待办 | 居中插画表情 + 「暂无待办,输入上方内容开始吧」 |
| 切换分类后无数据 | 「该分类下暂无待办」提示条 |
| 加载中 | 列表顶部显示细进度条 |
六、文章小结
本篇设计了五段式页面结构:统计面板让顶部信息量充足,分段控件提供三种视图,卡片承载待办内容,底部进度条呈现完成率。整体「清新卡片式」风格统一、信息层级清晰,且每个数字、每个标签都直接对应 DAO 查询结果,为后续文章的数据填充做好了 UI 容器准备。
七、页面信息架构与状态管理详解
1. @Entry 与 @Component:入口与组件的边界
在 ArkTS 中,@Entry标记的 struct 是页面级入口,可被router.pushUrl()跳转加载;@Component则声明一个可复用的 UI 组件。本页由三层结构组成:
@Entry// 页面入口:可被路由加载@Componentstruct TodoPage{// 顶层页面容器@StatetodoList:TodoItem[]=[]// 列表数据@Statestatistics:Statistics={pending:0,done:0,high:0}@StatecurrentTab:number=0// 0全部 1未完成 2已完成@StateinputText:string=''// 输入框内容@StateshowLoading:boolean=false// 加载中开关aboutToAppear(){this.refreshAll()// 页面首次出现即查库}...}2. @State 是声明式刷新的「信号源」
| 装饰器 | 作用 | 本页应用 |
|---|---|---|
@Entry | 标记页面入口,可被 router 跳转 | 顶层 TodoPage |
@Component | 声明组件,可独立复用 | TodoPage / TodoCard |
@State | 组件内可变状态,变化即触发 UI 重建 | todoList、currentTab 等 |
@Builder | 复用一段 UI 结构 | 统计数字块、卡片操作按钮 |
核心思想是只改数据、不碰 DOM:点「完成」后只需this.todoList[i].completed = 1,ArkUI 会自动对比新旧状态树并重绘对应卡片,开发者无需手动刷新或操作节点。
3. 从 DAO 到 UI 的数据流闭环
用户点击(新增/完成/筛选) ↓ 调用 TodoDao.insert() / toggleCompleted() / queryByCompleted() ↓ Promise 返回结果 赋值给 @State 变量(todoList / statistics) ↓ 状态变化 ArkUI 状态树 diff → 只重建受影响的组件这条链路里,DAO 负责「取数」,@State 负责「存数」,ArkUI 负责「画数」,三者各司其职,页面代码因此保持简洁。
八、四大区块的 UI 实现与样式拆解
1. 标题栏:Flex 两端对齐
Row(){Column(){Text('✅ 待办清单').fontSize(22).fontWeight(FontWeight.Bold)Text('优先级排序 · 未完成在前').fontSize(12).fontColor('#9CA3AF')}.alignItems(HorizontalAlign.Start)Blank()// 弹性占位:把两侧顶开Button('↻').onClick(()=>this.refreshAll())}.width('100%').padding(16)要点:Blank()是弹性空白组件,自动占据剩余宽度,实现「左标题、右按钮」的经典顶栏;刷新按钮回调refreshAll()重新查库。
2. 统计面板:三等分数字卡
Row(){this.statBlock('未完成',this.statistics.pending,'#3B82F6')this.statBlock('已完成',this.statistics.done,'#22C55E')this.statBlock('高优先级',this.statistics.high,'#F97316')}.padding(12).backgroundColor(Color.White).borderRadius(12).shadow({radius:6,color:'rgba(0,0,0,0.06)'})三个数字块用@Builder抽成公共方法,保证样式完全一致:
@BuilderstatBlock(label:string,value:number,color:string){Column(){Text(`${value}`).fontSize(28).fontWeight(FontWeight.Bold).fontColor(color)Text(label).fontSize(12).fontColor('#6B7280')}.layoutWeight(1).padding({top:12,bottom:12})}layoutWeight(1)让三块在 Row 内均分宽度,这是「卡片三等分」的关键,也方便日后增加第四块数字。
3. 输入区:TextInput + Select + 主色按钮
| 控件 | 关键属性 | 说明 |
|---|---|---|
| TextInput | placeholder / onChange | 待办内容输入,双向绑定 inputText |
| Select | options 数组 | 分类下拉:工作/生活/学习/日常 |
| Button | backgroundColor 主色 | 「+ 新增」提交并清空输入框 |
Row({space:8}){TextInput({placeholder:'输入新待办,回车或点新增…',text:this.inputText}).onChange(v=>this.inputText=v).layoutWeight(1)Button('+ 新增').backgroundColor('#3B82F6').onClick(()=>this.addTodo())}.width('100%').padding(12)4. 列表卡片:状态驱动的行样式
ForEach(this.todoList,(item:TodoItem)=>{TodoCard({item:item,onToggle:()=>this.toggleItem(item.id),onDelete:()=>this.confirmDelete(item.id),onEdit:()=>this.openEditDialog(item)})},(item:TodoItem)=>`${item.id}`)已完成卡片的置灰、删除线全部由三元表达式驱动,无需写 if/else 分支去操作样式对象:
.opacity(item.completed===1?0.6:1)Text(item.title).decoration({type:item.completed===1?TextDecorationType.LineThrough:TextDecorationType.None}).fontColor(item.completed===1?'#9CA3AF':'#1F2937')九、状态筛选与交互设计
1. 点击筛选:分段控件联动统计
Tabs({barPosition:BarPosition.Start}){TabContent(){this.listByState(0)}.tabBar('全部')TabContent(){this.listByState(1)}.tabBar('未完成')TabContent(){this.listByState(2)}.tabBar('已完成')}.onChange(asyncindex=>{this.currentTab=indexthis.todoList=awaitTodoDao.queryByCompleted(this.context,index===0?-1:index-1)// -1 表示不过滤this.statistics=awaitTodoDao.statistics(this.context)// 统计联动})切换 Tab 触发重新查库,列表与统计卡同步刷新,形成「筛选 → 查询 → 渲染」的完整闭环,保证任何视图下数字都真实可信。
2. 删除确认:AlertDialog 二次确认
AlertDialog.show({title:'确认删除',message:'删除后不可恢复,确定删除这条待办吗?',primaryButton:{value:'取消',action:()=>{}},secondaryButton:{value:'删除',fontColor:'#EF4444',action:()=>this.deleteItem(pendingId)}})删除是破坏性操作,二次确认可避免误触;「清空已完成」按钮同样复用该弹窗,只是 message 改为「将删除全部 N 条已完成待办」。
十、ArkTS 声明式开发的注意事项
1. ForEach 的 key 必须唯一且稳定
| key 生成写法 | 是否推荐 | 原因 |
|---|---|---|
(item) => item.id | ✅ 推荐 | 主键唯一、不随渲染变化,增删不错位 |
(item) => item.title | ⚠️ 谨慎 | 内容可重复、可编辑,复用会错乱 |
(item, index) => index | ❌ 避免 | 增删后 index 漂移,动画与状态全乱 |
2. 事件绑定传参要用箭头函数
// ❌ 错误:render 时就立即执行了 deleteItem.onClick(this.deleteItem(item.id))// ✅ 正确:箭头函数延迟到点击时才执行.onClick(()=>this.deleteItem(item.id))3. 状态驱动刷新:改「数据」而非改「样式」
- 数组整体替换:
this.todoList = newList,触发全量 diff(推荐) - 单项更新:先
splice(i, 1, newItem)整体替换该下标元素 - 禁止直接改
@State数组元素属性后不赋值——ArkUI 无法感知对象内部的细粒度变化
十一、FAQ 常见问题
Q1:统计数字刷新了,列表却没变,为什么?
A:多半是列表用了另一个数组(如临时变量)赋值,或queryByCompleted返回新数组后没有整体赋给todoList。检查 @State 变量是否被真正替换。
Q2:删除卡片后动画错乱、行内容串位?
A:几乎都是 ForEach 的 key 用了 index。改用item.id后,ArkUI 才能正确识别「谁被删、谁保留」。
Q3:@State 里存对象,改了对象属性页面不刷新?
A:需要整体替换对象引用(如this.statistics = { ...this.statistics, done: n }),或将高频变化的字段拆成独立 @State。
Q4:如何让加载进度条只在查询期间显示?
A:用@State showLoading,在 await 查询前置true、拿到结果后置false,配合if (this.showLoading) { LoadingProgress() }条件渲染即可。
