11 changed files with 780 additions and 262 deletions
Unified View
Diff Options
-
67lib/controller/home/timeline_trend_controller.dart
-
91lib/model/home/trend_data.dart
-
3lib/network/api_urls.dart
-
7lib/network/home_api.dart
-
37lib/network/home_api.g.dart
-
24lib/pages/home/event_info.dart
-
12lib/pages/home/timeline_page.dart
-
243lib/pages/home/timeline_trend.dart
-
523lib/pages/home/user_information_page.dart
-
16lib/pages/mine/edit_info_page.dart
-
19lib/pages/mine/mine_page.dart
@ -0,0 +1,67 @@ |
|||||
|
import 'package:dating_touchme_app/network/home_api.dart'; |
||||
|
import 'package:easy_refresh/easy_refresh.dart'; |
||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
import '../../model/home/trend_data.dart'; |
||||
|
|
||||
|
class TimelineTrendController extends GetxController { |
||||
|
|
||||
|
final trendList = <Records>[].obs; |
||||
|
|
||||
|
final page = 1.obs; |
||||
|
final size = 10.obs; |
||||
|
|
||||
|
late final EasyRefreshController listRefreshController; |
||||
|
|
||||
|
late final HomeApi _homeApi; |
||||
|
|
||||
|
@override |
||||
|
void onInit() { |
||||
|
super.onInit(); |
||||
|
listRefreshController = EasyRefreshController( |
||||
|
controlFinishRefresh: true, |
||||
|
controlFinishLoad: true, |
||||
|
); |
||||
|
// 从全局依赖中获取HomeApi |
||||
|
_homeApi = Get.find<HomeApi>(); |
||||
|
|
||||
|
getTrendData(); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
getTrendData() async { |
||||
|
try { |
||||
|
final response = await _homeApi.userPageOwnPostDynamic( |
||||
|
pageNum: page.value, |
||||
|
pageSize: size.value |
||||
|
); |
||||
|
if (response.data.isSuccess && response.data.data != null) { |
||||
|
final data = response.data.data?.records ?? []; |
||||
|
|
||||
|
trendList.addAll(data); |
||||
|
|
||||
|
if((data.length ?? 0) == size.value){ |
||||
|
|
||||
|
listRefreshController.finishLoad(IndicatorResult.success); |
||||
|
} else { |
||||
|
listRefreshController.finishLoad(IndicatorResult.noMore); |
||||
|
} |
||||
|
|
||||
|
} else { |
||||
|
|
||||
|
// 响应失败,抛出异常 |
||||
|
throw Exception(response.data.message ?? '获取数据失败'); |
||||
|
} |
||||
|
} catch(e){ |
||||
|
print('详情获取失败: $e'); |
||||
|
SmartDialog.showToast('动态失败'); |
||||
|
rethrow; |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,91 @@ |
|||||
|
class TrendData { |
||||
|
List<Records>? records; |
||||
|
int? total; |
||||
|
int? size; |
||||
|
int? current; |
||||
|
int? pages; |
||||
|
|
||||
|
TrendData({this.records, this.total, this.size, this.current, this.pages}); |
||||
|
|
||||
|
TrendData.fromJson(Map<String, dynamic> json) { |
||||
|
if (json['records'] != null) { |
||||
|
records = <Records>[]; |
||||
|
json['records'].forEach((v) { |
||||
|
records!.add(new Records.fromJson(v)); |
||||
|
}); |
||||
|
} |
||||
|
total = json['total']; |
||||
|
size = json['size']; |
||||
|
current = json['current']; |
||||
|
pages = json['pages']; |
||||
|
} |
||||
|
|
||||
|
Map<String, dynamic> toJson() { |
||||
|
final Map<String, dynamic> data = new Map<String, dynamic>(); |
||||
|
if (this.records != null) { |
||||
|
data['records'] = this.records!.map((v) => v.toJson()).toList(); |
||||
|
} |
||||
|
data['total'] = this.total; |
||||
|
data['size'] = this.size; |
||||
|
data['current'] = this.current; |
||||
|
data['pages'] = this.pages; |
||||
|
return data; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class Records { |
||||
|
String? postId; |
||||
|
int? operationType; |
||||
|
String? postCommentContent; |
||||
|
String? userId; |
||||
|
String? miId; |
||||
|
String? nickName; |
||||
|
String? profilePhoto; |
||||
|
String? content; |
||||
|
String? mediaUrls; |
||||
|
String? topicTags; |
||||
|
String? createTime; |
||||
|
|
||||
|
Records( |
||||
|
{this.postId, |
||||
|
this.operationType, |
||||
|
this.postCommentContent, |
||||
|
this.userId, |
||||
|
this.miId, |
||||
|
this.nickName, |
||||
|
this.profilePhoto, |
||||
|
this.content, |
||||
|
this.mediaUrls, |
||||
|
this.topicTags, |
||||
|
this.createTime}); |
||||
|
|
||||
|
Records.fromJson(Map<String, dynamic> json) { |
||||
|
postId = json['postId']; |
||||
|
operationType = json['operationType']; |
||||
|
postCommentContent = json['postCommentContent']; |
||||
|
userId = json['userId']; |
||||
|
miId = json['miId']; |
||||
|
nickName = json['nickName']; |
||||
|
profilePhoto = json['profilePhoto']; |
||||
|
content = json['content']; |
||||
|
mediaUrls = json['mediaUrls']; |
||||
|
topicTags = json['topicTags']; |
||||
|
createTime = json['createTime']; |
||||
|
} |
||||
|
|
||||
|
Map<String, dynamic> toJson() { |
||||
|
final Map<String, dynamic> data = new Map<String, dynamic>(); |
||||
|
data['postId'] = this.postId; |
||||
|
data['operationType'] = this.operationType; |
||||
|
data['postCommentContent'] = this.postCommentContent; |
||||
|
data['userId'] = this.userId; |
||||
|
data['miId'] = this.miId; |
||||
|
data['nickName'] = this.nickName; |
||||
|
data['profilePhoto'] = this.profilePhoto; |
||||
|
data['content'] = this.content; |
||||
|
data['mediaUrls'] = this.mediaUrls; |
||||
|
data['topicTags'] = this.topicTags; |
||||
|
data['createTime'] = this.createTime; |
||||
|
return data; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,243 @@ |
|||||
|
import 'package:cached_network_image/cached_network_image.dart'; |
||||
|
import 'package:dating_touchme_app/components/page_appbar.dart'; |
||||
|
import 'package:dating_touchme_app/config/emoji_config.dart'; |
||||
|
import 'package:dating_touchme_app/controller/home/timeline_trend_controller.dart'; |
||||
|
import 'package:dating_touchme_app/extension/ex_widget.dart'; |
||||
|
import 'package:dating_touchme_app/generated/assets.dart'; |
||||
|
import 'package:dating_touchme_app/model/home/trend_data.dart'; |
||||
|
import 'package:dating_touchme_app/pages/home/timeline_info.dart'; |
||||
|
import 'package:easy_refresh/easy_refresh.dart'; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
class TimelineTrend extends StatelessWidget { |
||||
|
const TimelineTrend({super.key}); |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return GetX<TimelineTrendController>( |
||||
|
init: TimelineTrendController(), |
||||
|
builder: (controller){ |
||||
|
return Scaffold( |
||||
|
appBar: PageAppbar(title: "互动通知"), |
||||
|
body: EasyRefresh( |
||||
|
header: const ClassicHeader( |
||||
|
dragText: '下拉刷新', |
||||
|
armedText: '释放刷新', |
||||
|
readyText: '刷新中...', |
||||
|
processingText: '刷新中...', |
||||
|
processedText: '刷新完成', |
||||
|
failedText: '刷新失败', |
||||
|
noMoreText: '没有更多数据', |
||||
|
showMessage: false |
||||
|
), |
||||
|
footer: ClassicFooter( |
||||
|
dragText: '上拉加载', |
||||
|
armedText: '释放加载', |
||||
|
readyText: '加载中...', |
||||
|
processingText: '加载中...', |
||||
|
processedText: '加载完成', |
||||
|
failedText: '加载失败', |
||||
|
noMoreText: '没有更多数据', |
||||
|
showMessage: false |
||||
|
), |
||||
|
// 下拉刷新 |
||||
|
onRefresh: () async { |
||||
|
print('推荐列表下拉刷新被触发'); |
||||
|
controller.page.value = 1; |
||||
|
controller.trendList.clear(); |
||||
|
await controller.getTrendData(); |
||||
|
controller.listRefreshController.finishRefresh(IndicatorResult.success); |
||||
|
controller.listRefreshController.finishLoad(IndicatorResult.none); |
||||
|
}, |
||||
|
// 上拉加载更多 |
||||
|
onLoad: () async { |
||||
|
print('推荐列表上拉加载被触发, hasMore: '); |
||||
|
controller.page.value += 1; |
||||
|
controller.getTrendData(); |
||||
|
}, |
||||
|
child: ListView.separated( |
||||
|
// 关键:始终允许滚动,即使内容不足 |
||||
|
// 移除顶部 padding,让刷新指示器可以正确显示在 AppBar 下方 |
||||
|
padding: EdgeInsets.only(left: 12, right: 12), |
||||
|
itemBuilder: (context, index){ |
||||
|
|
||||
|
if (controller.trendList.isEmpty) { |
||||
|
return Center( |
||||
|
child: Column( |
||||
|
mainAxisAlignment: MainAxisAlignment.center, |
||||
|
children: [ |
||||
|
Text('暂无数据'), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return TrendItem(item: controller.trendList[index]); |
||||
|
}, |
||||
|
|
||||
|
separatorBuilder: (context, index) { |
||||
|
// 空状态或加载状态时不显示分隔符 |
||||
|
// if (controller.postList.isEmpty) { |
||||
|
// return const SizedBox.shrink(); |
||||
|
// } |
||||
|
return const SizedBox(height: 12); |
||||
|
}, |
||||
|
itemCount: controller.trendList.isEmpty ? 1 : controller.trendList.length, |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
}, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class TrendItem extends StatefulWidget { |
||||
|
final Records item; |
||||
|
const TrendItem({super.key, required this.item}); |
||||
|
|
||||
|
@override |
||||
|
State<TrendItem> createState() => _TrendItemState(); |
||||
|
} |
||||
|
|
||||
|
class _TrendItemState extends State<TrendItem> { |
||||
|
|
||||
|
/// 构建输入框内容(文本+表情) |
||||
|
List<Widget> buildInputContentWidgets() { |
||||
|
final List<Widget> widgets = []; |
||||
|
final text = widget.item.content ?? ""; |
||||
|
final RegExp emojiRegex = RegExp(r'\[emoji:(\d+)\]'); |
||||
|
|
||||
|
int lastMatchEnd = 0; |
||||
|
final matches = emojiRegex.allMatches(text); |
||||
|
|
||||
|
for (final match in matches) { |
||||
|
// 添加表情之前的文本 |
||||
|
if (match.start > lastMatchEnd) { |
||||
|
final textPart = text.substring(lastMatchEnd, match.start); |
||||
|
widgets.add( |
||||
|
Text( |
||||
|
textPart, |
||||
|
style: TextStyle(fontSize: 14.sp, color: Colors.black), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 添加表情图片 |
||||
|
final emojiId = match.group(1); |
||||
|
if (emojiId != null) { |
||||
|
final emoji = EmojiConfig.getEmojiById(emojiId); |
||||
|
if (emoji != null) { |
||||
|
widgets.add( |
||||
|
Padding( |
||||
|
padding: EdgeInsets.symmetric(horizontal: 0), |
||||
|
child: Image.asset( |
||||
|
emoji.path, |
||||
|
width: 24.w, |
||||
|
height: 24.w, |
||||
|
fit: BoxFit.contain, |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
lastMatchEnd = match.end; |
||||
|
} |
||||
|
|
||||
|
// 添加剩余的文本 |
||||
|
if (lastMatchEnd < text.length) { |
||||
|
final textPart = text.substring(lastMatchEnd); |
||||
|
widgets.add( |
||||
|
Text( |
||||
|
textPart, |
||||
|
style: TextStyle(fontSize: 14.sp, color: Colors.black), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return widgets; |
||||
|
} |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return Row( |
||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||
|
children: [ |
||||
|
Row( |
||||
|
children: [ |
||||
|
ClipRRect( |
||||
|
borderRadius: BorderRadius.all(Radius.circular(40.w)), |
||||
|
child: CachedNetworkImage( |
||||
|
imageUrl: widget.item.profilePhoto ?? "", |
||||
|
width: 40.w, |
||||
|
height: 40.w, |
||||
|
fit: BoxFit.cover, |
||||
|
), |
||||
|
), |
||||
|
SizedBox(width: 15.w,), |
||||
|
Column( |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||
|
children: [ |
||||
|
Text( |
||||
|
widget.item.nickName ?? "", |
||||
|
style: TextStyle( |
||||
|
fontSize: 13.w, |
||||
|
fontWeight: FontWeight.w500 |
||||
|
), |
||||
|
), |
||||
|
if(widget.item.operationType == 1)Text( |
||||
|
"赞了你的动态", |
||||
|
style: TextStyle( |
||||
|
fontSize: 11.w, |
||||
|
fontWeight: FontWeight.w500 |
||||
|
), |
||||
|
), |
||||
|
if(widget.item.operationType == 2)Text( |
||||
|
widget.item.postCommentContent ?? "", |
||||
|
maxLines: 1, |
||||
|
overflow: TextOverflow.ellipsis, |
||||
|
style: TextStyle( |
||||
|
fontSize: 11.w, |
||||
|
fontWeight: FontWeight.w500 |
||||
|
), |
||||
|
), |
||||
|
Text( |
||||
|
widget.item.createTime ?? "", |
||||
|
style: TextStyle( |
||||
|
fontSize: 11.w, |
||||
|
color: const Color.fromRGBO(51, 51, 51, .6), |
||||
|
fontWeight: FontWeight.w500 |
||||
|
), |
||||
|
) |
||||
|
], |
||||
|
) |
||||
|
], |
||||
|
), |
||||
|
Container( |
||||
|
width: 80.w, |
||||
|
height: 80.w, |
||||
|
padding: EdgeInsets.all(10.w), |
||||
|
decoration: BoxDecoration( |
||||
|
borderRadius: BorderRadius.all(Radius.circular(8.w)), |
||||
|
color: const Color.fromRGBO(0, 0, 0, .2) |
||||
|
), |
||||
|
child: !widget.item.content!.contains('[emoji:') ? Text( |
||||
|
widget.item.content ?? "", |
||||
|
overflow: TextOverflow.ellipsis, |
||||
|
maxLines: 3, |
||||
|
) : ClipRect( |
||||
|
child: Wrap( |
||||
|
crossAxisAlignment: WrapCrossAlignment.center, |
||||
|
children: buildInputContentWidgets(), |
||||
|
), |
||||
|
), |
||||
|
) |
||||
|
], |
||||
|
).onTap((){ |
||||
|
Get.to(() => TimelineInfo(id: widget.item.postId ?? "")); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
Write
Preview
Loading…
Cancel
Save