flutter/lib/pages/goods/detail.dart
2025-09-18 16:13:37 +08:00

703 lines
28 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': '朋友圈'},
];
@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 部分
});
} catch (e) {
logger.e(e);
Get.back();
}
}
///创建定点杆
createOrder(String goodsId) async {
var params = {
"type": 1, // 订单类型1->团购2->拼团;3->秒杀;
"distribution": 1, // 配送方式 1->到店核销2->自提;3->配送;
"skuItemBOList": [
{"skuId": goodsId, "quantity": 1}
]
};
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),
),
),
),
),
],
),
),
);
},
);
}
@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: [
// 原价
// Text(
// '¥${shopObj['price']}',
// style: TextStyle(
// color: Colors.white,
// fontSize: 16.0,
// decoration: TextDecoration.lineThrough,
// decorationColor: Colors.black,
// decorationThickness: 1.5,
// ),
// ),
Container(
padding: EdgeInsets.symmetric(horizontal: 8.0, vertical: 3.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(50.0),
),
child: Text(
'¥${shopObj['price']}',
style: TextStyle(color: Colors.red, fontSize: 12.0),
),
),
Text(
// '已售${Utils().graceNumber(shopObj['sales'] ?? 0)}',
'已售${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(
margin: EdgeInsets.only(top: 10.0),
padding: EdgeInsets.all(10.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15.0),
),
// child: Column(
// spacing: 10.0,
// children: [
// Row(
// spacing: 5.0,
// children: [
// Icon(
// Icons.timer,
// size: 16.0,
// ),
// Expanded(
// child: Text(
// '本商品请于2025.01.25前进行核销',
// style: TextStyle(fontSize: 12.0),
// ),
// ),
// ],
// ),
// Row(
// spacing: 5.0,
// children: [
// Icon(
// Icons.house_outlined,
// size: 16.0,
// ),
// Expanded(
// child: Text(
// '营业时间7x24',
// style: TextStyle(fontSize: 12.0),
// ),
// ),
// ],
// ),
// Row(
// spacing: 5.0,
// children: [
// Icon(
// Icons.location_on,
// size: 16.0,
// ),
// Expanded(
// child: Text(
// '河北省唐山市玉田县',
// style: TextStyle(fontSize: 12.0),
// ),
// ),
// ],
// ),
// ],
// ),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: attrList.map<Widget>((attr) {
final attrName = attr['name'] ?? '';
final options = attr['options'] as List<dynamic>? ?? [];
final optionNames = options.map((o) => o['name']).join(' / ');
return Row(
children: [
Text(
'$attrName: ',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
),
Expanded(
child: Text(
optionNames,
style: TextStyle(fontSize: 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: [
// Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Icon(
// Icons.store,
// color: Color(0xFFFF5000),
// size: 18.0,
// ),
// Text(
// '店铺',
// style: TextStyle(fontSize: 12.0),
// )
// ],
// ),
// 联系商家
// GestureDetector(
// onTap: () async {
// // 可以在这里打开聊天
// logger.i('联系客服:$shopObj');
// final res = await ImService.instance.getConversation(conversationID: 'c2c_${shopObj['tenantId']}');
// V2TimConversation conversation = res.data;
// logger.i(conversation.toLogString());
// if (res.success) {
// // 客服聊天不用检测关注关系
// V2TimUserFullInfo? sellerInfo;
// final resIm = await ImService.instance.otherInfo(shopObj['tenantId']);
// if (resIm.success && resIm.data != null) {
// sellerInfo = resIm.data!;
// logger.i(sellerInfo!.toLogString());
// } else {
// logger.e(resIm.desc);
// }
// conversation.showName = conversation.showName ?? sellerInfo!.nickName;
// Get.toNamed('/chat', arguments: conversation);
// } else {
// MyDialog.toast(res.desc, icon: const Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
// }
// },
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Icon(
// Icons.child_care_outlined,
// size: 18.0,
// ),
// Text(
// '联系商家',
// style: TextStyle(fontSize: 12.0),
// )
// ],
// ),
// )
// Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Badge.count(
// backgroundColor: Color(0xFFFF5000),
// count: 6,
// child: Icon(
// Icons.shopping_cart_outlined,
// size: 18.0,
// ),
// ),
// Text(
// '购物车',
// style: TextStyle(fontSize: 12.0),
// )
// ],
// ),
],
),
),
Container(
alignment: Alignment.center,
height: 36.0,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Color(0xFFFFEBEB),
borderRadius: BorderRadius.circular(30.0),
),
child: Row(
children: [
// Padding(
// padding: EdgeInsets.symmetric(horizontal: 10.0),
// child: Text(
// '加入购物车',
// style: TextStyle(color: Color(0xFFFF5000), fontSize: 14.0),
// ),
// ),
Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 20.0),
color: Color(0xFFFF5000),
child: GestureDetector(
onTap: () async {
// 这里走生成预支付订单拿到orderId
// String orderId = '1958380183857659904'; //测试数据
String orderId = await createOrder(shopObj['skuList'][0]['id']);
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),
),
),
),
],
),
),
],
),
),
),
// 返回顶部
floatingActionButton: Backtop(controller: scrollController, offset: scrollOffset),
);
}
}