From 51ad2614f0c3c4b2aa490a10013ff05f5175087e Mon Sep 17 00:00:00 2001 From: abu <3109389044@qq.com> Date: Sat, 6 Sep 2025 14:57:47 +0800 Subject: [PATCH] merge --- lib/IM/im_group_listeners.dart | 151 ++ lib/IM/im_service.dart | 49 +- lib/layouts/index.dart | 14 +- lib/pages/chat/chat_group.dart | 1264 +++++++++++------ lib/pages/groupChat/index.dart | 257 +++- lib/pages/my/index.dart | 2 +- lib/pages/order/index.dart | 566 +++++--- lib/pages/order/seller.dart | 588 ++++++++ .../upload_video_page/upload_video_page.dart | 70 +- lib/router/index.dart | 2 + lib/service/http_config.dart | 3 + 11 files changed, 2278 insertions(+), 688 deletions(-) create mode 100644 lib/IM/im_group_listeners.dart create mode 100644 lib/pages/order/seller.dart diff --git a/lib/IM/im_group_listeners.dart b/lib/IM/im_group_listeners.dart new file mode 100644 index 0000000..a34d225 --- /dev/null +++ b/lib/IM/im_group_listeners.dart @@ -0,0 +1,151 @@ +import 'package:logger/logger.dart'; +import 'package:tencent_cloud_chat_sdk/enum/V2TimGroupListener.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_change_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_change_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_topic_info.dart'; +import 'package:tencent_cloud_chat_sdk/native_im/adapter/tim_manager.dart'; + +final logger = Logger(); + +class ImGroupListeners { + ImGroupListeners(); + + late final V2TimGroupListener _listener; + + void register() { + _listener = V2TimGroupListener( + onGroupCreated: (String groupID) { + logger.i('群被创建: $groupID'); + onGroupCreated(groupID); + }, + onGroupDismissed: (String groupID, V2TimGroupMemberInfo opUser) { + logger.i('群组被解散: $groupID, 操作人: ${opUser.nickName}'); + onGroupDismissed(groupID, opUser); + }, + onGroupRecycled: (String groupID, V2TimGroupMemberInfo opUser) { + logger.i('群组被回收: $groupID, 操作人: ${opUser.nickName}'); + }, + onGroupInfoChanged: (String groupID, List changeInfos) { + logger.i('群信息变更: $groupID, $changeInfos'); + onGroupInfoChanged(groupID, changeInfos); + }, + onMemberEnter: (String groupID, List memberList) { + logger.i('成员加入群 $groupID: ${memberList.map((e) => e.userID).join(', ')}'); + onMemberEnter(groupID, memberList); + }, + onMemberLeave: (String groupID, V2TimGroupMemberInfo member) { + logger.i('成员离开群 $groupID: ${member.userID}'); + onMemberLeave(groupID, member); + }, + onMemberInvited: (String groupID, V2TimGroupMemberInfo opUser, List memberList) { + logger.i('成员被邀请 $groupID, inviter: ${opUser.userID}, members: ${memberList.map((e) => e.userID).join(', ')}'); + onMemberInvited(groupID, memberList); + }, + onMemberKicked: (String groupID, V2TimGroupMemberInfo opUser, List memberList) { + logger.i('成员被踢 $groupID, opUser: ${opUser.userID}, members: ${memberList.map((e) => e.userID).join(', ')}'); + onMemberKicked(groupID, memberList); + }, + onMemberInfoChanged: (String groupID, List v2TIMGroupMemberChangeInfoList) { + logger.i('成员信息变更: $groupID, $v2TIMGroupMemberChangeInfoList'); + onMemberInfoChanged(groupID, v2TIMGroupMemberChangeInfoList); + }, + onQuitFromGroup: (String groupID) { + logger.i('主动退出群 $groupID'); + onQuitFromGroup(groupID); + }, + onReceiveJoinApplication: (String groupID, V2TimGroupMemberInfo member, String opReason) { + logger.i('收到入群申请 $groupID, member: ${member.userID}, reason: $opReason'); + onReceiveJoinApplication(groupID, member, opReason); + }, + onApplicationProcessed: (String groupID, V2TimGroupMemberInfo opUser, bool isAgreeJoin, String opReason) { + logger.i('入群申请已处理 $groupID, opUser: ${opUser.userID}, isAgree: $isAgreeJoin, reason: $opReason'); + onApplicationProcessed(groupID, opUser, isAgreeJoin, opUser); + }, + onGrantAdministrator: (String groupID, V2TimGroupMemberInfo opUser, List memberList) { + logger.i('被授予管理员 $groupID, opUser: ${opUser.userID}, members: ${memberList.map((e) => e.userID).join(', ')}'); + onGrantAdministrator(groupID, opUser, memberList); + }, + onRevokeAdministrator: (String groupID, V2TimGroupMemberInfo opUser, List memberList) { + logger.i('管理员权限被撤销 $groupID, opUser: ${opUser.userID}, members: ${memberList.map((e) => e.userID).join(', ')}'); + onRevokeAdministrator(groupID, opUser, memberList); + }, + onReceiveRESTCustomData: (String groupID, String customData) { + logger.i('收到自定义数据 $groupID: $customData'); + onReceiveRESTCustomData(groupID, customData); + }, + onGroupAttributeChanged: (String groupID, Map groupAttributeMap) { + logger.i('群属性变更 $groupID: $groupAttributeMap'); + }, + onAllGroupMembersMuted: (String groupID, bool isMute) { + logger.i('全体禁言状态变更 $groupID: $isMute'); + }, + onMemberMarkChanged: (String groupID, List memberIDList, int markType, bool enableMark) { + logger.i('成员标记变更 $groupID, members: ${memberIDList.join(', ')}, type: $markType, enable: $enableMark'); + }, + onGroupCounterChanged: (String groupID, String key, int newValue) { + logger.i('群计数器变更 $groupID, key: $key, value: $newValue'); + }, + onTopicCreated: (String groupID, String topicID) { + logger.i('话题创建 $groupID, topic: $topicID'); + }, + onTopicDeleted: (String groupID, List topicIDList) { + logger.i('话题删除 $groupID, topics: ${topicIDList.join(', ')}'); + }, + onTopicInfoChanged: (String groupID, V2TimTopicInfo topicInfo) { + logger.i('话题信息变更 $groupID, topic: ${topicInfo.topicID}'); + }, + ); + + TIMManager.instance.addGroupListener(listener: _listener); + + logger.i("群组监听器已注册"); + } + + void unregister() { + TIMManager.instance.removeGroupListener(listener: _listener); + logger.i("群组监听器已移除"); + } + + // 新群被创建 + void onGroupCreated(String groupID) {} + + //解散群 + void onGroupDismissed(String groupID, V2TimGroupMemberInfo opUser) {} + + // 群信息变更 + void onGroupInfoChanged(String groupID, List changeInfos) {} + + // 成员加入群 + void onMemberEnter(String groupID, List memberList) {} + + // 成员离开群 + void onMemberLeave(String groupID, V2TimGroupMemberInfo member) {} + + // 成员被邀请入群 + void onMemberInvited(String groupID, List memberList) {} + + // 成员被踢出群 + void onMemberKicked(String groupID, List memberList) {} + + // 成员信息变更 + void onMemberInfoChanged(String groupID, List v2timGroupMemberChangeInfoList) {} + + // 主动退出群 + void onQuitFromGroup(String groupID) {} + + // 入群申请 + void onReceiveJoinApplication(String groupID, V2TimGroupMemberInfo member, String opReason) {} + + // 处理入群申请处理结果 + void onApplicationProcessed(String groupID, V2TimGroupMemberInfo opUser, bool isAgreeJoin, V2TimGroupMemberInfo opUser2) {} + + // 册封管理员 + void onGrantAdministrator(String groupID, V2TimGroupMemberInfo opUser, List memberList) {} + + // 移除管理员权限 + void onRevokeAdministrator(String groupID, V2TimGroupMemberInfo opUser, List memberList) {} + + // 收到自定义消息 + void onReceiveRESTCustomData(String groupID, String customData) {} +} diff --git a/lib/IM/im_service.dart b/lib/IM/im_service.dart index 3fd1da8..12cbe9c 100644 --- a/lib/IM/im_service.dart +++ b/lib/IM/im_service.dart @@ -7,6 +7,7 @@ import 'package:loopin/IM/controller/tab_bar_controller.dart'; import 'package:loopin/IM/global_badge.dart'; import 'package:loopin/IM/im_core.dart'; import 'package:loopin/IM/im_friend_listeners.dart'; +import 'package:loopin/IM/im_group_listeners.dart'; import 'package:loopin/IM/im_message_listeners.dart'; import 'package:loopin/IM/im_result.dart'; import 'package:loopin/IM/push_service.dart'; @@ -15,7 +16,9 @@ import 'package:loopin/utils/wxsdk.dart'; import 'package:tencent_cloud_chat_sdk/enum/friend_application_type_enum.dart'; import 'package:tencent_cloud_chat_sdk/enum/friend_response_type_enum.dart'; import 'package:tencent_cloud_chat_sdk/enum/friend_type_enum.dart'; +import 'package:tencent_cloud_chat_sdk/enum/group_add_opt_enum.dart'; import 'package:tencent_cloud_chat_sdk/enum/history_msg_get_type_enum.dart'; +import 'package:tencent_cloud_chat_sdk/manager/v2_tim_group_manager.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation_filter.dart'; @@ -27,6 +30,7 @@ import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_type_check_result.da import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_info.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_info_result.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_operation_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_change_info.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_search_param.dart'; @@ -82,9 +86,13 @@ class ImService { // 登录成功后注册高级消息监听器 final messageService = ImMessageListenerService(); Get.put(messageService, permanent: true); - await messageService.init(); + // 注册群监听器 + final groupListener = ImGroupListeners(); + Get.put(groupListener, permanent: true); + groupListener.register(); + // 注册关系链监听器 final friendListener = ImFriendListeners(); Get.put(friendListener, permanent: true); @@ -122,6 +130,10 @@ class ImService { Get.find().unregister(); await Get.delete(force: true); + // 移除群监听器 + Get.find().unregister(); + await Get.delete(force: true); + /// 清理tabbar Get.find().badgeMap.clear(); @@ -721,4 +733,39 @@ class ImService { ); return ImResult.wrap(res); } + + /// 创建群 + Future> createGroup({ + String? groupID, + required String groupType, + required String groupName, + String? notification, + String? introduction, + String? faceUrl, + bool? isAllMuted, + bool isSupportTopic = false, + GroupAddOptTypeEnum? addOpt, + List? memberList, + GroupAddOptTypeEnum? approveOpt, + bool? isEnablePermissionGroup, + int? defaultPermissions, + }) async { + final res = await V2TIMGroupManager().createGroup( + groupID: groupID, + groupType: groupType, + groupName: groupName, + notification: notification, // 群公告 + introduction: introduction, // 群简介 + faceUrl: faceUrl, // 群头像 + isAllMuted: isAllMuted, // true=全员禁言 + isSupportTopic: isSupportTopic, // 是否支持话题 + addOpt: addOpt, // 加群方式(非直播群有效) + memberList: memberList, // 初始群成员(不支持直播群) + approveOpt: approveOpt, //入群申请的处理方式(一般和 addOpt 配合使用)对 Work / Public 群有用 + isEnablePermissionGroup: isEnablePermissionGroup, // 是否开启权限组 + defaultPermissions: defaultPermissions, // 默认权限配置,搭配 isEnablePermissionGroup 使用 + ); + + return ImResult.wrap(res); + } } diff --git a/lib/layouts/index.dart b/lib/layouts/index.dart index f0377e7..18b371d 100644 --- a/lib/layouts/index.dart +++ b/lib/layouts/index.dart @@ -32,7 +32,15 @@ class _LayoutState extends State { VideoModuleController videoModuleController = Get.put(VideoModuleController()); // page页面 - List pageList = [VideoPage(), IndexPage(), UploadVideoPage(), ChatPage(), MyPage()]; + List pageList = [ + VideoPage(), + IndexPage(), + UploadVideoPage( + visible: false, + ), + ChatPage(), + MyPage() + ]; // tabs选项 List navItems = [ BottomNavigationBarItem(icon: Icon(Icons.play_circle_outline), label: '视频'), @@ -131,7 +139,9 @@ class _LayoutState extends State { ), Offstage( offstage: videoModuleController.layoutPageCurrent.value != 2, - child: UploadVideoPage(), // 不需要保活 + child: UploadVideoPage( + visible: videoModuleController.layoutPageCurrent.value == 2, + ), // 不需要保活 ), Offstage( offstage: videoModuleController.layoutPageCurrent.value != 3, diff --git a/lib/pages/chat/chat_group.dart b/lib/pages/chat/chat_group.dart index a37637f..6d555f9 100644 --- a/lib/pages/chat/chat_group.dart +++ b/lib/pages/chat/chat_group.dart @@ -1,19 +1,26 @@ /// 聊天模板 library; +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:loopin/IM/controller/chat_controller.dart'; import 'package:loopin/IM/controller/chat_detail_controller.dart'; import 'package:loopin/IM/im_message.dart'; +import 'package:loopin/IM/im_result.dart'; import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/components/network_or_asset_image.dart'; +import 'package:loopin/components/preview_video.dart'; +import 'package:loopin/models/summary_type.dart'; +import 'package:loopin/utils/audio_player_service.dart'; +import 'package:loopin/utils/snapshot.dart'; +import 'package:loopin/utils/voice_service.dart'; import 'package:shirne_dialog/shirne_dialog.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; +import 'package:wechat_assets_picker/wechat_assets_picker.dart'; -import '../../behavior/custom_scroll_behavior.dart'; -import '../../components/image_group.dart'; import '../../styles/index.dart'; import '../../utils/index.dart'; import './components/redpacket.dart'; @@ -25,25 +32,22 @@ class ChatGroup extends StatefulWidget { const ChatGroup({super.key}); @override - State createState() => _ChatState(); + State createState() => _ChatGroupState(); } -class _ChatState extends State with SingleTickerProviderStateMixin { +class _ChatGroupState extends State with SingleTickerProviderStateMixin { late final ChatDetailController controller; // 接收参数 - // Rx arguments = Get.arguments.obs; - late final Rx arguments; + late V2TimConversation arguments; late String selfUserId; // 聊天消息模块 final bool isNeedScrollBottom = true; - // final RxList chatList = [].obs; - bool isLoading = false; // 是否在加载中 bool hasMore = true; // 是否还有更多数据 - bool _throttleFlag = false; // 滚动节流锁 + final RxBool _throttleFlag = false.obs; // 滚动节流锁 // 表情json List emoJson = emotionData; @@ -72,7 +76,9 @@ class _ChatState extends State with SingleTickerProviderStateMixin { ]; // controller监听 - ScrollController chatController = ScrollController(); + // ScrollController chatController = ScrollController(); + late ScrollController chatController; + ScrollController emojController = ScrollController(); // 模拟开红包按钮动画 @@ -85,10 +91,9 @@ class _ChatState extends State with SingleTickerProviderStateMixin { @override void initState() { super.initState(); - final arg = Get.arguments as V2TimConversation; - arguments = arg.obs; - - controller = Get.put(ChatDetailController(userID: arguments.value.userID ?? '')); + arguments = Get.arguments; + controller = Get.find(); + chatController = controller.chatController; animController = AnimationController( vsync: this, @@ -108,18 +113,22 @@ class _ChatState extends State with SingleTickerProviderStateMixin { setState(() { toolbarEnable = false; }); - scrollToBottom(); + controller.scrollToBottom(); } }); // 滚动监听 + // Future.delayed(Duration(milliseconds: 1000), () { + + // }); chatController.addListener(() { - if (_throttleFlag) return; + if (_throttleFlag.value) return; if (chatController.position.pixels >= chatController.position.maxScrollExtent - 50) { - _throttleFlag = true; + // if (chatController.position.pixels <= 50) { + _throttleFlag.value = true; getMsgData().then((_) { // 解锁 - Future.delayed(Duration(milliseconds: 300), () { - _throttleFlag = false; + Future.delayed(Duration(milliseconds: 1000), () { + _throttleFlag.value = false; }); }); } @@ -131,9 +140,6 @@ class _ChatState extends State with SingleTickerProviderStateMixin { if (Get.isRegistered()) { Get.delete(); } - // 更新会话列表数据 - Get.find().getConversationList(); - chatController.dispose(); emojController.dispose(); editorFocusNode.dispose(); animController.dispose(); @@ -175,16 +181,14 @@ class _ChatState extends State with SingleTickerProviderStateMixin { cancelText: '取消', onConfirm: () async { // print('备注为:$remark'); - final res = await ImService.instance.setFriendInfo(userID: arguments.value.userID!, friendRemark: remark); + final res = await ImService.instance.setFriendInfo(userID: arguments.userID!, friendRemark: remark); if (res.success) { - // 刷新会话列表数据 - // Get.find().getConversationList(); - arguments.update((val) { - val?.showName = remark; + setState(() { + arguments.showName = remark; }); } else { print(res.desc); - print(arguments.value.userID); + print(arguments.userID); MyDialog.toast(res.desc, icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); } return true; @@ -193,8 +197,8 @@ class _ChatState extends State with SingleTickerProviderStateMixin { } void cleanUnRead() async { - if ((arguments.value.unreadCount ?? 0) > 0) { - final res = await ImService.instance.clearConversationUnreadCount(conversationID: arguments.value.conversationID); + if ((arguments.unreadCount ?? 0) > 0) { + final res = await ImService.instance.clearConversationUnreadCount(conversationID: arguments.conversationID); if (!res.success) { MyDialog.toast(res.desc, icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); } @@ -215,18 +219,20 @@ class _ChatState extends State with SingleTickerProviderStateMixin { // 获取最旧一条消息作为游标 V2TimMessage? lastRealMsg; + // for (var msg in controller.chatList.reversed) { for (var msg in controller.chatList.reversed) { if (msg.localCustomData != 'time_label') { lastRealMsg = msg; break; } } - final lastMsg = lastRealMsg ?? arguments.value.lastMessage; // 如果找不到,就用传入的参数 + final lastMsg = lastRealMsg ?? arguments.lastMessage; // 如果找不到,就用传入的参数 + print(lastMsg?.toLogString()); // final lastMsg = controller.chatList.isNotEmpty ? controller.chatList.last : arguments.lastMessage; final res = await ImService.instance.getHistoryMessageList( - userID: arguments.value.userID, + userID: arguments.userID, lastMsg: lastMsg, ); @@ -235,14 +241,26 @@ class _ChatState extends State with SingleTickerProviderStateMixin { if (newMessages.isEmpty) { hasMore = false; - MyDialog.toast('没有更多了~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); - } else { - if (initFlag) { - newMessages.insert(0, lastMsg!); - } - controller.updateChatListWithTimeLabels(newMessages); - print('聊天数据加载成功'); + // MyDialog.toast('没有更多了~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); } + if (initFlag && lastMsg != null) { + newMessages.insert(0, lastMsg); + // controller.scrollToBottom(); + } + controller.updateChatListWithTimeLabels(newMessages); + if (initFlag) { + // 初始化时滚到最底部 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (chatController.hasClients) { + // controller.scrollToBottom(); + // final bottomPadding = MediaQuery.of(context).padding.bottom; // 底部安全区域高度 + // chatController.jumpTo(chatController.position.maxScrollExtent); // 60为底部操作栏高度 + chatController.jumpTo(0); + } + }); + } + + print('聊天数据加载成功'); } else { MyDialog.toast("获取聊天记录失败:${res.desc}", icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); } @@ -269,31 +287,33 @@ class _ChatState extends State with SingleTickerProviderStateMixin { ), )); } - // 文本消息模板 + // 文本消息模板=1 else if (item.elemType == 1) { - msgtpl.add(RenderChatItem( - data: item, - child: Ink( - decoration: BoxDecoration( - color: !(item.isSelf ?? false) ? Color(0xFFFFFFFF) : Color(0xFF89E45B), - borderRadius: BorderRadius.circular(10.0), - ), - child: InkWell( - overlayColor: WidgetStateProperty.all(Colors.transparent), - borderRadius: BorderRadius.circular(10.0), - child: Container( - padding: const EdgeInsets.all(10.0), - child: RichTextUtil.getRichText(item.textElem?.text ?? '', color: !(item.isSelf ?? false) ? Colors.black : Colors.white), // 可自定义解析emoj/网址/电话 + msgtpl.add( + RenderChatItem( + data: item, + child: Ink( + decoration: BoxDecoration( + color: !(item.isSelf ?? false) ? Color(0xFFFFFFFF) : Color(0xFF89E45B), + borderRadius: BorderRadius.circular(10.0), + ), + child: InkWell( + overlayColor: WidgetStateProperty.all(Colors.transparent), + borderRadius: BorderRadius.circular(10.0), + child: Container( + padding: const EdgeInsets.all(10.0), + child: RichTextUtil.getRichText(item.textElem?.text ?? '', color: !(item.isSelf ?? false) ? Colors.black : Colors.white), // 可自定义解析emoj/网址/电话 + ), + onLongPress: () { + contextMenuDialog(); + }, ), - onLongPress: () { - contextMenuDialog(); - }, ), ), - )); + ); } - // gif表情模板 - else if (item.elemType == 4) { + // gif表情模板=8 + else if (item.elemType == 8) { msgtpl.add(RenderChatItem( data: item, child: Ink( @@ -304,7 +324,8 @@ class _ChatState extends State with SingleTickerProviderStateMixin { maxHeight: 100.0, maxWidth: 100.0, ), - child: Image.asset('assets/images/emotion/${item.customElem?.data}'), + // child: Image.asset('assets/images/emotion/${item.faceElem?.data}'), + child: Image.asset('${item.faceElem?.data}'), ), onLongPress: () { contextMenuDialog(); @@ -313,9 +334,11 @@ class _ChatState extends State with SingleTickerProviderStateMixin { ), )); } - // 图片模板 - else if (item.elemType == 5) { - List imagePaths = item.imageElem?.imageList?.where((e) => e != null && e.url != null).map((e) => e!.url!).toList() ?? []; + // 图片模板=3 + else if (item.elemType == 3) { + // List imagePaths = item.imageElem?.imageList?.where((e) => e != null && e.url != null).map((e) => e!.url!).toList() ?? []; + final originImage = item.imageElem?.imageList?.firstWhere((e) => e?.type == 0 && e?.url != null, orElse: () => null); + List imagePaths = originImage != null ? [originImage.url!] : []; msgtpl.add(RenderChatItem( data: item, child: Ink( @@ -323,9 +346,36 @@ class _ChatState extends State with SingleTickerProviderStateMixin { overlayColor: WidgetStateProperty.all(Colors.transparent), child: ClipRRect( borderRadius: BorderRadius.circular(10.0), - child: ImageGroup( - images: imagePaths, + // child: ImageGroup( + // images: imagePaths, + // width: 120, + // ), + child: Image.network( + imagePaths.first, width: 120, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) { + // controller.scrollToBottom(); + return child; // 加载完成,显示图片 + } + return Container( + width: 120, + height: 240, + color: Colors.grey[300], + alignment: Alignment.center, + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey[300], + alignment: Alignment.center, + child: Icon(Icons.broken_image, color: Colors.grey, size: 40), + ); + }, ), ), onLongPress: () { @@ -335,22 +385,24 @@ class _ChatState extends State with SingleTickerProviderStateMixin { ), )); } - // 视频模板 - else if (item.elemType == 6) { + // 视频模板=5 + else if (item.elemType == 5) { + // print(item.videoElem!.toLogString()); msgtpl.add(RenderChatItem( data: item, child: Ink( child: InkWell( overlayColor: WidgetStateProperty.all(Colors.transparent), child: SizedBox( - width: 90.0, + width: 120.0, child: Stack( alignment: Alignment.center, children: [ ClipRRect( borderRadius: BorderRadius.circular(10.0), - child: Image.network( - item.videoElem?.videoUrl ?? '', + child: NetworkOrAssetImage( + imageUrl: item.videoElem?.snapshotUrl ?? '', + width: 120, ), ), const Align( @@ -365,7 +417,24 @@ class _ChatState extends State with SingleTickerProviderStateMixin { ), ), onTap: () { - MyDialog.toast('该功能暂未支持~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); + showGeneralDialog( + context: context, + // barrierDismissible: true, + barrierColor: Colors.black.withAlpha((1.0 * 255).round()), + pageBuilder: (_, __, ___) { + return SafeArea( + child: PreviewVideo( + videoUrl: item.videoElem?.videoUrl ?? '', + width: item.videoElem?.snapshotWidth?.toDouble(), + height: item.videoElem?.snapshotHeight?.toDouble(), + ), + ); + }, + transitionBuilder: (_, anim, __, child) { + return FadeTransition(opacity: anim, child: child); + }, + transitionDuration: const Duration(milliseconds: 200), + ); }, onLongPress: () { contextMenuDialog(); @@ -374,8 +443,12 @@ class _ChatState extends State with SingleTickerProviderStateMixin { ), )); } - // 语音模板 - else if (item.elemType == 7) { + // 语音模板=4 + else if (item.elemType == 4) { + final durationMs = item.soundElem?.duration ?? 0; + final durationSeconds = (durationMs / 1000).round(); + final maxWidth = (durationSeconds / 60 * 230).clamp(80.0, 230.0); + List audiobody = [ Ink( decoration: BoxDecoration( @@ -388,8 +461,8 @@ class _ChatState extends State with SingleTickerProviderStateMixin { child: Container( padding: const EdgeInsets.all(10.0), constraints: BoxConstraints( - // maxWidth: 120.0, - maxWidth: (item.soundElem?.duration)! / 60 * 230, + maxWidth: maxWidth, + // maxWidth: (item.soundElem!.duration! / 1000) / 60 * 230, ), child: Row( mainAxisAlignment: !(item.isSelf ?? false) ? MainAxisAlignment.start : MainAxisAlignment.end, @@ -399,10 +472,10 @@ class _ChatState extends State with SingleTickerProviderStateMixin { const SizedBox( width: 5.0, ), - Text('${item.soundElem?.duration}'), + Text('$durationSeconds"'), ] : [ - Text('${item.soundElem?.duration}'), + Text('$durationSeconds"'), const SizedBox( width: 5.0, ), @@ -411,7 +484,15 @@ class _ChatState extends State with SingleTickerProviderStateMixin { ), ), onTap: () { - MyDialog.toast('该功能暂未支持~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); + final locUrl = item.soundElem?.path ?? ''; + final netUrl = item.soundElem?.url ?? ''; + if (locUrl.isNotEmpty) { + AudioPlayerService().playNetwork(locUrl); + } else if (netUrl.isNotEmpty) { + AudioPlayerService().playLocal(netUrl); + } else { + MyDialog.toast('音频文件已过期', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); + } }, onLongPress: () { contextMenuDialog(); @@ -421,7 +502,8 @@ class _ChatState extends State with SingleTickerProviderStateMixin { const SizedBox( width: 5.0, ), - FStyle.badge(0, isdot: true), + + // FStyle.badge(0, isdot: true), ]; if (item.isSelf ?? false) { @@ -438,8 +520,160 @@ class _ChatState extends State with SingleTickerProviderStateMixin { children: audiobody, ))); } - // 红包模板 - else if (item.elemType == 0 && item.customElem?.desc == 'hongbao') { + // 分享团购商品 + else if (item.elemType == 2 && item.cloudCustomData == SummaryType.shareTuangou) { + //price,title,url,sell + final obj = jsonDecode(item.customElem!.data!); + logger.e(obj); + final url = obj['url']; + final title = obj['title']; + final price = obj['price']; + final sell = Utils.graceNumber(int.tryParse(obj['sell'])!); + msgtpl.add(RenderChatItem( + data: item, + child: GestureDetector( + child: Container( + width: 160, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(5), + offset: Offset(0.0, 1.0), + blurRadius: 1.0, + spreadRadius: 0.0, + ), + ]), + child: Column( + children: [ + NetworkOrAssetImage( + imageUrl: url, + width: 160.0, + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 5.0, + children: [ + Text( + '$title', + style: TextStyle(fontSize: 14.0, height: 1.2), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text.rich( + overflow: TextOverflow.ellipsis, + maxLines: 1, + TextSpan(style: TextStyle(color: Colors.red, fontSize: 12.0, fontWeight: FontWeight.w700, fontFamily: 'Arial'), children: [ + TextSpan(text: '¥'), + TextSpan( + text: '$price', + style: TextStyle( + fontSize: 14.0, + )), + ]), + ), + ), + SizedBox( + width: 5, + ), + Text( + '已售$sell件', + style: TextStyle(color: Colors.grey, fontSize: 10.0), + ), + ], + ), + ], + ), + ) + ], + ), + ), + onTap: () { + // 这里带上分享人的ID + // Get.toNamed('/goods'); + Get.toNamed('/goods', arguments: {}); + }, + ), + )); + } + // 分享短视频 + else if (item.elemType == 2 && item.cloudCustomData == SummaryType.shareVideo) { + /// {imgUrl,videoUrl,width,height} + final obj = jsonDecode(item.customElem!.data!); + logger.e(obj); + final videoUrl = obj['videoUrl']; + final imgUrl = obj['imgUrl']; + final width = obj['width'] as num; + final height = obj['height'] as num; + msgtpl.add(RenderChatItem( + data: item, + child: Ink( + child: InkWell( + overlayColor: WidgetStateProperty.all(Colors.transparent), + child: SizedBox( + width: 120.0, + child: Stack( + alignment: Alignment.center, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10.0), + child: NetworkOrAssetImage( + imageUrl: imgUrl, + width: 120, + ), + ), + const Align( + alignment: Alignment.center, + child: Icon( + Icons.play_circle, + color: Colors.white, + size: 30.0, + ), + ), + ], + ), + ), + onTap: () { + showGeneralDialog( + context: context, + barrierColor: Colors.black.withAlpha((1.0 * 255).round()), + pageBuilder: (_, __, ___) { + return SafeArea( + bottom: true, + child: Padding( + padding: const EdgeInsets.only(bottom: 4), + child: PreviewVideo( + videoUrl: videoUrl, + width: width.toDouble(), + height: height.toDouble(), + ), + ), + ); + }, + transitionBuilder: (_, anim, __, child) { + return FadeTransition(opacity: anim, child: child); + }, + transitionDuration: const Duration(milliseconds: 200), + ); + }, + onLongPress: () { + contextMenuDialog(); + }, + ), + ), + )); + } + // 红包模板=自定义=2; + else if (item.elemType == 2 && item.cloudCustomData == SummaryType.hongbao) { + final obj = jsonDecode(item.customElem!.data!); + final open = obj['open'] ?? false; + final remark = obj['remark']; + // final maxNum = obj['maxNum']; msgtpl.add(RenderChatItem( data: item, child: Ink( @@ -458,16 +692,26 @@ class _ChatState extends State with SingleTickerProviderStateMixin { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( + width: 210.0, padding: const EdgeInsets.all(10.0), child: Row( - spacing: 10.0, children: [ - Image.asset( - 'assets/images/hbico.png', - width: 32.0, - fit: BoxFit.contain, + open + ? Icon(Icons.check_circle, size: 32.0, color: Colors.white70) + : Image.asset( + 'assets/images/hbico.png', + width: 32.0, + fit: BoxFit.contain, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + '$remark', + style: const TextStyle(color: Colors.white, fontSize: 14.0), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), - Text(item.customElem?.data ?? '', style: const TextStyle(color: Colors.white, fontSize: 14.0)), ], ), ), @@ -477,7 +721,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin { width: double.infinity, decoration: const BoxDecoration(border: Border(top: BorderSide(color: Colors.white30, width: .5))), child: const Text( - '拼手气红包', + '红包', style: TextStyle(color: Colors.white70, fontSize: 11.0), ), ), @@ -494,8 +738,8 @@ class _ChatState extends State with SingleTickerProviderStateMixin { ), )); } - // 位置模板 - else if (item.elemType == 9) { + // 位置模板=7 + else if (item.elemType == 7) { msgtpl.add(RenderChatItem( data: item, child: Ink( @@ -642,7 +886,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin { child: Image.asset(emoj), ), onTap: () { - handleGIFClick(emoj); + handleGIFClick(emoj, item['index']); }, ), ); @@ -722,17 +966,19 @@ class _ChatState extends State with SingleTickerProviderStateMixin { // chatController.animateTo(isNeedScrollBottom ? 0 : chatController.position.maxScrollExtent, // duration: const Duration(milliseconds: 200), curve: Curves.easeIn); // } - void scrollToBottom() { - Future.delayed(Duration(milliseconds: 100), () { - if (chatController.hasClients) { - chatController.animateTo( - 0, // reverse: true 时滚动到底部是 offset: 0 - duration: Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - } - }); - } + + // void scrollToBottom() { + // Future.delayed(Duration(milliseconds: 300), () { + // if (chatController.hasClients) { + // chatController.animateTo( + // // 0, // reverse: true 时滚动到底部是 offset: 0 + // chatController.position.maxScrollExtent, + // duration: Duration(milliseconds: 300), + // curve: Curves.easeOut, + // ); + // } + // }); + // } // 点击消息区域 void handleClickChatArea() { @@ -804,21 +1050,25 @@ class _ChatState extends State with SingleTickerProviderStateMixin { } // 不需要时间标签 - // final res = await IMMessage().sendText( - // text: message['content'], - // toUserID: arguments.value.userID, - // ); + // 消息类型 + late final ImResult res; + res = await IMMessage().sendMessage( + msg: message, + toUserID: arguments.userID, + ); - // if (res.success && res.data != null) { - // messagesToInsert.insert(0, res.data); // 加入消息本体 + if (res.success && res.data != null) { + messagesToInsert.insert(0, res.data); // 加入消息本体 + // messagesToInsert.add(res.data); // 加入消息本体 - // controller.chatList.insertAll(0, messagesToInsert); + controller.chatList.insertAll(0, messagesToInsert); + // controller.chatList.addAll(messagesToInsert); - // scrollToBottom(); - // print('发送成功'); - // } else { - // print('消息发送失败: ${res.code} - ${res.desc}'); - // } + controller.scrollToBottom(); + print('发送成功'); + } else { + print('消息发送失败: ${res.code} - ${res.desc}'); + } } bool needInsertTimeLabel(int lastTimestamp, int newTimestamp, {int interval = 3 * 60}) { @@ -840,7 +1090,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin { toolbarIndex = index; voiceBtnEnable = false; }); - scrollToBottom(); + controller.scrollToBottom(); } // 表情Tab切换 @@ -856,51 +1106,107 @@ class _ChatState extends State with SingleTickerProviderStateMixin { emojController.jumpTo(0); } - // 点击表情 + // 点击表情插入到输入框 void handleEmojClick(emoj) { insertTextAtCursor(emoj); } - // 点击Gif大图 - void handleGIFClick(gifpath) { + // 点击Gif大图发送=8 + void handleGIFClick(gifpath, index) async { // 消息队列 - Map message = { - 'id': Utils.uuid(), - 'contentType': 4, - 'isme': true, - 'avatar': 'assets/images/avatar/img11.jpg', - 'author': 'Andy', - 'content': '', - 'image': gifpath, - 'video': '', - }; - sendMessage(message); + // Map message = { + // 'contentType': 8, + // 'content': gifpath, + // }; + final res = await IMMessage().createFaceMessage(data: gifpath, index: index); + if (res.success) { + sendMessage(res.data?.messageInfo); + } } - // 提交消息 - void handleSubmit() { + // 发送文本消息=1 + void handleSubmit() async { if (editorController.text.isEmpty) return; // 消息队列 - Map message = { - 'id': Utils.uuid(), - 'contentType': 3, - 'isme': true, - 'avatar': 'assets/images/avatar/img11.jpg', - 'author': 'Andy', - 'content': editorController.text, - 'image': '', - 'video': '', - }; - sendMessage(message); - editorController.clear(); + // Map message = { + // 'contentType': 1, + // 'content': editorController.text, + // }; + final res = await IMMessage().createTextMessage(text: editorController.text); + if (res.success) { + sendMessage(res.data?.messageInfo); + editorController.clear(); + } } - // 选择区操作 + // 发红包消息 + void sendHongbao(date) async { + final amount = date['amount']; //用户输入的金额 + final remark = date['remark']; //用户输入的留言 + final maxNum = date['maxNum']; //红包数量 + + // 先检测可用余额 + final makeJson = jsonEncode({ + "amount": amount, + "remark": remark, + "maxNum": maxNum, + "open": false, + }); + final res = await IMMessage().createCustomMessage(data: makeJson); + if (res.success && (res.data != null)) { + final custMsg = res.data!.messageInfo; + custMsg!.cloudCustomData = SummaryType.hongbao; + sendMessage(res.data!.messageInfo); + Get.back(); + } + } + + // 发送图片消息=3 + void sendImage(imgPath) async { + final resImg = await IMMessage().createImageMessage(imagePath: imgPath); + if (resImg.success) { + sendMessage(resImg.data?.messageInfo); + } + } + + // 发送语音消息=4 + void sendVoiceMsg() async { + final fileMap = await VoiceService().stopRecording(); + if (fileMap != null) { + final res = await IMMessage().createSoundMessage( + soundPath: fileMap['path'], + duration: fileMap['duration'], + ); + if (res.success && res.data != null) { + sendMessage(res.data!.messageInfo); + } else { + MyDialog.toast('创建语音消息失败'); + } + } else { + MyDialog.toast('语音限制1-60秒'); + } + } + + // 发送视频消息=5 + void sendVideo(videoFilePath, type, duration, snapshotPath) async { + final resImg = await IMMessage().createVideoMessage( + videoFilePath: videoFilePath, + type: type, + duration: duration, + snapshotPath: snapshotPath, + ); + if (resImg.success) { + sendMessage(resImg.data?.messageInfo); + } + } + + // 底部操作蓝选择区操作 void handleChooseAction(key) { MyDialog.toast('$key'); switch (key) { case 'photo': // .... + pickFile(context); break; case 'camera': // .... @@ -911,12 +1217,97 @@ class _ChatState extends State with SingleTickerProviderStateMixin { } } + ///从相册选取图片/视频 + void pickFile(BuildContext context) async { + final pickedAssets = await AssetPicker.pickAssets( + context, + pickerConfig: AssetPickerConfig( + textDelegate: const AssetPickerTextDelegate(), + pathNameBuilder: (AssetPathEntity album) { + return Utils.translateAlbumName(album); + }, + maxAssets: 5, + requestType: RequestType.common, + filterOptions: FilterOptionGroup( + imageOption: const FilterOption(), + videoOption: const FilterOption( + durationConstraint: DurationConstraint( + max: Duration(seconds: 120), + ), + ), + ), + ), + ); + + if (pickedAssets != null && pickedAssets.isNotEmpty) { + for (final asset in pickedAssets) { + switch (asset.type) { + case AssetType.image: + print("选中了图片:${asset.title}"); + var file = await asset.file; + if (file != null) { + var fileSizeInBytes = await file.length(); + var sizeInMB = fileSizeInBytes / (1024 * 1024); + if (sizeInMB > 28) { + MyDialog.toast('图片大小不能超过28MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); + } else { + print("图片合法,大小:$sizeInMB MB"); + // 执行发送逻辑 + sendImage(file.path); + } + } + + break; + case AssetType.video: + print("选中了视频:${asset.title}"); + var file = await asset.file; + if (file != null) { + var fileSizeInBytes = await file.length(); + var sizeInMB = fileSizeInBytes / (1024 * 1024); + if (sizeInMB > 28) { + MyDialog.toast('图片大小不能超过28MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); + } else { + print("图片合法,大小:$sizeInMB MB"); + // 执行发送逻辑 + var snapshot = await generateVideoThumbnail(file.path); + String? mimeType = await asset.mimeTypeAsync; + String vdType = mimeType?.split('/').last ?? 'mp4'; + print(vdType); + sendVideo(file.path, vdType, asset.duration, snapshot); + } + } + break; + default: + print("不支持的类型:${asset.type}"); + } + } + // final asset = pickedAssets.first; + // final file = await asset.file; // 获取实际文件 + // if (file != null) { + // final fileSizeInBytes = await file.length(); + // final sizeInMB = fileSizeInBytes / (1024 * 1024); + // if (sizeInMB > 100) { + // MyDialog.toast('图片大小不能超过100MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); + // } else { + // print("图片合法,大小:$sizeInMB MB"); + // //走upload(file)上传图片拿到url地址 + // // file; + // } + // } + } + } + /* ---------- { 弹窗功能模块 } ---------- */ // 红包弹窗 - void receiveRedPacketDialog(data) { + void receiveRedPacketDialog(V2TimMessage data) { showDialog( context: context, builder: (context) { + final obj = jsonDecode(data.customElem!.data!); + final amount = obj['amount']; + final remark = obj['remark']; + final open = obj['open'] ?? false; + return Material( type: MaterialType.transparency, child: Column( @@ -925,7 +1316,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin { Container( width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 50.0), - padding: const EdgeInsets.symmetric(vertical: 50.0), + padding: const EdgeInsets.symmetric(vertical: 50.0, horizontal: 20.0), decoration: const BoxDecoration( color: Color(0xFFFF7F43), borderRadius: BorderRadius.all(Radius.circular(12.0)), @@ -934,54 +1325,70 @@ class _ChatState extends State with SingleTickerProviderStateMixin { children: [ ClipRRect( borderRadius: BorderRadius.circular(5.0), - child: Image.asset(data['avatar'], height: 40.0, width: 40.0, fit: BoxFit.cover), + child: NetworkOrAssetImage( + imageUrl: data.senderProfile?.faceUrl, + ), ), const SizedBox( height: 5.0, ), Text( - data['author'], + '${data.senderProfile?.nickName}的红包', style: const TextStyle(color: Color(0xFFFFF9C7), fontWeight: FontWeight.w600), ), + SizedBox(height: 10), Text( - data['content'], + amount, + style: const TextStyle(color: Color(0xFFFFF9C7), fontWeight: FontWeight.w500, fontSize: 20.0), + ), + SizedBox(height: 20.0), + Text( + remark, style: const TextStyle(color: Color(0xFFFFF9C7), fontWeight: FontWeight.w500, fontSize: 20.0), ), SizedBox( height: 100.0, ), - AnimatedBuilder( - animation: animTurns, - builder: (context, child) { - return Transform( - transform: Matrix4.rotationY(animTurns.value), - alignment: Alignment.center, - child: FilledButton( - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all(const Color(0xFFFFF9C7)), - padding: WidgetStateProperty.all(EdgeInsets.zero), - minimumSize: WidgetStateProperty.all(const Size(80.0, 80.0)), - shape: WidgetStateProperty.all(const CircleBorder()), - elevation: WidgetStateProperty.all(3.0), + if (open == false && data.isSelf == false) + AnimatedBuilder( + animation: animTurns, + builder: (context, child) { + return Transform( + transform: Matrix4.rotationY(animTurns.value), + alignment: Alignment.center, + child: FilledButton( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(const Color(0xFFFFF9C7)), + padding: WidgetStateProperty.all(EdgeInsets.zero), + minimumSize: WidgetStateProperty.all(const Size(80.0, 80.0)), + shape: WidgetStateProperty.all(const CircleBorder()), + elevation: WidgetStateProperty.all(3.0), + ), + child: Text( + '开', + style: TextStyle(color: Color(0xFF3B3B3B), fontSize: 28.0), + ), + onPressed: () async { + // 点击开红包,开始动画 + animController.repeat(); + // 执行抢红包结果查询,(群)展示抢红包人员信息,单不用管 + // 执行消费红包动作 + //-------- + // 成功后修改消息体 + obj['open'] = true; //成功标记为true + data.customElem!.data = jsonEncode(obj); + ImService.instance.modifyMessage(message: data); + // 模拟开红包逻辑,1 秒后停止动画 + Future.delayed(Duration(seconds: 1), () { + animController.stop(); + animController.reset(); + Get.back(); + }); + }, ), - child: Text( - '開', - style: TextStyle(color: Color(0xFF3B3B3B), fontSize: 28.0), - ), - onPressed: () { - // 开始动画 - animController.repeat(); - // 模拟开红包逻辑,1 秒后停止动画 - Future.delayed(Duration(seconds: 1), () { - animController.stop(); - animController.reset(); - Get.back(); - }); - }, - ), - ); - }, - ), + ); + }, + ), ], ), ), @@ -1056,8 +1463,10 @@ class _ChatState extends State with SingleTickerProviderStateMixin { context: context, builder: (context) { return RedPacket( - flag: true, - ); + flag: false, + onSend: (date) { + sendHongbao(date); + }); }, ); } @@ -1072,6 +1481,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin { // 页面主体(聊天消息区/底部操作区) Scaffold( backgroundColor: Colors.grey[200], + resizeToAvoidBottomInset: true, // 启用键盘自动避让 appBar: AppBar( centerTitle: true, backgroundColor: Colors.transparent, @@ -1089,7 +1499,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin { title: Obx(() { return Text( // '${arguments['title']}', - '${arguments.value.showName}', + '${arguments.showName}', style: const TextStyle(fontSize: 18.0, fontFamily: 'Arial'), ); }), @@ -1219,19 +1629,31 @@ class _ChatState extends State with SingleTickerProviderStateMixin { children: [ // 渲染聊天消息 Expanded( - child: ScrollConfiguration( - behavior: CustomScrollBehavior(), - child: GestureDetector( - child: Obx(() { - return ListView( - controller: chatController, - reverse: true, - padding: const EdgeInsets.all(10.0), - children: renderChatList(), - ); - }), - onTap: () { - handleClickChatArea(); + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: handleClickChatArea, + child: LayoutBuilder( + builder: (context, constraints) { + return Obx(() { + final msgWidgets = renderChatList().reversed.toList(); + + return ListView( + controller: chatController, + reverse: true, + padding: const EdgeInsets.all(10.0), + children: [ + ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 40, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: msgWidgets, + ), + ), + ], + ); + }); }, ), ), @@ -1239,201 +1661,210 @@ class _ChatState extends State with SingleTickerProviderStateMixin { // 底部操作栏 Container( - color: Colors.grey[100], - child: SafeArea( - bottom: true, - child: Container( - decoration: BoxDecoration( - color: Colors.grey[100], - border: const Border(top: BorderSide(color: Colors.black38, width: .1)), - ), - child: Column( - children: [ - // 输入框编辑器模块 - Container( - padding: const EdgeInsets.all(10.0), - child: Row( - children: [ - InkWell( - child: Icon( - voiceBtnEnable ? Icons.keyboard_outlined : Icons.contactless_outlined, - color: const Color(0xFF3B3B3B), - size: 30.0, + color: Colors.grey[100], + child: SafeArea( + bottom: true, + child: Container( + decoration: BoxDecoration( + color: Colors.grey[100], + border: const Border(top: BorderSide(color: Colors.black38, width: .1)), + ), + child: Column( + children: [ + // 输入框编辑器模块 + Container( + padding: const EdgeInsets.all(10.0), + child: Row( + children: [ + InkWell( + child: Icon( + voiceBtnEnable ? Icons.keyboard_outlined : Icons.contactless_outlined, + color: const Color(0xFF3B3B3B), + size: 30.0, + ), + onTap: () { + setState(() { + toolbarEnable = false; + if (voiceBtnEnable) { + voiceBtnEnable = false; + editorFocusNode.requestFocus(); + } else { + voiceBtnEnable = true; + editorFocusNode.unfocus(); + } + }); + }, + ), + const SizedBox( + width: 10.0, + ), + Expanded( + child: Container( + constraints: const BoxConstraints(minHeight: 40.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), ), - onTap: () { - setState(() { - toolbarEnable = false; - if (voiceBtnEnable) { - voiceBtnEnable = false; - editorFocusNode.requestFocus(); - } else { - voiceBtnEnable = true; - editorFocusNode.unfocus(); - } - }); - }, - ), - const SizedBox( - width: 10.0, - ), - Expanded( - child: Container( - constraints: const BoxConstraints(minHeight: 40.0), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(5), - ), - child: Stack( - children: [ - // 输入框 - Offstage( - offstage: voiceBtnEnable, - child: TextField( - decoration: const InputDecoration( - isDense: true, - hoverColor: Colors.transparent, - contentPadding: EdgeInsets.all(8.0), - border: OutlineInputBorder(borderSide: BorderSide.none), - ), - style: const TextStyle( - fontSize: 16.0, - ), - maxLines: null, - controller: editorController, - focusNode: editorFocusNode, - cursorColor: const Color(0xFF07C160), - onChanged: (value) {}, + child: Stack( + children: [ + // 输入框 + Offstage( + offstage: voiceBtnEnable, + child: TextField( + decoration: const InputDecoration( + isDense: true, + hoverColor: Colors.transparent, + contentPadding: EdgeInsets.all(8.0), + border: OutlineInputBorder(borderSide: BorderSide.none), ), + style: const TextStyle( + fontSize: 16.0, + ), + maxLines: null, + controller: editorController, + focusNode: editorFocusNode, + cursorColor: const Color(0xFF07C160), + onChanged: (value) {}, ), - // 语音 - Offstage( - offstage: !voiceBtnEnable, - child: GestureDetector( - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(5), - ), - alignment: Alignment.center, - height: 40.0, - width: double.infinity, - child: Text( - voiceTypeMap[voiceType], - style: const TextStyle(fontSize: 15.0), - ), + ), + // 语音 + Offstage( + offstage: !voiceBtnEnable, + child: GestureDetector( + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), ), - onPanStart: (details) { + alignment: Alignment.center, + height: 40.0, + width: double.infinity, + child: Text( + voiceTypeMap[voiceType], + style: const TextStyle(fontSize: 15.0), + ), + ), + onPanStart: (details) async { + // 开始录音 + final res = await VoiceService().startRecording(); + if (res) { setState(() { voiceType = 1; voicePanelEnable = true; }); - }, - onPanUpdate: (details) { - Offset pos = details.globalPosition; - double swipeY = MediaQuery.of(context).size.height - 120; - double swipeX = MediaQuery.of(context).size.width / 2 + 50; - setState(() { - if (pos.dy >= swipeY) { - voiceType = 1; // 松开发送 - } else if (pos.dy < swipeY && pos.dx < swipeX) { - voiceType = 2; // 左滑松开取消 - } else if (pos.dy < swipeY && pos.dx >= swipeX) { - voiceType = 3; // 右滑语音转文字 - } - }); - }, - onPanEnd: (details) { - // print('停止录音'); - setState(() { - switch (voiceType) { - case 1: - MyDialog.toast('发送录音文件'); - voicePanelEnable = false; - break; - case 2: - MyDialog.toast('取消发送'); - voicePanelEnable = false; - break; - case 3: - MyDialog.toast('语音转文字'); - voicePanelEnable = true; - voiceToTransfer = true; - break; - } - voiceType = 0; - }); - }, - ), + } else { + MyDialog.toast('未获得麦克风权限'); + } + }, + onPanUpdate: (details) { + Offset pos = details.globalPosition; + double swipeY = MediaQuery.of(context).size.height - 120; + double swipeX = MediaQuery.of(context).size.width / 2 + 50; + setState(() { + if (pos.dy >= swipeY) { + voiceType = 1; // 松开发送 + } else if (pos.dy < swipeY && pos.dx < swipeX) { + voiceType = 2; // 左滑松开取消 + } else if (pos.dy < swipeY && pos.dx >= swipeX) { + voiceType = 3; // 右滑语音转文字 + } + }); + }, + onPanEnd: (details) { + // print('停止录音'); + setState(() { + switch (voiceType) { + case 1: + // MyDialog.toast('发送录音文件'); + sendVoiceMsg(); + voicePanelEnable = false; + break; + case 2: + // MyDialog.toast('取消发送'); + VoiceService().cancelRecording; + voicePanelEnable = false; + break; + case 3: + MyDialog.toast('语音转文字'); + voicePanelEnable = true; + voiceToTransfer = true; + break; + } + voiceType = 0; + }); + }, ), - ], - ), + ), + ], ), ), - const SizedBox( - width: 10.0, + ), + const SizedBox( + width: 10.0, + ), + InkWell( + child: const Icon( + Icons.add_reaction_rounded, + color: Color(0xFF3B3B3B), + size: 30.0, ), - InkWell( + onTap: () { + handleEmojChooseState(0); + }, + ), + const SizedBox( + width: 8.0, + ), + InkWell( + child: const Icon( + Icons.add, + color: Color(0xFF3B3B3B), + size: 30.0, + ), + onTap: () { + handleEmojChooseState(1); + }, + ), + const SizedBox( + width: 8.0, + ), + InkWell( + child: Container( + height: 25.0, + width: 25.0, + decoration: BoxDecoration( + color: const Color(0xFF07C160), + borderRadius: BorderRadius.circular(20.0), + ), child: const Icon( - Icons.add_reaction_rounded, - color: Color(0xFF3B3B3B), - size: 30.0, + Icons.arrow_upward, + color: Colors.white, + size: 20.0, ), - onTap: () { - handleEmojChooseState(0); - }, ), - const SizedBox( - width: 8.0, - ), - InkWell( - child: const Icon( - Icons.add, - color: Color(0xFF3B3B3B), - size: 30.0, - ), - onTap: () { - handleEmojChooseState(1); - }, - ), - const SizedBox( - width: 8.0, - ), - InkWell( - child: Container( - height: 25.0, - width: 25.0, - decoration: BoxDecoration( - color: const Color(0xFF07C160), - borderRadius: BorderRadius.circular(20.0), - ), - child: const Icon( - Icons.arrow_upward, - color: Colors.white, - size: 20.0, - ), - ), - onTap: () { - handleSubmit(); - }, - ), - ], + onTap: () { + handleSubmit(); + }, + ), + ], + ), + ), + + // 表情+选择模块 + Visibility( + visible: toolbarEnable, + child: SizedBox( + height: keyboardHeight, + child: Column( + children: toolbarIndex == 0 ? renderEmojWidget() : renderChooseWidget(), ), ), - - // 表情+选择模块 - Visibility( - visible: toolbarEnable, - child: SizedBox( - height: keyboardHeight, - child: Column( - children: toolbarIndex == 0 ? renderEmojWidget() : renderChooseWidget(), - ), - ), - ) - ], - ), + ) + ], ), - )) + ), + ), + ) ], ), ), @@ -1448,13 +1879,14 @@ class _ChatState extends State with SingleTickerProviderStateMixin { children: [ // 取消发送+语音转文字 Positioned( - bottom: 120, + bottom: 160, left: 30, right: 30, child: Visibility( visible: !voiceToTransfer, child: Column( - crossAxisAlignment: voiceType == 2 ? CrossAxisAlignment.start : CrossAxisAlignment.center, + // crossAxisAlignment: voiceType == 2 ? CrossAxisAlignment.start : CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ // 语音动画层 Stack( @@ -1463,7 +1895,8 @@ class _ChatState extends State with SingleTickerProviderStateMixin { AnimatedContainer( duration: Duration(milliseconds: 200), height: 70.0, - width: voiceType == 2 ? 70.0 : 200.0, + // width: voiceType == 2 ? 70.0 : 200.0, + width: 200.0, decoration: BoxDecoration( color: voiceType == 2 ? Colors.red : Color(0xFF89E45B), borderRadius: BorderRadius.circular(15.0), @@ -1487,7 +1920,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin { ), // 操作项 Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.center, children: [ // 取消发送 Container( @@ -1503,18 +1936,18 @@ class _ChatState extends State with SingleTickerProviderStateMixin { ), ), // 语音转文字 - Container( - height: 60.0, - width: 60.0, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(50.0), - color: voiceType == 3 ? Color(0xFF89E45B) : Colors.black38, - ), - child: Icon( - Icons.translate, - color: Colors.white54, - ), - ), + // Container( + // height: 60.0, + // width: 60.0, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(50.0), + // color: voiceType == 3 ? Color(0xFF89E45B) : Colors.black38, + // ), + // child: Icon( + // Icons.translate, + // color: Colors.white54, + // ), + // ), ], ), ], @@ -1567,7 +2000,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin { ), // 操作项 Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( child: Container( @@ -1661,7 +2094,12 @@ class _ChatState extends State with SingleTickerProviderStateMixin { alignment: Alignment.bottomCenter, child: Visibility( visible: !voiceToTransfer, - child: Image.asset('assets/images/voice_bg.webp', width: double.infinity, height: 100.0, fit: BoxFit.fill), + child: Image.asset( + 'assets/images/voice_bg.webp', + width: double.infinity, + height: 100.0, + fit: BoxFit.fill, + ), ), ), // 背景图标 @@ -1716,18 +2154,32 @@ class RenderChatItem extends StatelessWidget { @override Widget build(BuildContext context) { + String? displayName = (data.friendRemark?.isNotEmpty ?? false) + ? data.friendRemark + : (data.nameCard?.isNotEmpty ?? false) + ? data.nameCard + : (data.nickName?.isNotEmpty ?? false) + ? data.nickName + : '未知昵称'; return Container( margin: const EdgeInsets.only(bottom: 10.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ !(data.isSelf ?? false) - ? SizedBox( - height: 35.0, - width: 35.0, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(20.0)), - child: Image.network(data.faceUrl ?? 'https://wuzhongjie.com.cn/download/logo.png'), + ? GestureDetector( + onTap: () { + // 头像点击事件 + logger.e("点击了头像"); + Get.toNamed('/vloger', arguments: {'memberId': data.sender}); + }, + child: SizedBox( + height: 35.0, + width: 35.0, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(20.0)), + child: NetworkOrAssetImage(imageUrl: data.faceUrl), + ), ), ) : const SizedBox.shrink(), @@ -1737,13 +2189,13 @@ class RenderChatItem extends StatelessWidget { child: Column( crossAxisAlignment: !(data.isSelf ?? false) ? CrossAxisAlignment.start : CrossAxisAlignment.end, children: [ - Text( - data.friendRemark ?? data.nameCard ?? data.nickName ?? '未知昵称', - style: const TextStyle(color: Colors.grey, fontSize: 12.0), - ), - const SizedBox( - height: 3.0, - ), + // Text( + // displayName ?? '未知昵称', + // style: const TextStyle(color: Colors.grey, fontSize: 12.0), + // ), + // const SizedBox( + // height: 3.0, + // ), Stack( children: [ // 气泡箭头 @@ -1775,7 +2227,7 @@ class RenderChatItem extends StatelessWidget { width: 35.0, child: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(20.0)), - child: Image.network(data.faceUrl ?? 'https://wuzhongjie.com.cn/download/logo.png'), + child: NetworkOrAssetImage(imageUrl: data.faceUrl), ), ) : const SizedBox.shrink(), diff --git a/lib/pages/groupChat/index.dart b/lib/pages/groupChat/index.dart index 1af2c04..8510d72 100644 --- a/lib/pages/groupChat/index.dart +++ b/lib/pages/groupChat/index.dart @@ -1,5 +1,16 @@ +// 创建群聊 import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:loopin/IM/controller/im_user_info_controller.dart'; +import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/components/network_or_asset_image.dart'; +import 'package:loopin/styles/index.dart'; +import 'package:tencent_cloud_chat_sdk/enum/group_member_role_enum.dart'; +import 'package:tencent_cloud_chat_sdk/enum/group_type.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_group_member.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; +import 'package:tencent_cloud_chat_sdk/web/compatible_models/v2_tim_conversation.dart'; class StartGroupChatPage extends StatefulWidget { const StartGroupChatPage({super.key}); @@ -11,11 +22,12 @@ class StartGroupChatPage extends StatefulWidget { class _StartGroupChatPageState extends State { final TextEditingController _searchController = TextEditingController(); - List> dataList = []; - List> filteredList = []; + List dataList = []; + List filteredList = []; Set selectedIds = {}; // 已选中的用户 id - int page = 1; + String page = ''; bool hasMore = true; + bool isLoading = true; @override void initState() { @@ -29,29 +41,38 @@ class _StartGroupChatPageState extends State { super.dispose(); } + // 分页获取陌关注列表数据 Future _loadData({bool reset = false}) async { if (reset) { - page = 1; + page = ''; hasMore = true; dataList.clear(); } - - // 模拟网络请求 - await Future.delayed(const Duration(seconds: 1)); - - List> newItems = List.generate( - 10, - (index) => {"nickName": "用户 ${(page - 1) * 10 + index + 1}", "id": "${page}_$index"}, + final res = await ImService.instance.getMutualFollowersList( + nextCursor: page, ); + if (res.success && res.data != null) { + final userInfoList = res.data!.userFullInfoList ?? []; + final isFinished = res.data!.nextCursor == null || res.data!.nextCursor!.isEmpty; + logger.w('获取成功:${res.data!.nextCursor},是否还有更多:$isFinished'); - if (newItems.isEmpty) { - hasMore = false; + if (isFinished) { + setState(() { + hasMore = false; + }); + // 加载没数据了 + page = ''; + } else { + page = res.data!.nextCursor ?? ''; + } + logger.i('获取数据成功:$userInfoList'); + setState(() { + dataList.addAll(userInfoList); + _applySearch(_searchController.text); + }); } else { - dataList.addAll(newItems); - page++; + logger.e('获取数据失败:${res.desc}'); } - - _applySearch(_searchController.text); } void _applySearch(String query) { @@ -59,7 +80,7 @@ class _StartGroupChatPageState extends State { if (query.isEmpty) { filteredList = List.from(dataList); } else { - filteredList = dataList.where((item) => item["nickName"].toString().contains(query.trim())).toList(); + filteredList = dataList.where((item) => (item.nickName ?? '').contains(query.trim())).toList(); } }); } @@ -74,6 +95,77 @@ class _StartGroupChatPageState extends State { }); } + /// 创建群聊 + void createGroup(Set selectedIds) async { + // dataList是原始数据List dataList = []; + // 通过dataList和selectedIds来构建memberList + final ctl = Get.find(); + final memberList = dataList + .where((user) => selectedIds.contains(user.userID)) + .map((user) => V2TimGroupMember( + userID: user.userID!, + role: GroupMemberRoleTypeEnum.V2TIM_GROUP_MEMBER_ROLE_MEMBER, // 加群的成员角色(默认普通成员) + )) + .toList(); + final self = V2TimGroupMember( + userID: ctl.userID.value, + role: GroupMemberRoleTypeEnum.V2TIM_GROUP_MEMBER_ROLE_OWNER, // 建群的人为群主 + ); + memberList.insert(0, self); + final groupName = buildGroupName(selectedIds); + final res = await ImService.instance.createGroup(groupType: GroupType.Work, groupName: groupName, memberList: memberList); + if (res.success) { + final groupID = res.data; + logger.w(groupID); + final V2TimConversation conv = V2TimConversation(conversationID: 'group_$groupID'); + conv.showName = groupName; + Get.toNamed('/chatGroup', arguments: conv); + } + } + + // 构建默认群名称 + String buildGroupName(Set selectedIds) { + int maxLength = 20; + // 根据 selectedIds 找到对应的昵称 + final memberNicknames = selectedIds.map((id) { + final user = dataList.firstWhere( + (u) => u.userID == id, + orElse: () => V2TimUserFullInfo(userID: id, nickName: id), + ); + return user.nickName?.isNotEmpty == true ? user.nickName! : user.userID; + }).toList(); + final ctl = Get.find(); + // 插入群主的信息 + memberNicknames.insert(0, ctl.nickname.value); + + // 拼接成群名 + String name = memberNicknames.join("、"); + + // 如果超过长度限制,截断 + if (name.length > maxLength) { + name = "${name.substring(0, maxLength - 3)}..."; + } + + return name; + } + + // 空状态提示 + Widget _emptyTip(String text) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset('assets/images/empty.png', width: 100), + const SizedBox(height: 8), + Text( + text, + style: const TextStyle(color: Colors.grey, fontSize: 13), + ), + ], + ), + ); + } + Widget _buildCard({ required IconData icon, required String title, @@ -96,7 +188,6 @@ class _StartGroupChatPageState extends State { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( - // backgroundColor: Colors.white, centerTitle: true, forceMaterialTransparency: true, bottom: PreferredSize( @@ -122,23 +213,23 @@ class _StartGroupChatPageState extends State { icon: Icons.public, title: "创建公开群", onTap: () { - print("跳转到 创建公开群"); - }, - ), - _buildCard( - icon: Icons.group, - title: "创建好友群", - onTap: () { - print("跳转到 创建好友群"); + logger.w("跳转到 创建公开群"); }, ), + // _buildCard( + // icon: Icons.group, + // title: "创建好友群", + // onTap: () { + // logger.w("跳转到 创建好友群"); + // }, + // ), ], ), ), const Divider(), - // 下半部分 - 搜索框 + 列表 + // 下半部分 Expanded( child: Column( children: [ @@ -161,30 +252,56 @@ class _StartGroupChatPageState extends State { // 列表 Expanded( child: EasyRefresh( + header: ClassicHeader( + dragText: '下拉刷新', + armedText: '释放刷新', + readyText: '加载中...', + processingText: '加载中...', + processedText: '加载完成', + failedText: '加载失败,请重试', + messageText: '最后更新于 %T', + ), + footer: ClassicFooter( + dragText: '加载更多', + armedText: '释放加载', + readyText: '加载中...', + processingText: '加载中...', + processedText: hasMore ? '加载完成' : '没有更多了~', + failedText: '加载失败,请重试', + messageText: '最后更新于 %T', + ), onRefresh: () async => _loadData(reset: true), onLoad: () async { if (hasMore) await _loadData(); }, - child: ListView.builder( - itemCount: filteredList.length, - itemBuilder: (context, index) { - final item = filteredList[index]; - final id = item["id"] as String; - final isSelected = selectedIds.contains(id); + child: filteredList.isEmpty + ? _emptyTip('暂无数据') + : ListView.builder( + itemCount: filteredList.length, + itemBuilder: (context, index) { + final item = filteredList[index]; + final id = item.userID as String; + final isSelected = selectedIds.contains(id); - return ListTile( - leading: CircleAvatar( - child: Text(item["nickName"].substring(0, 1)), + return ListTile( + leading: ClipOval( + // child: Text((item.nickName ?? '未知').substring(0, 1)), + child: NetworkOrAssetImage( + imageUrl: item.faceUrl, + width: 48, + height: 48, + ), + ), + title: Text(item.nickName ?? '未知'), + trailing: Checkbox( + value: isSelected, + shape: const CircleBorder(), + onChanged: (_) => _toggleSelection(id), + ), + onTap: () => _toggleSelection(id), + ); + }, ), - title: Text(item["nickName"]), - trailing: Checkbox( - value: isSelected, - onChanged: (_) => _toggleSelection(id), - ), - onTap: () => _toggleSelection(id), - ); - }, - ), ), ), ], @@ -195,25 +312,39 @@ class _StartGroupChatPageState extends State { // 底部按钮 bottomNavigationBar: SafeArea( - child: Padding( - padding: const EdgeInsets.all(12.0), - child: ElevatedButton( - onPressed: selectedIds.isEmpty - ? null - : () { - print("选择了用户:$selectedIds"); - }, - style: ElevatedButton.styleFrom( - minimumSize: const Size.fromHeight(50), - backgroundColor: selectedIds.isEmpty ? Colors.grey : Colors.blue, - ), - child: const Text( - "发起聊天", - style: TextStyle(fontSize: 16), - ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: TweenAnimationBuilder( + duration: const Duration(milliseconds: 300), + tween: ColorTween( + begin: Colors.grey, + end: selectedIds.isEmpty ? Colors.grey : FStyle.primaryColor, ), + builder: (context, bgColor, _) { + return ElevatedButton( + onPressed: selectedIds.isEmpty + ? null + : () { + logger.w("选择了用户:$selectedIds"); + createGroup(selectedIds); + }, + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(50), + backgroundColor: bgColor, + shape: const StadiumBorder(), // 胶囊形状 + ), + child: AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 300), + style: TextStyle( + fontSize: 16, + color: selectedIds.isEmpty ? Colors.black : Colors.white, + ), + child: const Text("发起聊天"), + ), + ); + }, ), - ), + )), ); } } diff --git a/lib/pages/my/index.dart b/lib/pages/my/index.dart index bed0897..027d5d3 100644 --- a/lib/pages/my/index.dart +++ b/lib/pages/my/index.dart @@ -853,7 +853,7 @@ class MyPageState extends State with SingleTickerProviderStateMixin { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildOrderIcon('assets/images/ico_order.png', '订单', () { - Get.toNamed('/order'); + Get.toNamed('/sellerOrder'); }), _buildOrderIcon('assets/images/ico_dhx.png', '余额logout', () { showLogoutDialog(context); diff --git a/lib/pages/order/index.dart b/lib/pages/order/index.dart index cb2c528..3793955 100644 --- a/lib/pages/order/index.dart +++ b/lib/pages/order/index.dart @@ -18,8 +18,8 @@ class _OrderState extends State with SingleTickerProviderStateMixin { List tabList = [ {'name': "全部"}, - {'name': "待付款", 'badge': 1}, - {'name': "待核销"}, + {'name': "待退款", 'badge': 1}, + {'name': "待核销", 'badge': 1}, {'name': "已完成"}, {'name': "取消"} ]; @@ -65,28 +65,33 @@ class _OrderState extends State with SingleTickerProviderStateMixin { forceMaterialTransparency: true, titleSpacing: 1.0, title: Container( - height: 35.0, + // height: 35.0, decoration: BoxDecoration( - color: Colors.grey[50], + // color: Colors.grey[50], + color: Colors.white, borderRadius: BorderRadius.circular(30.0), ), - child: TextField( - decoration: InputDecoration( - isDense: true, - hintText: "搜索订单", - hintStyle: TextStyle(color: Colors.black38, fontSize: 14.0), - prefixIcon: Icon( - Icons.search, - color: Colors.black38, - size: 21.0, - ), - contentPadding: EdgeInsets.symmetric(vertical: 0, horizontal: 10.0), - border: OutlineInputBorder(borderSide: BorderSide.none, borderRadius: BorderRadius.circular(30.0))), - cursorColor: Colors.black, - onChanged: (val) { - debugPrint(val); - }, + child: Text( + '订单', + style: TextStyle(fontSize: 18), ), + // child: TextField( + // decoration: InputDecoration( + // isDense: true, + // hintText: "搜索订单", + // hintStyle: TextStyle(color: Colors.black38, fontSize: 14.0), + // prefixIcon: Icon( + // Icons.search, + // color: Colors.black38, + // size: 21.0, + // ), + // contentPadding: EdgeInsets.symmetric(vertical: 0, horizontal: 10.0), + // border: OutlineInputBorder(borderSide: BorderSide.none, borderRadius: BorderRadius.circular(30.0))), + // cursorColor: Colors.black, + // onChanged: (val) { + // debugPrint(val); + // }, + // ), ), bottom: PreferredSize( preferredSize: Size.fromHeight(45.0), @@ -99,10 +104,16 @@ class _OrderState extends State with SingleTickerProviderStateMixin { alignment: Alignment.center, height: 45.0, child: Badge.count( - backgroundColor: Colors.red, - count: item['badge'] ?? 0, - isLabelVisible: item['badge'] != null ? true : false, - child: Text(item['name'])), + backgroundColor: Colors.red, + count: item['badge'] ?? 0, + isLabelVisible: item['badge'] != null ? true : false, + child: Text( + item['name'], + style: TextStyle(fontSize: 16), + overflow: TextOverflow.ellipsis, + softWrap: false, + ), + ), )) .toList(), isScrollable: false, @@ -129,188 +140,363 @@ class _OrderState extends State with SingleTickerProviderStateMixin { behavior: CustomScrollBehavior().copyWith(scrollbars: false), child: Container( color: Colors.grey[50], - child: TabBarView(controller: tabController, children: [ - ListView( - controller: scrollController, - physics: BouncingScrollPhysics(), - padding: EdgeInsets.all(10.0), - children: [ - GestureDetector( - child: Container( - margin: EdgeInsets.only(bottom: 10.0), - padding: EdgeInsets.all(10.0), - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(10), - offset: Offset(0.0, 1.0), - blurRadius: 1.0, - spreadRadius: 0.0, - ), - ]), - child: Column( - spacing: 10.0, - children: [ - Row( - children: [ - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 5.0, - children: [ - ClipOval( - child: Image.asset( - 'assets/images/avatar/img11.jpg', - width: 25.0, - ), - ), - Text('老白干自营旗舰店'), - Icon( - Icons.arrow_forward_ios_rounded, - color: Colors.grey, - size: 12.0, - ), - ], - ), - Spacer(), - Text( - '待付款', - style: TextStyle(color: Colors.red), - ) - ], + child: TabBarView( + controller: tabController, + children: [ + ListView( + controller: scrollController, + physics: BouncingScrollPhysics(), + padding: EdgeInsets.all(10.0), + children: [ + GestureDetector( + child: Container( + margin: EdgeInsets.only(bottom: 10.0), + padding: EdgeInsets.all(10.0), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(10), + offset: Offset(0.0, 1.0), + blurRadius: 1.0, + spreadRadius: 0.0, ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 10.0, - children: [ - Image.network( - 'https://img13.360buyimg.com/n1/jfs/t1/263909/5/4187/123220/676eb220F3e481086/0cee829b1894fc4c.jpg', - width: 80.0, - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ]), + child: Column( + spacing: 10.0, + children: [ + Row( + children: [ + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, spacing: 5.0, children: [ - Text( - '茅台(MOUTAI)飞天 53度 酱香型白酒 500ml*2 海外版送礼袋年货送礼', - maxLines: 2, - overflow: TextOverflow.ellipsis, + ClipOval( + child: Image.asset( + 'assets/images/avatar/img11.jpg', + width: 25.0, + ), ), - Row( - children: [ - Text( - '¥3800', - style: TextStyle(color: Colors.red), - ), - Spacer(), - Text( - 'x10', - style: TextStyle(color: Colors.grey), - ), - ], + Text('老白干自营旗舰店'), + Icon( + Icons.arrow_forward_ios_rounded, + color: Colors.grey, + size: 12.0, ), ], ), - ) - ], - ), - // 提示信息 - Container( - padding: EdgeInsets.all(5.0), - decoration: BoxDecoration( - color: Colors.grey[50], - borderRadius: BorderRadius.circular(5.0), - ), - child: Row( - children: [ Spacer(), - Text.rich( - TextSpan(children: [ - TextSpan(text: '实付款: '), - TextSpan( - text: '¥38000', - style: TextStyle(color: Colors.red), - ), - ]), + Text( + '待付款', + style: TextStyle(color: Colors.red), + ) + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 10.0, + children: [ + Image.network( + 'https://img13.360buyimg.com/n1/jfs/t1/263909/5/4187/123220/676eb220F3e481086/0cee829b1894fc4c.jpg', + width: 80.0, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 5.0, + children: [ + Text( + '茅台(MOUTAI)飞天 53度 酱香型白酒 500ml*2 海外版送礼袋年货送礼', + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Row( + children: [ + Text( + '¥3800', + style: TextStyle(color: Colors.red), + ), + Spacer(), + Text( + 'x10', + style: TextStyle(color: Colors.grey), + ), + ], + ), + ], + ), + ) + ], + ), + // 提示信息 + Container( + padding: EdgeInsets.all(5.0), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(5.0), + ), + child: Row( + children: [ + Spacer(), + Text.rich( + TextSpan(children: [ + TextSpan(text: '实付款: '), + TextSpan( + text: '¥38000', + style: TextStyle(color: Colors.red), + ), + ]), + ), + ], + ), + ), + // 按钮组 + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('取消订单'), + ), + ElevatedButton( + onPressed: () {}, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(Color(0xff07c160)), foregroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('去支付'), + ), + ], + ), + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () {}, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(Color(0xFF10B9FC)), foregroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('评价'), + ), + ], + ), + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('申请退款'), + ), + ElevatedButton( + onPressed: () {}, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(Color(0xFFFCBE13)), foregroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('联系客服'), ), ], ), - ), - // 按钮组 - Row( - spacing: 10.0, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton( - onPressed: () {}, - style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.white)), - child: Text('取消订单'), - ), - ElevatedButton( - onPressed: () {}, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all(Color(0xff07c160)), foregroundColor: WidgetStateProperty.all(Colors.white)), - child: Text('去支付'), - ), - ], - ), - Row( - spacing: 10.0, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton( - onPressed: () {}, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all(Color(0xFF10B9FC)), foregroundColor: WidgetStateProperty.all(Colors.white)), - child: Text('评价'), - ), - ], - ), - Row( - spacing: 10.0, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton( - onPressed: () {}, - style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.white)), - child: Text('申请退款'), - ), - ElevatedButton( - onPressed: () {}, - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all(Color(0xFFFCBE13)), foregroundColor: WidgetStateProperty.all(Colors.white)), - child: Text('联系客服'), - ), - ], - ), - Row( - spacing: 10.0, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - ElevatedButton( - onPressed: () {}, - style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.white)), - child: Text('删除'), - ), - ], - ), - ], + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('删除'), + ), + ], + ), + ], + ), ), + onTap: () { + Get.toNamed('/order/detail'); + }, ), - onTap: () { - Get.toNamed('/order/detail'); - }, - ), - ], - ), - emptyTip(), - emptyTip(), - emptyTip(), - emptyTip(), - emptyTip(), - ]), + ], + ), + emptyTip(), + hexiao(), + emptyTip(), + emptyTip(), + emptyTip(), + ], + ), ), ), ); } } + +Widget hexiao() { + return ListView( + physics: BouncingScrollPhysics(), + padding: EdgeInsets.all(10.0), + children: [ + GestureDetector( + child: Container( + margin: EdgeInsets.only(bottom: 10.0), + padding: EdgeInsets.all(10.0), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(10), + offset: Offset(0.0, 1.0), + blurRadius: 1.0, + spreadRadius: 0.0, + ), + ]), + child: Column( + spacing: 10.0, + children: [ + Row( + children: [ + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 5.0, + children: [ + ClipOval( + child: Image.asset( + 'assets/images/avatar/img11.jpg', + width: 25.0, + ), + ), + Text('老白干自营旗舰店'), + Icon( + Icons.arrow_forward_ios_rounded, + color: Colors.grey, + size: 12.0, + ), + ], + ), + Spacer(), + Text( + '待付款', + style: TextStyle(color: Colors.red), + ) + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 10.0, + children: [ + Image.network( + 'https://img13.360buyimg.com/n1/jfs/t1/263909/5/4187/123220/676eb220F3e481086/0cee829b1894fc4c.jpg', + width: 80.0, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 5.0, + children: [ + Text( + '茅台(MOUTAI)飞天 53度 酱香型白酒 500ml*2 海外版送礼袋年货送礼', + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Row( + children: [ + Text( + '¥3800', + style: TextStyle(color: Colors.red), + ), + Spacer(), + Text( + 'x10', + style: TextStyle(color: Colors.grey), + ), + ], + ), + ], + ), + ) + ], + ), + // 提示信息 + Container( + padding: EdgeInsets.all(5.0), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(5.0), + ), + child: Row( + children: [ + Spacer(), + Text.rich( + TextSpan(children: [ + TextSpan(text: '实付款: '), + TextSpan( + text: '¥38000', + style: TextStyle(color: Colors.red), + ), + ]), + ), + ], + ), + ), + // 按钮组 + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('取消订单'), + ), + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Color(0xff07c160)), foregroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('去支付'), + ), + ], + ), + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Color(0xFF10B9FC)), foregroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('评价'), + ), + ], + ), + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('申请退款'), + ), + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Color(0xFFFCBE13)), foregroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('联系客服'), + ), + ], + ), + + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('删除'), + ), + ], + ), + ], + ), + ), + onTap: () { + Get.toNamed('/order/detail'); + }, + ), + ], + ); +} diff --git a/lib/pages/order/seller.dart b/lib/pages/order/seller.dart new file mode 100644 index 0000000..c3e764c --- /dev/null +++ b/lib/pages/order/seller.dart @@ -0,0 +1,588 @@ +/// 商家的订单 +library; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../behavior/custom_scroll_behavior.dart'; + +class Seller extends StatefulWidget { + const Seller({super.key}); + + @override + State createState() => _SellerState(); +} + +class _SellerState extends State with SingleTickerProviderStateMixin { + GlobalKey scaffoldKey = GlobalKey(); + + List tabList = [ + {'name': "全部"}, + {'name': "已退款", 'badge': 1}, + {'name': "待核销", 'badge': 1}, + {'name': "已完成"}, + {'name': "取消"} + ]; + + late ScrollController scrollController = ScrollController(); + late TabController tabController = TabController(initialIndex: 0, length: tabList.length, vsync: this); + + Widget emptyTip() { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 5.0, + children: [ + Image.asset( + 'assets/images/empty.png', + width: 100.0, + ), + Text( + '还没有相关订单~', + style: TextStyle(color: Colors.grey, fontSize: 12.0), + ) + ], + ); + } + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + scrollController.dispose(); + tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: scaffoldKey, + backgroundColor: Colors.white, + appBar: AppBar( + titleSpacing: 1.0, + title: Text( + '商家订单', + style: TextStyle(fontSize: 18), + ), + bottom: PreferredSize( + preferredSize: Size.fromHeight(45.0), + child: Row(children: [ + Expanded( + child: TabBar( + controller: tabController, + tabs: tabList + .map((item) => Container( + alignment: Alignment.center, + height: 45.0, + child: Badge.count( + backgroundColor: Colors.red, + offset: Offset(14, -4), + count: item['badge'] ?? 0, + isLabelVisible: item['badge'] != null ? true : false, + child: Text( + item['name'], + style: TextStyle(fontSize: 16), + overflow: TextOverflow.ellipsis, + softWrap: false, + ), + ), + )) + .toList(), + isScrollable: false, + overlayColor: WidgetStateProperty.all(Colors.transparent), + unselectedLabelColor: Colors.black87, + labelColor: Color(0xFFFF5000), + indicator: UnderlineTabIndicator( + borderRadius: BorderRadius.circular(10.0), + borderSide: BorderSide(color: Color(0xFFFF5000), width: 2.0), + ), + indicatorSize: TabBarIndicatorSize.tab, + unselectedLabelStyle: TextStyle(fontSize: 16.0, fontFamily: 'Microsoft YaHei'), + labelStyle: TextStyle(fontSize: 18.0, fontFamily: 'Microsoft YaHei', fontWeight: FontWeight.w700), + dividerHeight: 0, + padding: EdgeInsets.symmetric(horizontal: 10.0), + labelPadding: EdgeInsets.symmetric(horizontal: 10.0), + indicatorPadding: EdgeInsets.symmetric(horizontal: 15.0, vertical: 5.0), + ), + ), + ]), + ), + ), + body: ScrollConfiguration( + behavior: CustomScrollBehavior().copyWith(scrollbars: false), + child: Container( + color: Colors.grey[50], + child: TabBarView( + controller: tabController, + children: [ + ListView( + controller: scrollController, + physics: BouncingScrollPhysics(), + padding: EdgeInsets.all(10.0), + children: [ + GestureDetector( + child: Container( + margin: EdgeInsets.only(bottom: 10.0), + padding: EdgeInsets.all(10.0), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(10), + offset: Offset(0.0, 1.0), + blurRadius: 1.0, + spreadRadius: 0.0, + ), + ]), + child: Column( + spacing: 10.0, + children: [ + Row( + children: [ + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 5.0, + children: [ + ClipOval( + child: Image.asset( + 'assets/images/avatar/img11.jpg', + width: 25.0, + ), + ), + Text('人人乐超市'), + Icon( + Icons.arrow_forward_ios_rounded, + color: Colors.grey, + size: 12.0, + ), + ], + ), + Spacer(), + Text( + '待付款', + style: TextStyle(color: Colors.red), + ) + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 10.0, + children: [ + Image.network( + 'https://img13.360buyimg.com/n1/jfs/t1/263909/5/4187/123220/676eb220F3e481086/0cee829b1894fc4c.jpg', + width: 80.0, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 5.0, + children: [ + Text( + '茅台(MOUTAI)飞天 53度 酱香型白酒 500ml*2 海外版送礼袋年货送礼', + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Row( + children: [ + Text( + '¥3800', + style: TextStyle(color: Colors.red), + ), + Spacer(), + Text( + 'x10', + style: TextStyle(color: Colors.grey), + ), + ], + ), + ], + ), + ) + ], + ), + // 提示信息 + Container( + padding: EdgeInsets.all(5.0), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(5.0), + ), + child: Row( + children: [ + Spacer(), + Text.rich( + TextSpan(children: [ + TextSpan(text: '实付款: '), + TextSpan( + text: '¥38000', + style: TextStyle(color: Colors.red), + ), + ]), + ), + ], + ), + ), + // 按钮组 + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('取消订单'), + ), + ElevatedButton( + onPressed: () {}, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(Color(0xff07c160)), foregroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('去支付'), + ), + ], + ), + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () {}, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(Color(0xFF10B9FC)), foregroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('评价'), + ), + ], + ), + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('申请退款'), + ), + ElevatedButton( + onPressed: () {}, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all(Color(0xFFFCBE13)), foregroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('联系客服'), + ), + ], + ), + + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('删除'), + ), + ], + ), + ], + ), + ), + onTap: () { + Get.toNamed('/order/detail'); + }, + ), + ], + ), + emptyTip(), + hexiao(), + finish(), + emptyTip(), + emptyTip(), + ], + ), + ), + ), + ); + } +} + +Widget finish() { + return ListView( + physics: BouncingScrollPhysics(), + padding: EdgeInsets.all(10.0), + children: [ + GestureDetector( + child: Container( + margin: EdgeInsets.only(bottom: 10.0), + padding: EdgeInsets.all(10.0), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(10), + offset: Offset(0.0, 1.0), + blurRadius: 1.0, + spreadRadius: 0.0, + ), + ]), + child: Column( + spacing: 10.0, + children: [ + Row( + children: [ + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 5.0, + children: [ + ClipOval( + child: Image.asset( + 'assets/images/avatar/img12.jpg', + width: 25.0, + ), + ), + Text('人人乐超市'), + Icon( + Icons.arrow_forward_ios_rounded, + color: Colors.grey, + size: 12.0, + ), + ], + ), + Spacer(), + Text( + '已完成', + style: TextStyle(color: Colors.red), + ) + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 10.0, + children: [ + Image.network( + 'https://img13.360buyimg.com/n1/jfs/t1/263909/5/4187/123220/676eb220F3e481086/0cee829b1894fc4c.jpg', + width: 80.0, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 5.0, + children: [ + Text( + '茅台(MOUTAI)飞天 53度 酱香型白酒 500ml 海外版送礼袋年货送礼', + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Row( + children: [ + Text( + '¥1900', + style: TextStyle(color: Colors.red), + ), + ], + ), + ], + ), + ) + ], + ), + // 提示信息 + Container( + padding: EdgeInsets.all(5.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5.0), + ), + child: Row( + children: [ + Spacer(), + Text.rich( + TextSpan(children: [ + TextSpan(text: '实付款: '), + TextSpan( + text: '¥1880', + style: TextStyle(color: Colors.red), + ), + ]), + ), + ], + ), + ), + ], + ), + ), + onTap: () { + Get.toNamed('/order/detail'); + }, + ), + ], + ); +} + +Widget hexiao() { + return ListView( + physics: BouncingScrollPhysics(), + padding: EdgeInsets.all(10.0), + children: [ + GestureDetector( + child: Container( + margin: EdgeInsets.only(bottom: 10.0), + padding: EdgeInsets.all(10.0), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(10), + offset: Offset(0.0, 1.0), + blurRadius: 1.0, + spreadRadius: 0.0, + ), + ]), + child: Column( + spacing: 10.0, + children: [ + Row( + children: [ + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 5.0, + children: [ + ClipOval( + child: Image.asset( + 'assets/images/avatar/img11.jpg', + width: 25.0, + ), + ), + Text('老白干自营旗舰店'), + Icon( + Icons.arrow_forward_ios_rounded, + color: Colors.grey, + size: 12.0, + ), + ], + ), + Spacer(), + Text( + '待付款', + style: TextStyle(color: Colors.red), + ) + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 10.0, + children: [ + Image.network( + 'https://img13.360buyimg.com/n1/jfs/t1/263909/5/4187/123220/676eb220F3e481086/0cee829b1894fc4c.jpg', + width: 80.0, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 5.0, + children: [ + Text( + '茅台(MOUTAI)飞天 53度 酱香型白酒 500ml*2 海外版送礼袋年货送礼', + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + Row( + children: [ + Text( + '¥3800', + style: TextStyle(color: Colors.red), + ), + Spacer(), + Text( + 'x10', + style: TextStyle(color: Colors.grey), + ), + ], + ), + ], + ), + ) + ], + ), + // 提示信息 + Container( + padding: EdgeInsets.all(5.0), + decoration: BoxDecoration( + color: Colors.grey[50], + borderRadius: BorderRadius.circular(5.0), + ), + child: Row( + children: [ + Spacer(), + Text.rich( + TextSpan(children: [ + TextSpan(text: '实付款: '), + TextSpan( + text: '¥38000', + style: TextStyle(color: Colors.red), + ), + ]), + ), + ], + ), + ), + // 按钮组 + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('取消订单'), + ), + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Color(0xff07c160)), foregroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('去支付'), + ), + ], + ), + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Color(0xFF10B9FC)), foregroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('评价'), + ), + ], + ), + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('申请退款'), + ), + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Color(0xFFFCBE13)), foregroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('联系客服'), + ), + ], + ), + + Row( + spacing: 10.0, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () {}, + style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.white)), + child: Text('删除'), + ), + ], + ), + ], + ), + ), + onTap: () { + Get.toNamed('/order/detail'); + }, + ), + ], + ); +} diff --git a/lib/pages/upload_video_page/upload_video_page.dart b/lib/pages/upload_video_page/upload_video_page.dart index 1edc5d3..37ade64 100644 --- a/lib/pages/upload_video_page/upload_video_page.dart +++ b/lib/pages/upload_video_page/upload_video_page.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:loopin/IM/controller/im_user_info_controller.dart'; import 'package:loopin/IM/im_service.dart'; import 'package:loopin/api/common_api.dart'; import 'package:loopin/api/video_api.dart'; @@ -13,7 +14,8 @@ import 'package:loopin/utils/snapshot.dart'; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; class UploadVideoPage extends StatefulWidget { - const UploadVideoPage({super.key}); + final bool visible; + const UploadVideoPage({super.key, required this.visible}); @override State createState() => _UploadVideoPageState(); @@ -36,13 +38,22 @@ class _UploadVideoPageState extends State { final videoPath = ''.obs; // 本地视频地址 final imgPath = ''.obs; //本地图片地址 final snapshot = ''.obs; + // 文件id + final fileId = ''.obs; - final FocusNode descFocusNode = FocusNode(); + late final FocusNode descFocusNode; - final TextEditingController descriptionController = TextEditingController(); + late TextEditingController descriptionController; + + @override + void initState() { + super.initState(); + descFocusNode = FocusNode(); + descriptionController = TextEditingController(); + } Future pickVideo() async { - FocusScope.of(context).unfocus(); + descFocusNode.unfocus(); final result = await PhotoManager.requestPermissionExtend(); @@ -82,7 +93,7 @@ class _UploadVideoPageState extends State { // 选择封面图 Future pickCoverImage() async { - FocusScope.of(context).unfocus(); + descFocusNode.unfocus(); final result = await PhotoManager.requestPermissionExtend(); @@ -169,6 +180,7 @@ class _UploadVideoPageState extends State { status.value = '上传中...'; videoPath.value = file.path; logger.w(videoPath.value); + // vwidth.value = file.width; snapshot.value = (await generateVideoThumbnail(file.path))!; logger.w(snapshot.value); @@ -180,19 +192,25 @@ class _UploadVideoPageState extends State { fileKey: 'file', onSendProgress: (sent, total) { if (total > 0) { - uploadProgress.value = sent / total; + final pro = sent / total; + uploadProgress.value = pro.clamp(0, 0.999); } }, ); + logger.w('上传结果$res'); uploadedVideoUrl.value = res['data']['url']; + fileId.value = res['data']['ossId']; status.value = '上传成功'; - } catch (e) { descFocusNode.unfocus(); + uploading.value = false; + } catch (e) { if (e is SocketException) { status.value = '网络错误,请检查连接'; } else { status.value = '上传失败: ${e.toString()}'; } + logger.e(e); + descFocusNode.unfocus(); } finally { descFocusNode.unfocus(); uploading.value = false; @@ -203,8 +221,8 @@ class _UploadVideoPageState extends State { Future submitForm() async { logger.w(descriptionController.text); - FocusScope.of(context).unfocus(); - if (uploadedVideoUrl.value.isEmpty) { + descFocusNode.unfocus(); + if (fileId.value.isEmpty) { Get.snackbar('请先上传视频', '未检测到上传的视频'); return; } @@ -217,6 +235,10 @@ class _UploadVideoPageState extends State { final data = { 'url': uploadedVideoUrl.value, 'title': descriptionController.text.trim(), + 'fileId': fileId.value, + 'vlogerId': Get.find().userID.value, + 'width': selectedVideo.value!.width, + 'height': selectedVideo.value!.height, }; if (uploadedImgUrl.value.isNotEmpty) { data['cover'] = uploadedImgUrl.value; @@ -242,11 +264,14 @@ class _UploadVideoPageState extends State { uploadProgress.value = 0.0; uploadProgress2.value = 0.0; videoPath.value = ''; + // 本地回显 + snapshot.value = ''; + imgPath.value = ''; } // 预览视频 void openVd() { - FocusScope.of(context).unfocus(); + descFocusNode.unfocus(); showGeneralDialog( context: context, // barrierDismissible: true, @@ -282,10 +307,11 @@ class _UploadVideoPageState extends State { @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - FocusScope.of(context).unfocus(); - }, + return FocusScope( + canRequestFocus: widget.visible, + // 只有 UploadVideoPage 可见时才允许 TextField 聚焦 + child: GestureDetector( + onTap: () => descFocusNode.unfocus(), child: Scaffold( appBar: AppBar(title: const Text('上传视频')), body: SingleChildScrollView( @@ -303,12 +329,11 @@ class _UploadVideoPageState extends State { maxLines: 2, ), const SizedBox(height: 20), - // 视频 + // 视频上传区 Obx(() { return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - // 左侧封面预览 snapshot.value.isNotEmpty ? GestureDetector( onTap: () => openVd(), @@ -329,10 +354,7 @@ class _UploadVideoPageState extends State { style: TextStyle(fontSize: 16), )), ), - const SizedBox(width: 24), - - // 右侧按钮/进度条 Expanded( child: uploading.value ? Column( @@ -358,12 +380,11 @@ class _UploadVideoPageState extends State { ); }), const SizedBox(height: 20), - // 图片上传 + // 图片上传区 Obx(() { return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - // 左侧图片预览 imgPath.value.isNotEmpty ? GestureDetector( onTap: () => openImg(), @@ -384,10 +405,7 @@ class _UploadVideoPageState extends State { style: TextStyle(fontSize: 16), )), ), - const SizedBox(width: 24), - - // 右侧按钮/进度条 Expanded( child: uploading2.value ? Column( @@ -425,6 +443,8 @@ class _UploadVideoPageState extends State { ], ), ), - )); + ), + ), + ); } } diff --git a/lib/router/index.dart b/lib/router/index.dart index f3546af..5d4a735 100644 --- a/lib/router/index.dart +++ b/lib/router/index.dart @@ -19,6 +19,7 @@ import 'package:loopin/pages/my/nick_name.dart'; import 'package:loopin/pages/my/setting.dart'; import 'package:loopin/pages/my/user_info.dart'; import 'package:loopin/pages/my/vloger.dart'; +import 'package:loopin/pages/order/seller.dart'; import 'package:loopin/pages/search/index.dart'; import 'package:loopin/pages/search/search-result.dart'; import 'package:loopin/pages/video/commonVideo.dart'; @@ -42,6 +43,7 @@ final Map routes = { // '/chatNoFriend': const ChatNoFriend(), // '/chatGroup': const ChatGroup(), '/order': const Order(), + '/sellerOrder': const Seller(), '/order/detail': const OrderDetail(), '/vloger': const Vloger(), '/report': const ReportPage(), diff --git a/lib/service/http_config.dart b/lib/service/http_config.dart index 6b5bee6..8babd8c 100644 --- a/lib/service/http_config.dart +++ b/lib/service/http_config.dart @@ -14,6 +14,9 @@ class HttpConfig { // connectTimeout: Duration(seconds: 30), // receiveTimeout: Duration(seconds: 30), + connectTimeout: const Duration(seconds: 30), // 建立连接超时 + receiveTimeout: const Duration(seconds: 300), // 接收响应超时(下载) + sendTimeout: const Duration(minutes: 10), // 发送请求超时(上传) )); static final box = GetStorage();