Browse Source
refactor(home): 重构首页内容卡片和标签页结构
refactor(home): 重构首页内容卡片和标签页结构
- 将内容卡片拆分为独立的 ContentCard 和 _CardHeader 组件- 新增 _NetworkImageWidget 优化图片加载与显示 - 抽离推荐和同城列表为独立的 RecommendTab 和 NearbyTab 页面 - 移除原 HomePage 中的列表构建逻辑,提升代码可维护性 -优化图片展示逻辑,统一处理单张及多张图片显示样式 - 增加用户状态标签(在线、实名认证、直播中)的灵活展示 - 实现点击卡片跳转至用户信息页的功能 -保留原有下拉刷新与上拉加载交互逻辑 - 修复可能因图片加载引起的界面闪退问题- 提升组件复用性和页面渲染性能ios
5 changed files with 877 additions and 979 deletions
Split View
Diff Options
-
520lib/pages/home/content_card.dart
-
638lib/pages/home/home_page.dart
-
140lib/pages/home/nearby_tab.dart
-
140lib/pages/home/recommend_tab.dart
-
418pubspec.lock
@ -1,193 +1,399 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:cached_network_image/cached_network_image.dart'; |
|||
import 'package:dating_touchme_app/generated/assets.dart'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:dating_touchme_app/model/home/marriage_data.dart'; |
|||
import 'package:dating_touchme_app/pages/home/user_information_page.dart'; |
|||
|
|||
class ContentCard extends StatelessWidget { |
|||
// 通用头部组件:头像/昵称/在线/认证/Hi/直播中徽标 |
|||
class _CardHeader extends StatelessWidget { |
|||
final MarriageData item; |
|||
|
|||
const ContentCard({ |
|||
Key? key, |
|||
required this.item, |
|||
}) : super(key: key); |
|||
const _CardHeader({required this.item}); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Card( |
|||
margin: EdgeInsets.only(bottom: 12), |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
elevation: 2, |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
// 用户信息头部 |
|||
_buildUserHeader(), |
|||
|
|||
// 个人描述 |
|||
if (item.description.isNotEmpty) |
|||
Padding( |
|||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), |
|||
child: Text( |
|||
item.description, |
|||
style: TextStyle( |
|||
fontSize: 14, |
|||
color: Colors.black87 |
|||
final bool isLive = true; //item.isLive ?? false; |
|||
final bool isOnline = true; //item.isOnline ?? false; |
|||
|
|||
return Row( |
|||
crossAxisAlignment: CrossAxisAlignment.center, |
|||
children: [ |
|||
// 头像 + 状态小圆点 |
|||
Stack( |
|||
children: [ |
|||
// 头像 + 可选紫色描边 |
|||
Container( |
|||
padding: isLive ? const EdgeInsets.all(2) : EdgeInsets.zero, |
|||
decoration: isLive |
|||
? BoxDecoration( |
|||
borderRadius: BorderRadius.circular(32), |
|||
border: Border.all( |
|||
color: const Color(0xFFB58BFF), |
|||
width: 2, |
|||
), |
|||
) |
|||
: null, |
|||
child: ClipRRect( |
|||
borderRadius: BorderRadius.circular(30), |
|||
child: CachedNetworkImage( |
|||
imageUrl: item.avatar, |
|||
width: 60, |
|||
height: 60, |
|||
imageBuilder: (context, imageProvider) => Container( |
|||
decoration: BoxDecoration( |
|||
image: DecorationImage( |
|||
image: imageProvider, |
|||
fit: BoxFit.cover, |
|||
), |
|||
), |
|||
), |
|||
errorWidget: (context, url, error) => Image.asset( |
|||
Assets.imagesAvatarsExample, |
|||
width: 60, |
|||
height: 60, |
|||
fit: BoxFit.cover, |
|||
), |
|||
), |
|||
strutStyle: const StrutStyle(height: 1.5), |
|||
maxLines: 3, |
|||
overflow: TextOverflow.ellipsis, |
|||
) |
|||
), |
|||
), |
|||
|
|||
// 照片列表 |
|||
if (item.photoList.isNotEmpty) |
|||
_buildImageGrid(), |
|||
|
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildUserHeader() { |
|||
return Padding( |
|||
padding: const EdgeInsets.all(12), |
|||
child: Row( |
|||
crossAxisAlignment: CrossAxisAlignment.center, |
|||
children: [ |
|||
// 用户头像 |
|||
CircleAvatar( |
|||
radius: 30, |
|||
backgroundImage: NetworkImage(_cleanImageUrl(item.avatar)), |
|||
backgroundColor: Colors.grey[200], |
|||
), |
|||
|
|||
// 用户信息 |
|||
Expanded( |
|||
child: Padding( |
|||
padding: const EdgeInsets.only(left: 12), |
|||
child: Column( |
|||
if (isOnline) |
|||
Positioned( |
|||
right: 6, |
|||
bottom: 1, |
|||
child: Container( |
|||
width: 12, |
|||
height: 12, |
|||
decoration: BoxDecoration( |
|||
color: isOnline |
|||
? const Color(0xFF2ED573) |
|||
: const Color(0xFFCCCCCC), // 在线绿色,离线灰色 |
|||
shape: BoxShape.circle, |
|||
boxShadow: isOnline |
|||
? const [ |
|||
BoxShadow( |
|||
color: Color(0x332ED573), |
|||
blurRadius: 4, |
|||
offset: Offset(0, 2), |
|||
), |
|||
] |
|||
: [], |
|||
), |
|||
), |
|||
), |
|||
if (isLive) |
|||
Positioned( |
|||
bottom: 0, |
|||
left: 0, |
|||
right: 0, |
|||
child: Container( |
|||
width: 37, |
|||
height: 14, |
|||
alignment: Alignment.center, // 关键:让子内容居中 |
|||
decoration: BoxDecoration( |
|||
image: DecorationImage( |
|||
image: AssetImage(Assets.imagesBtnBgIcon), |
|||
), |
|||
), |
|||
child: const Text( |
|||
'直播中', |
|||
style: TextStyle( |
|||
color: Colors.white, |
|||
fontSize: 8, |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
const SizedBox(width: 10), |
|||
Expanded( |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Row( |
|||
// 使用Wrap组件让所有标签在空间允许时显示在一行,空间不足时自动换行 |
|||
Wrap( |
|||
spacing: 6, |
|||
runSpacing: 2, |
|||
children: [ |
|||
// 用户名 |
|||
Text( |
|||
item.nickName, |
|||
item.name ?? '用户', |
|||
style: TextStyle( |
|||
fontSize: 16, |
|||
fontWeight: FontWeight.bold, |
|||
color: Colors.black |
|||
fontWeight: FontWeight.w500, |
|||
color: Color.fromRGBO(51, 51, 51, 1), |
|||
), |
|||
maxLines: 1, |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
SizedBox(width: 6), |
|||
Text( |
|||
'${item.age}岁', |
|||
style: TextStyle( |
|||
fontSize: 14, |
|||
color: Colors.grey[600] |
|||
), |
|||
), |
|||
// 在线徽标 |
|||
if (isOnline == true) |
|||
Container( |
|||
padding: const EdgeInsets.symmetric( |
|||
horizontal: 8, |
|||
vertical: 2, |
|||
), |
|||
decoration: BoxDecoration( |
|||
color: Color.fromRGBO(234, 255, 219, 1), |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
child: Text( |
|||
isOnline ? '在线' : '', |
|||
style: TextStyle( |
|||
fontSize: 9, |
|||
color: Color.fromRGBO(38, 199, 124, 1), |
|||
), |
|||
), |
|||
), |
|||
// 实名徽标 |
|||
if (item.isRealNameCertified == true) |
|||
Container( |
|||
padding: const EdgeInsets.symmetric( |
|||
horizontal: 8, |
|||
vertical: 2, |
|||
), |
|||
decoration: BoxDecoration( |
|||
color: const Color(0xFFF3E9FF), |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
constraints: BoxConstraints( |
|||
minWidth: 40, // 设置最小宽度以保证视觉效果 |
|||
), |
|||
child: Row( |
|||
mainAxisSize: MainAxisSize.min, // 确保Row只占用必要的宽度 |
|||
children: [ |
|||
Image.asset( |
|||
Assets.imagesVerifiedIcon, |
|||
width: 14, |
|||
height: 12, |
|||
), |
|||
const SizedBox(width: 4), |
|||
const Text( |
|||
'实名', |
|||
style: TextStyle( |
|||
fontSize: 9, |
|||
color: Color.fromRGBO(160, 92, 255, 1), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
// 直播状态下显示视频相亲中标签 |
|||
if (isLive) |
|||
Container( |
|||
padding: const EdgeInsets.symmetric( |
|||
horizontal: 8, |
|||
vertical: 2, |
|||
), |
|||
decoration: BoxDecoration( |
|||
color: Color.fromRGBO(234, 255, 219, 1), |
|||
borderRadius: BorderRadius.circular(12), |
|||
), |
|||
child: Text( |
|||
'视频相亲中', |
|||
style: TextStyle( |
|||
fontSize: 9, |
|||
color: Color.fromRGBO(38, 199, 124, 1), |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
Text( |
|||
item.city, |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
color: Colors.grey[500] |
|||
), |
|||
strutStyle: const StrutStyle(height: 1.2), |
|||
) |
|||
], |
|||
), |
|||
), |
|||
), |
|||
|
|||
// 打招呼按钮 |
|||
GestureDetector( |
|||
onTap: () { |
|||
// 打招呼功能 |
|||
}, |
|||
child: Container( |
|||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6), |
|||
decoration: BoxDecoration( |
|||
color: Color(0xFFFF6B6B), |
|||
borderRadius: BorderRadius.circular(16), |
|||
), |
|||
child: Row( |
|||
children: [ |
|||
Image.asset( |
|||
Assets.imagesHiIcon, |
|||
width: 16, |
|||
height: 16, |
|||
const SizedBox(height: 6), |
|||
SizedBox( |
|||
height: 16, |
|||
child: Text( |
|||
'${item.age ?? 0}岁 · ${item.cityName ?? ''}${item.districtName != null ? item.districtName : ''}', |
|||
style: TextStyle( |
|||
fontSize: 12, |
|||
color: Color.fromRGBO(51, 51, 51, 1), |
|||
), |
|||
], |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
], |
|||
), |
|||
), |
|||
// 根据不同状态显示不同按钮 - 都放在HI按钮相同位置 |
|||
InkWell( |
|||
onTap: () { |
|||
// 点击事件处理 |
|||
if (isLive) { |
|||
// 进入直播间逻辑 |
|||
print('进入直播间'); |
|||
} else if (isOnline) { |
|||
// HI按钮点击逻辑 |
|||
print('HI点击'); |
|||
} else { |
|||
// 发消息按钮点击逻辑 |
|||
print('发送消息'); |
|||
} |
|||
}, |
|||
child: Image.asset( |
|||
_getButtonImage(), |
|||
// width: (item.isLive ?? false) ? 60 : 40, |
|||
width: (true) ? 60 : 40, |
|||
height: 20, |
|||
), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
Widget _buildImageGrid() { |
|||
int imageCount = item.photoList.length; |
|||
|
|||
if (imageCount == 0) return SizedBox(); |
|||
|
|||
// 单张图片 |
|||
if (imageCount == 1) { |
|||
return Container( |
|||
width: double.infinity, |
|||
height: 200, |
|||
String _getButtonImage() { |
|||
// if (item.isLive ?? false) { |
|||
// return Assets.imagesLiveIcon; // 直播显示直播间按钮(放在HI位置) |
|||
// } else if (item.isOnline ?? false) { |
|||
// return Assets.imagesHiIcon; // 在线显示HI按钮 |
|||
// } else { |
|||
// return Assets.imagesSendMessageIcon; // 下线显示发消息按钮(放在HI位置) |
|||
// } |
|||
// } |
|||
|
|||
return Assets.imagesLiveIcon; // 直播显示直播间按钮(放在HI位置) |
|||
} |
|||
} |
|||
|
|||
// 统一的内容卡片组件,根据接口数据显示不同状态 |
|||
class ContentCard extends StatelessWidget { |
|||
final MarriageData item; |
|||
const ContentCard({required this.item}); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return GestureDetector( |
|||
onTap: () { |
|||
// 点击卡片跳转到用户信息页面,传递用户数据 |
|||
Get.to(() => UserInformationPage(userData: item)); |
|||
}, |
|||
child: Container( |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.only( |
|||
bottomLeft: Radius.circular(12), |
|||
bottomRight: Radius.circular(12), |
|||
), |
|||
color: Colors.white, |
|||
borderRadius: BorderRadius.circular(12), |
|||
boxShadow: const [ |
|||
BoxShadow( |
|||
color: Color(0x14000000), |
|||
blurRadius: 8, |
|||
offset: Offset(0, 4), |
|||
), |
|||
], |
|||
), |
|||
child: Image.network( |
|||
_cleanImageUrl(item.photoList[0].photoUrl), |
|||
fit: BoxFit.cover, |
|||
errorBuilder: (context, error, stackTrace) { |
|||
return Image.asset( |
|||
Assets.imagesExampleContent, |
|||
fit: BoxFit.cover, |
|||
); |
|||
}, |
|||
padding: const EdgeInsets.all(12), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
// 头部:头像、昵称、在线状态、按钮等 - 所有按钮都放在HI位置 |
|||
_CardHeader(item: item), |
|||
const SizedBox(height: 8), |
|||
|
|||
// 内容区域 - 根据类型显示不同内容 |
|||
_buildContent(), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
// 多张图片网格布局 |
|||
return GridView.builder( |
|||
shrinkWrap: true, |
|||
physics: NeverScrollableScrollPhysics(), |
|||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( |
|||
crossAxisCount: 3, |
|||
mainAxisSpacing: 2, |
|||
crossAxisSpacing: 2, |
|||
), |
|||
itemCount: imageCount, |
|||
itemBuilder: (context, index) { |
|||
return Container( |
|||
height: 100, |
|||
child: Image.network( |
|||
_cleanImageUrl(item.photoList[index].photoUrl), |
|||
fit: BoxFit.cover, |
|||
errorBuilder: (context, error, stackTrace) { |
|||
return Image.asset( |
|||
Assets.imagesExampleContent, |
|||
fit: BoxFit.cover, |
|||
); |
|||
}, |
|||
), |
|||
); |
|||
} |
|||
|
|||
Widget _buildContent() { |
|||
return Builder( |
|||
builder: (context) { |
|||
final List<Widget> contentWidgets = []; |
|||
// 内容文本 |
|||
if (item.describeInfo != null && item.describeInfo!.isNotEmpty) { |
|||
contentWidgets.add( |
|||
SizedBox( |
|||
// height: 20, // 固定高度20 |
|||
child: Text( |
|||
item.describeInfo!, |
|||
style: TextStyle( |
|||
fontSize: 13, |
|||
color: Color.fromRGBO(51, 51, 51, 0.6), |
|||
), |
|||
overflow: TextOverflow.ellipsis, // 文本超出显示... |
|||
maxLines: 1, // 限制为单行 |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
// 根据接口返回的图片列表动态显示图片 |
|||
if (item.photoList != null && item.photoList!.isNotEmpty) { |
|||
contentWidgets.add(const SizedBox(height: 16)); |
|||
|
|||
// 计算固定宽度:每张图片的宽度 = (屏幕宽度 - 卡片左右padding - ListView左右padding - 图片间距) / 3 |
|||
// 卡片左右padding: 12 * 2 = 24 |
|||
// ListView左右padding: 12 * 2 = 24 |
|||
// 3张图片时,间距: 12 * 2 = 24 (第2张和第3张左边各12) |
|||
final screenWidth = MediaQuery.of(context).size.width; |
|||
final cardPadding = 12.0 * 2; // 卡片左右padding |
|||
final listPadding = 12.0 * 2; // ListView左右padding |
|||
final imageSpacing = 12.0 * 2; // 3张图片时的总间距 |
|||
final imageWidth = |
|||
(screenWidth - cardPadding - listPadding - imageSpacing) / 3; |
|||
final imageHeight = imageWidth * 1.05; // aspectRatio 1.05 |
|||
|
|||
// 统一使用相同的布局:无论1张、2张还是3张,都使用相同的显示方式和大小 |
|||
// 最多显示3张,按顺序排列 |
|||
final displayPhotos = item.photoList!.take(3).toList(); |
|||
contentWidgets.add( |
|||
Row( |
|||
children: displayPhotos.asMap().entries.map((entry) { |
|||
int index = entry.key; |
|||
String imageUrl = entry.value.photoUrl ?? ''; |
|||
return Padding( |
|||
padding: EdgeInsets.only(left: index > 0 ? 12 : 0), |
|||
child: SizedBox( |
|||
width: imageWidth, |
|||
height: imageHeight, |
|||
child: _NetworkImageWidget( |
|||
imageUrl: imageUrl, |
|||
aspectRatio: 1.05, |
|||
), |
|||
), |
|||
); |
|||
}).toList(), |
|||
), |
|||
); |
|||
} |
|||
|
|||
return Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: contentWidgets, |
|||
); |
|||
}, |
|||
); |
|||
} |
|||
|
|||
// 清理图片URL中的空格和多余字符 |
|||
String _cleanImageUrl(String url) { |
|||
return url.trim(); |
|||
} |
|||
|
|||
// 网络图片组件,支持加载网络图片并显示占位图 - 修复可能的闪退问题 |
|||
class _NetworkImageWidget extends StatelessWidget { |
|||
final String imageUrl; |
|||
final double aspectRatio; |
|||
const _NetworkImageWidget({ |
|||
required this.imageUrl, |
|||
required this.aspectRatio, |
|||
}); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Container( |
|||
decoration: BoxDecoration( |
|||
borderRadius: BorderRadius.circular(8), |
|||
image: DecorationImage( |
|||
image: imageUrl.isNotEmpty |
|||
? NetworkImage(imageUrl) |
|||
: AssetImage(Assets.imagesAvatarsExample) as ImageProvider, |
|||
fit: BoxFit.cover, |
|||
), |
|||
), |
|||
width: double.infinity, |
|||
height: double.infinity, |
|||
); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,140 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:easy_refresh/easy_refresh.dart'; |
|||
import 'package:dating_touchme_app/controller/home/home_controller.dart'; |
|||
import 'package:dating_touchme_app/model/home/marriage_data.dart'; |
|||
import 'package:dating_touchme_app/pages/home/content_card.dart'; |
|||
|
|||
/// 同城列表 Tab |
|||
class NearbyTab extends StatefulWidget { |
|||
const NearbyTab({super.key}); |
|||
|
|||
@override |
|||
State<NearbyTab> createState() => _NearbyTabState(); |
|||
} |
|||
|
|||
class _NearbyTabState extends State<NearbyTab> |
|||
with AutomaticKeepAliveClientMixin { |
|||
final HomeController controller = Get.find<HomeController>(); |
|||
late final EasyRefreshController _refreshController; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_refreshController = EasyRefreshController( |
|||
controlFinishRefresh: true, |
|||
controlFinishLoad: true, |
|||
); |
|||
} |
|||
|
|||
@override |
|||
void dispose() { |
|||
_refreshController.dispose(); |
|||
super.dispose(); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
super.build(context); |
|||
// 获取底部安全区域高度和 tabbar 高度(约64) |
|||
final bottomPadding = MediaQuery.of(context).padding.bottom; |
|||
final tabBarHeight = 64.0; |
|||
final totalBottomPadding = bottomPadding + tabBarHeight; |
|||
|
|||
return Obx(() { |
|||
final List<MarriageData> dataSource = controller.nearbyFeed; |
|||
final bool isLoading = controller.nearbyIsLoading.value; |
|||
final bool hasMore = controller.nearbyHasMore.value; |
|||
|
|||
return EasyRefresh( |
|||
controller: _refreshController, |
|||
header: MaterialHeader(backgroundColor: Colors.red.withOpacity(0.9)), |
|||
footer: MaterialFooter(backgroundColor: Colors.red.withOpacity(0.9)), |
|||
// 下拉刷新 |
|||
onRefresh: () async { |
|||
print('同城列表下拉刷新被触发'); |
|||
try { |
|||
await controller.refreshNearbyData(); |
|||
_refreshController.finishRefresh(IndicatorResult.success); |
|||
print('同城列表刷新完成'); |
|||
} catch (e) { |
|||
print('同城列表刷新失败: $e'); |
|||
_refreshController.finishRefresh(IndicatorResult.fail); |
|||
} |
|||
}, |
|||
// 上拉加载更多 |
|||
onLoad: () async { |
|||
print('同城列表上拉加载被触发, hasMore: $hasMore'); |
|||
if (hasMore && controller.nearbyHasMore.value) { |
|||
try { |
|||
await controller.loadNearbyMoreData(); |
|||
// 完成加载,根据是否有更多数据决定 |
|||
if (controller.nearbyHasMore.value) { |
|||
_refreshController.finishLoad(IndicatorResult.success); |
|||
print('同城列表加载更多成功'); |
|||
} else { |
|||
_refreshController.finishLoad(IndicatorResult.noMore); |
|||
print('同城列表没有更多数据了'); |
|||
} |
|||
} catch (e) { |
|||
print('同城列表加载更多失败: $e'); |
|||
_refreshController.finishLoad(IndicatorResult.fail); |
|||
} |
|||
} else { |
|||
_refreshController.finishLoad(IndicatorResult.noMore); |
|||
print('同城列表没有更多数据'); |
|||
} |
|||
}, |
|||
// EasyRefresh 的 child 必须始终是可滚动的 Widget |
|||
child: ListView.separated( |
|||
// 关键:始终允许滚动,即使内容不足 |
|||
physics: const AlwaysScrollableScrollPhysics( |
|||
parent: BouncingScrollPhysics(), |
|||
), |
|||
// 移除顶部 padding,让刷新指示器可以正确显示在 AppBar 下方 |
|||
padding: EdgeInsets.only( |
|||
left: 12, |
|||
right: 12, |
|||
bottom: totalBottomPadding + 12, |
|||
), |
|||
itemBuilder: (context, index) { |
|||
// 加载状态 |
|||
if (isLoading && dataSource.isEmpty && index == 0) { |
|||
// 使用足够的高度确保可以滚动 |
|||
return SizedBox( |
|||
height: MediaQuery.of(context).size.height * 1.2, |
|||
child: const Center(child: CircularProgressIndicator()), |
|||
); |
|||
} |
|||
// 空数据状态 |
|||
if (!isLoading && dataSource.isEmpty && index == 0) { |
|||
// 使用足够的高度确保可以滚动 |
|||
return SizedBox( |
|||
height: MediaQuery.of(context).size.height * 1.2, |
|||
child: const Center( |
|||
child: Text( |
|||
"暂无数据", |
|||
style: TextStyle(fontSize: 14, color: Color(0xFF999999)), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
// 数据项 |
|||
final item = dataSource[index]; |
|||
return ContentCard(item: item); |
|||
}, |
|||
separatorBuilder: (context, index) { |
|||
// 空状态或加载状态时不显示分隔符 |
|||
if (dataSource.isEmpty) return const SizedBox.shrink(); |
|||
return const SizedBox(height: 12); |
|||
}, |
|||
// 至少显示一个 item(用于显示加载或空状态) |
|||
itemCount: dataSource.isEmpty ? 1 : dataSource.length, |
|||
), |
|||
); |
|||
}); |
|||
} |
|||
|
|||
@override |
|||
bool get wantKeepAlive => true; |
|||
} |
|||
@ -0,0 +1,140 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:easy_refresh/easy_refresh.dart'; |
|||
import 'package:dating_touchme_app/controller/home/home_controller.dart'; |
|||
import 'package:dating_touchme_app/model/home/marriage_data.dart'; |
|||
import 'package:dating_touchme_app/pages/home/content_card.dart'; |
|||
|
|||
/// 推荐列表 Tab |
|||
class RecommendTab extends StatefulWidget { |
|||
const RecommendTab({super.key}); |
|||
|
|||
@override |
|||
State<RecommendTab> createState() => _RecommendTabState(); |
|||
} |
|||
|
|||
class _RecommendTabState extends State<RecommendTab> |
|||
with AutomaticKeepAliveClientMixin { |
|||
final HomeController controller = Get.find<HomeController>(); |
|||
late final EasyRefreshController _refreshController; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
_refreshController = EasyRefreshController( |
|||
controlFinishRefresh: true, |
|||
controlFinishLoad: true, |
|||
); |
|||
} |
|||
|
|||
@override |
|||
void dispose() { |
|||
_refreshController.dispose(); |
|||
super.dispose(); |
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
super.build(context); |
|||
// 获取底部安全区域高度和 tabbar 高度(约64) |
|||
final bottomPadding = MediaQuery.of(context).padding.bottom; |
|||
final tabBarHeight = 64.0; |
|||
final totalBottomPadding = bottomPadding + tabBarHeight; |
|||
|
|||
return Obx(() { |
|||
final List<MarriageData> dataSource = controller.recommendFeed; |
|||
final bool isLoading = controller.recommendIsLoading.value; |
|||
final bool hasMore = controller.recommendHasMore.value; |
|||
|
|||
return EasyRefresh( |
|||
controller: _refreshController, |
|||
header: MaterialHeader(backgroundColor: Colors.red.withOpacity(0.9)), |
|||
footer: MaterialFooter(backgroundColor: Colors.red.withOpacity(0.9)), |
|||
// 下拉刷新 |
|||
onRefresh: () async { |
|||
print('推荐列表下拉刷新被触发'); |
|||
try { |
|||
await controller.refreshRecommendData(); |
|||
_refreshController.finishRefresh(IndicatorResult.success); |
|||
print('推荐列表刷新完成'); |
|||
} catch (e) { |
|||
print('推荐列表刷新失败: $e'); |
|||
_refreshController.finishRefresh(IndicatorResult.fail); |
|||
} |
|||
}, |
|||
// 上拉加载更多 |
|||
onLoad: () async { |
|||
print('推荐列表上拉加载被触发, hasMore: $hasMore'); |
|||
if (hasMore && controller.recommendHasMore.value) { |
|||
try { |
|||
await controller.loadRecommendMoreData(); |
|||
// 完成加载,根据是否有更多数据决定 |
|||
if (controller.recommendHasMore.value) { |
|||
_refreshController.finishLoad(IndicatorResult.success); |
|||
print('推荐列表加载更多成功'); |
|||
} else { |
|||
_refreshController.finishLoad(IndicatorResult.noMore); |
|||
print('推荐列表没有更多数据了'); |
|||
} |
|||
} catch (e) { |
|||
print('推荐列表加载更多失败: $e'); |
|||
_refreshController.finishLoad(IndicatorResult.fail); |
|||
} |
|||
} else { |
|||
_refreshController.finishLoad(IndicatorResult.noMore); |
|||
print('推荐列表没有更多数据'); |
|||
} |
|||
}, |
|||
// EasyRefresh 的 child 必须始终是可滚动的 Widget |
|||
child: ListView.separated( |
|||
// 关键:始终允许滚动,即使内容不足 |
|||
physics: const AlwaysScrollableScrollPhysics( |
|||
parent: BouncingScrollPhysics(), |
|||
), |
|||
// 移除顶部 padding,让刷新指示器可以正确显示在 AppBar 下方 |
|||
padding: EdgeInsets.only( |
|||
left: 12, |
|||
right: 12, |
|||
bottom: totalBottomPadding + 12, |
|||
), |
|||
itemBuilder: (context, index) { |
|||
// 加载状态 |
|||
if (isLoading && dataSource.isEmpty && index == 0) { |
|||
// 使用足够的高度确保可以滚动 |
|||
return SizedBox( |
|||
height: MediaQuery.of(context).size.height * 1.2, |
|||
child: const Center(child: CircularProgressIndicator()), |
|||
); |
|||
} |
|||
// 空数据状态 |
|||
if (!isLoading && dataSource.isEmpty && index == 0) { |
|||
// 使用足够的高度确保可以滚动 |
|||
return SizedBox( |
|||
height: MediaQuery.of(context).size.height * 1.2, |
|||
child: const Center( |
|||
child: Text( |
|||
"暂无数据", |
|||
style: TextStyle(fontSize: 14, color: Color(0xFF999999)), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
// 数据项 |
|||
final item = dataSource[index]; |
|||
return ContentCard(item: item); |
|||
}, |
|||
separatorBuilder: (context, index) { |
|||
// 空状态或加载状态时不显示分隔符 |
|||
if (dataSource.isEmpty) return const SizedBox.shrink(); |
|||
return const SizedBox(height: 12); |
|||
}, |
|||
// 至少显示一个 item(用于显示加载或空状态) |
|||
itemCount: dataSource.isEmpty ? 1 : dataSource.length, |
|||
), |
|||
); |
|||
}); |
|||
} |
|||
|
|||
@override |
|||
bool get wantKeepAlive => true; |
|||
} |
|||
418
pubspec.lock
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save