Flutter:商品多规格内容总结,响应式数据,高亮切换显示。
如图所示:
代码为练习时写的项目,写的一般,功能实现了,等以后再来优化。
自己模拟的数据结构
var data = {
'id':1,
'name':'精品小米等多种五谷杂粮精品小等多种五谷杂粮',
'logo':'https://cdn.uviewui.com/uview/swiper/1.jpg',
'price':100.5,
'old_price':200.0,
'stock':998,
'images':[
{
'id':1,
'src':'https://cdn.uviewui.com/uview/swiper/1.jpg',
},
{
'id':2,
'src':'https://cdn.uviewui.com/uview/swiper/2.jpg',
},
{
'id':3,
'src':'https://cdn.uviewui.com/uview/swiper/3.jpg',
}
],
'sku':[
{
'name':'颜色',
'list':[
{
'id':8,
'name':'红色',
},
{
'id':9,
'name':'蓝色'
},
{
'id':10,
'name':'白加黑'
},
{
'id':11,
'name':'紫色'
},
{
'id':12,
'name':'绿色'
},
]
},
{
'name':'尺码',
'list':[
{
'id':13,
'name':'S'
},
{
'id':14,
'name':'M'
},
{
'id':15,
'name':'L'
},
{
'id':16,
'name':'XL'
},
{
'id':17,
'name':'XXL'
},
]
},
]
};
GoodsModel
import 'package:get/get.dart';
class GoodsModel {
final int id;
final String name;
final String logo;
final double price;
final double oldPrice;
final int stock;
final List<Image> images;
final List<Sku> sku;
GoodsModel({
required this.id,
required this.name,
required this.logo,
required this.price,
required this.oldPrice,
required this.stock,
required this.images,
required this.sku,
});
// 从JSON对象创建GoodsModel实例
factory GoodsModel.fromJson(Map<String, dynamic> json) {
return GoodsModel(
id: json['id'] as int,
name: json['name'] as String,
logo: json['logo'] as String,
price: json['price'] as double,
oldPrice: json['old_price'] as double,
stock: json['stock'] as int,
images: List.from(json['images'] as List).map((e) => Image.fromJson(e)).toList(),
sku: List.from(json['sku'] as List).map((e) => Sku.fromJson(e)).toList(),
);
}
// 将GoodsModel实例转换为JSON对象
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'logo': logo,
'price': price,
'old_price': oldPrice,
'stock': stock,
'images': images.map((e) => e.toJson()).toList(),
'sku': sku.map((e) => e.toJson()).toList(),
};
}
}
class Image {
final int id;
final String src;
Image({
required this.id,
required this.src,
});
// 从JSON对象创建Image实例
factory Image.fromJson(Map<String, dynamic> json) {
return Image(
id: json['id'] as int,
src: json['src'] as String,
);
}
// 将Image实例转换为JSON对象
Map<String, dynamic> toJson() {
return {
'id': id,
'src': src,
};
}
}
class Sku {
final String name;
final List<SkuItem> list;
Sku({
required this.name,
required this.list,
});
// 从JSON对象创建Sku实例
factory Sku.fromJson(Map<String, dynamic> json) {
return Sku(
name: json['name'] as String,
list: List.from(json['list'] as List).map((e) => SkuItem.fromJson(e)).toList(),
);
}
// 将Sku实例转换为JSON对象
Map<String, dynamic> toJson() {
return {
'name': name,
'list': list.map((e) => e.toJson()).toList(),
};
}
}
class SkuItem {
final int id;
final String name;
RxBool show; // 自定义响应式变量,判断当前项是否选中
SkuItem({
required this.id,
required this.name,
bool showValue = false, // 默认false
}) : show = RxBool(showValue);
// 从JSON对象创建SkuItem实例
factory SkuItem.fromJson(Map<String, dynamic> json) {
return SkuItem(
id: json['id'] as int,
name: json['name'] as String,
showValue: false, // JSON 数据中没有 show 字段,所以自定义设置为 false
);
}
// 将SkuItem实例转换为JSON对象
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
};
}
}
在GoodsModel
中,要确保sku
下的SkuItem
中,每一项都需要添加一个动态响应式数据show
字段来实现规格切换高亮显示,
controller
import 'package:flutter_aidishi/utils/loading.dart';
import 'package:get/get.dart';
import '../../../models/home/goods_detail.dart';
class GoodsDetailController extends GetxController {
GoodsDetailController();
final ProductId = Get.arguments['id'];
GoodsModel? goodsDetail; // 商品详情
List<Image> bannerList = []; // 商品列表
RxList<Sku> skuList = RxList<Sku>([]); // 直接指定类型为RxList
List skuSelected = []; // 选中后的sku
bool loading = false; // 提交状态
int type = 1; // 下单状态,1加入购物车,2立即购买
RxInt payNum = 1.obs; // 下单数量
_initData() {
// 模拟接口请求
var data = {
'id':1,
'name':'精品小米等多种五谷杂粮精品小等多种五谷杂粮',
'logo':'https://cdn.uviewui.com/uview/swiper/1.jpg',
'price':100.5,
'old_price':200.0,
'stock':998,
'images':[
{
'id':1,
'src':'https://cdn.uviewui.com/uview/swiper/1.jpg',
},
{
'id':2,
'src':'https://cdn.uviewui.com/uview/swiper/2.jpg',
},
{
'id':3,
'src':'https://cdn.uviewui.com/uview/swiper/3.jpg',
}
],
'sku':[
{
'name':'颜色',
'list':[
{
'id':8,
'name':'红色',
},
{
'id':9,
'name':'蓝色'
},
{
'id':10,
'name':'白加黑'
},
{
'id':11,
'name':'紫色'
},
{
'id':12,
'name':'绿色'
},
]
},
{
'name':'尺码',
'list':[
{
'id':13,
'name':'S'
},
{
'id':14,
'name':'M'
},
{
'id':15,
'name':'L'
},
{
'id':16,
'name':'XL'
},
{
'id':17,
'name':'XXL'
},
]
},
]
};
goodsDetail = GoodsModel.fromJson(data);
bannerList = goodsDetail!.images;
// 使用assignAll方法将普通的List<Sku>赋值给RxList<Sku>。这是GetX库中推荐的方法来填充响应式列表。
skuList.assignAll(goodsDetail!.sku); // 正确地将List<Sku>转换为RxList<Sku>
update(["goods_detail"]);
}
// 更改数量
onTapChangePayNum(int value){
payNum.value = value;
}
// SKU切换
void onTapChangeSku(index,i){
// 先循环,当前点击的小规格默认全部为false
skuList[index].list.forEach((sku)=> sku.show.value = false);
// 在赋值,当前点击项true
skuList[index].list[i].show.value = true;
update(["goods_detail"]);
}
// 下单
void submit(){
// 每次先清空,在去把已选择的sku放到skuSelected中
skuSelected.clear();
skuList.forEach((item){
item.list.forEach((val){
if(val.show.value){
skuSelected.add(val);
}
});
});
if(skuSelected.length != skuList.length){
Loading.error('请先选择完整规格');
}else{
print('下单方式:$type,购买数量:$payNum,看看选择了哪些规格:${skuSelected[0].id},${skuSelected[1].id}');
}
}
@override
void onInit() {
super.onInit();
}
@override
void onReady() {
super.onReady();
_initData();
}
@override
void onClose() {
super.onClose();
}
}
view
import 'package:flutter/material.dart';
import 'package:flutter_aidishi/extension/index.dart';
import 'package:flutter_aidishi/widget/index.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:tdesign_flutter/tdesign_flutter.dart';
import 'package:flutter_swiper_null_safety/flutter_swiper_null_safety.dart';
import 'index.dart';
class GoodsDetailPage extends GetView<GoodsDetailController> {
const GoodsDetailPage({super.key});
// 轮播图
Widget _buildBanner() {
return Container(
height: 312.w,
child: Swiper(
autoplay: true, // 自动轮播
itemCount: 2, // 轮播数量
loop: true, // 循环
pagination: const SwiperPagination(
alignment: Alignment.bottomCenter, // 指示器位置
builder: TDSwiperPagination.dotsBar, // 具体样式
),
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: (){
print('点击了第$index个图片');
},
// child: TDImage(imgUrl: controller.bannerList[index].src,),
child: TDImage(assetUrl: 'assets/images/banner.png',type: TDImageType.square,),
);
},
),
);
}
// 商品信息
Widget _buildGoodsName() {
return Container(
padding: EdgeInsets.only(left: 15.w,right: 15.w,top: 20.w,bottom: 20.w),
width: 375.w,
color: Colors.white,
child: <Widget>[
Text("${controller.goodsDetail?.name}",
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 15.sp,fontWeight: FontWeight.bold),
),
SizedBox(height: 15.w,),
<Widget>[
<Widget>[
Text('¥',style: TextStyle(fontSize: 13.sp,color: AppColors.mainColor),),
Text('${controller.goodsDetail?.price}',style: TextStyle(fontSize: 24.sp,color: AppColors.mainColor),),
Text('/现价',style: TextStyle(fontSize: 13.sp,color: AppColors.mainColor),),
SizedBox(width: 10.w,),
Text('¥${controller.goodsDetail?.oldPrice}/原价',
style: TextStyle(
fontSize: 14.sp,
color: AppColors.Color999,
decoration: TextDecoration.lineThrough,
decorationColor: AppColors.Color999
),
)
].toRow(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic
),
Text('库存 ${controller.goodsDetail?.stock}',style: TextStyle(fontSize: 11.sp,color: AppColors.Color999),)
].toRow(
mainAxisAlignment: MainAxisAlignment.spaceBetween
)
].toColumn(
crossAxisAlignment: CrossAxisAlignment.start
),
);
}
// 商品详情
Widget _buildGoodsDetail() {
return <Widget>[
SizedBox(height: 10.w,),
<Widget>[
Container(
width: 50.w,
height: 1,
color: Color(0xffe1e1e1),
),
SizedBox(width: 10.w,),
Text('商品详情',style: TextStyle(fontSize: 11.sp,color: AppColors.Color999),),
SizedBox(width: 10.w,),
Container(
width: 50.w,
height: 1,
color: Color(0xffe1e1e1),
),
].toRow(
mainAxisAlignment: MainAxisAlignment.center,
),
SizedBox(height: 10.w,),
Container(
height: 500.w,
color: Colors.blueGrey,
child: Center(
child: Text('富文本'),
),
)
].toColumn();
}
// 可滚动内容区域
Widget _buildTop(){
return SingleChildScrollView(
child: <Widget>[
_buildBanner(),
SizedBox(height: 15.w,),
_buildGoodsName(),
_buildGoodsDetail(),
].toColumn(),
);
}
// 底部悬浮按钮
Widget _buildGoodsFoot(BuildContext context) {
return <Widget>[
GestureDetector(
onTap: (){
print('弹出购物车');
controller.type = 1;
Navigator.of(context).push(
TDSlidePopupRoute(
modalBarrierColor: TDTheme.of(context).fontGyColor2,
slideTransitionFrom: SlideTransitionFrom.bottom,
builder: (context) {
return Container(
width: 375.w,
height: 500.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(topLeft: Radius.circular(10.w),topRight: Radius.circular(10.w)),
),
child: _buildFootPopup(context),
);
}
)
);
},
child: Container(
width: 187.5.w,
height: 49.w,
color: Colors.white,
child: Center(
child: Text('加入购物车',style: TextStyle(fontSize: 14.sp),),
),
)
),
GestureDetector(
onTap: (){
print('立即结算');
controller.type = 2;
Navigator.of(context).push(
TDSlidePopupRoute(
modalBarrierColor: TDTheme.of(context).fontGyColor2,
slideTransitionFrom: SlideTransitionFrom.bottom,
builder: (context) {
return Container(
width: 375.w,
height: 500.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.w)
),
child: _buildFootPopup(context),
);
}
)
);
},
child: Container(
width: 187.5.w,
height: 49.w,
color: AppColors.mainColor,
child: Center(
child: Text('立即购买',style: TextStyle(fontSize: 14.sp,color: AppColors.Colorfff),),
),
)
),
].toRow();
}
// 多规格弹窗
Widget _buildFootPopup(BuildContext context){
return Container(
padding: EdgeInsets.all(15.w),
child: <Widget>[
// 商品信息
<Widget>[
TDImage(
assetUrl: 'assets/images/goods.png',
type: TDImageType.roundedSquare,
width: 74,
height: 74,
),
Container(
width: 220.w,
child: <Widget>[
Text('精品小米等多种五谷杂粮精品小米等多精品小米等多种五谷杂粮精品小米等多种五谷杂粮种五谷杂粮',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 14.sp,fontWeight: FontWeight.bold),
),
SizedBox(height: 10.w,),
<Widget>[
Text('¥',style: TextStyle(fontSize: 12.sp,color: AppColors.mainColor),),
Text('2500',style: TextStyle(fontSize: 16.sp,color: AppColors.mainColor),),
Text('/现价',style: TextStyle(fontSize: 12.sp,color: AppColors.mainColor),),
SizedBox(width: 10.w,),
Text('¥2000/原价',
style: TextStyle(
fontSize: 10.sp,
color: AppColors.Color999,
decoration: TextDecoration.lineThrough,
decorationColor: AppColors.Color999
),
)
].toRow(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic
)
].toColumn(),
),
GestureDetector(
onTap: ()=> Navigator.of(context).pop(),
child: TDImage(
assetUrl: 'assets/images/close.png',
type: TDImageType.roundedSquare,
width: 20,
height: 20,
),
)
].toRow(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween
),
SizedBox(height: 15.w,),
Container(
width: 345.w,
height: 1,
color: Color(0xfff5f5f5),
),
SizedBox(height: 10.w,),
// 库存
<Widget>[
Text('库存:886',style: TextStyle(fontSize: 13.sp),),
<Widget>[
Text('数量',style: TextStyle(fontSize: 13.sp),),
SizedBox(width: 10.w,),
// 选择数量
TDStepper(
theme: TDStepperTheme.filled,
value:controller.payNum.value,
min:1,
onChange:(value){
controller.onTapChangePayNum(value);
}
)
].toRow()
].toRow(
mainAxisAlignment: MainAxisAlignment.spaceBetween
),
SizedBox(height: 20.w,),
Container(
height: 260.w, // 固定高度
color: Colors.white, // 可选:可改成灰色,查看滚动区域
child: SingleChildScrollView(
// 如果内容在垂直方向上超出Container的高度,则可以滚动
child: Column(
// 使用Column来垂直排列子组件
children: <Widget>[
// 大规格
for(var index = 0;index<controller.skuList.length;index++)
<Widget>[
Container(
width: 375.w,
child: Text('${controller.skuList[index].name}'),
),
SizedBox(height: 10.w,),
<Widget>[
// 小规格
// for(var val in item.list)
for(var i = 0;i<controller.skuList[index].list.length;i++)
GestureDetector(
onTap: (){
controller.onTapChangeSku(index,i);
},
child: Obx(() =>Container(
margin: EdgeInsets.only(right: 20.w,bottom: 20.w,left: 0),
padding: EdgeInsets.only(left: 30.w,right: 30.w,top: 6.w,bottom: 6.w),
child: Text('${controller.skuList[index].list[i].name}'),
decoration: BoxDecoration(
color: controller.skuList[index].list[i].show.value ? Color(0xffffffff) : Color(0xffF8F8F8),
borderRadius: BorderRadius.circular(5.w),
border: Border.all(
color: controller.skuList[index].list[i].show.value ? Color(0xffFF770F) : Color(0xff999999),
width: 1.0
)
),
)),
)
].toWrap()
].toColumn(
crossAxisAlignment: CrossAxisAlignment.start
),
// 你可以继续添加更多的子组件
],
),
),
),
// 结算
SizedBox(height: 20.w,),
Container(
width: 345.w,
child: TDButton(
text: '确定', // 按钮文案
height: 43.w, // 自定义高度
// 文案左侧图标,根据提交状态判断是否显示loading
iconWidget: controller.loading ? TDLoading(
size: TDLoadingSize.small,
icon: TDLoadingIcon.circle,
) : Container(),
size: TDButtonSize.large, // 按钮尺寸
type: TDButtonType.fill, // 类型:填充,描边,文字
theme: TDButtonTheme.primary, // 主题
shape: TDButtonShape.round, // 形状:圆角,胶囊,方形,圆形,填充 rectangle, round, square, circle, filled
isBlock: true, // 是否时Block
onTap:controller.submit, // 点击提交按钮触发
padding: EdgeInsets.all(0),
margin: EdgeInsets.all(0),
),
)
].toColumn(),
);
}
// 主视图
Widget _buildView(BuildContext context) {
return <Widget>[
_buildTop().expanded(),
_buildGoodsFoot(context)
].toColumn();
}
@override
Widget build(BuildContext context) {
return GetBuilder<GoodsDetailController>(
init: GoodsDetailController(),
id: "goods_detail",
builder: (_) {
return Scaffold(
appBar: AppBar(title: const Text("goods_detail")),
body: _buildView(context),
backgroundColor: Color(0xffF6F6F6),
);
},
);
}
}