十五条种子数据注入 项目:鸿蒙待办清单一开屏就热闹非凡

实例:待办事项清单(Todo List)
本文提供 15 条种子数据,让页面首次打开就内容丰富,面板统计数字饱满

一、种子数据设计思路

  • 覆盖 3 种优先级(低/中/高)
  • 覆盖 4 个分类(工作/生活/学习/日常)
  • 未完成 12 条、已完成 3 条(对应统计卡:未完成 12 / 已完成 3 / 高优 4)
  • 时间跨度为近一周,展示时间排序效果

二、种子数据表(15 条)

idtitleprioritycategorycompletedcreated_time备注
1完成季度周报并提交领导2工作0周一 09:30含图表
2预约明天上午的牙医1生活0周一 10:05电话 021-…
3阅读《SQLite 权威指南》第 4 章1学习0周一 20:15做笔记
4整理桌面文件,清理垃圾0日常0周二 08:40-
5给项目加异常日志上报2工作0周二 14:22需求 #128
6超市采购:牛奶、鸡蛋、水果1生活0周三 09:00预算 150
7复习 ArkTS 状态管理1学习0周三 19:30两小时
8给爸妈打电话1生活0周四 12:00半小时
9修复登录页闪退问题2工作0周四 15:47紧急
10缴纳水电燃气费0日常0周五 08:20-
11整理照片备份到云端1日常0周五 21:0050G
12准备周末分享会 PPT2工作0周六 10:00主题:RDB 实践
13完成健身卡续费1生活1上周 16:00已完成
14更新应用隐私协议1工作1上周 11:30已完成
15看完收藏的 3 篇技术文章0学习1上周 22:10已完成

统计效果:未完成 12(其中高优先级 4:id 1、5、9、12)、已完成 3 → 完成率 20%,底部进度条显示3/15 = 20%

三、种子数据 SQL

INSERTINTOtodo(title,priority,category,completed,created_time,completed_time,remark)VALUES('完成季度周报并提交领导',2,'工作',0,1715581800000,0,'含图表'),('预约明天上午的牙医',1,'生活',0,1715583900000,0,'电话 021-88886666'),('阅读《SQLite 权威指南》第 4 章',1,'学习',0,1715613300000,0,'做笔记'),('整理桌面文件,清理垃圾',0,'日常',0,1715647200000,0,''),('给项目加异常日志上报',2,'工作',0,1715660520000,0,'需求 #128'),('超市采购:牛奶、鸡蛋、水果',1,'生活',0,1715706000000,0,'预算 150'),('复习 ArkTS 状态管理',1,'学习',0,1715743800000,0,'两小时'),('给爸妈打电话',1,'生活',0,1715752800000,0,'半小时'),('修复登录页闪退问题',2,'工作',0,1715766420000,0,'紧急'),('缴纳水电燃气费',0,'日常',0,1715797200000,0,''),('整理照片备份到云端',1,'日常',0,1715850000000,0,'50G'),('准备周末分享会 PPT',2,'工作',0,1715868000000,0,'主题:RDB 实践'),('完成健身卡续费',1,'生活',1,1715500800000,1715500800000,'已完成'),('更新应用隐私协议',1,'工作',1,1715452200000,1715452200000,'已完成'),('看完收藏的 3 篇技术文章',0,'学习',1,1715590200000,1715590200000,'已完成');

时间戳为示例值,实际初始化时可用Date.now()偏移生成,保证列表时间展示合理。

四、DAO 内联初始化方法

TodoDao中提供initSeedData(),首次启动(表为空时)自动填充:

staticasyncinitSeedData(context:common.UIAbilityContext):Promise<void>{conststore=awaitthis.getStore(context);constcount=awaitstore.querySql(`SELECT COUNT(*) AS c FROM${this.TABLE}`);lettotal=0;if(count.goToNextRow())total=count.getLong(count.getColumnIndex('c'));count.close();if(total>0)return;// 已有数据,跳过constnow=Date.now();constseed=[{title:'完成季度周报并提交领导',priority:2,category:'工作',completed:0,createdTime:now-6*86400000,remark:'含图表'},// ... 其余 14 条同上表];for(constsofseed){constvalues:relationalStore.ValuesBucket={title:s.title,priority:s.priority,category:s.category,completed:s.completed,created_time:s.createdTime,completed_time:0,remark:s.remark,};awaitstore.insert(this.TABLE,values);}}

五、页面效果对照

面板区域填充后效果
统计卡未完成12/ 已完成3/ 高优先级4
全部视图15 张卡片,未完成在前,高优带 ⚡
未完成视图12 张卡片,前三张为高优任务
已完成视图3 张置灰卡片,带删除线
底部进度条已完成 3 项 ▏▏▏▏ 20%
分类标签工作 5 / 生活 4 / 学习 3 / 日常 3

六、扩展建议

方向做法
数据更多生成 50 条时用循环 + 随机组合标题
分组显示按 category GROUP BY 分段标题
截止日期增加 due_time 字段 + 逾期红色提示
拖动排序增加 sort_order 字段,长按拖动重排

七、文章小结

15 条种子数据覆盖了 4 分类 × 3 优先级 × 双状态 × 一周时间跨度,首次打开页面统计卡、列表、进度条全部有内容可展示,视觉饱满且能直接验证排序、过滤、聚合逻辑。initSeedData()的空表判断保证重复启动不重复插入,是生产环境初始化演示数据的标准做法。


八、种子数据设计思路复盘

1. 为什么是 15 条?

15 条是「演示数据黄金数量」:

数据量问题15 条的表现
5 条列表空荡,统计卡数字没分量,排序效果看不出来
15 条一屏半、有滚动感,统计数字饱满
50 条首屏刷屏、手工维护成本高、数字失去可读性

15 条 = 4 分类 × 3 优先级 = 12 种组合至少各出现 1 次,再留 3 条给「已完成」,恰好覆盖「未完成在前、高优置顶、完成率 20%」三类验证需求,不浪费一条数据。

2. 状态分布:12 未完成 / 3 已完成

  • 未完成 12 条(80%):撑起未完成视图的滚动列表,让统计卡「未完成 12」有分量。
  • 已完成 3 条(20%):不多不少——多了喧宾夺主,少了进度条3/15 = 20%没有存在感。
  • 完成率 20% 是个「有故事」的数字:一眼能读出五分之一,既不显得全员躺平,也不像演示数据那样 100% 完成,反而真实。

3. 优先级分布:4 高 / 7 中 / 4 低

优先级条数分布作用
高(2)4id 1、5、9、12,分散在周一~周六验证高优置顶,统计卡「高优 4」
中(1)7生活 / 学习 / 日常为主常规任务,构成列表主体
低(0)4id 4、10、15 等验证降序时沉底

高优先级刻意分散到一周各天而不是扎堆——这样无论按优先级排序还是按时间排序,都能看到高优任务穿插其中,避免出现「某一天全是高优」的失真画面。

九、种子数据完整表格(15 条)

#标题状态优先级分类创建时间
1完成季度周报并提交领导未完成工作周一 09:30
2预约明天上午的牙医未完成生活周一 10:05
3阅读《SQLite 权威指南》第 4 章未完成学习周一 20:15
4整理桌面文件,清理垃圾未完成日常周二 08:40
5给项目加异常日志上报未完成工作周二 14:22
6超市采购:牛奶、鸡蛋、水果未完成生活周三 09:00
7复习 ArkTS 状态管理未完成学习周三 19:30
8给爸妈打电话未完成生活周四 12:00
9修复登录页闪退问题未完成工作周四 15:47
10缴纳水电燃气费未完成日常周五 08:20
11整理照片备份到云端未完成日常周五 21:00
12准备周末分享会 PPT未完成工作周六 10:00
13完成健身卡续费已完成生活上周 16:00
14更新应用隐私协议已完成工作上周 11:30
15看完收藏的 3 篇技术文章已完成学习上周 22:10

十、initSeedData 的幂等保护实现

「幂等」指同一操作无论执行多少次,结果都一致。initSeedData()必须幂等——否则每次冷启动都重复插入 15 条,统计卡数字会像滚雪球一样翻倍(15 → 30 → 45 …)。

1. 双重防线实现

staticasyncinitSeedData(context:common.UIAbilityContext):Promise<void>{conststore=awaitthis.getStore(context);// 防线一:空表判断——表里已有任何数据就直接返回constresultSet=awaitstore.querySql(`SELECT COUNT(*) AS c FROM${this.TABLE}`);lettotal=0;if(resultSet.goToNextRow())total=resultSet.getLong(resultSet.getColumnIndex('c'));resultSet.close();if(total>0){console.info(`[TodoDao] 已有数据${total}条,跳过种子注入`);return;}// 防线二:事务包裹——15 条要么全成功,要么全回滚,杜绝「插了一半」constnow=Date.now();constDAY=86400000,H=3600000,M=60000;constseed:Array<relationalStore.ValuesBucket>=[{title:'完成季度周报并提交领导',priority:2,category:'工作',completed:0,created_time:now-6*DAY,completed_time:0,remark:'含图表'},{title:'预约明天上午的牙医',priority:1,category:'生活',completed:0,created_time:now-6*DAY+35*M,completed_time:0,remark:'电话 021-88886666'},{title:'阅读《SQLite 权威指南》第 4 章',priority:1,category:'学习',completed:0,created_time:now-6*DAY+10*H+45*M,completed_time:0,remark:'做笔记'},{title:'整理桌面文件,清理垃圾',priority:0,category:'日常',completed:0,created_time:now-5*DAY+8*H+40*M,completed_time:0,remark:''},{title:'给项目加异常日志上报',priority:2,category:'工作',completed:0,created_time:now-5*DAY+14*H+22*M,completed_time:0,remark:'需求 #128'},{title:'超市采购:牛奶、鸡蛋、水果',priority:1,category:'生活',completed:0,created_time:now-4*DAY+9*H,completed_time:0,remark:'预算 150'},{title:'复习 ArkTS 状态管理',priority:1,category:'学习',completed:0,created_time:now-4*DAY+19*H+30*M,completed_time:0,remark:'两小时'},{title:'给爸妈打电话',priority:1,category:'生活',completed:0,created_time:now-3*DAY+12*H,completed_time:0,remark:'半小时'},{title:'修复登录页闪退问题',priority:2,category:'工作',completed:0,created_time:now-3*DAY+15*H+47*M,completed_time:0,remark:'紧急'},{title:'缴纳水电燃气费',priority:0,category:'日常',completed:0,created_time:now-2*DAY+8*H+20*M,completed_time:0,remark:''},{title:'整理照片备份到云端',priority:1,category:'日常',completed:0,created_time:now-2*DAY+21*H,completed_time:0,remark:'50G'},{title:'准备周末分享会 PPT',priority:2,category:'工作',completed:0,created_time:now-1*DAY+10*H,completed_time:0,remark:'主题:RDB 实践'},{title:'完成健身卡续费',priority:1,category:'生活',completed:1,created_time:now-7*DAY+16*H,completed_time:now-7*DAY+16*H,remark:'已完成'},{title:'更新应用隐私协议',priority:1,category:'工作',completed:1,created_time:now-7*DAY+11*H+30*M,completed_time:now-7*DAY+11*H+30*M,remark:'已完成'},{title:'看完收藏的 3 篇技术文章',priority:0,category:'学习',completed:1,created_time:now-7*DAY+22*H+10*M,completed_time:now-7*DAY+22*H+10*M,remark:'已完成'},];store.beginTransaction();try{for(constitemofseed)awaitstore.insert(this.TABLE,item);store.commit();console.info(`[TodoDao] 种子数据注入完成,共${seed.length}`);}catch(e){store.rollBack();console.error('[TodoDao] 种子注入失败,已整体回滚:',JSON.stringify(e));}}

相比第四章的简化版,这里把时间戳改为Date.now()相对偏移:周一 =now - 6*DAY,再逐条加小时 / 分钟偏移,保证列表时间永远锚定「最近一周」,不会随真实日期漂移。

2. 为什么不用「建表时一次性写入」?

有人会在建表 SQL 里用INSERT ... SELECT ... WHERE NOT EXISTS一步完成判空 + 注入,但 ArkTS 的executeSql对多值 INSERT 支持不友好,而且判空本身只需一次COUNT(*),成本极低。「先查后插 + 事务回滚」是最贴合 RDB 最佳实践、也最容易读懂的方案:判空保护幂等,事务保护原子性,两者各司其职。

十一、注入后页面效果预测与验证方法

1. 效果预测

页面区域预期表现
统计卡未完成12/ 已完成3/ 高优4
全部视图15 张卡片,未完成在前,高优带 ⚡
底部进度条已完成 3 项 ▏▏▏▏ 20%
分类标签工作 5 / 生活 4 / 学习 3 / 日常 3

2. SQL 验证语句

真机或模拟器跑起来后,可用 DevEco Studio 的 RDB 调试面板,或直接在 DAO 里打日志执行以下 SQL 核对:

-- ① 总数与状态分布:期望 15 = 12 未完成 + 3 已完成SELECTcompleted,COUNT(*)FROMtodoGROUPBYcompleted;-- ② 优先级分布:期望 高4 / 中7 / 低4SELECTpriority,COUNT(*)FROMtodoGROUPBYpriority;-- ③ 分类分布:期望 工作5 / 生活4 / 学习3 / 日常3SELECTcategory,COUNT(*)FROMtodoGROUPBYcategory;-- ④ 未完成视图排序:高优在前,同优先级按创建时间倒序SELECTid,title,priority,created_timeFROMtodoWHEREcompleted=0ORDERBYpriorityDESC,created_timeDESC;-- ⑤ 幂等验证:连续冷启动两次后总数仍应为 15,而不是 30SELECTCOUNT(*)FROMtodo;

判断标准:①~③ 的数字与统计卡完全一致;④ 的前三条应为高优任务(id 9、12、5、1 之一);⑤ 能直接证明initSeedData()的幂等保护生效。

十二、FAQ

Q1:为什么不用固定时间戳,而用Date.now()偏移?

固定时间戳写死的是某个具体时刻,半年后打开 App,「昨天」的待办实际是一百多天前创建的,时间语义完全错乱。用now - 偏移生成,数据永远锚定「今天」,演示效果不随真实时间漂移。

Q2:种子数据会不会污染用户的真实数据?

不会。initSeedData()只在表为空(COUNT = 0)时注入,用户一旦新增或删除过任何一条,总数 > 0,后续启动全部跳过。生产环境建议在设置页提供「恢复示例数据」入口:先DELETE FROM todo;再重新调用注入。

Q3:已完成的 3 条,completed_time 为什么等于 created_time?

completed_time 的语义是「何时打勾」。演示数据没有真实勾选动作,把完成时间设为与创建时间相同,保证状态切换逻辑读到的完成时间是合法的非 0 值,不会出现「已完成了但完成时间为 0」的脏数据。

Q4:注入 15 条会不会卡启动?

15 条单条 INSERT 走本地 RDB,实测毫秒级(< 10ms),远低于首帧渲染耗时。真正的开销是每次getStore的建库动作,所以initSeedData()应放在onWindowStageCreate之后异步调用,不阻塞 UI 线程。