diff --git a/lib/IM/controller/chat_controller.dart b/lib/IM/controller/chat_controller.dart index 2955ea4..4286cef 100644 --- a/lib/IM/controller/chat_controller.dart +++ b/lib/IM/controller/chat_controller.dart @@ -13,12 +13,123 @@ class ChatController extends GetxController { final chatList = [].obs; void initChatData() { - // chatList.value = []; chatList.clear(); nextSeq.value = '0'; isFinished.value = false; } + /// 检测分组 + Future handleGroup({required String conversationID}) async { + final isReal = await ImService.instance.getConversation(conversationID: conversationID); + final V2TimConversation conv = isReal.data; + if (conv.lastMessage != null) { + // 会话存在执行后续逻辑 + return true; + } else { + // 会话不存在,结束 + return false; + } + } + + /// 关注后移除陌生人会话 + Future removeNoFriend({required String conversationID}) async { + try { + final isContinue = await handleGroup(conversationID: conversationID); + if (isContinue) { + await ImService.instance.deleteConversationsFromGroup( + groupName: myConversationType.ConversationTypeStatic.noFriend, + conversationIDList: [conversationID], + ); + } + } catch (e) { + logger.e('移除陌生人会话分组异常:$e'); + } + } + + /// 取关后将会话移入陌生人会话 + Future mergeNoFriend({required conversationID}) async { + try { + final isContinue = await handleGroup(conversationID: conversationID); + if (isContinue) { + final typeEnum = myConversationType.ConversationTypeStatic.noFriend; + final hasGroupRes = await ImService.instance.getConversationGroupList(); + if (hasGroupRes.success) { + final exists = hasGroupRes.data?.any((item) => item == typeEnum) ?? false; + if (!exists) { + // 组不存在,创建组并把会话加入group中 + await ImService.instance.createConversationGroup( + groupName: typeEnum, + conversationIDList: [conversationID], + ); + logger.i('取关触发添加会话分组$typeEnum成功'); + } else { + // 分组存在直接添加 + await ImService.instance.addConversationsToGroup( + groupName: typeEnum, + conversationIDList: [conversationID], + ); + } + } + final res = chatList.firstWhereOrNull((item) => item.conversation.conversationID == conversationID); + + if (res != null) { + chatList.remove(res); + } + } + } catch (e) { + logger.w('移除会话列表时发生错误: $e'); + } + } + + // 关注后更新nofd菜单入口 + Future updateNoFriendMenu() async { + final res = await ImService.instance.getConversationListByFilter( + filter: V2TimConversationFilter(conversationGroup: myConversationType.ConversationType.noFriend.name), + nextSeq: 0, + count: 1, + ); + if (res.success && res.data != null) { + final convList = res.data!.conversationList ?? []; + if (convList.isNotEmpty) { + // 只要有数据,入口一定存在,找到入口数据替换 + final unread = await ImService.instance.getUnreadMessageCountByFilter( + filter: V2TimConversationFilter( + conversationGroup: myConversationType.ConversationType.noFriend.name, + hasUnreadCount: true, + ), + ); + + if (unread.success) { + var viewModelList = await ConversationViewModel.createConversationViewModel(convList: [convList.first]); + final conviewModel = viewModelList.first; + conviewModel.faceUrl = 'assets/images/notify/msr.png'; + conviewModel.isCustomAdmin = myConversationType.ConversationTypeStatic.noFriend; + conviewModel.conversation.showName = '陌生人消息'; + conviewModel.conversation.unreadCount = unread.data; + conviewModel.conversation.conversationID = myConversationType.ConversationTypeStatic.noFriend; + + // final newList = List.from(chatList); + final index = chatList.indexWhere( + (item) => item.isCustomAdmin == myConversationType.ConversationTypeStatic.noFriend, + ); + if (index != -1) { + chatList[index] = conviewModel; + // 排序 + chatList.sort((a, b) { + final atime = a.conversation.lastMessage?.timestamp ?? 0; + final btime = b.conversation.lastMessage?.timestamp ?? 0; + return btime.compareTo(atime); // 降序 + }); + // 更新 + chatList.refresh(); + } + } + } + } else { + logger.e('刷新入口失败-----${res.desc}'); + } + } + // 获取所有会话列表 Future getConversationList() async { logger.e('开始获取会话列表数据'); @@ -57,65 +168,101 @@ class ChatController extends GetxController { } } - ///构建陌生人消息菜单入口 - void getNoFriendData({V2TimConversation? csion}) async { + ///构建陌生人消息菜单入口(更新时候传入对应的会话) + Future getNoFriendData({V2TimConversation? csion}) async { // 检测会话列表是否已有陌生人消息菜单 - final hasNoFriend = chatList.any((item) => item.conversation.conversationGroupList?.contains(myConversationType.ConversationType.noFriend.name) ?? false); + // final hasNoFriend = chatList.any((item) => item.conversation.conversationGroupList?.contains(myConversationType.ConversationType.noFriend.name) ?? false); + final hasNoFriend = chatList.any((item) => item.isCustomAdmin!.contains(myConversationType.ConversationType.noFriend.name)); logger.w('检测是否存在nofriend入口:$hasNoFriend'); if (hasNoFriend) { // 已经有了入口 - final ConversationViewModel matchItem = chatList.firstWhere( - (item) => item.conversation.conversationGroupList?.contains(myConversationType.ConversationType.noFriend.name) ?? false, - ); - // 获取陌生人未读总数 - final unreadTotal = await ImService.instance.getUnreadMessageCountByFilter( - filter: V2TimConversationFilter( - conversationGroup: myConversationType.ConversationType.noFriend.name, - hasUnreadCount: true, - ), - ); - matchItem.conversation.lastMessage = csion!.lastMessage; - matchItem.conversation.unreadCount = unreadTotal.data; - chatList.refresh(); - } else { - // 没有则执行创建逻辑 - final res = await ImService.instance.getConversationListByFilter( + // 先检测nofriend分组是否还有数据 + final noFriendData = await ImService.instance.getConversationListByFilter( filter: V2TimConversationFilter(conversationGroup: myConversationType.ConversationType.noFriend.name), nextSeq: 0, count: 1, ); - if (res.success && res.data != null) { - final convList = res.data!.conversationList ?? []; - if (convList.isNotEmpty) { - // logger.i(res.data!.toJson()); - // 有陌生人消息,1.获取未读数,2.组装converstaionviewmodel - final unread = await ImService.instance.getUnreadMessageCountByFilter( - filter: V2TimConversationFilter( - conversationGroup: myConversationType.ConversationType.noFriend.name, - hasUnreadCount: true, - ), - ); - if (unread.success) { - var viewModelList = await ConversationViewModel.createConversationViewModel(convList: [convList.first]); - final conviewModel = viewModelList.first; - conviewModel.faceUrl = 'assets/images/notify/msr.png'; - conviewModel.conversation.showName = '陌生人消息'; - conviewModel.conversation.unreadCount = unread.data; - // final createItem = ConversationViewModel( - // conversation: conv, - // faceUrl: faceUrl, - // ); - final newList = List.from(chatList); - newList.add(conviewModel); - newList.sort((a, b) { - final atime = a.conversation.lastMessage?.timestamp ?? 0; - final btime = b.conversation.lastMessage?.timestamp ?? 0; - return btime.compareTo(atime); // 降序 - }); - chatList.value = newList; - } + if (noFriendData.success && (noFriendData.data?.conversationList?.isNotEmpty == true)) { + // 还存在陌生人会话数据 + final ConversationViewModel matchItem = chatList.firstWhere( + (item) => item.conversation.conversationGroupList?.contains(myConversationType.ConversationType.noFriend.name) ?? false, + orElse: () { + //赋在viewmodel会话上的分组被移除 + logger.w("没有找到符合条件的元素,执行移除逻辑"); + return ConversationViewModel(conversation: V2TimConversation(conversationID: '')); + }, + ); + // 获取陌生人未读总数 + final unreadTotal = await ImService.instance.getUnreadMessageCountByFilter( + filter: V2TimConversationFilter( + conversationGroup: myConversationType.ConversationType.noFriend.name, + hasUnreadCount: true, + ), + ); + // 如果上面没找到 就是移除nofriend分组 + if (matchItem.conversation.conversationID.isEmpty) { + // 执行移除更新,用最新的一条会话数据,构建viewmodel + await ConversationViewModel.createConversationViewModel(convList: noFriendData.data!.conversationList!); + } else { + matchItem.conversation.lastMessage = csion!.lastMessage; + matchItem.conversation.unreadCount = unreadTotal.data; + } + } else { + // 没数据,移除入口 + chatList.removeWhere((chatItem) => chatItem.isCustomAdmin == myConversationType.ConversationType.noFriend.name); + } + + chatList.refresh(); + } else { + logger.w('构建陌生人消息入口--开始'); + await createNoFriendMenu(); + } + } + + // 创建陌生人入口 + Future createNoFriendMenu() async { + // 没有则执行创建逻辑 + final res = await ImService.instance.getConversationListByFilter( + filter: V2TimConversationFilter(conversationGroup: myConversationType.ConversationType.noFriend.name), + nextSeq: 0, + count: 1, + ); + if (res.success && res.data != null) { + final convList = res.data!.conversationList ?? []; + if (convList.isNotEmpty) { + // logger.i(res.data!.toJson()); + // 有陌生人消息,1.获取未读数,2.组装converstaionviewmodel + final unread = await ImService.instance.getUnreadMessageCountByFilter( + filter: V2TimConversationFilter( + conversationGroup: myConversationType.ConversationType.noFriend.name, + hasUnreadCount: true, + ), + ); + + if (unread.success) { + var viewModelList = await ConversationViewModel.createConversationViewModel(convList: [convList.first]); + final conviewModel = viewModelList.first; + conviewModel.faceUrl = 'assets/images/notify/msr.png'; + conviewModel.isCustomAdmin = myConversationType.ConversationTypeStatic.noFriend; + conviewModel.conversation.showName = '陌生人消息'; + conviewModel.conversation.unreadCount = unread.data; + conviewModel.conversation.conversationID = myConversationType.ConversationTypeStatic.noFriend; + // final createItem = ConversationViewModel( + // conversation: conv, + // faceUrl: faceUrl, + // ); + final newList = List.from(chatList); + newList.add(conviewModel); + newList.sort((a, b) { + final atime = a.conversation.lastMessage?.timestamp ?? 0; + final btime = b.conversation.lastMessage?.timestamp ?? 0; + return btime.compareTo(atime); // 降序 + }); + chatList.value = newList; } } + } else { + logger.e('构建失败-----${res.desc}\n${res.data!.toJson()}'); } } diff --git a/lib/IM/controller/chat_detail_controller.dart b/lib/IM/controller/chat_detail_controller.dart index 566fc13..de461e8 100644 --- a/lib/IM/controller/chat_detail_controller.dart +++ b/lib/IM/controller/chat_detail_controller.dart @@ -13,6 +13,7 @@ class ChatDetailController extends GetxController { final RxList chatList = [].obs; final RxBool isFriend = true.obs; + final RxInt followType = 0.obs; void updateChatListWithTimeLabels(List originMessages) async { final idRes = await ImService.instance.selfUserId(); @@ -55,7 +56,7 @@ class ChatDetailController extends GetxController { // 把当前消息先插入,label后插入 displayMessages.add(current); if (needInsertLabel) { - final labelTime = Utils().formatChatTime(currentTimestamp); + final labelTime = Utils.formatChatTime(currentTimestamp); final timeLabel = await IMMessage().insertTimeLabel(labelTime, selfUserId); displayMessages.add(timeLabel.data); } diff --git a/lib/IM/controller/im_user_info_controller.dart b/lib/IM/controller/im_user_info_controller.dart index d56ba08..95cc7fd 100644 --- a/lib/IM/controller/im_user_info_controller.dart +++ b/lib/IM/controller/im_user_info_controller.dart @@ -131,6 +131,4 @@ class ImUserInfoController extends GetxController { if (gender.value < 0) return; await ImService.instance.setSelfInfo(userFullInfo: V2TimUserFullInfo(gender: gender.value)); } - - /// updateAvatar、updateSignature 等方法 } diff --git a/lib/IM/global_badge.dart b/lib/IM/global_badge.dart index ed1510a..9a8ef85 100644 --- a/lib/IM/global_badge.dart +++ b/lib/IM/global_badge.dart @@ -51,12 +51,15 @@ class GlobalBadge extends GetxController { final chatItem = ctl.chatList[i]; logger.w('需要更新的ID:${chatItem.conversation.conversationID}'); if (updatedIds.contains(chatItem.conversation.conversationID)) { - logger.w('找到的chatList的ID:${chatItem.conversation.conversationID}'); + logger.w('chatList中包含:${chatItem.conversation.conversationID}'); + // 从onchange中找到原始数据 final updatedConv = conversationList.firstWhere( (c) => c.conversationID == chatItem.conversation.conversationID, orElse: () => V2TimConversation(conversationID: ''), ); + // 同步更新 + ctl.getNoFriendData(csion: chatItem.conversation); if (updatedConv.conversationID != '' && (updatedConv.conversationGroupList?.contains(ConversationType.noFriend.name) ?? false)) { // 单独处理陌生人会话 @@ -68,30 +71,45 @@ class GlobalBadge extends GetxController { ); chatItem.conversation.lastMessage = updatedConv.lastMessage; chatItem.conversation.unreadCount = unread.data; // 获取陌生人未读总数 - } else { + update(); + } else if (updatedConv.conversationID != '') { // 其他类型统一更新处理 - logger.w('不需要分组的会话,正常更新'); + logger.w('非陌生人消息的会话,正常更新'); chatItem.conversation = updatedConv; update(); } } else { - logger.e('会话列表中不包含的会话:$updatedIds'); + logger.e('本地会话列表中不包含的会话:$updatedIds'); // 情况1:nofriend分组会话,情况2:会话分页未拉取到或者分组变更 + // 检测这条会话数据远端是否存在 for (var cvID in updatedIds) { // 检测这条会话数据是否存在,如果存在说明在后面的分页中,不存在则是被删除不处理 final isReal = await ImService.instance.getConversation(conversationID: cvID); - final V2TimConversation realConv = isReal.data; if (isReal.success) { - if (realConv.conversationID.isNotEmpty) { + final V2TimConversation realConv = isReal.data; + + if (realConv.lastMessage != null) { // 陌生人会话列表单独处理 if (Get.isRegistered()) { + logger.w('在陌生人会话列表'); final notifyCtl = Get.find(); - notifyCtl.updateLastMsg(conversation: realConv); - } else { - // 收集要插入的数据 - if (realConv.conversationGroupList?.isEmpty ?? false) { - willInsert.add(realConv); + // 有分组更新,没分组删除 + if (realConv.conversationGroupList?.contains(ConversationType.noFriend.name) ?? false) { + notifyCtl.updateLastMsg(conversation: realConv); + } else { + notifyCtl.del(conversation: realConv); } } + // 同步更新nofriend会话菜单入口的未读数量 + if (chatItem.isCustomAdmin?.contains(ConversationType.noFriend.name) ?? false) { + await ctl.updateNoFriendMenu(); + } + if (realConv.conversationGroupList?.contains(ConversationType.noFriend.name) ?? false) { + // 这个方法执行的逻辑:已有则刷新菜单入口数据,没有则创建菜单入口 + await ctl.getNoFriendData(csion: realConv); + } else { + // 非陌生人会话,收集要插入的数据 (情况2)这条会话数据 + willInsert.add(realConv); + } } } } @@ -99,7 +117,17 @@ class GlobalBadge extends GetxController { } // 添加收集的数据,别忘了分页获取的去重 if (willInsert.isNotEmpty) { - var viewModelList = await ConversationViewModel.createConversationViewModel(convList: willInsert); + logger.w('收集到要插入的数据为:${willInsert.first.toLogString()}'); + // willInsert 去重 + final deduped = {for (var c in willInsert) c.conversationID.trim(): c}.values.toList(); + // 已有会话ID集合 + final existingIds = ctl.chatList.map((e) => e.conversation.conversationID.trim()).where((id) => id.isNotEmpty).toSet(); + // 过滤掉已存在的会话 + final filtered = deduped.where((c) => !existingIds.contains(c.conversationID.trim())).toList(); + logger.w('最终需要插入的会话数量: ${filtered.length}, ids: ${filtered.map((c) => '"${(c.conversationID).trim()}"').toList()}'); + + final viewModelList = await ConversationViewModel.createConversationViewModel(convList: filtered); + ctl.chatList.insertAll(0, viewModelList); } // 如果没当前会话列表为空 @@ -116,12 +144,23 @@ class GlobalBadge extends GetxController { final btime = b.conversation.lastMessage?.timestamp ?? 0; return btime.compareTo(atime); // 降序 }); + //去重 + final seen = {}; + ctl.chatList.retainWhere((item) { + final id = item.conversation.conversationID.trim(); + if (seen.contains(id)) { + return false; + } else { + seen.add(id); + return true; + } + }); ctl.chatList.refresh(); - }, + }, //changeEnd; ); // final ctl = Get.find(); // ctl.getConversationList(); - _initUnreadCount(); + initUnreadCount(); _addListener(); } @@ -171,7 +210,7 @@ class GlobalBadge extends GetxController { } /// 初始化时获取一次未读总数 - void _initUnreadCount() async { + void initUnreadCount() async { final res = await TencentImSDKPlugin.v2TIMManager.getConversationManager().getTotalUnreadMessageCount(); if (res.code == 0) { totalUnread.value = res.data ?? 0; @@ -187,7 +226,7 @@ class GlobalBadge extends GetxController { /// 处理安卓端异常的问题 handAndroid() { - _initUnreadCount(); + initUnreadCount(); final ctl = Get.find(); ctl.initChatData(); ctl.getConversationList(); @@ -201,7 +240,7 @@ class GlobalBadge extends GetxController { /// 手动更新total Future refreshUnreadCount() async { - _initUnreadCount(); + initUnreadCount(); } /// 移除监听器,防止重复注册 diff --git a/lib/IM/im_core.dart b/lib/IM/im_core.dart index 798a3f2..37adfe8 100644 --- a/lib/IM/im_core.dart +++ b/lib/IM/im_core.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:logger/logger.dart'; +import 'package:loopin/IM/controller/im_user_info_controller.dart'; import 'package:loopin/IM/im_service.dart'; import 'package:loopin/controller/video_module_controller.dart'; import 'package:loopin/utils/common.dart'; @@ -51,6 +52,8 @@ class ImCore { }, onUserSigExpired: () => logger.w("UserSig 过期"), onSelfInfoUpdated: (V2TimUserFullInfo info) { + final ImUserInfoController imUserInfoController = Get.find(); + imUserInfoController.refreshUserInfo(); logger.i("用户信息更新: ${info.toJson()}"); }, ), diff --git a/lib/IM/im_friend_listeners.dart b/lib/IM/im_friend_listeners.dart index 58234f3..68dd06c 100644 --- a/lib/IM/im_friend_listeners.dart +++ b/lib/IM/im_friend_listeners.dart @@ -1,5 +1,4 @@ import 'package:logger/logger.dart'; -import 'package:loopin/utils/notification_banner.dart'; import 'package:tencent_cloud_chat_sdk/enum/V2TimFriendshipListener.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_application.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_info.dart'; @@ -62,7 +61,7 @@ class ImFriendListeners { for (var item in userInfoList) { logger.i('新增粉丝:${item.toJson()}'); } - NotificationBanner.foucs(userInfoList.last); + // NotificationBanner.foucs(userInfoList.last); } else { // 粉丝列表删除用户的通知 for (var item in userInfoList) { diff --git a/lib/IM/im_message_listeners.dart b/lib/IM/im_message_listeners.dart index 2d85f31..921ba6a 100644 --- a/lib/IM/im_message_listeners.dart +++ b/lib/IM/im_message_listeners.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:get/get.dart'; import 'package:logger/logger.dart'; import 'package:loopin/IM/controller/chat_detail_controller.dart'; +import 'package:loopin/IM/global_badge.dart'; import 'package:loopin/IM/im_message.dart'; import 'package:loopin/IM/im_service.dart'; import 'package:loopin/utils/index.dart'; @@ -49,12 +50,12 @@ class ImMessageListenerService extends GetxService { DateTime.now().millisecondsSinceEpoch, )) { // 消息时间间隔超过3分钟插入伪消息 - final showLabel = Utils().formatChatTime(DateTime.now().millisecondsSinceEpoch ~/ 1000); + 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 showLabel = Utils.formatChatTime(DateTime.now().millisecondsSinceEpoch ~/ 1000); final resMsg = await IMMessage().insertTimeLabel(showLabel, selfUserId); messagesToInsert.add(resMsg.data); } @@ -177,11 +178,15 @@ class ImMessageListenerService extends GetxService { } }, onRecvC2CReadReceipt: (List receiptList) { + final gtl = Get.find(); + gtl.initUnreadCount(); for (var receipt in receiptList) { logger.i("C2C已读: msgID=${receipt.msgID}, userID=${receipt.userID}"); } }, onRecvMessageReadReceipts: (List receiptList) { + final gtl = Get.find(); + gtl.initUnreadCount(); for (var receipt in receiptList) { logger.i("群已读: groupID=${receipt.groupID}, msgID=${receipt.msgID}, read=${receipt.readCount}, unread=${receipt.unreadCount}"); } diff --git a/lib/IM/im_service.dart b/lib/IM/im_service.dart index 7ad4940..3fd1da8 100644 --- a/lib/IM/im_service.dart +++ b/lib/IM/im_service.dart @@ -710,4 +710,15 @@ class ImService { ); return ImResult.wrap(res); } + + /// 获取我的关注列表 + /// [nextCursor] 分页游标,首次传空字符串 + Future> getMyFollowingList({ + required String nextCursor, + }) async { + final res = await TIMFriendshipManager.instance.getMyFollowingList( + nextCursor: nextCursor, + ); + return ImResult.wrap(res); + } } diff --git a/lib/models/conversation_type.dart b/lib/models/conversation_type.dart index 4329638..2772766 100644 --- a/lib/models/conversation_type.dart +++ b/lib/models/conversation_type.dart @@ -8,6 +8,16 @@ enum ConversationType { groupNotify, //群通知 } +/// 常量映射 +class ConversationTypeStatic { + static const String noFriend = 'noFriend'; + static const String system = 'system'; + static const String newFoucs = 'newFoucs'; + static const String interaction = 'interaction'; + static const String order = 'order'; + static const String groupNotify = 'groupNotify'; +} + extension ConversationTypeExtension on ConversationType { String get name { switch (this) { diff --git a/lib/models/conversation_view_model.dart b/lib/models/conversation_view_model.dart index 5afac42..65625c6 100644 --- a/lib/models/conversation_view_model.dart +++ b/lib/models/conversation_view_model.dart @@ -19,7 +19,6 @@ class ConversationViewModel { // 提前收集所有需要批量查询的 userID 和 groupID for (var conv in convList) { - logger.e('未过滤前到会话数据:${conv.toLogString()}'); if (conv.userID != null) { userIDList.add(conv.userID!); } else if (conv.groupID != null) { diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index a0df04f..4d60796 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -530,7 +530,7 @@ class _ChatState extends State with SingleTickerProviderStateMixin { final url = obj['url']; final title = obj['title']; final price = obj['price']; - final sell = Utils().graceNumber(int.tryParse(obj['sell'])!); + final sell = Utils.graceNumber(int.tryParse(obj['sell'])!); msgtpl.add(RenderChatItem( data: item, child: GestureDetector( @@ -1039,12 +1039,12 @@ class _ChatState extends State with SingleTickerProviderStateMixin { DateTime.now().millisecondsSinceEpoch, )) { // 消息时间间隔超过3分钟插入伪消息 - final showLabel = Utils().formatChatTime(DateTime.now().millisecondsSinceEpoch ~/ 1000); + 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 showLabel = Utils.formatChatTime(DateTime.now().millisecondsSinceEpoch ~/ 1000); final resMsg = await IMMessage().insertTimeLabel(showLabel, selfUserId); messagesToInsert.add(resMsg.data); @@ -2155,18 +2155,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: NetworkOrAssetImage(imageUrl: data.faceUrl), + ? 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(), @@ -2176,13 +2190,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: [ // 气泡箭头 diff --git a/lib/pages/chat/chat_group.dart b/lib/pages/chat/chat_group.dart index 988dac8..a37637f 100644 --- a/lib/pages/chat/chat_group.dart +++ b/lib/pages/chat/chat_group.dart @@ -792,12 +792,12 @@ class _ChatState extends State with SingleTickerProviderStateMixin { DateTime.now().millisecondsSinceEpoch, )) { // 消息时间间隔超过3分钟插入伪消息 - final showLabel = Utils().formatChatTime(DateTime.now().millisecondsSinceEpoch ~/ 1000); + 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 showLabel = Utils.formatChatTime(DateTime.now().millisecondsSinceEpoch ~/ 1000); final resMsg = await IMMessage().insertTimeLabel(showLabel, selfUserId); messagesToInsert.add(resMsg.data); diff --git a/lib/pages/chat/chat_no_friend.dart b/lib/pages/chat/chat_no_friend.dart index 783f7bc..02c2f4e 100644 --- a/lib/pages/chat/chat_no_friend.dart +++ b/lib/pages/chat/chat_no_friend.dart @@ -3,16 +3,17 @@ library; 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/network_or_asset_image.dart'; import 'package:loopin/components/preview_video.dart'; import 'package:loopin/models/conversation_type.dart'; import 'package:loopin/pages/chat/notify_controller/notify_no_friend_controller.dart'; import 'package:loopin/utils/snapshot.dart'; import 'package:shirne_dialog/shirne_dialog.dart'; -import 'package:tencent_cloud_chat_sdk/enum/friend_type_enum.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'; @@ -41,6 +42,8 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt // 聊天消息模块 final bool isNeedScrollBottom = true; + final String _tips = '回复或关注对方之前,对方只能发送一条消息'; + bool isLoading = false; // 是否在加载中 bool hasMore = true; // 是否还有更多数据 final RxBool _throttleFlag = false.obs; // 滚动节流锁 @@ -152,13 +155,27 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt void isMyFriend() async { final isFriendId = arguments.value.userID; - final isfd = await ImService.instance.isMyFriend(isFriendId!, FriendTypeEnum.V2TIM_FRIEND_TYPE_BOTH); + // 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.isFriend.value = isfd.data; - print(isfd.data); - } else { - controller.isFriend.value = false; - print(isfd.desc); + 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}'); } } @@ -294,9 +311,9 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt 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(); + // }, ), ), ), @@ -883,12 +900,12 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt DateTime.now().millisecondsSinceEpoch, )) { // 消息时间间隔超过3分钟插入伪消息 - final showLabel = Utils().formatChatTime(DateTime.now().millisecondsSinceEpoch ~/ 1000); + 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 showLabel = Utils.formatChatTime(DateTime.now().millisecondsSinceEpoch ~/ 1000); final resMsg = await IMMessage().insertTimeLabel(showLabel, selfUserId); messagesToInsert.add(resMsg.data); @@ -1203,6 +1220,7 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt // 长按消息菜单 void contextMenuDialog() { + return; showDialog( context: context, builder: (context) { @@ -1295,115 +1313,66 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt )), ), actions: [ - IconButton( - icon: const Icon( - Icons.more_horiz, - color: Colors.white, - ), - onPressed: () async { - final paddingTop = MediaQuery.of(Get.context!).padding.top; + // 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: 'remark', - child: Row( - children: [ - Icon(Icons.edit, color: Colors.white, size: 18), - SizedBox(width: 8), - Text( - '设置备注', - style: TextStyle(color: Colors.white), - ), - ], - ), - ), - PopupMenuItem( - value: 'not', - child: Row( - children: [ - Icon(Icons.do_not_disturb_on, color: Colors.white, size: 18), - SizedBox(width: 8), - Text( - '设为免打扰', - style: TextStyle(color: Colors.white), - ), - ], - ), - ), - 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), - ), - ], - ), - ), - PopupMenuItem( - value: 'foucs', - child: Row( - children: [ - Icon(Icons.person_remove_alt_1, color: Colors.white, size: 18), - SizedBox(width: 8), - Text( - '取消关注', - style: TextStyle(color: Colors.white), - ), - ], - ), - ), - ], - ); + // 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 'remark': - print('点击了备注'); - setRemark(); - break; - case 'not': - print('点击了免打扰'); - break; - case 'report': - print('点击了举报'); - break; - case 'block': - print('点击了拉黑'); - break; - case 'foucs': - print('点击了取关'); - break; - } - } - }, - ), + // if (selected != null) { + // switch (selected) { + // case 'report': + // print('点击了举报'); + // break; + // case 'block': + // print('点击了拉黑'); + // break; + // } + // } + // }, + // ), ], ), body: Flex( @@ -1430,6 +1399,39 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt // ), // ), + // 聊天内容 + // 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( @@ -1438,22 +1440,92 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt child: LayoutBuilder( builder: (context, constraints) { return Obx(() { + final showTipWidget = controller.followType.value; + const double tipHeight = 50; // 提示横幅固定高度 + final msgWidgets = renderChatList().reversed.toList(); - return ListView( - controller: chatController, - reverse: true, - padding: const EdgeInsets.all(10.0), + return Stack( children: [ - ConstrainedBox( - constraints: BoxConstraints( - minHeight: constraints.maxHeight - 20, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: msgWidgets, + 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(); + } + } + }, + child: const Text('回关', style: TextStyle(color: Colors.white)), + ), + ) + ], + ), + ), + ), ], ); }); @@ -1481,19 +1553,28 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt children: [ InkWell( child: Icon( - voiceBtnEnable ? Icons.keyboard_outlined : Icons.contactless_outlined, + // voiceBtnEnable ? Icons.keyboard_outlined : Icons.contactless_outlined, + Icons.keyboard_outlined, color: const Color(0xFF3B3B3B), size: 30.0, ), onTap: () { setState(() { toolbarEnable = false; - if (voiceBtnEnable) { - voiceBtnEnable = false; - editorFocusNode.requestFocus(); - } else { - voiceBtnEnable = true; + if (editorFocusNode.hasFocus) { editorFocusNode.unfocus(); + } else { + editorFocusNode.requestFocus(); + } + if (toolbarEnable) { + // voiceBtnEnable = false; + // editorFocusNode.requestFocus(); + // editorFocusNode.requestFocus(); + } else { + // voiceBtnEnable = true; + // toolbarEnable = true; + + // editorFocusNode.unfocus(); } }); }, @@ -1597,29 +1678,31 @@ class _ChatNoFriendState extends State with SingleTickerProviderSt 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); - }, - ), + + // 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, ), @@ -1942,27 +2025,33 @@ 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', - errorBuilder: (context, error, stackTrace) { - return Image.asset( - 'assets/images/pic1.jpg', - height: 60.0, - width: 60.0, - fit: BoxFit.cover, - ); - }, + ? 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), ), ), ) @@ -1973,13 +2062,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: [ // 气泡箭头 @@ -2011,17 +2100,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', - errorBuilder: (context, error, stackTrace) { - return Image.asset( - 'assets/images/pic1.jpg', - height: 60.0, - width: 60.0, - fit: BoxFit.cover, - ); - }, - ), + child: NetworkOrAssetImage(imageUrl: data.faceUrl), ), ) : const SizedBox.shrink(), diff --git a/lib/pages/chat/index.dart b/lib/pages/chat/index.dart index 478316a..bf268ef 100644 --- a/lib/pages/chat/index.dart +++ b/lib/pages/chat/index.dart @@ -232,17 +232,18 @@ class ChatPageState extends State { if (selected != null) { switch (selected) { case 'group': - print('点击了发起群聊'); + logger.w('点击了发起群聊'); + Get.toNamed('/group'); break; case 'friend': - print('点击了添加朋友'); + logger.w('点击了添加朋友'); break; case 'scan': - print('点击了扫一扫'); + logger.w('点击了扫一扫'); ScanUtil.openScanner(onResult: (code) { - print('扫码结果:$code'); + logger.w('扫码结果:$code'); Get.snackbar('扫码成功', code); - // 在这里继续你的业务逻辑,比如跳转页面、请求接口等 + // 执行业务逻辑 }); break; } @@ -279,159 +280,174 @@ class ChatPageState extends State { Text('群聊'), ], ), - Column( - children: [ - SvgPicture.asset( - 'assets/images/svg/kefu.svg', - height: 36.0, - width: 36.0, - ), - Text('互关'), - ], + GestureDetector( + onTap: () { + //互关 + Get.toNamed('/eachFlow'); + }, + child: Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/kefu.svg', + height: 36.0, + width: 36.0, + ), + Text('互关'), + ], + ), ), - Column( - children: [ - SvgPicture.asset( - 'assets/images/svg/comment.svg', - height: 36.0, - width: 36.0, - ), - Text('粉丝'), - ], + GestureDetector( + onTap: () { + // 粉丝 + Get.toNamed('/fans'); + }, + child: Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/comment.svg', + height: 36.0, + width: 36.0, + ), + Text('粉丝'), + ], + ), ), - Column( - children: [ - SvgPicture.asset( - 'assets/images/svg/comment.svg', - height: 36.0, - width: 36.0, - ), - Text('关注'), - ], + GestureDetector( + onTap: () { + // 关注 + Get.toNamed('/flow'); + }, + child: Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/comment.svg', + height: 36.0, + width: 36.0, + ), + Text('关注'), + ], + ), ), ], ), ), Expanded( - child: RefreshIndicator( - backgroundColor: Colors.white, - color: Color(0xFFFF5000), - displacement: 10.0, - onRefresh: handleRefresh, - child: Obx(() { - final chatList = controller.chatList; + child: Obx( + () { + final chatList = controller.chatList; - return ListView.builder( - shrinkWrap: true, - physics: BouncingScrollPhysics(), - itemCount: chatList.length, - itemBuilder: (context, index) { - final isNoFriend = chatList[index].conversation.conversationGroupList?.contains(ConversationType.noFriend.name) ?? false; - final isAdmin = chatList[index].isCustomAdmin != null && chatList[index].isCustomAdmin != '0'; + return ListView.builder( + shrinkWrap: true, + physics: BouncingScrollPhysics(), + itemCount: chatList.length, + itemBuilder: (context, index) { + // logger.w(chatList[index].conversation.conversationGroupList); + // logger.w(chatList[index].isCustomAdmin); + final isNoFriend = chatList[index].conversation.conversationGroupList?.contains(ConversationType.noFriend.name) ?? false; + final isAdmin = + chatList[index].isCustomAdmin != null && (chatList[index].isCustomAdmin?.isNotEmpty ?? false) && chatList[index].isCustomAdmin != '0'; - // logger.e(chatList[index].isCustomAdmin); - return Ink( - // color: chatList[index]['topMost'] == null ? Colors.white : Colors.grey[100], //置顶颜色 - child: InkWell( - splashColor: Colors.grey[200], - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), - child: Row( - spacing: 10.0, - children: [ - // 头图 - ClipOval( - child: NetworkOrAssetImage( - imageUrl: chatList[index].faceUrl, - width: 50, - height: 50, - ), + // logger.e(chatList[index].isCustomAdmin); + return Ink( + // color: chatList[index]['topMost'] == null ? Colors.white : Colors.grey[100], //置顶颜色 + child: InkWell( + splashColor: Colors.grey[200], + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), + child: Row( + spacing: 10.0, + children: [ + // 头图 + ClipOval( + child: NetworkOrAssetImage( + imageUrl: chatList[index].faceUrl, + width: 50, + height: 50, ), + ), - // 消息 - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - chatList[index].conversation.showName ?? '未知', - style: TextStyle( - fontSize: (isAdmin || isNoFriend) ? 20 : 16, - fontWeight: (isAdmin || isNoFriend) ? FontWeight.bold : FontWeight.normal), - ), - const SizedBox(height: 2.0), - Text( - chatList[index].conversation.lastMessage != null - ? parseMessageSummary(chatList[index].conversation.lastMessage!) - : '', - style: const TextStyle(color: Colors.grey, fontSize: 13.0), - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - // 右侧 - - Column( - crossAxisAlignment: CrossAxisAlignment.end, + // 消息 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Visibility( - visible: !(isAdmin || isNoFriend), - child: Text( - // 转成日期字符串显示 - // DateTime.fromMillisecondsSinceEpoch( - // (chatList[index].conversation.lastMessage!.timestamp ?? 0) * 1000, - // ).toLocal().toString().substring(0, 16), // 简单截取年月日时分 - Utils.formatTime( - chatList[index].conversation.lastMessage!.timestamp ?? DateTime.now().millisecondsSinceEpoch ~/ 1000), - style: const TextStyle(color: Colors.grey, fontSize: 12.0), - ), + Text( + chatList[index].conversation.showName ?? '未知', + style: TextStyle( + fontSize: (isAdmin || isNoFriend) ? 20 : 16, + fontWeight: (isAdmin || isNoFriend) ? FontWeight.bold : FontWeight.normal), ), - const SizedBox(height: 5.0), - // 数字角标 - Visibility( - visible: (chatList[index].conversation.unreadCount ?? 0) > 0, - child: FStyle.badge(chatList[index].conversation.unreadCount ?? 0), + const SizedBox(height: 2.0), + Text( + chatList[index].conversation.lastMessage != null ? parseMessageSummary(chatList[index].conversation.lastMessage!) : '', + style: const TextStyle(color: Colors.grey, fontSize: 13.0), + overflow: TextOverflow.ellipsis, ), ], ), - Visibility( - visible: (isAdmin || isNoFriend), - child: const Icon( - Icons.arrow_forward_ios, - color: Colors.blueGrey, - size: 14.0, + ), + // 右侧 + + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Visibility( + visible: !(isAdmin || isNoFriend), + child: Text( + // 转成日期字符串显示 + // DateTime.fromMillisecondsSinceEpoch( + // (chatList[index].conversation.lastMessage!.timestamp ?? 0) * 1000, + // ).toLocal().toString().substring(0, 16), // 简单截取年月日时分 + Utils.formatTime(chatList[index].conversation.lastMessage!.timestamp ?? DateTime.now().millisecondsSinceEpoch ~/ 1000), + style: const TextStyle(color: Colors.grey, fontSize: 12.0), + ), ), + const SizedBox(height: 5.0), + // 数字角标 + Visibility( + visible: (chatList[index].conversation.unreadCount ?? 0) > 0, + child: FStyle.badge(chatList[index].conversation.unreadCount ?? 0), + ), + ], + ), + Visibility( + visible: (isAdmin || isNoFriend), + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.blueGrey, + size: 14.0, ), - ], - ), + ), + ], ), - onTap: () { - if (conversationTypeFromString(chatList[index].isCustomAdmin) != null) { - // 跳转对应的通知消息页 - logger.e(chatList[index].isCustomAdmin); - Get.toNamed('/${chatList[index].isCustomAdmin}', arguments: chatList[index].conversation.lastMessage); - } else if (chatList[index].conversation.conversationGroupList!.contains(ConversationType.noFriend.name)) { - // 跳转陌生人消息页面 - logger.e(chatList[index].conversation.conversationGroupList); - Get.toNamed('/noFriend'); - } else { - // 会话id查询会话详情 - Get.toNamed('/chat', arguments: chatList[index].conversation); - } - }, - onTapDown: (TapDownDetails details) { - posDX = details.globalPosition.dx; - posDY = details.globalPosition.dy; - }, - onLongPress: () { - showContextMenu(context, chatList[index]); - }, ), - ); - }, - ); - })), + onTap: () { + if (conversationTypeFromString(chatList[index].isCustomAdmin) != null) { + // 跳转对应的通知消息页 + logger.e(chatList[index].isCustomAdmin); + Get.toNamed('/${chatList[index].isCustomAdmin}', arguments: chatList[index].conversation.lastMessage); + } else if (chatList[index].conversation.conversationGroupList!.contains(ConversationType.noFriend.name)) { + // 跳转陌生人消息页面 + logger.e(chatList[index].conversation.conversationGroupList); + Get.toNamed('/noFriend'); + } else { + // 会话id查询会话详情 + Get.toNamed('/chat', arguments: chatList[index].conversation); + } + }, + onTapDown: (TapDownDetails details) { + posDX = details.globalPosition.dx; + posDY = details.globalPosition.dy; + }, + onLongPress: () { + showContextMenu(context, chatList[index]); + }, + ), + ); + }, + ); + }, + ), ), ], ), diff --git a/lib/pages/chat/notify/newFoucs.dart b/lib/pages/chat/notify/newFoucs.dart index e69de29..7fa09b7 100644 --- a/lib/pages/chat/notify/newFoucs.dart +++ b/lib/pages/chat/notify/newFoucs.dart @@ -0,0 +1,304 @@ +/// 关注列表 +library; + +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:loopin/IM/controller/chat_controller.dart'; +import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/behavior/custom_scroll_behavior.dart'; +import 'package:loopin/components/network_or_asset_image.dart'; +import 'package:loopin/styles/index.dart'; +import 'package:loopin/utils/index.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_type_check_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; + +class UserWithFollow { + final V2TimUserFullInfo userInfo; + int followType; + + UserWithFollow({ + required this.userInfo, + this.followType = 0, + }); +} + +class Newfoucs extends StatefulWidget { + const Newfoucs({super.key}); + + @override + State createState() => NewfoucsState(); +} + +class NewfoucsState extends State with SingleTickerProviderStateMixin { + bool isLoading = false; // 是否在加载中 + bool hasMore = true; // 是否还有更多数据 + String page = ''; + List dataList = []; + + ///------------------- + + @override + void initState() { + super.initState(); + getData(); + } + + // 分页获取陌关注列表数据 + Future getData() async { + /// 0:不是好友也没有关注 + /// 1:你关注了对方(单向) + /// 2:对方关注了你(单向) + /// 3:互相关注(双向好友) + final res = await ImService.instance.getMyFollowingList( + nextCursor: page, + ); + if (res.success && res.data != null) { + logger.i('获取成功:${res.data!.nextCursor}'); + final userInfoList = res.data!.userFullInfoList ?? []; + // 构建数据 + List wrappedList = userInfoList.map((u) { + return UserWithFollow(userInfo: u); + }).toList(); + // 获取id + final userIDList = userInfoList.map((item) => item.userID).whereType().toList(); + if (userIDList.isNotEmpty) { + final shiRes = await ImService.instance.checkFollowType(userIDList: userIDList); + if (shiRes.success && shiRes.data != null) { + final shipResData = shiRes.data!; + for (final uwf in wrappedList) { + final userID = uwf.userInfo.userID; + if (userID != null) { + // 查找对应关系 + final match = shipResData.firstWhere( + (e) => e.userID == userID, + orElse: () => V2TimFollowTypeCheckResult(userID: ''), + ); + if (match.userID?.isNotEmpty ?? false) { + uwf.followType = match.followType ?? 0; + } + } + } + } + } + final isFinished = res.data!.nextCursor == null || res.data!.nextCursor!.isEmpty; + if (isFinished) { + setState(() { + hasMore = false; + }); + // 加载没数据了 + page = ''; + } else { + page = res.data!.nextCursor ?? ''; + } + logger.i('获取数据成功:$userInfoList'); + setState(() { + dataList.addAll(wrappedList); + }); + } else { + logger.e('获取数据失败:${res.desc}'); + } + } + + // 下拉刷新 + Future handleRefresh() async { + dataList.clear(); + page = ''; + getData(); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + centerTitle: true, + forceMaterialTransparency: true, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container( + color: Colors.grey[300], + height: 1.0, + ), + ), + title: const Text( + '关注', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + actions: [], + ), + body: ScrollConfiguration( + behavior: CustomScrollBehavior().copyWith(scrollbars: false), + child: Column( + children: [ + Expanded( + child: EasyRefresh.builder( + callLoadOverOffset: 20, //触底距离 + callRefreshOverOffset: 20, // 下拉距离 + header: ClassicHeader( + dragText: '下拉刷新', + armedText: '释放刷新', + readyText: '加载中...', + processingText: '加载中...', + processedText: '加载完成', + failedText: '加载失败,请重试', + messageText: '最后更新于 %T', + ), + footer: ClassicFooter( + dragText: '加载更多', + armedText: '释放加载', + readyText: '加载中...', + processingText: '加载中...', + processedText: hasMore ? '加载完成' : '没有更多了~', + failedText: '加载失败,请重试', + messageText: '最后更新于 %T', + ), + onRefresh: () async { + await handleRefresh(); + }, + onLoad: () async { + if (hasMore) { + await getData(); + } + }, + childBuilder: (context, physics) { + return ListView.builder( + physics: physics, + itemCount: dataList.length, + itemBuilder: (context, index) { + final item = dataList[index]; + return Ink( + key: ValueKey(item.userInfo.userID), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 左侧部分(头像 + 昵称 + 描述) + Expanded( + child: InkWell( + onTap: () { + Get.toNamed( + '/vloger', + arguments: item.userInfo.userID, + ); + }, + child: Row( + children: [ + ClipOval( + child: NetworkOrAssetImage( + imageUrl: item.userInfo.faceUrl, + width: 50, + height: 50, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.userInfo.nickName?.isNotEmpty == true ? item.userInfo.nickName! : '未知昵称', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + if (item.userInfo.selfSignature?.isNotEmpty ?? false) ...[ + const SizedBox(height: 2.0), + Text( + item.userInfo.selfSignature!, + style: const TextStyle( + color: Colors.grey, + fontSize: 13.0, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + ], + ), + ), + ), + + SizedBox(width: 10), + + // 右侧按钮 + TextButton( + style: TextButton.styleFrom( + backgroundColor: item.followType == 3 ? Colors.grey : FStyle.primaryColor, + minimumSize: const Size(70, 32), + padding: const EdgeInsets.symmetric(horizontal: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + onPressed: () async { + final ctl = Get.find(); + final checkRes = await ImService.instance.checkFollowType(userIDList: [item.userInfo.userID!]); + int realFollowType = 0; + if (checkRes.success && checkRes.data != null) { + realFollowType = checkRes.data!.first.followType ?? 0; + if ([1, 3].contains(realFollowType)) { + // 取关 + final unRes = await ImService.instance.unfollowUser(userIDList: [item.userInfo.userID!]); + if (unRes.success) { + setState(() { + item.followType = 2; + }); + ctl.mergeNoFriend(conversationID: 'c2c_${item.userInfo.userID!}'); + } + } else { + // 关注 + final res = await ImService.instance.followUser(userIDList: [item.userInfo.userID!]); + if (res.success) { + setState(() { + item.followType = realFollowType == 0 + ? 1 + : realFollowType == 2 + ? 3 + : 0; + }); + final chatRes = await ImService.instance.followUser(userIDList: [item.userInfo.userID!]); + if (chatRes.success) { + final res = await ImService.instance.getConversation(conversationID: 'c2c_${item.userInfo.userID}'); + if (res.success) { + V2TimConversation conversation = res.data; + if (conversation.conversationGroupList?.isNotEmpty ?? false) { + await ImService.instance.deleteConversationsFromGroup( + groupName: conversation.conversationGroupList!.first!, + conversationIDList: [conversation.conversationID], + ); + ctl.updateNoFriendMenu(); + } + } + } + } + } + } + }, + child: Text( + Utils.getTipText(item.followType), + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/chat/notify/noFriend.dart b/lib/pages/chat/notify/noFriend.dart index 0dfca1e..75fe82b 100644 --- a/lib/pages/chat/notify/noFriend.dart +++ b/lib/pages/chat/notify/noFriend.dart @@ -111,7 +111,7 @@ class NofriendState extends State with SingleTickerProviderStateMixin ), title: Text( '陌生人消息', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), actions: [], ), @@ -137,6 +137,8 @@ class NofriendState extends State with SingleTickerProviderStateMixin final isNoFriend = conv.conversationGroupList?.contains(ConversationType.noFriend.name) ?? false; return Ink( + key: ValueKey(conv.conversationID), + // color: conv['topMost'] == null ? Colors.white : Colors.grey[100], //置顶颜色 child: InkWell( splashColor: Colors.grey[200], diff --git a/lib/pages/chat/notify_controller/notify_no_friend_controller.dart b/lib/pages/chat/notify_controller/notify_no_friend_controller.dart index b4f889b..899977d 100644 --- a/lib/pages/chat/notify_controller/notify_no_friend_controller.dart +++ b/lib/pages/chat/notify_controller/notify_no_friend_controller.dart @@ -20,11 +20,19 @@ class NotifyNoFriendController extends GetxController { if (index != -1) { convList[index] = conversation; convList.refresh(); - // convList.value = List.from(convList)..[index] = conversation; } else { - // 根据会话id查询会话检测是否存在 - logger.e('会话不存在,更新会话失败'); - // 如果存在,说明在后面的分页中,先insert进去 convlist做去重处理 + logger.e('当前会话数据未拉取到,执行插入逻辑'); + // 先insert进去 convlist做去重处理 + convList.insert(0, conversation); + convList.refresh(); + } + } + + void del({required V2TimConversation conversation}) { + final index = convList.indexWhere((c) => c.conversationID == conversation.conversationID); + if (index != -1) { + convList.removeAt(index); + convList.refresh(); } } diff --git a/lib/pages/goods/detail.dart b/lib/pages/goods/detail.dart index c258770..73b81d4 100644 --- a/lib/pages/goods/detail.dart +++ b/lib/pages/goods/detail.dart @@ -123,7 +123,7 @@ class _GoodsState extends State { "price": shopObj['price'], "title": shopObj['describe'], "url": shopObj['pic'], - "sell": Utils().graceNumber(int.parse(shopObj['sales'] ?? '0')), + "sell": Utils.graceNumber(int.parse(shopObj['sales'] ?? '0')), }); final res = await IMMessage().createCustomMessage( data: makeJson, @@ -389,7 +389,7 @@ class _GoodsState extends State { ), Text( // '已售${Utils().graceNumber(shopObj['sales'] ?? 0)}', - '已售${Utils().graceNumber(int.tryParse(shopObj['sales']?.toString() ?? '0') ?? 0)}', + '已售${Utils.graceNumber(int.tryParse(shopObj['sales']?.toString() ?? '0') ?? 0)}', style: TextStyle(color: Colors.white, fontSize: 12.0), ), ], diff --git a/lib/pages/groupChat/index.dart b/lib/pages/groupChat/index.dart new file mode 100644 index 0000000..1af2c04 --- /dev/null +++ b/lib/pages/groupChat/index.dart @@ -0,0 +1,219 @@ +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; + +class StartGroupChatPage extends StatefulWidget { + const StartGroupChatPage({super.key}); + + @override + State createState() => _StartGroupChatPageState(); +} + +class _StartGroupChatPageState extends State { + final TextEditingController _searchController = TextEditingController(); + + List> dataList = []; + List> filteredList = []; + Set selectedIds = {}; // 已选中的用户 id + int page = 1; + bool hasMore = true; + + @override + void initState() { + super.initState(); + _loadData(reset: true); + } + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _loadData({bool reset = false}) async { + if (reset) { + page = 1; + 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"}, + ); + + if (newItems.isEmpty) { + hasMore = false; + } else { + dataList.addAll(newItems); + page++; + } + + _applySearch(_searchController.text); + } + + void _applySearch(String query) { + setState(() { + if (query.isEmpty) { + filteredList = List.from(dataList); + } else { + filteredList = dataList.where((item) => item["nickName"].toString().contains(query.trim())).toList(); + } + }); + } + + void _toggleSelection(String id) { + setState(() { + if (selectedIds.contains(id)) { + selectedIds.remove(id); + } else { + selectedIds.add(id); + } + }); + } + + Widget _buildCard({ + required IconData icon, + required String title, + required VoidCallback onTap, + }) { + return Card( + color: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: ListTile( + leading: Icon(icon, color: Colors.black), + title: Text(title, style: const TextStyle(fontSize: 16)), + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + onTap: onTap, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + // backgroundColor: Colors.white, + centerTitle: true, + forceMaterialTransparency: true, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container( + color: Colors.grey[300], + height: 1.0, + ), + ), + title: const Text( + "创建群聊", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + body: Column( + children: [ + // 上半部分 - 两个卡片 + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + _buildCard( + icon: Icons.public, + title: "创建公开群", + onTap: () { + print("跳转到 创建公开群"); + }, + ), + _buildCard( + icon: Icons.group, + title: "创建好友群", + onTap: () { + print("跳转到 创建好友群"); + }, + ), + ], + ), + ), + + const Divider(), + + // 下半部分 - 搜索框 + 列表 + Expanded( + child: Column( + children: [ + // 搜索框 + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: "搜索用户昵称", + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onChanged: _applySearch, + ), + ), + + // 列表 + Expanded( + child: EasyRefresh( + 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); + + return ListTile( + leading: CircleAvatar( + child: Text(item["nickName"].substring(0, 1)), + ), + title: Text(item["nickName"]), + trailing: Checkbox( + value: isSelected, + onChanged: (_) => _toggleSelection(id), + ), + onTap: () => _toggleSelection(id), + ); + }, + ), + ), + ), + ], + ), + ), + ], + ), + + // 底部按钮 + 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), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/index/index.dart b/lib/pages/index/index.dart index 1e33e79..20df39d 100644 --- a/lib/pages/index/index.dart +++ b/lib/pages/index/index.dart @@ -88,7 +88,7 @@ class _IndexPageState extends State with SingleTickerProviderStateMix ]), ), Text( - '已售${Utils().graceNumber(int.parse(item['sales'] ?? '0'))}件', + '已售${Utils.graceNumber(int.parse(item['sales'] ?? '0'))}件', style: TextStyle(color: Colors.grey, fontSize: 10.0), ), ], @@ -254,8 +254,9 @@ class _IndexPageState extends State with SingleTickerProviderStateMix header: ClassicHeader( dragText: '下拉刷新', armedText: '释放刷新', - readyText: '刷新中...', - processingText: '刷新完成', + readyText: '加载中...', + processingText: '加载中...', + processedText: '加载完成', messageText: '最后更新于 %T', ), child: CustomScrollView( diff --git a/lib/pages/my/fans.dart b/lib/pages/my/fans.dart new file mode 100644 index 0000000..a3b6acf --- /dev/null +++ b/lib/pages/my/fans.dart @@ -0,0 +1,304 @@ +/// 粉丝列表 +library; + +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:loopin/IM/controller/chat_controller.dart'; +import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/behavior/custom_scroll_behavior.dart'; +import 'package:loopin/components/network_or_asset_image.dart'; +import 'package:loopin/styles/index.dart'; +import 'package:loopin/utils/index.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_type_check_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; + +class UserWithFollow { + final V2TimUserFullInfo userInfo; + int followType; + + UserWithFollow({ + required this.userInfo, + this.followType = 0, + }); +} + +class Fans extends StatefulWidget { + const Fans({super.key}); + + @override + State createState() => FansState(); +} + +class FansState extends State with SingleTickerProviderStateMixin { + bool isLoading = false; // 是否在加载中 + bool hasMore = true; // 是否还有更多数据 + String page = ''; + List dataList = []; + + ///------------------- + + @override + void initState() { + super.initState(); + getData(); + } + + // 分页获取陌粉丝数据 + Future getData() async { + /// 0:不是好友也没有关注 + /// 1:你关注了对方(单向) + /// 2:对方关注了你(单向) + /// 3:互相关注(双向好友) + final res = await ImService.instance.getMyFollowersList( + nextCursor: page, + ); + if (res.success && res.data != null) { + logger.i('获取成功:${res.data!.nextCursor}'); + final userInfoList = res.data!.userFullInfoList ?? []; + // 构建数据 + List wrappedList = userInfoList.map((u) { + return UserWithFollow(userInfo: u); + }).toList(); + // 获取id + final userIDList = userInfoList.map((item) => item.userID).whereType().toList(); + if (userIDList.isNotEmpty) { + final shiRes = await ImService.instance.checkFollowType(userIDList: userIDList); + if (shiRes.success && shiRes.data != null) { + final shipResData = shiRes.data!; + for (final uwf in wrappedList) { + final userID = uwf.userInfo.userID; + if (userID != null) { + // 查找对应关系 + final match = shipResData.firstWhere( + (e) => e.userID == userID, + orElse: () => V2TimFollowTypeCheckResult(userID: ''), + ); + if (match.userID?.isNotEmpty ?? false) { + uwf.followType = match.followType ?? 0; + } + } + } + } + } + final isFinished = res.data!.nextCursor == null || res.data!.nextCursor!.isEmpty; + if (isFinished) { + setState(() { + hasMore = false; + }); + // 加载没数据了 + page = ''; + } else { + page = res.data!.nextCursor ?? ''; + } + logger.i('获取数据成功:$userInfoList'); + setState(() { + dataList.addAll(wrappedList); + }); + } else { + logger.e('获取数据失败:${res.desc}'); + } + } + + // 下拉刷新 + Future handleRefresh() async { + dataList.clear(); + page = ''; + getData(); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + centerTitle: true, + forceMaterialTransparency: true, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container( + color: Colors.grey[300], + height: 1.0, + ), + ), + title: const Text( + '粉丝', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + actions: [], + ), + body: ScrollConfiguration( + behavior: CustomScrollBehavior().copyWith(scrollbars: false), + child: Column( + children: [ + Expanded( + child: EasyRefresh.builder( + callLoadOverOffset: 20, //触底距离 + callRefreshOverOffset: 20, // 下拉距离 + header: ClassicHeader( + dragText: '下拉刷新', + armedText: '释放刷新', + readyText: '加载中...', + processingText: '加载中...', + processedText: '加载完成', + failedText: '加载失败,请重试', + messageText: '最后更新于 %T', + ), + footer: ClassicFooter( + dragText: '加载更多', + armedText: '释放加载', + readyText: '加载中...', + processingText: '加载中...', + processedText: hasMore ? '加载完成' : '没有更多了~', + failedText: '加载失败,请重试', + messageText: '最后更新于 %T', + ), + onRefresh: () async { + await handleRefresh(); + }, + onLoad: () async { + if (hasMore) { + await getData(); + } + }, + childBuilder: (context, physics) { + return ListView.builder( + physics: physics, + itemCount: dataList.length, + itemBuilder: (context, index) { + final item = dataList[index]; + return Ink( + key: ValueKey(item.userInfo.userID), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 左侧部分(头像 + 昵称 + 描述) + Expanded( + child: InkWell( + onTap: () { + Get.toNamed( + '/vloger', + arguments: item.userInfo.userID, + ); + }, + child: Row( + children: [ + ClipOval( + child: NetworkOrAssetImage( + imageUrl: item.userInfo.faceUrl, + width: 50, + height: 50, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.userInfo.nickName?.isNotEmpty == true ? item.userInfo.nickName! : '未知昵称', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + if (item.userInfo.selfSignature?.isNotEmpty ?? false) ...[ + const SizedBox(height: 2.0), + Text( + item.userInfo.selfSignature!, + style: const TextStyle( + color: Colors.grey, + fontSize: 13.0, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + ], + ), + ), + ), + + SizedBox(width: 10), + + // 右侧按钮 + TextButton( + style: TextButton.styleFrom( + backgroundColor: item.followType != 3 ? FStyle.primaryColor : Colors.grey, + minimumSize: const Size(70, 32), + padding: const EdgeInsets.symmetric(horizontal: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + onPressed: () async { + final ctl = Get.find(); + final checkRes = await ImService.instance.checkFollowType(userIDList: [item.userInfo.userID!]); + int realFollowType = 0; + if (checkRes.success && checkRes.data != null) { + realFollowType = checkRes.data!.first.followType ?? 0; + if ([1, 3].contains(realFollowType)) { + // 取关 + final unRes = await ImService.instance.unfollowUser(userIDList: [item.userInfo.userID!]); + if (unRes.success) { + setState(() { + item.followType = 2; + }); + ctl.mergeNoFriend(conversationID: 'c2c_${item.userInfo.userID!}'); + } + } else { + // 关注 + final res = await ImService.instance.followUser(userIDList: [item.userInfo.userID!]); + if (res.success) { + setState(() { + item.followType = realFollowType == 0 + ? 1 + : realFollowType == 2 + ? 3 + : 0; + }); + final chatRes = await ImService.instance.followUser(userIDList: [item.userInfo.userID!]); + if (chatRes.success) { + final res = await ImService.instance.getConversation(conversationID: 'c2c_${item.userInfo.userID}'); + if (res.success) { + V2TimConversation conversation = res.data; + if (conversation.conversationGroupList?.isNotEmpty ?? false) { + await ImService.instance.deleteConversationsFromGroup( + groupName: conversation.conversationGroupList!.first!, + conversationIDList: [conversation.conversationID], + ); + ctl.updateNoFriendMenu(); + } + } + } + } + } + } + }, + child: Text( + Utils.getTipText(item.followType), + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/my/flowing.dart b/lib/pages/my/flowing.dart new file mode 100644 index 0000000..59ced48 --- /dev/null +++ b/lib/pages/my/flowing.dart @@ -0,0 +1,304 @@ +/// 关注列表 +library; + +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:loopin/IM/controller/chat_controller.dart'; +import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/behavior/custom_scroll_behavior.dart'; +import 'package:loopin/components/network_or_asset_image.dart'; +import 'package:loopin/styles/index.dart'; +import 'package:loopin/utils/index.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_type_check_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; + +class UserWithFollow { + final V2TimUserFullInfo userInfo; + int followType; + + UserWithFollow({ + required this.userInfo, + this.followType = 0, + }); +} + +class Flowing extends StatefulWidget { + const Flowing({super.key}); + + @override + State createState() => FlowingState(); +} + +class FlowingState extends State with SingleTickerProviderStateMixin { + bool isLoading = false; // 是否在加载中 + bool hasMore = true; // 是否还有更多数据 + String page = ''; + List dataList = []; + + ///------------------- + + @override + void initState() { + super.initState(); + getData(); + } + + // 分页获取陌关注列表数据 + Future getData() async { + /// 0:不是好友也没有关注 + /// 1:你关注了对方(单向) + /// 2:对方关注了你(单向) + /// 3:互相关注(双向好友) + final res = await ImService.instance.getMyFollowingList( + nextCursor: page, + ); + if (res.success && res.data != null) { + logger.i('获取成功:${res.data!.nextCursor}'); + final userInfoList = res.data!.userFullInfoList ?? []; + // 构建数据 + List wrappedList = userInfoList.map((u) { + return UserWithFollow(userInfo: u); + }).toList(); + // 获取id + final userIDList = userInfoList.map((item) => item.userID).whereType().toList(); + if (userIDList.isNotEmpty) { + final shiRes = await ImService.instance.checkFollowType(userIDList: userIDList); + if (shiRes.success && shiRes.data != null) { + final shipResData = shiRes.data!; + for (final uwf in wrappedList) { + final userID = uwf.userInfo.userID; + if (userID != null) { + // 查找对应关系 + final match = shipResData.firstWhere( + (e) => e.userID == userID, + orElse: () => V2TimFollowTypeCheckResult(userID: ''), + ); + if (match.userID?.isNotEmpty ?? false) { + uwf.followType = match.followType ?? 0; + } + } + } + } + } + final isFinished = res.data!.nextCursor == null || res.data!.nextCursor!.isEmpty; + if (isFinished) { + setState(() { + hasMore = false; + }); + // 加载没数据了 + page = ''; + } else { + page = res.data!.nextCursor ?? ''; + } + logger.i('获取数据成功:$userInfoList'); + setState(() { + dataList.addAll(wrappedList); + }); + } else { + logger.e('获取数据失败:${res.desc}'); + } + } + + // 下拉刷新 + Future handleRefresh() async { + dataList.clear(); + page = ''; + getData(); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + centerTitle: true, + forceMaterialTransparency: true, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container( + color: Colors.grey[300], + height: 1.0, + ), + ), + title: const Text( + '关注', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + actions: [], + ), + body: ScrollConfiguration( + behavior: CustomScrollBehavior().copyWith(scrollbars: false), + child: Column( + children: [ + Expanded( + child: EasyRefresh.builder( + callLoadOverOffset: 20, //触底距离 + callRefreshOverOffset: 20, // 下拉距离 + header: ClassicHeader( + dragText: '下拉刷新', + armedText: '释放刷新', + readyText: '加载中...', + processingText: '加载中...', + processedText: '加载完成', + failedText: '加载失败,请重试', + messageText: '最后更新于 %T', + ), + footer: ClassicFooter( + dragText: '加载更多', + armedText: '释放加载', + readyText: '加载中...', + processingText: '加载中...', + processedText: hasMore ? '加载完成' : '没有更多了~', + failedText: '加载失败,请重试', + messageText: '最后更新于 %T', + ), + onRefresh: () async { + await handleRefresh(); + }, + onLoad: () async { + if (hasMore) { + await getData(); + } + }, + childBuilder: (context, physics) { + return ListView.builder( + physics: physics, + itemCount: dataList.length, + itemBuilder: (context, index) { + final item = dataList[index]; + return Ink( + key: ValueKey(item.userInfo.userID), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 左侧部分(头像 + 昵称 + 描述) + Expanded( + child: InkWell( + onTap: () { + Get.toNamed( + '/vloger', + arguments: item.userInfo.userID, + ); + }, + child: Row( + children: [ + ClipOval( + child: NetworkOrAssetImage( + imageUrl: item.userInfo.faceUrl, + width: 50, + height: 50, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.userInfo.nickName?.isNotEmpty == true ? item.userInfo.nickName! : '未知昵称', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + if (item.userInfo.selfSignature?.isNotEmpty ?? false) ...[ + const SizedBox(height: 2.0), + Text( + item.userInfo.selfSignature!, + style: const TextStyle( + color: Colors.grey, + fontSize: 13.0, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + ], + ), + ), + ), + + SizedBox(width: 10), + + // 右侧按钮 + TextButton( + style: TextButton.styleFrom( + backgroundColor: item.followType == 3 ? Colors.grey : FStyle.primaryColor, + minimumSize: const Size(70, 32), + padding: const EdgeInsets.symmetric(horizontal: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + onPressed: () async { + final ctl = Get.find(); + final checkRes = await ImService.instance.checkFollowType(userIDList: [item.userInfo.userID!]); + int realFollowType = 0; + if (checkRes.success && checkRes.data != null) { + realFollowType = checkRes.data!.first.followType ?? 0; + if ([1, 3].contains(realFollowType)) { + // 取关 + final unRes = await ImService.instance.unfollowUser(userIDList: [item.userInfo.userID!]); + if (unRes.success) { + setState(() { + item.followType = 2; + }); + ctl.mergeNoFriend(conversationID: 'c2c_${item.userInfo.userID!}'); + } + } else { + // 关注 + final res = await ImService.instance.followUser(userIDList: [item.userInfo.userID!]); + if (res.success) { + setState(() { + item.followType = realFollowType == 0 + ? 1 + : realFollowType == 2 + ? 3 + : 0; + }); + final chatRes = await ImService.instance.followUser(userIDList: [item.userInfo.userID!]); + if (chatRes.success) { + final res = await ImService.instance.getConversation(conversationID: 'c2c_${item.userInfo.userID}'); + if (res.success) { + V2TimConversation conversation = res.data; + if (conversation.conversationGroupList?.isNotEmpty ?? false) { + await ImService.instance.deleteConversationsFromGroup( + groupName: conversation.conversationGroupList!.first!, + conversationIDList: [conversation.conversationID], + ); + ctl.updateNoFriendMenu(); + } + } + } + } + } + } + }, + child: Text( + Utils.getTipText(item.followType), + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/my/index.dart b/lib/pages/my/index.dart index a378963..bed0897 100644 --- a/lib/pages/my/index.dart +++ b/lib/pages/my/index.dart @@ -369,7 +369,10 @@ class MyPageState extends State with SingleTickerProviderStateMixin { padding: const EdgeInsets.all(10.0), child: Column( children: [ + const SizedBox(height: 10), Obx(() => _buildStatsCard()), + const SizedBox(height: 10), + Obx(() => _buildInfoDesc(context)), const SizedBox(height: 10.0), _buildOrderCard(context), const SizedBox(height: 10.0), @@ -463,6 +466,37 @@ class MyPageState extends State with SingleTickerProviderStateMixin { ); } + Widget _buildInfoDesc(BuildContext context) { + final tx = imUserInfoController?.signature; + if (tx == null || tx.isEmpty) { + return const SizedBox.shrink(); + } + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(15.0), + boxShadow: [BoxShadow(color: Colors.black.withAlpha(10), offset: const Offset(0.0, 1.0), blurRadius: 2.0, spreadRadius: 0.0)], + ), + clipBehavior: Clip.antiAlias, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(12), + width: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '${imUserInfoController!.signature}', + style: const TextStyle(fontSize: 16), + ), + ), + ], + )); + } + Widget _buildGridTab(int tabIndex) { final listToShow = tabIndex == 0 ? items : favoriteItems; PageParams params = tabIndex == 0 ? itemsParams : favoriteParams; @@ -724,104 +758,6 @@ class MyPageState extends State with SingleTickerProviderStateMixin { ); } - // Widget _buildFlexibleSpace() { - // return LayoutBuilder( - // builder: (BuildContext context, BoxConstraints constraints) { - // final double maxHeight = 180.0; - // final double minHeight = 120.0; - // final double currentHeight = constraints.maxHeight; - // double ratio = (currentHeight - minHeight) / (maxHeight - minHeight); - // ratio = ratio.clamp(0.0, 1.0); - // final faceUrl = imUserInfoController?.faceUrl.value ?? ''; - - // return Obx( - // () => Stack( - // fit: StackFit.expand, - // children: [ - // Positioned.fill( - // child: Opacity( - // opacity: 1.0, - // child: NetworkOrAssetImage(imageUrl: imUserInfoController?.customInfo['coverBg'] ?? '', placeholderAsset: 'assets/images/bk.jpg'), - // ), - // ), - // Positioned( - // left: 15.0, - // bottom: 0, - // right: 15.0, - // child: Container( - // padding: const EdgeInsets.symmetric(vertical: 10.0), - // child: Row( - // crossAxisAlignment: CrossAxisAlignment.end, - // children: [ - // ClipOval( - // child: ClipRRect( - // borderRadius: BorderRadius.circular(30), - // child: NetworkOrAssetImage( - // imageUrl: faceUrl, - // width: 80, - // height: 80, - // ), - // ), - // ), - // const SizedBox(width: 15.0), - // Expanded( - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Row( - // children: [ - // Container( - // padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), - // decoration: BoxDecoration(color: Colors.black.withAlpha((0.3 * 255).round()), borderRadius: BorderRadius.circular(20.0)), - // child: Obx( - // () => Text( - // imUserInfoController?.nickname.value.isNotEmpty ?? false == true - // ? imUserInfoController?.nickname.value ?? imUserInfoController!.nickname.value - // : '昵称', - // style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold, fontFamily: 'Arial', color: Colors.white), - // ), - // )), - // const SizedBox(width: 8.0), - // InkWell( - // onTap: () { - // qrcodeAlertDialog(context); - // }, - // child: Container( - // padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0), - // decoration: BoxDecoration(color: Colors.black.withAlpha((0.3 * 255).round()), borderRadius: BorderRadius.circular(20.0)), - // child: const Icon(Icons.qr_code_outlined, size: 18.0, color: Colors.white), - // ), - // ), - // ], - // ), - // const SizedBox(height: 8.0), - // Container( - // padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), - // decoration: BoxDecoration(color: Colors.black.withAlpha((0.3 * 255).round()), borderRadius: BorderRadius.circular(20.0)), - // child: InkWell( - // onTap: () { - // logger.i('点击id'); - // Clipboard.setData(ClipboardData(text: imUserInfoController?.userID.value ?? '')); - // MyDialog.toast('ID已复制', - // icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.green.withAlpha(200))); - // }, - // child: Text('ID:${imUserInfoController?.userID.value ?? ''}', style: TextStyle(fontSize: 12.0, color: Colors.white)), - // ), - // ), - // ], - // ), - // ), - // ], - // ), - // ), - // ), - // ], - // ), - // ); - // }, - // ); - // } - Widget _buildStatsCard() { return Container( decoration: BoxDecoration( @@ -836,21 +772,46 @@ class MyPageState extends State with SingleTickerProviderStateMixin { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Column(children: [Text('9999', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), SizedBox(height: 3.0), Text('获赞')]), - Column(children: [ - Text('${followInfo.value?.mutualFollowersCount ?? 0}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), - SizedBox(height: 3.0), - Text('互关') - ]), - Column(children: [ - Text('${followInfo.value?.followingCount ?? 0}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), - SizedBox(height: 3.0), - Text('关注') - ]), - Column(children: [ - Text('${followInfo.value?.followersCount ?? 0}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), - SizedBox(height: 3.0), - Text('粉丝') - ]), + GestureDetector( + onTap: () async { + // 互相关注 + await Get.toNamed('/eachFlow'); + refreshData(); + }, + child: Column(children: [ + Text('${followInfo.value?.mutualFollowersCount ?? 0}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), + SizedBox(height: 3.0), + Text('互关') + ]), + ), + GestureDetector( + onTap: () async { + // 关注 + await Get.toNamed('/flow'); + refreshData(); + }, + child: Column(children: [ + Text('${followInfo.value?.followingCount ?? 0}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), + SizedBox(height: 3.0), + Text('关注') + ]), + ), + GestureDetector( + onTap: () async { + await Get.toNamed('/fans'); + refreshData(); + }, + child: Column( + children: [ + Text( + '${followInfo.value?.followersCount ?? 0}', + style: const TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 3.0), + const Text('粉丝'), + ], + ), + ) ], )), ); diff --git a/lib/pages/my/mutual_followers.dart b/lib/pages/my/mutual_followers.dart new file mode 100644 index 0000000..b3ede42 --- /dev/null +++ b/lib/pages/my/mutual_followers.dart @@ -0,0 +1,304 @@ +/// 互关列表 +library; + +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:loopin/IM/controller/chat_controller.dart'; +import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/behavior/custom_scroll_behavior.dart'; +import 'package:loopin/components/network_or_asset_image.dart'; +import 'package:loopin/styles/index.dart'; +import 'package:loopin/utils/index.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_type_check_result.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; + +class UserWithFollow { + final V2TimUserFullInfo userInfo; + int followType; + + UserWithFollow({ + required this.userInfo, + this.followType = 0, + }); +} + +class MutualFollowers extends StatefulWidget { + const MutualFollowers({super.key}); + + @override + State createState() => MutualFollowersState(); +} + +class MutualFollowersState extends State with SingleTickerProviderStateMixin { + bool isLoading = false; // 是否在加载中 + bool hasMore = true; // 是否还有更多数据 + String page = ''; + List dataList = []; + + ///------------------- + + @override + void initState() { + super.initState(); + getData(); + } + + // 分页获取陌关注列表数据 + Future getData() async { + /// 0:不是好友也没有关注 + /// 1:你关注了对方(单向) + /// 2:对方关注了你(单向) + /// 3:互相关注(双向好友) + final res = await ImService.instance.getMutualFollowersList( + nextCursor: page, + ); + if (res.success && res.data != null) { + logger.i('获取成功:${res.data!.nextCursor}'); + final userInfoList = res.data!.userFullInfoList ?? []; + // 构建数据 + List wrappedList = userInfoList.map((u) { + return UserWithFollow(userInfo: u); + }).toList(); + // 获取id + final userIDList = userInfoList.map((item) => item.userID).whereType().toList(); + if (userIDList.isNotEmpty) { + final shiRes = await ImService.instance.checkFollowType(userIDList: userIDList); + if (shiRes.success && shiRes.data != null) { + final shipResData = shiRes.data!; + for (final uwf in wrappedList) { + final userID = uwf.userInfo.userID; + if (userID != null) { + // 查找对应关系 + final match = shipResData.firstWhere( + (e) => e.userID == userID, + orElse: () => V2TimFollowTypeCheckResult(userID: ''), + ); + if (match.userID?.isNotEmpty ?? false) { + uwf.followType = match.followType ?? 0; + } + } + } + } + } + final isFinished = res.data!.nextCursor == null || res.data!.nextCursor!.isEmpty; + if (isFinished) { + setState(() { + hasMore = false; + }); + // 加载没数据了 + page = ''; + } else { + page = res.data!.nextCursor ?? ''; + } + logger.i('获取数据成功:$userInfoList'); + setState(() { + dataList.addAll(wrappedList); + }); + } else { + logger.e('获取数据失败:${res.desc}'); + } + } + + // 下拉刷新 + Future handleRefresh() async { + dataList.clear(); + page = ''; + getData(); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + centerTitle: true, + forceMaterialTransparency: true, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(1.0), + child: Container( + color: Colors.grey[300], + height: 1.0, + ), + ), + title: const Text( + '互关', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + actions: [], + ), + body: ScrollConfiguration( + behavior: CustomScrollBehavior().copyWith(scrollbars: false), + child: Column( + children: [ + Expanded( + child: EasyRefresh.builder( + callLoadOverOffset: 20, //触底距离 + callRefreshOverOffset: 20, // 下拉距离 + header: ClassicHeader( + dragText: '下拉刷新', + armedText: '释放刷新', + readyText: '加载中...', + processingText: '加载中...', + processedText: '加载完成', + failedText: '加载失败,请重试', + messageText: '最后更新于 %T', + ), + footer: ClassicFooter( + dragText: '加载更多', + armedText: '释放加载', + readyText: '加载中...', + processingText: '加载中...', + processedText: hasMore ? '加载完成' : '没有更多了~', + failedText: '加载失败,请重试', + messageText: '最后更新于 %T', + ), + onRefresh: () async { + await handleRefresh(); + }, + onLoad: () async { + if (hasMore) { + await getData(); + } + }, + childBuilder: (context, physics) { + return ListView.builder( + physics: physics, + itemCount: dataList.length, + itemBuilder: (context, index) { + final item = dataList[index]; + return Ink( + key: ValueKey(item.userInfo.userID), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 左侧部分(头像 + 昵称 + 描述) + Expanded( + child: InkWell( + onTap: () { + Get.toNamed( + '/vloger', + arguments: item.userInfo.userID, + ); + }, + child: Row( + children: [ + ClipOval( + child: NetworkOrAssetImage( + imageUrl: item.userInfo.faceUrl, + width: 50, + height: 50, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.userInfo.nickName?.isNotEmpty == true ? item.userInfo.nickName! : '未知昵称', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + if (item.userInfo.selfSignature?.isNotEmpty ?? false) ...[ + const SizedBox(height: 2.0), + Text( + item.userInfo.selfSignature!, + style: const TextStyle( + color: Colors.grey, + fontSize: 13.0, + ), + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ], + ], + ), + ), + ], + ), + ), + ), + + SizedBox(width: 10), + + // 右侧按钮 + TextButton( + style: TextButton.styleFrom( + backgroundColor: item.followType == 3 ? Colors.grey : FStyle.primaryColor, + minimumSize: const Size(70, 32), + padding: const EdgeInsets.symmetric(horizontal: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + ), + onPressed: () async { + final ctl = Get.find(); + final checkRes = await ImService.instance.checkFollowType(userIDList: [item.userInfo.userID!]); + int realFollowType = 0; + if (checkRes.success && checkRes.data != null) { + realFollowType = checkRes.data!.first.followType ?? 0; + if ([1, 3].contains(realFollowType)) { + // 取关 + final unRes = await ImService.instance.unfollowUser(userIDList: [item.userInfo.userID!]); + if (unRes.success) { + setState(() { + item.followType = 2; + }); + ctl.mergeNoFriend(conversationID: 'c2c_${item.userInfo.userID!}'); + } + } else { + // 关注 + final res = await ImService.instance.followUser(userIDList: [item.userInfo.userID!]); + if (res.success) { + setState(() { + item.followType = realFollowType == 0 + ? 1 + : realFollowType == 2 + ? 3 + : 0; + }); + final chatRes = await ImService.instance.followUser(userIDList: [item.userInfo.userID!]); + if (chatRes.success) { + final res = await ImService.instance.getConversation(conversationID: 'c2c_${item.userInfo.userID}'); + if (res.success) { + V2TimConversation conversation = res.data; + if (conversation.conversationGroupList?.isNotEmpty ?? false) { + await ImService.instance.deleteConversationsFromGroup( + groupName: conversation.conversationGroupList!.first!, + conversationIDList: [conversation.conversationID], + ); + ctl.updateNoFriendMenu(); + } + } + } + } + } + } + }, + child: Text( + Utils.getTipText(item.followType), + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/my/user_info.dart b/lib/pages/my/user_info.dart index 20e2de5..dc65f38 100644 --- a/lib/pages/my/user_info.dart +++ b/lib/pages/my/user_info.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:loopin/IM/controller/im_user_info_controller.dart'; import 'package:loopin/api/common_api.dart'; +import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/service/http.dart'; import 'package:loopin/styles/index.dart'; import 'package:loopin/utils/index.dart'; @@ -321,9 +322,13 @@ class _UserInfoState extends State { final imageUrl = userInfoController.customInfo['coverBg']; return GestureDetector( onTap: () => pickCover(context), - child: Image( - image: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : const AssetImage('assets/images/pic2.jpg') as ImageProvider, - fit: BoxFit.cover, + // child: Image( + // image: (imageUrl != null && imageUrl.isNotEmpty) ? NetworkImage(imageUrl) : const AssetImage('assets/images/pic2.jpg') as ImageProvider, + // fit: BoxFit.cover, + // ), + child: NetworkOrAssetImage( + imageUrl: imageUrl, + placeholderAsset: 'assets/images/bk.jpg', ), ); }), @@ -423,7 +428,7 @@ class _UserInfoState extends State { maxLines: 1, ); } else { - String wxText = val['unionId'] == null || val['unionId'] == '' ? '未授权' : '已授权'; + String wxText = val['openId'] == null || val['openId'] == '' ? '未授权' : '已授权'; return Row( children: [ Spacer(), @@ -473,9 +478,13 @@ class _UserInfoState extends State { child: CircleAvatar( radius: 60, backgroundColor: Colors.white, - child: CircleAvatar( - radius: 57, - backgroundImage: avatar.isNotEmpty ? NetworkImage(avatar) : const AssetImage('assets/images/avatar/img11.jpg') as ImageProvider, + child: ClipOval( + child: NetworkOrAssetImage( + imageUrl: avatar, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + ), ), ), ), diff --git a/lib/pages/my/vloger.dart b/lib/pages/my/vloger.dart index 6251b34..21ed756 100644 --- a/lib/pages/my/vloger.dart +++ b/lib/pages/my/vloger.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:get/get_rx/src/rx_typedefs/rx_typedefs.dart'; +import 'package:loopin/IM/controller/chat_controller.dart'; import 'package:loopin/IM/im_service.dart'; import 'package:loopin/components/custom_sticky_header.dart'; import 'package:loopin/components/network_or_asset_image.dart'; @@ -202,105 +203,105 @@ class MyPageState extends State with SingleTickerProviderStateMixin { print('User navigated back'); } }, - child:Scaffold( - backgroundColor: const Color(0xFFFAF6F9), - body: Obx(() { - return NestedScrollViewPlus( - controller: scrollController, - physics: shouldFixHeader.value ? const OnlyDownScrollPhysics(parent: AlwaysScrollableScrollPhysics()) : const AlwaysScrollableScrollPhysics(), - overscrollBehavior: OverscrollBehavior.outer, - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - SliverAppBar( - backgroundColor: Colors.transparent, - surfaceTintColor: Colors.transparent, - expandedHeight: 180.0, - collapsedHeight: 120.0, - pinned: true, - stretch: true, + child: Scaffold( + backgroundColor: const Color(0xFFFAF6F9), + body: Obx(() { + return NestedScrollViewPlus( + controller: scrollController, + physics: shouldFixHeader.value ? const OnlyDownScrollPhysics(parent: AlwaysScrollableScrollPhysics()) : const AlwaysScrollableScrollPhysics(), + overscrollBehavior: OverscrollBehavior.outer, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverAppBar( + backgroundColor: Colors.transparent, + surfaceTintColor: Colors.transparent, + expandedHeight: 180.0, + collapsedHeight: 120.0, + pinned: true, + stretch: true, leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - Get.back(result: { + icon: const Icon(Icons.arrow_back), + onPressed: () { + Get.back(result: { 'returnTo': '/', 'vlogerId': args['memberId'], - 'followStatus': (followed.value == 1 || followed.value == 3)?true:false, + 'followStatus': (followed.value == 1 || followed.value == 3) ? true : false, }); + }, + ), + onStretchTrigger: () async { + logger.i('触发 stretch 拉伸'); + // 加载刷新逻辑 }, + flexibleSpace: Obx(() { + userInfo.value; + return _buildFlexibleSpace(); + }), ), - onStretchTrigger: () async { - logger.i('触发 stretch 拉伸'); - // 加载刷新逻辑 - }, - flexibleSpace: Obx(() { - userInfo.value; - return _buildFlexibleSpace(); - }), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Column( - children: [ - Obx(() => _buildStatsCard()), - const SizedBox(height: 10.0), - Obx(() => _buildInfoDesc(context)), - const SizedBox(height: 10.0), - Obx(() => _buildFoucsButton(context)), - ], + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + children: [ + Obx(() => _buildStatsCard()), + const SizedBox(height: 10.0), + Obx(() => _buildInfoDesc(context)), + const SizedBox(height: 10.0), + Obx(() => _buildFoucsButton(context)), + ], + ), ), ), - ), - SliverPersistentHeader( - pinned: true, - delegate: CustomStickyHeader( - child: PreferredSize( - preferredSize: const Size.fromHeight(48.0), - child: Container( - color: Colors.white, - child: TabBar( - controller: tabController, - tabs: tabList.map((item) { - return Tab( - child: Badge.count( - backgroundColor: Colors.red, - count: item['badge'] ?? 0, - isLabelVisible: item['badge'] != null, - alignment: Alignment.topRight, - offset: const Offset(14, -6), - child: Text(item['name'], style: const TextStyle(fontWeight: FontWeight.bold)), - ), - ); - }).toList(), - isScrollable: true, //禁止左右滑动 - tabAlignment: TabAlignment.start, - overlayColor: WidgetStateProperty.all(Colors.transparent), - unselectedLabelColor: Colors.black87, - labelColor: Colors.black, - indicator: const UnderlineTabIndicator(borderSide: BorderSide(color: Colors.transparent, width: 2.0)), - indicatorSize: TabBarIndicatorSize.label, - unselectedLabelStyle: const TextStyle(fontSize: 16.0, fontFamily: 'Microsoft YaHei'), - labelStyle: const TextStyle(fontSize: 18.0, fontFamily: 'Microsoft YaHei', fontWeight: FontWeight.bold), - dividerHeight: 0, - padding: const EdgeInsets.symmetric(horizontal: 10.0), - labelPadding: const EdgeInsets.symmetric(horizontal: 15.0), + SliverPersistentHeader( + pinned: true, + delegate: CustomStickyHeader( + child: PreferredSize( + preferredSize: const Size.fromHeight(48.0), + child: Container( + color: Colors.white, + child: TabBar( + controller: tabController, + tabs: tabList.map((item) { + return Tab( + child: Badge.count( + backgroundColor: Colors.red, + count: item['badge'] ?? 0, + isLabelVisible: item['badge'] != null, + alignment: Alignment.topRight, + offset: const Offset(14, -6), + child: Text(item['name'], style: const TextStyle(fontWeight: FontWeight.bold)), + ), + ); + }).toList(), + isScrollable: true, //禁止左右滑动 + tabAlignment: TabAlignment.start, + overlayColor: WidgetStateProperty.all(Colors.transparent), + unselectedLabelColor: Colors.black87, + labelColor: Colors.black, + indicator: const UnderlineTabIndicator(borderSide: BorderSide(color: Colors.transparent, width: 2.0)), + indicatorSize: TabBarIndicatorSize.label, + unselectedLabelStyle: const TextStyle(fontSize: 16.0, fontFamily: 'Microsoft YaHei'), + labelStyle: const TextStyle(fontSize: 18.0, fontFamily: 'Microsoft YaHei', fontWeight: FontWeight.bold), + dividerHeight: 0, + padding: const EdgeInsets.symmetric(horizontal: 10.0), + labelPadding: const EdgeInsets.symmetric(horizontal: 15.0), + ), ), ), ), ), - ), - ]; - }, - body: TabBarView( - controller: tabController, - children: [ - // Tab 1: - Obx(() => _buildGridTab(0)), - ], - ), - ); - }), - ), + ]; + }, + body: TabBarView( + controller: tabController, + children: [ + // Tab 1: + Obx(() => _buildGridTab(0)), + ], + ), + ); + }), + ), ); } @@ -387,7 +388,6 @@ class MyPageState extends State with SingleTickerProviderStateMixin { final double currentHeight = constraints.maxHeight; double ratio = (currentHeight - minHeight) / (maxHeight - minHeight); ratio = ratio.clamp(0.0, 1.0); - String coverBg = userInfo.value.customInfo?['coverBg'] ?? ''; return Stack( fit: StackFit.expand, children: [ @@ -395,8 +395,9 @@ class MyPageState extends State with SingleTickerProviderStateMixin { child: Opacity( opacity: 1.0, child: NetworkOrAssetImage( - imageUrl: coverBg, + imageUrl: userInfo.value.customInfo?['coverBg'], width: double.infinity, + placeholderAsset: 'assets/images/bk.jpg', ), ), ), @@ -410,7 +411,11 @@ class MyPageState extends State with SingleTickerProviderStateMixin { crossAxisAlignment: CrossAxisAlignment.end, children: [ ClipOval( - child: NetworkOrAssetImage(imageUrl: userInfo.value.faceUrl), + child: NetworkOrAssetImage( + imageUrl: userInfo.value.faceUrl, + width: 80, + height: 80, + ), ), const SizedBox(width: 15.0), Expanded( @@ -540,19 +545,42 @@ class MyPageState extends State with SingleTickerProviderStateMixin { child: child, ); }, - child: [1, 3].contains(followed.value) + child: [1, 2, 3].contains(followed.value) ? Row( key: const ValueKey('followed'), children: [ Expanded( child: ElevatedButton( onPressed: () async { - print('点击已关注'); - final res = await ImService.instance.unfollowUser(userIDList: [vlogerId]); - if (res.success) { - // 如果为1那么状态置为0,为3则置为2 - followed.value = followed.value == 1 ? 0 : 2; - // 取关后不需重置陌生人消息group + logger.w('点击已关注/已回关:${followed.value}'); + final ctl = Get.find(); + + if (followed.value == 2) { + //回关,走关注逻辑 + final res = await ImService.instance.followUser(userIDList: [vlogerId]); + if (res.success) { + followed.value = 3; + // 回关后,如果会话存在,将此会话移除noFriend会话组 + final res = await ImService.instance.getConversation(conversationID: 'c2c_$vlogerId'); + if (res.success) { + V2TimConversation conversation = res.data; + if (conversation.conversationGroupList?.isNotEmpty ?? false) { + //把会话数据从陌生人分组移除 + await ImService.instance.deleteConversationsFromGroup( + groupName: conversation.conversationGroupList!.first!, // 不涉及到加入多分组的情况,直接取 + conversationIDList: [conversation.conversationID], + ); + ctl.updateNoFriendMenu(); + } + } + } + } else { + final res = await ImService.instance.unfollowUser(userIDList: [vlogerId]); + if (res.success) { + // 如果为1那么状态置为0,为3则置为2 + followed.value = followed.value == 1 ? 0 : 2; + ctl.mergeNoFriend(conversationID: 'c2c_${userInfo.value.userID}'); + } } }, style: ElevatedButton.styleFrom( @@ -565,16 +593,18 @@ class MyPageState extends State with SingleTickerProviderStateMixin { ), child: Text(followed.value == 1 ? '已关注' - : followed.value == 3 - ? '互关' - : '未知状态'), + : followed.value == 2 + ? '回关' + : followed.value == 3 + ? '已互关' + : '未知状态'), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton( onPressed: () async { - print('私信'); + logger.w('私信'); // 获取指定会话 final res = await ImService.instance.getConversation(conversationID: 'c2c_$vlogerId'); V2TimConversation conversation = res.data; @@ -614,12 +644,12 @@ class MyPageState extends State with SingleTickerProviderStateMixin { child: ElevatedButton.icon( onPressed: () async { // 0没关系,2对方关注了我 - print('点击关注'); + logger.w('点击关注'); final res = await ImService.instance.followUser(userIDList: [vlogerId]); if (res.success) { followed.value = followed.value == 0 ? 1 : 3; if (followed.value == 3) { - // 修改后若为3,我将此会话移除noFriend会话组 + // 修改后若为3,将此会话移除noFriend会话组 final res = await ImService.instance.getConversation(conversationID: 'c2c_$vlogerId'); if (res.success) { V2TimConversation conversation = res.data; @@ -629,14 +659,13 @@ class MyPageState extends State with SingleTickerProviderStateMixin { groupName: conversation.conversationGroupList!.first!, conversationIDList: [conversation.conversationID], ); - //重新构建会话 } } } } }, icon: const Icon(Icons.add), - label: const Text('关注'), + label: Text('关注'), style: ElevatedButton.styleFrom( backgroundColor: FStyle.primaryColor, foregroundColor: Colors.white, diff --git a/lib/pages/search/search-result.dart b/lib/pages/search/search-result.dart index 6191cfa..6b17f7a 100644 --- a/lib/pages/search/search-result.dart +++ b/lib/pages/search/search-result.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:loopin/IM/controller/chat_controller.dart'; -import 'package:loopin/IM/im_core.dart'; -import 'package:loopin/IM/im_message.dart'; import 'package:loopin/IM/im_service.dart' hide logger; -import 'package:loopin/service/http.dart'; import 'package:loopin/api/common_api.dart'; -import 'package:loopin/utils/index.dart'; import 'package:loopin/components/my_toast.dart'; +import 'package:loopin/service/http.dart'; +import 'package:loopin/utils/index.dart'; + import '../../behavior/custom_scroll_behavior.dart'; class SearchResultPage extends StatefulWidget { @@ -198,7 +196,7 @@ class _SearchResultPageState extends State with SingleTickerPr final data = { 'title': _searchQuery, 'size': _pageSize, - 'type': tabIndex+1, // 1-视频;2-商品;3-用户 + 'type': tabIndex + 1, // 1-视频;2-商品;3-用户 'current': currentPage, }; final res = await Http.post(CommonApi.aggregationSearchApi, data: data); @@ -270,75 +268,77 @@ class _SearchResultPageState extends State with SingleTickerPr } // 点击关注按钮 - onFocusBtnClick (user,index) async { - final vlogerId = user['id']; - final doIFollowVloger = user['doIFollowVloger']; - print('是否关注此用户------------->${doIFollowVloger}'); - if (doIFollowVloger == null || doIFollowVloger == false) { - final res = await ImService.instance.followUser(userIDList: [vlogerId]); - print('关注结果------------->${res.success}'); - if (res.success) { - setState(() { - _userResults[index]['doIFollowVloger'] = true; - }); - } - }else{ - final res = await ImService.instance.unfollowUser(userIDList: [vlogerId]); - print('取消关注结果------------->${res.success}'); - if (res.success) { - setState(() { - _userResults[index]['doIFollowVloger'] = false; - }); - } - } + onFocusBtnClick(user, index) async { + final vlogerId = user['id']; + final doIFollowVloger = user['doIFollowVloger']; + print('是否关注此用户------------->$doIFollowVloger'); + print('此用户UserId------------->$vlogerId'); + if (doIFollowVloger == false) { + final res = await ImService.instance.followUser(userIDList: [vlogerId]); + print('关注结果------------->${res.success}'); + if (res.success) { + setState(() { + _userResults[index]['doIFollowVloger'] = !_userResults[index]['doIFollowVloger']; + }); + } + } else { + final res = await ImService.instance.followUser(userIDList: [vlogerId]); + print('取消关注结果------------->${res.success}'); + if (res.success) { + setState(() { + _userResults[index]['doIFollowVloger'] = !_userResults[index]['doIFollowVloger']; + }); + } + } } + // 构建加载更多组件 Widget _buildLoadMoreWidget(int tabIndex) { - if (_isLoadingMore) { - return const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), // 减少垂直间距 - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ), - ); - } - - bool hasMore; - switch (tabIndex) { - case 0: - hasMore = _videoHasMore; - break; - case 1: - hasMore = _productHasMore; - break; - case 2: - hasMore = _userHasMore; - break; - default: - hasMore = false; - } - - if (!hasMore && _getCurrentResults(tabIndex).isNotEmpty) { - return const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), // 减少垂直间距 - child: Text( - '没有更多数据了', - style: TextStyle( - fontSize: 12, // 减小字体 - color: Colors.grey, + if (_isLoadingMore) { + return const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), // 减少垂直间距 + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), ), ), - ), - ); - } + ); + } - return Container(); -} + bool hasMore; + switch (tabIndex) { + case 0: + hasMore = _videoHasMore; + break; + case 1: + hasMore = _productHasMore; + break; + case 2: + hasMore = _userHasMore; + break; + default: + hasMore = false; + } + + if (!hasMore && _getCurrentResults(tabIndex).isNotEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), // 减少垂直间距 + child: Text( + '没有更多数据了', + style: TextStyle( + fontSize: 12, // 减小字体 + color: Colors.grey, + ), + ), + ), + ); + } + + return Container(); + } // 获取当前Tab的数据 List _getCurrentResults(int tabIndex) { @@ -476,9 +476,7 @@ class _SearchResultPageState extends State with SingleTickerPr return NotificationListener( onNotification: (ScrollNotification scrollInfo) { - if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && - !_isLoadingMore && - _videoHasMore) { + if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && !_isLoadingMore && _videoHasMore) { _loadMoreData(0); } return false; @@ -580,14 +578,14 @@ class _SearchResultPageState extends State with SingleTickerPr crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.end, // 内容靠底部对齐 children: [ - Text( + Text( video['title']?.toString() ?? '无标题', style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w500, ), - maxLines: 1, // 改为 1,限制为单行 - overflow: TextOverflow.ellipsis, // 超出部分显示省略号 + maxLines: 1, // 改为 1,限制为单行 + overflow: TextOverflow.ellipsis, // 超出部分显示省略号 ), const SizedBox(height: 4), Row( @@ -605,9 +603,7 @@ class _SearchResultPageState extends State with SingleTickerPr ) : null, ), - child: video['avatar'] == null || video['avatar'].toString().isEmpty - ? const Icon(Icons.person, size: 10, color: Colors.grey) - : null, + child: video['avatar'] == null || video['avatar'].toString().isEmpty ? const Icon(Icons.person, size: 10, color: Colors.grey) : null, ), const SizedBox(width: 4), Expanded( @@ -627,7 +623,7 @@ class _SearchResultPageState extends State with SingleTickerPr const Icon(Icons.favorite_border, size: 10, color: Colors.grey), const SizedBox(width: 2), Text( - Utils().formatLikeCount(video['likeCounts'] ?? 0), + Utils.graceNumber(video['likeCounts'] ?? 0), style: const TextStyle( fontSize: 9, color: Colors.grey, @@ -688,9 +684,7 @@ class _SearchResultPageState extends State with SingleTickerPr return NotificationListener( onNotification: (ScrollNotification scrollInfo) { - if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && - !_isLoadingMore && - _productHasMore) { + if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && !_isLoadingMore && _productHasMore) { _loadMoreData(1); } return false; @@ -755,97 +749,97 @@ class _SearchResultPageState extends State with SingleTickerPr return Container(); } - Widget _buildProductItem(Map product) { - return GestureDetector( - onTap: () { - // 商品点击事件处理 - print('点击了商品: ${product['id']}'); - Get.toNamed('/goods', arguments: {'goodsId': product['id']}); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(_itemCornerRadius), - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.1), - blurRadius: 4, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 商品图片 - 宽度100%,高度自适应 - ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(_itemCornerRadius), - topRight: Radius.circular(_itemCornerRadius), + Widget _buildProductItem(Map product) { + return GestureDetector( + onTap: () { + // 商品点击事件处理 + print('点击了商品: ${product['id']}'); + Get.toNamed('/goods', arguments: {'goodsId': product['id']}); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(_itemCornerRadius), + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.1), + blurRadius: 4, + offset: const Offset(0, 2), ), - child: Container( - width: double.infinity, - color: Colors.grey[200], - child: product['pic'] != null - ? Image.network( - product['pic'].toString(), - fit: BoxFit.cover, - ) - : Container( - height: 110, - child: const Center( - child: Icon(Icons.shopping_bag, color: Colors.grey, size: 32), - ), - ), - ), - ), - // 商品信息 - 固定在容器底部 - Expanded( - child: Padding( - padding: const EdgeInsets.all(6), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.end, // 内容靠底部对齐 - children: [ - Text( - product['name']?.toString() ?? '未知商品', - style: const TextStyle( - fontSize: 11, - fontWeight: FontWeight.w500, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '¥${product['price']?.toString() ?? '0.00'}', - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.pink, + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 商品图片 - 宽度100%,高度自适应 + ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(_itemCornerRadius), + topRight: Radius.circular(_itemCornerRadius), + ), + child: Container( + width: double.infinity, + color: Colors.grey[200], + child: product['pic'] != null + ? Image.network( + product['pic'].toString(), + fit: BoxFit.cover, + ) + : SizedBox( + height: 110, + child: const Center( + child: Icon(Icons.shopping_bag, color: Colors.grey, size: 32), ), ), - Text( - '已售 ${product['sales']?.toString() ?? '0'}', - style: const TextStyle( - fontSize: 9, - color: Colors.grey, - ), - ), - ], - ), - ], ), ), - ), - ], + // 商品信息 - 固定在容器底部 + Expanded( + child: Padding( + padding: const EdgeInsets.all(6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, // 内容靠底部对齐 + children: [ + Text( + product['name']?.toString() ?? '未知商品', + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '¥${product['price']?.toString() ?? '0.00'}', + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.pink, + ), + ), + Text( + '已售 ${product['sales']?.toString() ?? '0'}', + style: const TextStyle( + fontSize: 9, + color: Colors.grey, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), ), - ), - ); -} + ); + } // 用户Tab Widget _buildUserTab() { @@ -855,9 +849,7 @@ class _SearchResultPageState extends State with SingleTickerPr return NotificationListener( onNotification: (ScrollNotification scrollInfo) { - if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && - !_isLoadingMore && - _userHasMore) { + if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && !_isLoadingMore && _userHasMore) { _loadMoreData(2); } return false; @@ -872,7 +864,7 @@ class _SearchResultPageState extends State with SingleTickerPr return _buildLoadMoreWidget(2); } final user = _userResults[index] as Map; - return _buildUserItem(user,index); + return _buildUserItem(user, index); }, ), ), @@ -882,7 +874,7 @@ class _SearchResultPageState extends State with SingleTickerPr Widget _buildUserItem(Map user, int index) { // 判断当前用户是否已被关注 bool isFollowing = user['doIFollowVloger'] ?? false; - print('111111111111111111111111${isFollowing}'); + print('111111111111111111111111$isFollowing'); return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(10), @@ -912,9 +904,7 @@ class _SearchResultPageState extends State with SingleTickerPr ) : null, ), - child: user['avatar'] == null - ? const Icon(Icons.person, color: Colors.grey, size: 20) - : null, + child: user['avatar'] == null ? const Icon(Icons.person, color: Colors.grey, size: 20) : null, ), const SizedBox(width: 10), Expanded( @@ -951,9 +941,7 @@ class _SearchResultPageState extends State with SingleTickerPr borderRadius: BorderRadius.circular(16), ), // 添加边框样式 - side: isFollowing - ? BorderSide(color: Colors.grey[400]!, width: 0.5) - : BorderSide.none, + side: isFollowing ? BorderSide(color: Colors.grey[400]!, width: 0.5) : BorderSide.none, ), child: Text( isFollowing ? '已关注' : '关注', @@ -984,4 +972,4 @@ class _SearchResultPageState extends State with SingleTickerPr ), ); } -} \ No newline at end of file +} diff --git a/lib/pages/video/module/recommend.dart b/lib/pages/video/module/recommend.dart index 1f20b69..c9ba0f9 100644 --- a/lib/pages/video/module/recommend.dart +++ b/lib/pages/video/module/recommend.dart @@ -675,7 +675,7 @@ class _RecommendModuleState extends State { 'size': pageSize, }); final data = res['data']; - logger.d('关注用户的视频列表:$data'); + // logger.d('关注用户的视频列表:$data'); if (data == null || (data is List && data.isEmpty)) { return; } @@ -698,7 +698,7 @@ class _RecommendModuleState extends State { if (videos.isNotEmpty) { page++; } - logger.i('视频数据列表------------------->$videoList'); + // logger.i('视频数据列表------------------->$videoList'); // 初始化播放器 player.open( @@ -1247,17 +1247,14 @@ class _RecommendModuleState extends State { ), ], ), - onTap: ()async { - player.pause(); - // 跳转到举报页面并等待返回结果 - final result = await Get.toNamed( - '/report', - arguments: videoList[videoModuleController - .videoPlayIndex.value]); - if (result != null) { - player.play(); - }; - }, + onTap: () async { + player.pause(); + // 跳转到举报页面并等待返回结果 + final result = await Get.toNamed('/report', arguments: videoList[videoModuleController.videoPlayIndex.value]); + if (result != null) { + player.play(); + } + }, ), ], ), diff --git a/lib/router/index.dart b/lib/router/index.dart index 1149d8b..627c650 100644 --- a/lib/router/index.dart +++ b/lib/router/index.dart @@ -10,15 +10,19 @@ import 'package:loopin/pages/chat/chat_no_friend.dart'; import 'package:loopin/pages/chat/notify/interaction.dart'; import 'package:loopin/pages/chat/notify/noFriend.dart'; import 'package:loopin/pages/chat/notify/system.dart'; +import 'package:loopin/pages/groupChat/index.dart'; import 'package:loopin/pages/my/des.dart'; +import 'package:loopin/pages/my/fans.dart'; +import 'package:loopin/pages/my/flowing.dart'; +import 'package:loopin/pages/my/mutual_followers.dart'; 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/video/report.dart'; import 'package:loopin/pages/search/index.dart'; import 'package:loopin/pages/search/search-result.dart'; import 'package:loopin/pages/video/commonVideo.dart'; +import 'package:loopin/pages/video/report.dart'; import '../layouts/index.dart'; /* 引入路由页面 */ @@ -42,8 +46,8 @@ final Map routes = { '/vloger': const Vloger(), '/report': const ReportPage(), '/videoDetail': const VideoDetailPage(), - '/search': const SearchPage(), - '/search-result': const SearchResultPage(), + '/search': const SearchPage(), + '/search-result': const SearchResultPage(), //settins '/setting': const Setting(), '/userInfo': const UserInfo(), @@ -56,6 +60,12 @@ final Map routes = { '/noFriend': const Nofriend(), '/system': const System(), '/interaction': const Interaction(), + //关系链 + '/fans': const Fans(), + '/flow': const Flowing(), + '/eachFlow': const MutualFollowers(), + //群 + '/group': const StartGroupChatPage(), }; final List routeList = routes.entries diff --git a/lib/service/http_config.dart b/lib/service/http_config.dart index 896ae88..6b5bee6 100644 --- a/lib/service/http_config.dart +++ b/lib/service/http_config.dart @@ -10,6 +10,7 @@ class HttpConfig { // baseUrl: 'http://cjh.wuzhongjie.com.cn', // baseUrl: 'http://82.156.121.2:8880', baseUrl: 'http://192.168.1.65:8880', + // baseUrl: 'http://192.168.1.22:8080', // connectTimeout: Duration(seconds: 30), // receiveTimeout: Duration(seconds: 30), diff --git a/lib/styles/index.dart b/lib/styles/index.dart index cae3ce4..2f8bd3c 100644 --- a/lib/styles/index.dart +++ b/lib/styles/index.dart @@ -5,26 +5,34 @@ import 'package:flutter/material.dart'; class FStyle { // 模拟Badge - static badge(int count, { - Color color = Colors.red, - bool isdot = false, - double height = 16.0, - double width = 16.0 - }) { + static badge(int count, {Color color = Colors.red, bool isdot = false, double height = 16.0, double width = 16.0}) { final num = count > 99 ? '99+' : count; return Container( alignment: Alignment.center, height: isdot ? height / 2 : height, width: isdot ? width / 2 : width, decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(100.00)), - child: isdot ? null : Text('$num', style: const TextStyle(color: Colors.white, fontSize: 10.0,)), + child: isdot + ? null + : Text('$num', + style: const TextStyle( + color: Colors.white, + fontSize: 10.0, + )), ); } + // 边框 - static const border = Divider(color: Color(0xFFBBBBBB), height: 1.0, thickness: .5,); + // static const border = Divider(color: Color(0xFFBBBBBB), height: 1.0, thickness: .5,); + static const border = Divider( + color: Colors.white, + height: 1.0, + thickness: .5, + ); // 颜色 - static const backgroundColor = Color(0xFFEEEEEE); + // static const backgroundColor = Color(0xFFEEEEEE); + static const backgroundColor = Colors.white; static const primaryColor = Color(0xFFFF5000); static const white = Colors.white; static const c999 = Color(0xFF999999); diff --git a/lib/utils/index.dart b/lib/utils/index.dart index de800eb..05a2fc3 100644 --- a/lib/utils/index.dart +++ b/lib/utils/index.dart @@ -113,7 +113,7 @@ class Utils { } // 大数字转换 - String graceNumber(int number) { + static String graceNumber(int number) { if (number == 0) return "0"; if (number < 1000) { @@ -130,12 +130,12 @@ class Utils { } } - String _trimTrailingZero(double number) { + static String _trimTrailingZero(double number) { return number.toStringAsFixed(1).replaceAll(RegExp(r'\.0$'), ''); } // 处理聊天消息时间 - String formatChatTime(int timestamp) { + static String formatChatTime(int timestamp) { // timestamp 是秒级时间戳,转成 DateTime DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); @@ -189,31 +189,29 @@ class Utils { '${messageTime.minute.toString().padLeft(2, '0')}'; } - String _formatHourMinute(DateTime dt) { + static String _formatHourMinute(DateTime dt) { final hour = dt.hour.toString().padLeft(2, '0'); final minute = dt.minute.toString().padLeft(2, '0'); return "$hour:$minute"; } - String _weekdayLabel(int weekday) { + static String _weekdayLabel(int weekday) { const weekdays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]; return weekdays[(weekday - 1) % 7]; } - // 格式化点赞数量 - String formatLikeCount(int count) { - if (count >= 10000) { - return '${(count / 10000).toStringAsFixed(1)}w'; - } else if (count >= 1000) { - return '${(count / 1000).toStringAsFixed(1)}k'; - } - return count.toString(); - } - // 分转换为元 - static String fenToYuanSimple(dynamic fen) { - if (fen == null) { - return '0.00'; - } - return (double.parse(fen) / 100).toStringAsFixed(2); + static String getTipText(int realFollowType) { + switch (realFollowType) { + case 0: + return '关注'; + case 1: + return '已关注'; + case 2: + return '回关'; + case 3: + return '已互关'; + default: + return '未知状态'; + } } } diff --git a/lib/utils/parse_message_summary.dart b/lib/utils/parse_message_summary.dart index c5fb883..b286114 100644 --- a/lib/utils/parse_message_summary.dart +++ b/lib/utils/parse_message_summary.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:loopin/IM/push_service.dart'; import 'package:loopin/models/notify_message.type.dart'; import 'package:loopin/models/summary_type.dart'; import 'package:tencent_cloud_chat_sdk/enum/message_elem_type.dart'; @@ -53,8 +52,8 @@ String _parseCustomMessage(V2TimMessage? msg) { if (msg == null) return '[null]'; final sum = msg.cloudCustomData; final elment = msg.customElem; // 所有服务端发送的通知消息都走【自定义消息类型】 - logger.w('解析自定义消息:${msg.toJson()}'); - logger.w('解析element:${elment?.desc ?? 'summary_error'}'); + // logger.w('解析自定义消息:${msg.toJson()}'); + // logger.w('解析element:${elment?.desc ?? 'summary_error'}'); try { switch (sum) { case SummaryType.hongbao: