From e660f78cf4071d2cc7e06a925d50c98c397c3a5e Mon Sep 17 00:00:00 2001 From: ZHR007 Date: Thu, 15 Jan 2026 11:53:10 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=93=E9=AA=8C=E4=BC=98=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E7=9A=84svga=E6=96=87=E4=BB=B6=E6=92=AD?= =?UTF-8?q?=E6=94=BE=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/controller/message/chat_controller.dart | 70 +++- lib/extension/ex_fuction.dart | 65 ++++ lib/pages/discover/live_room_page.dart | 6 +- lib/pages/message/chat_page.dart | 338 ++++++++++---------- lib/widget/live/live_room_action_bar.dart | 31 +- lib/widget/message/gift_item.dart | 29 +- 6 files changed, 347 insertions(+), 192 deletions(-) create mode 100644 lib/extension/ex_fuction.dart diff --git a/lib/controller/message/chat_controller.dart b/lib/controller/message/chat_controller.dart index 2588f13..59472b1 100644 --- a/lib/controller/message/chat_controller.dart +++ b/lib/controller/message/chat_controller.dart @@ -1,3 +1,4 @@ +import 'package:dating_touchme_app/controller/discover/svga_player_manager.dart'; import 'package:get/get.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter/material.dart'; @@ -889,6 +890,18 @@ class ChatController extends GetxController { return _cursor != null; } + bool _isGiftMessage(EMMessage message) { + try { + if (message.body.type == MessageType.CUSTOM) { + final customBody = message.body as EMCustomMessageBody; + return customBody.event == 'gift'; + } + } catch (e) { + // 解析失败,不是礼物消息 + } + return false; + } + /// 添加接收到的消息 void addReceivedMessage(EMMessage message) { // 检查消息是否已存在(避免重复添加) @@ -898,13 +911,53 @@ class ChatController extends GetxController { update(); // 更新会话列表 _refreshConversationList(); - + if(_isGiftMessage(message) && Get.currentRoute == '/ChatPage'){ + Get.log('message915: $message'); + final giftInfo = _parseGiftInfo(message); + var svgaFile = '', giftProductId = ''; + if (giftInfo != null) { + svgaFile = giftInfo['svgaFile']?.toString() ?? ''; + giftProductId = giftInfo['giftProductId']?.toString() ?? ''; + } + if(svgaFile.isNotEmpty){ + final svgaManager = SvgaPlayerManager.instance; + svgaManager.addToQueue( + SvgaAnimationItem( + svgaFile: svgaFile, + giftProductId: giftProductId, + ), + ); + } + } if (Get.isLogEnable) { Get.log('收到新消息并添加到列表: ${message.msgId}'); } } } + Map? _parseGiftInfo(EMMessage message) { + try { + if (message.body.type == MessageType.CUSTOM) { + final customBody = message.body as EMCustomMessageBody; + if (customBody.event == 'gift' && customBody.params != null) { + // 将 Map 转换为 Map + final params = customBody.params!; + return { + 'giftProductId': params['giftProductId'] ?? '', + 'giftProductTitle': params['giftProductTitle'] ?? '', + 'giftMainPic': params['giftMainPic'] ?? '', + 'svgaFile': params['svgaFile'] ?? '', + 'giftPrice': params['giftPrice'] ?? '0', + 'quantity': int.tryParse(params['quantity'] ?? '1') ?? 1, + }; + } + } + } catch (e) { + print('解析礼物信息失败: $e'); + } + return null; + } + /// 撤回消息 Future recallMessage(EMMessage message) async { try { @@ -1778,12 +1831,16 @@ class ChatController extends GetxController { if (code == 'E0002') { // 玫瑰不足,显示 toast 并弹出充值弹框 SmartDialog.showToast('玫瑰不足请充值'); + setDialogDismiss(true); SmartDialog.show( alignment: Alignment.bottomCenter, maskColor: Get.context != null ? TDTheme.of(Get.context!).fontGyColor2 : Colors.black.withOpacity(0.5), builder: (_) => const LiveRechargePopup(), + onDismiss: () { + setDialogDismiss(false); + } ); } else { SmartDialog.showToast('发送礼物失败'); @@ -1798,6 +1855,7 @@ class ChatController extends GetxController { 'giftMainPic': gift.mainPic, 'giftPrice': gift.unitSellingPrice.toString(), 'quantity': quantity.toString(), + 'svgaFile': gift.svgaFile, }; // 先创建自定义消息对象(即使发送失败也要显示在列表中) @@ -1826,6 +1884,16 @@ class ChatController extends GetxController { // 刷新玫瑰余额 _refreshRoseBalance(); + // 消费成功后再添加到本地播放队列 + final svgaManager = SvgaPlayerManager.instance; + svgaManager.addToQueue( + SvgaAnimationItem( + svgaFile: gift.svgaFile, + giftProductId: gift.productId, + ), + ); + print('✅ 礼物已添加到播放队列: ${gift.productTitle}'); + if (Get.isLogEnable) { Get.log('✅ 礼物消息发送成功: ${gift.productTitle}'); } diff --git a/lib/extension/ex_fuction.dart b/lib/extension/ex_fuction.dart new file mode 100644 index 0000000..9ce988e --- /dev/null +++ b/lib/extension/ex_fuction.dart @@ -0,0 +1,65 @@ +import 'dart:async'; +import 'dart:ui'; + +extension FunctionExt on Function{ + VoidCallback throttle(){ + return FunctionProxy(this).throttle; + } + + VoidCallback throttleWithTimeout({int? timeout}){ + return FunctionProxy(this, timeout: timeout).throttleWithTimeout; + } + + VoidCallback debounce({int? timeout}){ + return FunctionProxy(this, timeout: timeout).debounce; + } +} + +class FunctionProxy { + static final Map _funcThrottle = {}; + static final Map _funcDebounce = {}; + final Function? target; + + final int timeout; + + FunctionProxy(this.target, {int? timeout}) : timeout = timeout ?? 500; + + void throttle() async { + String key = hashCode.toString(); + bool enable = _funcThrottle[key] ?? true; + if (enable) { + _funcThrottle[key] = false; + try { + await target?.call(); + } catch (e) { + rethrow; + } finally { + _funcThrottle.remove(key); + } + } + } + + void throttleWithTimeout() { + String key = hashCode.toString(); + bool enable = _funcThrottle[key] ?? true; + if (enable) { + _funcThrottle[key] = false; + Timer(Duration(milliseconds: timeout), () { + _funcThrottle.remove(key); + }); + target?.call(); + } + } + + void debounce() { + String key = hashCode.toString(); + Timer? timer = _funcDebounce[key]; + timer?.cancel(); + timer = Timer(Duration(milliseconds: timeout), () { + Timer? t = _funcDebounce.remove(key); + t?.cancel(); + target?.call(); + }); + _funcDebounce[key] = timer; + } +} \ No newline at end of file diff --git a/lib/pages/discover/live_room_page.dart b/lib/pages/discover/live_room_page.dart index 67e522d..7b55e11 100644 --- a/lib/pages/discover/live_room_page.dart +++ b/lib/pages/discover/live_room_page.dart @@ -1,6 +1,7 @@ import 'package:dating_touchme_app/controller/discover/room_controller.dart'; import 'package:dating_touchme_app/controller/global.dart'; import 'package:dating_touchme_app/controller/overlay_controller.dart'; +import 'package:dating_touchme_app/extension/ex_fuction.dart'; import 'package:dating_touchme_app/generated/assets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -180,6 +181,9 @@ class _LiveRoomPageState extends State { } void _showGiftPopup() async { + if(_roomController.isDialogShowing.value){ + return; + } // 隐藏键盘 FocusScope.of(context).unfocus(); // 刷新玫瑰数量 @@ -355,7 +359,7 @@ class _LiveRoomPageState extends State { message = value; }, onSendTap: _sendMessage, - onGiftTap: _showGiftPopup, + onGiftTap: _showGiftPopup.throttle(), onChargeTap: _showRechargePopup, onInputTap: _openInputDialog, ), diff --git a/lib/pages/message/chat_page.dart b/lib/pages/message/chat_page.dart index 5d138e4..d562bef 100644 --- a/lib/pages/message/chat_page.dart +++ b/lib/pages/message/chat_page.dart @@ -1,4 +1,5 @@ import 'package:dating_touchme_app/extension/ex_widget.dart'; +import 'package:dating_touchme_app/widget/live/svga_player_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; @@ -76,13 +77,7 @@ class _ChatPageState extends State { if (Get.isRegistered(tag: tag)) { Get.delete(tag: tag); } - _controller = Get.put( - ChatController( - userId: widget.userId, - userData: widget.userData, - ), - tag: tag, - ); + _controller = Get.put(ChatController(userId: widget.userId, userData: widget.userData), tag: tag); // 监听滚动,当滚动到顶部时加载更多消息 _scrollController.addListener(() { @@ -158,11 +153,7 @@ class _ChatPageState extends State { maskColor: TDTheme.of(context).fontGyColor2, onDismiss: () { _controller.setDialogDismiss(false); - }, - maskWidget: GestureDetector( - onTap: () => SmartDialog.dismiss(), - child: Container(color: Colors.transparent), - ), + } ); } @@ -185,6 +176,9 @@ class _ChatPageState extends State { // 显示通话类型选择弹框 void _showCallTypeSelectionDialog(ChatController controller, {List? products}) { // 隐藏键盘 + if(_controller.isDialogShowing.value){ + return; + } FocusScope.of(context).unfocus(); _controller.setDialogDismiss(true); SmartDialog.show( @@ -227,14 +221,10 @@ class _ChatPageState extends State { }, alignment: Alignment.bottomCenter, animationType: SmartAnimationType.centerFade_otherSlide, - maskColor: TDTheme.of(context).fontGyColor2, + maskColor: TDTheme.of(context).fontGyColor3, onDismiss: () { _controller.setDialogDismiss(false); - }, - maskWidget: GestureDetector( - onTap: () => SmartDialog.dismiss(), - child: Container(color: Colors.transparent), - ), + } ); } @@ -256,172 +246,178 @@ class _ChatPageState extends State { Get.back(); } }, - child: Scaffold( - backgroundColor: Color(0xffF5F5F5), - appBar: AppBar( - title: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - controller.userNickName ?? widget.userData?.nickName ?? '', - style: TextStyle(fontSize: 18.sp), - ), - // 使用 Obx 来响应在线状态的变化 - Obx(() { - final isOnline = controller.isUserOnline.value; - if (isOnline) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(width: 8.w), - Container( - width: 46.w, - height: 26.h, - decoration: BoxDecoration( - color: Color.fromRGBO(234, 255, 219, 0.5), - borderRadius: BorderRadius.circular(178), - ), - child: Center( - child: Text( - '在线', - style: TextStyle( - fontSize: 12.sp, - color: Color.fromRGBO(38, 199, 124, 1), + child: Stack( + children: [ + Scaffold( + backgroundColor: Color(0xffF5F5F5), + appBar: AppBar( + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + controller.userNickName ?? widget.userData?.nickName ?? '', + style: TextStyle(fontSize: 18.sp), + ), + // 使用 Obx 来响应在线状态的变化 + Obx(() { + final isOnline = controller.isUserOnline.value; + if (isOnline) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(width: 8.w), + Container( + width: 46.w, + height: 26.h, + decoration: BoxDecoration( + color: Color.fromRGBO(234, 255, 219, 0.5), + borderRadius: BorderRadius.circular(178), + ), + child: Center( + child: Text( + '在线', + style: TextStyle( + fontSize: 12.sp, + color: Color.fromRGBO(38, 199, 124, 1), + ), + ), ), ), + ], + ); + } + return SizedBox.shrink(); + }), + ], + ), + centerTitle: false, + actions: [ + Container( + padding: EdgeInsets.only(right: 16.w), + child: Image.asset(Assets.imagesMore, width: 16.w), + ).onTap(() { + // 跳转到聊天设置页面,传递用户信息 + Get.to(() => ChatSettingsPage( + userId: widget.userId, + userData: widget.userData ?? _controller.userData, + )); + }), + ], + leading: IconButton( + icon: Icon(Icons.arrow_back_ios), + onPressed: () { + Get.back(); + }, + ), + ), + body: GestureDetector( + onTap: () { + // 点击空白处收起键盘 + FocusManager.instance.primaryFocus?.unfocus(); + // 关闭底部面板(表情、更多选项、语音输入) + ChatInputBar.closePanels(_chatInputBarKey); + }, + behavior: HitTestBehavior.opaque, + child: Column( + children: [ + // 消息列表区域 + Expanded( + child: Container( + color: Color(0xffF5F5F5), + child: ListView.builder( + controller: _scrollController, + reverse: false, + padding: EdgeInsets.only( + top: 16.w, + left: 16.w, + right: 16.w, + bottom: 16.w, ), - ), - ], - ); - } - return SizedBox.shrink(); - }), - ], - ), - centerTitle: false, - actions: [ - Container( - padding: EdgeInsets.only(right: 16.w), - child: Image.asset(Assets.imagesMore, width: 16.w), - ).onTap(() { - // 跳转到聊天设置页面,传递用户信息 - Get.to(() => ChatSettingsPage( - userId: widget.userId, - userData: widget.userData ?? _controller.userData, - )); - }), - ], - leading: IconButton( - icon: Icon(Icons.arrow_back_ios), - onPressed: () { - Get.back(); - }, - ), - ), - body: GestureDetector( - onTap: () { - // 点击空白处收起键盘 - FocusManager.instance.primaryFocus?.unfocus(); - // 关闭底部面板(表情、更多选项、语音输入) - ChatInputBar.closePanels(_chatInputBarKey); - }, - behavior: HitTestBehavior.opaque, - child: Column( - children: [ - // 消息列表区域 - Expanded( - child: Container( - color: Color(0xffF5F5F5), - child: ListView.builder( - controller: _scrollController, - reverse: false, - padding: EdgeInsets.only( - top: 16.w, - left: 16.w, - right: 16.w, - bottom: 16.w, - ), - itemCount: controller.messages.length + 1, // 添加用户信息卡片 - // 🚀 性能优化:添加缓存范围,减少重建 - cacheExtent: 500, // 缓存屏幕外500像素的内容 - itemBuilder: (context, index) { - // 第一个是用户信息卡片 - if (index == 0) { - return _buildUserInfoCard(controller, widget.userData); - } - - final message = controller.messages[index - 1]; - Get.log('message: $message'); - final isSentByMe = - message.direction == MessageDirection.SEND; + itemCount: controller.messages.length + 1, // 添加用户信息卡片 + // 🚀 性能优化:添加缓存范围,减少重建 + cacheExtent: 500, // 缓存屏幕外500像素的内容 + itemBuilder: (context, index) { + // 第一个是用户信息卡片 + if (index == 0) { + return _buildUserInfoCard(controller, widget.userData); + } - final previousMessage = index > 1 - ? controller.messages[index - 2] - : null; + final message = controller.messages[index - 1]; + Get.log('message347: $message'); + final isSentByMe = + message.direction == MessageDirection.SEND; - // 检查是否需要显示508错误提示(使用临时状态,不持久化) - final showRoseError = isSentByMe && - message.status == MessageStatus.FAIL && - controller.shouldShowRoseError(message.msgId); + final previousMessage = index > 1 + ? controller.messages[index - 2] + : null; - // 检查是否需要显示敏感词错误提示(使用临时状态,不持久化) - final showSensitiveWordError = isSentByMe && - message.status == MessageStatus.FAIL && - controller.shouldShowSensitiveWordError(message.msgId); + // 检查是否需要显示508错误提示(使用临时状态,不持久化) + final showRoseError = isSentByMe && + message.status == MessageStatus.FAIL && + controller.shouldShowRoseError(message.msgId); - // 🚀 性能优化:为每个消息项设置唯一的 key - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - MessageItem( - key: ValueKey(message.msgId), - message: message, - isSentByMe: isSentByMe, - previousMessage: previousMessage, - chatController: controller, // 传递 controller 避免使用 Get.find - ), - // 显示错误提示(错误码508) - if (showRoseError) _buildRoseErrorHint(context), - // 显示敏感词错误提示(错误码E0001) - if (showSensitiveWordError) _buildSensitiveWordErrorHint(context), - ], - ); + // 检查是否需要显示敏感词错误提示(使用临时状态,不持久化) + final showSensitiveWordError = isSentByMe && + message.status == MessageStatus.FAIL && + controller.shouldShowSensitiveWordError(message.msgId); + + // 🚀 性能优化:为每个消息项设置唯一的 key + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + MessageItem( + key: ValueKey(message.msgId), + message: message, + isSentByMe: isSentByMe, + previousMessage: previousMessage, + chatController: controller, // 传递 controller 避免使用 Get.find + ), + // 显示错误提示(错误码508) + if (showRoseError) _buildRoseErrorHint(context), + // 显示敏感词错误提示(错误码E0001) + if (showSensitiveWordError) _buildSensitiveWordErrorHint(context), + ], + ); + }, + ), + ), + ), + // 使用抽离的聊天输入栏组件 + ChatInputBar( + key: _chatInputBarKey, + onSendMessage: (message) async { + await controller.sendMessage(message); + }, + onImageSelected: (imagePaths) async { + // 为每个图片路径调用控制器的方法发送图片消息 + for (var imagePath in imagePaths) { + await controller.sendImageMessage(imagePath); + } + }, + onVoiceRecorded: (filePath, seconds) async { + // 处理语音录音完成,回传文件路径和秒数 + await controller.sendVoiceMessage(filePath, seconds); + }, + onGiftTap: () { + // 显示礼物弹窗 + _showGiftPopup(); + }, + // 视频通话回调 + onVideoCall: () async { + // 先调用获取聊天音频产品列表接口 + final products = await CallController.instance.listChatAudioProduct(widget.userId); + // 显示通话类型选择弹框,传入产品数据 + _showCallTypeSelectionDialog(controller, products: products); }, ), - ), + ], ), - // 使用抽离的聊天输入栏组件 - ChatInputBar( - key: _chatInputBarKey, - onSendMessage: (message) async { - await controller.sendMessage(message); - }, - onImageSelected: (imagePaths) async { - // 为每个图片路径调用控制器的方法发送图片消息 - for (var imagePath in imagePaths) { - await controller.sendImageMessage(imagePath); - } - }, - onVoiceRecorded: (filePath, seconds) async { - // 处理语音录音完成,回传文件路径和秒数 - await controller.sendVoiceMessage(filePath, seconds); - }, - onGiftTap: () { - // 显示礼物弹窗 - _showGiftPopup(); - }, - // 视频通话回调 - onVideoCall: () async { - // 先调用获取聊天音频产品列表接口 - final products = await CallController.instance.listChatAudioProduct(widget.userId); - // 显示通话类型选择弹框,传入产品数据 - _showCallTypeSelectionDialog(controller, products: products); - }, ), - ], ), - ), + const SvgaPlayerWidget(), + ], ), + ); }, ); diff --git a/lib/widget/live/live_room_action_bar.dart b/lib/widget/live/live_room_action_bar.dart index 2f84737..a4cc557 100644 --- a/lib/widget/live/live_room_action_bar.dart +++ b/lib/widget/live/live_room_action_bar.dart @@ -1,6 +1,7 @@ import 'package:dating_touchme_app/generated/assets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; class LiveRoomActionBar extends StatelessWidget { const LiveRoomActionBar({ @@ -25,23 +26,21 @@ class LiveRoomActionBar extends StatelessWidget { return Row( children: [ SizedBox(width: 16.w), - InkWell( - onTap: onGiftTap, - child: Container( - width: 38.w, - height: 38.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(38.w)), - color: const Color.fromRGBO(0, 0, 0, .3), - ), - child: Center( - child: Image.asset( - Assets.imagesGiftIcon, - width: 28.w, - height: 28.w, - ), - ), + TDButton( + width: 38.w, + height: 38.w, + iconWidget: Image.asset(Assets.imagesGiftIcon, width: 28.w, height: 28.w), + padding: EdgeInsets.all(5.w), + type: TDButtonType.fill, + shape: TDButtonShape.circle, + theme: TDButtonTheme.primary, + style: TDButtonStyle( + backgroundColor: const Color.fromRGBO(0, 0, 0, .3), + ), + activeStyle: TDButtonStyle( + backgroundColor: const Color.fromRGBO(0, 0, 0, .2), ), + onTap: onGiftTap, ), SizedBox(width: 9.w), Expanded( diff --git a/lib/widget/message/gift_item.dart b/lib/widget/message/gift_item.dart index 663cdc2..bc5289d 100644 --- a/lib/widget/message/gift_item.dart +++ b/lib/widget/message/gift_item.dart @@ -1,4 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; +import 'package:dating_touchme_app/controller/discover/svga_player_manager.dart'; +import 'package:dating_touchme_app/extension/ex_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; @@ -36,6 +38,7 @@ class GiftItem extends StatelessWidget { 'giftProductId': params['giftProductId'] ?? '', 'giftProductTitle': params['giftProductTitle'] ?? '', 'giftMainPic': params['giftMainPic'] ?? '', + 'svgaFile': params['svgaFile'] ?? '', 'giftPrice': params['giftPrice'] ?? '0', 'quantity': int.tryParse(params['quantity'] ?? '1') ?? 1, }; @@ -56,6 +59,14 @@ class GiftItem extends StatelessWidget { return '礼物'; } + String _getGiftSvga() { + final giftInfo = _parseGiftInfo(); + if (giftInfo != null) { + return giftInfo['svgaFile']?.toString() ?? ''; + } + return ''; + } + /// 获取礼物图片 String _getGiftImage() { final giftInfo = _parseGiftInfo(); @@ -98,8 +109,7 @@ class GiftItem extends StatelessWidget { Container( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), child: Row( - mainAxisAlignment: - isSentByMe ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisAlignment: isSentByMe ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ if (!isSentByMe) _buildAvatar(), @@ -219,7 +229,20 @@ class GiftItem extends StatelessWidget { ), ], ), - ), + ).onTap((){ + final svgaFile = _getGiftSvga(); + Get.log(giftInfo.toString()); + Get.log('${message.status}'); + if ((message.status == MessageStatus.SUCCESS || message.status == MessageStatus.PROGRESS) && svgaFile.isNotEmpty) { + final svgaManager = SvgaPlayerManager.instance; + svgaManager.addToQueue( + SvgaAnimationItem( + svgaFile: svgaFile, + giftProductId: giftInfo['giftProductId']?.toString() ?? '', + ), + ); + } + }), if (isSentByMe) SizedBox(width: 8.w), if (isSentByMe) _buildAvatar(), ],