初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
@@ -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,
),
];
}
}