当前位置: 首页 > news >正文

HarmonyOS 实战|中式美食排行榜页:综合评分、人气切换与首屏静态视觉兜底

HarmonyOS 实战|中式美食排行榜页:综合评分、人气切换与首屏静态视觉兜底

排行榜页看起来像一个简单列表,但在菜谱类应用里,它承担的是“快速帮用户做选择”的职责。用户不一定知道今天要吃什么,但会自然相信“综合榜”“高分榜”“人气榜”这些入口,因为它们把大量菜品压缩成可扫描的决策线索。

这篇复盘基于已上架 HarmonyOS 应用 **中式美食**,对应工程环境为 HarmonyOS 6.1.0(23)、ArkTS、ArkUI V2、Stage 模型。本文拆解 `RankPage.ets` 如何在同一份 `DishRepository` 数据上完成三种排序口径,并通过首屏静态视觉兜底、透明点击热区、动态榜单列表,让排行榜既有视觉入口,也有真实可交互的数据能力。

这张图对应的是排行榜页的核心闭环:用户先看到一个强视觉的综合榜首屏,点击榜单或切换模式后进入真实列表,列表项继续跳转到菜品详情页。视觉不是摆设,它负责降低进入成本;数据排序才是页面长期可维护的基础。

本章导读

| 章节位置 | 关键文件 | 解决的问题 | 验收入口 | | --- | --- | --- | --- | | 页面入口 | `view/pages/RankPage.ets` | 综合榜、高分榜、人气榜三种模式切换 | 首页“热门排行” | | 数据来源 | `model/repository/DishRepository.ets` | 统一读取内置菜谱和用户菜谱 | `getAll()` | | 排序规则 | `compositeScore()` / `@Computed list` | 把评分与评价量拆成可解释排序 | 切换三个榜单 | | 首屏兜底 | `rank_v3_first_screen_composite` | 静态视觉首屏与真实点击热区结合 | 首次进入排行榜 | | 跳转闭环 | `onOpenDetail(id)` | 榜单项进入菜谱详情页 | 点击任意榜单项 |

排行榜首先是一个决策页面

在中式美食里,排行榜不是单纯展示“数据好看”的页面,而是解决三个用户问题:

| 用户问题 | 对应榜单 | 设计目标 | | --- | --- | --- | | 不知道吃什么,想看综合推荐 | 综合榜 | 平衡评分和热度 | | 想找口碑稳定的菜 | 高分榜 | 直接按 `rating` 排序 | | 想看大家都在看的菜 | 人气榜 | 直接按 `reviewCount` 排序 |

如果只做一个榜单,用户会把“高分但冷门”和“热门但评分一般”的菜混在一起理解。三榜切换能把推荐理由拆开:综合榜负责默认决策,高分榜负责人群口碑,人气榜负责热度反馈。

页面状态要少,但语义要清楚

`RankPage` 的状态集中在三个字段:

```typescript
type RankMode = 'composite' | 'rating' | 'popular';

@Local mode: RankMode = 'composite';
@Local all: Dish[] = [];
@Local showCompositeRank: boolean = true;
```

这三个字段分别回答三个问题:

| 状态 | 含义 | 为什么需要 | | --- | --- | --- | | `mode` | 当前排序口径 | 控制综合/高分/人气切换 | | `all` | 当前参与排行的菜品数据 | 避免每次 build 都重新取仓库 | | `showCompositeRank` | 是否展示首屏静态视觉 | 首次进入时给更强入口感 |

页面出现时只做一次数据读取:

```typescript
aboutToAppear(): void {
this.all = DishRepository.getInstance().getAll();
}
```

这里使用 `getAll()` 而不是直接引用静态数据,是因为前面自定义菜谱已经接入了 `DishRepository`。排行榜页天然能拿到内置菜和用户自建菜,页面层不需要知道数据来自哪里。

综合榜不能只按评分排

菜谱榜单如果只按评分排序,会出现一个问题:一条评价的 5 分菜可能排在几百条评价的 4.8 分菜前面。中式美食的综合榜用了一个轻量公式:

```typescript
private compositeScore(d: Dish): number {
return d.rating * Math.log(d.reviewCount + 10) / Math.log(10);
}
```

这个公式把评分和评价量一起考虑:

| 因子 | 作用 | | --- | --- | | `rating` | 代表口碑质量 | | `reviewCount` | 代表用户反馈规模 | | `+ 10` | 避免评价量太低时分数过小 | | `log10` | 降低超大评价量的碾压效应 |

它不是复杂推荐算法,但很适合本地菜谱应用:可解释、可复现、无需服务端,并且用户能直观理解“评分高且被更多人看过的菜会更靠前”。

三种榜单共享同一个计算出口

`@Computed list` 是排行榜真正的输出口:

```typescript
@Computed get list(): Dish[] {
const arr: Dish[] = this.all.slice();
if (this.mode === 'rating') {
arr.sort((a: Dish, b: Dish): number => b.rating - a.rating);
} else if (this.mode === 'popular') {
arr.sort((a: Dish, b: Dish): number => b.reviewCount - a.reviewCount);
} else {
arr.sort((a: Dish, b: Dish): number => this.compositeScore(b) - this.compositeScore(a));
}
return arr.slice(0, 30);
}
```

这段代码有三个值得保留的工程习惯:

| 写法 | 价值 | | --- | --- | | `this.all.slice()` | 不污染原始数组顺序 | | 分支只切排序规则 | 列表渲染不用关心模式细节 | | `slice(0, 30)` | 控制首屏和滚动性能,不一次性展示过多 |

排序逻辑集中后,UI 层只需要消费 `this.list`。未来要新增“新菜榜”“家常榜”,也可以继续扩展 `RankMode`,不用重写列表卡片。

首屏视觉兜底不是假页面

`RankPage` 里有一个特殊逻辑:

```typescript
private useCompositeRank(): boolean {
return this.showCompositeRank && getImage('rank_v3_first_screen_composite') !== undefined;
}
```

当静态首屏图存在时,页面优先展示 `CompositeRankFirstScreen()`。这不是为了绕过真实 UI,而是为了在首屏给用户更明确的“榜单入口”感。排行榜这种页面,第一眼的信任感很重要。

```typescript
if (this.useCompositeRank()) {
this.CompositeRankFirstScreen();
} else {
Column() {
this.Header();
this.Hero();
Scroll() {
Column() {
this.TabRow();
ForEach(this.list, (d: Dish, i: number) => {
this.RankRow(d, i);
}, (d: Dish, i: number) => i + ':' + d.id);
}
}
}
}
```

这套结构把视觉和数据分成两层:

| 层 | 责任 | | --- | --- | | 静态首屏 | 强化榜单氛围,承接首次进入 | | 透明点击区 | 把首屏图上的榜单项映射到真实详情 | | 动态列表 | 支持模式切换和后续维护 |

透明点击热区解决视觉稿和路由的连接

首屏图不是纯图片。页面在上面铺了透明点击区域:

```typescript
ForEach(this.rankDishIds, (id: string, index: number) => {
Column()
.width('100%')
.height(118)
.backgroundColor('#00000000')
.position({ x: 0, y: 98 + index * 124 })
.onClick((): void => this.onOpenDetail(id));
}, (id: string) => id);
```

这里的 `rankDishIds` 是首屏榜单的菜品 ID 顺序:

```typescript
private rankDishIds: string[] = ['d024', 'd003', 'd001', 'd005', 'd002'];
```

这种做法适合已经设计好首屏视觉的场景。它的关键是:图片上展示的菜品顺序必须和 `rankDishIds` 保持一致,否则用户点击“第一名”却进了另一道菜,会破坏信任。

切换榜单时要退出首屏模式

用户点击顶部模式区域后,会调用:

```typescript
private enterRankMode(mode: RankMode): void {
this.mode = mode;
this.showCompositeRank = false;
}
```

这一步非常重要。首屏只是入口,用户开始切换模式后,页面必须进入真实列表态。否则视觉图仍停留在综合榜,用户会以为“高分榜没有变化”。

| 操作 | 状态变化 | 页面结果 | | --- | --- | --- | | 首次进入 | `showCompositeRank=true` | 展示综合榜首屏 | | 点击高分榜 | `mode=rating`,`showCompositeRank=false` | 展示动态高分列表 | | 点击人气榜 | `mode=popular`,`showCompositeRank=false` | 展示动态人气列表 | | 点击榜单菜品 | 调用 `onOpenDetail(id)` | 进入详情页 |

列表卡片负责扫描效率

动态列表的每一行由 `RankRow` 生成,结构上包含名次、图片、菜名、摘要和跳转提示。

```typescript
@Builder RankRow(d: Dish, idx: number) {
Row() {
Stack() {
Column()
.width(36).height(36)
.borderRadius(18)
.backgroundColor(idx < 3 ? AppColors.brandPrimary : AppColors.bgSoft);
Text((idx + 1).toString())
.fontSize(idx < 3 ? AppFonts.lg : AppFonts.md)
.fontColor(idx < 3 ? AppColors.textOnBrand : AppColors.textSub)
.fontWeight(FontWeight.Bold);
}
.margin({ right: 12 });

Column() {
Text(d.name)
.fontSize(AppFonts.md)
.fontColor(AppColors.textMain)
.fontWeight(FontWeight.Bold)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis });
Text(d.summary)
.fontSize(AppFonts.xs)
.fontColor(AppColors.textSub)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis });
}
.layoutWeight(1);
}
.onClick((): void => { this.onOpenDetail(d.id); });
}
```

排行榜是高密度页面,卡片不能太花。前三名用品牌色强化,后面的名次保持低调;菜名一行、摘要两行,保证用户能连续扫描。

工程验收记录

| 检查项 | 操作方式 | 通过标准 | | --- | --- | --- | | 首屏兜底 | 进入排行榜页 | 有首屏图时展示静态综合榜 | | 榜单项点击 | 点击首屏前五个热区 | 进入对应菜品详情页 | | 模式切换 | 点击综合/高分/人气 | 列表排序口径切换 | | 数据完整性 | 读取 `DishRepository.getAll()` | 内置菜和用户菜都参与排序 | | 空数据状态 | 模拟空列表 | 展示 `EmptyState` | | 列表性能 | 展示排序结果 | 最多渲染前 30 条 | | 文本溢出 | 长菜名、长摘要 | 不撑破卡片 |

常见问题复盘

| 问题 | 原因 | 处理方式 | | --- | --- | --- | | 高分榜看起来和综合榜一样 | 切换模式后仍停留首屏图 | 点击模式时设置 `showCompositeRank=false` | | 首屏点击进错详情 | 图片顺序和 `rankDishIds` 不一致 | 每次换首屏图同步检查 ID 顺序 | | 用户自建菜不参与排行 | 页面直接读静态数据 | 使用 `DishRepository.getAll()` | | 超大评价量碾压评分 | 直接按评论数排序 | 综合榜使用 `rating * log10(reviewCount + 10)` | | 列表滚动卡顿 | 一次渲染全部菜谱 | 只取前 30 条 |

本章小结

- 中式美食排行榜页不是简单列表,而是把“综合推荐、高分口碑、人气热度”拆成三种可解释决策入口。
- 静态首屏图负责第一眼信任感,透明点击热区和动态列表负责真实交互,二者要保持菜品 ID 一致。
- 排序逻辑集中在 `@Computed list`,页面渲染只消费结果,后续扩展新榜单会更稳。

http://www.jsqmd.com/news/1092922/

相关文章:

  • 【LeetCode】第1题 两数之和
  • 分库分表实战
  • Java 调试入门工具
  • 大湾区EMBA特色测评:科学选型理性指南
  • python: Deadline Pattern
  • 从零到一:如何用免费开源Verilog工具链打造专业数字电路
  • StockWidget:桌面悬浮的轻量盯盘小工具
  • 关于vidocoding的开发流程
  • 微信小程序云开发实战:从0到1构建“商业清洁预约”双向匹配后端
  • CBDC安全架构:密码学签名与硬件防护核心技术解析
  • 【单片机毕业设计】基于 STM32 的多模式智能路灯控制系统设计, 基于单片机的光照自适应路灯亮度调节系统设计(014001)
  • Python 文件打开模式总结
  • 为什么顶尖AI团队拒绝“通用提示词”?——稀缺首发:金融/医疗/法律三大垂直领域217条经审计Prompt资产包(限时开放下载)
  • 图片进知识库:先让模型生成文字描述再检索
  • StyleGAN 技术脉络:从风格空间到无混叠生成
  • 《科技代替了我工作》值得被认真放进中文歌单
  • Java 多线程:继承 Thread 与实现 Runnable 两种创建方式完整对比
  • 吾爱出品,必属精品,离线可用!
  • 自动定期备份服务器数据
  • python下载M3U8视频脚本
  • 【单片机毕业设计】基于 STM32 的超重声光报警电子秤设计与实现,基于 STM32 的阈值式重量监测报警系统设计(013701)
  • AI截图工具免费下载,基于DeepSeek的OCR截图软件支持Mac和Win
  • 三列布局三大方案对比总结
  • TI TPIC7710EVM评估模块:汽车EPB系统ASIC驱动与电机控制实战解析
  • Burp Suite实战:验证码场景下的自动化渗透测试与绕过技术
  • 专业iOS激活锁绕过工具applera1n:5分钟恢复iPhone 6s-X使用权限
  • 单板测试正常,整机运行异常:6 类系统排查清单
  • 关于我的第十次web作业
  • YOLO轻量化与部署优化- 第75篇:ONNX导出与优化:从PyTorch到ONNX部署
  • 传统潮流款库存一定会亏损,编程潮流款二手转售,改款二次销售收益模型,降低滞销亏损。