在这里插入图片描述

引言

钱包功能是用户资金管理的核心模块,支持充值、消费、提现等操作。本篇将实现一个功能完善的钱包页面。

数据模型定义

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

/// 交易记录模型
class TransactionRecord {
  final String id;
  final String type;
  final double amount;
  final String description;
  final DateTime time;
  final TransactionStatus status;

  TransactionRecord({
    required this.id,
    required this.type,
    required this.amount,
    required this.description,
    required this.time,
    required this.status,
  });
}

  double get totalAmount => amount + bonus;

TransactionRecord类定义了交易记录的基本属性,包括唯一标识id、交易类型type、金额amount、描述description、时间time和状态status。TransactionStatus枚举定义了三种交易状态:成功、待处理和失败。RechargeOption类定义了充值选项,包含充值金额amount、赠送金额bonus和是否热门标记isHot。totalAmount计算属性返回充值后的总金额。这种数据模型设计使代码结构清晰,易于维护和扩展。

钱包控制器实现

class WalletController extends GetxController {
  final balance = 168.00.obs;
  final frozenAmount = 20.00.obs;
  final points = 1580.obs;
  final records = <TransactionRecord>[].obs;
  final isLoading = false.obs;
  final selectedRechargeAmount = 0.0.obs;
  
  final rechargeOptions = <RechargeOption>[
    RechargeOption(amount: 50, bonus: 0),
    RechargeOption(amount: 100, bonus: 5, isHot: true),
    RechargeOption(amount: 200, bonus: 15),
    RechargeOption(amount: 500, bonus: 50),
    RechargeOption(amount: 1000, bonus: 120),
  ];

  
  void onInit() {
    super.onInit();
    loadTransactionRecords();
  }
  
  Future<void> loadTransactionRecords() async {
    isLoading.value = true;
    await Future.delayed(const Duration(milliseconds: 800));
    
    records.value = [
      TransactionRecord(
        id: '1',
        type: '充值',
        amount: 100.00,
        description: '余额充值',
        time: DateTime.now().subtract(const Duration(days: 1)),
        status: TransactionStatus.success,
      ),
      TransactionRecord(
        id: '2',
        type: '消费',
        amount: -88.00,
        description: '《年轮》剧本杀组队',
        time: DateTime.now().subtract(const Duration(days: 2)),
        status: TransactionStatus.success,
      ),
    ];
    
    isLoading.value = false;
  }

WalletController使用GetX的响应式变量obs来管理状态。balance存储可用余额,frozenAmount存储冻结金额,points存储积分余额。records列表存储交易记录,isLoading标记加载状态。rechargeOptions定义了多个充值档位,部分档位有赠送金额。onInit方法在控制器初始化时自动加载交易记录。loadTransactionRecords方法模拟从服务器获取交易记录,实际项目中应该调用API接口。这种设计使状态管理清晰,数据与UI自动同步。

  Future<void> recharge(double amount) async {
    if (amount <= 0) {
      Get.snackbar('提示', '请选择充值金额');
      return;
    }
    
    Get.dialog(
      const Center(child: CircularProgressIndicator(color: Color(0xFF6B4EFF))),
      barrierDismissible: false,
    );
    
    await Future.delayed(const Duration(seconds: 2));
    Get.back();
    
    final option = rechargeOptions.firstWhereOrNull((o) => o.amount == amount);
    final bonus = option?.bonus ?? 0;
    
    balance.value += amount + bonus;
    records.insert(0, TransactionRecord(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      type: '充值',
      amount: amount + bonus,
      description: bonus > 0 ? '余额充值(含赠送¥$bonus)' : '余额充值',
      time: DateTime.now(),
      status: TransactionStatus.success,
    ));
    
    Get.snackbar('充值成功', '已到账¥${amount + bonus}');
  }

recharge方法处理充值逻辑,首先验证金额是否有效,然后显示加载对话框模拟网络请求。充值成功后根据选择的金额查找对应的赠送金额,更新账户余额,并在交易记录列表头部插入新记录。如果有赠送金额,描述中会标注赠送部分。最后显示成功提示。这种设计确保了充值流程的完整性和用户反馈的及时性。

  Future<void> withdraw(double amount) async {
    if (amount <= 0 || amount > balance.value || amount < 10) {
      Get.snackbar('提示', '请输入有效的提现金额');
      return;
    }
    
    Get.dialog(
      const Center(child: CircularProgressIndicator(color: Color(0xFF6B4EFF))),
      barrierDismissible: false,
    );
    
    await Future.delayed(const Duration(seconds: 2));
    Get.back();
    
    balance.value -= amount;
    records.insert(0, TransactionRecord(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      type: '提现',
      amount: -amount,
      description: '提现到微信',
      time: DateTime.now(),
      status: TransactionStatus.pending,
    ));
    
    Get.snackbar('提现申请已提交', '预计1-3个工作日到账');
  }
  
  double get availableBalance => balance.value - frozenAmount.value;
  
  String formatAmount(double amount) {
    return amount >= 0 ? '+${amount.toStringAsFixed(2)}' : amount.toStringAsFixed(2);
  }
}

withdraw方法处理提现逻辑,包含多重验证:金额必须大于0、不能超过余额、最低提现10元。这些验证确保提现操作的合法性。提现成功后扣减余额,添加状态为pending的交易记录。提现通常需要人工审核,所以状态不是立即成功。availableBalance计算属性返回可用余额,formatAmount方法格式化金额显示。这种设计提高了代码的健壮性和用户体验。

钱包页面UI实现

class WalletPage extends StatelessWidget {
  WalletPage({super.key});
  
  final WalletController controller = Get.put(WalletController());

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF5F5F5),
      appBar: AppBar(
        title: const Text('我的钱包'),
        backgroundColor: const Color(0xFF6B4EFF),
        foregroundColor: Colors.white,
        elevation: 0,
      ),
      body: RefreshIndicator(
        onRefresh: () => controller.loadTransactionRecords(),
        color: const Color(0xFF6B4EFF),
        child: SingleChildScrollView(
          physics: const AlwaysScrollableScrollPhysics(),
          child: Column(
            children: [
              _buildBalanceCard(),
              const SizedBox(height: 16),
              _buildQuickActions(),
              const SizedBox(height: 16),
              _buildRecordSection(),
            ],
          ),
        ),
      ),
    );
  }

WalletPage使用GetX的Get.put注入控制器。页面主体使用RefreshIndicator支持下拉刷新,SingleChildScrollView包裹内容实现滚动。AlwaysScrollableScrollPhysics确保即使内容不足也能触发下拉刷新。页面包含余额卡片、快捷操作和交易记录三个部分。这种结构清晰,用户能快速了解账户状态并进行相关操作。

  Widget _buildBalanceCard() {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(24),
      decoration: const BoxDecoration(
        gradient: LinearGradient(
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
          colors: [Color(0xFF6B4EFF), Color(0xFF9D4EDD)],
        ),
        borderRadius: BorderRadius.only(
          bottomLeft: Radius.circular(24),
          bottomRight: Radius.circular(24),
        ),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Text('账户余额(元)', style: TextStyle(color: Colors.white70, fontSize: 14)),
          const SizedBox(height: 12),
          Obx(() => Text(
            ${controller.balance.value.toStringAsFixed(2)}',
            style: const TextStyle(
              color: Colors.white,
              fontSize: 40,
              fontWeight: FontWeight.bold,
            ),
          )),
          const SizedBox(height: 8),
          Obx(() => Row(
            children: [
              Text('可用 ¥${controller.availableBalance.toStringAsFixed(2)}', 
                style: const TextStyle(color: Colors.white70, fontSize: 12)),
              const SizedBox(width: 16),
              Text('冻结 ¥${controller.frozenAmount.value.toStringAsFixed(2)}',
                style: const TextStyle(color: Colors.white70, fontSize: 12)),
            ],
          )),
        ],
      ),
    );
  }

余额卡片使用渐变背景,从紫色过渡到浅紫色,底部圆角设计与页面背景形成自然过渡。余额数字使用大号加粗白色字体,是卡片的视觉焦点。下方显示可用余额和冻结金额,帮助用户了解资金状态。Obx包裹响应式变量,当数据变化时自动更新UI。这种设计既美观又实用,提升了用户体验。

  Widget _buildQuickActions() {
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 16),
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)],
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: [
          _buildActionButton(Icons.add_circle_outline, '充值', () => _showRechargeSheet()),
          _buildActionButton(Icons.account_balance_wallet_outlined, '提现', () => _showWithdrawSheet()),
          _buildActionButton(Icons.credit_card, '银行卡', () => Get.snackbar('提示', '功能开发中')),
          _buildActionButton(Icons.help_outline, '帮助', () => Get.snackbar('提示', '功能开发中')),
        ],
      ),
    );
  }
  
  Widget _buildActionButton(IconData icon, String label, VoidCallback onTap) {
    return GestureDetector(
      onTap: onTap,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Container(
            width: 50,
            height: 50,
            decoration: BoxDecoration(
              color: const Color(0xFF6B4EFF).withOpacity(0.1),
              borderRadius: BorderRadius.circular(25),
            ),
            child: Icon(icon, color: const Color(0xFF6B4EFF), size: 24),
          ),
          const SizedBox(height: 8),
          Text(label, style: const TextStyle(fontSize: 12, color: Colors.black87)),
        ],
      ),
    );
  }

快捷操作区域使用白色卡片,添加轻微阴影提升层次感。按钮水平排列,使用spaceAround均匀分布。充值和提现是最常用的操作,放在显眼位置。_buildActionButton是可复用的按钮组件,包含圆形图标背景和文字标签。图标使用浅紫色背景和紫色图标,与主题色保持一致。这种设计既美观又易于使用,提升了操作效率。

  Widget _buildRecordSection() {
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)],
      ),
      child: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text('交易记录', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
                GestureDetector(
                  onTap: () => _showAllRecords(),
                  child: const Text('全部 >', style: TextStyle(color: Colors.grey, fontSize: 14)),
                ),
              ],
            ),
          ),
          const Divider(height: 1),
          Obx(() {
            if (controller.isLoading.value) {
              return const Padding(
                padding: EdgeInsets.all(40),
                child: CircularProgressIndicator(color: Color(0xFF6B4EFF)),
              );
            }
            
            if (controller.records.isEmpty) {
              return const Padding(
                padding: EdgeInsets.all(40),
                child: Text('暂无交易记录', style: TextStyle(color: Colors.grey)),
              );
            }
            
            return ListView.separated(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              itemCount: controller.records.length > 5 ? 5 : controller.records.length,
              separatorBuilder: (_, __) => const Divider(height: 1),
              itemBuilder: (context, index) => _buildRecordItem(controller.records[index]),
            );
          }),
        ],
      ),
    );
  }

交易记录区域顶部显示标题和"全部"入口,方便用户查看完整的交易历史。使用白色卡片背景,与其他区域保持视觉一致性。使用Obx监听加载状态和记录列表变化。加载中显示进度指示器,无记录时显示空状态提示。交易记录列表最多显示5条,更多记录需要点击"全部"查看。这种设计既节省空间又提供了完整的功能。

  Widget _buildRecordItem(TransactionRecord record) {
    final isPositive = record.amount >= 0;
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Row(
            child: Icon(
              isPositive ? Icons.add_circle : Icons.remove_circle,
              color: isPositive ? Colors.green : Colors.red,
              size: 22,
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(record.description, style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14)),
                const SizedBox(height: 4),
                Text(
                  '${record.time.month}${record.time.day}日 ${record.time.hour.toString().padLeft(2, '0')}:${record.time.minute.toString().padLeft(2, '0')}',
                  style: TextStyle(color: Colors.grey[600], fontSize: 12),
                ),
              ],
            ),
          ),         
          ),
  }

交易记录项左侧显示类型图标,收入使用绿色,支出使用红色,直观区分交易方向。中间区域显示交易描述和时间,格式化为"X月X日 HH:mm"的中文格式。右侧显示金额,使用红绿色区分收支。这种设计清晰易读,用户能快速了解交易信息。

充值和提现弹窗

  void _showRechargeSheet() {
    Get.bottomSheet(
      Container(
        padding: const EdgeInsets.all(20),
        decoration: const BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
        ),
            const SizedBox(height: 24),
            SizedBox(
              width: double.infinity,
              height: 50,
              child: Obx(() => ElevatedButton(
                onPressed: controller.selectedRechargeAmount.value > 0
                    ? () {
                        Get.back();
                        controller.recharge(controller.selectedRechargeAmount.value);
                      }
                    : null,
                style: ElevatedButton.styleFrom(
                  backgroundColor: const Color(0xFF6B4EFF),
                  disabledBackgroundColor: Colors.grey[300],
                  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
                ),
                child: Text(
                  controller.selectedRechargeAmount.value > 0
                      ? '确认充值 ¥${controller.selectedRechargeAmount.value.toInt()}'
                      : '请选择充值金额',
                  style: const TextStyle(color: Colors.white, fontSize: 16),
                ),
              )),
            ),
        ),
      ),
  }

充值弹窗使用GetX的bottomSheet方法,从底部弹出。充值金额选项使用Wrap布局,每行显示3个选项。选中状态使用紫色边框和浅紫色背景,未选中使用灰色背景。每个选项显示金额,如果有赠送金额则显示"送¥X"的橙色文字。确认按钮根据是否选择金额动态显示文字和状态。未选择时按钮禁用显示"请选择充值金额",选择后显示"确认充值 ¥X"。这种设计既美观又易于使用。

  void _showWithdrawSheet() {
    final amountController = TextEditingController();
    
    Get.bottomSheet(
      Container(
        padding: const EdgeInsets.all(20),
        decoration: const BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
        ),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text('提现', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
                IconButton(icon: const Icon(Icons.close), onPressed: () => Get.back()),
              ],
            ),
            const SizedBox(height: 12),
            Text(
              '最低提现金额10元,预计1-3个工作日到账',
              style: TextStyle(color: Colors.grey[600], fontSize: 12),
            ),
            const SizedBox(height: 24),
            SizedBox(
              width: double.infinity,
              height: 50,
              child: ElevatedButton(
                onPressed: () {
                  final amount = double.tryParse(amountController.text) ?? 0;
                  Get.back();
                  controller.withdraw(amount);
                },
                style: ElevatedButton.styleFrom(
                  backgroundColor: const Color(0xFF6B4EFF),
                  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
                ),
            ),
          ],
        ),
      ),
    );
  }

提现弹窗结构与充值弹窗类似,使用bottomSheet从底部弹出。顶部显示可提现余额,使用浅紫色背景突出显示。金额输入框使用数字键盘,前缀显示人民币符号。右侧"全部提现"按钮方便用户快速填入全部可用余额,提升操作效率。提示文字说明最低提现金额和预计到账时间,让用户了解提现规则。确认提现按钮点击后解析输入金额,关闭弹窗并调用控制器的withdraw方法。_showAllRecords方法跳转到完整的交易记录页面,展示所有历史记录。

总结

本篇实现了一个功能完善的钱包页面,包括余额展示、充值提现、交易记录等功能。通过合理的UI设计和状态管理,提供了良好的用户体验。钱包功能是用户资金管理的核心,设计时需要注重安全性和易用性。清晰的余额展示、便捷的充值提现操作、完整的交易记录,这些都是提升用户体验的关键要素。

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

Logo

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

更多推荐