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';
|
2025-09-03 11:25:31 +08:00
|
|
|
|
import 'package:loopin/IM/controller/chat_controller.dart';
|
2025-08-21 10:50:38 +08:00
|
|
|
|
import 'package:loopin/IM/im_service.dart';
|
2025-09-17 18:09:07 +08:00
|
|
|
|
import 'package:loopin/api/common_api.dart';
|
2025-09-15 17:18:10 +08:00
|
|
|
|
import 'package:loopin/api/video_api.dart';
|
2025-09-22 14:41:47 +08:00
|
|
|
|
import 'package:loopin/components/custom_sticky_header.dart';
|
2025-08-21 10:50:38 +08:00
|
|
|
|
import 'package:loopin/components/network_or_asset_image.dart';
|
|
|
|
|
import 'package:loopin/components/only_down_scroll_physics.dart';
|
2025-09-22 14:41:47 +08:00
|
|
|
|
import 'package:loopin/service/http.dart';
|
2025-08-21 10:50:38 +08:00
|
|
|
|
import 'package:loopin/styles/index.dart';
|
2025-09-22 14:41:47 +08:00
|
|
|
|
import 'package:loopin/utils/index.dart';
|
2025-08-21 10:50:38 +08:00
|
|
|
|
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': "作品"},
|
|
|
|
|
];
|
2025-09-17 18:09:07 +08:00
|
|
|
|
late int vlogLikeCount = 0; // 点赞数量
|
2025-08-21 10:50:38 +08:00
|
|
|
|
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 ?? {};
|
2025-09-22 14:41:47 +08:00
|
|
|
|
print('argsssssssssssssssssssssss$args');
|
2025-08-21 10:50:38 +08:00
|
|
|
|
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);
|
2025-09-17 18:09:07 +08:00
|
|
|
|
getUserLikesCount();
|
2025-08-21 10:50:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
tabController.removeListener(tabListener);
|
|
|
|
|
scrollController.removeListener(scrollListener);
|
|
|
|
|
|
|
|
|
|
tabController.dispose();
|
|
|
|
|
scrollController.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-22 14:41:47 +08:00
|
|
|
|
// 获取用户的所有视频的点赞数量
|
2025-09-17 18:09:07 +08:00
|
|
|
|
void getUserLikesCount() async {
|
|
|
|
|
try {
|
2025-09-22 14:41:47 +08:00
|
|
|
|
final resData = await Http.get('${CommonApi.accountInfo}?memberId=${args['memberId']}');
|
|
|
|
|
print('aaaaaaaaaaaaaaaaaaa$resData');
|
|
|
|
|
if (resData != null && resData['code'] == 200) {
|
|
|
|
|
vlogLikeCount = resData['data']['vlogLikeCount'] ?? 0;
|
2025-09-17 18:09:07 +08:00
|
|
|
|
}
|
2025-09-22 14:41:47 +08:00
|
|
|
|
} catch (e) {}
|
2025-09-17 18:09:07 +08:00
|
|
|
|
}
|
2025-09-22 14:41:47 +08:00
|
|
|
|
|
2025-08-21 10:50:38 +08:00
|
|
|
|
void loadData([int? tabIndex]) async {
|
|
|
|
|
final index = tabIndex ?? currentTabIndex.value;
|
|
|
|
|
if (index == 0) {
|
|
|
|
|
if (itemsParams.isLoading || !itemsParams.hasMore) return;
|
2025-09-22 14:41:47 +08:00
|
|
|
|
itemsParams.isLoading = true;
|
|
|
|
|
final res = await Http.post(VideoApi.getVideoListByMemberId, data: {
|
|
|
|
|
"memberId": args['memberId'],
|
|
|
|
|
"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 = false;
|
|
|
|
|
}
|
|
|
|
|
// 页码加一
|
|
|
|
|
itemsParams.page++;
|
|
|
|
|
//
|
|
|
|
|
itemsParams.isLoading = false;
|
2025-08-21 10:50:38 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
2025-08-25 18:07:40 +08:00
|
|
|
|
return PopScope(
|
|
|
|
|
canPop: true,
|
|
|
|
|
onPopInvokedWithResult: (bool didPop, Object? result) {
|
|
|
|
|
if (didPop) {
|
|
|
|
|
print('User navigated back');
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-09-03 11:25:31 +08:00
|
|
|
|
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,
|
2025-08-25 18:07:40 +08:00
|
|
|
|
leading: IconButton(
|
2025-09-03 11:25:31 +08:00
|
|
|
|
icon: const Icon(Icons.arrow_back),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
Get.back(result: {
|
2025-08-25 18:07:40 +08:00
|
|
|
|
'returnTo': '/',
|
|
|
|
|
'vlogerId': args['memberId'],
|
2025-09-03 11:25:31 +08:00
|
|
|
|
'followStatus': (followed.value == 1 || followed.value == 3) ? true : false,
|
2025-08-25 18:07:40 +08:00
|
|
|
|
});
|
2025-09-03 11:25:31 +08:00
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
onStretchTrigger: () async {
|
|
|
|
|
logger.i('触发 stretch 拉伸');
|
|
|
|
|
// 加载刷新逻辑
|
2025-08-25 18:07:40 +08:00
|
|
|
|
},
|
2025-09-03 11:25:31 +08:00
|
|
|
|
flexibleSpace: Obx(() {
|
|
|
|
|
userInfo.value;
|
|
|
|
|
return _buildFlexibleSpace();
|
|
|
|
|
}),
|
2025-08-25 18:07:40 +08:00
|
|
|
|
),
|
2025-09-03 11:25:31 +08:00
|
|
|
|
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)),
|
|
|
|
|
],
|
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
),
|
|
|
|
|
),
|
2025-09-03 11:25:31 +08:00
|
|
|
|
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),
|
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
2025-09-03 11:25:31 +08:00
|
|
|
|
];
|
|
|
|
|
},
|
|
|
|
|
body: TabBarView(
|
|
|
|
|
controller: tabController,
|
|
|
|
|
children: [
|
|
|
|
|
// Tab 1:
|
|
|
|
|
Obx(() => _buildGridTab(0)),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 空状态提示
|
|
|
|
|
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) {
|
2025-09-17 18:09:07 +08:00
|
|
|
|
final item = listToShow[index];
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
onTap: () {
|
|
|
|
|
// 点击跳转到视频播放详情页面
|
2025-09-22 14:41:47 +08:00
|
|
|
|
Get.toNamed('/videoDetail', arguments: {'videoId': item['id']});
|
2025-09-17 18:09:07 +08:00
|
|
|
|
},
|
|
|
|
|
child: Container(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
|
|
|
boxShadow: [
|
|
|
|
|
BoxShadow(
|
|
|
|
|
color: Colors.black.withOpacity(0.1),
|
|
|
|
|
blurRadius: 4,
|
|
|
|
|
offset: const Offset(0, 2),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
child: ClipRRect(
|
|
|
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
|
|
|
child: Stack(
|
|
|
|
|
fit: StackFit.expand,
|
|
|
|
|
children: [
|
|
|
|
|
// 视频封面图片
|
|
|
|
|
NetworkOrAssetImage(
|
|
|
|
|
imageUrl: item['firstFrameImg'] ?? item['cover'],
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
height: double.infinity,
|
|
|
|
|
fit: BoxFit.cover,
|
2025-09-22 14:41:47 +08:00
|
|
|
|
placeholderAsset: 'assets/images/bk.jpg',
|
2025-09-17 18:09:07 +08:00
|
|
|
|
),
|
|
|
|
|
// 半透明渐变底部背景
|
|
|
|
|
Positioned(
|
|
|
|
|
bottom: 0,
|
|
|
|
|
left: 0,
|
|
|
|
|
right: 0,
|
|
|
|
|
child: Container(
|
|
|
|
|
height: 40,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
begin: Alignment.bottomCenter,
|
|
|
|
|
end: Alignment.topCenter,
|
|
|
|
|
colors: [
|
|
|
|
|
Colors.black.withOpacity(0.6),
|
|
|
|
|
Colors.transparent,
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// 右下角点赞数
|
|
|
|
|
Positioned(
|
|
|
|
|
right: 8,
|
|
|
|
|
bottom: 8,
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
2025-09-22 14:41:47 +08:00
|
|
|
|
const Icon(
|
|
|
|
|
Icons.favorite,
|
2025-09-17 18:09:07 +08:00
|
|
|
|
color: Colors.white,
|
|
|
|
|
size: 16,
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 4),
|
|
|
|
|
Text(
|
|
|
|
|
'${item['likeCounts'] ?? 0}',
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
childCount: listToShow.length,
|
|
|
|
|
),
|
|
|
|
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
|
|
|
crossAxisCount: 3,
|
|
|
|
|
crossAxisSpacing: 10.0,
|
|
|
|
|
mainAxisSpacing: 10.0,
|
2025-09-17 18:09:07 +08:00
|
|
|
|
childAspectRatio: 0.7, // 调整为更适合视频封面的比例
|
2025-08-21 10:50:38 +08:00
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
SliverToBoxAdapter(
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 20.0),
|
|
|
|
|
child: Center(
|
2025-09-22 14:41:47 +08:00
|
|
|
|
child: params.hasMore ? const CircularProgressIndicator() : const Text('没有更多数据了'),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
return Stack(
|
|
|
|
|
fit: StackFit.expand,
|
|
|
|
|
children: [
|
|
|
|
|
Positioned.fill(
|
2025-08-26 17:38:59 +08:00
|
|
|
|
child: Opacity(
|
|
|
|
|
opacity: 1.0,
|
|
|
|
|
child: NetworkOrAssetImage(
|
2025-09-03 11:25:31 +08:00
|
|
|
|
imageUrl: userInfo.value.customInfo?['coverBg'],
|
2025-08-26 17:38:59 +08:00
|
|
|
|
width: double.infinity,
|
2025-09-03 11:25:31 +08:00
|
|
|
|
placeholderAsset: 'assets/images/bk.jpg',
|
2025-08-26 17:38:59 +08:00
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
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(
|
2025-09-03 11:25:31 +08:00
|
|
|
|
child: NetworkOrAssetImage(
|
|
|
|
|
imageUrl: userInfo.value.faceUrl,
|
|
|
|
|
width: 80,
|
|
|
|
|
height: 80,
|
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
),
|
|
|
|
|
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: [
|
2025-09-22 14:41:47 +08:00
|
|
|
|
Column(children: [
|
|
|
|
|
Text(Utils.graceNumber(vlogLikeCount), style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold)),
|
|
|
|
|
SizedBox(height: 3.0),
|
|
|
|
|
Text('获赞')
|
|
|
|
|
]),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
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,
|
|
|
|
|
);
|
|
|
|
|
},
|
2025-09-03 11:25:31 +08:00
|
|
|
|
child: [1, 2, 3].contains(followed.value)
|
2025-08-21 10:50:38 +08:00
|
|
|
|
? Row(
|
|
|
|
|
key: const ValueKey('followed'),
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: ElevatedButton(
|
|
|
|
|
onPressed: () async {
|
2025-09-03 11:25:31 +08:00
|
|
|
|
logger.w('点击已关注/已回关:${followed.value}');
|
|
|
|
|
final ctl = Get.find<ChatController>();
|
|
|
|
|
|
|
|
|
|
if (followed.value == 2) {
|
|
|
|
|
//回关,走关注逻辑
|
|
|
|
|
final res = await ImService.instance.followUser(userIDList: [vlogerId]);
|
|
|
|
|
if (res.success) {
|
|
|
|
|
followed.value = 3;
|
|
|
|
|
// 回关后,如果会话存在,将此会话移除noFriend会话组
|
|
|
|
|
final res = await ImService.instance.getConversation(conversationID: 'c2c_$vlogerId');
|
|
|
|
|
if (res.success) {
|
|
|
|
|
V2TimConversation conversation = res.data;
|
|
|
|
|
if (conversation.conversationGroupList?.isNotEmpty ?? false) {
|
|
|
|
|
//把会话数据从陌生人分组移除
|
|
|
|
|
await ImService.instance.deleteConversationsFromGroup(
|
|
|
|
|
groupName: conversation.conversationGroupList!.first!, // 不涉及到加入多分组的情况,直接取
|
|
|
|
|
conversationIDList: [conversation.conversationID],
|
|
|
|
|
);
|
|
|
|
|
ctl.updateNoFriendMenu();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
final res = await ImService.instance.unfollowUser(userIDList: [vlogerId]);
|
|
|
|
|
if (res.success) {
|
|
|
|
|
// 如果为1那么状态置为0,为3则置为2
|
|
|
|
|
followed.value = followed.value == 1 ? 0 : 2;
|
|
|
|
|
ctl.mergeNoFriend(conversationID: 'c2c_${userInfo.value.userID}');
|
|
|
|
|
}
|
2025-08-21 10:50:38 +08:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
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
|
|
|
|
|
? '已关注'
|
2025-09-03 11:25:31 +08:00
|
|
|
|
: followed.value == 2
|
|
|
|
|
? '回关'
|
|
|
|
|
: followed.value == 3
|
|
|
|
|
? '已互关'
|
|
|
|
|
: '未知状态'),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: ElevatedButton(
|
|
|
|
|
onPressed: () async {
|
2025-09-03 11:25:31 +08:00
|
|
|
|
logger.w('私信');
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// 获取指定会话
|
|
|
|
|
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对方关注了我
|
2025-09-03 11:25:31 +08:00
|
|
|
|
logger.w('点击关注');
|
2025-08-21 10:50:38 +08:00
|
|
|
|
final res = await ImService.instance.followUser(userIDList: [vlogerId]);
|
|
|
|
|
if (res.success) {
|
|
|
|
|
followed.value = followed.value == 0 ? 1 : 3;
|
|
|
|
|
if (followed.value == 3) {
|
2025-09-03 11:25:31 +08:00
|
|
|
|
// 修改后若为3,将此会话移除noFriend会话组
|
2025-08-21 10:50:38 +08:00
|
|
|
|
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),
|
2025-09-03 11:25:31 +08:00
|
|
|
|
label: Text('关注'),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
backgroundColor: FStyle.primaryColor,
|
|
|
|
|
foregroundColor: Colors.white,
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|