HarmonyOS应用《趣答》开发第16篇:Ranking排行榜页面设计——打造激励竞争的学习氛围
·

📖 引言
排行榜是知识问答学习应用中的重要社交功能模块,它通过展示用户的学习成绩和排名,激发用户的竞争意识和学习动力。一个设计良好的排行榜页面应该能够清晰地展示排名信息、提供多种筛选维度、同时保护用户隐私。
本文将详细讲解排行榜页面的设计与实现,包括排名展示、筛选功能、数据统计等核心功能。通过本文,你将掌握:
- 如何设计排行榜页面的整体布局
- 如何实现排名展示和用户信息卡片
- 如何处理筛选功能和时间段切换
- 如何优化大量数据的渲染性能
🎯 学习目标
完成本文后,你将能够:
- ✅ 理解排行榜页面的核心功能和布局设计
- ✅ 实现排名列表和用户信息展示
- ✅ 处理筛选功能和排序逻辑
- ✅ 优化长列表渲染性能
💡 需求分析
功能模块设计
| 模块 | 功能描述 | 技术要点 |
|---|---|---|
| 排名头部 | 显示当前用户排名 | 位置标识、数据绑定 |
| 筛选栏 | 切换不同维度(积分/正确率/连胜) | Tab切换、状态管理 |
| 排名列表 | 展示所有用户的排名 | 虚拟列表、性能优化 |
| 用户卡片 | 显示用户头像、昵称、分数 | 组件封装、数据展示 |
| 时间选择 | 切换不同时间段(周/月/总) | 时间筛选、数据聚合 |
🛠️ 核心实现
步骤1:排行榜页面整体布局
功能说明
设计排行榜页面的整体布局结构,包括顶部标题、筛选栏、当前用户排名卡片和排名列表。
完整代码
// pages/Ranking/Ranking.ets
import router from '@ohos.router';
import promptAction from '@ohos.promptAction';
// 排名用户接口
interface RankUser {
rank: number;
userId: string;
nickname: string;
avatar: string;
score: number;
accuracy: number;
level: number;
isCurrentUser: boolean;
}
// 筛选类型枚举
enum RankingFilter {
SCORE = 'score',
ACCURACY = 'accuracy',
STREAK = 'streak'
}
// 时间范围枚举
enum TimeRange {
WEEK = 'week',
MONTH = 'month',
ALL = 'all'
}
@Component
export struct RankingPage {
@State currentFilter: RankingFilter = RankingFilter.SCORE;
@State currentTimeRange: TimeRange = TimeRange.ALL;
@State rankingList: RankUser[] = [];
@State currentUserRank: number = 0;
@State isLoading: boolean = true;
@State refreshEnabled: boolean = true;
aboutToAppear() {
this.loadRankingData();
}
build() {
Column() {
// 顶部导航栏
this.NavigationBar()
// 筛选栏
this.FilterBar()
// 当前用户排名卡片
this.CurrentUserCard()
// 排名列表
this.RankingList()
}
.width('100%')
.height('100%')
.backgroundColor('#f5f5f5')
}
// 顶部导航栏
@Builder
NavigationBar() {
Row() {
Image('https://example.com/icons/back.png')
.width(24)
.height(24)
.fillColor('#333')
.onClick(() => {
router.back();
})
Text('排行榜')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333')
Blank()
// 刷新按钮
Image('https://example.com/icons/refresh.png')
.width(24)
.height(24)
.fillColor('#333')
.enabled(this.refreshEnabled)
.opacity(this.refreshEnabled ? 1 : 0.5)
.onClick(() => {
this.refreshRanking();
})
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor('#fff')
}
// 筛选栏
@Builder
FilterBar() {
Column() {
// 维度筛选
Row() {
Text('筛选维度')
.fontSize(12)
.fontColor('#999')
.margin({ right: 12 })
Row({ space: 8 }) {
ForEach([
{ key: RankingFilter.SCORE, label: '积分' },
{ key: RankingFilter.ACCURACY, label: '正确率' },
{ key: RankingFilter.STREAK, label: '连胜' }
], (item: { key: RankingFilter, label: string }) => {
Text(item.label)
.fontSize(14)
.fontColor(this.currentFilter === item.key ? '#fff' : '#666')
.backgroundColor(this.currentFilter === item.key ? '#4CAF50' : '#f0f0f0')
.padding({ left: 16, right: 16, top: 6, bottom: 6 })
.borderRadius(16)
.onClick(() => {
this.currentFilter = item.key;
this.loadRankingData();
})
})
}
}
// 时间范围筛选
Row() {
Text('时间范围')
.fontSize(12)
.fontColor('#999')
.margin({ right: 12 })
Row({ space: 8 }) {
ForEach([
{ key: TimeRange.WEEK, label: '本周' },
{ key: TimeRange.MONTH, label: '本月' },
{ key: TimeRange.ALL, label: '总榜' }
], (item: { key: TimeRange, label: string }) => {
Text(item.label)
.fontSize(12)
.fontColor(this.currentTimeRange === item.key ? '#4CAF50' : '#999')
.borderWidth(this.currentTimeRange === item.key ? 1 : 0)
.borderColor('#4CAF50')
.padding({ left: 12, right: 12, top: 4, bottom: 4 })
.borderRadius(12)
.onClick(() => {
this.currentTimeRange = item.key;
this.loadRankingData();
})
})
}
}
}
.width('100%')
.padding(16)
.backgroundColor('#fff')
}
// 当前用户排名卡片
@Builder
CurrentUserCard() {
if (this.currentUserRank === 0) return;
Row() {
// 排名
Text(`#${this.currentUserRank}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#4CAF50')
Column({ space: 4 }) {
Text('我的排名')
.fontSize(12)
.fontColor('#999')
Text(this.getCurrentUserScore())
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#333')
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 16 })
Blank()
Text(`超过 ${this.getPercentile()}% 的用户`)
.fontSize(12)
.fontColor('#FF9800')
}
.width('100%')
.padding(16)
.backgroundColor('#E8F5E9')
.borderRadius(12)
.margin({ left: 16, right: 16, top: 12 })
}
// 排名列表
@Builder
RankingList() {
if (this.isLoading) {
Column() {
LoadingProgress()
.width(32)
.height(32)
.color('#4CAF50')
Text('加载中...')
.fontSize(14)
.fontColor('#999')
.margin({ top: 12 })
}
.width('100%')
.height('60%')
.justifyContent(FlexAlign.Center)
} else {
List() {
ForEach(this.rankingList, (user: RankUser) => {
ListItem() {
this.RankItem(user)
}
})
}
.width('100%')
.height('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.divider({ strokeWidth: 1, color: '#f0f0f0' })
}
}
// 排名项组件
@Builder
RankItem(user: RankUser) {
Row({ space: 12 }) {
// 排名标识
this.RankBadge(user.rank)
// 用户头像
Image(user.avatar)
.width(48)
.height(48)
.borderRadius(24)
.borderWidth(user.isCurrentUser ? 2 : 0)
.borderColor('#4CAF50')
// 用户信息
Column({ space: 4 }) {
Row({ space: 8 }) {
Text(user.nickname)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor(user.isCurrentUser ? '#4CAF50' : '#333')
Text(`Lv.${user.level}`)
.fontSize(12)
.fontColor('#999')
.backgroundColor('#f0f0f0')
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.borderRadius(4)
}
Text(this.getFilterLabel() + `: ${this.getUserScore(user)}`)
.fontSize(14)
.fontColor('#666')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 分数显示
Column({ space: 2 }) {
Text(this.getUserScore(user))
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor(this.getScoreColor(user.rank))
Text(this.getFilterUnit())
.fontSize(10)
.fontColor('#999')
}
}
.width('100%')
.padding({ top: 12, bottom: 12 })
.backgroundColor(user.isCurrentUser ? '#E8F5E9' : '#fff')
.borderRadius(12)
.margin({ bottom: 8 })
}
// 排名徽章
@Builder
RankBadge(rank: number) {
if (rank <= 3) {
// 前三名特殊样式
Stack({ alignContent: Alignment.Center }) {
Circle()
.width(36)
.height(36)
.fill(this.getRankColor(rank))
Text(rank.toString())
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#fff')
}
} else {
// 普通排名
Text(`#${rank}`)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#666')
.width(36)
.textAlign(TextAlign.Center)
}
}
// 加载排名数据
private loadRankingData(): void {
this.isLoading = true;
this.refreshEnabled = false;
// 模拟数据加载
setTimeout(() => {
this.rankingList = this.generateMockData();
this.currentUserRank = Math.floor(Math.random() * 50) + 1;
this.isLoading = false;
this.refreshEnabled = true;
}, 500);
}
// 生成模拟数据
private generateMockData(): RankUser[] {
const users: RankUser[] = [];
const nicknames = ['小明', '小红', '学霸君', '进步者', '挑战者', '学习达人', '知识探索者', '智慧小达人'];
for (let i = 1; i <= 50; i++) {
users.push({
rank: i,
userId: `user_${i}`,
nickname: nicknames[i % nicknames.length] + i,
avatar: `https://example.com/avatar/avatar_${i % 10}.png`,
score: 10000 - i * 100 + Math.floor(Math.random() * 50),
accuracy: 95 - i * 0.5 + Math.random() * 5,
level: Math.min(20, Math.floor(i / 3) + 1),
isCurrentUser: i === this.currentUserRank
});
}
return users;
}
// 刷新排名
private refreshRanking(): void {
if (!this.refreshEnabled) return;
this.loadRankingData();
}
// 获取当前用户分数
private getCurrentUserScore(): string {
const currentUser = this.rankingList.find(u => u.isCurrentUser);
if (!currentUser) return '0';
switch (this.currentFilter) {
case RankingFilter.SCORE:
return currentUser.score.toString();
case RankingFilter.ACCURACY:
return `${currentUser.accuracy.toFixed(1)}%`;
case RankingFilter.STREAK:
return `${Math.floor(currentUser.score / 100)}连胜`;
default:
return currentUser.score.toString();
}
}
// 获取用户分数
private getUserScore(user: RankUser): string {
switch (this.currentFilter) {
case RankingFilter.SCORE:
return user.score.toString();
case RankingFilter.ACCURACY:
return `${user.accuracy.toFixed(1)}%`;
case RankingFilter.STREAK:
return `${Math.floor(user.score / 100)}连胜`;
default:
return user.score.toString();
}
}
// 获取筛选标签
private getFilterLabel(): string {
switch (this.currentFilter) {
case RankingFilter.SCORE:
return '总积分';
case RankingFilter.ACCURACY:
return '正确率';
case RankingFilter.STREAK:
return '连胜';
default:
return '积分';
}
}
// 获取筛选单位
private getFilterUnit(): string {
switch (this.currentFilter) {
case RankingFilter.SCORE:
return '分';
case RankingFilter.ACCURACY:
return '%';
case RankingFilter.STREAK:
return '连胜';
default:
return '分';
}
}
// 获取排名颜色
private getRankColor(rank: number): string {
switch (rank) {
case 1: return '#FFD700'; // 金色
case 2: return '#C0C0C0'; // 银色
case 3: return '#CD7F32'; // 铜色
default: return '#f0f0f0';
}
}
// 获取分数颜色
private getScoreColor(rank: number): string {
if (rank <= 3) {
return '#FF9800';
} else if (rank <= 10) {
return '#4CAF50';
} else {
return '#333';
}
}
// 获取百分位
private getPercentile(): string {
if (this.currentUserRank === 0) return '0';
const percentile = ((50 - this.currentUserRank) / 50 * 100).toFixed(0);
return percentile;
}
}
代码解析
1. 页面布局结构
┌─────────────────────────┐
│ ← 排行榜 🔄 │ 顶部导航栏
├─────────────────────────┤
│ 筛选维度 [积分][正确率][连胜] │ 筛选栏
│ 时间范围 本周 本月 总榜 │
├─────────────────────────┤
│ #12 我的排名 │ 当前用户排名
│ 超过 76% 用户 │
├─────────────────────────┤
│ #1 🥇 小明 Lv.20 │ 排名列表
│ 总积分: 9800 分 │
├─────────────────────────┤
│ #2 🥈 小红 Lv.19 │
│ 总积分: 9700 分 │
├─────────────────────────┤
│ #3 🥉 学霸君 Lv.18 │
│ 总积分: 9600 分 │
└─────────────────────────┘
2. 核心功能说明
- 筛选栏:支持按积分、正确率、连胜三个维度筛选,以及本周、本月、总榜三个时间范围
- 当前用户排名卡片:突出显示用户当前排名和超过的用户百分比
- 排名列表:使用虚拟列表优化性能,前三名使用特殊样式
步骤2:排名徽章组件实现
功能说明
实现前三名用户的特殊排名徽章样式(金、银、铜色)以及普通排名数字显示。
完整代码
// 排名徽章组件
@Builder
RankBadge(rank: number) {
if (rank <= 3) {
// 前三名特殊样式
Stack({ alignContent: Alignment.Center }) {
Circle()
.width(36)
.height(36)
.fill(this.getRankColor(rank))
Text(rank.toString())
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#fff')
}
} else {
// 普通排名
Text(`#${rank}`)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#666')
.width(36)
.textAlign(TextAlign.Center)
}
}
// 获取排名颜色
private getRankColor(rank: number): string {
switch (rank) {
case 1: return '#FFD700'; // 金色
case 2: return '#C0C0C0'; // 银色
case 3: return '#CD7F32'; // 铜色
default: return '#f0f0f0';
}
}
代码解析
- 前三名:使用圆形背景和 emoji 显示,金色(#FFD700)、银色(#C0C0C0)、铜色(#CD7F32)
- 普通排名:使用
#数字格式,灰色文字显示
⚠️ 常见问题与解决方案
问题1:长列表渲染性能差
现象:
排行榜数据量大时,页面滚动出现卡顿。
错误代码:
// ❌ 错误:直接渲染所有数据
List() {
ForEach(this.rankingList, (user: RankUser) => {
ListItem() {
this.RankItem(user)
}
})
}
正确代码:
// ✅ 正确:使用虚拟列表
List() {
LazyForEach(this.rankingDataSource, (user: RankUser) => {
ListItem() {
this.RankItem(user)
}
})
}
.width('100%')
.height('100%')
.cacheCount(10) // 缓存数量
规则/建议:
- 使用
LazyForEach替代ForEach - 设置合理的
cacheCount值 - 避免在列表项中执行复杂计算
问题2:筛选状态未同步更新
现象:
切换筛选维度后,列表数据未立即更新。
错误代码:
// ❌ 错误:切换后未重新加载数据
.onClick(() => {
this.currentFilter = item.key;
// 缺少数据更新逻辑
})
正确代码:
// ✅ 正确:切换后重新加载数据
.onClick(() => {
this.currentFilter = item.key;
this.loadRankingData(); // 重新加载数据
})
规则/建议:
- 切换筛选条件后立即重新加载数据
- 显示加载状态提示用户
- 考虑添加防抖避免频繁请求
问题3:当前用户未高亮显示
现象:
当前用户在列表中未突出显示。
错误代码:
// ❌ 错误:未区分当前用户
Row() {
Text(user.nickname)
.fontColor('#333')
}
.backgroundColor('#fff')
正确代码:
// ✅ 正确:突出显示当前用户
Row() {
Text(user.nickname)
.fontColor(user.isCurrentUser ? '#4CAF50' : '#333')
}
.backgroundColor(user.isCurrentUser ? '#E8F5E9' : '#fff')
.borderWidth(user.isCurrentUser ? 2 : 0)
.borderColor('#4CAF50')
规则/建议:
- 使用特殊背景色区分当前用户
- 添加边框或高亮标识
- 在列表顶部或底部单独显示当前用户排名
问题4:时间范围筛选逻辑错误
现象:
切换时间范围后,显示的数据不正确。
错误代码:
// ❌ 错误:未正确处理时间范围
private loadRankingData(): void {
// 所有时间范围都使用相同的数据
this.rankingList = this.getAllData();
}
正确代码:
// ✅ 正确:根据时间范围加载不同数据
private loadRankingData(): void {
switch (this.currentTimeRange) {
case TimeRange.WEEK:
this.rankingList = this.getWeekData();
break;
case TimeRange.MONTH:
this.rankingList = this.getMonthData();
break;
case TimeRange.ALL:
this.rankingList = this.getAllTimeData();
break;
}
}
规则/建议:
- 根据时间范围请求不同的数据
- 考虑在前端进行数据聚合
- 合理缓存不同时间范围的数据
问题5:刷新按钮重复点击
现象:
用户快速连续点击刷新按钮,导致多次请求。
错误代码:
// ❌ 错误:未禁用刷新按钮
Image('refresh.png')
.onClick(() => {
this.loadRankingData();
})
正确代码:
// ✅ 正确:禁用刷新按钮
Image('refresh.png')
.enabled(this.refreshEnabled) // 控制是否可点击
.opacity(this.refreshEnabled ? 1 : 0.5) // 可视化禁用状态
.onClick(() => {
this.refreshRanking();
})
// 在刷新方法中禁用
private refreshRanking(): void {
if (!this.refreshEnabled) return; // 防止重复点击
this.refreshEnabled = false;
this.loadRankingData();
}
规则/建议:
- 使用状态变量控制按钮可用性
- 提供视觉反馈(透明度、颜色变化)
- 在数据加载完成后重新启用
📝 本章小结
核心知识点
本文详细讲解了排行榜页面的设计与实现,主要包括:
1. 页面布局设计
- 顶部导航栏(返回按钮、标题、刷新按钮)
- 筛选栏(维度筛选、时间范围筛选)
- 当前用户排名卡片
- 排名列表(虚拟列表优化)
2. 核心功能实现
- 多维度筛选(积分、正确率、连胜)
- 多时间范围(本周、本月、总榜)
- 排名徽章(前三名特殊样式)
- 当前用户高亮显示
3. 性能优化
- 虚拟列表使用
LazyForEach - 缓存数量设置
- 刷新按钮状态管理
最佳实践总结
✅ 虚拟列表优化
LazyForEach(this.rankingDataSource, (user: RankUser) => {
ListItem() {
this.RankItem(user)
}
})
.cacheCount(10)
✅ 前三名特殊样式
Stack({ alignContent: Alignment.Center }) {
Circle()
.width(36)
.height(36)
.fill(this.getRankColor(rank)) // 金银铜色
Text(rank.toString())
.fontColor('#fff')
}
✅ 刷新状态管理
Image('refresh.png')
.enabled(this.refreshEnabled)
.opacity(this.refreshEnabled ? 1 : 0.5)
.onClick(() => {
this.refreshRanking();
})
✅ 当前用户高亮
.backgroundColor(user.isCurrentUser ? '#E8F5E9' : '#fff')
.borderWidth(user.isCurrentUser ? 2 : 0)
.borderColor('#4CAF50')
下一步预告
在下一篇文章中,我们将继续完善页面组件详解系列,讲解以下内容:
- 🎨 Statistics 统计页面实现:学习数据的可视化展示
- 📊 WrongQuestions 错题本功能:错题收集与复习管理
- 🎯 DailyChallenge 每日挑战页面:限时挑战任务设计
🔗 相关链接
- 项目源码:Atomgit仓库
- 官方文档:HarmonyOS List开发指南
💡 提示:排行榜是激励用户学习的重要功能,建议结合用户成长系统一起设计,合理设置排名规则和奖励机制。
更多推荐


所有评论(0)