flutter/lib/pages/my/vloger.dart

649 lines
24 KiB
Dart
Raw Normal View History

2025-08-21 10:50:38 +08:00
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<Vloger> createState() => MyPageState();
}
class MyPageState extends State<Vloger> with SingleTickerProviderStateMixin {
late dynamic args;
late RxInt currentTabIndex = 0.obs;
late RxList items = [].obs;
late RxList favoriteItems = [].obs;
late final Rx<V2TimUserFullInfo> userInfo = Rx(V2TimUserFullInfo(
userID: '',
nickName: '',
faceUrl: '',
selfSignature: '',
gender: 0,
customInfo: {
'coverBg': '',
},
role: 0,
));
late RxInt followed = 0.obs; // 是否关注
// followersCount粉丝多少人关注了我,mutualFollowersCount互关,followingCount我关注了多少人
late final Rx<V2TimFollowInfo> 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<String> 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 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,
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'] ?? '';
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,
))),
Positioned(
left: 15.0,
bottom: 0,
right: 15.0,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
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(
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
2025-08-21 17:50:34 +08:00
final vlogerId = args['memberId'];
2025-08-21 10:50:38 +08:00
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) {
final offsetAnimation = Tween<Offset>(
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),
),
),
),
),
);
}
}