
商品卡片是二手交易App中最常见的UI组件,在首页、搜索结果、分类列表等多个地方都会用到。今天我们来详细讲解"闲置换"中商品卡片的实现。
商品卡片的设计思路
一个好的商品卡片应该在有限的空间内展示最重要的信息:商品图片、标题、价格、位置、发布时间。图片占主要空间吸引用户注意,价格用醒目的颜色突出显示,辅助信息用小字不抢风头。
完整代码实现
dart
Widget _buildProductCard(Map<String, dynamic> product) {
return GestureDetector(
onTap: () => Get.to(() => ProductDetailPage(productId: product['id'])),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 3,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
color: Colors.grey[200],
),
child: Center(
child: Icon(
Icons.image,
size: 60,
color: Colors.grey[400],
),
),
),
),
卡片整体用GestureDetector包裹处理点击事件,点击跳转到商品详情页。外层Container设置白色背景和12像素圆角,让卡片看起来像一张卡。
Column垂直排列图片和信息区域。图片区域用Expanded并设置flex: 3,占卡片高度的大部分。只有顶部有圆角,和卡片整体圆角衔接。
这里用灰色背景加图标作为占位,实际项目要用Image.network或CachedNetworkImage加载网络图片。
dart
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product['title'],
style: const TextStyle(fontSize: 14),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const Spacer(),
Row(
children: [
Text(
'¥${product['price'].toStringAsFixed(0)}',
style: const TextStyle(
color: Color(0xFFFF4D4F),
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 4),
Text(
'¥${product['originalPrice'].toStringAsFixed(0)}',
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
decoration: TextDecoration.lineThrough,
),
),
],
),
信息区域flex: 2,占剩余空间。Padding给内容加10像素内边距,不会贴着边缘。
商品标题最多两行,超出显示省略号。Spacer把标题和价格撑开,价格始终靠近底部。
售价用红色0xFFFF4D4F粗体突出显示,这是电商App的通用做法,红色能吸引用户注意力。原价用灰色小字加删除线,让用户直观感受到折扣力度。toStringAsFixed(0)把价格格式化成整数,去掉小数点。
dart
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.location_on, size: 12, color: Colors.grey),
const SizedBox(width: 2),
Expanded(
child: Text(
product['location'],
style: const TextStyle(color: Colors.grey, fontSize: 10),
overflow: TextOverflow.ellipsis,
),
),
Text(
product['time'],
style: const TextStyle(color: Colors.grey, fontSize: 10),
),
],
),
],
),
),
),
],
),
),
);
}
卡片底部显示位置和发布时间。位置前面加个定位图标,用Expanded让位置文字自适应宽度,太长就显示省略号。发布时间放右边,比如"3小时前"。
这些辅助信息用灰色10号小字,不抢主要信息的风头。整个卡片信息层次分明:图片最大、标题次之、价格醒目、辅助信息最小。
加载网络图片
实际项目中图片区域应该这样实现:
dart
Expanded(
flex: 3,
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: product['image'],
fit: BoxFit.cover,
width: double.infinity,
placeholder: (context, url) => Container(
color: Colors.grey[200],
child: const Center(child: CircularProgressIndicator()),
),
errorWidget: (context, url, error) => Container(
color: Colors.grey[200],
child: Icon(Icons.image, size: 60, color: Colors.grey[400]),
),
),
),
)
CachedNetworkImage来自cached_network_image包,会自动缓存图片,下次加载更快。placeholder是加载中显示的内容,errorWidget是加载失败显示的内容。
ClipRRect裁剪图片让它有圆角,fit: BoxFit.cover让图片填满容器并保持比例。
添加收藏按钮
可以在图片右上角加一个收藏按钮:
dart
Expanded(
flex: 3,
child: Stack(
children: [
// 图片
Positioned.fill(
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: product['image'],
fit: BoxFit.cover,
),
),
),
// 收藏按钮
Positioned(
top: 8,
right: 8,
child: GestureDetector(
onTap: () => _toggleFavorite(product),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
shape: BoxShape.circle,
),
child: Icon(
product['isFavorite'] ? Icons.favorite : Icons.favorite_border,
color: product['isFavorite'] ? Colors.red : Colors.white,
size: 16,
),
),
),
),
],
),
)
用Stack叠加收藏按钮在图片上,半透明黑色背景让按钮在任何图片上都清晰可见。收藏后图标变成实心红色爱心。
添加标签
可以在图片左上角加标签,比如"急售"、"包邮":
dart
Positioned(
top: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: const Color(0xFFFF4D4F),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'急售',
style: TextStyle(color: Colors.white, fontSize: 10),
),
),
)
红色背景白色文字,非常醒目。可以根据商品属性动态显示不同的标签。
封装成独立组件
商品卡片在多个地方使用,应该封装成独立组件:
dart
class ProductCard extends StatelessWidget {
final Map<String, dynamic> product;
final VoidCallback? onTap;
final VoidCallback? onFavorite;
const ProductCard({
super.key,
required this.product,
this.onTap,
this.onFavorite,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: // ... 卡片内容
),
);
}
}
使用时:
dart
ProductCard(
product: products[index],
onTap: () => Get.to(() => ProductDetailPage(productId: products[index]['id'])),
onFavorite: () => _toggleFavorite(products[index]),
)
封装后代码更清晰,修改卡片样式只需要改一个地方。
不同尺寸的卡片
有时候需要不同尺寸的卡片,比如首页推荐用大卡片,列表用小卡片:
dart
class ProductCard extends StatelessWidget {
final Map<String, dynamic> product;
final ProductCardSize size;
const ProductCard({
super.key,
required this.product,
this.size = ProductCardSize.medium,
});
@override
Widget build(BuildContext context) {
switch (size) {
case ProductCardSize.small:
return _buildSmallCard();
case ProductCardSize.medium:
return _buildMediumCard();
case ProductCardSize.large:
return _buildLargeCard();
}
}
}
enum ProductCardSize { small, medium, large }
根据size参数返回不同样式的卡片。
小结
这篇详细讲解了"闲置换"App中商品卡片的实现,包括图片展示、标题价格、位置时间等信息的布局。还介绍了加载网络图片、添加收藏按钮、添加标签、封装组件等进阶内容。商品卡片是电商App最重要的UI组件,设计好了能显著提升用户体验。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net