diff --git a/lib/IM/controller/chat_controller.dart b/lib/IM/controller/chat_controller.dart index cda9069..5e4e786 100644 --- a/lib/IM/controller/chat_controller.dart +++ b/lib/IM/controller/chat_controller.dart @@ -13,31 +13,42 @@ class ChatController extends GetxController { final chatList = [].obs; void initChatData() { - chatList.value = []; + // chatList.value = []; + chatList.clear(); nextSeq.value = '0'; isFinished.value = false; } // 获取所有会话列表 - void getConversationList() async { + Future getConversationList() async { + logger.e('开始获取会话列表数据'); if (isFinished.value) { // 拉取完数据了,直接结束 + logger.e('会话触底无数据'); return; } - final res = await ImService.instance.getConversationList(nextSeq.value, count.value); + try { + final res = await ImService.instance.getConversationList(nextSeq.value, count.value); - if (!res.success || res.data == null) return; + if (!res.success || res.data == null) { + logger.e('获取会话失败::${res.desc}'); + return; + } - final List convList = res.data; - for (var conv in convList) { - logger.i('基本会话: ${conv.conversation.toJson()}, 会话ID: ${conv.conversation.conversationID}'); - } + final List convList = res.data; + for (var conv in convList) { + logger.w('基本会话: ${conv.conversation.toLogString()}'); + } - chatList.addAll(convList); - // 不包含noFriend才执行加载数据逻辑,分页加载时候过滤 - final hasNoFriend = chatList.any((item) => item.conversation.conversationGroupList?.contains(myConversationType.ConversationType.noFriend.name) ?? false); - if (!hasNoFriend) { - getNoFriendData(); + chatList.addAll(convList); + // 不包含noFriend才执行加载数据逻辑,分页加载时候过滤 + final hasNoFriend = chatList.any((item) => item.conversation.conversationGroupList?.contains(myConversationType.ConversationType.noFriend.name) ?? false); + logger.e('开始构建陌生人入口是否包含noFriend?:$hasNoFriend'); + if (!hasNoFriend) { + getNoFriendData(); + } + } catch (e) { + logger.e('获取会话异常::$e'); } } @@ -45,6 +56,7 @@ class ChatController extends GetxController { void getNoFriendData({V2TimConversation? csion}) async { // 检测会话列表是否已有陌生人消息菜单 final hasNoFriend = chatList.any((item) => item.conversation.conversationGroupList?.contains(myConversationType.ConversationType.noFriend.name) ?? false); + logger.w('检测是否存在nofriend入口:$hasNoFriend'); if (hasNoFriend) { // 已经有了入口 final ConversationViewModel matchItem = chatList.firstWhere( @@ -60,49 +72,49 @@ class ChatController extends GetxController { matchItem.conversation.lastMessage = csion!.lastMessage; matchItem.conversation.unreadCount = unreadTotal.data; chatList.refresh(); - return; - } - // 没有则执行创建逻辑 - 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) { - final conv = convList.first; - final faceUrl = 'assets/images/notify/msr.png'; - conv.showName = '陌生人消息'; - conv.unreadCount = unread.data; - final createItem = ConversationViewModel( - conversation: conv, - faceUrl: faceUrl, + } else { + // 没有则执行创建逻辑 + 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, + ), ); - final newList = List.from(chatList); - newList.add(createItem); - 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 (unread.success) { + final conv = convList.first; + final faceUrl = 'assets/images/notify/msr.png'; + conv.showName = '陌生人消息'; + conv.unreadCount = unread.data; + final createItem = ConversationViewModel( + conversation: conv, + faceUrl: faceUrl, + ); + final newList = List.from(chatList); + newList.add(createItem); + 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; + } } } } } /// 按会话分组查询 getConversationListByFilter - // void getConversationList() async { + // void filterConversationList() async { // final res = await ImService.instance.getConversationListByFilter( // filter: V2TimConversationFilter(conversationGroup: null), // nextSeq: nextSeq.value, diff --git a/lib/IM/controller/im_user_info_controller.dart b/lib/IM/controller/im_user_info_controller.dart index 5485fb3..d56ba08 100644 --- a/lib/IM/controller/im_user_info_controller.dart +++ b/lib/IM/controller/im_user_info_controller.dart @@ -8,8 +8,7 @@ class ImUserInfoController extends GetxController { @override void onInit() { super.onInit(); - refreshUserInfo(); - logger.i('IM用户信息初始化'); + logger.i('开始IM用户信息初始化'); } V2TimUserFullInfo? rawUserInfo; @@ -52,14 +51,14 @@ class ImUserInfoController extends GetxController { birthday.value = userInfo.birthday ?? 0; } - void refreshUserInfo() async { + Future refreshUserInfo() async { try { final updatedUserInfo = await ImService.instance.selfInfo(); if (updatedUserInfo.success) { init(updatedUserInfo.data); } } catch (e) { - print('刷新用户信息失败: $e'); + logger.e('刷新用户信息失败: $e'); } } diff --git a/lib/IM/global_badge.dart b/lib/IM/global_badge.dart index aef3fe0..c8b0852 100644 --- a/lib/IM/global_badge.dart +++ b/lib/IM/global_badge.dart @@ -16,6 +16,10 @@ class GlobalBadge extends GetxController { /// 监听器对象(用于 add/remove) late final V2TimConversationListener _listener; + void rest() { + totalUnread.value = 0; + } + @override void onInit() { super.onInit(); @@ -34,12 +38,16 @@ class GlobalBadge extends GetxController { onConversationChanged: (List conversationList) async { logger.w('会话变更:会话分组:${conversationList.first.conversationGroupList},会话内容${conversationList.first.toLogString()}'); final ctl = Get.find(); + logger.w('当前会话列表内容:${ctl.chatList.length}'); + final updatedIds = conversationList.map((e) => e.conversationID).toSet(); logger.w('要变更的会话id:$updatedIds'); for (int i = 0; i < ctl.chatList.length; i++) { final chatItem = ctl.chatList[i]; logger.w('需要更新的ID:${chatItem.conversation.conversationID}'); if (updatedIds.contains(chatItem.conversation.conversationID)) { + logger.w('找到的chatList的ID:${chatItem.conversation.conversationID}'); + final updatedConv = conversationList.firstWhere( (c) => c.conversationID == chatItem.conversation.conversationID, orElse: () => V2TimConversation(conversationID: ''), @@ -57,10 +65,20 @@ class GlobalBadge extends GetxController { chatItem.conversation.unreadCount = unread.data; // 获取陌生人未读总数 } else { // 其他类型统一更新处理 + logger.w('不需要分组的会话,正常更新'); chatItem.conversation = updatedConv; + update(); } } } + // 如果没当前会话列表为空 + if (ctl.chatList.isEmpty) { + // 重新获取一次 + logger.w('重新获取会话'); + ctl.initChatData(); + ctl.getConversationList(); + } + //重新排序 ctl.chatList.sort((a, b) { final atime = a.conversation.lastMessage?.timestamp ?? 0; @@ -70,8 +88,8 @@ class GlobalBadge extends GetxController { ctl.chatList.refresh(); }, ); - final ctl = Get.find(); - ctl.getConversationList(); + // final ctl = Get.find(); + // ctl.getConversationList(); _initUnreadCount(); _addListener(); } @@ -117,7 +135,7 @@ class GlobalBadge extends GetxController { } } } else { - logger.w('不需要进行分组'); + logger.w('新会话不需分组'); } } @@ -129,9 +147,21 @@ class GlobalBadge extends GetxController { Get.find().setBadge(TabType.chat, totalUnread.value); final to = res.data; logger.i('初始化未读消息数$to'); + } else { + //处理安卓端重新登录后获取未读数量失败的问题 + logger.e('获取初始化未读数失败:${res.desc},重新补偿获取'); + Future.delayed(Duration(seconds: 1), handAndroid); } } + /// 处理安卓端异常的问题 + handAndroid() { + _initUnreadCount(); + final ctl = Get.find(); + ctl.initChatData(); + ctl.getConversationList(); + } + /// 添加会话未读数监听器 void _addListener() { TencentImSDKPlugin.v2TIMManager.getConversationManager().addConversationListener(listener: _listener); @@ -146,6 +176,8 @@ class GlobalBadge extends GetxController { /// 移除监听器,防止重复注册 @override void onClose() { + logger.i(_listener); + logger.i('移除global未读监听器'); TencentImSDKPlugin.v2TIMManager.getConversationManager().removeConversationListener(listener: _listener); super.onClose(); } diff --git a/lib/IM/im_core.dart b/lib/IM/im_core.dart index 3f7ba92..798a3f2 100644 --- a/lib/IM/im_core.dart +++ b/lib/IM/im_core.dart @@ -1,4 +1,9 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:logger/logger.dart'; +import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/controller/video_module_controller.dart'; +import 'package:loopin/utils/common.dart'; import 'package:tencent_cloud_chat_sdk/enum/V2TimSDKListener.dart'; import 'package:tencent_cloud_chat_sdk/enum/log_level_enum.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; @@ -9,6 +14,18 @@ final logger = Logger(); class ImCore { static bool _isInitialized = false; + static Future handleLogout() async { + final loginRes = await ImService.instance.logout(); + if (loginRes.success) { + // 清除存储信息 + Common.logout(); + // 初始化视频 + final videoController = Get.find(); + videoController.init(); + Get.toNamed('/login'); + } + } + static Future init({required int sdkAppId}) async { if (_isInitialized) return true; @@ -20,7 +37,18 @@ class ImCore { logger.i("IM连接成功"); }, onConnectFailed: (code, error) => logger.e("IM连接失败: $code $error"), - onKickedOffline: () => logger.w("IM被踢下线"), + onKickedOffline: () { + logger.w("IM被踢下线"); + Get.snackbar( + '提示', + '您的帐号已在其他处登录', + duration: Duration(seconds: 10), + backgroundColor: Colors.red.withAlpha(230), + colorText: Colors.white, + icon: const Icon(Icons.error_outline, color: Colors.white), + ); + handleLogout(); + }, onUserSigExpired: () => logger.w("UserSig 过期"), onSelfInfoUpdated: (V2TimUserFullInfo info) { logger.i("用户信息更新: ${info.toJson()}"); diff --git a/lib/IM/im_service.dart b/lib/IM/im_service.dart index b401f5d..5175293 100644 --- a/lib/IM/im_service.dart +++ b/lib/IM/im_service.dart @@ -29,6 +29,8 @@ import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_info_result.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_friend_operation_result.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_change_info.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_search_param.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message_search_result.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_info_result.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart'; @@ -61,16 +63,22 @@ class ImService { if (result.success) { logger.i("IM 登录成功:$userID"); - // 初始化push服务 - PushService().initPush( - sdkAppId: 1600080789, - appKey: 'vkFpe55aYqfV7Sk5uGaoxhEstJ3tcI9dquk7JwG1GloDSLD2HeMWeQweWWXgNlhC', - ); - // 初始化微信 SDK + + // 初始化会话数据 + final ctl = Get.find(); + await ctl.getConversationList(); + + /// 初始化微信 SDK await Wxsdk.init(); // 注册用户信息(基本信息+自定义信息) - Get.put(ImUserInfoController(), permanent: true); + if (!Get.isRegistered()) { + final imInfo = Get.put(ImUserInfoController(), permanent: true); + await imInfo.refreshUserInfo(); + } else { + await Get.find().refreshUserInfo(); + } + // 登录成功后注册高级消息监听器 final messageService = ImMessageListenerService(); Get.put(messageService, permanent: true); @@ -84,6 +92,12 @@ class ImService { /// 注册消息未读数监听器 Get.put(GlobalBadge(), permanent: true); + + // 初始化push服务 + PushService().initPush( + sdkAppId: 1600080789, + appKey: 'vkFpe55aYqfV7Sk5uGaoxhEstJ3tcI9dquk7JwG1GloDSLD2HeMWeQweWWXgNlhC', + ); } else { logger.i("IM 登录失败:${result.code} - ${result.desc}"); Get.snackbar( @@ -101,16 +115,12 @@ class ImService { Future logout() async { final res = await TencentImSDKPlugin.v2TIMManager.logout(); if (res.code == 0) { - /// 清理用户信息 - Get.delete(force: true); - /// 移出消息监听器 - Get.find().onClose(); - Get.delete(force: true); + await Get.delete(force: true); /// 移出关系链监听器 Get.find().unregister(); - Get.delete(force: true); + await Get.delete(force: true); /// 清理tabbar Get.find().badgeMap.clear(); @@ -120,13 +130,13 @@ class ImService { /// 移出未读消息监听器 Get.find().onClose(); - Get.delete(force: true); + await Get.delete(force: true); /// 移除推送服务 - PushService.unInitPush(); + await PushService.unInitPush(); /// 反初始化 - ImCore.unInit(); + await ImCore.unInit(); } return ImResult.wrapNoData(res); } @@ -231,26 +241,26 @@ class ImService { } final convList = res.data?.conversationList ?? []; + logger.w('未过滤前到会话数据:$convList'); + final userIDList = []; final groupIDList = []; // 提前收集所有需要批量查询的 userID 和 groupID for (var conv in convList) { - if ((conv.faceUrl == null || conv.faceUrl!.isEmpty)) { - if (conv.userID != null) { - userIDList.add(conv.userID!); - } else if (conv.groupID != null) { - groupIDList.add(conv.groupID!); - } + if (conv.userID != null) { + userIDList.add(conv.userID!); + } else if (conv.groupID != null) { + groupIDList.add(conv.groupID!); } } Map userFaceUrlMap = {}; Map groupFaceUrlMap = {}; - String isCustomAdmin = '0'; - + Map isCustomAdmin = {}; if (userIDList.isNotEmpty) { final userRes = await TencentImSDKPlugin.v2TIMManager.getUsersInfo(userIDList: userIDList); + if (userRes.code == 0) { for (var user in userRes.data!) { final userId = user.userID ?? ''; @@ -258,8 +268,9 @@ class ImService { // 读取管理员标识 final customInfo = user.customInfo; + if (customInfo != null) { - isCustomAdmin = customInfo['admin'] ?? '0'; + isCustomAdmin[userId] = customInfo['admin'] ?? '0'; } } } @@ -280,7 +291,6 @@ class ImService { final viewList = convList.map((conv) { String? faceUrl = conv.faceUrl; - // 如果 faceUrl 本身为空,尝试从查到的 map 中取值 if (faceUrl == null || faceUrl.isEmpty) { if (conv.userID != null) { faceUrl = userFaceUrlMap[conv.userID!]; @@ -289,7 +299,11 @@ class ImService { } } - return ConversationViewModel(conversation: conv, faceUrl: faceUrl, isCustomAdmin: isCustomAdmin); + return ConversationViewModel( + conversation: conv, + faceUrl: faceUrl, + isCustomAdmin: isCustomAdmin[conv.userID], + ); }).toList(); // 筛选数据,过滤掉陌生人消息 @@ -337,9 +351,6 @@ class ImService { ///获取所有会话数据 Future> getConvData(String nextSeq, int count) async { final res = await TencentImSDKPlugin.v2TIMManager.getConversationManager().getConversationList(nextSeq: nextSeq, count: count); - // for (var element in res.data!.conversationList) { - // logger.e('所有的会话数据:${element.toJson()}'); - // } return ImResult.wrap(res); } @@ -357,6 +368,33 @@ class ImService { ); } + /// 搜索本地消息 + Future> searchLocalMessages({ + required String page, + required String conversationID, + + /// 关键词匹配机制or=0,and=1, + int type = 1, + + /// ['你好','周末'] + required List keywordList, + + /// 默认自定义消息 + List messageTypeList = const [1, 2], + }) async { + final searchParam = V2TimMessageSearchParam( + type: type, + conversationID: conversationID, + keywordList: keywordList, + messageTypeList: messageTypeList, + pageSize: 100, + // pageIndex: page, + searchCursor: page, + ); + final V2TimValueCallback res = await TIMMessageManager.instance.searchLocalMessages(searchParam: searchParam); + return ImResult.wrap(res); + } + /// 获取消息 Future>> findMessages({ required List messageIDList, diff --git a/lib/IM/push_service.dart b/lib/IM/push_service.dart index 02b839a..b0f4206 100644 --- a/lib/IM/push_service.dart +++ b/lib/IM/push_service.dart @@ -135,7 +135,7 @@ class PushService { logger.w(router); if (router == null || router != '') { // 聊天 - if (data['userID'] != null) { + if (data['userID'] != null || data['userID'] != '') { logger.w('有userID'); // 单聊,获取会话 final covRes = await ImService.instance.getConversation(conversationID: 'c2c_${data['userID']}'); diff --git a/lib/api/shop_api.dart b/lib/api/shop_api.dart index f984fd5..6f12ace 100644 --- a/lib/api/shop_api.dart +++ b/lib/api/shop_api.dart @@ -6,8 +6,8 @@ class ShopApi { /// [nameLike] 商品名称 static const String shopList = '/app/product/page'; // 商品列表 - /// [showStatus]1=显示 [nameLike]分类名称 - static const String shopCategory = '/app/productCategory/page'; // 商品分类 + /// [showStatus]1=显示 [nameLike]分类名称 [level]1 + static const String shopCategory = '/app/product/category/tree'; // 商品分类 /// [] static const String shopSwiperList = '/app/article/carousel'; // 商品首页轮播图 diff --git a/lib/components/network_or_asset_image.dart b/lib/components/network_or_asset_image.dart index 5d3d3b9..013447e 100644 --- a/lib/components/network_or_asset_image.dart +++ b/lib/components/network_or_asset_image.dart @@ -26,6 +26,18 @@ class NetworkOrAssetImage extends StatelessWidget { width: width, height: height, fit: fit, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) { + return child; + } + // 显示占位 + return Image.asset( + placeholderAsset, + width: width, + height: height, + fit: fit, + ); + }, errorBuilder: (context, error, stackTrace) { return Image.asset( placeholderAsset, diff --git a/lib/controller/shop_index_controller.dart b/lib/controller/shop_index_controller.dart index 17667ce..0fec11b 100644 --- a/lib/controller/shop_index_controller.dart +++ b/lib/controller/shop_index_controller.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:loopin/IM/im_service.dart'; import 'package:loopin/api/shop_api.dart'; import 'package:loopin/service/http.dart'; @@ -39,7 +38,7 @@ class ShopIndexController extends GetxController with GetSingleTickerProviderSta RxInt currentTabIndex = 0.obs; /// 初始化 Tab 分类 - void initTabs() async { + void initTabs({required TickerProvider vsync}) async { // 释放旧的 ScrollController tabs.forEach((_, state) => state.scrollController.dispose()); tabs.clear(); @@ -50,10 +49,10 @@ class ShopIndexController extends GetxController with GetSingleTickerProviderSta // 赋值 tab 数据 final res = await Http.post(ShopApi.shopCategory, data: { - 'showStatus': 1, + 'level': 1, }); - final data = res['data']['records'] as List; - logger.w(data); + final data = res['data'] as List; + // logger.w(data); tabList.addAll(data); // 初始化每个 tab 的状态 @@ -70,8 +69,9 @@ class ShopIndexController extends GetxController with GetSingleTickerProviderSta } // 创建新的 TabController - tabController = TabController(length: tabList.length, vsync: this); - tabController!.addListener(_tabListener); + tabController = TabController(length: tabList.length, vsync: vsync); + + tabController?.addListener(_tabListener); // 初始化第一个 tab 的数据 if (tabList.isNotEmpty) { loadSwiperData(); @@ -118,7 +118,7 @@ class ShopIndexController extends GetxController with GetSingleTickerProviderSta final data = res['data']['records']; tab.dataList.addAll(data); - logger.w(res); + // logger.w(res); tab.currentPage.value += 1; tab.isLoading.value = false; @@ -131,14 +131,13 @@ class ShopIndexController extends GetxController with GetSingleTickerProviderSta 'type': 1, }); final data = res['data']; - logger.w(res); + // logger.w(res); swiperData.assignAll(data); } @override void onClose() { tabController?.removeListener(_tabListener); - tabController?.dispose(); tabs.forEach((_, state) => state.scrollController.dispose()); super.onClose(); } diff --git a/lib/layouts/index.dart b/lib/layouts/index.dart index 17cff8a..f0377e7 100644 --- a/lib/layouts/index.dart +++ b/lib/layouts/index.dart @@ -3,7 +3,6 @@ library; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:loopin/IM/controller/chat_controller.dart'; import 'package:loopin/IM/controller/tab_bar_controller.dart'; import 'package:loopin/models/tab_type.dart'; import 'package:loopin/pages/video/module/recommend.dart'; @@ -222,10 +221,11 @@ class _LayoutState extends State { } if (index == 3) { // 更新会话列表 - final ctl = Get.find(); - if (ctl.chatList.isEmpty) { - Get.find().getConversationList(); - } + // final ctl = Get.find(); + // logger.e(ctl.chatList.toJson()); + // if (ctl.chatList.isEmpty) { + // Get.find().getConversationList(); + // } } if (index == 4) { myPageKey.currentState?.refreshData(); diff --git a/lib/main.dart b/lib/main.dart index 4d8de78..3b49e7e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:loopin/IM/controller/chat_controller.dart'; import 'package:loopin/IM/controller/tab_bar_controller.dart'; import 'package:loopin/IM/im_core.dart' as im_core; import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/controller/shop_index_controller.dart'; import 'package:loopin/service/http_config.dart'; import 'package:loopin/utils/common.dart'; import 'package:loopin/utils/lifecycle_handler.dart'; @@ -21,13 +22,14 @@ import 'package:shirne_dialog/shirne_dialog.dart'; import 'layouts/index.dart'; // 引入路由配置 import 'router/index.dart'; -// import 'utils/common.dart'; void main() async { // 注入tabbar - Get.put(TabBarController()); + Get.put(TabBarController(), permanent: true); // 注入会话列表 - Get.put(ChatController()); + Get.put(ChatController(), permanent: true); + // 注入底导菜单易选的 + Get.put(ShopIndexController(), permanent: true); // 监听app前后台状态 WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/models/conversation_type.dart b/lib/models/conversation_type.dart index e8e286a..4329638 100644 --- a/lib/models/conversation_type.dart +++ b/lib/models/conversation_type.dart @@ -1,4 +1,4 @@ -/// 枚举定义:所有的会话类型分组,用于一级消息分类 +/// 枚举定义:所有的会话类型分组,用于一级消息分类和路由地址 enum ConversationType { noFriend, // 陌生人消息 system, //系统消息 diff --git a/lib/models/notify_message.type.dart b/lib/models/notify_message.type.dart index 97ebfde..2830e8d 100644 --- a/lib/models/notify_message.type.dart +++ b/lib/models/notify_message.type.dart @@ -4,7 +4,7 @@ enum NotifyMessageType { systemNotify, // 系统->通知 systemReport, // 系统->举报下架(视频,视频评论) systemCheck, // 系统->审核结果(复审,驳回 ,通过) - systemPush, //系统->推广 + systemPush, //系统->推广类的 interactionComment, //互动->评论 interactionAt, //互动->视频评论中的@ interactionLike, //互动->点赞 @@ -18,6 +18,26 @@ enum NotifyMessageType { groupNotifyLeaveUp, // 群通知->群升级为达人群通知 } +/// 常量映射 +class NotifyMessageTypeConstants { + static const String newFoucs = 'newFoucs'; + static const String systemNotify = 'systemNotify'; + static const String systemReport = 'systemReport'; + static const String systemCheck = 'systemCheck'; + static const String systemPush = 'systemPush'; + static const String interactionComment = 'interactionComment'; + static const String interactionAt = 'interactionAt'; + static const String interactionLike = 'interactionLike'; + static const String interactionReply = 'interactionReply'; + static const String orderRecharge = 'orderRecharge'; + static const String orderPay = 'orderPay'; + static const String orderRefund = 'orderRefund'; + static const String groupNotifyCheck = 'groupNotifyCheck'; + static const String groupNotifyAccpet = 'groupNotifyAccpet'; + static const String groupNotifyFail = 'groupNotifyFail'; + static const String groupNotifyLeaveUp = 'groupNotifyLeaveUp'; +} + extension NotifyMessageTypeExtension on NotifyMessageType { String get name { switch (this) { diff --git a/lib/models/share_type.dart b/lib/models/share_type.dart new file mode 100644 index 0000000..97b2287 --- /dev/null +++ b/lib/models/share_type.dart @@ -0,0 +1,13 @@ +/// 枚举定义:所有分享相关的 +enum ShareType { video, shop } + +extension ShareTypeExtension on ShareType { + String get name { + switch (this) { + case ShareType.video: + return 'https://wuzhongjie.com.cn/video'; + case ShareType.shop: + return 'https://wuzhongjie.com.cn/shop'; + } + } +} diff --git a/lib/pages/auth/login.dart b/lib/pages/auth/login.dart index 6d44e59..ddf3662 100644 --- a/lib/pages/auth/login.dart +++ b/lib/pages/auth/login.dart @@ -72,36 +72,38 @@ class _LoginState extends State { String userId = obj['userId']; String userSig = obj['userSig']; // 初始化im_sdk - await im_core.ImCore.init(sdkAppId: 1600080789); + final initRes = await im_core.ImCore.init(sdkAppId: 1600080789); + if (initRes) { + // String userId = '1940667704585248769'; //13212279365 + // String userId = '1943510443312078850'; //18832510385 + // String userSig = + // 'eJwtjcEKgkAURf9l1iFPm*e8EdoYYUWFURAtg5nk5VRiEln0703q8p57Ofcj9qtd8LS1SEQUgBh1mY29NXzmDodaQhwrBRIJI0kq1sPsYcpTVbERSRgDAIEi3Tf2VXFtPUfEyFc9bfj6ZwrH4J1Ig4UL-6LX0ihyS7U5bi-Wzd8LzrK8TFs6TJ1sZwWGxlGas71PxPcHwH4y9Q__'; + // 'eJwtzLEKwjAUheF3ySwlNzXNbcHFxSIOaqTWUUgsF1FDG2tEfHdj2-F8P5wPO2x00tuWFUwknM2GTcbePV1oYEBMhQSeopxyZ65n58iwAjLOOXKF*VhscNTa6FJKEdOonm5-UxJQpZhN2lET3599Xllbv9ZBH2uHuDfvst5tG6FX0EFYVhpOpZ973z8W7PsDmYwyIw__'; - // String userId = '1940667704585248769'; //13212279365 - // String userId = '1943510443312078850'; //18832510385 - // String userSig = - // 'eJwtjcEKgkAURf9l1iFPm*e8EdoYYUWFURAtg5nk5VRiEln0703q8p57Ofcj9qtd8LS1SEQUgBh1mY29NXzmDodaQhwrBRIJI0kq1sPsYcpTVbERSRgDAIEi3Tf2VXFtPUfEyFc9bfj6ZwrH4J1Ig4UL-6LX0ihyS7U5bi-Wzd8LzrK8TFs6TJ1sZwWGxlGas71PxPcHwH4y9Q__'; - // 'eJwtzLEKwjAUheF3ySwlNzXNbcHFxSIOaqTWUUgsF1FDG2tEfHdj2-F8P5wPO2x00tuWFUwknM2GTcbePV1oYEBMhQSeopxyZ65n58iwAjLOOXKF*VhscNTa6FJKEdOonm5-UxJQpZhN2lET3599Xllbv9ZBH2uHuDfvst5tG6FX0EFYVhpOpZ973z8W7PsDmYwyIw__'; + try { + final loginRes = await ImService.instance.login(userID: userId, userSig: userSig); - try { - final loginRes = await ImService.instance.login(userID: userId, userSig: userSig); + if (loginRes.success) { + // 存储登录信息 + Storage.write('hasLogged', true); + Storage.write('userSig', userSig); + Storage.write('userId', userId); + Storage.write('token', obj['access_token']); + // 获取用户账户信息 + // final accountRes = await Http.get(CommonApi.accountInfo); + // logger.e(accountRes); + // 刷新短视频列表 + final videoController = Get.find(); + videoController.markNeedRefresh(); + dialogController.close(); - logger.i(loginRes); - if (loginRes.success) { - // 存储登录信息 - Storage.write('hasLogged', true); - Storage.write('userSig', userSig); - Storage.write('userId', userId); - Storage.write('token', obj['access_token']); - // 获取用户账户信息 - final accountRes = await Http.get('${CommonApi.accountInfo}'); - logger.i(accountRes); - // 刷新短视频列表 - final videoController = Get.find(); - videoController.markNeedRefresh(); - dialogController.close(); - - Get.back(); + Get.back(); + } + } catch (e) { + logger.i(e); } - } catch (e) { - logger.i(e); + } else { + logger.e('登陆异常:im初始化失败'); } } } diff --git a/lib/pages/chat/index.dart b/lib/pages/chat/index.dart index 9bb40e8..0c52f64 100644 --- a/lib/pages/chat/index.dart +++ b/lib/pages/chat/index.dart @@ -7,7 +7,9 @@ import 'package:get/get.dart'; import 'package:loopin/IM/controller/chat_controller.dart'; import 'package:loopin/IM/global_badge.dart'; import 'package:loopin/IM/im_service.dart'; +import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/components/scan_util.dart'; +import 'package:loopin/models/conversation_type.dart'; import 'package:loopin/models/conversation_view_model.dart'; import 'package:loopin/utils/parse_message_summary.dart'; import 'package:shirne_dialog/shirne_dialog.dart'; @@ -323,6 +325,10 @@ class ChatPageState extends State { 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'; + + logger.e(chatList[index].isCustomAdmin); return Ink( // color: chatList[index]['topMost'] == null ? Colors.white : Colors.grey[100], //置顶颜色 child: InkWell( @@ -334,35 +340,10 @@ class ChatPageState extends State { children: [ // 头图 ClipOval( - child: Builder( - builder: (context) { - final faceUrl = chatList[index].faceUrl; - final isNetwork = - faceUrl != null && faceUrl.isNotEmpty && (faceUrl.startsWith('http://') || faceUrl.startsWith('https://')); - if (isNetwork) { - return Image.network( - faceUrl, - height: 50.0, - width: 50.0, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Image.asset( - 'assets/images/pic1.jpg', - height: 50.0, - width: 50.0, - fit: BoxFit.cover, - ); - }, - ); - } else { - return Image.asset( - (faceUrl != null && faceUrl.isNotEmpty) ? faceUrl : 'assets/images/pic1.jpg', - height: 50.0, - width: 50.0, - fit: BoxFit.cover, - ); - } - }, + child: NetworkOrAssetImage( + imageUrl: chatList[index].faceUrl, + width: 50, + height: 50, ), ), @@ -371,17 +352,11 @@ class ChatPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Text( - // chatList[index].conversation.showName ?? '', - // style: const TextStyle(fontSize: 16.0), - // ), Text( - chatList[index].conversation.showName ?? '', + chatList[index].conversation.showName ?? '未知', style: TextStyle( - fontSize: (chatList[index].conversation.conversationGroupList?.isNotEmpty ?? false) ? 20 : 16, - fontWeight: - (chatList[index].conversation.conversationGroupList?.isNotEmpty ?? false) ? FontWeight.bold : FontWeight.normal, - ), + fontSize: (isAdmin || isNoFriend) ? 20 : 16, + fontWeight: (isAdmin || isNoFriend) ? FontWeight.bold : FontWeight.normal), ), const SizedBox(height: 2.0), Text( @@ -400,7 +375,7 @@ class ChatPageState extends State { crossAxisAlignment: CrossAxisAlignment.end, children: [ Visibility( - visible: chatList[index].conversation.lastMessage?.timestamp != null, + visible: !(isAdmin || isNoFriend), child: Text( // 转成日期字符串显示 DateTime.fromMillisecondsSinceEpoch( @@ -418,19 +393,25 @@ class ChatPageState extends State { ], ), Visibility( - visible: chatList[index].isCustomAdmin != '0', + visible: (isAdmin || isNoFriend), child: const Icon( Icons.arrow_forward_ios, - color: Colors.grey, - size: 12.0, + color: Colors.blueGrey, + size: 14.0, ), ), ], ), ), onTap: () { - if (chatList[index].isCustomAdmin != '0') { - // 跳转系统消息级别页面 + 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); diff --git a/lib/pages/chat/notify/groupNotify.dart b/lib/pages/chat/notify/groupNotify.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/pages/chat/notify/interaction.dart b/lib/pages/chat/notify/interaction.dart new file mode 100644 index 0000000..a0f6ba4 --- /dev/null +++ b/lib/pages/chat/notify/interaction.dart @@ -0,0 +1,368 @@ +/// 聊天首页模板 +library; + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:get/get.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/models/conversation_type.dart'; +import 'package:loopin/models/notify_message.type.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_message.dart'; + +// interactionComment, //互动->评论 +// interactionAt, //互动->视频评论中的@ +// interactionLike, //互动->点赞 +// interactionReply, //互动->评论回复 + +class Interaction extends StatefulWidget { + const Interaction({super.key}); + + @override + State createState() => InteractionState(); +} + +class InteractionState extends State with SingleTickerProviderStateMixin { + bool isLoading = false; // 是否在加载中 + bool hasMore = true; // 是否还有更多数据 + final RxBool _throttleFlag = false.obs; // 滚动节流锁 + final ScrollController chatController = ScrollController(); + String page = ''; + + ///------------------- + V2TimConversation? conv; + RxList msgList = [].obs; + final RxString selectedMessageType = '全部'.obs; + final List> messageFilters = [ + {'value': 'all', 'label': '全部', 'icon': Icons.all_inclusive}, + {'value': NotifyMessageTypeConstants.interactionLike, 'label': '点赞', 'icon': Icons.favorite_border}, + {'value': NotifyMessageTypeConstants.interactionComment, 'label': '评论', 'icon': Icons.comment}, + {'value': NotifyMessageTypeConstants.interactionAt, 'label': '@我', 'icon': Icons.alternate_email}, + {'value': NotifyMessageTypeConstants.interactionReply, 'label': '回复', 'icon': Icons.reply}, + ]; + RxList demoList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].obs; + + @override + void initState() { + super.initState(); + if (Get.arguments != null && Get.arguments is V2TimConversation) { + // 如果有参数 + conv = Get.arguments as V2TimConversation; + final lastmsg = conv?.lastMessage; + if (lastmsg != null) { + msgList.add(lastmsg); + } + } + chatController.addListener(() { + if (_throttleFlag.value) return; + if (chatController.position.pixels >= chatController.position.maxScrollExtent - 50) { + _throttleFlag.value = true; + getMsgData().then((_) { + // 解锁 + Future.delayed(Duration(milliseconds: 1000), () { + _throttleFlag.value = false; + }); + }); + } + }); + } + + @override + void dispose() { + super.dispose(); + chatController.dispose(); + } + + // 分页获取全部数据 + Future getMsgData() async { + // 获取最旧一条消息作为游标 + V2TimMessage? lastRealMsg; + lastRealMsg = msgList.last; + final res = await ImService.instance.getHistoryMessageList( + userID: ConversationType.interaction.name, // userID为固定的interaction + lastMsg: lastRealMsg, + ); + if (res.success && res.data != null) { + msgList.addAll(res.data!); + if (res.data!.isEmpty) { + hasMore = false; + } + logger.i('聊天数据加载成功'); + } else { + logger.e('聊天数据加载失败:${res.desc}'); + } + } + + // 下拉刷新 + Future handleRefresh() async { + await Future.delayed(Duration(seconds: 5)); + setState(() {}); + } + + // 获取不同的消息分类数据 + void _filterMessages(String filterType) async { + logger.e(filterType); + final res = await ImService.instance.searchLocalMessages( + page: page, + conversationID: 'c2c_${ConversationType.interaction.name}', + keywordList: ['action', filterType], + ); + logger.e(res.data!.toLogString()); + if (res.success && res.data != null) { + final resultList = res.data?.messageSearchResultItems ?? []; + if (resultList.isNotEmpty) { + for (var item in resultList) { + logger.e(item.toJson()); + msgList.addAll(item.messageList ?? []); + } + } else { + logger.e('数据为空${res.desc}'); + } + } + } + + // 下拉选择 + void _showFilterMenu(BuildContext context) { + final double screenWidth = MediaQuery.of(context).size.width; + final double statusBarHeight = MediaQuery.of(context).padding.top; + + showMenu( + context: context, + position: RelativeRect.fromLTRB( + 0, + kToolbarHeight + statusBarHeight + 1, + 0, + 0, + ), + elevation: 8, + color: Colors.white, + constraints: BoxConstraints( + minWidth: screenWidth, + maxWidth: screenWidth, + ), + useRootNavigator: true, //移除默认的内边距 + items: messageFilters.map((filter) { + return PopupMenuItem( + value: filter['value'], + padding: EdgeInsets.zero, // 移除菜单项的内边距 + child: Container( + width: screenWidth, + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 16), + decoration: BoxDecoration( + border: Border( + bottom: filter['value'] != messageFilters.last['value'] ? BorderSide(color: Colors.grey[200]!) : BorderSide.none, + ), + ), + child: Row( + children: [ + Icon(filter['icon'], size: 22, color: Colors.grey[700]), + SizedBox(width: 16), + Expanded( + child: Text( + filter['label'], + style: TextStyle( + fontSize: 16, + color: selectedMessageType.value == filter['label'] ? Colors.orange : Colors.black, + ), + ), + ), + if (selectedMessageType.value == filter['label']) Icon(Icons.check, size: 20, color: Colors.orange), + ], + ), + ), + ); + }).toList(), + ).then((value) { + if (value != null) { + final selectedFilter = messageFilters.firstWhere((f) => f['value'] == value); + selectedMessageType.value = selectedFilter['label']; + _filterMessages(value); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + centerTitle: true, + forceMaterialTransparency: true, + bottom: PreferredSize( + preferredSize: Size.fromHeight(1.0), + child: Container( + color: Colors.grey[300], + height: 1.0, + ), + ), + title: Obx( + () => GestureDetector( + onTap: () => _showFilterMenu(context), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: 4), + Text( + selectedMessageType.value, + style: TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Icon(Icons.arrow_drop_down, color: Colors.black, size: 20), + ], + ), + ), + ), + ), + actions: [], + ), + body: ScrollConfiguration( + behavior: CustomScrollBehavior().copyWith(scrollbars: false), + child: Column( + children: [ + Expanded( + child: RefreshIndicator( + backgroundColor: Colors.white, + color: Color(0xFFFF5000), + displacement: 10.0, + onRefresh: handleRefresh, + child: Obx(() { + return ListView.builder( + controller: chatController, + shrinkWrap: true, + physics: BouncingScrollPhysics(), + // itemCount: msgList.length, + itemCount: demoList.length, + itemBuilder: (context, index) { + //检测cloudCustomData + // interactionComment, //互动->评论 + // interactionAt, //互动->视频评论中的@ + // interactionLike, //互动->点赞 + // interactionReply, //互动->评论回复 + + //---- + // final element =msgList[index].customElem!; + // final cloudCustomData = msgList[index].cloudCustomData; + // final desc = msgList[index].customElem!.desc!; + // final jsonData = msgList[index].customElem!.data; + + // ----测试数据 + final jsonData = '{"faceUrl":"","nickName":"测试昵称","userID":"213213"}'; + final item = jsonDecode(jsonData); // 数据 + final desc = '测试desc'; + final cloudCustomData = 'interactionLike'; + V2TimMessage element = V2TimMessage(elemType: 2, isRead: index > 2 ? true : false); + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), + child: Row( + spacing: 10.0, + children: [ + // 头像 + InkWell( + onTap: () async { + // 点击头像转到对方主页 + // 先获取视频详情 + // 如果cloudCustomData是interactionComment,interactionAt,interactionReply,传参时带上评论id, + // + // final res = await Http.get('${VideoApi.detail}/${item['vlogID']}'); + // Get.toNamed('/vloger', arguments: res['data']); + Get.toNamed('/vloger'); + }, + child: ClipOval( + child: NetworkOrAssetImage( + imageUrl: item['faceUrl'], + width: 50, + height: 50, + ), + ), + ), + + // 消息 + Expanded( + child: InkWell( + onTap: () { + // 点击头像转到对方主页 + // 先获取视频详情 + // 如果cloudCustomData是interactionComment,interactionAt,interactionReply,传参时带上评论id, + // + // final res = await Http.get('${VideoApi.detail}/${item['vlogID']}'); + // Get.toNamed('/vloger', arguments: res['data']); + Get.toNamed('/vloger'); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 昵称 + Text( + item['nickName'] ?? '未知', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + const SizedBox(height: 2.0), + // 描述内容 + Text( + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: Colors.grey, fontSize: 12.0), + // desc, + '很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容', + ), + Text( + Utils.formatTime(element.timestamp ?? DateTime.now().millisecondsSinceEpoch ~/ 1000), + style: TextStyle( + color: Colors.grey[600], + fontSize: 12, + ), + ), + ], + ), + ), + ), + + // 右侧 + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Visibility( + visible: true, + // 视频首图 + child: NetworkOrAssetImage( + imageUrl: item['firstFrameImg'], + placeholderAsset: 'assets/images/bk.jpg', + width: 40, + height: 60, + ), + ), + const SizedBox(width: 5.0), + // 角标 + Visibility( + visible: !(element.isRead ?? true), + child: FStyle.badge(0, isdot: true), + ), + ], + ), + ], + ), + ); + }, + ); + })), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/chat/notify/newFoucs.dart b/lib/pages/chat/notify/newFoucs.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/pages/chat/notify/noFriend.dart b/lib/pages/chat/notify/noFriend.dart new file mode 100644 index 0000000..72ec762 --- /dev/null +++ b/lib/pages/chat/notify/noFriend.dart @@ -0,0 +1,235 @@ +/// 聊天首页模板 +library; + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:get/get.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/models/conversation_type.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_conversation_filter.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; + +// noFriend, //陌生人消息 + +class Nofriend extends StatefulWidget { + const Nofriend({super.key}); + + @override + State createState() => NofriendState(); +} + +class NofriendState extends State with SingleTickerProviderStateMixin { + bool isLoading = false; // 是否在加载中 + bool hasMore = true; // 是否还有更多数据 + final RxBool _throttleFlag = false.obs; // 滚动节流锁 + final ScrollController chatController = ScrollController(); + int page = 1; + + ///------------------- + RxList convList = [].obs; + + RxList demoList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].obs; + + @override + void initState() { + super.initState(); + getMsgData(); + chatController.addListener(() { + if (_throttleFlag.value) return; + if (chatController.position.pixels >= chatController.position.maxScrollExtent - 50) { + _throttleFlag.value = true; + getMsgData().then((_) { + // 解锁 + Future.delayed(Duration(milliseconds: 1000), () { + _throttleFlag.value = false; + }); + }); + } + }); + } + + @override + void dispose() { + super.dispose(); + chatController.dispose(); + } + + // 分页获取全部数据 + Future getMsgData() async { + final res = await ImService.instance.getConversationListByFilter( + filter: V2TimConversationFilter(conversationGroup: ConversationType.noFriend.name), + nextSeq: page, + ); + logger.i('获取会话数据成功:${res.data!.toJson()}'); + if (res.success && res.data != null) { + final newList = res.data!.conversationList ?? []; + final isFinished = res.data!.isFinished ?? true; + convList.addAll(newList); + if (isFinished) { + hasMore = false; + // 加载没数据了 + page = int.parse(res.data!.nextSeq ?? '0'); + } else { + page++; + } + logger.i('获取会话数据成功:$newList'); + } else { + logger.e('获取会话数据失败:${res.data!.isFinished}'); + } + } + + // 下拉刷新 + Future handleRefresh() async { + await Future.delayed(Duration(seconds: 5)); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + centerTitle: true, + forceMaterialTransparency: true, + bottom: PreferredSize( + preferredSize: Size.fromHeight(1.0), + child: Container( + color: Colors.grey[300], + height: 1.0, + ), + ), + title: Text( + '陌生人消息', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + ), + actions: [], + ), + body: ScrollConfiguration( + behavior: CustomScrollBehavior().copyWith(scrollbars: false), + child: Column( + children: [ + Expanded( + child: RefreshIndicator( + backgroundColor: Colors.white, + color: Color(0xFFFF5000), + displacement: 10.0, + onRefresh: handleRefresh, + child: Obx(() { + return ListView.builder( + controller: chatController, + shrinkWrap: true, + physics: BouncingScrollPhysics(), + // itemCount: convList.length, + itemCount: demoList.length, + itemBuilder: (context, index) { + // ----测试数据 + final jsonData = '{"faceUrl":"","nickName":"测试昵称","userID":"213213"}'; + final item = jsonDecode(jsonData); // 数据 + final desc = '测试desc'; + final cloudCustomData = 'interactionLike'; + V2TimMessage element = V2TimMessage(elemType: 2, isRead: index > 2 ? true : false); + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), + child: Row( + spacing: 10.0, + children: [ + // 头像 + InkWell( + onTap: () async { + // 点击头像转到对方主页 + // 先获取视频详情 + // 如果cloudCustomData是interactionComment,interactionAt,interactionReply,传参时带上评论id, + // + // final res = await Http.get('${VideoApi.detail}/${item['vlogID']}'); + // Get.toNamed('/vloger', arguments: res['data']); + Get.toNamed('/vloger'); + }, + child: ClipOval( + child: NetworkOrAssetImage( + imageUrl: item['faceUrl'], + width: 50, + height: 50, + ), + ), + ), + + // 消息 + Expanded( + child: InkWell( + onTap: () { + // 点击头像转到对应的聊天 + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 昵称 + Text( + item['nickName'] ?? '未知', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ), + ), + const SizedBox(height: 2.0), + // 描述内容 + Text( + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: Colors.grey, fontSize: 12.0), + // desc, + '很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容很长文本内容', + ), + Text( + Utils.formatTime(element.timestamp ?? DateTime.now().millisecondsSinceEpoch ~/ 1000), + style: TextStyle( + color: Colors.grey[600], + fontSize: 12, + ), + ), + ], + ), + ), + ), + + // 右侧 + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Visibility( + visible: true, + // 视频首图 + child: NetworkOrAssetImage( + imageUrl: item['firstFrameImg'], + placeholderAsset: 'assets/images/bk.jpg', + width: 40, + height: 60, + ), + ), + const SizedBox(width: 5.0), + // 角标 + Visibility( + visible: !(element.isRead ?? true), + child: FStyle.badge(0, isdot: true), + ), + ], + ), + ], + ), + ); + }, + ); + })), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/chat/notify/order.dart b/lib/pages/chat/notify/order.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/pages/chat/notify/system.dart b/lib/pages/chat/notify/system.dart new file mode 100644 index 0000000..f8472ce --- /dev/null +++ b/lib/pages/chat/notify/system.dart @@ -0,0 +1,444 @@ +/// 聊天首页模板 +library; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:loopin/IM/controller/chat_controller.dart'; +import 'package:loopin/IM/global_badge.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/components/scan_util.dart'; +import 'package:loopin/models/conversation_type.dart'; +import 'package:loopin/models/conversation_view_model.dart'; +import 'package:loopin/styles/index.dart'; +import 'package:loopin/utils/parse_message_summary.dart'; +import 'package:shirne_dialog/shirne_dialog.dart'; + +// systemNotify, // 系统->通知 +// systemReport, // 系统->举报下架(视频,视频评论) +// systemCheck, // 系统->审核结果(复审,驳回 ,通过) +// systemPush, //系统->推广类的 + +class System extends StatefulWidget { + const System({super.key}); + + @override + State createState() => SystemState(); +} + +class SystemState extends State { + late final ChatController controller; + + @override + void initState() { + super.initState(); + controller = Get.find(); + } + + void deletConv(context, ConversationViewModel item) async { + final res = await ImService.instance.deleteConversation(conversationID: item.conversation.conversationID); + if (res.success) { + Navigator.of(context).pop(); + controller.chatList.remove(item); + } + } + + // 长按坐标点 + double posDX = 0.0; + double posDY = 0.0; + + // 下拉刷新 + Future handleRefresh() async { + await Future.delayed(Duration(seconds: 1)); + setState(() {}); + } + + // 长按菜单 + void showContextMenu(BuildContext context, ConversationViewModel item) { + bool isLeft = posDX > MediaQuery.of(context).size.width / 2 ? false : true; + bool isTop = posDY > MediaQuery.of(context).size.height / 2 ? false : true; + + showDialog( + context: context, + barrierColor: Colors.black12, // 遮罩透明 + builder: (context) { + return Stack( + children: [ + Positioned( + top: isTop ? posDY : posDY - 135, + left: isLeft ? posDX : posDX - 135, + width: 135, + child: Material( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), + color: Colors.white, + elevation: 2.0, + clipBehavior: Clip.hardEdge, + child: Column( + children: [ + ListTile( + title: const Text( + '设为免打扰', + style: TextStyle(color: Colors.black87, fontSize: 14.0), + ), + dense: true, + onTap: () {}, + ), + ListTile( + title: const Text( + '置顶消息', + style: TextStyle(color: Colors.black87, fontSize: 14.0), + ), + dense: true, + onTap: () {}, + ), + ListTile( + title: const Text( + '不显示该消息', + style: TextStyle(color: Colors.black87, fontSize: 14.0), + ), + dense: true, + onTap: () {}, + ), + ListTile( + title: const Text( + '删除', + style: TextStyle(color: Colors.black87, fontSize: 14.0), + ), + dense: true, + onTap: () { + deletConv(context, item); + }, + ) + ], + ), + ), + ) + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: AppBar( + forceMaterialTransparency: true, + title: Row( + spacing: 8.0, + children: [ + Text('消息'), + Container( + padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 4.0), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(20.0), boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(20), + offset: Offset(0.0, 1.0), + blurRadius: 2.0, + spreadRadius: 0.0, + ), + ]), + child: InkWell( + onTap: () async { + if (Get.find().totalUnread > 0) { + final res = await ImService.instance.clearConversationUnreadCount(conversationID: ''); + if (res.success) { + MyDialog.toast('操作成功', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.green.withAlpha(200))); + } else { + MyDialog.toast(res.desc, icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); + } + } + }, + child: Row( + spacing: 3.0, + children: [ + Icon( + Icons.cleaning_services_sharp, + size: 14.0, + ), + Text( + '清除未读', + style: TextStyle(fontSize: 12.0), + ) + ], + ), + )), + ], + ), + actions: [ + /// 先不做搜索功能了后面再说 + // IconButton( + // icon: const Icon(Icons.search), + // onPressed: () {}, + // ), + IconButton( + icon: const Icon(Icons.add_circle_outline), + 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: 'group', + child: Row( + children: [ + Icon(Icons.group, color: Colors.white, size: 18), + SizedBox(width: 8), + Text( + '发起群聊', + style: TextStyle(color: Colors.white), + ), + ], + ), + ), + PopupMenuItem( + value: 'friend', + child: Row( + children: [ + Icon(Icons.person_add, color: Colors.white, size: 18), + SizedBox(width: 8), + Text( + '添加朋友', + style: TextStyle(color: Colors.white), + ), + ], + ), + ), + PopupMenuItem( + value: 'scan', + child: Row( + children: [ + Icon(Icons.qr_code_scanner, color: Colors.white, size: 18), + SizedBox(width: 8), + Text( + '扫一扫', + style: TextStyle(color: Colors.white), + ), + ], + ), + ), + ], + ); + + if (selected != null) { + switch (selected) { + case 'group': + print('点击了发起群聊'); + break; + case 'friend': + print('点击了添加朋友'); + break; + case 'scan': + print('点击了扫一扫'); + ScanUtil.openScanner(onResult: (code) { + print('扫码结果:$code'); + Get.snackbar('扫码成功', code); + // 在这里继续你的业务逻辑,比如跳转页面、请求接口等 + }); + break; + } + } + }, + ), + ], + ), + body: ScrollConfiguration( + behavior: CustomScrollBehavior().copyWith(scrollbars: false), + child: Column( + children: [ + Container( + margin: EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0), + padding: EdgeInsets.all(10.0), + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [ + BoxShadow( + color: Colors.black.withAlpha(10), + offset: Offset(0.0, 1.0), + blurRadius: 2.0, + spreadRadius: 0.0, + ), + ]), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Column( + children: [ + SvgPicture.asset( + 'assets/images/svg/order.svg', + height: 36.0, + width: 36.0, + ), + Text('群聊'), + ], + ), + 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('粉丝'), + ], + ), + 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; + + return ListView.builder( + shrinkWrap: true, + physics: BouncingScrollPhysics(), + itemCount: chatList.length, + itemBuilder: (context, index) { + 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: (chatList[index].conversation.conversationGroupList!.contains(ConversationType.noFriend.name)) || + chatList[index].isCustomAdmin != '0' + ? 20 + : 16, + fontWeight: (chatList[index].conversation.conversationGroupList!.contains(ConversationType.noFriend.name)) || + chatList[index].isCustomAdmin != '0' + ? 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, + children: [ + Visibility( + visible: !(chatList[index].isCustomAdmin != '0' || + chatList[index].conversation.conversationGroupList!.contains(ConversationType.noFriend.name)), + child: Text( + // 转成日期字符串显示 + DateTime.fromMillisecondsSinceEpoch( + (chatList[index].conversation.lastMessage!.timestamp ?? 0) * 1000, + ).toLocal().toString().substring(0, 16), // 简单截取年月日时分 + 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: chatList[index].isCustomAdmin != '0' || + chatList[index].conversation.conversationGroupList!.contains(ConversationType.noFriend.name), + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.blueGrey, + size: 14.0, + ), + ), + ], + ), + ), + onTap: () { + if (conversationTypeFromString(chatList[index].isCustomAdmin)) { + // 跳转对应的通知消息页 + logger.e(chatList[index].isCustomAdmin); + } else if (chatList[index].conversation.conversationGroupList!.contains(ConversationType.noFriend.name)) { + // 跳转陌生人消息页面 + logger.e(chatList[index].conversation.conversationGroupList); + } 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/goods/detail.dart b/lib/pages/goods/detail.dart index 5c00772..0987c77 100644 --- a/lib/pages/goods/detail.dart +++ b/lib/pages/goods/detail.dart @@ -13,6 +13,7 @@ import 'package:loopin/IM/im_service.dart'; import 'package:loopin/api/shop_api.dart'; import 'package:loopin/components/my_toast.dart'; import 'package:loopin/components/network_or_asset_image.dart'; +import 'package:loopin/models/share_type.dart'; import 'package:loopin/models/summary_type.dart'; import 'package:loopin/service/http.dart'; import 'package:loopin/utils/index.dart'; @@ -80,10 +81,10 @@ class _GoodsState extends State { final description = shopObj['describe']; // 商品描述 if (index == 1) { // 好友 - Wxsdk.shareToFriend(title: '快看看我分享的商品', description: description, webpageUrl: 'https://baidu.com'); + Wxsdk.shareToFriend(title: '快看看我分享的商品', description: description, webpageUrl: '${ShareType.shop.name}?id=${shopObj['id']}'); } else if (index == 2) { // 朋友圈 - Wxsdk.shareToTimeline(title: '快看看我分享的商品', webpageUrl: 'https://baidu.com'); + Wxsdk.shareToTimeline(title: '快看看我分享的商品', webpageUrl: '${ShareType.shop.name}?id=${shopObj['id']}'); } } @@ -551,9 +552,9 @@ class _GoodsState extends State { // ), GestureDetector( onTap: () async { - // 可以在这里打开聊天、拨打电话等 + // 可以在这里打开聊天 logger.i('联系客服'); - final res = await ImService.instance.getConversation(conversationID: 'c2c_${shopObj['shoperId']}'); + final res = await ImService.instance.getConversation(conversationID: 'c2c_${shopObj['tenantId']}'); V2TimConversation conversation = res.data; logger.i(conversation.toLogString()); if (res.success) { @@ -619,9 +620,16 @@ class _GoodsState extends State { alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 20.0), color: Color(0xFFFF5000), - child: Text( - '立即购买', - style: TextStyle(color: Colors.white, fontSize: 14.0), + child: GestureDetector( + onTap: () { + // 这里走生成预支付订单,拿到orderId + String orderId = '1958380183857659904'; //测试数据 + Get.toNamed('/order/detail', arguments: orderId); + }, + child: Text( + '立即购买', + style: TextStyle(color: Colors.white, fontSize: 14.0), + ), ), ), ], diff --git a/lib/pages/index/index.dart b/lib/pages/index/index.dart index f04c58d..9eaee83 100644 --- a/lib/pages/index/index.dart +++ b/lib/pages/index/index.dart @@ -8,6 +8,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:get/get.dart'; import 'package:loopin/components/backtop.dart'; import 'package:loopin/components/loading.dart'; +import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/controller/shop_index_controller.dart'; import 'package:loopin/utils/index.dart'; @@ -38,7 +39,7 @@ class _IndexPageState extends State with SingleTickerProviderStateMix // } // ]; final ScrollController pageScrollController = ScrollController(); - final ShopIndexController controller = Get.put(ShopIndexController()); + late ShopIndexController controller; // 瀑布流卡片 Widget cardList(item) { @@ -55,7 +56,12 @@ class _IndexPageState extends State with SingleTickerProviderStateMix ]), child: Column( children: [ - Image.network('${item['pic']}'), + // Image.network(), + NetworkOrAssetImage( + imageUrl: '${item['pic']}', + width: double.infinity, + placeholderAsset: 'assets/images/bk.jpg', + ), Container( padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0), child: Column( @@ -106,109 +112,121 @@ class _IndexPageState extends State with SingleTickerProviderStateMix @override void initState() { super.initState(); - controller.initTabs(); + controller = Get.find(); + controller.initTabs(vsync: this); } @override Widget build(BuildContext context) { - return Obx(() { - final tabIndex = controller.currentTabIndex.value; - final currentTab = controller.tabs[tabIndex]; + return Scaffold( + backgroundColor: Colors.grey[50], + body: Column( + children: [ + _buildTopSection(), + // 内容区域 + Expanded( + child: controller.tabController == null + ? const Center(child: CircularProgressIndicator()) + : TabBarView( + controller: controller.tabController, + children: List.generate( + controller.tabList.length, + (index) => _buildTabContent(index), + ), + ), + ), + ], + ), + floatingActionButton: Obx(() { + final tabIndex = controller.currentTabIndex.value; + final currentTab = controller.tabs[tabIndex]; + if (currentTab == null) return const SizedBox.shrink(); - return Scaffold( - backgroundColor: Colors.grey[50], - body: Column( - children: [ - // 顶部固定区域(轮播图 + TabBar) - _buildTopSection(), - - // 内容区域 - Expanded( - child: TabBarView( - controller: controller.tabController, - children: controller.tabList.asMap().entries.map((entry) { - final index = entry.key; - return _buildTabContent(index); - }).toList(), - ), - ), - ], - ), - floatingActionButton: currentTab != null - ? Backtop( - controller: currentTab.scrollController, - offset: currentTab.scrollOffset.value, - ) - : null, - ); - }); + return Backtop( + controller: currentTab.scrollController, + offset: currentTab.scrollOffset.value, + ); + }), + ); } // 构建顶部固定区域 Widget _buildTopSection() { - double screenWidth = MediaQuery.of(context).size.width; - int tabCount = controller.tabList.length; - // 每个 Tab 的最小宽度 - double minTabWidth = 80; - // 是否可滚动 - bool isScrollable = tabCount * minTabWidth > screenWidth; + if (controller.tabController == null) { + return SizedBox(); + } + return Column( children: [ // 轮播图 - Container( - width: double.infinity, - height: 240, - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Color(0xFFFF5000), Color(0xFFfcaec4)], - ), - ), - child: controller.swiperData.length <= 1 - ? (controller.swiperData.isNotEmpty - ? Image.network( - controller.swiperData.first['images'] ?? '', - fit: BoxFit.fill, - ) - : const SizedBox.shrink()) - : Swiper( - itemCount: controller.swiperData.length, - autoplay: true, - loop: true, - pagination: SwiperPagination( - builder: DotSwiperPaginationBuilder( - color: Colors.white70, - activeColor: Colors.white, - ), - ), - itemBuilder: (context, index) { - final imageUrl = controller.swiperData[index]['images'] ?? ''; - return imageUrl.isNotEmpty ? Image.network(imageUrl, fit: BoxFit.fill) : const SizedBox.shrink(); - }, + Obx(() { + if (controller.swiperData.isEmpty) return const SizedBox.shrink(); + if (controller.swiperData.length == 1) { + final imageUrl = controller.swiperData.first['images'] ?? ''; + return Image.network(imageUrl, fit: BoxFit.fill, width: double.infinity, height: 240); + } + return SizedBox( + width: double.infinity, + height: 240, + child: Swiper( + itemCount: controller.swiperData.length, + autoplay: true, + loop: true, + pagination: const SwiperPagination( + builder: DotSwiperPaginationBuilder( + color: Colors.white70, + activeColor: Colors.white, ), - ), + ), + itemBuilder: (context, index) { + final imageUrl = controller.swiperData[index]['images'] ?? ''; + return imageUrl.isNotEmpty ? Image.network(imageUrl, fit: BoxFit.fill) : const SizedBox.shrink(); + }, + ), + ); + }), // TabBar - Container( - color: Colors.white, - child: TabBar( - controller: controller.tabController, - tabs: controller.tabList.map((item) { - return Tab( - child: Text(item['name'], style: const TextStyle(fontWeight: FontWeight.bold)), - ); - }).toList(), - isScrollable: isScrollable, - overlayColor: WidgetStateProperty.all(Colors.transparent), - unselectedLabelColor: Colors.black87, - labelColor: Color.fromARGB(255, 236, 108, 49), - indicator: const UnderlineTabIndicator(borderSide: BorderSide(color: Color.fromARGB(255, 236, 108, 49), width: 2.0)), - unselectedLabelStyle: const TextStyle(fontSize: 16.0, fontFamily: 'Microsoft YaHei'), - labelStyle: const TextStyle(fontSize: 18.0, fontFamily: 'Microsoft YaHei', fontWeight: FontWeight.bold), - dividerHeight: 0, - ), - ), + Obx(() { + int tabCount = controller.tabList.length; + double screenWidth = MediaQuery.of(context).size.width; + double minTabWidth = 60; + bool isScrollable = tabCount * minTabWidth > screenWidth; + + return Container( + color: Colors.white, + child: TabBar( + controller: controller.tabController, + tabs: controller.tabList.map((item) { + return Tab( + child: Text( + item['name'] ?? '', + style: const TextStyle(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + softWrap: false, + ), + ); + }).toList(), + isScrollable: isScrollable, + overlayColor: WidgetStateProperty.all(Colors.transparent), + unselectedLabelColor: Colors.black87, + labelColor: const Color.fromARGB(255, 236, 108, 49), + indicator: const UnderlineTabIndicator( + borderSide: BorderSide(color: Color.fromARGB(255, 236, 108, 49), width: 2.0), + ), + unselectedLabelStyle: const TextStyle( + fontSize: 14.0, + fontFamily: 'Microsoft YaHei', + ), + labelStyle: const TextStyle( + fontSize: 16.0, + fontFamily: 'Microsoft YaHei', + fontWeight: FontWeight.bold, + ), + dividerHeight: 0, + ), + ); + }), ], ); } diff --git a/lib/pages/index/indexcopy.dart b/lib/pages/index/indexcopy.dart index 5d0eb6a..2d51ea7 100644 --- a/lib/pages/index/indexcopy.dart +++ b/lib/pages/index/indexcopy.dart @@ -1,8 +1,6 @@ /// 首页模板 library; -import 'dart:ui'; - import 'package:card_swiper/card_swiper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; @@ -118,7 +116,7 @@ class _IndexPageState extends State with SingleTickerProviderStateMix @override void initState() { super.initState(); - controller.initTabs(); + controller.initTabs(vsync: this); } @override diff --git a/lib/pages/my/index.dart b/lib/pages/my/index.dart index eed230a..a378963 100644 --- a/lib/pages/my/index.dart +++ b/lib/pages/my/index.dart @@ -21,7 +21,7 @@ class PageParams { int page; int pageSize; bool isLoading; - bool hasMore; + RxBool hasMore; int total; bool isInitLoading; @@ -29,16 +29,16 @@ class PageParams { this.page = 1, this.pageSize = 10, this.isLoading = false, - this.hasMore = true, + bool hasMore = true, this.total = 0, this.isInitLoading = true, - }); + }) : hasMore = hasMore.obs; void init() { page = 1; pageSize = 10; isLoading = false; - hasMore = true; + hasMore.value = true; total = 0; isInitLoading = true; } @@ -86,7 +86,6 @@ class MyPageState extends State with SingleTickerProviderStateMixin { @override void initState() { super.initState(); - initControllers(); scrollListener = () { @@ -95,9 +94,9 @@ class MyPageState extends State with SingleTickerProviderStateMixin { if (!isNearBottom) return; - if (currentTabIndex.value == 0 && !itemsParams.isLoading && itemsParams.hasMore) { + if (currentTabIndex.value == 0 && !itemsParams.isLoading && itemsParams.hasMore.value) { loadData(0); - } else if (currentTabIndex.value == 1 && !favoriteParams.isLoading && favoriteParams.hasMore) { + } else if (currentTabIndex.value == 1 && !favoriteParams.isLoading && favoriteParams.hasMore.value) { loadData(1); } }; @@ -145,62 +144,58 @@ class MyPageState extends State with SingleTickerProviderStateMixin { Future loadData([int? tabIndex]) async { final index = tabIndex ?? currentTabIndex.value; if (index == 0) { - if (itemsParams.isLoading || !itemsParams.hasMore) return; + if (itemsParams.isLoading || !itemsParams.hasMore.value) return; itemsParams.isLoading = true; // itemsParams.isInitLoading = true; - try { - final res = await Http.post(VideoApi.myPublicList, data: { - "userId": imUserInfoController?.userID.value, - "yesOrNo": 0, - "current": itemsParams.page, - "size": itemsParams.pageSize, - }); - final obj = res['data']; - final total = obj['total']; - final row = obj['rows']; - logger.i(res['data']); - // 判断是否还有更多数据 - if (items.length >= total) { - itemsParams.hasMore = false; - } - // 添加新数据,触发响应式更新 - items.addAll(row); - - // 页码加一 - itemsParams.page++; - } finally { - itemsParams.isLoading = false; - itemsParams.isInitLoading = false; + final res = await Http.post(VideoApi.myPublicList, data: { + "userId": imUserInfoController?.userID.value, + "yesOrNo": 0, + "current": itemsParams.page, + "size": itemsParams.pageSize, + }); + final obj = res['data']; + final total = obj['total']; + final row = obj['records'] ?? []; + logger.i(res['data']); + // 判断是否还有更多数据 + logger.e(items.length); + // 添加新数据,触发响应式更新 + items.addAll(row); + logger.e(obj); + if (items.length >= total) { + itemsParams.hasMore.value = false; } + // 页码加一 + itemsParams.page++; + // + itemsParams.isLoading = false; + itemsParams.isInitLoading = false; } else if (index == 1) { - if (favoriteParams.isLoading || !favoriteParams.hasMore) return; + if (favoriteParams.isLoading || !favoriteParams.hasMore.value) return; favoriteParams.isLoading = true; // favoriteParams.isInitLoading = true; - try { - final res = await Http.post(VideoApi.myPublicList, data: { - "userId": imUserInfoController?.userID.value, - "yesOrNo": 0, - "current": itemsParams.page, - "size": itemsParams.pageSize, - }); - final obj = res['data']; - final total = obj['total']; - final row = obj['rows']; + final res = await Http.post(VideoApi.myLikedList, data: { + "userId": imUserInfoController?.userID.value, + "yesOrNo": 0, + "current": favoriteParams.page, + "size": favoriteParams.pageSize, + }); + final obj = res['data']; + final total = obj['total']; + final row = obj['records'] ?? []; + favoriteItems.addAll(row); - if (favoriteItems.length >= total) { - itemsParams.hasMore = false; - } - - favoriteItems.addAll(row); - favoriteParams.page++; - } finally { - favoriteParams.isLoading = false; - favoriteParams.isInitLoading = false; + if (favoriteItems.length >= total) { + favoriteParams.hasMore.value = false; } + + favoriteParams.page++; + favoriteParams.isLoading = false; + favoriteParams.isInitLoading = false; } } @@ -237,7 +232,11 @@ class MyPageState extends State with SingleTickerProviderStateMixin { // 获取当前登录用户基本信息 void selfInfo() async { // imUserInfoController = Get.find(); - final res = await ImService.instance.getUserFollowInfo(userIDList: [imUserInfoController!.userID.value]); + if (!Get.isRegistered()) { + logger.e('用户信息controller未注册'); + return; + } + final res = await ImService.instance.getUserFollowInfo(userIDList: [imUserInfoController?.userID.value ?? '']); if (res.success) { //这里少个点赞,从服务端获取 // followersCount粉丝,多少人关注了我,mutualFollowersCount互关,followingCount我关注了多少人 @@ -330,108 +329,106 @@ class MyPageState extends State with SingleTickerProviderStateMixin { } else { return Scaffold( backgroundColor: const Color(0xFFFAF6F9), - body: Obx(() { - return NestedScrollViewPlus( - controller: scrollController, - physics: shouldFixHeader.value - ? OnlyDownScrollPhysics(parent: AlwaysScrollableScrollPhysics()) - : isPinned.value - ? NeverScrollableScrollPhysics() - : AlwaysScrollableScrollPhysics(), - // physics: shouldFixHeader.value ? OnlyDownScrollPhysics(parent: AlwaysScrollableScrollPhysics()) : 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, - onStretchTrigger: () async { - logger.i('触发 stretch 拉伸'); - // 加载刷新逻辑 - }, - actions: [ - // _buildIcon('assets/images/svg/service.svg', () { - // logger.i('点击客服按钮'); - // }), - const SizedBox(width: 8.0), - _buildIcon('assets/images/svg/setting.svg', () { - logger.i('点击设置按钮'); - Get.toNamed('/setting'); - }), - const SizedBox(width: 10.0), - ], - flexibleSpace: _buildFlexibleSpace(), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Column( - children: [ - Obx(() => _buildStatsCard()), - const SizedBox(height: 10.0), - _buildOrderCard(context), - const SizedBox(height: 10.0), - ], - ), + body: NestedScrollViewPlus( + controller: scrollController, + physics: shouldFixHeader.value + ? OnlyDownScrollPhysics(parent: AlwaysScrollableScrollPhysics()) + : isPinned.value + ? NeverScrollableScrollPhysics() + : AlwaysScrollableScrollPhysics(), + // physics: shouldFixHeader.value ? OnlyDownScrollPhysics(parent: AlwaysScrollableScrollPhysics()) : 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, + onStretchTrigger: () async { + logger.i('触发 stretch 拉伸'); + // 加载刷新逻辑 + }, + actions: [ + // _buildIcon('assets/images/svg/service.svg', () { + // logger.i('点击客服按钮'); + // }), + const SizedBox(width: 8.0), + _buildIcon('assets/images/svg/setting.svg', () { + logger.i('点击设置按钮'); + Get.toNamed('/setting'); + }), + const SizedBox(width: 10.0), + ], + flexibleSpace: _buildFlexibleSpace(), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + children: [ + Obx(() => _buildStatsCard()), + const SizedBox(height: 10.0), + _buildOrderCard(context), + const SizedBox(height: 10.0), + ], ), ), - SliverPersistentHeader( - pinned: true, - delegate: CustomStickyHeader( - isPinned: isPinned, - positions: positions, - 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: false, - overlayColor: WidgetStateProperty.all(Colors.transparent), - unselectedLabelColor: Colors.black87, - labelColor: const Color(0xFFFF5000), - indicator: const UnderlineTabIndicator(borderSide: BorderSide(color: Color(0xFFFF5000), width: 2.0)), - indicatorSize: TabBarIndicatorSize.tab, - 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( + isPinned: isPinned, + positions: positions, + 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: false, + overlayColor: WidgetStateProperty.all(Colors.transparent), + unselectedLabelColor: Colors.black87, + labelColor: const Color(0xFFFF5000), + indicator: const UnderlineTabIndicator(borderSide: BorderSide(color: Color(0xFFFF5000), width: 2.0)), + indicatorSize: TabBarIndicatorSize.tab, + 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: - _buildGridTab(0), + ), + ]; + }, + body: TabBarView( + controller: tabController, + children: [ + // Tab 1: + _buildGridTab(0), - // Tab 2: - _buildGridTab(1) - ], - ), - ); - }), + // Tab 2: + _buildGridTab(1) + ], + ), + ), ); } }); @@ -510,7 +507,7 @@ class MyPageState extends State with SingleTickerProviderStateMixin { child: Padding( padding: const EdgeInsets.symmetric(vertical: 20.0), child: Center( - child: params.hasMore ? const CircularProgressIndicator() : const Text('没有更多数据了'), + child: params.hasMore.value ? CircularProgressIndicator() : Text('没有更多数据了'), ), ), ), @@ -612,86 +609,108 @@ 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); + builder: (context, constraints) { + final double maxHeight = 180; + final double minHeight = 120; + final currentHeight = constraints.maxHeight; + double ratio = ((currentHeight - minHeight) / (maxHeight - minHeight)).clamp(0.0, 1.0); return Stack( fit: StackFit.expand, children: [ - Positioned.fill( - child: Opacity( - opacity: 1.0, - child: NetworkOrAssetImage(imageUrl: imUserInfoController?.customInfo['coverBg'], placeholderAsset: 'assets/images/bk.jpg'), - ), - ), + // 背景图 Obx + Obx(() { + final bgUrl = imUserInfoController?.customInfo['coverBg'] ?? ''; + return NetworkOrAssetImage( + imageUrl: bgUrl, + placeholderAsset: 'assets/images/bk.jpg', + fit: BoxFit.cover, + ); + }), + Positioned( - left: 15.0, + left: 15, bottom: 0, - right: 15.0, + right: 15, child: Container( - padding: const EdgeInsets.symmetric(vertical: 10.0), + padding: const EdgeInsets.symmetric(vertical: 10), child: Row( crossAxisAlignment: CrossAxisAlignment.end, - children: [ - ClipOval( - child: Obx(() { - final faceUrl = imUserInfoController?.faceUrl.value; - return ClipRRect( - borderRadius: BorderRadius.circular(30), - child: NetworkOrAssetImage( - imageUrl: faceUrl, - width: 80, - height: 80, - ), - ); - }), - ), - const SizedBox(width: 15.0), + children: [ + // 头像 Obx + Obx(() { + final faceUrl = imUserInfoController?.faceUrl.value ?? ''; + return ClipOval( + child: NetworkOrAssetImage( + imageUrl: faceUrl, + width: 80, + height: 80, + ), + ); + }), + const SizedBox(width: 15), 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 == true ? imUserInfoController!.nickname.value : '昵称', - style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold, fontFamily: 'Arial', color: Colors.white), + // 昵称 Obx + Obx(() { + final nickname = imUserInfoController?.nickname.value ?? ''; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: Colors.black.withAlpha(76), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + nickname.isNotEmpty ? nickname : '昵称', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, ), - )), - const SizedBox(width: 8.0), + ), + ); + }), + const SizedBox(width: 8), InkWell( - onTap: () { - qrcodeAlertDialog(context); - }, + 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), + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), + decoration: BoxDecoration( + color: Colors.black.withAlpha(76), + borderRadius: BorderRadius.circular(20), + ), + child: const Icon(Icons.qr_code_outlined, size: 18, 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)), - ), - ), + const SizedBox(height: 8), + // 用户ID Obx + Obx(() { + final userId = imUserInfoController?.userID.value ?? ''; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: Colors.black.withAlpha(76), + borderRadius: BorderRadius.circular(20), + ), + child: InkWell( + onTap: () { + Clipboard.setData(ClipboardData(text: userId)); + MyDialog.toast( + 'ID已复制', + icon: const Icon(Icons.check_circle), + style: ToastStyle(backgroundColor: Colors.green.withAlpha(200)), + ); + }, + child: Text('ID:$userId', style: const TextStyle(fontSize: 12, color: Colors.white)), + ), + ); + }), ], ), ), @@ -705,6 +724,104 @@ 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( @@ -783,7 +900,9 @@ class MyPageState extends State with SingleTickerProviderStateMixin { _buildOrderIcon('assets/images/ico_sh.png', '提现vloger', () { Get.toNamed('/vloger'); }), - _buildOrderIcon('assets/images/ico_tgm.png', '推广码', () {}), + _buildOrderIcon('assets/images/ico_tgm.png', '推广码', () { + logger.e('推广码'); + }), ], ), ), diff --git a/lib/pages/my/vloger.dart b/lib/pages/my/vloger.dart index 6f42d44..6251b34 100644 --- a/lib/pages/my/vloger.dart +++ b/lib/pages/my/vloger.dart @@ -388,18 +388,18 @@ class MyPageState extends State with SingleTickerProviderStateMixin { double ratio = (currentHeight - minHeight) / (maxHeight - minHeight); ratio = ratio.clamp(0.0, 1.0); String coverBg = userInfo.value.customInfo?['coverBg'] ?? ''; - coverBg = coverBg.isEmpty ? 'assets/images/pic2.jpg' : coverBg; - logger.w(coverBg); return Stack( fit: StackFit.expand, children: [ Positioned.fill( - child: Opacity( - opacity: 1.0, - child: Image.asset( - coverBg, - fit: BoxFit.cover, - ))), + child: Opacity( + opacity: 1.0, + child: NetworkOrAssetImage( + imageUrl: coverBg, + width: double.infinity, + ), + ), + ), Positioned( left: 15.0, bottom: 0, @@ -411,20 +411,6 @@ class MyPageState extends State with SingleTickerProviderStateMixin { children: [ ClipOval( child: NetworkOrAssetImage(imageUrl: userInfo.value.faceUrl), - // child: Image.asset( - // userInfo.value.faceUrl ?? 'assets/images/pic1.jpg', - // height: 60.0, - // width: 60.0, - // fit: BoxFit.cover, - // errorBuilder: (context, error, stackTrace) { - // return Image.asset( - // 'assets/images/pic1.jpg', - // height: 60.0, - // width: 60.0, - // fit: BoxFit.cover, - // ); - // }, - // ), ), const SizedBox(width: 15.0), Expanded( diff --git a/lib/pages/order/detail.dart b/lib/pages/order/detail.dart index 6218500..412d0cf 100644 --- a/lib/pages/order/detail.dart +++ b/lib/pages/order/detail.dart @@ -1,7 +1,9 @@ library; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:shirne_dialog/shirne_dialog.dart'; +import 'package:timer_count_down/timer_count_down.dart'; import '../../behavior/custom_scroll_behavior.dart'; @@ -13,6 +15,30 @@ class OrderDetail extends StatefulWidget { } class _OrderDetailState extends State with SingleTickerProviderStateMixin { + late String _orderId; + @override + void initState() { + super.initState(); + _orderId = Get.arguments; + getOrderDetail(orderId: _orderId); + } + + // 获取订单详情信息,包含商品参数 + void getOrderDetail({required String orderId}) async { + // + } + // 计算时间 + int handleTime() { + String dbTime = '2025-08-21 15:07:31.293'; + DateTime orderTime = DateTime.parse(dbTime); + // 计算过期时间 = 下单时间 + 15分钟 + DateTime expireTime = orderTime.add(const Duration(minutes: 15)); + // 剩余秒数 + int remainSeconds = expireTime.difference(DateTime.now()).inSeconds; + if (remainSeconds < 0) remainSeconds = 0; + return remainSeconds; + } + Widget emptyTip() { return Column( mainAxisAlignment: MainAxisAlignment.center, @@ -39,12 +65,12 @@ class _OrderDetailState extends State with SingleTickerProviderStat foregroundColor: Colors.white, title: Text('订单详情'), titleSpacing: 1.0, - actions: [ - IconButton( - icon: Icon(Icons.help, size: 18.0), - onPressed: () {}, - ), - ], + // actions: [ + // IconButton( + // icon: Icon(Icons.help, size: 18.0), + // onPressed: () {}, + // ), + // ], ), body: ScrollConfiguration( behavior: CustomScrollBehavior().copyWith(scrollbars: false), @@ -64,9 +90,31 @@ class _OrderDetailState extends State with SingleTickerProviderStat )), TextSpan(text: ' 待支付, '), TextSpan(text: ' 剩余 '), - TextSpan( - text: '00 : 29 : 55', - style: TextStyle(color: Colors.red), + // TextSpan( + // text: '00 : 29 : 55', + // style: TextStyle(color: Colors.red), + // ), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Countdown( + // createtime + // seconds: 15 * 60, // 15分钟 + seconds: handleTime(), + build: (_, double time) { + int m = ((time % 3600) ~/ 60).toInt(); + int s = (time % 60).toInt(); + String formatted = "${m.toString().padLeft(2, '0')} : ${s.toString().padLeft(2, '0')}"; + + return Text( + formatted, + style: const TextStyle(color: Colors.red), + ); + }, + interval: const Duration(seconds: 1), + onFinished: () { + print("倒计时结束"); + }, + ), ), ]), ), diff --git a/lib/pages/video/index.dart b/lib/pages/video/index.dart index 94b3ecb..da6c742 100644 --- a/lib/pages/video/index.dart +++ b/lib/pages/video/index.dart @@ -9,12 +9,12 @@ import '../../behavior/custom_scroll_behavior.dart'; import '../../components/keepalive_wrapper.dart'; import '../../controller/video_module_controller.dart'; import './module/attention.dart'; -import './module/recommend.dart'; // import './module/browse.dart'; // import './module/buying.dart'; // import './module/drama.dart'; // import './module/live.dart'; import './module/friend.dart'; +import './module/recommend.dart'; // 引入tab内容模块 // import './module/subscribe.dart'; @@ -138,19 +138,19 @@ class _VideoPageState extends State with SingleTickerProviderStateMix child: PageView( controller: pageController, onPageChanged: (index) { - logger.i('$index'); + logger.i('$index'); // 根据当前 tab 控制对应播放器播放,其它暂停 if (index == 0) { AttentionModule.playVideo(); - // FriendModule.pauseVideo(); + // FriendModule.pauseVideo(); RecommendModule.pauseVideo(); } else if (index == 1) { AttentionModule.pauseVideo(); - // FriendModule.playVideo(); + // FriendModule.playVideo(); RecommendModule.pauseVideo(); } else if (index == 2) { AttentionModule.pauseVideo(); - // FriendModule.pauseVideo(); + // FriendModule.pauseVideo(); RecommendModule.playVideo(); } videoModuleController.updateVideoTabIndex(index); diff --git a/lib/pages/video/module/recommend.dart b/lib/pages/video/module/recommend.dart index 5dae672..782d63d 100644 --- a/lib/pages/video/module/recommend.dart +++ b/lib/pages/video/module/recommend.dart @@ -1,10 +1,11 @@ /// 精选推荐模块 library; + import 'dart:async'; import 'dart:convert'; -import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:loopin/IM/controller/chat_controller.dart'; @@ -16,9 +17,9 @@ import 'package:loopin/components/my_toast.dart'; import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/models/summary_type.dart'; import 'package:loopin/service/http.dart'; -import 'package:loopin/utils/wxsdk.dart'; -import 'package:loopin/utils/permissions.dart'; import 'package:loopin/utils/download_video.dart'; +import 'package:loopin/utils/permissions.dart'; +import 'package:loopin/utils/wxsdk.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart'; @@ -48,6 +49,7 @@ class RecommendModule extends StatefulWidget { @override State createState() => _RecommendModuleState(); } + class CommentBottomSheet extends StatefulWidget { final String videoId; final Function(int) onCommentCountChanged; // 新增回调函数 @@ -72,7 +74,7 @@ class _CommentBottomSheetState extends State { String replyingCommentId = ''; String replyingCommentUser = ''; - // 新增:子评论相关状态 + // 新增:子评论相关状态 Map expandedReplies = {}; // 存储已展开的回复 {commentId: true/false} Map>> replyData = {}; // 存储子评论数据 {commentId: [replies]} Map replyPage = {}; // 子评论分页 @@ -97,7 +99,7 @@ class _CommentBottomSheetState extends State { hasMoreComments = true; commentList.clear(); } - logger.d('入参vlogId-------------------->: ${widget.videoId}'); + logger.d('入参vlogId-------------------->: ${widget.videoId}'); try { final res = await Http.post(VideoApi.videoCommentList, data: { 'vlogId': widget.videoId, @@ -109,8 +111,7 @@ class _CommentBottomSheetState extends State { if (res['code'] == 200 && res['data'] != null) { final data = res['data']; - final List> newComments = - List>.from(data['records'] ?? []); + final List> newComments = List>.from(data['records'] ?? []); final int total = data['total'] ?? 0; setState(() { @@ -134,91 +135,87 @@ class _CommentBottomSheetState extends State { } } -Future postComment(String content, {String parentCommentId = ''}) async { - try { - final res = await Http.post(VideoApi.doVideoComment, data: { - 'vlogId': widget.videoId, - 'content': content, - 'fatherCommentId': parentCommentId.isNotEmpty ? parentCommentId : null, - }); + Future postComment(String content, {String parentCommentId = ''}) async { + try { + final res = await Http.post(VideoApi.doVideoComment, data: { + 'vlogId': widget.videoId, + 'content': content, + 'fatherCommentId': parentCommentId.isNotEmpty ? parentCommentId : null, + }); - if (res['code'] == 200) { - // 如果是回复,刷新对应的子评论 - if (parentCommentId.isNotEmpty) { - fetchReplies(parentCommentId, false); - // 更新主评论的子评论数量 - setState(() { - final comment = commentList.firstWhere( - (c) => c['id'] == parentCommentId, - orElse: () => {} - ); - if (comment != null && comment.isNotEmpty) { - comment['childCount'] = (comment['childCount'] ?? 0) + 1; + if (res['code'] == 200) { + // 如果是回复,刷新对应的子评论 + if (parentCommentId.isNotEmpty) { + fetchReplies(parentCommentId, false); + // 更新主评论的子评论数量 + setState(() { + final comment = commentList.firstWhere((c) => c['id'] == parentCommentId, orElse: () => {}); + if (comment.isNotEmpty) { + comment['childCount'] = (comment['childCount'] ?? 0) + 1; + } + }); + } else { + // 如果是新评论,刷新整个列表 + fetchComments(false); } - }); - } else { - // 如果是新评论,刷新整个列表 - fetchComments(false); + widget.onCommentCountChanged(commentList.length + 1); + MyToast().tip( + title: '评论成功', + position: 'center', + type: 'success', + ); } - widget.onCommentCountChanged(commentList.length + 1); + } catch (e) { + logger.i('发布评论失败: $e'); MyToast().tip( - title: '评论成功', + title: '评论失败', position: 'center', - type: 'success', + type: 'error', ); } - } catch (e) { - logger.i('发布评论失败: $e'); - MyToast().tip( - title: '评论失败', - position: 'center', - type: 'error', - ); } -} // 获取二级评论,复用一级评论的接口,修改传参 Future fetchReplies(String commentId, bool loadMore) async { - if (isLoadingReplies[commentId] == true && !loadMore) return; + if (isLoadingReplies[commentId] == true && !loadMore) return; - setState(() { - isLoadingReplies[commentId] = true; - }); - - if (!loadMore) { - replyPage[commentId] = 1; - replyData[commentId] = []; - } - - try { - final res = await Http.post(VideoApi.videoCommentList, data: { - 'fatherCommentId': commentId, - 'current': replyPage[commentId], - 'size': commentPageSize, + setState(() { + isLoadingReplies[commentId] = true; }); - if (res['code'] == 200 && res['data'] != null) { - final data = res['data']; - final List> newReplies = - List>.from(data['records'] ?? []); + if (!loadMore) { + replyPage[commentId] = 1; + replyData[commentId] = []; + } + try { + final res = await Http.post(VideoApi.videoCommentList, data: { + 'fatherCommentId': commentId, + 'current': replyPage[commentId], + 'size': commentPageSize, + }); + + if (res['code'] == 200 && res['data'] != null) { + final data = res['data']; + final List> newReplies = List>.from(data['records'] ?? []); + + setState(() { + if (loadMore) { + replyData[commentId]!.addAll(newReplies); + } else { + replyData[commentId] = newReplies; + } + replyPage[commentId] = replyPage[commentId]! + 1; + }); + } + } catch (e) { + logger.e('获取子评论异常: $e'); + } finally { setState(() { - if (loadMore) { - replyData[commentId]!.addAll(newReplies); - } else { - replyData[commentId] = newReplies; - } - replyPage[commentId] = replyPage[commentId]! + 1; + isLoadingReplies[commentId] = false; }); } - } catch (e) { - logger.e('获取子评论异常: $e'); - } finally { - setState(() { - isLoadingReplies[commentId] = false; - }); } -} @override Widget build(BuildContext context) { @@ -234,7 +231,7 @@ Future postComment(String content, {String parentCommentId = ''}) async { children: [ Row( children: [ - /* Expanded( + /* Expanded( child: Text.rich(TextSpan(children: [ TextSpan( text: '大家都在搜: ', @@ -270,9 +267,7 @@ Future postComment(String content, {String parentCommentId = ''}) async { Expanded( child: NotificationListener( onNotification: (ScrollNotification scrollInfo) { - if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && - !isLoadingMoreComments && - hasMoreComments) { + if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && !isLoadingMoreComments && hasMoreComments) { fetchComments(true); } return false; @@ -292,9 +287,7 @@ Future postComment(String content, {String parentCommentId = ''}) async { return Center( child: Padding( padding: EdgeInsets.all(16.0), - child: isLoadingMoreComments - ? CircularProgressIndicator() - : Text('没有更多评论了'), + child: isLoadingMoreComments ? CircularProgressIndicator() : Text('没有更多评论了'), ), ); } @@ -388,7 +381,7 @@ Future postComment(String content, {String parentCommentId = ''}) async { ), ), Text( - '${comment['createTime']?.toString().substring(0, 10) ?? ''}', + comment['createTime']?.toString().substring(0, 10) ?? '', style: TextStyle(color: Colors.grey, fontSize: 12.0), ), ], @@ -402,67 +395,65 @@ Future postComment(String content, {String parentCommentId = ''}) async { child: Column( children: [ ...replies.map((reply) => ListTile( - leading: ClipRRect( - borderRadius: BorderRadius.circular(25.0), - child: NetworkOrAssetImage( - imageUrl: reply['commentUserFace'] ?? 'assets/images/avatar/default.png', - width: 25.0, - height: 25.0, - fit: BoxFit.cover, - ), - ), - title: Row( - children: [ - Expanded( - child: Text( - reply['commentUserNickname'] ?? '未知用户', - style: TextStyle( - color: Colors.grey, - fontSize: 11.0, + leading: ClipRRect( + borderRadius: BorderRadius.circular(25.0), + child: NetworkOrAssetImage( + imageUrl: reply['commentUserFace'] ?? 'assets/images/avatar/default.png', + width: 25.0, + height: 25.0, + fit: BoxFit.cover, + ), + ), + title: Row( + children: [ + Expanded( + child: Text( + reply['commentUserNickname'] ?? '未知用户', + style: TextStyle( + color: Colors.grey, + fontSize: 11.0, + ), + ), ), - ), + GestureDetector( + onTap: () { + setState(() { + replyingCommentId = comment['id']; + replyingCommentUser = reply['commentUserNickname'] ?? '未知用户'; + }); + }, + child: Icon( + Icons.reply, + color: Colors.black54, + size: 14.0, + ), + ), + ], ), - GestureDetector( - onTap: () { - setState(() { - replyingCommentId = comment['id']; - replyingCommentUser = reply['commentUserNickname'] ?? '未知用户'; - }); - }, - child: Icon( - Icons.reply, - color: Colors.black54, - size: 14.0, - ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + margin: EdgeInsets.symmetric(vertical: 3.0), + child: Text( + reply['content'] ?? '', + style: TextStyle(fontSize: 13.0), + ), + ), + Text( + reply['createTime']?.toString().substring(0, 10) ?? '', + style: TextStyle(color: Colors.grey, fontSize: 11.0), + ), + ], ), - ], - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: EdgeInsets.symmetric(vertical: 3.0), - child: Text( - reply['content'] ?? '', - style: TextStyle(fontSize: 13.0), - ), - ), - Text( - '${reply['createTime']?.toString().substring(0, 10) ?? ''}', - style: TextStyle(color: Colors.grey, fontSize: 11.0), - ), - ], - ), - )).toList(), + )), // 加载更多子评论按钮 if (replies.length < comment['childCount']) Center( child: TextButton( onPressed: () => fetchReplies(comment['id'], true), - child: isLoadingReplies[comment['id']] == true - ? CircularProgressIndicator() - : Text('加载更多回复'), + child: isLoadingReplies[comment['id']] == true ? CircularProgressIndicator() : Text('加载更多回复'), ), ), ], @@ -474,8 +465,8 @@ Future postComment(String content, {String parentCommentId = ''}) async { ); }, ), - ), ), + ), // 在回复输入区域显示更详细的信息 if (replyingCommentId.isNotEmpty) Container( @@ -502,54 +493,58 @@ Future postComment(String content, {String parentCommentId = ''}) async { ], ), ), - GestureDetector( - child: Container( - margin: EdgeInsets.all(10.0), - height: 40.0, - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: BorderRadius.circular(30.0), - ), - child: Row( - children: [ - SizedBox(width: 15.0), - Icon( - Icons.edit_note, - color: Colors.black54, - size: 16.0, - ), - SizedBox(width: 5.0), - Text( - replyingCommentId.isNotEmpty ? '回复 @$replyingCommentUser' : '说点什么...', - style: TextStyle(color: Colors.black54, fontSize: 14.0), - ), - ], - ), + GestureDetector( + child: Container( + margin: EdgeInsets.all(10.0), + height: 40.0, + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(30.0), + ), + child: Row( + children: [ + SizedBox(width: 15.0), + Icon( + Icons.edit_note, + color: Colors.black54, + size: 16.0, + ), + SizedBox(width: 5.0), + Text( + replyingCommentId.isNotEmpty ? '回复 @$replyingCommentUser' : '说点什么...', + style: TextStyle(color: Colors.black54, fontSize: 14.0), + ), + ], ), - onTap: () { - Navigator.push(context, FadeRoute(child: PopupReply( - hintText: replyingCommentId.isNotEmpty ? '回复 @$replyingCommentUser' : '说点什么...', - onChanged: (value) { - debugPrint('评论内容: $value'); - }, - onSubmitted: (value) { - if (value.isNotEmpty) { - postComment(value, parentCommentId: replyingCommentId); - setState(() { - replyingCommentId = ''; - replyingCommentUser = ''; - }); - Navigator.pop(context); - } - }, - ))); - }, ), - ], - ), - ); - } + onTap: () { + Navigator.push( + context, + FadeRoute( + child: PopupReply( + hintText: replyingCommentId.isNotEmpty ? '回复 @$replyingCommentUser' : '说点什么...', + onChanged: (value) { + debugPrint('评论内容: $value'); + }, + onSubmitted: (value) { + if (value.isNotEmpty) { + postComment(value, parentCommentId: replyingCommentId); + setState(() { + replyingCommentId = ''; + replyingCommentUser = ''; + }); + Navigator.pop(context); + } + }, + ))); + }, + ), + ], + ), + ); } +} + class _RecommendModuleState extends State { VideoModuleController videoModuleController = Get.put(VideoModuleController()); final ChatController chatController = Get.find(); @@ -731,12 +726,10 @@ class _RecommendModuleState extends State { Future doUnLikeVideo(item) async { logger.d('点击了点赞按钮$item'); try { - final res = await Http.post(VideoApi.unlike, data:{ - 'vlogId': item['id'] - }); + final res = await Http.post(VideoApi.unlike, data: {'vlogId': item['id']}); final resCode = res['code']; if (resCode == 200) { - item['doILikeThisVlog'] = !item['doILikeThisVlog']; + item['doILikeThisVlog'] = !item['doILikeThisVlog']; } } catch (e) { logger.i('点击取消喜欢按钮报错: $e'); @@ -746,9 +739,7 @@ class _RecommendModuleState extends State { // 点击喜欢视频 Future doLikeVideo(item) async { try { - final res = await Http.post(VideoApi.like, data:{ - 'vlogId': item['id'] - }); + final res = await Http.post(VideoApi.like, data: {'vlogId': item['id']}); final resCode = res['code']; if (resCode == 200) { item['doILikeThisVlog'] = !item['doILikeThisVlog']; @@ -760,35 +751,34 @@ class _RecommendModuleState extends State { } // 评论弹框 -void handleComment(index) { - final videoId = videoList[index]['id']; - logger.i('点击了评论按钮$videoId'); + void handleComment(index) { + final videoId = videoList[index]['id']; + logger.i('点击了评论按钮$videoId'); - showModalBottomSheet( - backgroundColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(15.0))), - showDragHandle: false, - clipBehavior: Clip.antiAlias, - isScrollControlled: true, - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 3 / 4, - ), - context: context, - builder: (context) { - return CommentBottomSheet( - videoId: videoId, - onCommentCountChanged: (newCount) { - // 更新对应视频的评论数量 - setState(() { - if (index < videoList.length) { - videoList[index]['commentsCounts'] = newCount; - } - }); - } - ); - }, - ); -} + showModalBottomSheet( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(15.0))), + showDragHandle: false, + clipBehavior: Clip.antiAlias, + isScrollControlled: true, + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 3 / 4, + ), + context: context, + builder: (context) { + return CommentBottomSheet( + videoId: videoId, + onCommentCountChanged: (newCount) { + // 更新对应视频的评论数量 + setState(() { + if (index < videoList.length) { + videoList[index]['commentsCounts'] = newCount; + } + }); + }); + }, + ); + } // 分享弹框 void handleShare(index) { @@ -843,40 +833,41 @@ void handleComment(index) { ), // 会话列表 - SizedBox( - height: 110, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).length, - padding: EdgeInsets.symmetric(horizontal: 0, vertical: 20.0), - itemBuilder: (context, index) { - final filteredList = chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).toList(); - return GestureDetector( - onTap: () => handlCoverClick(filteredList[index].conversation), - child: Container( - width: 64, - margin: EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - NetworkOrAssetImage( - imageUrl: filteredList[index].faceUrl, - width: 48.0, - height: 48.0, - ), - SizedBox(height: 5), - Text( - '${filteredList[index].conversation.showName}', - style: TextStyle(fontSize: 12.0), - overflow: TextOverflow.ellipsis, - ), - ], + if (chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).isNotEmpty) + SizedBox( + height: 110, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).length, + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 20.0), + itemBuilder: (context, index) { + final filteredList = chatController.chatList.where((item) => item.conversation.conversationGroupList?.isEmpty == true).toList(); + return GestureDetector( + onTap: () => handlCoverClick(filteredList[index].conversation), + child: Container( + width: 64, + margin: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + NetworkOrAssetImage( + imageUrl: filteredList[index].faceUrl, + width: 48.0, + height: 48.0, + ), + const SizedBox(height: 5), + Text( + filteredList[index].conversation.showName ?? '', + style: const TextStyle(fontSize: 12.0), + overflow: TextOverflow.ellipsis, + ), + ], + ), ), - ), - ); - }, + ); + }, + ), ), - ), // 取消按钮 SafeArea( @@ -909,36 +900,37 @@ void handleComment(index) { final videoUrl = videoList[videoModuleController.videoPlayIndex.value]['url']; final description = videoList[videoModuleController.videoPlayIndex.value]['title'] ?? '快来看看这个视频'; var httpPrefix = 'http://43.143.227.203/adv'; - logger.i('分享链接地址----------------: ${httpPrefix}/goods-detail?id=${videoId}'); + logger.i('分享链接地址----------------: $httpPrefix/goods-detail?id=$videoId'); if (index == 0) { // 分享好友 - Wxsdk.shareToFriend(title: '快来看看这个视频', description: description, webpageUrl: '${httpPrefix}/video-detail?id=${videoId}'); + Wxsdk.shareToFriend(title: '快来看看这个视频', description: description, webpageUrl: '$httpPrefix/video-detail?id=$videoId'); } else if (index == 1) { // 分享到朋友圈 - Wxsdk.shareToTimeline(title: '快来看看这个视频', webpageUrl: '${httpPrefix}/goods-detail?id=${videoId}'); + Wxsdk.shareToTimeline(title: '快来看看这个视频', webpageUrl: '$httpPrefix/goods-detail?id=$videoId'); } else if (index == 2) { // 复制链接到剪切板 copyToClipboard(videoUrl); } else if (index == 3) { // 下载视频到本地 - _downloadVideoWithDio(videoUrl,description); + _downloadVideoWithDio(videoUrl, description); } } - // 复制链接到剪贴板 - void copyToClipboard(String text) async { + + // 复制链接到剪贴板 + void copyToClipboard(String text) async { try { await Clipboard.setData(ClipboardData(text: text)); - MyToast().tip( - title: '链接已复制到剪贴板', - position: 'center', - type: 'success', - ); + MyToast().tip( + title: '链接已复制到剪贴板', + position: 'center', + type: 'success', + ); } catch (e) { - MyToast().tip( - title: '复制失败', - position: 'center', - type: 'success', - ); + MyToast().tip( + title: '复制失败', + position: 'center', + type: 'success', + ); } } @@ -946,14 +938,14 @@ void handleComment(index) { Future _downloadVideoWithDio(String videoUrl, String fileName) async { try { // 请求存储权限 - String? toastId; // 用于存储toast的ID,以便后续关闭 + String? toastId; // 用于存储toast的ID,以便后续关闭 var status = await Permissions.requestStoragePermission(); if (!status) { - MyToast().tip( - title: '需要存储权限才能下载视频', - position: 'center', - type: 'success', - ); + MyToast().tip( + title: '需要存储权限才能下载视频', + position: 'center', + type: 'success', + ); return; } await DownloadManager.downloadFile( @@ -964,14 +956,14 @@ void handleComment(index) { // 显示进度组件 }, onComplete: (filePath) { - MyToast().tip( + MyToast().tip( title: '下载完成', position: 'center', type: 'success', ); }, onError: (error) { - MyToast().tip( + MyToast().tip( title: '下载失败: $error', position: 'center', type: 'error', @@ -1080,7 +1072,6 @@ void handleComment(index) { height: double.infinity, ), ), - StreamBuilder( stream: player.stream.playing, builder: (context, playing) { @@ -1123,20 +1114,16 @@ void handleComment(index) { height: 55.0, width: 48.0, child: GestureDetector( - onTap: ()async { + onTap: () async { player.pause(); // 跳转到 Vloger 页面并等待返回结果 - final result = await Get.toNamed( - '/vloger', - arguments: videoList[videoModuleController - .videoPlayIndex.value]); + final result = await Get.toNamed('/vloger', arguments: videoList[videoModuleController.videoPlayIndex.value]); if (result != null) { // 处理返回的参数 print('返回的数据: ${result['followStatus']}'); - player.play(); - videoList[index]['doIFollowVloger'] = result['followStatus']; - - }; + player.play(); + videoList[index]['doIFollowVloger'] = result['followStatus']; + } }, child: UnconstrainedBox( alignment: Alignment.topCenter, @@ -1174,17 +1161,17 @@ void handleComment(index) { ), ), onTap: () async { - final vlogerId = videoList[index]['memberId']; - final doIFollowVloger = videoList[index]['doIFollowVloger']; - // 未关注点击才去关注 - if(doIFollowVloger == false){ + final vlogerId = videoList[index]['memberId']; + final doIFollowVloger = videoList[index]['doIFollowVloger']; + // 未关注点击才去关注 + if (doIFollowVloger == false) { final res = await ImService.instance.followUser(userIDList: [vlogerId]); if (res.success) { - setState(() { + setState(() { videoList[index]['doIFollowVloger'] = !videoList[index]['doIFollowVloger']; - }); - } + }); } + } }, ), ), @@ -1207,10 +1194,10 @@ void handleComment(index) { ), onTap: () { logger.d('点击了点赞按钮${videoList[index]['doILikeThisVlog']}'); - if(videoList[index]['doILikeThisVlog'] == true){ + if (videoList[index]['doILikeThisVlog'] == true) { logger.d('点击了点赞按钮${videoList[index]['doILikeThisVlog']}'); doUnLikeVideo(videoList[index]); - }else{ + } else { doLikeVideo(videoList[index]); } }, @@ -1411,4 +1398,4 @@ void handleComment(index) { ), ); } -} \ No newline at end of file +} diff --git a/lib/router/index.dart b/lib/router/index.dart index 0d694a0..c137e63 100644 --- a/lib/router/index.dart +++ b/lib/router/index.dart @@ -7,6 +7,9 @@ import 'package:loopin/bings/chat_binding.dart'; import 'package:loopin/pages/chat/chat.dart'; import 'package:loopin/pages/chat/chat_group.dart'; 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/my/des.dart'; import 'package:loopin/pages/my/nick_name.dart'; import 'package:loopin/pages/my/setting.dart'; @@ -29,7 +32,6 @@ import '../utils/common.dart'; final Map routes = { '/': const Layout(), '/goods': const Goods(), - // '/chat': const Chat(), // '/chatNoFriend': const ChatNoFriend(), // '/chatGroup': const ChatGroup(), '/order': const Order(), @@ -44,6 +46,10 @@ final Map routes = { '/about': const Setting(), '/des': const Des(), '/nickName': const NickName(), + //通知相关 + '/noFriend': const Nofriend(), + '/system': const System(), + '/interaction': const Interaction(), }; final List routeList = routes.entries diff --git a/lib/service/http_config.dart b/lib/service/http_config.dart index 140ca7c..5183c0f 100644 --- a/lib/service/http_config.dart +++ b/lib/service/http_config.dart @@ -8,7 +8,9 @@ class HttpConfig { // baseUrl: 'http://43.143.227.203:8099', // baseUrl: 'http://111.62.22.190:8080', // baseUrl: 'http://cjh.wuzhongjie.com.cn', - baseUrl: 'http://82.156.121.2:8880', + // baseUrl: 'http://82.156.121.2:8880', + baseUrl: 'http://192.168.1.65:8880', + // connectTimeout: Duration(seconds: 30), // receiveTimeout: Duration(seconds: 30), )); diff --git a/lib/utils/index.dart b/lib/utils/index.dart index 50d613c..eb35840 100644 --- a/lib/utils/index.dart +++ b/lib/utils/index.dart @@ -134,6 +134,7 @@ class Utils { return number.toStringAsFixed(1).replaceAll(RegExp(r'\.0$'), ''); } + // 处理聊天消息时间 String formatChatTime(int timestamp) { // timestamp 是秒级时间戳,转成 DateTime DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000); @@ -157,6 +158,23 @@ class Utils { } } + // 处理IM消息时间显示 + static String formatTime(int timestamp) { + // 获取消息时间 + final messageTime = DateTime.fromMillisecondsSinceEpoch( + timestamp * 1000, + ).toLocal(); + // 获取当前时间 + final now = DateTime.now(); + if (messageTime.year == now.year) { + // 同一年:显示月-日 + return '${messageTime.month.toString().padLeft(2, '0')}-${messageTime.day.toString().padLeft(2, '0')}'; + } else { + // 不同年:显示年-月-日 时:分 + return '${messageTime.year}-${messageTime.month.toString().padLeft(2, '0')}-${messageTime.day.toString().padLeft(2, '0')}'; + } + } + String _formatHourMinute(DateTime dt) { final hour = dt.hour.toString().padLeft(2, '0'); final minute = dt.minute.toString().padLeft(2, '0'); diff --git a/lib/utils/parse_message_summary.dart b/lib/utils/parse_message_summary.dart index a395485..41e76be 100644 --- a/lib/utils/parse_message_summary.dart +++ b/lib/utils/parse_message_summary.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +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'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart'; @@ -31,9 +32,26 @@ String parseMessageSummary(V2TimMessage msg) { } } +// newFoucs, //新的关注 +// systemNotify, // 系统->通知 +// systemReport, // 系统->举报下架(视频,视频评论) +// systemCheck, // 系统->审核结果(复审,驳回 ,通过) +// systemPush, //系统->推广类的 +// interactionComment, //互动->评论 +// interactionAt, //互动->视频评论中的@ +// interactionLike, //互动->点赞 +// interactionReply, //互动->评论回复 +// orderRecharge, //订单->充值 online +// orderPay, //订单->订单交易成功通知 online +// orderRefund, //订单->退款结果通知 +// groupNotifyCheck, //群通知->进群申请 online +// groupNotifyAccpet, // 群通知->进群审核审核通过 online +// groupNotifyFail, // 群通知->进群审核审核拒绝 online +// groupNotifyLeaveUp, // 群通知->群升级为达人群通知 String _parseCustomMessage(V2TimMessage? msg) { if (msg == null) return '[null]'; final sum = msg.cloudCustomData; + final elment = msg.customElem; // 所有服务端发送的通知消息都走【自定义消息类型】 try { switch (sum) { case SummaryType.hongbao: @@ -43,6 +61,115 @@ String _parseCustomMessage(V2TimMessage? msg) { return '[分享商品]'; case SummaryType.shareVideo: return '[分享视频]'; + // 解析服务端通知类的 + /// [des]显示的文本内容 + /// [data] json数据 + /// 名称firstFrameImg:先取cover,如果没有在取firstFrameImg + case NotifyMessageTypeConstants.newFoucs: + // 关注 + + ///发起关注人的[userID], + ///发起关注人的昵称[nickName], + ///发起关注人的头像地址[faceUrl], + ///------ + ///客户端检测发起关注人与本人的关注关系 + return elment!.desc!; + case NotifyMessageTypeConstants.systemNotify: + + ///系统级别的没有就先不做 + return elment!.desc!; + case NotifyMessageTypeConstants.systemReport: + // 视频举报 + + ///先只处理视频,评论的先不管 + ///被举报的视频[vlogID], + ///被举报的视频标题[title], + ///被举报的视频首帧图[firstFrameImg] + return elment!.desc!; + case NotifyMessageTypeConstants.systemCheck: + // 视频审核 + + ///视频审核结果通知 通过还是驳回 + ///被审核的视频[vlogID], + ///被审核的视频标题[title], + ///被审核的视频首帧图[firstFrameImg] + return elment!.desc!; + case NotifyMessageTypeConstants.systemPush: + + ///系统推广类的先不管了 + return elment!.desc!; + case NotifyMessageTypeConstants.interactionComment: + // 评论视频 + + /// 发起评论人的[userID] + /// 发起评论人的头像[faceUrl] + /// 发起评论人的[nickName] + /// 评论的内容[comment] + /// 评论的id[commentID] + /// 被评论的视频[vlogID] + /// 被评论的视频首帧图[firstFrameImg] + return elment!.desc!; + case NotifyMessageTypeConstants.interactionAt: + // 视频评论中@互关好友 + + /// 发起@人的[userID] + /// 发起@人的[nickName] + /// 发起@人的头像[faceUrl] + /// 关联视频的首帧图[firstFrameImg] + /// 关联视频的[vlogID] + /// 评论的id[commentID] + return elment!.desc!; + case NotifyMessageTypeConstants.interactionLike: + // 点赞 + + /// 发起点赞人的[userID] + /// 发起点赞人的头像[faceUrl] + /// 发起点赞人的[nickName] + /// 被点赞视频的[vlogID] + /// 被点赞视频的封面图[firstFrameImg] + return elment!.desc!; + case NotifyMessageTypeConstants.interactionReply: + // 回复评论 + /// 回复人的[userID] + /// 回复人的[faceUrl] + /// 回复人的[nickName] + /// 关联视频的[vlogID] + /// 关联视频的首帧图[firstFrameImg] + /// 被回复评论的[commentID] + /// 回复的评论内容[comment] + return elment!.desc!; + case NotifyMessageTypeConstants.orderRecharge: + // 充值成功通知 + /// 订单编号[orderID] + /// 充值金额[amount] + /// 充值后帐户余额[totalAmount] + return elment!.desc!; + case NotifyMessageTypeConstants.orderPay: + // 订单交易结果通知(商品购买成功后) + /// 订单编号[orderID] + /// 订单金额[amount] + /// 商品名称[name] + /// 商品描述[describe] + /// 商品价格[price] + /// 商品主图[pic] + return elment!.desc!; + case NotifyMessageTypeConstants.orderRefund: + // 订单退款通知 + /// 订单编号[orderID] + /// 订单金额[amount] + /// 商品名称[name] + /// 商品描述[describe] + /// 商品价格[price] + /// 商品主图[pic] + return elment!.desc!; + case NotifyMessageTypeConstants.groupNotifyCheck: + return elment!.desc!; + case NotifyMessageTypeConstants.groupNotifyAccpet: + return elment!.desc!; + case NotifyMessageTypeConstants.groupNotifyFail: + return elment!.desc!; + case NotifyMessageTypeConstants.groupNotifyLeaveUp: + return elment!.desc!; default: return '[未知消息类型2]'; } diff --git a/pubspec.lock b/pubspec.lock index 8315bef..1278df3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1274,6 +1274,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "0.7.4" + timer_count_down: + dependency: "direct main" + description: + name: timer_count_down + sha256: d025d408c2654e497ca0bd4bde014bd7509d4c6397af4ed23a0f9b692bbcf337 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.2" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2ebbf70..2f6e8db 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -87,6 +87,7 @@ dependencies: record: ^6.0.0 #音频 audioplayers: ^6.5.0 #音频播放 flutter_html: ^3.0.0 + timer_count_down: ^2.2.2 #倒计时 dev_dependencies: flutter_launcher_icons: ^0.13.1 # 使用最新版本