/// 聊天模板 library; import 'dart:convert'; import 'package:flutter/material.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/image_viewer.dart'; import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/components/preview_video.dart'; import 'package:loopin/models/conversation_type.dart'; import 'package:loopin/models/summary_type.dart'; import 'package:loopin/pages/chat/notify_controller/notify_no_friend_controller.dart'; import 'package:loopin/utils/audio_player_service.dart'; import 'package:loopin/utils/snapshot.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 '../../styles/index.dart'; import '../../utils/index.dart'; import './components/redpacket.dart'; import './components/richtext.dart'; // import 'mock/chat_json.dart'; import 'mock/emoj_json.dart'; class ChatNoFriend extends StatefulWidget { const ChatNoFriend({super.key}); @override State createState() => _ChatNoFriendState(); } class _ChatNoFriendState extends State with SingleTickerProviderStateMixin { late final ChatDetailController controller; // 接收参数 late final Rx arguments; late String selfUserId; // 聊天消息模块 final bool isNeedScrollBottom = true; final String _tips = '回复或关注对方之前,对方只能发送一条消息'; bool isLoading = false; // 是否在加载中 bool hasMore = true; // 是否还有更多数据 final RxBool _throttleFlag = false.obs; // 滚动节流锁 // 表情json List emoJson = emotionData; // 底部操作栏模块 TextEditingController editorController = TextEditingController(); FocusNode editorFocusNode = FocusNode(); bool voiceBtnEnable = false; // 语音按钮 bool voicePanelEnable = false; // 语音操作面板 bool voiceToTransfer = false; // 语音转文字中 int voiceType = 0; // 语音操作类型 Map voiceTypeMap = { 0: '按住 说话', // 按住说话 1: '松开 发送', // 松开发送 2: '松开 取消', // 松开取消(左滑) 3: '语音转文字', // 语音转文字(右滑) }; bool toolbarEnable = false; // 显示表情/选择区域 int toolbarIndex = 0; // 0 表情 1 选择 double keyboardHeight = 157.6; // 键盘高度 List chooseOptions = [ {'key': 'photo', 'name': '相册', 'icon': 'assets/images/icon_photo.webp'}, {'key': 'camera', 'name': '拍摄', 'icon': 'assets/images/icon_camera.webp'}, {'key': 'location', 'name': '位置', 'icon': 'assets/images/icon_location.webp'}, {'key': 'redpacket', 'name': '红包', 'icon': 'assets/images/icon_hb.webp'}, ]; // controller监听 // ScrollController chatController = ScrollController(); late ScrollController chatController; ScrollController emojController = ScrollController(); // 模拟开红包按钮动画 late AnimationController animController; // 创建一个从 0 到 π 的旋转动画 late Animation animTurns; // 初始化状态 @override void initState() { super.initState(); final arg = Get.arguments as V2TimConversation; arguments = arg.obs; controller = Get.find(); chatController = controller.chatController; animController = AnimationController( vsync: this, duration: Duration(milliseconds: 500), ); animTurns = Tween(begin: 0, end: 3.1415926).animate(animController); cleanUnRead(); getUserId(); isMyFriend(); getMsgData(initFlag: true); // 编辑框获取焦点 editorFocusNode.addListener(() { if (editorFocusNode.hasFocus) { setState(() { toolbarEnable = false; }); controller.scrollToBottom(); } }); // 滚动监听 // Future.delayed(Duration(milliseconds: 1000), () { // }); chatController.addListener(() { if (_throttleFlag.value) return; if (chatController.position.pixels >= chatController.position.maxScrollExtent - 50) { // if (chatController.position.pixels <= 50) { _throttleFlag.value = true; getMsgData().then((_) { // 解锁 Future.delayed(Duration(milliseconds: 1000), () { _throttleFlag.value = false; }); }); } }); } @override void dispose() { if (Get.isRegistered()) { Get.delete(); } emojController.dispose(); editorFocusNode.dispose(); animController.dispose(); super.dispose(); } // 设置好友备注 void setRemark() async { print('不支持'); } void isMyFriend() async { final isFriendId = arguments.value.userID; // final isfd = await ImService.instance.isMyFriend(isFriendId!, FriendTypeEnum.V2TIM_FRIEND_TYPE_BOTH); // if (isfd.success) { // controller.isFriend.value = isfd.data; // print(isfd.data); // } else { // controller.isFriend.value = false; // print(isfd.desc); // } /// 0:不是好友也没有关注 /// 1:你关注了对方(单向) /// 2:对方关注了你(单向) /// 3:互相关注(双向好友) final isfd = await ImService.instance.checkFollowType(userIDList: [isFriendId!]); if (isfd.success) { controller.followType.value = isfd.data?.first.followType ?? 0; if ([3].contains(controller.followType.value)) { controller.isFriend.value = true; } else { controller.isFriend.value = false; } logger.i('当前聊天的关系为:${controller.followType.value}'); } } bool checkSend() { if (controller.isFriend.value) { // 是好友直接发 return true; } else { // 不是好友 if (arguments.value.lastMessage != null) { // 最后一条消息是否我发的 // final isSelf = arguments.value.lastMessage!.isSelf; final isSelf = controller.chatList.first.isSelf; return isSelf == true ? false : true; } else { return true; } } } void cleanUnRead() async { if ((arguments.value.unreadCount ?? 0) > 0) { final res = await ImService.instance.clearConversationUnreadCount(conversationID: arguments.value.conversationID); if (!res.success) { MyDialog.toast('清理未读异常:${res.desc}', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); } logger.w('清理陌生人会话未读消息,准备刷新消息页的UI,陌生人会话列表的UI'); // 是否从未读会话列表而来 if (Get.isRegistered()) { final ctl = Get.find(); ctl.updateUnread(conversationID: arguments.value.conversationID); } } } void getUserId() async { final idRes = await ImService.instance.selfUserId(); if (idRes.success) { selfUserId = idRes.data; } } Future getMsgData({bool initFlag = false}) async { if (isLoading || !hasMore) return; // 正在加载 or 没有更多了 isLoading = true; // 获取最旧一条消息作为游标 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; // 如果找不到,就用传入的参数 // print(lastMsg?.toJson()); // final lastMsg = controller.chatList.isNotEmpty ? controller.chatList.last : arguments.lastMessage; final res = await ImService.instance.getHistoryMessageList( userID: arguments.value.userID, lastMsg: lastMsg, ); if (res.success) { final newMessages = res.data ?? []; if (newMessages.isEmpty) { hasMore = false; // 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))); } isLoading = false; } // 渲染聊天消息 List renderChatList() { List msgtpl = []; for (var item in controller.chatList) { // 时间提示,公告提示 if (item.localCustomData == 'time_label') { msgtpl.add(Container( margin: const EdgeInsets.only(bottom: 15.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( item.customElem!.data ?? '', style: TextStyle(color: Colors.grey[600], fontSize: 12.0), ), ], ), )); } // 写入记录的tips else if (item.cloudCustomData == 'tips') { msgtpl.add( Container( margin: const EdgeInsets.only(bottom: 15.0), width: double.infinity, child: Text( item.textElem!.text ?? '', style: TextStyle(color: Colors.grey[600], fontSize: 12.0), softWrap: true, maxLines: 5, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, ), ), ); } // 文本消息模板=1 else if (item.elemType == 1 && item.cloudCustomData != 'tips') { 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(); }, ), ), ), ); } // gif表情模板=8 else if (item.elemType == 8) { msgtpl.add(RenderChatItem( data: item, child: Ink( child: InkWell( overlayColor: WidgetStateProperty.all(Colors.transparent), child: Container( constraints: const BoxConstraints( maxHeight: 100.0, maxWidth: 100.0, ), // child: Image.asset('assets/images/emotion/${item.faceElem?.data}'), child: Image.asset('${item.faceElem?.data}'), ), onLongPress: () { contextMenuDialog(); }, ), ), )); } // 图片模板=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( child: InkWell( onTap: () { // 预览图片 Get.to(() => ImageViewer( images: [imagePaths.first], index: 0, )); }, overlayColor: WidgetStateProperty.all(Colors.transparent), child: ClipRRect( borderRadius: BorderRadius.circular(10.0), // 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: () { contextMenuDialog(); }, ), ), )); } // 视频模板=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: 120.0, child: Stack( alignment: Alignment.center, children: [ ClipRRect( borderRadius: BorderRadius.circular(10.0), child: NetworkOrAssetImage( imageUrl: item.videoElem?.snapshotUrl ?? '', width: 120, ), ), const Align( alignment: Alignment.center, child: Icon( Icons.play_circle, color: Colors.white, size: 30.0, ), ), ], ), ), onTap: () { 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(); }, ), ), )); } // 语音模板=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( color: !(item.isSelf ?? false) ? const Color(0xFFFFFFFF) : const 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), constraints: BoxConstraints( maxWidth: maxWidth, // maxWidth: (item.soundElem!.duration! / 1000) / 60 * 230, ), child: Row( mainAxisAlignment: !(item.isSelf ?? false) ? MainAxisAlignment.start : MainAxisAlignment.end, children: !(item.isSelf ?? false) ? [ const Icon(Icons.multitrack_audio), const SizedBox( width: 5.0, ), Text('$durationSeconds"'), ] : [ Text('$durationSeconds"'), const SizedBox( width: 5.0, ), const Icon(Icons.multitrack_audio), ], ), ), onTap: () { 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(); }, ), ), const SizedBox( width: 5.0, ), // FStyle.badge(0, isdot: true), ]; if (item.isSelf ?? false) { // 内容反转 audiobody = audiobody.reversed.toList(); } else { audiobody = audiobody; } msgtpl.add(RenderChatItem( data: item, child: Row( mainAxisAlignment: !(item.isSelf ?? false) ? MainAxisAlignment.start : MainAxisAlignment.end, children: audiobody, ))); } // 分享团购商品 else if (item.elemType == 2 && item.cloudCustomData == SummaryType.shareTuangou) { // final makeJson = jsonEncode({ // "price": shopObj['price'], // "title": shopObj['name'], // "url": shopObj['pic'], // "sell": Utils.graceNumber(int.parse(shopObj['sales'] ?? '0')), // "goodsId": shopObj['id'], // "userID": Get.find().userID.value, // }); final obj = jsonDecode(item.customElem!.data!); logger.e(obj); final goodsId = obj['goodsId']; final userID = obj['userID']; 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( onTap: () { // 这里带上分享人的ID Get.toNamed('/goods', arguments: {'goodsId': goodsId, 'userID': userID}); }, 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), ), ], ), ], ), ) ], ), ), ), )); } // 分享短视频 else if (item.elemType == 2 && item.cloudCustomData == SummaryType.shareVideo) { /// {imgUrl,videoUrl,width,height} final obj = jsonDecode(item.customElem!.data!); logger.e(obj); final videoId = obj['videoId']; final videoUrl = obj['videoUrl']; final imgUrl = obj['imgUrl']; final width = obj['width'] as num; final height = obj['height'] as num; final isHorizontal = width > height; 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: Container( width: 120, height: 240, color: Colors.black, child: NetworkOrAssetImage( imageUrl: imgUrl, fit: isHorizontal ? BoxFit.contain : BoxFit.cover, placeholderAsset: 'assets/images/bk.jpg', ), ), ), const Align( alignment: Alignment.center, child: Icon( Icons.play_circle, color: Colors.white, size: 30.0, ), ), ], ), ), onTap: () { Get.toNamed('/videoDetail', arguments: {'videoId': videoId}); // 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( decoration: BoxDecoration( color: const Color(0xFFFF7F43), borderRadius: BorderRadius.circular(10.0), ), child: InkWell( overlayColor: WidgetStateProperty.all(Colors.transparent), borderRadius: BorderRadius.circular(10.0), child: Container( constraints: const BoxConstraints( maxWidth: 210.0, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 210.0, padding: const EdgeInsets.all(10.0), child: Row( children: [ 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, ), ), ], ), ), Container( margin: const EdgeInsets.symmetric(horizontal: 10.0), padding: const EdgeInsets.symmetric(vertical: 5.0), 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), ), ), ], ), ), onTap: () { receiveRedPacketDialog(item); }, onLongPress: () { contextMenuDialog(); }, ), ), )); } // 位置模板=7 else if (item.elemType == 7) { msgtpl.add(RenderChatItem( data: item, child: Ink( decoration: BoxDecoration( color: const Color(0xFFFFFFFF), borderRadius: BorderRadius.circular(10.0), ), child: InkWell( // splashColor: Colors.transparent, overlayColor: WidgetStateProperty.all(Colors.transparent), borderRadius: BorderRadius.circular(10.0), child: Container( constraints: const BoxConstraints( maxWidth: 210.0, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 10.0, vertical: 5.0, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.locationElem?.desc ?? '位置信息异常', overflow: TextOverflow.ellipsis, ), Text( "${item.locationElem?.latitude},${item.locationElem?.longitude}", style: const TextStyle(color: Colors.grey, fontSize: 12.0), overflow: TextOverflow.ellipsis, ), ], ), ), ClipRRect( borderRadius: const BorderRadius.vertical(bottom: Radius.circular(10.0)), child: Image.asset('assets/images/map.jpg', width: 210.0, height: 70.0, fit: BoxFit.cover), ) ], ), ), onTap: () { MyDialog.toast('该功能暂未支持~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); }, onLongPress: () { contextMenuDialog(); }, ), ), )); } } return msgtpl; } // 表情列表集合 List renderEmojWidget() { return [ // Tab切换 Container( padding: const EdgeInsets.symmetric(horizontal: 5.0), child: Row( children: emoJson.map((item) { return InkWell( child: Container( margin: const EdgeInsets.all(5.0), alignment: Alignment.center, height: 40.0, width: 40.0, decoration: BoxDecoration(color: item['selected'] ? Colors.white : Colors.transparent, borderRadius: BorderRadius.circular(5.0)), child: item['index'] == 0 ? Text( item['pathLabel'], style: const TextStyle(fontSize: 22.0), ) : Image.asset(item['pathLabel'], height: 24.0, width: 24.0, fit: BoxFit.cover), ), onTap: () { handleEmojTab(item['index']); }, ); }).toList(), ), ), Expanded( child: Container( decoration: BoxDecoration( color: Colors.grey[200], border: const Border(top: BorderSide(color: Colors.black54, width: .1)), ), child: ListView( controller: emojController, padding: const EdgeInsets.all(10.0), children: emoJson.map((item) { return Visibility( visible: item['selected'], child: GridView( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( // 横轴元素个数 crossAxisCount: item['type'] == 'emoj' ? 8 : 5, // 纵轴间距 mainAxisSpacing: 5.0, // 横轴间距 crossAxisSpacing: 5.0, // 子组件宽高比例 childAspectRatio: 1, ), children: item['nodes'].map((emoj) { if (item['type'] == 'emoj') { return Material( type: MaterialType.transparency, child: InkWell( borderRadius: BorderRadius.circular(5.0), child: Container( alignment: Alignment.center, height: 40.0, width: 40.0, child: Text( emoj, style: const TextStyle(fontSize: 24.0), ), ), onTap: () { handleEmojClick(emoj); }, ), ); } else { return Material( type: MaterialType.transparency, child: InkWell( borderRadius: BorderRadius.circular(5.0), child: Container( alignment: Alignment.center, padding: const EdgeInsets.all(5.0), height: 68.0, width: 68.0, child: Image.asset(emoj), ), onTap: () { handleGIFClick(emoj, item['index']); }, ), ); } }).toList(), ), ); }).toList(), ), ), ), ]; } // 选择功能列表 List renderChooseWidget() { return [ Expanded( child: Container( padding: const EdgeInsets.fromLTRB(30.0, 35.0, 30.0, 15.0), decoration: BoxDecoration( color: Colors.grey[200], border: const Border(top: BorderSide(color: Colors.black38, width: .1)), ), child: GridView( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( // 横轴元素个数 crossAxisCount: 4, // 纵轴间距 mainAxisSpacing: 30.0, // 横轴间距 crossAxisSpacing: 25.0, // 子组件宽高比例 childAspectRatio: .8, ), children: chooseOptions.map((item) { return Column( children: [ Expanded( child: Material( type: MaterialType.transparency, child: Ink( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15.0), ), child: InkWell( borderRadius: BorderRadius.circular(15.0), child: Image.asset(item['icon'], height: 40.0, fit: BoxFit.cover), onTap: () { handleChooseAction(item['key']); }, ), ), ), ), const SizedBox(height: 5.0), Text( item['name'], style: const TextStyle(color: Colors.black87, fontSize: 12.0), ) ], ); }).toList(), ), ), ), ]; } /* ---------- { 聊天消息模块 } ---------- */ // 聊天消息滚动到底部 // void scrollToBottom() async { // chatList = await fetchChatList(); // chatController.animateTo(isNeedScrollBottom ? 0 : chatController.position.maxScrollExtent, // duration: const Duration(milliseconds: 200), curve: Curves.easeIn); // } // 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() { hideKeyboard(); setState(() { toolbarEnable = false; }); } /* ---------- { 底部Toolbar模块 } ---------- */ // 光标处插入内容 void insertTextAtCursor(String html) { var editorNotifier = editorController.value; // The current value stored in this notifier. var start = editorNotifier.selection.baseOffset; var end = editorNotifier.selection.extentOffset; if (editorNotifier.selection.isValid) { String newText = ''; if (editorNotifier.selection.isCollapsed) { if (end > 0) { newText += editorNotifier.text.substring(0, end); } newText += html; if (editorNotifier.text.length > end) { newText += editorNotifier.text.substring(end, editorNotifier.text.length); } } else { newText = editorNotifier.text.replaceRange(start, end, html); end = start; } editorController.value = editorNotifier.copyWith(text: newText, selection: editorNotifier.selection.copyWith(baseOffset: end + html.length, extentOffset: end + html.length)); } else { editorController.value = TextEditingValue( text: html, selection: TextSelection.fromPosition(TextPosition(offset: html.length)), ); } } // 发送消息队列 void sendMessage(message) async { final canSend = checkSend(); if (!canSend) { final baseStyle = MyDialog.theme.toastStyle?.top(); MyDialog.toast( '对方未回关或回复,只能发送一条消息', icon: const Icon(Icons.check_circle), duration: Duration(milliseconds: 5000), style: baseStyle?.copyWith( backgroundColor: Colors.red.withAlpha(200), ), ); print('禁止发送$canSend'); return; } // 待插入的消息 List messagesToInsert = []; V2TimMessage? lastRealMsg; for (var msg in controller.chatList) { if (msg.localCustomData != 'time_label') { lastRealMsg = msg; break; } } // 如果有数据,检测时间,是否需要插入伪消息 if (lastRealMsg != null && needInsertTimeLabel( (lastRealMsg.timestamp ?? 0) * 1000, // 转为毫秒级 DateTime.now().millisecondsSinceEpoch, )) { // 消息时间间隔超过3分钟插入伪消息 final showLabel = Utils.formatChatTime(DateTime.now().millisecondsSinceEpoch ~/ 1000); final resMsg = await IMMessage().insertTimeLabel(showLabel, selfUserId); messagesToInsert.add(resMsg.data); } else { // 没数据的时候直接插入伪消息 final showLabel = Utils.formatChatTime(DateTime.now().millisecondsSinceEpoch ~/ 1000); final resMsg = await IMMessage().insertTimeLabel(showLabel, selfUserId); messagesToInsert.add(resMsg.data); } // 不需要时间标签 // 消息类型,message['type'] late final ImResult res; res = await IMMessage().sendMessage( msg: message, toUserID: arguments.value.userID, cloudCustomData: canSend == true ? ConversationType.noFriend.name : '', ); if (res.success && res.data != null) { messagesToInsert.insert(0, res.data); // 加入消息本体 // messagesToInsert.add(res.data); // 加入消息本体 controller.chatList.insertAll(0, messagesToInsert); // controller.chatList.addAll(messagesToInsert); controller.scrollToBottom(); print('发送成功'); } else { print('消息发送失败: ${res.code} - ${res.desc}'); } } bool needInsertTimeLabel(int lastTimestamp, int newTimestamp, {int interval = 3 * 60}) { return (newTimestamp - lastTimestamp) > interval * 1000; } // 隐藏键盘 void hideKeyboard() { if (editorFocusNode.hasFocus) { editorFocusNode.unfocus(); } } // 表情/选择切换 void handleEmojChooseState(index) { hideKeyboard(); setState(() { toolbarEnable = true; toolbarIndex = index; voiceBtnEnable = false; }); controller.scrollToBottom(); } // 表情Tab切换 void handleEmojTab(index) { var emols = emoJson; for (var i = 0, len = emols.length; i < len; i++) { emols[i]['selected'] = false; } emols[index]['selected'] = true; setState(() { emoJson = emols; }); emojController.jumpTo(0); } // 点击表情插入到输入框 void handleEmojClick(emoj) { insertTextAtCursor(emoj); } // 点击Gif大图发送=8 void handleGIFClick(gifpath, index) async { // 消息队列 // Map message = { // 'contentType': 8, // 'content': gifpath, // }; final res = await IMMessage().createFaceMessage(data: gifpath, index: index); if (res.success) { sendMessage(res.data?.messageInfo); } } // 发送文本消息=1 void handleSubmit() async { if (editorController.text.isEmpty) return; // 消息队列 // Map message = { // 'contentType': 1, // 'content': editorController.text, // }; final res = await IMMessage().createTextMessage(text: editorController.text); if (res.success) { sendMessage(res.data?.messageInfo); editorController.clear(); } } // 发送图片消息=3 void sendImage(imgPath) async { final resImg = await IMMessage().createImageMessage(imagePath: imgPath); if (resImg.success) { sendMessage(resImg.data?.messageInfo); } } // 发送视频消息=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': // .... break; case 'redpacket': sendRedPacketDialog(); break; } } ///从相册选取图片/视频 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 > 100) { MyDialog.toast('视频大小不能超过100MB', 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) { showDialog( context: context, builder: (context) { return Material( type: MaterialType.transparency, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 50.0), padding: const EdgeInsets.symmetric(vertical: 50.0), decoration: const BoxDecoration( color: Color(0xFFFF7F43), borderRadius: BorderRadius.all(Radius.circular(12.0)), ), child: Column( children: [ ClipRRect( borderRadius: BorderRadius.circular(5.0), child: Image.asset(data['avatar'], height: 40.0, width: 40.0, fit: BoxFit.cover), ), const SizedBox( height: 5.0, ), Text( data['author'], style: const TextStyle(color: Color(0xFFFFF9C7), fontWeight: FontWeight.w600), ), Text( data['content'], 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), ), 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(); }); }, ), ); }, ), ], ), ), GestureDetector( child: Container( margin: const EdgeInsets.only(top: 20.0), height: 30.0, width: 30.0, decoration: BoxDecoration( border: Border.all(color: Colors.white, width: 1.5), borderRadius: BorderRadius.circular(50.0), ), child: const Icon( Icons.close_outlined, color: Colors.white, size: 18.0, ), ), onTap: () { Navigator.of(context).pop(); }, ) ], )); }); } // 长按消息菜单 void contextMenuDialog() { return; showDialog( context: context, builder: (context) { return SimpleDialog( backgroundColor: Colors.white, surfaceTintColor: Colors.white, contentPadding: const EdgeInsets.symmetric(vertical: 5.0), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), children: [ SimpleDialogOption( child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('复制')), onPressed: () {}, ), SimpleDialogOption( child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('发送给朋友')), onPressed: () {}, ), SimpleDialogOption( child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('收藏')), onPressed: () {}, ), SimpleDialogOption( child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('删除')), onPressed: () {}, ), ], ); }, ); } // 发群红包弹窗 void sendRedPacketDialog() { showModalBottomSheet( backgroundColor: Colors.grey[50], shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(15.0))), showDragHandle: true, clipBehavior: Clip.hardEdge, isScrollControlled: true, // 屏幕最大高度 constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height - 180, // 自定义最大高度 ), context: context, builder: (context) { return RedPacket( flag: false, ); }, ); } /* ---------- { 其它功能模块 } ---------- */ @override Widget build(BuildContext context) { return Stack( fit: StackFit.expand, children: [ // 页面主体(聊天消息区/底部操作区) Scaffold( backgroundColor: Colors.grey[200], resizeToAvoidBottomInset: true, // 启用键盘自动避让 appBar: AppBar( centerTitle: true, backgroundColor: Colors.transparent, foregroundColor: Colors.white, leading: IconButton( icon: Icon( Icons.arrow_back_ios_rounded, size: 20.0, ), onPressed: () { Get.back(); }, ), titleSpacing: 1.0, title: Obx(() { return Text( // '${arguments['title']}', '${arguments.value.showName}', style: const TextStyle(fontSize: 18.0, fontFamily: 'Arial'), ); }), flexibleSpace: Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFBE4EFF), Color(0xFF1DFFC7)], )), ), actions: [ // IconButton( // icon: const Icon( // Icons.more_horiz, // color: Colors.white, // ), // onPressed: () async { // final paddingTop = MediaQuery.of(Get.context!).padding.top; // final selected = await showMenu( // context: Get.context!, // position: RelativeRect.fromLTRB( // double.infinity, // kToolbarHeight + paddingTop - 12, // 8, // double.infinity, // ), // color: FStyle.primaryColor, // elevation: 8, // items: [ // PopupMenuItem( // value: 'report', // child: Row( // children: [ // Icon(Icons.report, color: Colors.white, size: 18), // SizedBox(width: 8), // Text( // '举报', // style: TextStyle(color: Colors.white), // ), // ], // ), // ), // PopupMenuItem( // value: 'block', // child: Row( // children: [ // Icon(Icons.block, color: Colors.white, size: 18), // SizedBox(width: 8), // Text( // '拉黑', // style: TextStyle(color: Colors.white), // ), // ], // ), // ), // ], // ); // if (selected != null) { // switch (selected) { // case 'report': // print('点击了举报'); // break; // case 'block': // print('点击了拉黑'); // break; // } // } // }, // ), ], ), body: Flex( direction: Axis.vertical, crossAxisAlignment: CrossAxisAlignment.start, children: [ // 渲染聊天消息 // Expanded( // child: ScrollConfiguration( // behavior: CustomScrollBehavior(), // child: GestureDetector( // child: Obx(() { // return ListView( // reverse: true, // controller: chatController, // padding: const EdgeInsets.all(10.0), // children: renderChatList(), // ); // }), // onTap: () { // handleClickChatArea(); // }, // ), // ), // ), // 聊天内容 // Expanded( // child: GestureDetector( // behavior: HitTestBehavior.opaque, // onTap: handleClickChatArea, // child: LayoutBuilder( // builder: (context, constraints) { // return Obx(() { // // final tipWidget = Builder(builder: (context) => Text(tips)); // final msgWidgets = renderChatList().reversed.toList(); // // if (controller.isFriend.value) {} // return ListView( // controller: chatController, // reverse: true, // padding: const EdgeInsets.all(10.0), // children: [ // ConstrainedBox( // constraints: BoxConstraints( // minHeight: constraints.maxHeight - 20, // ), // child: Column( // mainAxisSize: MainAxisSize.min, // children: msgWidgets, // ), // ), // ], // ); // }); // }, // ), // ), // ), // 聊天内容 Expanded( child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: handleClickChatArea, child: LayoutBuilder( builder: (context, constraints) { return Obx(() { final showTipWidget = controller.followType.value; const double tipHeight = 50; // 提示横幅固定高度 final msgWidgets = renderChatList().reversed.toList(); return Stack( children: [ ListView( controller: chatController, reverse: true, padding: EdgeInsets.only( top: [1, 2].contains(showTipWidget) ? tipHeight + 10 : 0, // 动态预留顶部空间 left: 10, right: 10, bottom: 10, ), children: [ ConstrainedBox( constraints: BoxConstraints( minHeight: constraints.maxHeight - ([1, 2].contains(showTipWidget) ? tipHeight : 0), ), child: Column( mainAxisSize: MainAxisSize.min, children: msgWidgets, ), ), ], ), if ([1, 2].contains(showTipWidget)) Positioned( top: 0, left: 0, right: 0, height: tipHeight, // 固定高度 child: Container( color: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Icon( Icons.error_outline, color: FStyle.primaryColor, size: 20, ), SizedBox( width: 10, ), Expanded( child: Text( '对方未回复或互关前仅可发送一条消息', style: const TextStyle(color: Colors.black, fontSize: 14), overflow: TextOverflow.ellipsis, ), ), Visibility( visible: [2].contains(showTipWidget), child: TextButton( style: TextButton.styleFrom( backgroundColor: FStyle.primaryColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), onPressed: () async { // 回关 final res = await ImService.instance.followUser(userIDList: [arguments.value.userID!]); if (res.success) { controller.isFriend.value = true; controller.followType.value = 3; if (arguments.value.conversationGroupList?.isNotEmpty == true) { //把会话数据从陌生人分组移除 final ctl = Get.find(); ctl.removeNoFriend(conversationID: arguments.value.conversationID); ctl.updateNoFriendMenu(); } // 跳转到chat地址,销毁当前页面 Get.offAllNamed( '/chat', arguments: arguments.value, predicate: (route) { // 清理栈,只保留 `/` return route.settings.name == '/'; }, ); } }, child: const Text('回关', style: TextStyle(color: Colors.white)), ), ) ], ), ), ), ], ); }); }, ), ), ), // 底部操作栏 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, Icons.keyboard_outlined, color: const Color(0xFF3B3B3B), size: 30.0, ), onTap: () { setState(() { toolbarEnable = false; if (editorFocusNode.hasFocus) { editorFocusNode.unfocus(); } else { editorFocusNode.requestFocus(); } if (toolbarEnable) { // voiceBtnEnable = false; // editorFocusNode.requestFocus(); // editorFocusNode.requestFocus(); } else { // voiceBtnEnable = true; // toolbarEnable = 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) {}, ), ), // 语音 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), ), ), onPanStart: (details) { 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; }); }, ), ), ], ), ), ), const SizedBox( width: 10.0, ), // InkWell( // child: const Icon( // Icons.add_reaction_rounded, // color: Color(0xFF3B3B3B), // size: 30.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(); }, ), ], ), ), // 表情+选择模块 Visibility( visible: toolbarEnable, child: SizedBox( height: keyboardHeight, child: Column( children: toolbarIndex == 0 ? renderEmojWidget() : renderChooseWidget(), ), ), ) ], ), ), ), ) ], ), ), // 录音主体(按住说话/松开取消/语音转文本) IgnorePointer( ignoring: false, child: Visibility( visible: voicePanelEnable, child: Material( color: const Color(0xDD1B1B1B), child: Stack( children: [ // 取消发送+语音转文字 Positioned( bottom: 120, left: 30, right: 30, child: Visibility( visible: !voiceToTransfer, child: Column( crossAxisAlignment: voiceType == 2 ? CrossAxisAlignment.start : CrossAxisAlignment.center, children: [ // 语音动画层 Stack( alignment: Alignment.bottomCenter, children: [ AnimatedContainer( duration: Duration(milliseconds: 200), height: 70.0, width: voiceType == 2 ? 70.0 : 200.0, decoration: BoxDecoration( color: voiceType == 2 ? Colors.red : Color(0xFF89E45B), borderRadius: BorderRadius.circular(15.0), ), clipBehavior: Clip.antiAlias, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset('assets/images/voice_waves.gif', height: 23.0, width: voiceType == 2 ? 30.0 : 70.0, fit: BoxFit.cover) ], ), ), RotatedBox( quarterTurns: 0, child: CustomPaint(painter: ArrowShape(arrowColor: voiceType == 2 ? Colors.red : Color(0xFF89E45B), arrowSize: 10.0)), ) ], ), const SizedBox( height: 50.0, ), // 操作项 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // 取消发送 Container( height: 60.0, width: 60.0, decoration: BoxDecoration( borderRadius: BorderRadius.circular(50.0), color: voiceType == 2 ? Colors.red : Colors.black38, ), child: Icon( Icons.close, 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, ), ), ], ), ], ), ), ), // 语音转文字(识别结果状态) Positioned( bottom: 120, left: 30, right: 30, child: Visibility( visible: voiceToTransfer, child: Column( children: [ // 提示结果 Stack( children: [ Container( height: 100.0, decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(15.0), ), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.info_outlined, color: Colors.white, ), Text( '未识别到文字。', style: TextStyle(color: Colors.white), ), ], ), ), Positioned( right: 35.0, bottom: 1, child: RotatedBox( quarterTurns: 0, child: CustomPaint(painter: ArrowShape(arrowColor: Colors.red, arrowSize: 10.0)), )), ], ), const SizedBox( height: 50.0, ), // 操作项 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ GestureDetector( child: Container( height: 60.0, width: 60.0, decoration: const BoxDecoration( color: Colors.transparent, ), child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.undo, color: Colors.white54, ), Text( '取消', style: TextStyle(color: Colors.white70), ) ], ), ), onTap: () { setState(() { voicePanelEnable = false; voiceToTransfer = false; }); }, ), GestureDetector( child: Container( height: 60.0, width: 100.0, decoration: const BoxDecoration( color: Colors.transparent, ), child: const Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.graphic_eq_rounded, color: Colors.white54, ), Text( '发送原语音', style: TextStyle(color: Colors.white70), ) ], ), ), onTap: () {}, ), GestureDetector( child: Container( height: 60.0, width: 60.0, decoration: BoxDecoration( borderRadius: BorderRadius.circular(50.0), color: Colors.white12, ), child: const Icon( Icons.check, color: Colors.white12, ), ), onTap: () {}, ), ], ), ], ), ), ), // 提示文字(操作状态) Positioned( bottom: 120, left: 0, width: MediaQuery.of(context).size.width, child: Visibility( visible: !voiceToTransfer, child: Align( child: Text( voiceTypeMap[voiceType], style: const TextStyle(color: Colors.white70), ), ), ), ), // 背景 Align( alignment: Alignment.bottomCenter, child: Visibility( visible: !voiceToTransfer, child: Image.asset('assets/images/voice_bg.webp', width: double.infinity, height: 100.0, fit: BoxFit.fill), ), ), // 背景图标 Positioned( bottom: 25, left: 0, width: MediaQuery.of(context).size.width, child: Visibility( visible: !voiceToTransfer, child: const Align( child: Icon( Icons.graphic_eq_rounded, color: Colors.black54, ), ), ), ), ], ), ), ), ) ], ); } } // 渲染聊天消息公共部分 class RenderChatItem extends StatelessWidget { const RenderChatItem({ super.key, required this.data, required this.child, }); final V2TimMessage data; // 消息数据 final Widget? child; // 消息体 // 设置箭头颜色 // Color arrowColor(data) { // Color color = Colors.transparent; // if ([8].contains(data.elemType)) { // // 红包箭头颜色 // color = const Color(0xFFFFA52F); // } else if ([9].contains(data.elemType)) { // // 位置箭头颜色 // color = const Color(0xFFFFFFFF); // } else { // color = !data['isme'] ? const Color(0xFFFFFFFF) : const Color(0xFF9543FF); // } // return color; // } @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) ? 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(), Expanded( child: Padding( padding: !(data.isSelf ?? false) ? const EdgeInsets.only(left: 10.0, right: 40.0) : const EdgeInsets.only(left: 40.0, right: 10.0), child: Column( crossAxisAlignment: !(data.isSelf ?? false) ? CrossAxisAlignment.start : CrossAxisAlignment.end, children: [ // Text( // displayName ?? '未知昵称', // style: const TextStyle(color: Colors.grey, fontSize: 12.0), // ), // const SizedBox( // height: 3.0, // ), Stack( children: [ // 气泡箭头 /* Visibility( // 显示箭头(消息+语音+红包+位置) visible: [3, 7, 8, 9].contains(data['contentType']), child: Positioned( left: !data['isme'] ? 1 : null, right: data['isme'] ? 1 : null, top: 20.0, child: RotatedBox( quarterTurns: !data['isme'] ? 1 : -1, child: CustomPaint(painter: ArrowShape(arrowColor: arrowColor(data))), ) ), ), */ Container( child: child, ), ], ), ], ), ), ), data.isSelf ?? false ? 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(), ], ), ); } } // 绘制气泡箭头 class ArrowShape extends CustomPainter { ArrowShape({ required this.arrowColor, this.arrowSize = 7, }); final Color arrowColor; // 箭头颜色 final double arrowSize; // 箭头大小 @override void paint(Canvas canvas, Size size) { var paint = Paint()..color = arrowColor; var path = Path(); path.lineTo(-arrowSize, 0); path.lineTo(0, arrowSize); path.lineTo(arrowSize, 0); canvas.drawPath(path, paint); } @override bool shouldRepaint(CustomPainter oldDelegate) { return false; } }