六十条商品铺满三页:鸿蒙分页列表种子数据与加载效果

实例:商品分页列表(Product)|技术:循环生成 60 条商品、分页效果验证
一、种子数据的设计目标
实例 8 的种子数据要验证分页,设计目标非常明确:数据量必须足够分页。pageSize=10,60 条 = 6 页——首页加载 10 条,触底加载 20、30、40、50、60,最后显示「已全部加载」。
具体目标:
- 60 条商品:6 页 × 10 条,足够演示多次触底加载;
- 6 大分类:数码/服饰/食品/家居/美妆/图书各 10 条——分类筛选条有 6 个分类可选;
- 销量梯度:50~500 不等——按销量排序时顺序有区分度,验证 ORDER BY sales DESC;
- 价格梯度:9.9 起,每类递增——划线价/现价对比有层次;
- 标签覆盖:热卖/新品/清仓/空标签交替——标签徽标的三种状态都能展示。
二、循环生成:60 条数据的手工与自动之争
60 条商品如果逐条手写 seed 数组,代码会膨胀到 200 行。本实例用循环生成——6 类 × 10 件,名称、价格、销量全部按公式生成:
const categories: string[] = ['数码', '服饰', '食品', '家居', '美妆', '图书'];
const images: string[] = ['📱', '👟', '🍫', '🛋️', '💄', '📖', '🎧', '🧥', '🍪', '🕯️'];
const tags: string[] = ['热卖', '新品', '清仓', '', '', ''];
const names: string[] = [ /* 60 个商品名,见下表 */ ];
const now = Date.now();
for (let i = 0; i < 60; i++) {
const cat = categories[Math.floor(i / 10)]; // 每 10 条一个分类
const price = 9.9 + (i % 10) * 12.5; // 每类 10 档价格
const originalPrice = price * 1.4; // 划线价 = 现价 × 1.4
const sales = 50 + ((i * 37) % 450); // 销量 50~499
const values: relationalStore.ValuesBucket = {
name: names[i],
price: price,
original_price: Math.round(originalPrice * 10) / 10,
sales: sales,
image: images[i % images.length],
tag: tags[i % tags.length],
category: cat,
created_time: now - i * 3600000,
};
await store.insert(ProductDao.TABLE, values);
}
生成公式拆解:
| 字段 | 公式 | 效果 |
|---|---|---|
| 分类 | categories[i / 10] |
每 10 条轮换一个分类 |
| 现价 | 9.9 + (i%10) * 12.5 |
每类内 10 档价格(9.9~122.4) |
| 划线价 | price * 1.4 四舍五入 |
约 4 折折扣感 |
| 销量 | 50 + (i*37 % 450) |
伪随机 50~499,有区分度 |
| 图片 | images[i % 10] |
10 个 emoji 循环 |
| 标签 | tags[i % 6] |
热卖/新品/清仓/空 循环 |
为什么 (i * 37) % 450 而不是 i * 7? 37 是与 450 互质的数,(i*37)%450 在 i=0~59 时产生近似均匀分布的伪随机销量(不会出现明显的线性递增)。i * 7 会线性递增,销量排序变成「按插入顺序」无区分度。伪随机公式让数据看起来自然——这是种子生成的小技巧。
四舍五入:Math.round(originalPrice * 10) / 10 保一位小数(如 13.86 → 13.9),避免浮点长尾(13.860000001)。
三、60 个商品名清单
为了「有故事」的商品名,60 条名称按分类设计(节选):
| 分类 | 商品名(每类 10 个) |
|---|---|
| 数码 | 旗舰手机、无线耳机、智能手表、蓝牙音箱、机械键盘、电竞鼠标、显示器、充电宝、平板电脑、相机 |
| 服饰 | 运动鞋、卫衣、牛仔裤、羽绒服、帆布包、棒球帽、袜子三双、围巾、休闲裤、T恤 |
| 食品 | 坚果礼盒、进口巧克力、曲奇饼干、速溶咖啡、茶叶礼盒、蜂蜜、辣条、薯片、酸奶、水果干 |
| 家居 | 懒人沙发、落地灯、香薰蜡烛、收纳盒、地毯、窗帘、靠垫、桌布、花瓶、挂画 |
| 美妆 | 保湿面霜、口红、眼影盘、洗发水、面膜、香水、防晒霜、粉底液、卸妆水、精华液 |
| 图书 | 编程入门、数据结构、设计模式、营销宝典、历史小说、科幻经典、散文集、人物传记、心理学、经济学 |
每个分类的商品名是「该品类真实存在的商品」,读者滚动瀑布流时能产生「这店真在卖货」的代入感。商品名的真实性直接影响演示效果。
四、分页效果预测
60 条商品注入后,分页表现:
| 操作 | 已加载 | 页码 | 底部状态 |
|---|---|---|---|
| 进入页面 | 10 | 0 | 上滑加载更多 |
| 触底一次 | 20 | 1 | 上滑加载更多 |
| 触底两次 | 30 | 2 | 上滑加载更多 |
| … | … | … | … |
| 触底五次 | 60 | 5 | — 已全部加载 — |
**「已加载 60 / 共 60 件」**时 finished=true,不再触发加载。整个过程 6 次网络交互(数据库查询),每次 10 条——这就是懒加载的完整生命周期。
分类筛选预测:切到「数码」→ 加载数码的 10 条(正好一页,第二页不足一页 → 直接 finished)→ 标题「共 60 件」不变(total 是全表),列表 10 条。单分类不足 10 条时,一次加载即完成——验证「不足一页即完成」的终止判据。
五、验证方法
- hilog 日志:搜
ProductDao,看到「已填充 60 条商品种子数据」; - 页面显示:首屏 10 条双列商品,底部「上滑加载更多」;滚动触底自动追加;
- 数据库直查:
SELECT COUNT(*) FROM goods; -- 60 SELECT category, COUNT(*) FROM goods GROUP BY category; -- 每类 10 SELECT name, sales FROM goods ORDER BY sales DESC LIMIT 3; -- 销量前三 - 分页验证:观察「已加载」数字从 10 → 20 → 30 递增,最终 60 后显示「已全部加载」。
六、FAQ
Q1:为什么 60 条而不是 600 条?
A:60 条 = 6 页,足够演示「多次触底加载」且每次加载肉眼可见。600 条要触底 60 次,演示冗长;且 600 条循环生成对页面初次渲染有压力。种子量 = 演示场景所需的最小充分量。
Q2:销量「伪随机」会不会有重复值?
A:会,50~499 区间 60 个数必然有重复。重复销量不影响排序正确性(相同销量按插入顺序稳定),也不影响分页。真正的随机分布反而让数据更真实。
Q3:价格为什么从 9.9 开始递增?
A:9.9 是电商「心理定价」的经典锚点(9.9、19.9、29.9…),递增公式 9.9 + (i%10)*12.5 生成 9.9~122.4 的十档价格,覆盖「低价引流款」到「中端主力款」。价格梯度让瀑布流的价格显示有层次。
Q4:可以手动添加商品吗?
A:数据层有 insert 方法,但页面未提供「添加商品」UI(只读列表)。读者可自行扩展:悬浮按钮 + 表单 + insert + refresh。新增商品会出现在按销量排序的对应位置。
Q5:60 条数据注入需要多久?
A:毫秒级(60 次 insert)。如果未来扩展到几千条,应包事务批量提交(6-3 文章讲过),性能提升一个数量级。
Q6:为什么 created_time 用 now - i * 3600000?
A:每条间隔 1 小时的上架时间——虽然本实例未按时间排序,但保留时间戳字段让「按新品排序」成为可扩展能力。种子数据的时间字段为后续功能留余地(与 8-1 文章的设计呼应)。
七、文章小结
本篇文章展示了实例 8 的种子数据:循环生成的 60 条商品(6 类 × 10 件)+ 伪随机销量 + 价格梯度 + 标签覆盖。核心是「循环生成代替手工数组」——用分类轮换、销量伪随机、价格公式把 60 条数据压缩到十几行代码,同时保证数据自然不机械。60 条正好 6 页,分页与懒加载在真实数据下得到完整验证。
下一篇(8-5)是实例 8 的收官文章,展示 ProductPage 全量代码与运行效果。
八、60 条种子数据全览与批量注入实录
8.1 种子数据示例(前 18 条)
公式生成了 60 条数据,但「公式正确 ≠ 数据合理」——把前 18 条摊开看(数码 10 条 + 服饰 8 条,其余 42 条由同一公式依分类轮换生成):
| # | 名称 | 分类 | 现价(元) | 划线价(元) | 销量 | 库存 |
|---|---|---|---|---|---|---|
| 1 | 旗舰手机 | 数码 | 9.9 | 13.9 | 50 | 100 |
| 2 | 无线耳机 | 数码 | 22.4 | 31.4 | 87 | 117 |
| 3 | 智能手表 | 数码 | 34.9 | 48.9 | 124 | 134 |
| 4 | 蓝牙音箱 | 数码 | 47.4 | 66.4 | 161 | 151 |
| 5 | 机械键盘 | 数码 | 59.9 | 83.9 | 198 | 168 |
| 6 | 电竞鼠标 | 数码 | 72.4 | 101.4 | 235 | 185 |
| 7 | 显示器 | 数码 | 84.9 | 118.9 | 272 | 202 |
| 8 | 充电宝 | 数码 | 97.4 | 136.4 | 309 | 219 |
| 9 | 平板电脑 | 数码 | 109.9 | 153.9 | 346 | 236 |
| 10 | 相机 | 数码 | 122.4 | 171.4 | 383 | 253 |
| 11 | 运动鞋 | 服饰 | 9.9 | 13.9 | 420 | 270 |
| 12 | 卫衣 | 服饰 | 22.4 | 31.4 | 457 | 287 |
| 13 | 牛仔裤 | 服饰 | 34.9 | 48.9 | 494 | 304 |
| 14 | 羽绒服 | 服饰 | 47.4 | 66.4 | 81 | 321 |
| 15 | 帆布包 | 服饰 | 59.9 | 83.9 | 118 | 338 |
| 16 | 棒球帽 | 服饰 | 72.4 | 101.4 | 155 | 355 |
| 17 | 袜子三双 | 服饰 | 84.9 | 118.9 | 192 | 372 |
| 18 | 围巾 | 服饰 | 97.4 | 136.4 | 229 | 389 |
表内观察:
- 价格循环:1~10 号是数码的 10 档价格,11 号运动鞋回到 9.9 起——价格按
i%10循环、与分类解耦,每个分类都有「引流款 + 主力款」的价格层次; - 销量跳变:13 号牛仔裤 494 全表最高、14 号羽绒服 81 骤降——
(i*37)%450伪随机让相邻行差异大,按销量排序的瀑布流才有「意外感」; - 划线价收尾:所有划线价都以 .4/.9 结尾(13.9、31.4、48.9…)——
price*1.4四舍五入后的零售习惯价,比整数价更「电商」; - 其余 42 条:服饰后 2 条 + 食品/家居/美妆/图书各 10 条,名称见第三章分类清单——公式保证结构,名称保证真实。
8.2 种子设计复盘:四组梯度是否达标
60 条数据设计完,回看四组梯度:
| 设计维度 | 期望 | 实际 | 验证方式 |
|---|---|---|---|
| 销量梯度 | 50~500 有区分度 | 50~494 全覆盖 | ORDER BY sales DESC 前三名各不相同 |
| 价格区间 | 9.9 起逐档递增 | 9.9~122.4 十档 | 瀑布流卡片价格高低错落 |
| 库存梯度 | 模拟真实库存 | 100~899 伪随机 | 详情页「仅剩 X 件」有数据可显示 |
| 分页页数 | 3 页到底 | 60 ÷ 20 = 3 页 | 触底 2 次后显示「已全部加载」 |
销量梯度是分页排序的命根子:ORDER BY sales DESC 依赖销量有区分度——如果 60 条销量全相同,分页边界就退化为「不可预测的插入序」,触底加载的页码验证失去意义。伪随机公式保证了这一点;同时 60 条恰好能被 20 整除,pageSize=20 时 3 页走完,页码验证特别清爽。
8.3 initSeedData 批量插入:循环 + 事务
第二节的种子代码为了突出生成公式用了「裸循环 insert」;工程上应包事务——60 次写操作合并为 1 次提交:
static async initSeedData(context: common.Context): Promise<void> {
const store = await ProductDao.getStore(context);
const categories: string[] = ['数码', '服饰', '食品', '家居', '美妆', '图书'];
const images: string[] = ['📱', '👟', '🍫', '🛋️', '💄', '📖', '🎧', '🧥', '🍪', '🕯️'];
const tags: string[] = ['热卖', '新品', '清仓', '', '', ''];
const names: string[] = [ /* 60 个商品名,见第三章清单 */ ];
const now = Date.now();
try {
await store.beginTransaction(); // 1. 开启事务
for (let i = 0; i < 60; i++) {
const price = 9.9 + (i % 10) * 12.5;
const values: relationalStore.ValuesBucket = {
name: names[i],
price: price,
original_price: Math.round(price * 1.4 * 10) / 10,
sales: 50 + ((i * 37) % 450),
stock: 100 + ((i * 17) % 800), // 库存伪随机 100~899
image: images[i % images.length],
tag: tags[i % tags.length],
category: categories[Math.floor(i / 10)],
created_time: now - i * 3600000,
};
await store.insert(ProductDao.TABLE, values); // 2. 循环插入 60 条
}
await store.commit(); // 3. 统一提交
hilog.info(0x0000, 'ProductDao', '已填充 60 条商品种子数据');
} catch (e) {
await store.rollBack(); // 4. 异常回滚
hilog.error(0x0000, 'ProductDao', '种子注入失败: %{public}s', JSON.stringify(e));
}
}
事务的三个收益:
| 收益 | 说明 |
|---|---|
| 原子性 | 60 条要么全进、要么全不进——第 35 条失败不会留下「半批数据」 |
| 性能 | 60 次独立提交 ≈ 60 次磁盘刷写;合并为 1 次提交,量级越大差距越大 |
| 可恢复 | catch 里 rollBack 兜底 + 错误日志,失败原因可查 |
注意:beginTransaction 必须与 commit/rollBack 成对出现,任何路径都要收尾——事务编码的纪律。种子数据是一次性写入,用 try-catch 包住最稳妥。
8.4 注入后的分页效果预测:每页 20 条,3 页到底
60 条数据注入后,把 pageSize 从 10 调到 20,分页变为「3 页到底」——比 6 页更紧凑:
| 操作 | 已加载 | 页码 | 底部状态 |
|---|---|---|---|
| 进入页面 | 20 | 0(OFFSET 0) | 上滑加载更多 |
| 触底一次 | 40 | 1(OFFSET 20) | 上滑加载更多 |
| 触底两次 | 60 | 2(OFFSET 40) | — 已全部加载 — |
预测推演:COUNT(*) = 60,第 3 次查询 LIMIT 20 OFFSET 40 返回 20 条,加载后「已加载 60 == 总数 60」→ finished = true——恰好 3 次数据库查询走完全程,比 pageSize=10 的 6 次交互少一半。若某页不足 20 条(如分类筛选),直接置 finished——「不足一页即完成」的判据在两种 pageSize 下都成立。
pageSize 权衡:10 条/页 → 加载动画频繁、演示效果直观;20 条/页 → 交互少、加载快,真实 App 更常用。pageSize 是体验参数而非固定值——种子 60 条在 6 页和 3 页两种配置下都能完整验证分页。
8.5 验证 SQL:直查种子结果
页面跑起来前,先用 SQL 验证种子「按设计」落库:
SELECT COUNT(*) FROM goods; -- 60:总量正确
SELECT category, COUNT(*) FROM goods GROUP BY category; -- 6 类 × 10:分类均衡
SELECT MIN(price), MAX(price) FROM goods; -- 9.9 / 122.4:价格区间
SELECT MIN(sales), MAX(sales) FROM goods; -- 50 / 494:销量梯度
SELECT MIN(stock), MAX(stock) FROM goods; -- 100 / 899:库存梯度
SELECT name, sales FROM goods ORDER BY sales DESC LIMIT 3; -- 前三名各不相同
SELECT COUNT(*) FROM goods WHERE tag = '热卖'; -- 10:标签覆盖
验证要点:总量 60、分类 6×10、价格/销量/库存区间在预期内、排序前三不重复——全部通过,种子才算合格。
8.6 FAQ
Q1:14 号羽绒服销量 81,比 13 号牛仔裤 494 差这么多,是 bug 吗?
A:不是 bug,是伪随机想要的效果——(i*37)%450 在 i=13 时余数骤降到 31。真实电商的销量就是忽高忽低,相邻商品销量差异大,按销量排序的瀑布流才不呆板。
Q2:库存字段之前的文章没提,种子为什么要带?
A:8-1 的表结构里 stock 字段已建(默认 0),种子里一并填伪随机值,是为了让「仅剩 X 件」「库存紧张」这类 UI 有数据可显示——字段建了不用等于没建,种子把每个字段都激活。
Q3:事务版和裸循环版性能差多少?
A:60 条差距不明显(都是毫秒级),但放大到 6000 条:裸循环 = 6000 次独立提交(每次都有磁盘刷写开销),事务版 = 1 次提交,差一个数量级。种子代码养成「循环即事务」的习惯,数据量增长不用返工。
Q4:pageSize 用 10 还是 20?
A:演示用 10(多几次触底动画,效果直观),真实产品用 20~30(交互少、加载快)。两种配置种子数据都不用改——60 ÷ 10 = 6 页、60 ÷ 20 = 3 页,分页逻辑天然适配。
Q5:GROUP BY 每类正好 10 条,是巧合吗?
A:不是巧合——生成公式 categories[i/10] 保证每 10 条一个分类,这是设计保证而非数据巧合。种子数据的一大价值就是「可预期的验证结果」:SQL 查出的数字应与设计值完全一致。
更多推荐




所有评论(0)