Flutter for OpenHarmony 商城App实战 - 购物车实现

购物车是电商应用的核心功能,用户在这里管理选择的商品、调整数量、计算总价。一个好的购物车实现需要提供流畅的交互体验、准确的价格计算和清晰的商品展示。本文将详细讲解如何在 Flutter for OpenHarmony 项目中实现一个功能完整的购物车页面,包括商品列表展示、数量调整、价格计算和清空购物车等功能。

通用卡片组件

在购物车中,我们使用 ShopCard 组件来展示商品和总价信息。这是一个可复用的卡片组件,提供了统一的样式。

dart 复制代码
class ShopCard extends StatelessWidget {
  const ShopCard({
    super.key,
    required this.child,      // 卡片内容
    this.padding,             // 内边距(可选)
  });

  final Widget child;
  final EdgeInsetsGeometry? padding;

  @override
  Widget build(BuildContext context) {
    // 使用提供的内边距,或使用默认值
    final p = padding ?? const EdgeInsets.all(12);

    return Card(
      // 不显示阴影,使用边框代替
      elevation: 0,
      shape: RoundedRectangleBorder(
        // 圆角边框
        borderRadius: BorderRadius.circular(14),
        // 使用主题的分割线颜色作为边框颜色
        side: BorderSide(color: Theme.of(context).dividerColor),
      ),
      // 添加内边距
      child: Padding(padding: p, child: child),
    );
  }
}

这个卡片组件展示了如何创建可复用的UI组件:

设计特点:

  • 使用 Card 作为基础,提供Material Design风格
  • elevation: 0 移除阴影,使用边框代替,更加简洁
  • 圆角边框 borderRadius: BorderRadius.circular(14) 提供现代感
  • 使用主题的分割线颜色保持风格一致

灵活性:

  • 内边距可选,允许调用者自定义
  • 默认内边距为12,适合大多数场景
  • 通过 child 参数接收任意内容

通用按钮组件

ShopButton 是一个通用的按钮组件,支持主按钮和次按钮两种样式。

dart 复制代码
class ShopButton extends StatelessWidget {
  const ShopButton({
    super.key,
    required this.label,      // 按钮文字
    required this.onPressed,  // 点击回调
    this.icon,                // 按钮图标(可选)
    this.isPrimary = true,    // 是否为主按钮
  });

  final String label;
  final VoidCallback? onPressed;
  final IconData? icon;
  final bool isPrimary;

  @override
  Widget build(BuildContext context) {
    // 获取主题的颜色方案
    final colorScheme = Theme.of(context).colorScheme;

    // 根据按钮类型选择样式
    final ButtonStyle style = isPrimary
        ? ElevatedButton.styleFrom(
            // 主按钮:使用主色作为背景
            backgroundColor: colorScheme.primary,
            foregroundColor: colorScheme.onPrimary,
            padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
          )
        : OutlinedButton.styleFrom(
            // 次按钮:使用主色作为边框和文字颜色
            foregroundColor: colorScheme.primary,
            padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
            shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
          );

    // 构建按钮内容(图标+文字)
    final Widget child = Row(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        // 如果提供了图标,则显示图标
        if (icon != null) ...<Widget>[
          Icon(icon, size: 18),
          const SizedBox(width: 8),
        ],
        // 显示按钮文字
        Text(label),
      ],
    );

    // 根据按钮类型返回不同的按钮
    if (isPrimary) {
      return ElevatedButton(onPressed: onPressed, style: style, child: child);
    }
    return OutlinedButton(onPressed: onPressed, style: style, child: child);
  }
}

这个按钮组件展示了如何创建灵活的按钮组件:

样式管理:

  • 主按钮使用 ElevatedButton,背景色为主色
  • 次按钮使用 OutlinedButton,只有边框和文字
  • 两种样式都使用圆角,保持一致性

内容组合:

  • 支持图标和文字的组合
  • 使用 RowmainAxisSize: MainAxisSize.min 实现紧凑布局
  • 图标和文字之间有适当的间距

主题集成:

  • 使用 Theme.of(context).colorScheme 获取主题颜色
  • 确保按钮与应用风格一致
  • 支持深色模式和浅色模式

购物车页面

CartPage 是购物车的主页面,负责显示购物车中的商品、处理数量调整和计算总价。

dart 复制代码
class CartPage extends StatelessWidget {
  const CartPage({
    super.key,
    required this.currency,         // 当前货币
    required this.usdToCurrencyRate, // 汇率
  });

  final String currency;
  final double usdToCurrencyRate;

  @override
  Widget build(BuildContext context) {
    // 获取购物车实例
    final cart = CartScope.of(context);

    return Scaffold(
      appBar: AppBar(title: const Text('购物车')),
      body: AnimatedBuilder(
        // 监听购物车的变化
        animation: cart,
        builder: (context, _) {
          // 获取购物车中的所有商品
          final items = cart.items;
          
          // 如果购物车为空,显示提示信息
          if (items.isEmpty) {
            return const Center(child: Text('购物车是空的'));
          }

          // 显示购物车列表
          return ListView(
            padding: const EdgeInsets.all(12),
            children: <Widget>[
              // 显示每个购物车项
              ...items.map((item) {
                return Padding(
                  padding: const EdgeInsets.only(bottom: 10),
                  child: ShopCard(
                    child: Row(
                      children: <Widget>[
                        // 商品图片
                        ClipRRect(
                          borderRadius: BorderRadius.circular(10),
                          child: Image.network(
                            item.product.imageUrl,
                            width: 54,
                            height: 54,
                            fit: BoxFit.contain,
                            // 图片加载失败时显示空白区域
                            errorBuilder: (_, __, ___) => const SizedBox(
                              width: 54,
                              height: 54,
                            ),
                          ),
                        ),
                        const SizedBox(width: 12),
                        
                        // 商品信息
                        Expanded(
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: <Widget>[
                              // 商品标题
                              Text(
                                item.product.title,
                                maxLines: 2,
                                overflow: TextOverflow.ellipsis,
                                style: Theme.of(context).textTheme.titleSmall,
                              ),
                              const SizedBox(height: 8),
                              
                              // 价格和数量
                              Row(
                                children: <Widget>[
                                  // 显示小计价格
                                  PriceText(
                                    amount: item.subtotalUsd * usdToCurrencyRate,
                                    currency: currency,
                                  ),
                                  const SizedBox(width: 10),
                                  // 显示数量
                                  Text('x${item.quantity}'),
                                ],
                              ),
                            ],
                          ),
                        ),
                        const SizedBox(width: 10),
                        
                        // 数量调整按钮
                        Column(
                          children: <Widget>[
                            // 增加按钮
                            IconButton(
                              tooltip: '增加',
                              onPressed: () => cart.add(item.product),
                              icon: const Icon(Icons.add),
                            ),
                            // 减少按钮
                            IconButton(
                              tooltip: '减少',
                              onPressed: () => cart.removeOne(item.product.id),
                              icon: const Icon(Icons.remove),
                            ),
                          ],
                        ),
                      ],
                    ),
                  ),
                );
              }),
              const SizedBox(height: 6),
              
              // 总价和清空按钮
              ShopCard(
                child: Row(
                  children: <Widget>[
                    Expanded(
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: <Widget>[
                          // 商品总数
                          Text('共 ${cart.totalItems} 件商品'),
                          const SizedBox(height: 6),
                          // 总价
                          PriceText(
                            amount: cart.totalUsd * usdToCurrencyRate,
                            currency: currency,
                          ),
                        ],
                      ),
                    ),
                    // 清空购物车按钮
                    ShopButton(
                      label: '清空',
                      isPrimary: false,
                      icon: Icons.delete_outline,
                      onPressed: cart.clear,
                    ),
                  ],
                ),
              ),
            ],
          );
        },
      ),
    );
  }
}

这段代码展示了完整的购物车页面实现:

状态监听:

  • 使用 AnimatedBuilder 监听购物车的变化
  • 当购物车中的商品改变时,页面自动更新
  • 无需手动调用 setState

空状态处理:

  • 当购物车为空时显示友好的提示信息
  • 提示用户购物车是空的
  • 改善用户体验

商品列表展示:

  • 使用 ListView 显示购物车中的所有商品
  • 每个商品显示图片、标题、价格和数量
  • 使用 map 方法遍历商品列表

数量调整:

  • 提供增加和减少按钮
  • 点击增加按钮调用 cart.add()
  • 点击减少按钮调用 cart.removeOne()
  • 实时更新购物车状态

总价计算:

  • 显示购物车中的总商品数量
  • 显示购物车的总价(转换为用户选择的货币)
  • 提供清空购物车按钮

购物车项的布局设计

购物车中的每个商品项需要清晰地展示商品信息和操作按钮。

dart 复制代码
// 购物车项的布局结构
ShopCard(
  child: Row(
    children: <Widget>[
      // 左侧:商品图片
      ClipRRect(
        borderRadius: BorderRadius.circular(10),
        child: Image.network(
          item.product.imageUrl,
          width: 54,
          height: 54,
          fit: BoxFit.contain,
        ),
      ),
      const SizedBox(width: 12),
      
      // 中间:商品信息(可扩展)
      Expanded(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(item.product.title),
            const SizedBox(height: 8),
            Row(
              children: <Widget>[
                PriceText(
                  amount: item.subtotalUsd * usdToCurrencyRate,
                  currency: currency,
                ),
                const SizedBox(width: 10),
                Text('x${item.quantity}'),
              ],
            ),
          ],
        ),
      ),
      const SizedBox(width: 10),
      
      // 右侧:数量调整按钮
      Column(
        children: <Widget>[
          IconButton(
            onPressed: () => cart.add(item.product),
            icon: const Icon(Icons.add),
          ),
          IconButton(
            onPressed: () => cart.removeOne(item.product.id),
            icon: const Icon(Icons.remove),
          ),
        ],
      ),
    ],
  ),
)

这个布局设计展示了如何组织购物车项的内容:

布局结构:

  • 使用 Row 实现水平布局
  • 左侧是商品图片,中间是商品信息,右侧是操作按钮
  • 使用 Expanded 让商品信息占据剩余空间

信息展示:

  • 商品图片使用 ClipRRect 实现圆角
  • 商品标题显示商品名称
  • 价格显示该商品的小计
  • 数量显示购物车中该商品的数量

操作按钮:

  • 增加和减少按钮垂直排列
  • 点击增加按钮增加商品数量
  • 点击减少按钮减少商品数量

总价计算与显示

购物车的总价需要根据用户选择的货币进行转换。

dart 复制代码
// 总价卡片
ShopCard(
  child: Row(
    children: <Widget>[
      Expanded(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            // 显示商品总数
            Text('共 ${cart.totalItems} 件商品'),
            const SizedBox(height: 6),
            // 显示总价(转换为用户选择的货币)
            PriceText(
              amount: cart.totalUsd * usdToCurrencyRate,
              currency: currency,
            ),
          ],
        ),
      ),
      // 清空购物车按钮
      ShopButton(
        label: '清空',
        isPrimary: false,
        icon: Icons.delete_outline,
        onPressed: cart.clear,
      ),
    ],
  ),
)

这段代码展示了如何显示购物车的总价:

价格计算:

  • cart.totalUsd 获取美元总价
  • 乘以 usdToCurrencyRate 转换为用户选择的货币
  • 使用 PriceText 组件格式化显示

信息展示:

  • 显示购物车中的总商品数量
  • 显示转换后的总价
  • 提供清空购物车的快捷方式

用户交互:

  • 清空按钮使用次按钮样式(isPrimary: false
  • 点击清空按钮调用 cart.clear()
  • 清空后购物车为空,显示空状态提示

多货币价格转换

购物车需要支持多种货币的价格显示。

dart 复制代码
// 在购物车中进行价格转换
// 商品小计
PriceText(
  amount: item.subtotalUsd * usdToCurrencyRate,
  currency: currency,
)

// 购物车总价
PriceText(
  amount: cart.totalUsd * usdToCurrencyRate,
  currency: currency,
)

这个转换过程展示了如何在购物车中应用多货币支持:

转换公式:

  • 后端返回的是美元价格
  • 乘以汇率得到本地货币价格
  • 使用 PriceText 组件显示

一致性保证:

  • 所有价格显示都使用相同的转换公式
  • 确保购物车中的价格与商品列表一致
  • 用户看到的价格始终准确

总结

购物车的实现涉及多个重要的技术点。首先是通用组件的设计,包括卡片组件和按钮组件,提供了可复用的UI元素。其次是购物车页面的实现,使用 AnimatedBuilder 监听购物车变化,提供流畅的交互体验。再次是购物车项的布局设计,清晰地展示商品信息和操作按钮。最后是多货币价格转换,确保价格显示的准确性。

这种设计确保了购物车的功能完整性和用户体验的流畅性。用户可以轻松查看购物车中的商品、调整数量、计算总价,整个流程自然而直观。


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

相关推荐
遇印记2 小时前
蓝桥java求最大公约数
java·开发语言
ONExiaobaijs2 小时前
【无标题】
java·开发语言·spring·maven·程序员创富
符哥20082 小时前
Mybatis和Mybatis-plus区别
java·开发语言·mybatis
lkbhua莱克瓦242 小时前
JavaWeb技术概述
java·javaweb·web
爬山算法2 小时前
Hibernate(46) Hibernate的配置文件如何加载?
java·后端·hibernate
风景的人生2 小时前
springboot项目用maven插件打包时候报错
java·spring boot·maven
二哈喇子!2 小时前
基于SSM框架的网上商城购物系统的设计与实现(开源项目——实现CRUD功能整体流程超详细)
java·spring·mybatis·ssm
容沁风2 小时前
pycharm启动报错incompatible with Text-specific LCD
java·pycharm
馨谙2 小时前
面试题----用户,组,su,su-,sudo,sudo-,nologin shell
java·前端·数据库