import 'package:dating_touchme_app/extension/ex_widget.dart'; import 'package:dating_touchme_app/im/im_manager.dart'; import 'package:dating_touchme_app/pages/message/chat_page.dart'; import 'package:dating_touchme_app/pages/message/connect_history_page.dart'; import 'package:flutter/material.dart'; import 'package:dating_touchme_app/generated/assets.dart'; import 'package:get/get.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../../controller/message/conversation_controller.dart'; import '../../widget/message/emoji_text_widget.dart'; import '../../config/emoji_config.dart'; class ConversationTab extends StatefulWidget { const ConversationTab({super.key}); @override State createState() => _ConversationTabState(); } class _ConversationTabState extends State with AutomaticKeepAliveClientMixin { final ConversationController controller = Get.find(); @override Widget build(BuildContext context) { super.build(context); return Column( children: [ Container( padding: EdgeInsets.symmetric( horizontal: 22.w ), child: Row( children: [ Column( children: [ Container( width: 90.w, height: 50.w, margin: EdgeInsets.only(bottom: 5.w), decoration: BoxDecoration( color: const Color.fromRGBO(226, 222, 255, 1), borderRadius: BorderRadius.all(Radius.circular(50.w)) ), child: Center( child: Image.asset( Assets.imagesConnectHistoryIcon, width: 40.w, height: 40.w, ), ), ), Text( "连线记录", style: TextStyle( fontSize: 11.w ), ) ], ).onTap((){ Get.to(() => ConnectHistoryPage()); }) ], ), ), // 聊天列表 Expanded( child: Obx(() { if (controller.isLoading.value) { return const Center(child: CircularProgressIndicator()); } if (controller.errorMessage.value.isNotEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(controller.errorMessage.value), ElevatedButton( onPressed: () => controller.refreshConversations(), child: const Text('重试'), ), ], ), ); } // 直接使用 Obx 监听 conversations 和 filterType,避免 FutureBuilder 重建导致的闪烁 return Obx(() { final filteredConversations = controller.conversations; if (filteredConversations.isEmpty) { return Center( child: Text( controller.filterType.value == FilterType.none ? '暂无会话' : '暂无符合条件的会话', style: const TextStyle( fontSize: 14, color: Colors.grey, ), ), ); } return ListView.builder( padding: const EdgeInsets.only(top: 8), itemCount: filteredConversations.length, itemBuilder: (context, index) { final conversation = filteredConversations[index]; return BuildConversationItem(conversation: conversation); }, ); }); }), ), ], ); } @override bool get wantKeepAlive => true; } class BuildConversationItem extends StatefulWidget { final EMConversation conversation; const BuildConversationItem({super.key, required this.conversation}); @override State createState() => _BuildConversationItemState(); } class _BuildConversationItemState extends State { late EMConversation conversation; bool isOnline = false; late var cachedUserInfo; final ConversationController controller = Get.find(); @override void initState() { super.initState(); conversation = widget.conversation; cachedUserInfo = controller.getCachedUserInfo(conversation.id); IMManager.instance.getUserPresenceStatus(conversation.id).then((e){ isOnline = e == true; setState(() { }); }); } @override Widget build(BuildContext context) { return FutureBuilder( future: cachedUserInfo != null ? Future.value(cachedUserInfo) : controller.loadContact(conversation.id), initialData: cachedUserInfo, builder: (context, userSnapshot) { final ExtendedUserInfo? userInfo = userSnapshot.data; return FutureBuilder( future: controller.lastMessage(conversation), builder: (context, messageSnapshot) { final EMMessage? message = messageSnapshot.data; return FutureBuilder( future: controller.getUnreadCount(conversation), builder: (context, unreadSnapshot) { final int unreadCount = unreadSnapshot.data ?? 0; final double screenWidth = MediaQuery.of(context).size.width; final Widget cellContent = Builder( builder: (cellContext) => GestureDetector( onTap: () async { TDSwipeCellInherited.of(cellContext)?.cellClick(); Get.to(ChatPage(userId: conversation.id)); }, child: Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(16)), ), margin: const EdgeInsets.only( bottom: 8, left: 16, right: 16, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Stack( children: [ Container( width: 56, height: 56, decoration: BoxDecoration( borderRadius: BorderRadius.circular(28), color: Colors.grey[300], ), child: ClipRRect( borderRadius: BorderRadius.circular(28), child: (userInfo?.avatarUrl != null && userInfo!.avatarUrl!.isNotEmpty) ? CachedNetworkImage( imageUrl: userInfo.avatarUrl!, width: 56, height: 56, fit: BoxFit.cover, placeholder: (context, url) => Image.asset( Assets.imagesUserAvatar, width: 56, height: 56, fit: BoxFit.cover, ), errorWidget: (context, url, error) => Image.asset( Assets.imagesUserAvatar, width: 56, height: 56, fit: BoxFit.cover, ), ) : Image.asset( Assets.imagesUserAvatar, width: 56, height: 56, fit: BoxFit.cover, ), ), ), if(isOnline) Positioned( bottom: 0, right: 0, child: Container( width: 11.w, height: 11.w, decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(11.w)), color: const Color.fromRGBO(43, 255, 191, 1) ), ), ) ], ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Row( children: [ Flexible( child: Text( userInfo?.nickName ?? conversation.id, // 如果没有昵称,显示用户ID maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.black, ), ), ), if(isOnline) Container( width: 33.w, height: 13.w, margin: EdgeInsets.only(left: 6.w), decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(13.w)), color: const Color.fromRGBO(234, 255, 219, 1) ), child: Center( child: Text( "在线", style: TextStyle( fontSize: 11.w, color: const Color.fromRGBO(38, 199, 124, 1) ), ), ), ), ], ), ), SizedBox(width: 6.w,), Text( controller.formatMessageTime( message?.serverTime ?? 0, ), style: const TextStyle( fontSize: 12, color: Colors.grey, ), ), ], ), const SizedBox(height: 6), Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: _buildLastMessageContent( controller.getLastMessageContent(message), message, ), ), if (unreadCount > 0) TDBadge(TDBadgeType.message, count: unreadCount.toString(), maxCount: '99') ], ), ], ), ), ], ), ), ), ); return TDSwipeCell( slidableKey: ValueKey('conversation_${conversation.id}'), groupTag: 'conversation_swipe_group', right: TDSwipeCellPanel( extentRatio: 72 / screenWidth, children: [ TDSwipeCellAction( backgroundColor: TDTheme.of(context).errorColor6, label: '删除', onPressed: (actionContext) async { final success = await controller.deleteConversation( conversation.id, ); if (!success) { Get.snackbar('提示', '删除会话失败'); } }, ), ], ), cell: cellContent, ); }, ); }, ); }, ); } // 构建最后一条消息内容(支持表情显示) Widget _buildLastMessageContent(String content, EMMessage? message) { // 检查是否是发送失败的消息 final isFailed = message != null && message.direction == MessageDirection.SEND && message.status == MessageStatus.FAIL; // 检查是否包含表情 final containsEmoji = EmojiTextHelper.containsEmoji(content); if (containsEmoji && !isFailed) { // 如果包含表情且不是失败消息,使用自定义的单行表情文本 widget return _buildSingleLineEmojiText( content, TextStyle( fontSize: 14.sp, color: Colors.grey, ), ); } else { // 如果不包含表情或是失败消息,使用普通 Text 显示 return Text( content, style: TextStyle( fontSize: 14.sp, color: isFailed ? Color.fromRGBO(248, 85, 66, 1) // 发送失败时显示红色 : Colors.grey, ), overflow: TextOverflow.ellipsis, maxLines: 1, ); } } // 构建单行表情文本(支持省略) Widget _buildSingleLineEmojiText(String text, TextStyle textStyle) { final RegExp emojiRegex = RegExp(r'\[emoji:(\d+)\]'); final List spans = []; int lastMatchEnd = 0; final matches = emojiRegex.allMatches(text); for (final match in matches) { // 添加表情之前的文本 if (match.start > lastMatchEnd) { final textPart = text.substring(lastMatchEnd, match.start); spans.add(TextSpan(text: textPart, style: textStyle)); } // 添加表情图片(使用 WidgetSpan) final emojiId = match.group(1); if (emojiId != null) { final emoji = EmojiConfig.getEmojiById(emojiId); if (emoji != null) { spans.add(WidgetSpan( alignment: PlaceholderAlignment.middle, child: Padding( padding: EdgeInsets.symmetric(horizontal: 2.w), child: Image.asset( emoji.path, width: 16.w, height: 16.w, fit: BoxFit.contain, ), ), )); } else { // 如果表情不存在,显示原始文本 spans.add(TextSpan(text: match.group(0)!, style: textStyle)); } } lastMatchEnd = match.end; } // 添加剩余的文本 if (lastMatchEnd < text.length) { final textPart = text.substring(lastMatchEnd); spans.add(TextSpan(text: textPart, style: textStyle)); } return RichText( text: TextSpan(children: spans), maxLines: 1, overflow: TextOverflow.ellipsis, ); } }