【HarmonyOS】开源鸿蒙跨平台DAY11:Flutter电商实战:从零开发商品详情页面(含轮播图点击跳转完整实现)

Flutter电商实战:从零开发商品详情页面(含轮播图点击跳转完整实现)


🌸你好呀!我是 lbb小魔仙
🌟 感谢陪伴~ 小白博主在线求友
🌿 跟着小白学Linux/Java/Python
📖 专栏汇总:
《Linux》专栏 | 《Java》专栏 | 《Python》专栏

前言

在开发Flutter电商应用时,商品详情页是核心功能之一。本文将带你从零开始实现一个完整的商品详情页面,参考开源鸿蒙的水果详情页面设计,适配电商场景。

参考文章【开源鸿蒙跨平台开发先锋训练营】DAY15~DAY19为开源鸿蒙跨平台应用全面集成添加核心场景-水果详情页面

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

一、项目背景

本文基于一个Flutter跨平台电商项目,技术栈如下:

  • Flutter版本:3.x
  • 项目结构:底部导航栏 + 多页面(首页、分类、购物车、个人中心)
  • 现有功能:轮播图、分类列表、特惠推荐

二、开发流程

复制代码
需求分析 → 数据模型设计 → API接口封装 → 页面UI开发 → 点击跳转联调 → 问题修复

2.1 需求分析

根据CSDN文章中的水果详情页面设计,我们需要实现:

模块 功能说明
顶部导航栏 返回按钮、标题、分享按钮
商品基本信息 图片、名称、英文名、分类、价格
商品卖点 营养价值/功效描述
商品特性 规格特性列表(卡片展示)
商品参数 网格形式展示参数数据
底部操作栏 客服、收藏、加入购物车、立即购买

2.2 数据模型设计

参考水果详情的数据结构,设计商品详情数据模型:

dart 复制代码
// 商品参数项
class ProductParam {
  final String name;  // 参数名称
  final String value; // 参数值
}

// 商品规格特性
class ProductFeature {
  final String short; // 简短描述
  final String long;  // 详细描述
}

// 商品详情
class ProductDetail {
  final String id;                  // 商品ID
  final String name;                // 商品名称
  final String englishName;         // 英文名称
  final List<ProductParam> params;  // 商品参数列表
  final String category;            // 分类
  final String picture;             // 商品图片
  final String benefits;            // 商品功效/卖点
  final List<String> detailImages;  // 详情图片列表
  final String detailDescription;   // 详细描述
  final String mainColor;           // 主题色
  final String mainBg;              // 背景色
  final List<ProductFeature> features; // 规格特性列表
  final String price;               // 价格
  final String originalPrice;       // 原价
}

三、代码实现

3.1 创建数据模型文件

文件路径lib/viewmodels/product_detail.dart

dart 复制代码
/**
 * 商品详情数据模型
 *
 * 参考CSDN文章: https://blog.csdn.net/qq_33247427/article/details/157554060
 * 来源: 【开源鸿蒙跨平台开发先锋训练营】DAY15~DAY19为开源鸿蒙跨平台应用全面集成添加核心场景-水果详情页面
 */

// 商品参数项
class ProductParam {
  final String name;  // 参数名称
  final String value; // 参数值

  ProductParam({required this.name, required this.value});

  factory ProductParam.fromJSON(List<dynamic> json) {
    return ProductParam(
      name: json[0] ?? '',
      value: json[1] ?? '',
    );
  }
}

// 商品规格特性
class ProductFeature {
  final String short; // 简短描述
  final String long;  // 详细描述

  ProductFeature({required this.short, required this.long});

  factory ProductFeature.fromJSON(Map<String, dynamic> json) {
    return ProductFeature(
      short: json['short'] ?? '',
      long: json['long'] ?? '',
    );
  }
}

// 商品详情
class ProductDetail {
  final String id;                  // 商品ID
  final String name;                // 商品名称
  final String englishName;         // 英文名称
  final List<ProductParam> params;  // 商品参数列表
  final String category;            // 分类
  final String picture;             // 商品图片
  final String benefits;            // 商品功效/卖点
  final List<String> detailImages;  // 详情图片列表
  final String detailDescription;   // 详细描述
  final String mainColor;           // 主题色
  final String mainBg;              // 背景色
  final List<ProductFeature> features; // 规格特性列表
  final String price;               // 价格
  final String originalPrice;       // 原价

  ProductDetail({
    required this.id,
    required this.name,
    required this.englishName,
    required this.params,
    required this.category,
    required this.picture,
    required this.benefits,
    required this.detailImages,
    required this.detailDescription,
    required this.mainColor,
    required this.mainBg,
    required this.features,
    required this.price,
    required this.originalPrice,
  });

  factory ProductDetail.fromJSON(Map<String, dynamic> json) {
    // 解析参数列表
    List<ProductParam> paramList = [];
    if (json['params'] != null) {
      paramList = (json['params'] as List)
          .map((e) => ProductParam.fromJSON(e as List<dynamic>))
          .toList();
    }

    // 解析特性列表
    List<ProductFeature> featureList = [];
    if (json['features'] != null) {
      featureList = (json['features'] as List)
          .map((e) => ProductFeature.fromJSON(e as Map<String, dynamic>))
          .toList();
    }

    // 解析详情图片
    List<String> images = [];
    if (json['detailImages'] != null) {
      images = List<String>.from(json['detailImages']);
    }

    return ProductDetail(
      id: json['id'] ?? '',
      name: json['name'] ?? '',
      englishName: json['englishName'] ?? '',
      params: paramList,
      category: json['category'] ?? '默认分类',
      picture: json['picture'] ?? '',
      benefits: json['benefits'] ?? '',
      detailImages: images,
      detailDescription: json['detailDescription'] ?? '',
      mainColor: json['mainColor'] ?? '#FF6B00',
      mainBg: json['mainBg'] ?? '#FFF3E0',
      features: featureList,
      price: json['price'] ?? '0.00',
      originalPrice: json['originalPrice'] ?? '0.00',
    );
  }

  // 创建模拟数据(用于演示)
  static ProductDetail createMock() {
    return ProductDetail(
      id: '1',
      name: '精选有机红富士苹果',
      englishName: 'Organic Fuji Apple',
      params: [
        ProductParam(name: '产地', value: '陕西烟台'),
        ProductParam(name: '规格', value: '500g/个'),
        ProductParam(name: '保质期', value: '30天'),
        ProductParam(name: '储存方式', value: '冷藏保存'),
        ProductParam(name: '净含量', value: '2.5kg'),
        ProductParam(name: '品牌', value: '果园直供'),
      ],
      category: '新鲜水果',
      picture: 'https://images.unsplash.com/photo-1560806887-1e4cd0b6cbd6?w=400',
      benefits: '富含维生素C、膳食纤维和多种矿物质,口感清脆香甜,是日常健康饮食的理想选择。',
      detailImages: [
        'https://images.unsplash.com/photo-1560806887-1e4cd0b6cbd6?w=400',
        'https://images.unsplash.com/photo-1567306226416-28f0efdc88ce?w=400',
      ],
      detailDescription: '我们的红富士苹果来自优质果园,采用有机种植方式,不使用任何化学农药和化肥。每一颗苹果都经过精心挑选,确保最佳口感和品质。',
      mainColor: '#FF6B00',
      mainBg: '#FFF3E0',
      features: [
        ProductFeature(
          short: '新鲜直达',
          long: '果园直采,从枝头到舌尖不超过48小时,确保新鲜度。',
        ),
        ProductFeature(
          short: '有机认证',
          long: '通过国家有机食品认证,无农药残留,吃得放心。',
        ),
        ProductFeature(
          short: '营养丰富',
          long: '富含维生素C、果胶、膳食纤维等营养元素,有益健康。',
        ),
      ],
      price: '39.90',
      originalPrice: '59.90',
    );
  }
}

3.2 创建API接口文件

文件路径lib/api/product_detail.dart

dart 复制代码
/**
 * 商品详情API接口
 */

import 'package:harmonyos_day_four/utils/DioRequest.dart';
import 'package:harmonyos_day_four/viewmodels/product_detail.dart';

/// 获取商品详情数据
/// 商品详情接口(实际项目中需要替换为真实的接口地址)
Future<ProductDetail> getProductDetailAPI(String productId) async {
  // 模拟API请求 - 实际项目中应该请求真实接口
  // final result = await dioRequest.get('/product/detail/$productId');
  // return ProductDetail.fromJSON(result);

  // 当前返回模拟数据用于演示
  return ProductDetail.createMock();
}

3.3 创建商品详情页面

文件路径lib/pages/product/detail.dart

页面结构:

dart 复制代码
Scaffold
├── AppBar (顶部导航栏)
├── SingleChildScrollView (可滚动内容)
│   ├── 商品基本信息卡片
│   ├── 商品卖点卡片
│   ├── 商品特性卡片
│   ├── 商品参数网格
│   └── 商品详情描述卡片
└── BottomNavigationBar (底部操作栏)

关键代码片段:

dart 复制代码
// 商品基本信息卡片
Widget _buildProductHeader() {
  return Container(
    width: double.infinity,
    padding: const EdgeInsets.all(16),
    decoration: const BoxDecoration(color: Colors.white),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        // 商品图片
        ClipRRect(
          borderRadius: BorderRadius.circular(12),
          child: Image.network(_productDetail!.picture, ...),
        ),
        // 商品名称和价格
        Text(_productDetail!.name, ...),
        // 价格信息(含原价删除线)
        Row(
          children: [
            Text('¥${_productDetail!.price}', style: 主题色),
            Text('¥${_productDetail!.originalPrice}',
                  style: 删除线样式),
          ],
        ),
      ],
    ),
  );
}

// 商品参数网格(2列布局)
GridView.builder(
  shrinkWrap: true,
  physics: const NeverScrollableScrollPhysics(),
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    mainAxisSpacing: 10,
    crossAxisSpacing: 10,
    childAspectRatio: 2.0,
  ),
  itemBuilder: (context, index) {
    return _buildParamCard(_productDetail!.params[index]);
  },
)

3.4 修改轮播图组件,添加点击跳转

文件路径lib/components/Home/HmSlider.dart

dart 复制代码
// 添加导入
import 'package:harmonyos_day_four/pages/product/detail.dart';

// 修改PageView.builder的itemBuilder
itemBuilder: (context, index) {
  return GestureDetector(
    onTap: () {
      // 点击轮播图跳转到商品详情页
      Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) => ProductDetailPage(
            productId: widget.bannerList[index].id,
          ),
        ),
      );
    },
    child: Image.network(
      widget.bannerList[index].imgUrl,
      fit: BoxFit.cover,
      width: screenWidth,
      // ... loadingBuilder 和 errorBuilder
    ),
  );
},

四、遇到的问题及解决方法

问题1:轮播图点击无法跳转到详情页

问题描述

点击轮播图时没有反应,无法跳转到商品详情页面。

问题原因

轮播图使用 Stack 布局,搜索框组件(_getSearch)使用了 Padding(padding: const EdgeInsets.all(10)),导致搜索框覆盖了整个轮播图区域,拦截了点击事件。

dart 复制代码
// 问题代码
Widget _getSearch() {
  return Positioned(
    top: 10,
    left: 0,
    right: 0,
    child: Padding(
      padding: const EdgeInsets.all(10),  // ❌ 这里的Padding导致覆盖
      child: Container(...),
    ),
  );
}

解决方法

移除外层的 Padding,只使用 leftright 属性控制边距:

dart 复制代码
// 修复后的代码
Widget _getSearch() {
  return Positioned(
    top: 10,
    left: 10,   // ✅ 直接使用left和right控制边距
    right: 10,
    child: Container(...),  // 移除外层Padding
  );
}

修复效果

  • 搜索框只占据顶部约70px的区域
  • 点击轮播图的其他区域可以正常跳转

问题2:图片加载失败处理

问题描述

网络图片加载失败时显示不友好。

解决方法

使用 Image.networkerrorBuilder 参数:

dart 复制代码
Image.network(
  _productDetail!.picture,
  errorBuilder: (context, error, stackTrace) {
    return Container(
      decoration: BoxDecoration(
        color: bgColor,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Center(
        child: Icon(Icons.image_not_supported,
                    size: 50, color: mainColor.withOpacity(0.5)),
      ),
    );
  },
)

问题3:主题色动态解析

问题描述

商品的主题色是从服务器返回的十六进制字符串(如 #FF6B00),需要转换为 Color 对象。

解决方法

dart 复制代码
Color get mainColor {
  if (_productDetail == null) return const Color(0xFFFF6B00);
  try {
    return Color(
      int.parse(_productDetail!.mainColor.replaceFirst('#', '0xFF')),
    );
  } catch (e) {
    return const Color(0xFFFF6B00);  // 解析失败使用默认颜色
  }
}

五、页面配色方案

参考水果详情页面的配色,电商详情页采用橙色系:

用途 颜色值 说明
主题色 #FF6B00 橙色,用于标题、价格、按钮
浅橙背景 #FFF3E0 半透明橙色,用于卡片背景
页面背景 #F5F5F5 浅灰色
主文本 #1F2937 深灰色
次要文本 #9CA3AF 中灰色
标签文本 #6B7280 灰色

六、项目结构

复制代码
lib/
├── api/
│   └── product_detail.dart          # 商品详情API
├── components/
│   └── Home/
│       └── HmSlider.dart            # 轮播图组件(已修改)
├── pages/
│   └── product/
│       └── detail.dart              # 商品详情页面
└── viewmodels/
    └── product_detail.dart          # 商品详情数据模型

七、总结

本文通过参考开源鸿蒙的水果详情页面设计,实现了一个完整的Flutter电商商品详情页面。主要完成以下工作:

  1. 数据模型设计:参考水果数据结构,设计商品详情数据模型
  2. 页面UI开发:使用卡片式布局,展示商品完整信息
  3. 点击跳转联调:从轮播图点击跳转到商品详情页
  4. 问题修复:解决Stack布局中点击事件被拦截的问题

关键点总结

  • 使用 GridView.builder 实现参数网格展示
  • 使用 Positioned 组件在 Stack 中精确定位子元素
  • 注意 Padding 的使用,避免意外覆盖其他组件
  • 使用 errorBuilder 处理图片加载失败情况

八、参考资料


如果本文对你有帮助,欢迎点赞、收藏、评论!
📕个人领域 :Linux/C++/java/AI

🚀 个人主页有点流鼻涕 · CSDN

💬 座右铭 : "向光而行,沐光而生。"

相关推荐
GitCode官方2 小时前
开源星期六第五期!开源鸿蒙跨平台三方库适配实战,打通跨端开发
华为·开源·harmonyos
盐焗西兰花2 小时前
鸿蒙学习实战之路-Reader Kit获取目录列表最佳实践
学习·华为·harmonyos
恋猫de小郭3 小时前
小米 HyperOS 4 大变样?核心应用以 Rust / Flutter 重写,不兼容老系统
android·前端·人工智能·flutter·ios
果粒蹬i3 小时前
【HarmonyOS】鸿蒙应用开发实战指南:构建网络数据列表应用
网络·华为·harmonyos
一起养小猫3 小时前
Flutter for OpenHarmony 实战:2048游戏算法与优化深度解析
算法·flutter·游戏
马剑威(威哥爱编程)3 小时前
鸿蒙开发实战:玩转“智感握姿”——新闻列表左右手智能切换
华为·harmonyos·arkts·arkui·鸿蒙6
2601_949857433 小时前
Flutter for OpenHarmony Web开发助手App实战:HTML参考
前端·flutter·html
ITUnicorn3 小时前
【HarmonyOS6】从零实现随机数生成器
华为·harmonyos·arkts·鸿蒙·harmonyos6
Swift社区4 小时前
HarmonyOS 网络请求与数据持久化
华为·harmonyos