import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; import 'package:get/get_rx/src/rx_typedefs/rx_typedefs.dart'; import 'package:loopin/IM/controller/im_user_info_controller.dart'; import 'package:loopin/IM/im_service.dart'; import 'package:loopin/api/video_api.dart'; import 'package:loopin/api/common_api.dart'; import 'package:loopin/utils/index.dart'; import 'package:loopin/components/custom_sticky_header.dart'; import 'package:loopin/components/my_confirm.dart'; import 'package:loopin/components/network_or_asset_image.dart'; import 'package:loopin/components/only_down_scroll_physics.dart'; import 'package:loopin/controller/video_module_controller.dart'; import 'package:loopin/service/http.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_follow_info.dart'; import '../../utils/common.dart'; class PageParams { int page; int pageSize; bool isLoading; RxBool hasMore; int total; bool isInitLoading; PageParams({ this.page = 1, this.pageSize = 10, this.isLoading = false, bool hasMore = true, this.total = 0, this.isInitLoading = true, }) : hasMore = hasMore.obs; void init() { page = 1; pageSize = 10; isLoading = false; hasMore.value = true; total = 0; isInitLoading = true; } } class MyPage extends StatefulWidget { const MyPage({super.key}); @override State createState() => MyPageState(); } class MyPageState extends State with SingleTickerProviderStateMixin { late RxInt currentTabIndex = 0.obs; late RxList items = [].obs; late RxList favoriteItems = [].obs; RxBool isLogin = Common.isLogin().obs; //用户基本信息 // late Rx userInfo = Rx(null); ImUserInfoController? imUserInfoController; // 关注,互关,粉丝数量 late Rx followInfo = Rx(null); RxBool get shouldFixHeader => (currentTabIndex.value == 0 && items.isEmpty) || (currentTabIndex.value == 1 && favoriteItems.isEmpty) ? true.obs : false.obs; List tabList = [ {'name': "作品"}, {'name': "喜欢"}, ]; PageParams itemsParams = PageParams(); PageParams favoriteParams = PageParams(); late TabController tabController; late ScrollController scrollController; RxDouble positions = 0.0.obs; late Callback tabListener; late Callback scrollListener; late int vlogLikeCount = 0; // 点赞数量 RxBool isPinned = false.obs; // 是否吸顶 @override void initState() { super.initState(); initControllers(); scrollListener = () { final pos = scrollController.position; final isNearBottom = pos.pixels >= pos.maxScrollExtent - 100; if (!isNearBottom) return; if (currentTabIndex.value == 0 && !itemsParams.isLoading && itemsParams.hasMore.value) { loadData(0); } else if (currentTabIndex.value == 1 && !favoriteParams.isLoading && favoriteParams.hasMore.value) { loadData(1); } }; scrollController.addListener(scrollListener); tabListener = () { currentTabIndex.value = tabController.index; if (tabController.index == 0 && items.isEmpty) { loadData(0); scrollInnerList(); } else if (tabController.index == 1 && favoriteItems.isEmpty) { loadData(1); scrollInnerList(); } }; tabController.addListener(tabListener); getUserLikesCount(); // loadData(0); } @override void dispose() { tabController.removeListener(tabListener); scrollController.removeListener(scrollListener); tabController.dispose(); scrollController.dispose(); super.dispose(); } // 获取用户的所有视频的点赞数量 void getUserLikesCount() async { try { final resData = await Http.get(CommonApi.accountInfo); if(resData != null && resData['code'] == 200){ vlogLikeCount = resData['data']['vlogLikeCount']??0; } } catch (e) { } } // 添加控制子列表滚动的方法 void scrollInnerList([double? offset]) async { if (isPinned.value) { // 如果已经吸顶,先给父滚动权限 WidgetsBinding.instance.addPostFrameCallback((_) { isPinned.value = false; // 直接滚动到指定位置 scrollController.jumpTo(positions.value); // 重置滚动位置 positions.value = 0.0; }); } } Future loadData([int? tabIndex]) async { final index = tabIndex ?? currentTabIndex.value; if (index == 0) { if (itemsParams.isLoading || !itemsParams.hasMore.value) return; itemsParams.isLoading = true; // itemsParams.isInitLoading = true; 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.value) return; favoriteParams.isLoading = true; // favoriteParams.isInitLoading = true; 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) { favoriteParams.hasMore.value = false; } favoriteParams.page++; favoriteParams.isLoading = false; favoriteParams.isInitLoading = false; } } void initControllers() { tabController = TabController(initialIndex: 0, length: tabList.length, vsync: this); scrollController = ScrollController(); if (Common.isLogin()) { imUserInfoController = Get.find(); } } // 初始化页面数据 void refreshData([int? tabIndex]) async { if (!mounted) { logger.i('未挂载'); return; } isLogin.value = Common.isLogin(); if (!Common.isLogin()) return; final idx = tabIndex ?? currentTabIndex.value; // 恢复位置 WidgetsBinding.instance.addPostFrameCallback((_) { scrollInnerList(); }); items.clear(); favoriteItems.clear(); itemsParams.init(); favoriteParams.init(); // currentTabIndex.value = 0; selfInfo(); loadData(idx); } // 获取当前登录用户基本信息 void selfInfo() async { // imUserInfoController = Get.find(); if (!Get.isRegistered()) { logger.e('用户信息controller未注册'); return; } final res = await ImService.instance.getUserFollowInfo(userIDList: [imUserInfoController?.userID.value ?? '']); if (res.success) { //这里少个点赞,从服务端获取 // followersCount粉丝,多少人关注了我,mutualFollowersCount互关,followingCount我关注了多少人 followInfo.value = res.data!.first; logger.i(followInfo.value!.toJson()); } } // 删除当前用户发布的视频 void deletePersonalVideo(videoId) async { logger.i('删除视频${videoId}'); try { final res = await Http.post('${VideoApi.deleteVideo}', data: { 'vlogerId':videoId }); logger.i('删除成功响应${res}'); if(res != null && res['code'] == 200){ MyDialog.toast('删除成功', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.green.withAlpha(200))); loadData(0); } } catch (e) { } } // 二维码名片弹窗 void qrcodeAlertDialog(BuildContext context) { showDialog( context: context, builder: (context) { return UnconstrainedBox( constrainedAxis: Axis.vertical, child: SizedBox( width: 350.0, child: AlertDialog( contentPadding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 20.0), backgroundColor: const Color(0xff07c160), surfaceTintColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), content: Padding( padding: const EdgeInsets.symmetric(horizontal: 10.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Image.asset('assets/images/pic1.jpg', width: 250.0, fit: BoxFit.contain), const SizedBox(height: 15.0), const Text('扫一扫,加好友', style: TextStyle( color: Colors.white38, fontSize: 14.0, )), ], ), ), ), ), ); }, ); } // 退出登录弹窗 void showLogoutDialog(BuildContext context) { showDialog( context: context, builder: (context) { return AlertDialog( content: const Text('确认退出当前账号吗?', style: TextStyle(fontSize: 16.0)), backgroundColor: Colors.white, surfaceTintColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), elevation: 2.0, actionsPadding: const EdgeInsets.all(15.0), actions: [ TextButton( onPressed: () { Get.back(); }, child: const Text('取消', style: TextStyle(color: Colors.black54)), ), TextButton(onPressed: handleLogout, child: const Text('退出登录', style: TextStyle(color: Colors.red))), ], ); }, ); } // 退出登录 void handleLogout() async { final loginRes = await ImService.instance.logout(); if (loginRes.success) { // 清除存储信息 Common.logout(); // 初始化视频 final videoController = Get.find(); videoController.init(); Get.back(); } } @override Widget build(BuildContext context) { // 如果没登录,直接返回一个登录提示页面 return Obx(() { if (!isLogin.value) { return SizedBox(); } else { return Scaffold( backgroundColor: const Color(0xFFFAF6F9), 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: [ const SizedBox(height: 10), Obx(() => _buildStatsCard()), const SizedBox(height: 10), Obx(() => _buildInfoDesc(context)), const SizedBox(height: 10.0), _buildOrderCard(context), const SizedBox(height: 10.0), ], ), ), ), 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), // Tab 2: _buildGridTab(1) ], ), ), ); } }); } // 空状态提示 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 _buildInfoDesc(BuildContext context) { final tx = imUserInfoController?.signature; if (tx == null || tx.isEmpty) { return const SizedBox.shrink(); } return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [BoxShadow(color: Colors.black.withAlpha(10), offset: const Offset(0.0, 1.0), blurRadius: 2.0, spreadRadius: 0.0)], ), clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(12), width: double.infinity, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), ), child: Text( '${imUserInfoController!.signature}', style: const TextStyle(fontSize: 16), ), ), ], )); } Widget _buildGridTab(int tabIndex) { final listToShow = tabIndex == 0 ? items : favoriteItems; PageParams params = tabIndex == 0 ? itemsParams : favoriteParams; if (params.isInitLoading) { return Center(child: CircularProgressIndicator()); } if (listToShow.isEmpty) { return emptyTip('暂无相关数据'); } return Obx(() { return CustomScrollView( // physics: !isPinned.value ? NeverScrollableScrollPhysics() : AlwaysScrollableScrollPhysics(), // physics: AlwaysScrollableScrollPhysics(), key: PageStorageKey('myindex_$tabIndex'), slivers: [ SliverPadding( padding: 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: _buildVdCard(listToShow[index],tabIndex), ); }, childCount: listToShow.length, ), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, crossAxisSpacing: 10.0, mainAxisSpacing: 10.0, childAspectRatio: 0.6, ), ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.symmetric(vertical: 20.0), child: Center( child: params.hasMore.value ? CircularProgressIndicator() : Text('没有更多数据了'), ), ), ), ], ); }); } Widget _buildVdCard(item,tabIndex) { return InkWell( onTap: () { //去视频详情 Get.toNamed('/videoDetail', arguments: {'videoId': item['id']}); }, onLongPress: () { if(tabIndex == 0){ // 个人发布作品可以长按删除 showModalBottomSheet( context: Get.context!, backgroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (context) { return SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ // ListTile( // leading: const Icon(Icons.lock, color: Colors.black), // title: const Text('设为私密', style: TextStyle(color: Colors.black)), // onTap: () { // Navigator.pop(context); // // TODO: 修改为私密逻辑 // }, // ), ListTile( leading: const Icon(Icons.delete, color: Colors.redAccent), title: const Text('删除视频', style: TextStyle(color: Colors.redAccent)), onTap: () async { Get.back(); final confirmed = await ConfirmDialog.show( title: "提示", content: "确认要删除吗?", ); if (confirmed == true) { Get.back(); deletePersonalVideo(item['id']); } }, ), ], ), ); }, ); } }, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12.0), color: Colors.grey[900], ), child: Stack( children: [ /// 视频缩略图 ClipRRect( borderRadius: BorderRadius.circular(12.0), child: NetworkOrAssetImage( imageUrl: item['cover'] ?? item['firstFrameImg'] ?? '', placeholderAsset: 'assets/images/bk.jpg', fit: BoxFit.cover, width: double.infinity, height: double.infinity, ), ), /// 右下角的点赞数 Positioned( right: 8, bottom: 8, child: Row( children: [ const Icon(Icons.favorite, color: Colors.white, size: 16), const SizedBox(width: 4), Text( '${item['likeCounts'] ?? 0}', style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ), ], ), ), ], ), ), ); } Widget _buildIcon(String assetPath, VoidCallback onTap) { return InkWell( onTap: onTap, 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: SvgPicture.asset(assetPath, height: 20.0, width: 20.0, colorFilter: const ColorFilter.mode(Colors.white70, BlendMode.srcIn)), ), ); } Widget _buildFlexibleSpace() { return LayoutBuilder( 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: [ // 背景图 Obx Obx(() { final bgUrl = imUserInfoController?.customInfo['coverBg'] ?? ''; return NetworkOrAssetImage( imageUrl: bgUrl, placeholderAsset: 'assets/images/bk.jpg', fit: BoxFit.cover, ); }), Positioned( left: 15, bottom: 0, right: 15, child: Container( padding: const EdgeInsets.symmetric(vertical: 10), child: Row( crossAxisAlignment: CrossAxisAlignment.end, 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: [ // 昵称 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), InkWell( onTap: () => qrcodeAlertDialog(context), child: Container( 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), // 用户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)), ), ); }), ], ), ), ], ), ), ), ], ); }, ); } 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: [ // '已售${Utils.graceNumber(int.tryParse(vlogLikeCount?.toString() ?? '0') ?? 0)}', Column(children: [Text('${Utils.graceNumber(vlogLikeCount)}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), SizedBox(height: 3.0), Text('获赞')]), GestureDetector( onTap: () async { // 互相关注 await Get.toNamed('/eachFlow'); refreshData(); }, child: Column(children: [ Text('${followInfo.value?.mutualFollowersCount ?? 0}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), SizedBox(height: 3.0), Text('互关') ]), ), GestureDetector( onTap: () async { // 关注 await Get.toNamed('/flow'); refreshData(); }, child: Column(children: [ Text('${followInfo.value?.followingCount ?? 0}', style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)), SizedBox(height: 3.0), Text('关注') ]), ), GestureDetector( onTap: () async { await Get.toNamed('/fans'); refreshData(); }, child: Column( children: [ Text( '${followInfo.value?.followersCount ?? 0}', style: const TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold), ), const SizedBox(height: 3.0), const Text('粉丝'), ], ), ) ], )), ); } Widget _buildOrderCard(BuildContext context) { 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( children: [ GestureDetector( child: Container( padding: const EdgeInsets.all(10.0), child: Row( children: [ Row( children: [ Image.asset('assets/images/more.png', width: 16.0), const SizedBox(width: 5.0), const Text('常用功能', style: TextStyle(fontSize: 15.0, fontWeight: FontWeight.bold)), ], ), const Spacer(), const Text('全部', style: TextStyle(color: Colors.grey, fontSize: 12.0)), const Icon(Icons.arrow_forward_ios_rounded, color: Colors.grey, size: 12.0), ], ), ), onTap: () { Get.toNamed('/functions'); }, ), Padding( padding: const EdgeInsets.all(10.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildOrderIcon('assets/images/ico_order.png', '订单', () { Get.toNamed('/myOrder'); }), _buildOrderIcon('assets/images/ico_dhx.png', '余额logout', () { showLogoutDialog(context); }), _buildOrderIcon('assets/images/ico_sh.png', '提现vloger', () { Get.toNamed('/vloger'); }), _buildOrderIcon('assets/images/ico_tgm.png', '推广码', () { logger.e('推广码'); }), ], ), ), ], ), ); } Widget _buildOrderIcon(String assetPath, String label, VoidCallback onTap) { return GestureDetector( onTap: onTap, child: Column( children: [ Image.asset(assetPath, width: 24.0), const SizedBox(height: 3.0), Text(label), ], ), ); } }