import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:get/get_rx/src/rx_typedefs/rx_typedefs.dart'; import 'package:loopin/IM/im_service.dart'; import 'package:loopin/components/custom_sticky_header.dart'; import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/components/only_down_scroll_physics.dart'; import 'package:loopin/styles/index.dart'; import 'package:nested_scroll_view_plus/nested_scroll_view_plus.dart'; import 'package:shirne_dialog/shirne_dialog.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_follow_info.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_user_full_info.dart'; class PageParams { int page; int pageSize; bool isLoading; bool hasMore; PageParams({ this.page = 1, this.pageSize = 10, this.isLoading = false, this.hasMore = true, }); } class Vloger extends StatefulWidget { const Vloger({super.key}); @override State createState() => MyPageState(); } class MyPageState extends State with SingleTickerProviderStateMixin { late dynamic args; late RxInt currentTabIndex = 0.obs; late RxList items = [].obs; late RxList favoriteItems = [].obs; late final Rx userInfo = Rx(V2TimUserFullInfo( userID: '', nickName: '', faceUrl: '', selfSignature: '', gender: 0, customInfo: { 'coverBg': '', }, role: 0, )); late RxInt followed = 0.obs; // 是否关注 // followersCount粉丝,多少人关注了我,mutualFollowersCount互关,followingCount我关注了多少人 late final Rx followInfo = Rx(V2TimFollowInfo( followersCount: 0, followingCount: 0, )); RxBool get shouldFixHeader => (currentTabIndex.value == 0 && items.isEmpty) || (currentTabIndex.value == 1 && favoriteItems.isEmpty) ? true.obs : false.obs; List tabList = [ {'name': "作品"}, ]; late PageParams itemsParams; late PageParams favoriteParams; late TabController tabController; late ScrollController scrollController; late Callback tabListener; late Callback scrollListener; @override void initState() { super.initState(); args = Get.arguments ?? {}; itemsParams = PageParams(); favoriteParams = PageParams(); selfInfo(); flowInfo(); checkFollowType(); initControllers(); scrollListener = () { final pos = scrollController.position; final isNearBottom = pos.pixels >= pos.maxScrollExtent - 100; if (!isNearBottom) return; if (currentTabIndex.value == 0 && !itemsParams.isLoading && itemsParams.hasMore) { loadData(0); } else if (currentTabIndex.value == 1 && !favoriteParams.isLoading && favoriteParams.hasMore) { loadData(1); } }; scrollController.addListener(scrollListener); tabListener = () { currentTabIndex.value = tabController.index; scrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeIn); loadData(0); }; tabController.addListener(tabListener); loadData(0); } @override void dispose() { tabController.removeListener(tabListener); scrollController.removeListener(scrollListener); tabController.dispose(); scrollController.dispose(); super.dispose(); } void loadData([int? tabIndex]) async { final index = tabIndex ?? currentTabIndex.value; if (index == 0) { if (itemsParams.isLoading || !itemsParams.hasMore) return; itemsParams.isLoading = true; await Future.delayed(const Duration(seconds: 1)); // 模拟生成新数据 List newItems = List.generate( itemsParams.pageSize, (i) => '作品 ${(itemsParams.page - 1) * itemsParams.pageSize + i + 1}', ); // 模拟判断是否还有更多数据 if (itemsParams.page >= 2) { itemsParams.hasMore = false; } // 添加新数据,触发响应式更新 items.addAll(newItems); // 页码加一 itemsParams.page++; itemsParams.isLoading = false; } } void initControllers() { tabController = TabController(initialIndex: 0, length: tabList.length, vsync: this); scrollController = ScrollController(); } // 获取当前博主基本信息 void selfInfo() async { final resIm = await ImService.instance.otherInfo(args['memberId']); if (resIm.success && resIm.data != null) { userInfo.value = resIm.data!; logger.i(userInfo.value.toLogString()); } else { logger.e(resIm.desc); } } // 博主的关注与粉丝 void flowInfo() async { logger.w(args.toString()); final res = await ImService.instance.getUserFollowInfo(userIDList: [args['memberId']]); if (res.success && res.data?.first != null) { //这里少个点赞,从服务端获取 // followersCount粉丝,多少人关注了我,mutualFollowersCount互关,followingCount我关注了多少人 followInfo.value = res.data!.first; logger.i(followInfo.value.toJson()); } else { logger.e(res.desc); } } // 检测当前用户是否关注博主 void checkFollowType() async { /// 0:不是好友也没有关注 /// 1:你关注了对方(单向) /// 2:对方关注了你(单向) /// 3:互相关注(双向好友) final res = await ImService.instance.checkFollowType(userIDList: [args['memberId']]); if (res.success) { final followType = res.data?.first.followType ?? 0; logger.i(res.data?.first.toJson()); followed.value = followType; logger.i(followed.value); } } @override Widget build(BuildContext context) { return PopScope( canPop: true, onPopInvokedWithResult: (bool didPop, Object? result) { if (didPop) { print('User navigated back'); } }, child:Scaffold( backgroundColor: const Color(0xFFFAF6F9), body: Obx(() { return NestedScrollViewPlus( controller: scrollController, physics: shouldFixHeader.value ? const OnlyDownScrollPhysics(parent: AlwaysScrollableScrollPhysics()) : const AlwaysScrollableScrollPhysics(), overscrollBehavior: OverscrollBehavior.outer, headerSliverBuilder: (context, innerBoxIsScrolled) { return [ SliverAppBar( backgroundColor: Colors.transparent, surfaceTintColor: Colors.transparent, expandedHeight: 180.0, collapsedHeight: 120.0, pinned: true, stretch: true, leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { Get.back(result: { 'returnTo': '/', 'vlogerId': args['memberId'], 'followStatus': (followed.value == 1 || followed.value == 3)?true:false, }); }, ), onStretchTrigger: () async { logger.i('触发 stretch 拉伸'); // 加载刷新逻辑 }, flexibleSpace: Obx(() { userInfo.value; return _buildFlexibleSpace(); }), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.all(10.0), child: Column( children: [ Obx(() => _buildStatsCard()), const SizedBox(height: 10.0), Obx(() => _buildInfoDesc(context)), const SizedBox(height: 10.0), Obx(() => _buildFoucsButton(context)), ], ), ), ), SliverPersistentHeader( pinned: true, delegate: CustomStickyHeader( child: PreferredSize( preferredSize: const Size.fromHeight(48.0), child: Container( color: Colors.white, child: TabBar( controller: tabController, tabs: tabList.map((item) { return Tab( child: Badge.count( backgroundColor: Colors.red, count: item['badge'] ?? 0, isLabelVisible: item['badge'] != null, alignment: Alignment.topRight, offset: const Offset(14, -6), child: Text(item['name'], style: const TextStyle(fontWeight: FontWeight.bold)), ), ); }).toList(), isScrollable: true, //禁止左右滑动 tabAlignment: TabAlignment.start, overlayColor: WidgetStateProperty.all(Colors.transparent), unselectedLabelColor: Colors.black87, labelColor: Colors.black, indicator: const UnderlineTabIndicator(borderSide: BorderSide(color: Colors.transparent, width: 2.0)), indicatorSize: TabBarIndicatorSize.label, unselectedLabelStyle: const TextStyle(fontSize: 16.0, fontFamily: 'Microsoft YaHei'), labelStyle: const TextStyle(fontSize: 18.0, fontFamily: 'Microsoft YaHei', fontWeight: FontWeight.bold), dividerHeight: 0, padding: const EdgeInsets.symmetric(horizontal: 10.0), labelPadding: const EdgeInsets.symmetric(horizontal: 15.0), ), ), ), ), ), ]; }, body: TabBarView( controller: tabController, children: [ // Tab 1: Obx(() => _buildGridTab(0)), ], ), ); }), ), ); } // 空状态提示 Widget emptyTip(String text) { return CustomScrollView( physics: const OnlyDownScrollPhysics(), slivers: [ SliverFillRemaining( hasScrollBody: false, child: Align( alignment: Alignment.topCenter, child: Padding( padding: const EdgeInsets.only(top: 50.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Image.asset('assets/images/empty.png', width: 100.0), const SizedBox(height: 8.0), Text( text, style: const TextStyle(color: Colors.grey, fontSize: 13.0), ), ], ), ), ), ), ], ); } Widget _buildGridTab(int tabIndex) { final listToShow = tabIndex == 0 ? items : favoriteItems; final params = tabIndex == 0 ? itemsParams : favoriteParams; if (listToShow.isEmpty) { return emptyTip('暂无相关数据'); } return CustomScrollView( slivers: [ SliverPadding( padding: const EdgeInsets.all(10.0), sliver: SliverGrid( delegate: SliverChildBuilderDelegate( (context, index) { return Container( decoration: BoxDecoration( color: Colors.blue[100 * ((index % 8) + 1)], borderRadius: BorderRadius.circular(10.0), ), alignment: Alignment.center, child: Text(listToShow[index], style: const TextStyle(fontWeight: FontWeight.bold)), ); }, childCount: listToShow.length, ), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, crossAxisSpacing: 10.0, mainAxisSpacing: 10.0, childAspectRatio: 1.0, ), ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.symmetric(vertical: 20.0), child: Center( child: params.hasMore ? const CircularProgressIndicator() : const Text('没有更多数据了'), ), ), ), ], ); } Widget _buildFlexibleSpace() { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final double maxHeight = 180.0; final double minHeight = 100.0; final double currentHeight = constraints.maxHeight; double ratio = (currentHeight - minHeight) / (maxHeight - minHeight); ratio = ratio.clamp(0.0, 1.0); String coverBg = userInfo.value.customInfo?['coverBg'] ?? ''; return Stack( fit: StackFit.expand, children: [ Positioned.fill( child: Opacity( opacity: 1.0, child: NetworkOrAssetImage( imageUrl: coverBg, width: double.infinity, ), ), ), 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: NetworkOrAssetImage(imageUrl: userInfo.value.faceUrl), ), 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: Text( userInfo.value.nickName ?? '未知', overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 20.0, fontWeight: FontWeight.bold, fontFamily: 'Arial', 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: '${userInfo.value.userID}')); MyDialog.toast('ID已复制', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.green.withAlpha(200))); }, child: Text('ID:${userInfo.value.userID}', style: TextStyle(fontSize: 12.0, color: Colors.white)), ), ), ], ), ), ], ), ), ), ], ); }, ); } Widget _buildStatsCard() { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [BoxShadow(color: Colors.black.withAlpha(10), offset: const Offset(0.0, 1.0), blurRadius: 2.0, spreadRadius: 0.0)], ), clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(10.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Column(children: [Text('9999', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), SizedBox(height: 3.0), Text('获赞')]), Column(children: [ Text('${followInfo.value.followingCount}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), SizedBox(height: 3.0), Text('关注') ]), Column(children: [ Text('${followInfo.value.followersCount}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), SizedBox(height: 3.0), Text('粉丝') ]), ], ), ), ); } Widget _buildInfoDesc(BuildContext context) { final tx = userInfo.value.selfSignature; if (tx == null || tx.isEmpty) { return const SizedBox.shrink(); } return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [BoxShadow(color: Colors.black.withAlpha(10), offset: const Offset(0.0, 1.0), blurRadius: 2.0, spreadRadius: 0.0)], ), clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(12), width: double.infinity, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), ), child: Text( '${userInfo.value.selfSignature}', style: const TextStyle(fontSize: 16), ), ), ], )); } /// 关注按钮 Widget _buildFoucsButton(BuildContext context) { // final vlogerId = '1943510443312078850'; // 18832510385,后面改回博主的id final vlogerId = args['memberId']; return AnimatedSwitcher( duration: const Duration(milliseconds: 300), transitionBuilder: (child, animation) { final offsetAnimation = Tween( begin: Offset((followed.value == 0) ? -1 : 1, 0), end: Offset.zero, ).animate(animation); return SlideTransition( position: offsetAnimation, child: child, ); }, child: [1, 3].contains(followed.value) ? Row( key: const ValueKey('followed'), children: [ Expanded( child: ElevatedButton( onPressed: () async { print('点击已关注'); final res = await ImService.instance.unfollowUser(userIDList: [vlogerId]); if (res.success) { // 如果为1那么状态置为0,为3则置为2 followed.value = followed.value == 1 ? 0 : 2; // 取关后不需重置陌生人消息group } }, style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[300], foregroundColor: Colors.black87, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: Text(followed.value == 1 ? '已关注' : followed.value == 3 ? '互关' : '未知状态'), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton( onPressed: () async { print('私信'); // 获取指定会话 final res = await ImService.instance.getConversation(conversationID: 'c2c_$vlogerId'); V2TimConversation conversation = res.data; logger.i(conversation.toLogString()); if (res.success) { // final isFriend = await ImService.instance.isMyFriend(vlogerId, FriendTypeEnum.V2TIM_FRIEND_TYPE_BOTH); // 这里需要注意处理取关后重新关注逻辑 // 是否互相关注 if (followed.value == 3) { Get.toNamed('/chat', arguments: conversation); } else { logger.i('对方没关注我'); logger.i(conversation.toLogString()); conversation.showName = conversation.showName ?? userInfo.value.nickName; Get.toNamed('/chatNoFriend', arguments: conversation); } } else { MyDialog.toast(res.desc, icon: const Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200))); } }, style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[300], foregroundColor: Colors.black87, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), child: const Text('私信'), ), ), ], ) : SizedBox( key: const ValueKey('not_followed'), width: double.infinity, child: ElevatedButton.icon( onPressed: () async { // 0没关系,2对方关注了我 print('点击关注'); final res = await ImService.instance.followUser(userIDList: [vlogerId]); if (res.success) { followed.value = followed.value == 0 ? 1 : 3; if (followed.value == 3) { // 修改后若为3,我将此会话移除noFriend会话组 final res = await ImService.instance.getConversation(conversationID: 'c2c_$vlogerId'); if (res.success) { V2TimConversation conversation = res.data; if (conversation.conversationGroupList?.isNotEmpty == true) { //移除陌生人会话 await ImService.instance.deleteConversationsFromGroup( groupName: conversation.conversationGroupList!.first!, conversationIDList: [conversation.conversationID], ); //重新构建会话 } } } } }, icon: const Icon(Icons.add), label: const Text('关注'), style: ElevatedButton.styleFrom( backgroundColor: FStyle.primaryColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ); } }