flutter/lib/pages/goods/detail.dart
2025-09-19 17:56:34 +08:00

773 lines
32 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// 商品详情页
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<Goods> createState() => _GoodsState();
}
class _GoodsState extends State<Goods> {
final shareUserId = Get.arguments['userID'] ?? ''; //分享人的id,生成订单请求时必须携带的参数
dynamic shopObj;
late ScrollController scrollController = ScrollController();
final ChatController chatController = Get.find<ChatController>();
// 滚动位置
double scrollOffset = 0;
// 分享列表
List shareList = [
{'icon': 'assets/images/share-wx.png', 'label': '微信'},
{'icon': 'assets/images/share-pyq.png', 'label': '朋友圈'},
];
// 新增状态变量
Map<String, String> 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<dynamic> 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<dynamic>? ?? [];
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<ImUserInfoController>().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<String> swiperList;
if (swiperInfo.isNotEmpty) {
swiperList = swiperInfo.split(','); // 商品详情轮播图
} else {
swiperList = [];
}
dynamic attr = shopObj['productAttr']; //json数据
List<dynamic> 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<Widget>((attr) {
final attrName = attr['name'] ?? '';
final options = attr['options'] as List<dynamic>? ?? [];
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<Widget>((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),
);
}
}