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

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

- Flutter电商实战:从零开发商品详情页面(含轮播图点击跳转完整实现)
前言
在开发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,只使用 left 和 right 属性控制边距:
dart
// 修复后的代码
Widget _getSearch() {
return Positioned(
top: 10,
left: 10, // ✅ 直接使用left和right控制边距
right: 10,
child: Container(...), // 移除外层Padding
);
}
修复效果:
- 搜索框只占据顶部约70px的区域
- 点击轮播图的其他区域可以正常跳转
问题2:图片加载失败处理
问题描述 :
网络图片加载失败时显示不友好。
解决方法 :
使用 Image.network 的 errorBuilder 参数:
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电商商品详情页面。主要完成以下工作:
- 数据模型设计:参考水果数据结构,设计商品详情数据模型
- 页面UI开发:使用卡片式布局,展示商品完整信息
- 点击跳转联调:从轮播图点击跳转到商品详情页
- 问题修复:解决Stack布局中点击事件被拦截的问题
关键点总结:
- 使用
GridView.builder实现参数网格展示 - 使用
Positioned组件在Stack中精确定位子元素 - 注意
Padding的使用,避免意外覆盖其他组件 - 使用
errorBuilder处理图片加载失败情况
八、参考资料
如果本文对你有帮助,欢迎点赞、收藏、评论!
📕个人领域 :Linux/C++/java/AI🚀 个人主页 :有点流鼻涕 · CSDN
💬 座右铭 : "向光而行,沐光而生。"
