在这里插入图片描述

排行榜是剧本杀组队App中展示热门剧本、优质店铺和活跃玩家的重要功能模块。通过排行榜,用户可以快速发现高评分剧本、热门店铺和资深玩家,为选择剧本和组队提供参考依据。本篇文章将详细讲解如何实现一个功能完善的排行榜页面,包括多维度榜单切换、排名展示和数据可视化。

排行榜页面采用顶部Tab切换不同榜单类型的设计,支持剧本榜、店铺榜和玩家榜三个维度。每个榜单内部又细分为热门榜、评分榜和新晋榜,通过嵌套TabBar实现多层级筛选。排名前三的项目使用金银铜三色徽章突出显示,其他排名使用普通样式。

排行榜页面的基础结构

排行榜功能的实现需要一个主页面来承载所有的排行榜内容。我们使用StatefulWidget来创建RankingPage,这样可以管理多个TabController的状态。StatefulWidget提供了完整的生命周期管理,使得我们可以在initState中初始化TabController,在dispose中释放资源。这种设计模式是Flutter中处理复杂状态的标准做法。

import 'package:flutter/material.dart';
import 'package:get/get.dart';

/// 排行榜页面 - 展示剧本、店铺、玩家的多维度排名
/// 支持热门榜、评分榜、新晋榜三种排序方式
/// 使用嵌套TabBar实现多层级榜单切换
class RankingPage extends StatefulWidget {
  const RankingPage({super.key});

  
  State<RankingPage> createState() => _RankingPageState();
}

这段代码是排行榜页面的核心类定义,首先导入了Flutter核心包和GetX路由管理包,GetX用于后续页面跳转的便捷处理。RankingPage继承自StatefulWidget,因为页面包含多个Tab切换的动态状态,需要通过State类来管理这些状态,这是Flutter中处理有状态组件的标准写法。类注释清晰标注了页面的核心功能,包括多维度排名展示和嵌套TabBar的使用,便于后续代码维护和理解。

RankingPage使用StatefulWidget是因为需要管理多个TabController的状态。页面采用嵌套Tab结构,外层Tab切换榜单类型(剧本/店铺/玩家),内层Tab切换排序方式(热门/评分/新晋)。这种嵌套结构为用户提供了清晰的信息层次,使得用户可以快速找到所需的榜单内容。

TabController的初始化与管理

在实现嵌套Tab结构时,需要为每个Tab层级创建对应的TabController。TabController是Flutter中用于管理Tab切换的核心组件,它负责处理Tab的动画、状态管理和事件监听。通过TickerProviderStateMixin,我们可以为TabController提供必要的动画支持。

class _RankingPageState extends State<RankingPage>
    with TickerProviderStateMixin {
  /// 主Tab控制器 - 控制剧本榜、店铺榜、玩家榜切换
  late TabController _mainTabController;
  
  /// 剧本榜子Tab控制器
  late TabController _scriptTabController;
  
  /// 玩家榜子Tab控制器
  late TabController _playerTabController;

这里定义了RankingPage对应的State类,并混入TickerProviderStateMixin,该Mixin的作用是为TabController提供动画帧回调支持,保证Tab切换时的动画流畅性。接着声明了三个TabController对象,分别对应主Tab(剧本/店铺/玩家)和剧本榜、玩家榜的子Tab,使用late关键字表示延迟初始化,避免在构造函数中直接初始化需要上下文的对象。每个控制器都添加了清晰的注释,明确其负责的Tab切换范围,提升代码可读性。

  /// 店铺榜子Tab控制器
  late TabController _storeTabController;

  /// 主题色 - 紫色
  static const Color _primaryColor = Color(0xFF6B4EFF);

补充声明了店铺榜的子Tab控制器,确保三个子榜单都有对应的控制器管理。同时定义了全局的主题色_primaryColor,使用16进制色值设置为紫色,统一整个排行榜页面的视觉风格,避免多处硬编码色值导致的样式不一致问题,符合Flutter组件开发中样式统一的最佳实践。

排行榜数据的定义

排行榜的数据结构设计是实现功能的基础。我们为不同的排序维度定义了不同的数据列表,包括热门榜、评分榜和新晋榜。每个数据项都包含了展示所需的所有信息,如id、名称、类型、评分、游玩人数和封面图片路径。

  /// 剧本排行榜数据 - 包含热门、评分、新晋三个维度
  final List<Map<String, dynamic>> _hotScripts = [
    {'id': '1', 'name': '年轮', 'type': '情感本', 'rating': 9.5, 'plays': 12580, 'cover': 'assets/scripts/nianlun.jpg'},
    {'id': '2', 'name': '古木吟', 'type': '恐怖本', 'rating': 9.4, 'plays': 10230, 'cover': 'assets/scripts/gumuyin.jpg'},
    {'id': '3', 'name': '云使', 'type': '机制本', 'rating': 9.3, 'plays': 8960, 'cover': 'assets/scripts/yunshi.jpg'},

这段代码定义了剧本热门榜的数据源,采用List<Map<String, dynamic>>的结构,兼顾灵活性和可读性。每个Map包含剧本的核心展示字段:id用于唯一标识、name为剧本名称、type区分剧本类型、rating是评分、plays记录游玩人数、cover指定封面图路径。选择这种数据结构是因为可以快速适配不同维度的榜单展示,同时Map的键值对形式便于后续扩展字段,比如新增剧本时长、难度等信息。

    {'id': '4', 'name': '你好', 'type': '情感本', 'rating': 9.2, 'plays': 7850, 'cover': 'assets/scripts/nihao.jpg'},
    {'id': '5', 'name': '白夜追凶', 'type': '硬核本', 'rating': 9.1, 'plays': 6540, 'cover': 'assets/scripts/baiye.jpg'},
  ];

  /// 店铺排行榜数据
  final List<Map<String, dynamic>> _hotStores = [
    {'id': '1', 'name': '迷雾剧本杀', 'address': '朝阳区三里屯SOHO', 'rating': 4.9, 'reviews': 2580, 'distance': '1.2km'},

继续完善剧本热门榜数据后,定义了店铺排行榜的数据源。店铺数据的字段贴合线下门店的展示需求,除了基础的id、name、rating外,新增address(地址)、reviews(评价数)、distance(距离)字段,这些都是用户选择剧本杀店铺时核心关注的信息。数据类型同样采用List,保持和剧本数据结构的一致性,便于后续统一封装列表渲染逻辑。

    {'id': '2', 'name': '探案馆', 'address': '海淀区中关村大街', 'rating': 4.8, 'reviews': 2130, 'distance': '2.5km'},
    {'id': '3', 'name': '推理社', 'address': '西城区西单大悦城', 'rating': 4.7, 'reviews': 1890, 'distance': '3.1km'},
  ];

  /// 玩家排行榜数据
  final List<Map<String, dynamic>> _topPlayers = [
    {'id': '1', 'name': '推理大神', 'level': 'Lv.10', 'games': 256, 'fans': 12580, 'avatar': 'assets/avatars/1.jpg'},

完成店铺热门榜数据定义后,开始编写玩家排行榜数据源。玩家数据聚焦于用户属性,包含level(等级)、games(游玩场次)、fans(粉丝数)、avatar(头像路径)等字段,符合玩家榜单展示核心诉求。保持和前两个榜单一致的List结构,让后续的列表构建方法可以复用核心逻辑,减少重复代码,提升开发效率。

    {'id': '2', 'name': '剧本杀王者', 'level': 'Lv.9', 'games': 198, 'fans': 9860, 'avatar': 'assets/avatars/2.jpg'},
    {'id': '3', 'name': '沉浸式玩家', 'level': 'Lv.9', 'games': 175, 'fans': 8520, 'avatar': 'assets/avatars/3.jpg'},
  ];

补充玩家排行榜的剩余数据,至此三大核心榜单的静态数据源全部定义完成。所有数据源都使用final修饰,确保数据不可变,符合Flutter中状态管理的最佳实践,避免意外的数据修改导致UI展示异常。静态数据的定义方式适合前期功能开发和调试,后续可无缝替换为接口请求的动态数据。

TabController的初始化与资源管理

在initState方法中初始化所有的TabController。每个TabController都需要指定标签页数量和vsync参数。vsync参数用于提供Ticker,确保动画与屏幕刷新率同步。在dispose方法中释放所有控制器资源是使用TabController时必须要做的清理工作,避免内存泄漏。

  
  void initState() {
    super.initState();
    _mainTabController = TabController(length: 3, vsync: this);
    _scriptTabController = TabController(length: 3, vsync: this);
    _storeTabController = TabController(length: 3, vsync: this);
    _playerTabController = TabController(length: 3, vsync: this);
  }

initState方法是State类的生命周期方法,在组件创建时执行,适合初始化需要上下文或动画支持的对象。这里为四个TabController完成初始化,length参数设置为3,对应每个TabBar的3个标签(如剧本榜的热门/评分/新晋),vsync传入this(因为混入了TickerProviderStateMixin),保证Tab切换动画的性能和流畅性。集中初始化控制器便于后续统一管理,也符合Flutter生命周期管理的规范。

  
  void dispose() {
    _mainTabController.dispose();
    _scriptTabController.dispose();
    _storeTabController.dispose();
    _playerTabController.dispose();
    super.dispose();
  }

dispose方法同样是State类的生命周期方法,在组件销毁时执行,核心作用是释放资源避免内存泄漏。这里依次调用所有TabController的dispose方法,释放动画控制器和相关资源。如果遗漏这一步,会导致控制器占用的内存无法回收,长期使用可能引发内存泄漏问题,这是Flutter开发中必须注意的资源管理细节。

正确的资源管理是编写高质量Flutter应用的基础。

页面主体结构的构建

build方法构建页面主体结构。AppBar使用紫色背景,底部放置主TabBar用于切换剧本榜、店铺榜和玩家榜。TabBarView与TabBar联动,展示对应的榜单内容。

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[100],
      appBar: AppBar(
        backgroundColor: _primaryColor,
        elevation: 0,
        title: const Text('排行榜', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
        centerTitle: true,

build方法是Flutter组件的核心,负责构建UI结构。这里首先返回Scaffold作为页面根布局,设置背景色为浅灰色(Colors.grey[100]),提升页面视觉层次感。AppBar部分使用之前定义的主题色_primaryColor作为背景,取消阴影(elevation: 0),标题设置为“排行榜”并调整样式,centerTitle设为true让标题居中,符合移动端UI设计的常见风格。

        bottom: TabBar(
          controller: _mainTabController,
          indicatorColor: Colors.white,
          indicatorWeight: 3,
          labelColor: Colors.white,
          unselectedLabelColor: Colors.white70,
          tabs: const [Tab(text: '剧本榜'), Tab(text: '店铺榜'), Tab(text: '玩家榜')],
        ),
      ),

在AppBar的bottom属性中添加主TabBar,绑定_mainTabController实现联动。indicatorColor设置为白色,indicatorWeight增加到3像素,让选中标签的下划线更醒目;labelColor和unselectedLabelColor区分选中/未选中标签的颜色,提升交互反馈的清晰度。tabs数组定义了三个主标签,文本简洁明了,符合用户对榜单分类的认知习惯。

      body: TabBarView(
        controller: _mainTabController,
        children: [_buildScriptRanking(), _buildStoreRanking(), _buildPlayerRanking()],
      ),
    );
  }

Scaffold的body部分使用TabBarView与主TabBar联动,同样绑定_mainTabController,确保标签切换时内容同步更新。children数组对应三个标签的内容,分别调用_buildScriptRanking、_buildStoreRanking、_buildPlayerRanking方法构建不同榜单的UI,这种拆分方式让build方法结构更清晰,每个子方法专注于一个榜单的构建,便于后续维护和扩展。

排行榜列表的构建

  Widget _buildScriptRanking() {
    return Column(
      children: [
        Container(
          color: Colors.white,
          child: TabBar(
            controller: _scriptTabController,
            indicatorColor: _primaryColor,
            labelColor: _primaryColor,
            unselectedLabelColor: Colors.grey,

_buildScriptRanking方法专门构建剧本榜的UI结构,返回Column布局,分为上下两部分:子TabBar和TabBarView。首先创建白色背景的Container包裹子TabBar,避免和页面背景色混淆;TabBar绑定_scriptTabController,指示器和选中标签颜色使用主题色_primaryColor,未选中标签为灰色,形成视觉对比,保持和主TabBar的设计风格统一,同时区分层级。

            tabs: const [Tab(text: '热门榜'), Tab(text: '评分榜'), Tab(text: '新晋榜')],
          ),
        ),
        Expanded(
          child: TabBarView(
            controller: _scriptTabController,
            children: [
              _buildScriptList(_hotScripts, 'hot'),
              _buildScriptList(_hotScripts, 'rating'),
              _buildScriptList(_hotScripts, 'new'),
            ],
          ),
        ),
      ],
    );
  }

子TabBar定义了热门榜、评分榜、新晋榜三个标签,下方使用Expanded包裹TabBarView(占满剩余空间),绑定_scriptTabController,children数组调用_buildScriptList方法并传入不同参数,分别构建三个子榜单的列表。这种设计让剧本榜的不同排序维度复用同一个列表构建方法,仅通过参数区分,减少重复代码,符合DRY(Don’t Repeat Yourself)编程原则。

  Widget _buildScriptList(List<Map<String, dynamic>> scripts, String type) {
    return ListView.builder(
      padding: const EdgeInsets.all(12),
      itemCount: scripts.length,
      itemBuilder: (context, index) => _buildScriptItem(scripts[index], index + 1, type),
    );
  }

_buildScriptList方法接收剧本数据列表和榜单类型参数,返回ListView.builder构建可滚动列表。ListView.builder是按需渲染的列表组件,仅创建可见区域的列表项,相比普通ListView更节省内存,适合展示榜单类数据(可能有较多条目)。padding设置为12像素,让列表和页面边缘保持间距,提升视觉舒适度;itemCount为数据长度,itemBuilder调用_buildScriptItem构建单个列表项,传入索引+1作为排名(榜单排名从1开始)。

  Widget _buildScriptItem(Map<String, dynamic> script, int rank, String type) {
    Color rankColor = rank == 1 ? const Color(0xFFFFD700) : rank == 2 ? const Color(0xFFC0C0C0) : rank == 3 ? const Color(0xFFCD7F32) : Colors.grey[400]!;
    IconData? rankIcon = rank <= 3 ? Icons.emoji_events : null;

    return GestureDetector(
      onTap: () => Get.toNamed('/script/detail', arguments: {'id': script['id']}),
      child: Container(
        margin: const EdgeInsets.only(bottom: 12),
        padding: const EdgeInsets.all(12),

_buildScriptItem方法构建单个剧本榜单项,首先根据排名设置不同的徽章颜色:第1名金色、第2名银色、第3名铜色,其余为灰色,符合用户对排行榜名次的视觉认知;rankIcon为排名前三的项添加奖杯图标,强化视觉突出效果。外层使用GestureDetector包裹,添加点击事件,通过GetX的路由跳转至剧本详情页,并传入剧本id,实现榜单项的交互跳转,符合App的核心使用流程。

        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(12),
          boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2))],
        ),
        child: Row(
          children: [
            _buildRankBadge(rank, rankColor, rankIcon),
            const SizedBox(width: 12),
            Container(
              width: 60,
              height: 80,
              decoration: BoxDecoration(color: _primaryColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8)),

为列表项容器设置白色背景、12像素圆角和轻微阴影,提升卡片式视觉效果,符合现代移动端UI设计风格。内部使用Row布局横向排列内容,首先调用_buildRankBadge构建排名徽章,接着用SizedBox设置间距,然后创建60x80的容器作为剧本封面占位(后续可替换为Image组件加载封面图),背景色为主题色浅透效果,搭配书籍图标,在无图片时保证UI完整性。

              child: const Icon(Icons.auto_stories, color: Color(0xFF6B4EFF), size: 30),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(script['name'], style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), maxLines: 1, overflow: TextOverflow.ellipsis),
                  const SizedBox(height: 4),
                  Container(
                    padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
                    decoration: BoxDecoration(color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(4)),

封面容器右侧是剧本信息区域,使用Expanded占满剩余横向空间,内部Column纵向排列剧本名称和类型。剧本名称设置加粗、16号字体,maxLines和overflow处理超长名称的截断;剧本类型使用蓝色浅透背景的标签样式,字体缩小,突出分类信息的同时不抢视觉焦点,符合移动端信息层级的设计原则。

                    child: Text(script['type'], style: const TextStyle(color: Colors.blue, fontSize: 11)),
                  ),
                ],
              ),
            ),
            Column(
              crossAxisAlignment: CrossAxisAlignment.end,
              children: [
                Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    const Icon(Icons.star, size: 16, color: Colors.amber),
                    const SizedBox(width: 2),
                    Text('${script['rating']}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),

信息区域右侧是评分和游玩人数展示区,Column右对齐排列。评分部分用星星图标+数字的组合,星星为琥珀色,数字加粗,直观展示剧本评分;游玩人数使用灰色小字,通过_formatNumber方法格式化数值(如万/千单位),提升数据可读性,符合用户快速浏览的需求。

                  ],
                ),
                const SizedBox(height: 4),
                Text('${_formatNumber(script['plays'])}人玩过', style: TextStyle(color: Colors.grey[600], fontSize: 11)),
              ],
            ),
          ],
        ),
      ),
    );
  }

完成剧本项的评分和游玩人数展示,Row和Column的嵌套布局保证了各元素的对齐和间距,整体结构清晰。GestureDetector包裹整个容器,确保点击任意区域都能触发跳转,提升交互体验。各项间距的设置(如SizedBox)让UI元素不拥挤,符合移动端舒适的触控和视觉体验。

  Widget _buildRankBadge(int rank, Color color, IconData? icon) {
    return Container(
      width: 36,
      height: 36,
      decoration: BoxDecoration(
        color: color,
        shape: BoxShape.circle,
        boxShadow: rank <= 3 ? [BoxShadow(color: color.withOpacity(0.4), blurRadius: 8, offset: const Offset(0, 2))] : null,
      ),
      child: Center(
        child: icon != null ? Icon(icon, size: 20, color: Colors.white) : Text('$rank', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14)),

_buildRankBadge方法专门构建排名徽章,36x36的圆形容器,背景色根据排名动态设置。排名前三的徽章添加阴影效果,增强视觉层次感;内部根据是否有图标(rank<=3)展示奖杯图标或排名数字,数字白色加粗,确保在彩色背景上的可读性。该方法封装了排名徽章的UI逻辑,便于在店铺榜、玩家榜中复用,保持整个排行榜的视觉一致性。

      ),
    );
  }

  String _formatNumber(int number) {
    if (number >= 10000) return '${(number / 10000).toStringAsFixed(1)}万';
    if (number >= 1000) return '${(number / 1000).toStringAsFixed(1)}k';
    return '$number';
  }

_formatNumber方法封装了数字格式化逻辑,将大数值转换为“万”或“k”单位,保留1位小数,让榜单中的游玩人数、粉丝数等数据更简洁易读。比如12580转换为1.3万,符合移动端信息展示的简洁性原则,避免长数字占用过多UI空间,也提升用户快速理解数据的效率。

  Widget _buildStoreRanking() {
    return Column(
      children: [
        Container(
          color: Colors.white,
          child: TabBar(
            controller: _storeTabController,
            indicatorColor: _primaryColor,
            labelColor: _primaryColor,
            unselectedLabelColor: Colors.grey,
            tabs: const [Tab(text: '人气榜'), Tab(text: '好评榜'), Tab(text: '距离榜')],

_buildStoreRanking方法构建店铺榜UI,结构和剧本榜一致:Column包含子TabBar和TabBarView。子TabBar的标签改为人气榜、好评榜、距离榜,贴合店铺榜单的排序维度;样式上和剧本榜子TabBar保持一致(主题色指示器、灰色未选中标签),保证整个排行榜页面的视觉统一性,降低用户的学习成本。

          ),
        ),
        Expanded(
          child: TabBarView(
            controller: _storeTabController,
            children: [
              _buildStoreList(_hotStores, 'hot'),
              _buildStoreList(_hotStores, 'rating'),
              _buildStoreList(_hotStores, 'distance'),
            ],
          ),
        ),
      ],
    );
  }

TabBarView绑定_storeTabController,children调用_buildStoreList方法并传入不同排序类型参数,和剧本榜的实现逻辑一致,体现了代码的复用性和一致性设计。Expanded确保TabBarView占满剩余空间,适配不同屏幕尺寸,避免内容溢出或留白过多的问题。

  Widget _buildStoreList(List<Map<String, dynamic>> stores, String type) {
    return ListView.builder(
      padding: const EdgeInsets.all(12),
      itemCount: stores.length,
      itemBuilder: (context, index) => _buildStoreItem(stores[index], index + 1, type),
    );
  }

_buildStoreList方法和_buildScriptList结构完全一致,使用ListView.builder按需构建店铺列表项,传入店铺数据、排名和排序类型。这种复用的方法设计减少了重复代码,后续若需调整列表的通用样式(如padding、item间距),只需修改一处即可,提升代码维护效率。

  Widget _buildStoreItem(Map<String, dynamic> store, int rank, String type) {
    Color rankColor = rank == 1 ? const Color(0xFFFFD700) : rank == 2 ? const Color(0xFFC0C0C0) : rank == 3 ? const Color(0xFFCD7F32) : Colors.grey[400]!;

    return GestureDetector(
      onTap: () => Get.toNamed('/store/detail', arguments: {'id': store['id']}),
      child: Container(
        margin: const EdgeInsets.only(bottom: 12),
        padding: const EdgeInsets.all(12),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(12),
          boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2))],

_buildStoreItem方法构建单个店铺榜单项,排名颜色逻辑和剧本项一致,保证视觉统一。点击事件跳转至店铺详情页,传入店铺id。容器样式(白色背景、圆角、阴影)和剧本项保持一致,维持榜单UI的统一性,让用户在切换不同榜单时感知连贯。

        ),
        child: Row(
          children: [
            _buildRankBadge(rank, rankColor, rank <= 3 ? Icons.emoji_events : null),
            const SizedBox(width: 12),
            Container(
              width: 60,
              height: 60,
              decoration: BoxDecoration(color: _primaryColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8)),
              child: const Icon(Icons.store, color: Color(0xFF6B4EFF), size: 30),

Row布局内首先是复用的排名徽章,接着是60x60的店铺图标容器(比剧本封面容器矮,适配店铺展示的视觉比例),使用店铺图标+主题色浅透背景,替代实际店铺图片,保证无图时的UI完整性。间距设置和剧本项一致,维持布局的节奏感。

            ),
            const SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(store['name'], style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
                  const SizedBox(height: 4),
                  Row(
                    children: [
                      Icon(Icons.location_on, size: 12, color: Colors.grey[500]),
                      const SizedBox(width: 2),
                      Expanded(child: Text(store['address'], style: TextStyle(color: Colors.grey[600], fontSize: 12), maxLines: 1, overflow: TextOverflow.ellipsis)),

店铺信息区域使用Expanded占满剩余空间,Column纵向排列店铺名称和地址。名称加粗15号字体,地址搭配定位图标,灰色小字,超长地址截断处理,符合移动端地址展示的常见设计方式,既展示核心信息又不占用过多空间。

                    ],
                  ),
                ],
              ),
            ),
            Column(
              crossAxisAlignment: CrossAxisAlignment.end,
              children: [
                Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    const Icon(Icons.star, size: 14, color: Colors.amber),
                    Text(' ${store['rating']}', style: const TextStyle(fontWeight: FontWeight.bold)),

店铺信息右侧是评分、评价数和距离展示区,Column右对齐。评分部分星星图标缩小至14号,保持和剧本项的视觉比例协调;评价数用灰色小字展示,距离信息仅在“距离榜”排序时显示,通过type参数控制,适配不同排序维度的展示需求。

                  ],
                ),
                const SizedBox(height: 4),
                Text('${store['reviews']}条评价', style: TextStyle(color: Colors.grey[600], fontSize: 11)),
                if (type == 'distance') Text(store['distance'], style: TextStyle(color: _primaryColor, fontSize: 11)),
              ],
            ),
          ],
        ),
      ),
    );
  }

完成店铺项的评价数和距离展示,距离信息通过条件渲染仅在距离榜显示,并用主题色突出,符合“距离榜”的核心展示诉求。整体布局和剧本项保持一致,让用户在浏览不同榜单时形成统一的视觉认知,提升使用体验。

  Widget _buildPlayerRanking() {
    return Column(
      children: [
        Container(
          color: Colors.white,
          child: TabBar(
            controller: _playerTabController,
            indicatorColor: _primaryColor,
            labelColor: _primaryColor,
            unselectedLabelColor: Colors.grey,
            tabs: const [Tab(text: '场次榜'), Tab(text: '人气榜'), Tab(text: '活跃榜')],

_buildPlayerRanking方法构建玩家榜UI,结构仍为Column包含子TabBar和TabBarView。子TabBar标签改为场次榜、人气榜、活跃榜,贴合玩家榜单的排序维度;样式和前两个榜单的子TabBar完全一致,保证页面视觉风格统一,符合用户的使用习惯。

          ),
        ),
        Expanded(
          child: TabBarView(
            controller: _playerTabController,
            children: [
              _buildPlayerList(_topPlayers, 'games'),
              _buildPlayerList(_topPlayers, 'fans'),
              _buildPlayerList(_topPlayers, 'active'),
            ],
          ),
        ),
      ],
    );
  }

TabBarView绑定_playerTabController,children调用_buildPlayerList方法并传入不同排序类型参数,延续了剧本榜、店铺榜的实现逻辑,保证代码结构的一致性,便于后续维护和扩展新的排序维度。

  Widget _buildPlayerList(List<Map<String, dynamic>> players, String type) {
    return ListView.builder(
      padding: const EdgeInsets.all(12),
      itemCount: players.length,
      itemBuilder: (context, index) => _buildPlayerItem(players[index], index + 1, type),
    );
  }

_buildPlayerList方法和剧本、店铺的列表构建方法结构一致,使用ListView.builder按需渲染玩家列表项,传入玩家数据、排名和排序类型。这种高度复用的方法设计降低了代码冗余,也让三个榜单的列表性能优化策略(如按需渲染)保持一致。

  Widget _buildPlayerItem(Map<String, dynamic> player, int rank, String type) {
    Color rankColor = rank == 1 ? const Color(0xFFFFD700) : rank == 2 ? const Color(0xFFC0C0C0) : rank == 3 ? const Color(0xFFCD7F32) : Colors.grey[400]!;

    return GestureDetector(
      onTap: () => Get.toNamed('/player/profile', arguments: {'id': player['id']}),
      child: Container(
        margin: const EdgeInsets.only(bottom: 12),
        padding: const EdgeInsets.all(12),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(12),
          boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2))],

_buildPlayerItem方法构建单个玩家榜单项,排名颜色逻辑和前两个榜单一致,点击事件跳转至玩家个人主页,传入玩家id。容器样式(白色背景、圆角、阴影)和剧本、店铺项保持统一,维持整个排行榜的视觉风格一致性。

        ),
        child: Row(
          children: [
            _buildRankBadge(rank, rankColor, rank <= 3 ? Icons.emoji_events : null),
            const SizedBox(width: 12),
            Stack(
              children: [
                CircleAvatar(
                  radius: 28,
                  backgroundColor: _primaryColor.withOpacity(0.1),
                  child: const Icon(Icons.person, color: Color(0xFF6B4EFF), size: 28),

玩家项的布局中,排名徽章右侧是玩家头像区域,使用Stack叠加CircleAvatar(圆形头像容器)和等级标签。CircleAvatar设置28像素半径,主题色浅透背景+人物图标,替代实际头像图片;等级标签定位在头像右下角,紫色背景+白色小字,突出玩家等级信息,符合社交类榜单的展示需求。

                ),
                Positioned(
                  bottom: 0,
                  right: 0,
                  child: Container(
                    padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
                    decoration: BoxDecoration(color: _primaryColor, borderRadius: BorderRadius.circular(8)),
                    child: Text(player['level'], style: const TextStyle(color: Colors.white, fontSize: 9, fontWeight: FontWeight.bold)),
                  ),
                ),
              ],
            ),

完成玩家等级标签的定位和样式设置,Positioned组件精准控制标签在头像右下角,小字体+加粗保证可读性,同时不遮挡头像主体。这种叠加布局让头像区域包含更多信息,提升空间利用率,符合移动端紧凑但不拥挤的设计原则。

            const SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(player['name'], style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
                  const SizedBox(height: 4),
                  Text('已玩${player['games']}场', style: TextStyle(color: Colors.grey[600], fontSize: 12)),

头像右侧是玩家信息区域,Expanded占满剩余空间,Column纵向排列玩家名称和游玩场次。名称加粗15号字体,游玩场次灰色小字,信息简洁明了,符合用户快速浏览玩家核心信息的需求,和前两个榜单的信息层级设计保持一致。

                ],
              ),
            ),
            Column(
              crossAxisAlignment: CrossAxisAlignment.end,
              children: [
                Text('${_formatNumber(player['fans'])}粉丝', style: TextStyle(color: Colors.grey[600], fontSize: 12)),
                const SizedBox(height: 4),
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
                  decoration: BoxDecoration(color: _primaryColor, borderRadius: BorderRadius.circular(12)),
                  child: const Text('关注', style: TextStyle(color: Colors.white, fontSize: 11)),

玩家信息右侧是粉丝数和关注按钮展示区,Column右对齐。粉丝数通过_formatNumber格式化,灰色小字展示;关注按钮使用主题色背景、圆角设计,白色小字,符合移动端按钮的常见样式,点击事件可后续扩展,当前仅做UI展示,为后续交互预留接口。

                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

完成玩家项的UI构建,整体布局和剧本、店铺项保持高度一致,仅替换核心展示信息和图标,让整个排行榜页面的视觉和交互逻辑统一。这种一致性设计不仅提升了用户体验,也让代码结构清晰,便于后续扩展和维护。

排行榜设计的最佳实践

排行榜功能的实现涉及多个方面的设计考量。首先是数据结构的设计,需要支持多维度的排序和筛选。其次是UI层次的设计,嵌套TabBar的使用为用户提供了清晰的信息层次。第三是视觉设计的细节,金银铜徽章的使用是排行榜设计中的经典元素。

性能优化建议

在实现排行榜功能时,性能优化是一个重要考虑因素。首先是列表的虚拟化,使用ListView.builder可以确保只有可见的项目被渲染。其次是图片的懒加载,应该使用缓存和懒加载策略。第三是数据的分页加载,当用户滚动到列表底部时自动加载下一页数据。第四是缓存策略,可以设置合理的缓存时间来减少服务器压力。

交互设计与缓存策略

排行榜的交互设计应该考虑用户的使用场景。用户通常想要快速浏览排行榜,找到感兴趣的内容。排行榜数据通常不需要实时更新,可以设置合理的缓存时间来减少服务器压力。建议在本地缓存排行榜数据,设置合理的过期时间(如1小时),并提供手动刷新按钮。

扩展功能与用户体验

排行榜功能可以进一步扩展,例如添加个人排名显示、排行榜变化趋势、分享功能等。在设计排行榜时,应该重点关注用户体验,排行榜的加载速度应该尽可能快,可以使用骨架屏或加载动画来提升用户体验。排行榜的信息展示应该清晰明了,重要信息应该突出显示。

数据安全与隐私

在实现排行榜功能时,应该考虑数据安全和隐私问题。用户的个人信息应该得到保护,不应该在排行榜中暴露敏感信息。排行榜数据应该经过验证,防止数据被篡改。应该遵守相关的数据保护法规,如GDPR等。

通过本篇文章的学习,我们完成了排行榜展示功能的实现。排行榜为用户提供了发现优质内容的入口,是提升用户活跃度和留存率的重要功能。排行榜的设计需要考虑数据结构、性能优化、API设计、交互设计、缓存策略等多个方面。通过合理的设计和实现,可以创建一个高效、易用、安全的排行榜功能。下一篇文章我们将实现活动中心功能。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐