flutter/lib/pages/my/vloger.dart
2025-08-21 10:50:38 +08:00

649 lines
24 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
final vlogerId = args['vlogerId'];
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),
),
),
),
),
);
}
}