在这里插入图片描述

📖 引言

排行榜是知识问答学习应用中的重要社交功能模块,它通过展示用户的学习成绩和排名,激发用户的竞争意识和学习动力。一个设计良好的排行榜页面应该能够清晰地展示排名信息、提供多种筛选维度、同时保护用户隐私。

本文将详细讲解排行榜页面的设计与实现,包括排名展示、筛选功能、数据统计等核心功能。通过本文,你将掌握:

  • 如何设计排行榜页面的整体布局
  • 如何实现排名展示和用户信息卡片
  • 如何处理筛选功能和时间段切换
  • 如何优化大量数据的渲染性能

🎯 学习目标

完成本文后,你将能够:

  • ✅ 理解排行榜页面的核心功能和布局设计
  • ✅ 实现排名列表和用户信息展示
  • ✅ 处理筛选功能和排序逻辑
  • ✅ 优化长列表渲染性能

💡 需求分析

功能模块设计

模块 功能描述 技术要点
排名头部 显示当前用户排名 位置标识、数据绑定
筛选栏 切换不同维度(积分/正确率/连胜) 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 每日挑战页面:限时挑战任务设计

🔗 相关链接


💡 提示:排行榜是激励用户学习的重要功能,建议结合用户成长系统一起设计,合理设置排名规则和奖励机制。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐