Browse Source

feat(message): 实现会话列表功能- 新增 ConversationController 管理会话数据

-从 IMManager 获取真实会话列表替代模拟数据
- 支持会话列表加载状态与错误处理
- 实现会话项 UI 展示包括头像、昵称、最后消息等
- 添加未读消息数与消息时间格式化显示
- 集成 GetX 状态管理与响应式更新
- 优化会话列表空状态与加载失败重试机制
ios
Jolie 4 months ago
parent
commit
e99a75cab4
4 changed files with 258 additions and 151 deletions
  1. 113
      lib/controller/message/conversation_controller.dart
  2. 12
      lib/im/im_manager.dart
  3. 280
      lib/pages/message/conversation_tab.dart
  4. 4
      lib/pages/message/message_page.dart

113
lib/controller/message/conversation_controller.dart

@ -0,0 +1,113 @@
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import '../../im/im_manager.dart';
class ConversationController extends GetxController {
//
final conversations = <EMConversation>[].obs;
//
final isLoading = false.obs;
//
final errorMessage = ''.obs;
@override
void onInit() {
super.onInit();
//
loadConversations();
}
///
Future<void> loadConversations() async {
if (isLoading.value) return;
try {
isLoading.value = true;
errorMessage.value = '';
// IMManager获取会话列表
final List<EMConversation> convList = await IMManager.instance.getConversations();
//
conversations.value = convList;
// 使GetX日志系统
if (Get.isLogEnable) {
Get.log('Loaded ${convList.length} conversations');
}
} catch (e) {
// 使GetX日志系统
if (Get.isLogEnable) {
Get.log('Failed to load conversations: $e');
}
errorMessage.value = '加载会话列表失败,请稍后重试';
} finally {
isLoading.value = false;
}
}
///
Future<void> refreshConversations() async {
await loadConversations();
}
///
String getLastMessageContent(EMMessage? message) {
if(message?.body.type == MessageType.TXT){
}
return '暂无消息';
}
///
Future<int> getUnreadCount(EMConversation conversation) async {
try {
// 0
return await conversation.unreadCount();
} catch (e) {
if (Get.isLogEnable) {
Get.log('Error getting unread count: $e');
}
return 0;
}
}
///
String getConversationLastMessageTime(EMConversation conversation) {
try {
//
return '刚刚';
} catch (e) {
if (Get.isLogEnable) {
Get.log('Error getting last message time: $e');
}
return '';
}
}
///
String formatMessageTime(int timestamp) {
DateTime messageTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
DateTime now = DateTime.now();
Duration difference = now.difference(messageTime);
if (difference.inDays > 0) {
return '${difference.inDays}天前';
} else if (difference.inHours > 0) {
return '${difference.inHours}小时前';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes}分钟前';
} else {
return '刚刚';
}
}
Future<EMUserInfo> loadContact(String userId) async{
var data = await IMManager.instance.getContacts(userId);
return data[userId]!;
}
Future<EMMessage?> lastMessage(EMConversation conversation) async{
return await conversation.latestMessage();
}
}

12
lib/im/im_manager.dart

@ -207,12 +207,12 @@ class IMManager {
/// ///
Future<List<EMConversation>> getConversations() async { Future<List<EMConversation>> getConversations() async {
try {
return EMClient.getInstance.chatManager.loadAllConversations();
} catch (e) {
print('Failed to get conversations: $e');
return [];
}
return EMClient.getInstance.chatManager.loadAllConversations();
}
///
Future<Map<String, EMUserInfo>> getContacts(String userId) async {
return await EMClient.getInstance.userInfoManager.fetchUserInfoById([userId]);
} }
/// ///

280
lib/pages/message/conversation_tab.dart

@ -1,5 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dating_touchme_app/generated/assets.dart'; import 'package:dating_touchme_app/generated/assets.dart';
import 'package:get/get.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import '../../controller/message/conversation_controller.dart';
class ConversationTab extends StatefulWidget { class ConversationTab extends StatefulWidget {
const ConversationTab({super.key}); const ConversationTab({super.key});
@ -9,6 +12,8 @@ class ConversationTab extends StatefulWidget {
} }
class _ConversationTabState extends State<ConversationTab> with AutomaticKeepAliveClientMixin { class _ConversationTabState extends State<ConversationTab> with AutomaticKeepAliveClientMixin {
final ConversationController controller = Get.find<ConversationController>();
// - // -
final List<Map<String, dynamic>> _recommendedUsers = [ final List<Map<String, dynamic>> _recommendedUsers = [
{"id": 1, "avatar": Assets.imagesAvatarsExample, "type": "相亲"}, {"id": 1, "avatar": Assets.imagesAvatarsExample, "type": "相亲"},
@ -19,51 +24,6 @@ class _ConversationTabState extends State<ConversationTab> with AutomaticKeepAli
{"id": 6, "avatar": Assets.imagesAvatarsExample, "type": "相亲"}, {"id": 6, "avatar": Assets.imagesAvatarsExample, "type": "相亲"},
]; ];
// -
final List<Map<String, dynamic>> _chatList = [
{
"id": 1,
"name": "系统通知",
"avatar": Assets.imagesVerifiedIcon,
"message": "在干嘛",
"time": "19:24",
"unreadCount": 2,
},
{
"id": 2,
"name": "林园园",
"avatar": Assets.imagesAvatarsExample,
"message": "在干嘛",
"time": "19:24",
"unreadCount": 2,
"isOnline": true,
},
{
"id": 3,
"name": "李晖",
"avatar": Assets.imagesAvatarsExample,
"message": "好的",
"time": "19:24",
"unreadCount": 0,
},
{
"id": 4,
"name": "李哲",
"avatar": Assets.imagesAvatarsExample,
"message": "在干嘛",
"time": "19:24",
"unreadCount": 0,
},
{
"id": 5,
"name": "李夏",
"avatar": Assets.imagesAvatarsExample,
"message": "在干嘛",
"time": "19:24",
"unreadCount": 0,
},
];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
@ -73,14 +33,39 @@ class _ConversationTabState extends State<ConversationTab> with AutomaticKeepAli
_buildRecommendedUsers(), _buildRecommendedUsers(),
// //
Expanded( Expanded(
child: ListView.builder(
padding: const EdgeInsets.only(top: 8),
itemCount: _chatList.length,
itemBuilder: (context, index) {
final chat = _chatList[index];
return _buildChatItem(chat);
},
),
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('重试'),
),
],
),
);
}
if (controller.conversations.isEmpty) {
return const Center(child: Text('暂无会话'));
}
return ListView.builder(
padding: const EdgeInsets.only(top: 8),
itemCount: controller.conversations.length,
itemBuilder: (context, index) {
final conversation = controller.conversations[index];
return _buildConversationItem(conversation);
},
);
}),
), ),
], ],
); );
@ -149,100 +134,105 @@ class _ConversationTabState extends State<ConversationTab> with AutomaticKeepAli
); );
} }
//
Widget _buildChatItem(Map<String, dynamic> chat) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
//
Stack(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(25),
child: Image.asset(chat["avatar"], fit: BoxFit.cover),
),
),
if (chat["isOnline"] == true)
Positioned(
bottom: 0,
right: 0,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.white, width: 2),
//
Widget _buildConversationItem(EMConversation conversation) {
// 使FutureBuilder获取未读消息数和最新消息
return FutureBuilder<EMUserInfo>(
future: controller.loadContact(conversation.id),
builder: (context, snapshot) {
EMUserInfo? userInfo = snapshot.data;
return FutureBuilder<EMMessage?>(
future: controller.lastMessage(conversation),
builder: (context, snapshot) {
EMMessage? message = snapshot.data;
return FutureBuilder<int>(
future: controller.getUnreadCount(conversation),
builder: (context, snapshot){
int unreadCount = snapshot.data ?? 0;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
),
),
],
),
//
Expanded(
child: Container(
margin: const EdgeInsets.only(left: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
chat["name"],
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
chat["time"],
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
margin: const EdgeInsets.only(bottom: 8, left: 16, right: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
image: DecorationImage(
image: userInfo?.avatarUrl != '' ? NetworkImage(userInfo?.avatarUrl ?? '') : AssetImage(Assets.imagesAvatarsExample),
fit: BoxFit.cover,
),
),
), ),
),
],
),
const SizedBox(height: 4),
Text(
chat["message"],
style: const TextStyle(color: Colors.grey),
overflow: TextOverflow.ellipsis,
),
],
),
),
),
//
if (chat["unreadCount"] > 0)
Container(
width: 18,
height: 18,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(9),
),
alignment: Alignment.center,
child: Text(
chat["unreadCount"].toString(),
style: const TextStyle(color: Colors.white, fontSize: 12),
),
),
],
),
const SizedBox(width: 12),
//
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
userInfo?.nickName ?? '联系人',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black,
),
),
Text(
controller.formatMessageTime(message?.serverTime ?? 0),
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
const SizedBox(height: 4),
Text(
controller.getLastMessageContent(message),
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
//
if (unreadCount > 0)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10),
),
child: Text(
unreadCount.toString(),
style: const TextStyle(
fontSize: 12,
color: Colors.white,
),
),
),
],
),
);
}
);
},
);
},
); );
} }

4
lib/pages/message/message_page.dart

@ -1,8 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:dating_touchme_app/generated/assets.dart'; import 'package:dating_touchme_app/generated/assets.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'conversation_tab.dart'; import 'conversation_tab.dart';
import 'friend_tab.dart'; import 'friend_tab.dart';
import '../../controller/message/conversation_controller.dart';
class MessagePage extends StatefulWidget { class MessagePage extends StatefulWidget {
const MessagePage({super.key}); const MessagePage({super.key});
@ -18,6 +20,8 @@ class _MessagePageState extends State<MessagePage> with AutomaticKeepAliveClient
void initState() { void initState() {
super.initState(); super.initState();
_tabController = TabController(length: 2, vsync: this); _tabController = TabController(length: 2, vsync: this);
// ConversationController
Get.put(ConversationController());
} }
@override @override

Loading…
Cancel
Save