初始化
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/user/user_info_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
import 'package:hgdj/tools_base/debug_log.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
|
||||
class UserCenterController extends GetxController
|
||||
with GetTickerProviderStateMixin {
|
||||
// ===== 数据 =====
|
||||
final int id;
|
||||
UserInfoModel? model;
|
||||
|
||||
UserCenterController(this.id);
|
||||
|
||||
final titleArr = ['影视', '抖音', '帖子', '图集'];
|
||||
final titleParam = [
|
||||
MediaStyle.Video,
|
||||
MediaStyle.ShortVideo,
|
||||
MediaStyle.Community,
|
||||
MediaStyle.Pic
|
||||
];
|
||||
|
||||
// ===== 头部测量 =====
|
||||
//头部高度需实测:量到之前只渲染 header 本身,量到后才切 SliverAppBar
|
||||
double? expandedHeight;
|
||||
final headerKey = GlobalKey();
|
||||
|
||||
// ===== 交互态 =====
|
||||
//滚过头图后在 AppBar 上显示用户信息条
|
||||
final isShowTitle = false.obs;
|
||||
|
||||
// ===== Controller =====
|
||||
final scrollCtr = ScrollController();
|
||||
late final TabController mainTabCtr;
|
||||
|
||||
// ===== 生命周期 =====
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
mainTabCtr = TabController(length: titleArr.length, vsync: this);
|
||||
scrollCtr.addListener(_onScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
fetchUserInfo();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
scrollCtr.dispose();
|
||||
mainTabCtr.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// ===== 公开方法 =====
|
||||
/// 量出头部实际高度,量到后 build 才切到 NestedScrollView
|
||||
void calculateHeaderHeight() {
|
||||
if (expandedHeight != null) return;
|
||||
final renderBox =
|
||||
headerKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
if (renderBox == null) {
|
||||
debugLog('无法获取 calculateHeaderHeight');
|
||||
} else {
|
||||
expandedHeight = renderBox.size.height;
|
||||
}
|
||||
update(); //量到则切正式布局;没量到则再触发一次 build 重来
|
||||
}
|
||||
|
||||
Future<void> fetchUserInfo() async {
|
||||
//是自己则先拿本地信息垫一下,避免等接口白屏
|
||||
if (globalStore.isMe(id)) {
|
||||
model = globalStore.meInfo;
|
||||
update();
|
||||
}
|
||||
model = await MineService.getUserInfo(id);
|
||||
update();
|
||||
}
|
||||
|
||||
/// 关注/取关成功后更新本地状态;[followStateBeforeAction] 为操作前的关注状态(与 [FollowButton] 回调一致)
|
||||
void onUserFollowSuccess(bool followStateBeforeAction) {
|
||||
if (model == null) return;
|
||||
final wasFollow = followStateBeforeAction;
|
||||
final nowFollow = !wasFollow;
|
||||
|
||||
model!.isFollow = nowFollow;
|
||||
final fans = model!.fans ?? 0;
|
||||
if (nowFollow) {
|
||||
model!.fans = fans + 1;
|
||||
} else if (fans > 0) {
|
||||
model!.fans = fans - 1;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
// ===== 私有方法 =====
|
||||
void _onScroll() => isShowTitle.value = scrollCtr.offset > 190;
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_page/user_center_page/view/user_work_list_view.dart';
|
||||
import 'package:hgdj/hj_page/user_center_page/widget/user_center_header.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
|
||||
import 'package:hgdj/tools_base/widget/follow_button.dart';
|
||||
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
|
||||
|
||||
import '../../tools_base/indicator/custom_tab_indicator.dart';
|
||||
import '../../tools_base/widget/net_image_widget.dart';
|
||||
import 'user_center_logic.dart';
|
||||
|
||||
//用户首页
|
||||
class UserCenterPage extends StatefulWidget {
|
||||
final int uid;
|
||||
|
||||
const UserCenterPage({super.key, required this.uid});
|
||||
|
||||
@override
|
||||
State<UserCenterPage> createState() => _UserCenterPageState();
|
||||
}
|
||||
|
||||
//可从评论/短视频等多处重复 push,用 uniqueTag 隔离每个实例的 logic
|
||||
class _UserCenterPageState extends State<UserCenterPage> with UniqueTagMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<UserCenterController>(
|
||||
init: UserCenterController(widget.uid),
|
||||
tag: uniqueTag,
|
||||
builder: (logic) => Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
body: _buildContent(logic),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(UserCenterController logic) {
|
||||
if (logic.model == null) return const LoadingCenterWidget();
|
||||
// 头部高度未知:先只渲染 header 供测量,量到后 update 会切到下面的正式布局
|
||||
if (logic.expandedHeight == null) {
|
||||
WidgetsBinding.instance
|
||||
.addPostFrameCallback((_) => logic.calculateHeaderHeight());
|
||||
return Container(
|
||||
alignment: Alignment.topCenter,
|
||||
child: UserCenterHeader(key: logic.headerKey, logic: logic),
|
||||
);
|
||||
}
|
||||
return ExtendedNestedScrollView(
|
||||
controller: logic.scrollCtr,
|
||||
onlyOneScrollInBody: true,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
headerSliverBuilder: (_, __) => [_buildAppBar(logic)],
|
||||
body: TabBarView(
|
||||
controller: logic.mainTabCtr,
|
||||
children: List.generate(
|
||||
logic.titleArr.length,
|
||||
(index) =>
|
||||
UserWorkListView(logic.model?.uid ?? 0, logic.titleParam[index])
|
||||
.keepAlive,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBar(UserCenterController logic) {
|
||||
return SliverAppBar(
|
||||
expandedHeight: logic.expandedHeight,
|
||||
centerTitle: false,
|
||||
titleSpacing: 0,
|
||||
title: _buildAppBarTitle(logic),
|
||||
leading: Center(
|
||||
child: InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => Get.back(),
|
||||
child: const Icon(Icons.arrow_back_ios),
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
pinned: true,
|
||||
floating: false,
|
||||
elevation: 0,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
collapseMode: CollapseMode.pin,
|
||||
background: UserCenterHeader(logic: logic),
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(44),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
width: double.infinity,
|
||||
height: 44,
|
||||
color: Colors.black,
|
||||
child: TabBar(
|
||||
controller: logic.mainTabCtr,
|
||||
tabAlignment: TabAlignment.fill,
|
||||
tabs: logic.titleArr
|
||||
.map((e) =>
|
||||
Container(alignment: Alignment.center, child: Text(e)))
|
||||
.toList(),
|
||||
labelStyle: const TextStyle(fontSize: 14),
|
||||
labelColor: const Color(0xE5FFFFFF),
|
||||
unselectedLabelStyle: const TextStyle(fontSize: 14),
|
||||
unselectedLabelColor: const Color(0x73FFFFFF),
|
||||
padding: EdgeInsets.zero,
|
||||
isScrollable: false,
|
||||
labelPadding: EdgeInsets.zero,
|
||||
indicator: CustomIndicator(
|
||||
borderRadius: BorderRadius.circular(1.5),
|
||||
height: 3,
|
||||
width: 13,
|
||||
isGradient: true,
|
||||
offsetY: 4,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//滚过头图后,AppBar 上浮出头像 + 昵称 + 关注按钮
|
||||
Widget _buildAppBarTitle(UserCenterController logic) {
|
||||
return Obx(() {
|
||||
if (!logic.isShowTitle.value) return const SizedBox();
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: Row(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: logic.model?.portrait ?? '',
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 38,
|
||||
),
|
||||
12.sizeBoxW,
|
||||
//保留 Column:它把昵称顶到 Row 顶部,直接放 Text 会变成垂直居中
|
||||
Column(
|
||||
children: [
|
||||
Text(
|
||||
logic.model?.name ?? "",
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xffffffff),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
if (!globalStore.isMe(logic.model?.uid))
|
||||
FollowButton(
|
||||
mediaId: (logic.model?.uid ?? 0).toString(),
|
||||
isFollow: logic.model?.isFollow,
|
||||
followType: FollowEnum.user,
|
||||
successsAction: logic.onUserFollowSuccess,
|
||||
),
|
||||
16.sizeBoxW,
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
import 'package:hgdj/tools_base/base_list_controller.dart';
|
||||
|
||||
class UserWorkListLogic extends ListBaseLogic<VideoModel> {
|
||||
final int uid;
|
||||
final MediaStyle newsType;
|
||||
|
||||
UserWorkListLogic(this.uid, this.newsType);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
loadData();
|
||||
}
|
||||
|
||||
//isRefresh 下拉刷新/加载更多;showLoading 重试时先清空显示 loading
|
||||
void loadData({bool isRefresh = true, bool showLoading = false}) {
|
||||
if (showLoading) {
|
||||
dataList = null;
|
||||
update();
|
||||
}
|
||||
fetchData(isRefresh: isRefresh, fetch: _fetch);
|
||||
}
|
||||
|
||||
Future<(List<VideoModel>?, bool)> _fetch(int page) async {
|
||||
final res = await MineService.fetchPublishes(
|
||||
page: page, uid: uid, newsType: newsType.searchRealm);
|
||||
return (res.list, res.hasNext ?? false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_page/user_center_page/view/user_work_list_logic.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
|
||||
|
||||
import '../../../routers/jump_router.dart';
|
||||
import '../../cartoon/photo_gallery_item.dart';
|
||||
import '../../community/widget/community_post_widget.dart';
|
||||
import '../../home/home_cell_style/video_simple_cell.dart';
|
||||
|
||||
class UserWorkListView extends StatefulWidget {
|
||||
final int uid;
|
||||
final MediaStyle newsType;
|
||||
|
||||
const UserWorkListView(this.uid, this.newsType, {super.key});
|
||||
|
||||
@override
|
||||
State<UserWorkListView> createState() => _UserWorkListViewState();
|
||||
}
|
||||
|
||||
class _UserWorkListViewState extends State<UserWorkListView>
|
||||
with UniqueTagMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<UserWorkListLogic>(
|
||||
init: UserWorkListLogic(widget.uid, widget.newsType),
|
||||
tag: uniqueTag,
|
||||
builder: (logic) {
|
||||
if (logic.isLoading) return const LoadingCenterWidget();
|
||||
if (logic.isEmptyData)
|
||||
return CErrorWidget(
|
||||
retryOnTap: () => logic.loadData(showLoading: true));
|
||||
return pullYsRefresh(
|
||||
onInit: (ctr) => logic.refreshCtr = ctr,
|
||||
onRefresh: (_) => logic.loadData(),
|
||||
onLoading: (_) => logic.loadData(isRefresh: false),
|
||||
child: _buildTypeList(logic),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTypeList(UserWorkListLogic logic) {
|
||||
final list = logic.dataList!;
|
||||
// 帖子:单列列表
|
||||
if (widget.newsType == MediaStyle.Community) {
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: list.length,
|
||||
itemBuilder: (_, index) =>
|
||||
CommunityPostWidget(videoModel: list[index], videoModels: list),
|
||||
);
|
||||
}
|
||||
// 其余(影视/抖音/图集)都是双列网格,只差宽高比与 item 样式
|
||||
final isPic = widget.newsType == MediaStyle.Pic;
|
||||
final isShort = widget.newsType == MediaStyle.ShortVideo;
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 6,
|
||||
childAspectRatio: isShort || isPic ? 168 / 246 : 168 / 154,
|
||||
),
|
||||
itemCount: list.length,
|
||||
itemBuilder: (_, index) => isPic
|
||||
? PhotoGalleryItem(videoModel: list[index], textline: 1)
|
||||
: VideoSimpleCell(
|
||||
videoModel: list[index],
|
||||
textLines: isShort ? 1 : 2,
|
||||
isShowBottom: !isShort,
|
||||
onTap: () =>
|
||||
pushToVideoPage(videoModel: list[index], videoArr: list),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/hj_page/user_center_page/user_center_logic.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/widget/follow_button.dart';
|
||||
|
||||
import '../../../tools_base/widget/net_image_widget.dart';
|
||||
|
||||
/// 用户中心头部:背景图 + 头像 + 昵称 / 作品粉丝数 / 简介
|
||||
class UserCenterHeader extends StatelessWidget {
|
||||
final UserCenterController logic;
|
||||
|
||||
const UserCenterHeader({super.key, required this.logic});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: Stack(
|
||||
children: [
|
||||
//背景图,foregroundDecoration 是压在图上的暗色遮罩
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
height: 190,
|
||||
foregroundDecoration: BoxDecoration(
|
||||
color: const Color(0xff151515).withValues(alpha: 0.1)),
|
||||
child: Image.asset("user_center_bg.webp".mineImgPath,
|
||||
fit: BoxFit.fill),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
//背景图 190 高,头像下沿压过来 24,与背景交叠
|
||||
(190 - 24).sizeBoxH,
|
||||
//头像 + 关注按钮
|
||||
SizedBox(
|
||||
height: 80,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
height: 80,
|
||||
width: 80,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xff030F18)),
|
||||
borderRadius: BorderRadius.circular(40),
|
||||
),
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: logic.model?.portrait ?? "",
|
||||
height: 74,
|
||||
width: 74,
|
||||
borderRadius: 40,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (!globalStore.isMe(logic.model?.uid))
|
||||
..._actionButtons(),
|
||||
],
|
||||
),
|
||||
),
|
||||
12.sizeBoxH,
|
||||
//昵称
|
||||
Text(
|
||||
logic.model?.name ?? "",
|
||||
style: const TextStyle(
|
||||
color: Color(0xffEFEFEF),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
6.sizeBoxH,
|
||||
//作品数 / 粉丝数
|
||||
Text(
|
||||
"${logic.model?.totalWorkCount ?? "0"} 作品 ${logic.model?.fans?.countStr ?? "0"}粉丝",
|
||||
style:
|
||||
const TextStyle(color: Color(0xff656565), fontSize: 12),
|
||||
),
|
||||
24.sizeBoxH,
|
||||
//个人简介
|
||||
if (logic.model?.summary?.isNotEmpty == true)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 24),
|
||||
child: Text(
|
||||
logic.model?.summary ?? "",
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Color(0x99FFFFFF), fontSize: 12, height: 1.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
//关注,仅他人主页展示
|
||||
List<Widget> _actionButtons() {
|
||||
return [
|
||||
FollowButton(
|
||||
mediaId: (logic.model?.uid ?? 0).toString(),
|
||||
followType: FollowEnum.user,
|
||||
isFollow: logic.model?.isFollow,
|
||||
successsAction: logic.onUserFollowSuccess,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user