在这里插入图片描述

社团日常运营中,任务管理是个刚需。活动策划要分工、物资采购要跟进、宣传推广要落实,这些都需要有人负责。今天我们就来实现一个任务列表页面,让社团的每件事都有人管、有人跟。

这个页面要做的事情:按状态分类展示任务、显示优先级和截止时间、支持状态切换和新建任务。

引入依赖

import 'package:flutter/material.dart';

Material库提供基础UI组件,每个页面都要引入。

没它啥也干不了。

import 'package:provider/provider.dart';

Provider做状态管理,任务数据从这里拿。

数据变了页面自动刷新。

import 'package:intl/intl.dart';

intl库用来格式化日期,显示截止时间要用到。

这个库功能挺强大的。

import '../../providers/app_provider.dart';
import '../../models/user.dart';

我们自己的Provider和数据模型。

Task模型定义在user.dart里。

页面基本结构

class TaskListPage extends StatefulWidget {
  const TaskListPage({super.key});

  
  State<TaskListPage> createState() => _TaskListPageState();
}

用StatefulWidget因为要管理TabController。

Tab切换需要状态。

状态类定义

class _TaskListPageState extends State<TaskListPage> 
    with SingleTickerProviderStateMixin {
  late TabController _tabController;

SingleTickerProviderStateMixin提供动画需要的vsync。

TabController需要这个。

初始化TabController

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

initState里创建TabController,length是Tab数量。

三个Tab:待完成、进行中、已完成。

销毁资源

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

dispose里销毁TabController,避免内存泄漏。

这是标准做法,别忘了。

构建页面

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('我的任务'),
        bottom: TabBar(
          controller: _tabController,
          indicatorColor: Colors.white,
          tabs: const [
            Tab(text: '待完成'),
            Tab(text: '进行中'),
            Tab(text: '已完成'),
          ],
        ),
      ),

AppBar里嵌入TabBar,实现顶部标签导航。

indicatorColor设置指示器颜色。

TabBarView

      body: Consumer<AppProvider>(
        builder: (context, provider, _) {
          return TabBarView(
            controller: _tabController,
            children: [
              _buildTaskList(
                provider.tasks.where(
                  (t) => t.status == '待完成'
                ).toList(), 
                provider
              ),

Consumer监听Provider,TabBarView和TabBar配合使用。

where过滤出对应状态的任务。

              _buildTaskList(
                provider.tasks.where(
                  (t) => t.status == '进行中'
                ).toList(), 
                provider
              ),
              _buildTaskList(
                provider.tasks.where(
                  (t) => t.status == '已完成'
                ).toList(), 
                provider
              ),
            ],
          );
        },
      ),

三个Tab分别显示不同状态的任务。

滑动或点击切换。

浮动按钮

      floatingActionButton: FloatingActionButton(
        backgroundColor: const Color(0xFF4A90E2),
        child: const Icon(
          Icons.add, 
          color: Colors.white
        ),
        onPressed: () => _showAddTaskDialog(context),
      ),
    );
  }

右下角放个加号按钮,点击新建任务。

蓝色背景白色图标,很醒目。

任务列表构建

  Widget _buildTaskList(
    List<Task> tasks, 
    AppProvider provider
  ) {
    if (tasks.isEmpty) {
      return const Center(
        child: Text(
          '暂无任务', 
          style: TextStyle(color: Colors.grey)
        )
      );
    }

空列表显示提示文字。

不能让用户看着空白发呆。

    return ListView.builder(
      padding: const EdgeInsets.all(16),
      itemCount: tasks.length,
      itemBuilder: (context, index) {
        final task = tasks[index];
        return _buildTaskCard(task, provider);
      },
    );
  }

ListView.builder渲染任务列表。

每个任务用卡片展示。

任务卡片

  Widget _buildTaskCard(
    Task task, 
    AppProvider provider
  ) {
    Color priorityColor;
    switch (task.priority) {
      case 1:
        priorityColor = Colors.red;
        break;
      case 2:
        priorityColor = Colors.orange;
        break;
      default:
        priorityColor = Colors.green;
    }

根据优先级设置颜色,1是高优先级用红色。

2是中优先级用橙色,其他用绿色。

判断是否逾期

    final isOverdue = task.deadline.isBefore(
      DateTime.now()
    ) && task.status != '已完成';

截止时间在当前时间之前,且任务未完成,就是逾期。

逾期的任务要特殊标记。

卡片容器

    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [

Card包裹卡片,Padding加内边距。

Column纵向排列内容。

优先级指示条

                Container(
                  width: 4,
                  height: 40,
                  decoration: BoxDecoration(
                    color: priorityColor,
                    borderRadius: BorderRadius.circular(2),
                  ),
                ),
                const SizedBox(width: 12),

左边一个彩色竖条表示优先级。

红色高、橙色中、绿色低,一目了然。

任务标题

                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        task.title, 
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          fontSize: 16,
                          decoration: task.status == '已完成' 
                              ? TextDecoration.lineThrough 
                              : null,
                        )
                      ),

任务标题用粗体,已完成的加删除线。

删除线表示这事儿已经办完了。

任务描述

                      if (task.description.isNotEmpty)
                        Text(
                          task.description, 
                          style: const TextStyle(
                            color: Colors.grey, 
                            fontSize: 13
                          )
                        ),
                    ],
                  ),
                ),

描述不为空才显示,用灰色小字。

条件渲染避免空白。

状态切换菜单

                PopupMenuButton<String>(
                  onSelected: (value) {
                    provider.updateTaskStatus(
                      task.id, 
                      value
                    );
                  },
                  itemBuilder: (context) => [
                    const PopupMenuItem(
                      value: '待完成', 
                      child: Text('待完成')
                    ),
                    const PopupMenuItem(
                      value: '进行中', 
                      child: Text('进行中')
                    ),
                    const PopupMenuItem(
                      value: '已完成', 
                      child: Text('已完成')
                    ),
                  ],
                ),
              ],
            ),

点击弹出菜单切换状态。

选择后调用provider更新。

截止时间显示

            const SizedBox(height: 12),
            Row(
              children: [
                Icon(
                  Icons.access_time, 
                  size: 14, 
                  color: isOverdue ? Colors.red : Colors.grey
                ),
                const SizedBox(width: 4),
                Text(
                  '截止: ${DateFormat('MM-dd HH:mm').format(task.deadline)}',
                  style: TextStyle(
                    fontSize: 12, 
                    color: isOverdue ? Colors.red : Colors.grey
                  ),
                ),

时间图标配合截止时间,逾期变红色。

DateFormat格式化成月日时分。

逾期标签

                if (isOverdue) ...[
                  const SizedBox(width: 8),
                  Container(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 6, 
                      vertical: 2
                    ),
                    decoration: BoxDecoration(
                      color: Colors.red.withOpacity(0.1),
                      borderRadius: BorderRadius.circular(4),
                    ),
                    child: const Text(
                      '已逾期', 
                      style: TextStyle(
                        color: Colors.red, 
                        fontSize: 10
                      )
                    ),
                  ),
                ],

逾期的任务显示红色标签。

提醒用户赶紧处理。

状态标签

                const Spacer(),
                Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 8, 
                    vertical: 4
                  ),
                  decoration: BoxDecoration(
                    color: task.status == '已完成' 
                        ? Colors.green.withOpacity(0.1) 
                        : const Color(0xFF4A90E2).withOpacity(0.1),
                    borderRadius: BorderRadius.circular(4),
                  ),
                  child: Text(
                    task.status, 
                    style: TextStyle(
                      color: task.status == '已完成' 
                          ? Colors.green 
                          : const Color(0xFF4A90E2), 
                      fontSize: 12
                    )
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

右下角显示当前状态,已完成用绿色,其他用蓝色。

Spacer把状态推到最右边。

新建任务对话框

  void _showAddTaskDialog(BuildContext context) {
    final titleController = TextEditingController();
    final descController = TextEditingController();

    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('新建任务'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextField(
              controller: titleController,
              decoration: const InputDecoration(
                labelText: '任务标题', 
                border: OutlineInputBorder()
              ),
            ),

AlertDialog做弹窗,两个输入框。

mainAxisSize.min让高度自适应。

            const SizedBox(height: 12),
            TextField(
              controller: descController,
              decoration: const InputDecoration(
                labelText: '任务描述', 
                border: OutlineInputBorder()
              ),
              maxLines: 2,
            ),
          ],
        ),

描述输入框两行高度。

OutlineInputBorder加边框。

对话框按钮

        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx), 
            child: const Text('取消')
          ),
          ElevatedButton(
            onPressed: () {
              if (titleController.text.isNotEmpty) {
                final provider = Provider.of<AppProvider>(
                  context, 
                  listen: false
                );

取消按钮关闭对话框。

创建按钮先验证标题不为空。

                provider.addTask(Task(
                  clubId: 'club001',
                  title: titleController.text,
                  description: descController.text,
                  assignee: provider.currentUser.name,
                  deadline: DateTime.now().add(
                    const Duration(days: 7)
                  ),
                ));
                Navigator.pop(ctx);
              }
            },
            child: const Text('创建'),
          ),
        ],
      ),
    );
  }
}

创建任务后关闭对话框。

默认截止时间一周后。

任务数据模型

class Task {
  final String id;
  final String clubId;
  final String title;
  final String description;
  final String assignee;
  final DateTime deadline;
  final int priority;
  final String status;
  
  Task({
    String? id,
    required this.clubId,
    required this.title,
    this.description = '',
    required this.assignee,
    required this.deadline,
    this.priority = 3,
    this.status = '待完成',
  }) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString();
}

任务模型包含这些字段。

id默认用时间戳生成。

Provider中的任务方法

void addTask(Task task) {
  _tasks.add(task);
  notifyListeners();
}

void updateTaskStatus(String taskId, String status) {
  final index = _tasks.indexWhere(
    (t) => t.id == taskId
  );
  if (index != -1) {
    _tasks[index] = Task(
      id: _tasks[index].id,
      clubId: _tasks[index].clubId,
      title: _tasks[index].title,
      description: _tasks[index].description,
      assignee: _tasks[index].assignee,
      deadline: _tasks[index].deadline,
      priority: _tasks[index].priority,
      status: status,
    );
    notifyListeners();
  }
}

addTask添加任务,updateTaskStatus更新状态。

都要调用notifyListeners刷新UI。

测试数据

void initTestTasks(AppProvider provider) {
  provider.addTask(Task(
    clubId: 'club001',
    title: '准备迎新活动物资',
    description: '横幅、海报、小礼品',
    assignee: '张三',
    deadline: DateTime.now().add(
      const Duration(days: 3)
    ),
    priority: 1,
  ));

开发阶段用测试数据调试。

这个是高优先级任务。

  provider.addTask(Task(
    clubId: 'club001',
    title: '联系场地负责人',
    assignee: '李四',
    deadline: DateTime.now().subtract(
      const Duration(days: 1)
    ),
    priority: 2,
    status: '进行中',
  ));
}

这个任务已经逾期了。

页面上会显示红色警告。

小结

任务列表页面功能挺全的,Tab分类、优先级标识、逾期提醒、状态切换、新建任务都有了。代码量不小但结构清晰。

代码里用到的技巧:TabController管理标签页、PopupMenuButton做菜单、条件表达式控制样式。这些在其他场景也经常用到。

实际项目中可能还要加任务编辑、删除、指派这些功能,但基本框架就是这样。


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

Logo

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

更多推荐