如图所示:
代码为练习时写的项目,写的一般,功能实现了,等以后再来优化。
自己模拟的数据结构
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>([]); // 直接指定类型为RxListList skuSelected = []; // 选中后的skubool 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){// 先循环,当前点击的小规格默认全部为falseskuList[index].list.forEach((sku)=> sku.show.value = false);// 在赋值,当前点击项trueskuList[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}');}}@overridevoid onInit() {super.onInit();}@overridevoid onReady() {super.onReady();_initData();}@overridevoid 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, // 自定义高度// 文案左侧图标,根据提交状态判断是否显示loadingiconWidget: 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, filledisBlock: true, // 是否时BlockonTap:controller.submit, // 点击提交按钮触发padding: EdgeInsets.all(0),margin: EdgeInsets.all(0),),)].toColumn(),);}// 主视图Widget _buildView(BuildContext context) {return <Widget>[_buildTop().expanded(),_buildGoodsFoot(context)].toColumn();}@overrideWidget 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),);},);}
}