在这里插入图片描述

缴费详情是小区门禁管理系统中的核心功能,它为住户提供了查看账单明细、选择缴费项目、选择支付方式、完成缴费流程的完整体验。

在实际项目中,缴费详情页面需要解决几个关键问题:

  • 如何清晰展示账单的详细信息和费用明细
  • 如何支持灵活的账单选择和费用计算
  • 如何集成多种支付方式并提供安全保障
  • 如何处理缴费成功后的状态更新和通知

这篇讲缴费详情页面的实现,重点是如何让缴费流程既安全又便捷。

对应源码

  • lib/pages/home/payment_detail_page.dart

缴费详情页面的设计思路是:信息透明 + 选择灵活 + 支付安全

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

首先引入核心依赖包,flutter/material.dart 提供基础UI组件,flutter_screenutil 用于屏幕适配,get 则是常用的状态管理工具,为后续页面开发奠定基础。

class PaymentDetailPage extends StatefulWidget {
  final List<Map<String, dynamic>> bills;
  
  const PaymentDetailPage({
    Key? key,
    required this.bills,
  }) : super(key: key);

  
  State<PaymentDetailPage> createState() => _PaymentDetailPageState();
}

将页面定义为StatefulWidget,因为缴费页面需要维护账单选中状态、支付状态等可变数据;通过构造函数接收bills参数,用于展示外部传入的账单列表数据。

class _PaymentDetailPageState extends State<PaymentDetailPage> {
  final List<bool> _selectedBills = [];
  String _paymentMethod = 'wechat';
  bool _isProcessing = false;
  bool _isCalculating = false;

在状态类中定义核心状态变量:_selectedBills 存储每个账单的选中状态,_paymentMethod 默认选中微信支付,_isProcessing 标记支付是否正在处理中,避免重复操作。

基础结构说明

  • 缴费详情页面需要维护账单选择和支付状态,所以用 StatefulWidget
  • 接收 bills 参数,显示指定账单的详情。
  • _selectedBills 存储每个账单的选中状态。
  • _paymentMethod 存储选择的支付方式。
  
  void initState() {
    super.initState();
    _initializeBills();
  }

重写initState生命周期方法,页面初始化时调用_initializeBills方法,完成账单选中状态的初始赋值。

  void _initializeBills() {
    setState(() {
      _selectedBills = List<bool>.filled(widget.bills.length, (index) => true);
    });
  }

_initializeBills方法通过setState更新状态,默认将所有账单设置为选中状态,提升用户操作效率,无需手动逐个勾选。

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

重写dispose方法,此处暂未添加额外清理逻辑,若后续有定时器、监听器等资源,可在此处释放,避免内存泄漏。

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('选择缴费'),
        centerTitle: true,
      ),

build方法构建页面核心布局,首先返回Scaffold组件,配置顶部导航栏,标题为“选择缴费”并居中显示,符合移动端UI设计习惯。

      body: Column(
        children: [
          // 账单列表
          Expanded(
            child: SingleChildScrollView(
              padding: EdgeInsets.all(16.w),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    '选择要缴费的项目',
                    style: TextStyle(
                      fontSize: 16.sp,
                      fontWeight: FontWeight.bold,
                    ),
                  ),

页面主体采用Column布局,分为账单列表和底部支付区域两部分;账单列表外层包裹ExpandedSingleChildScrollView,保证账单数量过多时可滚动,同时填充剩余空间;首先显示“选择要缴费的项目”标题,通过字体加粗突出层级。

                  SizedBox(height: 16.h),
                  
                  // 账单列表
                  ...widget.bills.asMap().entries.map((entry) {
                    final index = entry.key;
                    final bill = entry.value;
                    return _buildBillItem(bill, index);
                  }).toList(),
                ],
              ),
            ),
          ),

通过SizedBox设置标题与账单列表的间距,提升页面呼吸感;遍历bills列表,通过asMap()获取索引,调用_buildBillItem方法构建每个账单项,将账单数据和索引传入,实现账单列表的动态渲染。

          
          // 底部支付区域
          _buildPaymentSection(),
        ],
      ),
    );
  }

最后调用_buildPaymentSection构建底部支付区域,固定在页面底部,方便用户随时操作支付,无需滚动到底部,提升操作便捷性。

页面布局设计

  • 使用 Column 布局,上部是账单列表,底部是支付区域。
  • 账单列表使用 SingleChildScrollView 确保可滚动。
  • 底部支付区域固定显示,便于用户操作。
  • 初始化时默认选中所有账单。
  Widget _buildBillItem(Map<String, dynamic> bill, int index) {
    final isSelected = _selectedBills[index];
    
    return Container(
      margin: EdgeInsets.only(bottom: 12.h),
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.r),

_buildBillItem方法接收单个账单数据和索引,首先获取该账单的选中状态;返回的容器组件设置底部外边距和内边距,白色背景搭配圆角,符合移动端卡片式设计风格,提升视觉舒适度。

        border: Border.all(
          color: isSelected ? Colors.blue[300]! : Colors.grey[300]!,
          width: isSelected ? 2.w : 1.w,
        ),
        boxShadow: [
          BoxShadow(
            color: Colors.grey.withOpacity(0.1),
            blurRadius: 4.r,
            offset: Offset(0, 2.h),
          ),
        ],
      ),

为卡片添加边框样式,选中状态下使用蓝色边框且宽度更粗,未选中时为灰色细边框,视觉上清晰区分选中状态;同时添加轻微阴影,增强卡片的立体感。

      child: Column(
        children: [
          // 账单头部
          Row(
            children: [
              // 选择框
              Checkbox(
                value: isSelected,
                onChanged: (value) {
                  setState(() {
                    _selectedBills[index] = value!;
                  });
                },
                activeColor: Colors.blue,
              ),

卡片内部采用Column布局,首先是账单头部的Row布局;左侧放置Checkbox复选框,绑定当前账单的选中状态,点击时通过setState更新_selectedBills对应索引的状态,实现选中状态的实时切换。

              SizedBox(width: 12.w),
              
              // 账单信息
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Text(
                          bill['name'],
                          style: TextStyle(
                            fontSize: 16.sp,
                            fontWeight: FontWeight.bold,
                          ),
                        ),

复选框右侧通过SizedBox设置间距,然后用Expanded包裹账单信息区域,避免内容溢出;账单名称使用加粗字体,突出显示核心信息,提升可读性。

                        Container(
                          padding: EdgeInsets.symmetric(
                            horizontal: 8.w,
                            vertical: 4.h,
                          ),
                          decoration: BoxDecoration(
                            color: _getStatusColor(bill['status']),
                            borderRadius: BorderRadius.circular(8.r),
                          ),
                          child: Text(
                            _getStatusText(bill['status']),
                            style: TextStyle(
                              fontSize: 10.sp,
                              color: Colors.white,
                            ),
                          ),
                        ),
                      ],
                    ),

账单名称右侧放置状态标签,通过_getStatusColor_getStatusText获取对应状态的颜色和文本,标签采用圆角设计,小字体搭配内边距,样式简洁且信息明确。

                    SizedBox(height: 8.h),
                    
                    // 账单详情
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Text(
                          '账单编号:${bill['id']}',
                          style: TextStyle(
                            fontSize: 12.sp,
                            color: Colors.grey[600],
                          ),
                        ),
                        Text(
                          '截止日期:${bill['dueDate']}',
                          style: TextStyle(
                            fontSize: 12.sp,
                            color: Colors.grey[600],
                          ),
                        ),
                      ],
                    ),
                  ],
                ),
              ),
            ],
          ),

账单名称下方展示账单编号和截止日期,采用灰色小字,与核心信息区分层级,同时通过MainAxisAlignment.spaceBetween让两个信息分别居左、居右,布局更规整。

          SizedBox(height: 12.h),
          
          // 费用明细
          Container(
            width: double.infinity,
            padding: EdgeInsets.all(12.w),
            decoration: BoxDecoration(
              color: Colors.grey[50],
              borderRadius: BorderRadius.circular(8.r),
            ),

账单头部下方设置间距后,构建费用明细区域;该区域使用浅灰色背景,与白色卡片主体形成对比,同时铺满整行宽度,内边距和圆角设计让区域边界更清晰。

            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  '费用明细',
                  style: TextStyle(
                    fontSize: 12.sp,
                    fontWeight: FontWeight.w500,
                    color: Colors.grey[700],
                  ),
                ),
                SizedBox(height: 8.h),

费用明细区域首先显示“费用明细”标题,字体稍粗,与下方明细项区分;通过SizedBox设置标题与明细项的间距,优化视觉布局。

                // 费用项目
                if (bill['details'] != null)
                  ...bill['details'].map<Widget>((detail) {
                    return Padding(
                      padding: EdgeInsets.only(bottom: 4.h),
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          Text(
                            detail['name'],
                            style: TextStyle(
                              fontSize: 11.sp,
                              color: Colors.grey[600],
                            ),
                          ),
                          Text(
                            '¥${detail['amount']}',
                            style: TextStyle(
                              fontSize: 11.sp,
                              color: Colors.grey[700],
                            ),
                          ),
                        ],
                      ),
                    );
                  }).toList(),

判断账单是否有明细数据,若有则遍历渲染每个费用项;每个费用项包含名称和金额,分别居左、居右排列,使用更小的字体,同时设置底部内边距,让明细项之间有清晰的间隔。

                Divider(height: 12.h),
                
                // 合计金额
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text(
                      '合计金额',
                      style: TextStyle(
                        fontSize: 12.sp,
                        fontWeight: FontWeight.bold,
                        color: Colors.grey[700],
                      ),
                    ),
                    Text(
                      '¥${bill['amount']}',
                      style: TextStyle(
                        fontSize: 14.sp,
                        fontWeight: FontWeight.bold,
                        color: Colors.red,
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

明细项下方添加分割线,分隔明细和合计金额;合计金额使用加粗字体,金额部分用红色突出显示,让用户快速感知需要支付的总金额,符合用户对金额的视觉关注习惯。

账单项组件

  • 显示账单名称、状态、编号、截止日期等信息。
  • 使用 Checkbox 支持账单选择。
  • 费用明细区域展示具体的费用项目和金额。
  • 选中状态用蓝色边框突出显示。
  Color _getStatusColor(String status) {
    switch (status) {
      case 'unpaid':
        return Colors.red;
      case 'paid':
        return Colors.green;
      case 'overdue':
        return Colors.orange;
      default:
        return Colors.grey;
    }
  }

_getStatusColor方法根据账单状态返回对应颜色,未缴为红色、已缴为绿色、逾期为橙色,通过色彩直观区分账单状态,符合用户的视觉认知习惯。

  String _getStatusText(String status) {
    switch (status) {
      case 'unpaid':
        return '未缴';
      case 'paid':
        return '已缴';
      case 'overdue':
        return '逾期';
      default:
        return '未知';
    }
  }

_getStatusText方法将英文状态值转换为中文文本,适配中文用户场景,确保界面文字通俗易懂,降低理解成本。

状态处理方法

  • _getStatusColor 获取状态对应的颜色。
  • _getStatusText 获取状态的中文文本。
  • 未缴(红色)、已缴(绿色)、逾期(橙色)。
  Widget _buildPaymentSection() {
    return Container(
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            color: Colors.grey.withOpacity(0.2),
            blurRadius: 4.r,
            offset: Offset(0, -2.h),
          ),
        ],
      ),

_buildPaymentSection方法构建底部支付区域,容器设置白色背景和顶部阴影,模拟悬浮效果,与上方的账单列表形成视觉区分,同时内边距保证内容不贴边。

      child: Column(
        children: [
          // 合计信息
          Container(
            width: double.infinity,
            padding: EdgeInsets.all(16.w),
            decoration: BoxDecoration(
              color: Colors.grey[50],
              borderRadius: BorderRadius.circular(8.r),
            ),

支付区域内部首先是合计信息模块,浅灰色背景搭配圆角,与底部白色背景区分,突出合计信息的重要性。

            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      '已选择 ${_getSelectedCount()} 项',
                      style: TextStyle(
                        fontSize: 12.sp,
                        color: Colors.grey[600],
                      ),
                    ),
                    Text(
                      '合计金额',
                      style: TextStyle(
                        fontSize: 14.sp,
                        fontWeight: FontWeight.bold,
                        color: Colors.grey[700],
                      ),
                    ),
                  ],
                ),

合计信息模块采用Row布局,左侧显示选中项数量和“合计金额”标题,选中项数量用灰色小字,标题加粗,层级分明,让用户清楚知晓选择的账单数量。

                Text(
                  ${_getTotalAmount()}',
                  style: TextStyle(
                    fontSize: 20.sp,
                    fontWeight: FontWeight.bold,
                    color: Colors.red,
                  ),
                ),
              ],
            ),
          ),

右侧显示合计金额,使用大号加粗红色字体,成为视觉焦点,让用户快速看到需要支付的总金额,符合金额展示的设计规范。

          SizedBox(height: 16.h),
          
          // 支付方式选择
          Text(
            '选择支付方式',
            style: TextStyle(
              fontSize: 14.sp,
              fontWeight: FontWeight.w500,
            ),
          ),
          SizedBox(height: 12.h),

合计信息下方设置间距后,显示“选择支付方式”标题,字体稍粗,与下方支付方式选项区分,同时通过SizedBox设置标题与选项的间距。

          Row(
            children: [
              _buildPaymentMethod('wechat', '微信支付', Icons.wechat),
              SizedBox(width: 12.w),
              _buildPaymentMethod('alipay', '支付宝', Icons.account_balance_wallet),
              SizedBox(width: 12.w),
              _buildPaymentMethod('card', '银行卡', Icons.credit_card),
            ],
          ),

采用Row布局横向排列三种支付方式,分别是微信支付、支付宝、银行卡,通过_buildPaymentMethod方法构建统一样式的支付方式选项,选项之间设置间距,布局更均匀。

          SizedBox(height: 16.h),
          
          // 支付按钮
          SizedBox(
            width: double.infinity,
            child: ElevatedButton(
              onPressed: _getSelectedCount() > 0 && !_isProcessing 
                  ? _processPayment 
                  : null,

支付方式下方设置间距后,构建确认支付按钮,按钮宽度铺满整行,点击回调通过判断选中账单数量和支付状态,只有选中账单且非处理中时才触发支付逻辑,避免无效点击。

              style: ElevatedButton.styleFrom(
                backgroundColor: Colors.blue,
                foregroundColor: Colors.white,
                padding: EdgeInsets.symmetric(vertical: 16.h),
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(8.r),
                ),
              ),

按钮样式设置为蓝色背景、白色文字,垂直内边距保证按钮点击区域足够大,圆角设计符合移动端按钮风格,提升点击体验。

              child: _isProcessing
                  ? Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        SizedBox(
                          width: 20.w,
                          height: 20.w,
                          child: CircularProgressIndicator(
                            strokeWidth: 2.w,
                            valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
                          ),
                        ),
                        SizedBox(width: 12.w),
                        const Text('处理中...'),
                      ],
                    )

支付处理中时,按钮显示加载动画和“处理中…”文本,替换原有文字,告知用户当前状态,避免重复点击;加载动画为白色细圆环,与蓝色按钮背景对比清晰。

                  : const Text(
                      '确认支付',
                      style: TextStyle(fontSize: 16.sp),
                    ),
            ),
          ),
        ],
      ),
    );
  }

非处理状态下,按钮显示“确认支付”文字,字体大小适中,清晰易读,引导用户完成支付操作。

支付区域设计

  • 显示选中账单数量和合计金额。
  • 提供三种支付方式:微信支付、支付宝、银行卡。
  • 支付按钮在无选中账单或处理中时禁用。
  • 处理过程中显示进度器,防止重复操作。
  Widget _buildPaymentMethod(String value, String label, IconData icon) {
    final isSelected = _paymentMethod == value;
    
    return GestureDetector(
      onTap: () {
        setState(() {
          _paymentMethod = value;
        });
      },

_buildPaymentMethod方法构建统一的支付方式选项,接收值、标签、图标参数;通过GestureDetector实现点击选中,点击时更新_paymentMethod状态,切换选中的支付方式。

      child: Container(
        padding: EdgeInsets.all(12.w),
        decoration: BoxDecoration(
          color: isSelected ? Colors.blue[50] : Colors.grey[50],
          borderRadius: BorderRadius.circular(8.r),
          border: Border.all(
            color: isSelected ? Colors.blue : Colors.grey[300]!,
            width: isSelected ? 2.w : 1.w,
          ),
        ),

支付方式选项容器设置内边距和圆角,选中状态下使用浅蓝色背景和蓝色粗边框,未选中时为浅灰色背景和灰色细边框,视觉上清晰区分选中状态。

        child: Column(
          children: [
            Icon(
              icon,
              color: isSelected ? Colors.blue : Colors.grey[600],
              size: 24.sp,
            ),
            SizedBox(height: 4.h),
            Text(
              label,
              style: TextStyle(
                fontSize: 10.sp,
                color: isSelected ? Colors.blue : Colors.grey[600],
              ),
            ),
          ],
        ),
      ),
    );
  }

选项内部显示图标和标签,图标颜色与边框颜色对应,选中时为蓝色,未选中时为灰色,标签字体较小,整体样式简洁统一,符合支付方式选项的设计风格。

支付方式组件

  • 使用 GestureDetector 支持点击选择。
  • 选中状态用蓝色背景和边框突出显示。
  • 显示支付方式图标和标签。
  • 三种支付方式水平排列,便于选择。
  int _getSelectedCount() {
    return _selectedBills.where((selected) => selected).length;
  }

_getSelectedCount方法通过遍历_selectedBills列表,统计选中状态为true的数量,返回选中的账单数量,用于合计信息区域展示。

  double _getTotalAmount() {
    double total = 0.0;
    for (int i = 0; i < widget.bills.length; i++) {
      if (_selectedBills[i]) {
        total += double.parse(widget.bills[i]['amount'].toString());
      }
    }
    return total;
  }

_getTotalAmount方法遍历账单列表,累加选中账单的金额,实现实时计算选中账单的总金额,确保金额计算的准确性。

  Future<void> _processPayment() async {
    setState(() {
      _isProcessing = true;
    });

    try {
      // 模拟支付处理
      await Future.delayed(const Duration(seconds: 3));

_processPayment方法处理支付逻辑,首先将_isProcessing设为true,标记支付中状态;通过Future.delayed模拟3秒的支付接口请求过程,模拟真实的网络请求耗时。

      
      final paymentData = {
        'bills': widget.bills.where((bill) {
          final index = widget.bills.indexOf(bill);
          return _selectedBills[index];
        }).toList(),
        'paymentMethod': _paymentMethod,
        'totalAmount': _getTotalAmount(),
        'paymentTime': DateTime.now().toIso8601String(),
      };

模拟支付成功后,构建支付数据对象,包含选中的账单、支付方式、总金额、支付时间等信息,便于后续接口调用或数据存储。

      
      // 实际项目中这里会调用支付接口
      print('支付数据:$paymentData');
      
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('支付成功'),
          backgroundColor: Colors.green,
        ),
      );
      
      Navigator.pop(context, paymentData);

打印支付数据便于调试,然后显示绿色的“支付成功”提示条,告知用户支付结果;最后返回上一级页面,并携带支付数据,方便上级页面处理后续逻辑。

      
    } catch (e) {
      setState(() {
        _isProcessing = false;
      });
      
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('支付失败,请重试'),
          backgroundColor: Colors.red,
        ),
      );
    }
  }
}

捕获支付过程中的异常,将_isProcessing重置为false,恢复按钮可点击状态;同时显示红色的“支付失败,请重试”提示条,告知用户失败原因,引导用户重新操作。

支付处理逻辑

  • _getSelectedCount 计算选中的账单数量。
  • _getTotalAmount 计算选中账单的总金额。
  • _processPayment 模拟3秒支付处理过程。
  • 支付成功后返回支付数据并显示提示。

用户体验设计

缴费详情页面的用户体验重点:

  1. 信息透明:清晰展示账单明细和费用构成
  2. 选择灵活:支持多账单选择和实时计算
  3. 支付安全:提供多种支付方式和处理状态
  4. 操作便捷:底部固定支付区域,便于操作

这些设计让缴费流程既安全又便捷。

数据管理策略

缴费详情的数据管理考虑:

  1. 状态管理:账单选中状态和支付状态
  2. 金额计算:实时计算选中账单的总金额
  3. 支付集成:支持多种支付方式的集成
  4. 状态同步:支付成功后同步更新账单状态

这些策略确保缴费数据的准确性和一致性。


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

Logo

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

更多推荐