【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

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

相关推荐
Zsnoin能1 小时前
Flutter仿ios液态玻璃效果
flutter
傅里叶5 小时前
iOS相机权限获取
flutter·ios
Haha_bj6 小时前
Flutter—— 本地存储(shared_preferences)
flutter
心之语歌7 小时前
Flutter 存储权限:适配主流系统
flutter
Bigger7 小时前
为什么你的 Git 提交需要签名?—— Git Commit Signing 完全指南
git·开源·github
恋猫de小郭7 小时前
Android 官方正式官宣 AI 支持 AppFunctions ,Android 官方 MCP 和系统级 OpenClaw 雏形
android·前端·flutter
在人间耕耘1 天前
HarmonyOS Vision Kit 视觉AI实战:把官方 Demo 改造成一套能长期复用的组件库
人工智能·深度学习·harmonyos
MakeZero1 天前
Flutter那些事-布局篇
flutter
王码码20351 天前
Flutter for OpenHarmony:socket_io_client 实时通信的事实标准(Node.js 后端的最佳拍档) 深度解析与鸿蒙适配指南
android·flutter·ui·华为·node.js·harmonyos
zhangkai1 天前
flutter存储知识点总结
flutter·ios