在这里插入图片描述

活动详情页展示活动的完整信息,包括时间地点、报名进度、活动介绍等,并提供报名和取消报名功能。

页面参数设计

详情页接收活动对象作为参数:

class ActivityDetailPage extends StatelessWidget {
  final Activity activity;

  const ActivityDetailPage({super.key, required this.activity});

通过构造函数传入activity对象,类型安全。
使用StatelessWidget因为页面本身不维护状态。

页面整体结构

使用Consumer监听数据变化:

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('活动详情')),
      body: Consumer<AppProvider>(
        builder: (context, provider, _) {
          final currentActivity = provider.activities.firstWhere(
            (a) => a.id == activity.id, 
            orElse: () => activity
          );

从Provider获取最新的活动数据,确保报名状态实时更新。
orElse处理找不到的情况,返回传入的原始数据。

页面布局:

          return SingleChildScrollView(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                _buildHeader(currentActivity),
                Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      _buildInfoCard(currentActivity),
                      const SizedBox(height: 16),
                      _buildProgressCard(currentActivity),
                      const SizedBox(height: 16),
                      _buildDescriptionCard(currentActivity),
                      const SizedBox(height: 24),
                      _buildActionButton(context, provider, currentActivity),
                    ],
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }

SingleChildScrollView让内容可滚动。
模块顺序:头部、信息卡片、进度卡片、详情卡片、操作按钮。

头部区域

根据状态设置颜色:

  Widget _buildHeader(Activity activity) {
    Color statusColor;
    switch (activity.status) {
      case '报名中':
        statusColor = Colors.green;
        break;
      case '即将开始':
        statusColor = Colors.orange;
        break;
      case '已结束':
        statusColor = Colors.grey;
        break;
      default:
        statusColor = Colors.blue;
    }

switch语句根据状态返回对应颜色。
颜色语义和活动列表保持一致。

头部容器:

    return Container(
      height: 180,
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [
            const Color(0xFF4A90E2), 
            const Color(0xFF357ABD).withOpacity(0.8)
          ],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        ),
      ),

渐变背景从左上到右下,蓝色调和主题一致。
180像素的高度足够展示标题和状态。

背景图标和内容:

      child: Stack(
        children: [
          Center(
            child: Icon(
              Icons.event, 
              size: 80, 
              color: Colors.white.withOpacity(0.3)
            )
          ),
          Positioned(
            left: 16,
            bottom: 16,
            right: 16,
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [

Stack叠加背景图标和前景内容。
Positioned把内容定位在左下角。

状态标签和标题:

                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
                  decoration: BoxDecoration(
                    color: statusColor,
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Text(
                    activity.status, 
                    style: const TextStyle(color: Colors.white, fontSize: 12)
                  ),
                ),
                const SizedBox(height: 8),
                Text(
                  activity.title, 
                  style: const TextStyle(
                    color: Colors.white, 
                    fontSize: 22, 
                    fontWeight: FontWeight.bold
                  )
                ),
                const SizedBox(height: 4),
                Text(
                  activity.clubName, 
                  style: const TextStyle(color: Colors.white70, fontSize: 14)
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

状态标签用实心背景,在渐变背景上更醒目。
标题22像素白色粗体,是头部最重要的信息。

信息卡片

构建信息卡片:

  Widget _buildInfoCard(Activity activity) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            _buildInfoRow(
              Icons.access_time, 
              '开始时间', 
              DateFormat('yyyy-MM-dd HH:mm').format(activity.startTime)
            ),
            const Divider(),
            _buildInfoRow(
              Icons.access_time_filled, 
              '结束时间', 
              DateFormat('yyyy-MM-dd HH:mm').format(activity.endTime)
            ),

开始和结束时间用不同的图标区分。
日期格式化成完整的年月日时分。

继续添加信息行:

            const Divider(),
            _buildInfoRow(Icons.location_on, '活动地点', activity.location),
            const Divider(),
            _buildInfoRow(Icons.person, '组织者', activity.organizer),
          ],
        ),
      ),
    );
  }

四行信息涵盖了活动的基本情况。
Divider分割线让各行更清晰。

封装信息行组件:

  Widget _buildInfoRow(IconData icon, String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        children: [
          Icon(icon, size: 20, color: const Color(0xFF4A90E2)),
          const SizedBox(width: 12),
          Text(label, style: const TextStyle(color: Colors.grey)),
          const Spacer(),
          Flexible(
            child: Text(
              value, 
              style: const TextStyle(fontWeight: FontWeight.w500), 
              textAlign: TextAlign.right
            )
          ),
        ],
      ),
    );
  }

Flexible让值文字可以换行,避免溢出。
textAlign右对齐让布局更整齐。

报名进度卡片

构建进度卡片:

  Widget _buildProgressCard(Activity activity) {
    final progress = activity.currentParticipants / activity.maxParticipants;
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text(
                  '报名进度', 
                  style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)
                ),
                Text(
                  '${activity.currentParticipants}/${activity.maxParticipants}人', 
                  style: const TextStyle(
                    color: Color(0xFF4A90E2), 
                    fontWeight: FontWeight.bold
                  )
                ),
              ],
            ),

标题和人数分居两端。
人数用蓝色粗体突出显示。

进度条:

            const SizedBox(height: 12),
            ClipRRect(
              borderRadius: BorderRadius.circular(8),
              child: LinearProgressIndicator(
                value: progress,
                minHeight: 10,
                backgroundColor: Colors.grey[200],
                valueColor: AlwaysStoppedAnimation<Color>(
                  progress >= 1 ? Colors.red : const Color(0xFF4A90E2)
                ),
              ),
            ),

ClipRRect给进度条加圆角。
满员时变红色警示。

剩余名额提示:

            const SizedBox(height: 8),
            Text(
              progress >= 1 
                  ? '名额已满' 
                  : '还剩${activity.maxParticipants - activity.currentParticipants}个名额', 
              style: TextStyle(
                color: progress >= 1 ? Colors.red : Colors.grey, 
                fontSize: 13
              )
            ),
          ],
        ),
      ),
    );
  }

满员显示红色"名额已满",否则显示剩余名额。
文字颜色和进度条颜色保持一致。

活动详情卡片

构建详情卡片:

  Widget _buildDescriptionCard(Activity activity) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              '活动详情', 
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)
            ),
            const SizedBox(height: 12),
            Text(
              activity.description, 
              style: const TextStyle(color: Colors.grey, height: 1.6)
            ),
          ],
        ),
      ),
    );
  }

行高1.6让多行文字阅读更舒适。
灰色文字作为正文内容。

操作按钮

处理已结束状态:

  Widget _buildActionButton(
    BuildContext context, 
    AppProvider provider, 
    Activity activity
  ) {
    final canJoin = activity.status == '报名中' && 
        activity.currentParticipants < activity.maxParticipants;

    if (activity.status == '已结束') {
      return SizedBox(
        width: double.infinity,
        height: 48,
        child: ElevatedButton(
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.grey, 
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(24)
            )
          ),
          onPressed: null,
          child: const Text(
            '活动已结束', 
            style: TextStyle(fontSize: 16, color: Colors.white)
          ),
        ),
      );
    }

已结束的活动显示灰色禁用按钮。
onPressed为null时按钮自动禁用。

正常状态的按钮:

    return SizedBox(
      width: double.infinity,
      height: 48,
      child: ElevatedButton(
        style: ElevatedButton.styleFrom(
          backgroundColor: activity.isJoined 
              ? Colors.red 
              : (canJoin ? const Color(0xFF4A90E2) : Colors.grey),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(24)
          ),
        ),

已报名显示红色取消按钮,可报名显示蓝色报名按钮,满员显示灰色。
圆角24像素的胶囊形按钮。

点击事件处理:

        onPressed: canJoin || activity.isJoined
            ? () {
                if (activity.isJoined) {
                  showDialog(
                    context: context,
                    builder: (ctx) => AlertDialog(
                      title: const Text('确认取消'),
                      content: const Text('确定要取消报名吗?'),
                      actions: [
                        TextButton(
                          onPressed: () => Navigator.pop(ctx), 
                          child: const Text('取消')
                        ),
                        TextButton(
                          onPressed: () {
                            provider.cancelActivity(activity.id);
                            Navigator.pop(ctx);
                            ScaffoldMessenger.of(context).showSnackBar(
                              const SnackBar(content: Text('已取消报名'))
                            );
                          },
                          child: const Text(
                            '确定', 
                            style: TextStyle(color: Colors.red)
                          ),
                        ),
                      ],
                    ),
                  );

取消报名需要二次确认,防止误操作。
确认后调用provider方法并显示SnackBar反馈。

报名处理:

                } else {
                  provider.joinActivity(activity.id);
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('报名成功'))
                  );
                }
              }
            : null,
        child: Text(
          activity.isJoined 
              ? '取消报名' 
              : (canJoin ? '立即报名' : '名额已满'),
          style: const TextStyle(fontSize: 16, color: Colors.white),
        ),
      ),
    );
  }
}

报名操作直接执行,不需要确认。
按钮文字根据状态动态变化。

小结

活动详情页通过渐变头部展示活动标题和状态,信息卡片展示时间地点等基本信息,进度卡片直观显示报名情况。操作按钮根据活动状态和用户报名状态动态变化,取消报名有二次确认,整体交互完善。


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

Logo

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

更多推荐