/// 商品详情页 library; import 'dart:convert'; import 'package:card_swiper/card_swiper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:get/get.dart'; import 'package:loopin/IM/controller/chat_controller.dart'; import 'package:loopin/IM/controller/im_user_info_controller.dart'; import 'package:loopin/IM/im_message.dart'; import 'package:loopin/IM/im_service.dart'; import 'package:loopin/api/shop_api.dart'; import 'package:loopin/components/my_toast.dart'; import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/models/conversation_type.dart'; import 'package:loopin/models/share_type.dart'; import 'package:loopin/models/summary_type.dart'; import 'package:loopin/service/http.dart'; import 'package:loopin/utils/index.dart'; import 'package:loopin/utils/wxsdk.dart'; import 'package:shirne_dialog/shirne_dialog.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; import '../../behavior/custom_scroll_behavior.dart'; import '../../components/backtop.dart'; class Goods extends StatefulWidget { const Goods({super.key}); @override State createState() => _GoodsState(); } class _GoodsState extends State { final shareUserId = Get.arguments['userID'] ?? ''; //分享人的id,生成订单请求时必须携带的参数 dynamic shopObj; late ScrollController scrollController = ScrollController(); final ChatController chatController = Get.find(); // 滚动位置 double scrollOffset = 0; // 分享列表 List shareList = [ {'icon': 'assets/images/share-wx.png', 'label': '微信'}, {'icon': 'assets/images/share-pyq.png', 'label': '朋友圈'}, ]; // 新增状态变量 Map selectedAttributes = {}; // 存储选中的属性 dynamic selectedSku; // 当前选中的SKU int _quantity = 1; // 商品数量 @override void initState() { super.initState(); final goodsId = Get.arguments['goodsId'] ?? ''; scrollController.addListener(() { setState(() { scrollOffset = scrollController.offset; }); }); shopDetail(goodsId); } @override void dispose() { scrollController.dispose(); super.dispose(); } ///商品详情 void shopDetail(goodsId) async { try { final res = await Http.get('${ShopApi.shopDetail}/$goodsId'); logger.e(res['data']); setState(() { shopObj = res['data']; // 注意取 data 部分 // 初始化选中的SKU为第一个 if (shopObj != null && shopObj['skuList'] != null && shopObj['skuList'].isNotEmpty) { // selectedSku = shopObj['skuList'][0]; // 默认选中每一个分类中的第一条数据 dynamic attr = shopObj['productAttr']; List attrList = []; if (!Utils.isEmpty(attr)) { attrList = jsonDecode(attr); } // 清空已选属性 selectedAttributes.clear(); // 为每个属性选择第一个选项 for (var attr in attrList) { final attrName = attr['name'] ?? ''; final options = attr['options'] as List? ?? []; if (options.isNotEmpty) { final firstOption = options[0]['name'] ?? ''; selectedAttributes[attrName] = firstOption; } } // 根据选中的属性定位到对应的商品 locateSelectedSku(); } }); } catch (e) { logger.e(e); Get.back(); } } // 根据选中的属性定位到对应的SKU void locateSelectedSku() { if (shopObj != null && shopObj['skuList'] != null) { for (var sku in shopObj['skuList']) { try { final spData = jsonDecode(sku['spData'] ?? '{}'); bool match = true; // 检查所有已选属性是否匹配 selectedAttributes.forEach((key, value) { if (spData[key] != value) { match = false; } }); if (match) { setState(() { selectedSku = sku; }); print('333333333333333333'); print(sku); break; } } catch (e) { logger.e('解析spData错误: $e'); } } } } // 处理属性选择 void handleAttributeSelect(String attrName, String optionName) { setState(() { selectedAttributes[attrName] = optionName; locateSelectedSku(); // 选择属性后重新定位SKU }); } ///创建定点杆 createOrder(String goodsId) async { var params = { "type": 1, // 订单类型:1->团购;2->拼团;3->秒杀; "distribution": 1, // 配送方式 1->到店核销;2->自提;3->配送; "skuItemBOList": [ {"skuId": goodsId, "quantity": _quantity} ] }; print('下单请求参数---->$params'); try { final res = await Http.post(ShopApi.createGoodsOrder, data: params); var resData = res['data']; print('1111111111111111111111111---->$res'); if (resData['id'].isNotEmpty) { return resData['id']; } else { return null; } } catch (e) { logger.e(e); return null; } } void handleShareClick(int index) { logger.w(shopObj); final description = shopObj['describe'] ?? '未上传商品描述'; // 商品描述 if (index == 0) { // 好友 Wxsdk.shareToFriend(title: '快看看我分享的商品', description: description, webpageUrl: '${ShareType.shop.name}?id=${shopObj['id']}'); } else if (index == 1) { // 朋友圈 Wxsdk.shareToTimeline(title: '快看看我分享的商品', webpageUrl: '${ShareType.shop.name}?id=${shopObj['id']}'); } } void handlCoverClick(V2TimConversation conv) async { // 发送自定义消息 商品信息 final userId = conv.userID; //price,title,url,sell logger.w(shopObj['name']); final makeJson = jsonEncode({ "price": shopObj['price'], "title": shopObj['name'], "url": shopObj['pic'], "sell": Utils.graceNumber(int.parse(shopObj['sales'] ?? '0')), "goodsId": shopObj['id'], "userID": Get.find().userID.value, }); final res = await IMMessage().createCustomMessage( data: makeJson, ); if (res.success) { final sendRes = await IMMessage().sendMessage(msg: res.data!.messageInfo!, toUserID: userId, cloudCustomData: SummaryType.shareTuangou); if (sendRes.success) { MyToast().tip( title: '分享成功', position: 'center', type: 'success', ); Get.back(); } else { logger.e(res.desc); } } else { logger.e(res.desc); } } // 分享弹框 void handleShare() { if (chatController.chatList.isNotEmpty) { chatController.getConversationList(); } showModalBottomSheet( backgroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(15.0)), ), clipBehavior: Clip.antiAlias, context: context, isScrollControlled: true, builder: (context) { return Material( color: Colors.white, child: Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 分享列表 SizedBox( height: 110, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: shareList.length, padding: EdgeInsets.symmetric(horizontal: 0, vertical: 20.0), itemBuilder: (context, index) { return GestureDetector( onTap: () { handleShareClick(index); }, child: Container( width: 64, margin: EdgeInsets.symmetric(horizontal: 8.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Image.asset('${shareList[index]['icon']}', width: 48.0), SizedBox(height: 5), Text( '${shareList[index]['label']}', style: TextStyle(fontSize: 12.0), overflow: TextOverflow.ellipsis, ), ], ), ), ); }, ), ), // 会话列表 Obx(() { // 这里过滤掉有分组的会话 final filteredList = chatController.chatList.where((item) => conversationTypeFromString(item.isCustomAdmin) == null).toList(); if (filteredList.isEmpty) return SizedBox.shrink(); return SizedBox( height: 110, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: filteredList.length, padding: EdgeInsets.symmetric(horizontal: 0, vertical: 20.0), itemBuilder: (context, index) { return GestureDetector( // 点击分享 onTap: () => handlCoverClick(filteredList[index].conversation), child: Container( width: 64, margin: EdgeInsets.symmetric(horizontal: 8.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Image.asset('${chatController.chatList[index].faceUrl}', width: 48.0), ClipOval( child: NetworkOrAssetImage( imageUrl: filteredList[index].faceUrl, width: 48.0, height: 48.0, ), ), SizedBox(height: 5), Text( '${filteredList[index].conversation.showName}', style: TextStyle(fontSize: 12.0), overflow: TextOverflow.ellipsis, ), ], ), ), ); }, ), ); }), // 取消按钮 SafeArea( top: false, child: InkWell( onTap: () => Get.back(), child: Container( alignment: Alignment.center, width: double.infinity, height: 50.0, color: Colors.grey[50], child: Text( '取消', style: TextStyle(color: Colors.black87), ), ), ), ), ], ), ), ); }, ); } // 检查属性是否被选中 bool isAttributeSelected(String attrName, String optionName) { return selectedAttributes[attrName] == optionName; } @override Widget build(BuildContext context) { if (shopObj == null) { return Center(child: CircularProgressIndicator()); } String swiperInfo = shopObj['albumPics'] ?? ""; List swiperList; if (swiperInfo.isNotEmpty) { swiperList = swiperInfo.split(','); // 商品详情轮播图 } else { swiperList = []; } dynamic attr = shopObj['productAttr']; //json数据 List attrList = []; if (!Utils.isEmpty(attr)) { attrList = jsonDecode(attr); } logger.e(attrList); return Scaffold( backgroundColor: Colors.grey[50], body: CustomScrollView( scrollBehavior: CustomScrollBehavior().copyWith(scrollbars: false), controller: scrollController, slivers: [ SliverAppBar( backgroundColor: Colors.transparent, foregroundColor: Colors.white, pinned: true, expandedHeight: 280.0, titleSpacing: 10.0, leading: IconButton( icon: Icon( Icons.arrow_back, size: 20.0, ), style: ButtonStyle( backgroundColor: WidgetStateProperty.all(Colors.black.withAlpha(20)), ), onPressed: () { Get.back(); }, ), actions: [ IconButton( icon: Icon( Icons.share, size: 20.0, ), onPressed: () { // 分享 handleShare(); }, ), ], // 自定义伸缩区域(轮播图) flexibleSpace: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFFF5000), Color(0xFFFFAA00)], ), ), child: FlexibleSpaceBar( background: ScrollConfiguration( behavior: CustomScrollBehavior(), child: Swiper.children( pagination: SwiperPagination( builder: DotSwiperPaginationBuilder( color: Colors.white70, activeColor: Colors.white, )), indicatorLayout: PageIndicatorLayout.SCALE, children: swiperList .map((sw) => NetworkOrAssetImage( imageUrl: sw, placeholderAsset: 'assets/images/bk.jpg', )) .toList(), ), ), ), ), ), SliverToBoxAdapter( child: ScrollConfiguration( behavior: CustomScrollBehavior().copyWith(scrollbars: false), child: Column( children: [ Container( padding: EdgeInsets.fromLTRB(15.0, 10.0, 15.0, 25.0), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFFF5000), Color(0xFFFFAA00)], ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, spacing: 5.0, children: [ Row( spacing: 5.0, children: [ // 显示当前选中的SKU价格或默认价格 Container( padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 3.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(50.0), ), child: Text( '¥${selectedSku != null ? selectedSku['price'] : shopObj['price']}', style: TextStyle(color: Colors.red, fontSize: 12.0), ), ), Text( '已售${Utils.graceNumber(int.tryParse(shopObj['sales']?.toString() ?? '0') ?? 0)}', style: TextStyle(color: Colors.white, fontSize: 12.0), ), ], ), ], ), ), Container( padding: EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 0), width: double.infinity, decoration: BoxDecoration( color: Color(0xFFFAFAFA), borderRadius: BorderRadius.vertical(top: Radius.circular(15.0)), ), transform: Matrix4.translationValues(0.0, -15.0, 0.0), child: Column( children: [ // 标题 Container( padding: EdgeInsets.all(5.0), child: Align( alignment: Alignment.centerLeft, child: Text.rich( TextSpan( children: [ WidgetSpan( alignment: PlaceholderAlignment.middle, child: Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( color: const Color(0xFFFF5000), borderRadius: BorderRadius.circular(4), ), child: Text( shopObj['productCategoryName'] ?? '未知分类名称', style: const TextStyle( fontSize: 12.0, color: Colors.white, ), ), ), ), const WidgetSpan(child: SizedBox(width: 4)), TextSpan( text: '${shopObj['describe'] ?? ''}', style: TextStyle( fontSize: 14.0, fontWeight: FontWeight.w700, ), ), ], ), maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.left, ), ), ), // 规格 // 规格 Container( width: double.infinity, margin: EdgeInsets.only(top: 10.0), padding: EdgeInsets.all(10.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15.0), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 商品缩略图和数量选择 Row( children: [ // 商品缩略图 Container( width: 60, height: 60, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), image: DecorationImage( image: NetworkImage(selectedSku['pic'] != null ? selectedSku['pic'] : shopObj['pic']), fit: BoxFit.cover, ), ), ), SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '¥${selectedSku != null ? selectedSku['price'] : shopObj['price']}', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.red, ), ), SizedBox(height: 4), Text( '库存: ${selectedSku != null ? selectedSku['stock'] : shopObj['stock']}', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), ], ), ), // 数量加减按钮 Row( children: [ // 减少按钮 GestureDetector( onTap: () { setState(() { if (_quantity > 1) { _quantity--; } }); }, child: Container( width: 28, height: 28, decoration: BoxDecoration( color: _quantity > 1 ? Colors.grey[200] : Colors.grey[100], borderRadius: BorderRadius.circular(4), border: Border.all(color: Colors.grey[300]!), ), child: Icon( Icons.remove, size: 16, color: _quantity > 1 ? Colors.black : Colors.grey[400], ), ), ), SizedBox(width: 8), // 数量显示 Container( width: 40, alignment: Alignment.center, child: Text( '$_quantity', style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, ), ), ), SizedBox(width: 8), // 增加按钮 GestureDetector( onTap: () { setState(() { final maxStock = selectedSku != null ? selectedSku['stock'] : shopObj['stock']; if (_quantity < maxStock) { _quantity++; } }); }, child: Container( width: 28, height: 28, decoration: BoxDecoration( color: Colors.grey[200], borderRadius: BorderRadius.circular(4), border: Border.all(color: Colors.grey[300]!), ), child: Icon( Icons.add, size: 16, ), ), ), ], ), ], ), Divider(height: 20, color: Colors.grey[200]), // 属性选择 ...attrList.map((attr) { final attrName = attr['name'] ?? ''; final options = attr['options'] as List? ?? []; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '$attrName:', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold), ), SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: options.map((option) { final optionName = option['name'] ?? ''; final isSelected = isAttributeSelected(attrName, optionName); return GestureDetector( onTap: () => handleAttributeSelect(attrName, optionName), child: Container( padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: isSelected ? Color(0xFFFFF0F0) : Colors.grey[200], borderRadius: BorderRadius.circular(20), border: isSelected ? Border.all(color: Color(0xFFFF5000), width: 1) : null, ), child: Text( optionName, style: TextStyle( fontSize: 12, color: isSelected ? Color(0xFFFF5000) : Colors.black87, ), ), ), ); }).toList(), ), SizedBox(height: 12), ], ); }).toList(), ], ), ), // 详情 Container( margin: EdgeInsets.only(top: 10.0), padding: EdgeInsets.all(10.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15.0), ), child: Html( data: shopObj['detailMobileHtml'] ?? '暂无', )), ], ), ), ], ), ), ), ], ), // 商品导航栏 bottomNavigationBar: SafeArea( bottom: true, child: Container( height: 50.0, color: Colors.white, padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), child: Row( children: [ Expanded( child: Row( spacing: 15.0, children: [ // 这里可以保留原有的图标按钮 ], ), ), Container( alignment: Alignment.center, height: 36.0, clipBehavior: Clip.antiAlias, decoration: BoxDecoration( color: Color(0xFFFFEBEB), borderRadius: BorderRadius.circular(30.0), ), child: shopObj['canOrder'] == true?Row( children: [ Container( alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 20.0), color: Color(0xFFFF5000), child: GestureDetector( onTap: () async { // 这里走生成预支付订单,拿到orderId String skuId = selectedSku != null ? selectedSku['id'] : shopObj['skuList'][0]['id']; String orderId = await createOrder(skuId); if (orderId.isNotEmpty) { Get.toNamed('/order/detail', arguments: {'orderId': orderId}); } else { MyDialog.toast('生成订单失败', icon: const Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); } }, child: Text( '立即购买', style: TextStyle(color: Colors.white, fontSize: 14.0), ), ), ), ], ):null, ), ], ), ), ), // 返回顶部 floatingActionButton: Backtop(controller: scrollController, offset: scrollOffset), ); } }