在这里插入图片描述

引言

我的组队管理是剧本杀组队App中用户管理自己组队信息的核心功能模块。用户可以查看自己发起的组队、参与的组队以及历史组队记录,还可以对组队进行编辑、取消等操作。本篇文章将详细讲解如何实现一个功能完善的组队管理页面,包括多Tab切换、组队状态管理和操作功能。

我的组队页面采用顶部Tab切换不同类型组队的设计,分为"我发起的"、"我参与的"和"历史记录"三个标签页。每个组队卡片展示剧本信息、时间、人数和状态,并提供相应的操作按钮。

功能需求分析

核心功能模块

  1. 我发起的组队:显示用户发起的所有组队,包括进行中和已完成的
  2. 我参与的组队:显示用户参与的所有组队
  3. 历史记录:显示用户的历史组队记录
  4. 组队操作:编辑、取消、退出、删除等操作
  5. 状态管理:显示组队的当前状态(招募中、进行中、已完成等)

用户交互需求

  • 用户可以快速切换不同类型的组队
  • 用户可以查看组队的详细信息
  • 用户可以对自己发起的组队进行编辑
  • 用户可以取消或退出组队
  • 用户可以查看历史组队记录

核心代码实现

第一部分:导入依赖与类定义

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

/// 我的组队管理页面 - 管理用户的所有组队信息
/// 支持查看发起的组队、参与的组队和历史记录
/// 提供组队编辑、取消、退出等操作功能
class MyTeamsPage extends StatefulWidget {
  const MyTeamsPage({super.key});

  
  State<MyTeamsPage> createState() => _MyTeamsPageState();
}

首先导入必要的Flutter和GetX包。我们使用GetX框架来简化状态管理和路由导航。MyTeamsPage是一个StatefulWidget,用于管理组队列表的状态。这个页面需要维护多个Tab的状态,因此选择使用StatefulWidget而不是StatelessWidget。

GetX是一个强大的状态管理框架,提供了简洁的API用于路由导航、状态管理和依赖注入。通过使用GetX,我们可以减少样板代码,提高开发效率。

class _MyTeamsPageState extends State<MyTeamsPage>
    with SingleTickerProviderStateMixin {
  /// 主题色 - 紫色
  static const Color _primaryColor = Color(0xFF6B4EFF);

_MyTeamsPageState是MyTeamsPage的State类,负责管理页面的状态和生命周期。SingleTickerProviderStateMixin提供了对TabController的支持,使得Tab切换时能够产生平滑的动画效果。

主题色定义为紫色(0xFF6B4EFF),这是整个应用的主色调。使用static const修饰符定义常量颜色,这样可以在整个类中复用,避免硬编码颜色值。

  /// Tab控制器
  late TabController _tabController;
  
  /// 当前选中的Tab索引
  int _currentTabIndex = 0;
  
  /// 我发起的组队列表
  final List<Map<String, dynamic>> _initiatedTeams = [
    {
      'id': '1',
      'script': '年轮',
      'type': '情感本',
      'time': '今天 19:00',
      'current': 4,
      'total': 6,
      'status': '招募中',
      'store': '迷雾剧本杀',
    },
    {
      'id': '2',
      'script': '谍影重重',
      'type': '推理本',
      'time': '明天 14:00',
      'current': 5,
      'total': 6,
      'status': '招募中',
      'store': '剧本杀工坊',
    },
  ];

TabController用于管理三个Tab的切换。_currentTabIndex记录当前选中的Tab索引,用于在Tab切换时更新UI。这个变量在Tab切换时会被更新,触发页面重建。

_initiatedTeams列表存储用户发起的所有组队信息。每个组队是一个Map对象,包含id、剧本名称、剧本类型、组队时间、当前人数、总人数、组队状态和店铺名称等关键信息。

使用Map而不是自定义类的好处是灵活性高,可以快速添加或修改字段。但在大型项目中,建议使用自定义的数据模型类来提高类型安全性和代码可维护性。

  /// 我参与的组队列表
  final List<Map<String, dynamic>> _participatedTeams = [
    {
      'id': '3',
      'script': '密室逃脱',
      'type': '悬疑本',
      'time': '后天 20:00',
      'current': 6,
      'total': 6,
      'status': '进行中',
      'store': '谜题工坊',
      'host': '张三',
    },
  ];

  /// 历史组队列表
  final List<Map<String, dynamic>> _historyTeams = [
    {
      'id': '4',
      'script': '古墓迷踪',
      'type': '冒险本',
      'time': '上周 19:00',
      'current': 6,
      'total': 6,
      'status': '已完成',
      'store': '冒险岛',
      'rating': 4.5,
    },
  ];

_participatedTeams列表存储用户参与的组队信息。与发起的组队不同,参与的组队包含组队发起者的信息(host字段),这样用户可以快速了解谁发起了这个组队。

_historyTeams列表存储用户的历史组队记录。历史组队包含评分信息(rating字段),用户可以查看之前参与的组队的评分。这个评分通常是由其他参与者给出的,反映了这个组队的质量。

这三个列表都是final修饰的,表示列表本身不会被重新赋值,但列表中的元素可以被修改。在实际项目中,这些数据应该从后端API获取,而不是硬编码在代码中。

  
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
    _tabController.addListener(_handleTabChange);
  }

  /// 处理Tab切换
  void _handleTabChange() {
    setState(() {
      _currentTabIndex = _tabController.index;
    });
  }

  
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

在initState中初始化TabController,指定有3个Tab。vsync参数设置为this,表示使用当前State作为TickerProvider。TickerProvider是Flutter中用于管理动画帧的接口,确保动画与屏幕刷新率同步。

添加监听器来处理Tab切换事件。当用户切换Tab时,_handleTabChange方法会被调用,更新_currentTabIndex并触发UI重建。setState方法会通知Flutter框架状态已改变,需要重新构建UI。

在dispose方法中释放TabController资源,防止内存泄漏。这是Flutter中的最佳实践。当页面被销毁时,必须释放所有占用的资源,包括控制器、流订阅等。

第二部分:页面主体结构

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('我的组队'),
        centerTitle: true,
        elevation: 0,
        backgroundColor: _primaryColor,
        foregroundColor: Colors.white,
        bottom: TabBar(
          controller: _tabController,
          indicatorColor: Colors.white,
          indicatorWeight: 3,
          labelColor: Colors.white,
          unselectedLabelColor: Colors.white70,
          tabs: const [
            Tab(text: '我发起的'),
            Tab(text: '我参与的'),
            Tab(text: '历史记录'),
          ],
        ),
      ),
      backgroundColor: const Color(0xFFF5F5F5),
      body: TabBarView(
        controller: _tabController,
        children: [
          _buildInitiatedTeamsView(),
          _buildParticipatedTeamsView(),
          _buildHistoryTeamsView(),
        ],
      ),
    );
  }

build方法是页面的主入口。Scaffold提供了基本的Material Design布局结构,包括AppBar、body等主要组件。AppBar设置了页面标题"我的组队"和背景色,elevation设置为0表示不显示阴影。

TabBar包含三个Tab标签,分别对应"我发起的"、"我参与的"和"历史记录"三个功能模块。indicatorColor设置为白色,indicatorWeight设置为3表示指示器的厚度。labelColor和unselectedLabelColor分别设置选中和未选中Tab的文字颜色。

TabBarView与TabBar关联,当用户切换Tab时,会显示对应的视图内容。三个子视图分别通过_buildInitiatedTeamsView()、_buildParticipatedTeamsView()和_buildHistoryTeamsView()方法构建。

  /// 构建我发起的组队视图
  Widget _buildInitiatedTeamsView() {
    if (_initiatedTeams.isEmpty) {
      return _buildEmptyState(
        icon: Icons.group_add_outlined,
        title: '还没有发起过组队',
        subtitle: '点击下方按钮发起一个新的组队吧',
        buttonText: '发起组队',
        onButtonTap: () => _navigateToCreateTeam(),
      );
    }

    return ListView.builder(
      padding: const EdgeInsets.all(12),
      itemCount: _initiatedTeams.length,
      itemBuilder: (context, index) {
        return _buildTeamCard(
          team: _initiatedTeams[index],
          isInitiated: true,
          onEdit: () => _editTeam(_initiatedTeams[index]),
          onCancel: () => _cancelTeam(_initiatedTeams[index]),
          onDelete: () => _deleteTeam(_initiatedTeams[index]),
        );
      },
    );
  }

_buildInitiatedTeamsView方法构建用户发起的组队列表视图。首先检查列表是否为空,如果为空则显示空状态提示,引导用户发起新的组队。

如果列表不为空,使用ListView.builder构建可滚动的列表。ListView.builder是高效的列表构建方式,只会渲染可见的项目,这对于大列表来说性能更好。

每个列表项通过_buildTeamCard方法构建,并传入相应的回调函数处理编辑、取消等操作。isInitiated参数设置为true,表示这是用户发起的组队,会显示不同的操作按钮。

  /// 构建我参与的组队视图
  Widget _buildParticipatedTeamsView() {
    if (_participatedTeams.isEmpty) {
      return _buildEmptyState(
        icon: Icons.search_outlined,
        title: '还没有参与过组队',
        subtitle: '去组队大厅找一个感兴趣的组队吧',
        buttonText: '浏览组队',
        onButtonTap: () => _navigateToTeamHall(),
      );
    }

    return ListView.builder(
      padding: const EdgeInsets.all(12),
      itemCount: _participatedTeams.length,
      itemBuilder: (context, index) {
        return _buildTeamCard(
          team: _participatedTeams[index],
          isInitiated: false,
          onQuit: () => _quitTeam(_participatedTeams[index]),
          onContact: () => _contactHost(_participatedTeams[index]),
        );
      },
    );
  }

_buildParticipatedTeamsView方法构建用户参与的组队列表视图。与发起的组队不同,参与的组队提供"联系车主"和"退出"两个操作按钮。isInitiated参数设置为false,表示这是用户参与的组队。

  /// 构建历史记录视图
  Widget _buildHistoryTeamsView() {
    if (_historyTeams.isEmpty) {
      return _buildEmptyState(
        icon: Icons.history_outlined,
        title: '还没有历史记录',
        subtitle: '参与组队后会显示在这里',
        buttonText: '返回首页',
        onButtonTap: () => Get.back(),
      );
    }

    return ListView.builder(
      padding: const EdgeInsets.all(12),
      itemCount: _historyTeams.length,
      itemBuilder: (context, index) {
        return _buildHistoryTeamCard(_historyTeams[index]);
      },
    );
  }

_buildHistoryTeamsView方法构建历史组队记录视图。历史组队使用不同的卡片样式_buildHistoryTeamCard来展示,包含评分信息和完成状态。

这三个视图方法都遵循相同的模式:先检查列表是否为空,然后使用ListView.builder构建列表。这样的设计使得代码结构清晰,易于维护和扩展。每个视图都有对应的空状态提示,提升了用户体验。

第三部分:组队卡片组件

  /// 构建组队卡片
  Widget _buildTeamCard({
    required Map<String, dynamic> team,
    required bool isInitiated,
    VoidCallback? onEdit,
    VoidCallback? onCancel,
    VoidCallback? onDelete,
    VoidCallback? onQuit,
    VoidCallback? onContact,
  }) {
    return Container(
      margin: const EdgeInsets.only(bottom: 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),
          ),
        ],
      ),

_buildTeamCard方法构建组队卡片的主体部分。卡片使用Container包装,设置了圆角(borderRadius: BorderRadius.circular(12))和阴影效果(boxShadow),使其看起来更加立体和现代。

第四部分:事件处理方法

  /// 获取状态对应的颜色
  Color _getStatusColor(String status) {
    switch (status) {
      case '招募中':
        return Colors.blue;
      case '进行中':
        return Colors.orange;
      case '已完成':
        return Colors.green;
      default:
        return Colors.grey;
    }
  }

  /// 编辑组队
  void _editTeam(Map<String, dynamic> team) {
    Get.snackbar('提示', '编辑组队:${team['script']}');
  }

  /// 取消组队
  void _cancelTeam(Map<String, dynamic> team) {
    Get.dialog(
      AlertDialog(
        title: const Text('取消组队'),
        content: Text('确定要取消组队"${team['script']}"吗?'),
        actions: [
          TextButton(
            onPressed: () => Get.back(),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              Get.back();
              Get.snackbar('成功', '组队已取消');
            },
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.red,
            ),
            child: const Text(
              '确定',
              style: TextStyle(color: Colors.white),
            ),
          ),
        ],
      ),
    );
  }

_getStatusColor方法根据组队状态返回对应的颜色。这样的设计使得状态颜色的管理集中在一个地方,便于维护和修改。使用switch语句判断状态,返回对应的颜色。招募中使用蓝色,进行中使用橙色,已完成使用绿色,其他状态使用灰色。

_editTeam方法处理编辑组队的操作。实际项目中应该导航到编辑页面,这里使用SnackBar作为演示。SnackBar是一个临时的提示消息,会在屏幕底部显示,然后自动消失。

  /// 删除组队
  void _deleteTeam(Map<String, dynamic> team) {
    Get.snackbar('提示', '删除组队:${team['script']}');
  }

  /// 退出组队
  void _quitTeam(Map<String, dynamic> team) {
    Get.dialog(
      AlertDialog(
        title: const Text('退出组队'),
        content: Text('确定要退出组队"${team['script']}"吗?'),
        actions: [
          TextButton(
            onPressed: () => Get.back(),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              Get.back();
              Get.snackbar('成功', '已退出组队');
            },
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.red,
            ),
            child: const Text(
              '确定',
              style: TextStyle(color: Colors.white),
            ),
          ),
        ],
      ),
    );
  }

_cancelTeam和_quitTeam方法都使用AlertDialog显示确认对话框。这样的设计能够防止用户误操作,提高应用的安全性。AlertDialog包含标题、内容和操作按钮,用户需要确认才能执行操作。

_contactHost方法用于与组队发起者联系。实际项目中应该导航到聊天页面或打开聊天窗口。这个方法可以集成消息系统,让用户能够直接与组队发起者沟通。


  /// 联系车主
  void _contactHost(Map<String, dynamic> team) {
    Get.snackbar('提示', '跳转到聊天页面');
  }

  /// 导航到发起组队页面
  void _navigateToCreateTeam() {
    Get.snackbar('提示', '跳转到发起组队页面');
  }

  /// 导航到组队大厅
  void _navigateToTeamHall() {
    Get.snackbar('提示', '跳转到组队大厅');
  }
}

_navigateToCreateTeam和_navigateToTeamHall方法用于页面导航。使用GetX框架的Get.snackbar进行提示,实际项目中应该使用Get.to()进行页面导航。GetX提供了简洁的API用于页面跳转和参数传递。

数据模型与业务逻辑

在实际项目中,应该定义清晰的数据模型。基础Team类定义了所有组队共有的属性,包括id、剧本名称、类型、状态、时间、人数和店铺名称。canEdit()方法检查是否可编辑,canCancel()方法检查是否可取消,getRemainingPlayers()方法计算剩余人数。

这样的设计使得数据结构清晰,便于维护和扩展。每个属性都有明确的含义,代码的可读性更高。

不同类型的组队可以继承Team类并添加特定属性。InitiatedTeam添加memberIds用于管理成员,ParticipatedTeam添加hostName和hostId用于存储发起者信息,HistoryTeam添加rating和review用于存储评分和评价。

这样的继承设计使得代码更加模块化,每个类只负责自己的属性和方法。当需要添加新的组队类型时,只需要继承Team类并添加新的属性即可。

状态管理与业务逻辑

组队的状态管理是核心功能。使用枚举定义所有可能的状态(招募中、进行中、已完成、已取消),每个状态对应不同的颜色和可执行的操作。

这样的设计使得状态管理更加清晰,避免了使用字符串导致的错误。枚举提供了类型安全,编译器可以检查状态值是否有效。

业务逻辑应该封装在服务类中。TeamService类提供editTeam()、cancelTeam()、quitTeam()等方法处理各种操作,同时提供getInitiatedTeams()、getParticipatedTeams()、getHistoryTeams()等方法获取不同类型的组队列表。这样的设计将业务逻辑与UI层分离,提高代码的可维护性。

服务类可以与后端API通信,获取和更新组队数据。这样的设计使得应用更加灵活,可以轻松切换数据源。

总结

通过本篇文章的学习,我们完成了我的组队管理页面的实现。这个页面提供了完整的组队生命周期管理功能,用户可以查看和管理自己的所有组队信息。

页面设计遵循了清晰的Tab切换、直观的卡片布局和友好的交互方式,为用户提供了良好的组队管理体验。在实际项目中,应该根据具体需求进一步完善这个功能,添加更多的业务逻辑和用户交互。

在后续的开发中,你可以根据实际需求进一步优化这个页面,添加更多的功能和交互。例如,可以添加下拉刷新、上拉加载更多、搜索和筛选等功能。这些功能可以进一步提升用户体验。


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

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐