From 6bda01513a946641e975580e3e6dfcc8e6f693d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=AD=90=E8=B4=A4?= Date: Mon, 29 Dec 2025 18:12:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E6=8E=A5=E5=B9=BF=E5=9C=BA=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/images/like_active.png | Bin 0 -> 613 bytes lib/controller/home/timeline_controller.dart | 176 +++++ .../home/timeline_info_controller.dart | 224 ++++++ lib/generated/assets.dart | 1 + lib/model/home/post_comment_data.dart | 149 ++++ lib/model/home/post_data.dart | 103 +++ lib/network/api_urls.dart | 18 + lib/network/home_api.dart | 35 + lib/network/home_api.g.dart | 214 ++++++ lib/pages/home/all_timeline.dart | 24 +- lib/pages/home/send_timeline.dart | 340 ++++++++- lib/pages/home/timeline_info.dart | 654 +++++++++++------- lib/pages/home/timeline_item.dart | 156 ++++- lib/pages/home/timeline_page.dart | 11 +- lib/pages/main/main_page.dart | 4 +- pubspec.yaml | 6 +- 16 files changed, 1843 insertions(+), 272 deletions(-) create mode 100644 assets/images/like_active.png create mode 100644 lib/controller/home/timeline_controller.dart create mode 100644 lib/controller/home/timeline_info_controller.dart create mode 100644 lib/model/home/post_comment_data.dart create mode 100644 lib/model/home/post_data.dart diff --git a/assets/images/like_active.png b/assets/images/like_active.png new file mode 100644 index 0000000000000000000000000000000000000000..46d14dd4079df552a3a551d91e2d32a4b50b1d52 GIT binary patch literal 613 zcmV-r0-F7aP)Px#1am@3R0s$N2z&@+hyVZr4@pEpRA_dHJ4ps4SadE_apG?^Q~ zK4vJVG!=DS1x$|_n#*-sl=I-=Q#9i{?3DxdzO{QQxa<%adqvXW+go zhEpr)v8+mSCKHmOhY~||8u=!MQ>j`E@7~t`+N^SeO zoj}LT=xpX2?=~gs;h4c&9B>1H0m6VlalnNF1BC$x#Et(|*I)eRSkSpZ`*8RSg}Sav zKG6_0M}+|=oHHcbs(HLbvl}8t9?{3)f$`-cia5jRaTviZo2ciOr^aDqVL)W(41w>B zdB24hN)mB~-N`0_Ta)mq0vntz>e5Xq3;_NCtkMGR5zJjK00000NkvXXu0mjfYGV!q literal 0 HcmV?d00001 diff --git a/lib/controller/home/timeline_controller.dart b/lib/controller/home/timeline_controller.dart new file mode 100644 index 0000000..6d1beee --- /dev/null +++ b/lib/controller/home/timeline_controller.dart @@ -0,0 +1,176 @@ +import 'package:dating_touchme_app/model/home/marriage_data.dart'; +import 'package:dating_touchme_app/network/home_api.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +import '../../model/home/post_data.dart'; + +class TimelineController extends GetxController { + // 推荐列表数据 + final recommendFeed = [].obs; + // 同城列表数据 + final nearbyFeed = [].obs; + + // 推荐列表的加载状态和分页信息 + final recommendIsLoading = false.obs; + final recommendPage = 1.obs; + final recommendHasMore = true.obs; + + // 同城列表的加载状态和分页信息 + final nearbyIsLoading = false.obs; + final nearbyPage = 1.obs; + final nearbyHasMore = true.obs; + + // 当前标签页索引 + final selectedTabIndex = 0.obs; + + final topTab = 0.obs; + + final timelineTab = 0.obs; + + // 分页大小 + final pageSize = 10; + + // 从GetX依赖注入中获取HomeApi实例 + late final HomeApi _homeApi; + + final page = 1.obs; + final size = 10.obs; + + final hasMore = true.obs; + + final postList = [].obs; + + @override + void onInit() { + super.onInit(); + // 从全局依赖中获取HomeApi + _homeApi = Get.find(); + loadPostList(); + } + + loadPostList() async { + if (recommendIsLoading.value || !recommendHasMore.value) return; + try{ + recommendIsLoading.value = true; + final response = await _homeApi.userPagePost( + pageNum: page.value, + pageSize: size.value, + ); + if (response.data.isSuccess && response.data.data != null) { + final data = response.data.data?.records ?? []; + + postList.addAll(data.toList()); + if((data.length ?? 0) == size.value){ + hasMore.value = true; + } else { + hasMore.value = false; + } + } else { + + // 响应失败,抛出异常 + throw Exception(response.data.message ?? '获取数据失败'); + } + } catch(e) { + print('帖子列表获取失败: $e'); + SmartDialog.showToast('帖子列表获取失败'); + rethrow; + } finally { + + recommendIsLoading.value = false; + } + } + + /// 设置当前标签页 + void setSelectedTabIndex(int index) { + print('Setting selected tab index to: $index'); + selectedTabIndex.value = index; + // 确保UI能够更新 + update(); + } + + + void setTopTab(int index) { + print('Setting selected tab index to: $index'); + topTab.value = index; + // 确保UI能够更新 + update(); + } + + void setTimelineTab(int index) { + print('Setting selected tab index to: $index'); + timelineTab.value = index; + // 确保UI能够更新 + update(); + } + + /// 获取当前标签页的列表数据 + List getFeedListByTab(int tabIndex) { + return tabIndex == 0 ? List.from(recommendFeed) : List.from(nearbyFeed); + } + + /// 私有方法:获取婚姻数据(统一的数据获取逻辑) + /// 返回包含records(数据列表)、current(当前页)、pages(总页数)的Map + Future> _fetchMarriageData({ + required int pageNum, + required int type, + }) async { + try { + print('_fetchMarriageData - pageNum: $pageNum, pageSize: $pageSize, type: $type'); + // 调用API获取数据 + var response = await _homeApi.getMarriageList( + pageNum: pageNum, + pageSize: pageSize, + type: type, + ); + + if (response.data.isSuccess) { + final paginatedData = response.data.data; + + // 检查data是否为空 + if (paginatedData == null) { + return { + 'records': [], + 'current': pageNum, + 'pages': 1, + 'total': 0, + 'size': pageSize, + }; + } + + // data 是 PaginatedResponse,直接使用其属性 + // records 中的每个项是 dynamic,需要转换为 MarriageData + final allRecords = paginatedData.records + .map((item) => MarriageData.fromJson(item as Map)) + .toList(); + + // 过滤掉直播类型的项 + final records = allRecords.where((item) => !item.isLive).toList(); + + print('_fetchMarriageData 返回 - 请求页码: $pageNum, 返回当前页: ${paginatedData.current}, 总页数: ${paginatedData.pages}, 原始记录数: ${allRecords.length}, 过滤后记录数: ${records.length}'); + + return { + 'records': records, + 'current': paginatedData.current, + 'pages': paginatedData.pages, + 'total': paginatedData.total, + 'size': paginatedData.size, + }; + } else { + // 响应失败,抛出异常 + throw Exception(response.data.message); + } + } catch (e) { + // 向上抛出异常,让调用方处理 + rethrow; + } + } + + /// 私有方法:统一的错误处理 + void _handleError(String logMessage, dynamic error, String toastMessage) { + // 打印错误日志 + print('$logMessage: $error'); + // 显示错误提示 + SmartDialog.showToast(toastMessage); + } +} \ No newline at end of file diff --git a/lib/controller/home/timeline_info_controller.dart b/lib/controller/home/timeline_info_controller.dart new file mode 100644 index 0000000..acaad91 --- /dev/null +++ b/lib/controller/home/timeline_info_controller.dart @@ -0,0 +1,224 @@ +import 'package:dating_touchme_app/config/emoji_config.dart'; +import 'package:dating_touchme_app/model/home/post_data.dart'; +import 'package:dating_touchme_app/network/home_api.dart'; +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:dating_touchme_app/model/home/post_comment_data.dart' as pcd; + +class TimelineInfoController extends GetxController { + final String id; + TimelineInfoController({required this.id}); + + final showInput = false.obs; + + late final HomeApi _homeApi; + + final item = Records().obs; + + final imgList = [].obs; + + + final commentList = [].obs; + + final page = 1.obs; + final size = 10.obs; + final parentId = "0".obs; + + late final EasyRefreshController listRefreshController; + + + final message = ''.obs; + final messageController = TextEditingController().obs; + + @override + void onInit() { + super.onInit(); + listRefreshController = EasyRefreshController( + controlFinishRefresh: true, + controlFinishLoad: true, + ); + // 从全局依赖中获取HomeApi + _homeApi = Get.find(); + getPostData(); + getCommentData(); + } + + getPostData() async { + try { + final response = await _homeApi.userPagePostDetail(id: id); + if (response.data.isSuccess && response.data.data != null) { + item.value = response.data.data ?? Records(); + + if(item.value.mediaUrls != null && item.value.mediaUrls != ""){ + imgList.value = item.value.mediaUrls!.split(","); + } + } else { + + // 响应失败,抛出异常 + throw Exception(response.data.message ?? '获取数据失败'); + } + } catch(e){ + print('详情获取失败: $e'); + SmartDialog.showToast('详情获取失败'); + rethrow; + + } + } + + getCommentData() async { + try { + final response = await _homeApi.userPagePostComment( + postId: id, + pageNum: page.value, + pageSize: size.value + ); + if (response.data.isSuccess && response.data.data != null) { + final data = response.data.data?.records ?? []; + + commentList.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; + + } + } + + likePost() async { + try { + final response = await _homeApi.userLikePost({ + "id": id, + "isLiked": !(item.value.isLiked ?? false), + }); + if (response.data.isSuccess) { + if(item.value.isLiked ?? false){ + SmartDialog.showToast('取消点赞成功'); + } else { + SmartDialog.showToast('点赞成功'); + } + getPostData(); + } else { + + // 响应失败,抛出异常 + throw Exception(response.data.message ?? '获取数据失败'); + } + } catch(e){ + print('帖子发布失败: $e'); + SmartDialog.showToast('帖子发布失败'); + rethrow; + + } + } + + sendComment() async { + try { + if(message.value == ""){ + SmartDialog.showToast('请输入评论'); + return; + } + final response = await _homeApi.userCreatePostComment({ + "postId": id, + "parentId": parentId.value, + "content": message.value, + }); + if (response.data.isSuccess) { + + page.value = 1; + commentList.clear(); + getPostData(); + getCommentData(); + showInput.value = false; + parentId.value = "0"; + + message.value = ""; + messageController.value.value = TextEditingValue( + text: message.value, + selection: TextSelection.fromPosition(TextPosition(offset: message.value.length)), + ); + } else { + + // 响应失败,抛出异常 + throw Exception(response.data.message ?? '获取数据失败'); + } + } catch(e){ + print('帖子发布失败: $e'); + SmartDialog.showToast('帖子发布失败'); + rethrow; + + } + } + + + /// 构建输入框内容(文本+表情) + List buildInputContentWidgets() { + final List widgets = []; + final text = item.value.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: 2.w), + 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; + } +} \ No newline at end of file diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart index 1a08167..038821d 100644 --- a/lib/generated/assets.dart +++ b/lib/generated/assets.dart @@ -131,6 +131,7 @@ class Assets { static const String imagesImg = 'assets/images/img.png'; static const String imagesInformationBg = 'assets/images/information_bg.png'; static const String imagesLastMsgIcon = 'assets/images/last_msg_icon.png'; + static const String imagesLikeActive = 'assets/images/like_active.png'; static const String imagesLikeIcon = 'assets/images/like_icon.png'; static const String imagesLimitTime = 'assets/images/limit_time.png'; static const String imagesLiveIcon = 'assets/images/live_icon.png'; diff --git a/lib/model/home/post_comment_data.dart b/lib/model/home/post_comment_data.dart new file mode 100644 index 0000000..9f8617b --- /dev/null +++ b/lib/model/home/post_comment_data.dart @@ -0,0 +1,149 @@ +class PostCommentData { + List? records; + int? total; + int? size; + int? current; + int? pages; + + PostCommentData( + {this.records, this.total, this.size, this.current, this.pages}); + + PostCommentData.fromJson(Map json) { + if (json['records'] != null) { + records = []; + json['records'].forEach((v) { + records!.add(new Records.fromJson(v)); + }); + } + total = json['total']; + size = json['size']; + current = json['current']; + pages = json['pages']; + } + + Map toJson() { + final Map data = new Map(); + 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? id; + String? userId; + String? miId; + String? nickName; + int? genderCode; + String? profilePhoto; + String? parentId; + String? content; + String? createTime; + List? childPostCommentList; + + Records( + {this.id, + this.userId, + this.miId, + this.nickName, + this.genderCode, + this.profilePhoto, + this.parentId, + this.content, + this.createTime, + this.childPostCommentList}); + + Records.fromJson(Map json) { + id = json['id']; + userId = json['userId']; + miId = json['miId']; + nickName = json['nickName']; + genderCode = json['genderCode']; + profilePhoto = json['profilePhoto']; + parentId = json['parentId']; + content = json['content']; + createTime = json['createTime']; + if (json['childPostCommentList'] != null) { + childPostCommentList = []; + json['childPostCommentList'].forEach((v) { + childPostCommentList!.add(new ChildPostCommentList.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = new Map(); + data['id'] = this.id; + data['userId'] = this.userId; + data['miId'] = this.miId; + data['nickName'] = this.nickName; + data['genderCode'] = this.genderCode; + data['profilePhoto'] = this.profilePhoto; + data['parentId'] = this.parentId; + data['content'] = this.content; + data['createTime'] = this.createTime; + if (this.childPostCommentList != null) { + data['childPostCommentList'] = + this.childPostCommentList!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class ChildPostCommentList { + String? id; + String? userId; + String? miId; + String? nickName; + int? genderCode; + String? profilePhoto; + String? parentId; + String? content; + String? createTime; + Null? childPostCommentList; + + ChildPostCommentList( + {this.id, + this.userId, + this.miId, + this.nickName, + this.genderCode, + this.profilePhoto, + this.parentId, + this.content, + this.createTime, + this.childPostCommentList}); + + ChildPostCommentList.fromJson(Map json) { + id = json['id']; + userId = json['userId']; + miId = json['miId']; + nickName = json['nickName']; + genderCode = json['genderCode']; + profilePhoto = json['profilePhoto']; + parentId = json['parentId']; + content = json['content']; + createTime = json['createTime']; + childPostCommentList = json['childPostCommentList']; + } + + Map toJson() { + final Map data = new Map(); + data['id'] = this.id; + data['userId'] = this.userId; + data['miId'] = this.miId; + data['nickName'] = this.nickName; + data['genderCode'] = this.genderCode; + data['profilePhoto'] = this.profilePhoto; + data['parentId'] = this.parentId; + data['content'] = this.content; + data['createTime'] = this.createTime; + data['childPostCommentList'] = this.childPostCommentList; + return data; + } +} diff --git a/lib/model/home/post_data.dart b/lib/model/home/post_data.dart new file mode 100644 index 0000000..0acbd11 --- /dev/null +++ b/lib/model/home/post_data.dart @@ -0,0 +1,103 @@ +class PostData { + List? records; + int? total; + int? size; + int? current; + int? pages; + + PostData({this.records, this.total, this.size, this.current, this.pages}); + + PostData.fromJson(Map json) { + if (json['records'] != null) { + records = []; + json['records'].forEach((v) { + records!.add(new Records.fromJson(v)); + }); + } + total = json['total']; + size = json['size']; + current = json['current']; + pages = json['pages']; + } + + Map toJson() { + final Map data = new Map(); + 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? id; + String? userId; + String? miId; + String? nickName; + int? genderCode; + String? profilePhoto; + String? content; + String? mediaUrls; + String? topicTags; + int? status; + bool? isLiked; + int? likeCount; + int? commentCount; + String? createTime; + + Records( + {this.id, + this.userId, + this.miId, + this.nickName, + this.genderCode, + this.profilePhoto, + this.content, + this.mediaUrls, + this.topicTags, + this.status, + this.isLiked, + this.likeCount, + this.commentCount, + this.createTime}); + + Records.fromJson(Map json) { + id = json['id']; + userId = json['userId']; + miId = json['miId']; + nickName = json['nickName']; + genderCode = json['genderCode']; + profilePhoto = json['profilePhoto']; + content = json['content']; + mediaUrls = json['mediaUrls']; + topicTags = json['topicTags']; + status = json['status']; + isLiked = json['isLiked']; + likeCount = json['likeCount']; + commentCount = json['commentCount']; + createTime = json['createTime']; + } + + Map toJson() { + final Map data = new Map(); + data['id'] = this.id; + data['userId'] = this.userId; + data['miId'] = this.miId; + data['nickName'] = this.nickName; + data['genderCode'] = this.genderCode; + data['profilePhoto'] = this.profilePhoto; + data['content'] = this.content; + data['mediaUrls'] = this.mediaUrls; + data['topicTags'] = this.topicTags; + data['status'] = this.status; + data['isLiked'] = this.isLiked; + data['likeCount'] = this.likeCount; + data['commentCount'] = this.commentCount; + data['createTime'] = this.createTime; + return data; + } +} diff --git a/lib/network/api_urls.dart b/lib/network/api_urls.dart index 4c7da72..18572c0 100644 --- a/lib/network/api_urls.dart +++ b/lib/network/api_urls.dart @@ -107,6 +107,24 @@ class ApiUrls { static const String getMarriageList = 'dating-agency-service/user/page/dongwo/marriage-information'; + static const String userPagePost = + 'dating-agency-service/user/page/post'; + + static const String userPagePostDetail = + 'dating-agency-service/user/page/post/detail'; + + static const String userCreatePost = + 'dating-agency-service/user/create/post'; + + static const String userPagePostComment = + 'dating-agency-service/user/page/post-comment'; + + static const String userLikePost = + 'dating-agency-service/user/like/post'; + + static const String userCreatePostComment = + 'dating-agency-service/user/create/post-comment'; + // 后续可以在此添加更多API端点 static const String listMatchmakerProduct = 'dating-agency-mall/user/page/product/by/matchmaker'; diff --git a/lib/network/home_api.dart b/lib/network/home_api.dart index 1663b10..2fc5bbf 100644 --- a/lib/network/home_api.dart +++ b/lib/network/home_api.dart @@ -1,3 +1,5 @@ +import 'package:dating_touchme_app/model/home/post_comment_data.dart' hide Records; +import 'package:dating_touchme_app/model/home/post_data.dart'; import 'package:dating_touchme_app/network/api_urls.dart'; import 'package:dating_touchme_app/network/response_model.dart'; import 'package:retrofit/retrofit.dart'; @@ -20,4 +22,37 @@ abstract class HomeApi { @Query('pageSize') required int pageSize, @Query('type') required int type, }); + + @GET(ApiUrls.userPagePost) + Future>> userPagePost({ + @Query('pageNum') required int pageNum, + @Query('pageSize') required int pageSize, + }); + + @GET(ApiUrls.userPagePostDetail) + Future>> userPagePostDetail({ + @Query('id') required String id, + }); + + @POST(ApiUrls.userCreatePost) + Future>> userCreatePost( + @Body() Map data, + ); + + @GET(ApiUrls.userPagePostComment) + Future>> userPagePostComment({ + @Query('pageNum') required int pageNum, + @Query('pageSize') required int pageSize, + @Query('postId') required String postId, + }); + + @POST(ApiUrls.userLikePost) + Future>> userLikePost( + @Body() Map data, + ); + + @POST(ApiUrls.userCreatePostComment) + Future>> userCreatePostComment( + @Body() Map data, + ); } \ No newline at end of file diff --git a/lib/network/home_api.g.dart b/lib/network/home_api.g.dart index 84908ed..2310596 100644 --- a/lib/network/home_api.g.dart +++ b/lib/network/home_api.g.dart @@ -65,6 +65,220 @@ class _HomeApi implements HomeApi { return httpResponse; } + @override + Future>> userPagePost({ + required int pageNum, + required int pageSize, + }) async { + final _extra = {}; + final queryParameters = { + r'pageNum': pageNum, + r'pageSize': pageSize, + }; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType>>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'dating-agency-service/user/page/post', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late BaseResponse _value; + try { + _value = BaseResponse.fromJson( + _result.data!, + (json) => PostData.fromJson(json as Map), + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future>> userPagePostDetail({ + required String id, + }) async { + final _extra = {}; + final queryParameters = {r'id': id}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType>>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'dating-agency-service/user/page/post/detail', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late BaseResponse _value; + try { + _value = BaseResponse.fromJson( + _result.data!, + (json) => Records.fromJson(json as Map), + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future>> userCreatePost( + Map data, + ) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(data); + final _options = _setStreamType>>( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'dating-agency-service/user/create/post', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late BaseResponse _value; + try { + _value = BaseResponse.fromJson( + _result.data!, + (json) => json as dynamic, + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future>> userPagePostComment({ + required int pageNum, + required int pageSize, + required String postId, + }) async { + final _extra = {}; + final queryParameters = { + r'pageNum': pageNum, + r'pageSize': pageSize, + r'postId': postId, + }; + final _headers = {}; + const Map? _data = null; + final _options = + _setStreamType>>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'dating-agency-service/user/page/post-comment', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl), + ), + ); + final _result = await _dio.fetch>(_options); + late BaseResponse _value; + try { + _value = BaseResponse.fromJson( + _result.data!, + (json) => PostCommentData.fromJson(json as Map), + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future>> userLikePost( + Map data, + ) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(data); + final _options = _setStreamType>>( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'dating-agency-service/user/like/post', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late BaseResponse _value; + try { + _value = BaseResponse.fromJson( + _result.data!, + (json) => json as dynamic, + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future>> userCreatePostComment( + Map data, + ) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(data); + final _options = _setStreamType>>( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + 'dating-agency-service/user/create/post-comment', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late BaseResponse _value; + try { + _value = BaseResponse.fromJson( + _result.data!, + (json) => json as dynamic, + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || diff --git a/lib/pages/home/all_timeline.dart b/lib/pages/home/all_timeline.dart index fe6fd47..b062879 100644 --- a/lib/pages/home/all_timeline.dart +++ b/lib/pages/home/all_timeline.dart @@ -1,3 +1,4 @@ +import 'package:dating_touchme_app/controller/home/timeline_controller.dart'; import 'package:dating_touchme_app/pages/home/timeline_item.dart'; import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; @@ -15,7 +16,7 @@ class AllTimeline extends StatefulWidget { class _AllTimelineState extends State with AutomaticKeepAliveClientMixin { - final HomeController controller = Get.find(); + final TimelineController controller = Get.find(); late final EasyRefreshController _refreshController; @override @@ -38,7 +39,7 @@ class _AllTimelineState extends State final tabBarHeight = 64.0; final totalBottomPadding = bottomPadding + tabBarHeight; return Obx(() { - if (controller.recommendIsLoading.value && controller.recommendFeed.isEmpty) { + if (controller.recommendIsLoading.value && controller.postList.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -76,7 +77,9 @@ class _AllTimelineState extends State onRefresh: () async { print('推荐列表下拉刷新被触发'); try { - await controller.refreshRecommendData(); + controller.page.value = 1; + controller.postList.clear(); + await controller.loadPostList(); print( '推荐列表刷新完成, hasMore: $controller.recommendHasMore.value'); _refreshController.finishRefresh(); _refreshController.resetFooter(); @@ -89,9 +92,10 @@ class _AllTimelineState extends State onLoad: () async { print('推荐列表上拉加载被触发, hasMore: $controller.recommendHasMore.value'); try { - await controller.loadRecommendMoreData(); + controller.page.value += 1; + await controller.loadPostList(); // 完成加载,根据是否有更多数据决定 - if (controller.recommendHasMore.value) { + if (controller.hasMore.value) { _refreshController.finishLoad(IndicatorResult.success); print('推荐列表加载更多成功'); } else { @@ -109,7 +113,7 @@ class _AllTimelineState extends State padding: EdgeInsets.only(left: 12, right: 12), itemBuilder: (context, index) { // 空数据状态 - if (controller.recommendFeed.isEmpty && index == 0) { + if (controller.postList.isEmpty && index == 0) { // 使用足够的高度确保可以滚动 if (controller.recommendIsLoading.value) { return Center( @@ -134,18 +138,18 @@ class _AllTimelineState extends State } } // 数据项 - // final item = controller.recommendFeed[index]; - return TimelineItem(); + final item = controller.postList[index]; + return TimelineItem(item: item,); }, separatorBuilder: (context, index) { // 空状态或加载状态时不显示分隔符 - if (controller.recommendFeed.isEmpty) { + if (controller.postList.isEmpty) { return const SizedBox.shrink(); } return const SizedBox(height: 12); }, // 至少显示一个 item(用于显示加载或空状态) - itemCount: 10, + itemCount: controller.postList.length, ) ); }); diff --git a/lib/pages/home/send_timeline.dart b/lib/pages/home/send_timeline.dart index c756106..5b73a25 100644 --- a/lib/pages/home/send_timeline.dart +++ b/lib/pages/home/send_timeline.dart @@ -1,12 +1,22 @@ +import 'dart:io'; + +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/send_timeline_controller.dart'; import 'package:dating_touchme_app/extension/ex_widget.dart'; import 'package:dating_touchme_app/generated/assets.dart'; +import 'package:dating_touchme_app/network/home_api.dart'; +import 'package:dating_touchme_app/oss/oss_manager.dart'; import 'package:dating_touchme_app/widget/emoji_panel.dart'; +import 'package:flustars/flustars.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; class SendTimeline extends StatefulWidget { const SendTimeline({super.key}); @@ -25,9 +35,16 @@ class _SendTimelineState extends State { bool isEmojiVisible = false; + List imgList = []; + + + late final HomeApi _homeApi; + @override void initState() { super.initState(); + + _homeApi = Get.find(); focusNode.addListener(() { if (focusNode.hasFocus) { // 输入框获得焦点(键盘弹起),关闭所有控制面板 @@ -47,6 +64,265 @@ class _SendTimelineState extends State { + void _showAvatarPopup(){ + Navigator.of(Get.context!).push( + TDSlidePopupRoute( + slideTransitionFrom: SlideTransitionFrom.bottom, + builder: (context) { + return Container( + height: 176, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.0), + topRight: Radius.circular(12.0), + ), + ), + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.0), + topRight: Radius.circular(12.0), + ), + child: TDCell( + arrow: false, + titleWidget: Center( + child: Text('拍照', style: TextStyle(fontSize: 16.w, color: const Color.fromRGBO(51, 51, 51, 1))), + ), + style: TDCellStyle( + padding: EdgeInsets.all(TDTheme.of(context).spacer16), + clickBackgroundColor: TDTheme.of(context).bgColorContainerHover, + cardBorderRadius: BorderRadius.only( + topLeft: Radius.circular(12.0), + topRight: Radius.circular(12.0), + ) + ), + onClick: (cell) async{ + Navigator.pop(context); + + if(9 - imgList.length == 1){ + + await handleCameraCapture(); + } else { + if(imgList.length >= 9){ + + SmartDialog.showToast('超出数量限制,请先删除再尝试上传'); + return; + } + await handleCameraCapture(); + } + }, + ), + ), + const TDDivider(), + TDCell( + arrow: false, + titleWidget: Center( + child: Text('从相册选择'), + ), + onClick: (cell) async{ + Navigator.pop(context); + if(9 - imgList.length == 1){ + await handleGallerySelection(); + } else { + if(imgList.length >= 9){ + + SmartDialog.showToast('超出数量限制,请先删除再尝试上传'); + return; + } + await handleMultiGallerySelection(); + } + }, + ), + Expanded( + child: Container( + color: Color(0xFFF3F3F3), + ), + ), + TDCell( + arrow: false, + titleWidget: Center( + child: Text('取消'), + ), + onClick: (cell){ + Navigator.pop(context); + }, + ), + ], + ), + ); + }), + ); + } + + // 选择头像 - 业务逻辑处理 + Future handleCameraCapture() async { + try { + // 请求相机权限 + final ok = await _ensurePermission( + Permission.camera, + denyToast: '相机权限被拒绝,请在设置中允许访问相机', + ); + if (!ok) return; + + // 请求麦克风权限(部分设备拍照/录像会一并请求建议预授权) + await _ensurePermission(Permission.microphone, denyToast: '麦克风权限被拒绝'); + + // 权限通过后拍照 + final ImagePicker picker = ImagePicker(); + final XFile? photo = await picker.pickImage(source: ImageSource.camera); + + if (photo != null) { + await processSelectedImage(File(photo.path)); + } + } catch (e) { + print('拍照失败: $e'); + // 更友好的错误提示 + if (e.toString().contains('permission') || e.toString().contains('权限')) { + SmartDialog.showToast('相机权限被拒绝,请在设置中允许访问相机'); + } else if (e.toString().contains('camera') || + e.toString().contains('相机')) { + SmartDialog.showToast('设备没有可用的相机'); + } else { + SmartDialog.showToast('拍照失败,请重试'); + } + } + } + + Future handleGallerySelection() async { + try { + // 请求相册/照片权限 + // final ok = await _ensurePermission( + // Permission.photos, + // // Android 上 photos 等价于 storage/mediaLibrary,permission_handler 会映射 + // denyToast: '相册权限被拒绝,请在设置中允许访问相册', + + // ); + // if (!ok) return; + + // 从相册选择图片 + final ImagePicker picker = ImagePicker(); + final XFile? image = await picker.pickImage(source: ImageSource.gallery); + + if (image != null) { + await processSelectedImage(File(image.path)); + } + } catch (e) { + print('选择图片失败: $e'); + // 更友好的错误提示 + if (e.toString().contains('permission') || e.toString().contains('权限')) { + SmartDialog.showToast('相册权限被拒绝,请在设置中允许访问相册'); + } else { + SmartDialog.showToast('选择图片失败,请重试'); + } + } + } + + Future handleMultiGallerySelection() async { + try { + // 请求相册/照片权限 + // final ok = await _ensurePermission( + // Permission.photos, + // // Android 上 photos 等价于 storage/mediaLibrary,permission_handler 会映射 + // denyToast: '相册权限被拒绝,请在设置中允许访问相册', + + // ); + // if (!ok) return; + + // 从相册选择图片 + final ImagePicker picker = ImagePicker(); + final List? image = await picker.pickMultiImage(limit: 9 - imgList.length); + + if (image != null) { + final futures = image.map((e){ + return processSelectedMoreImage(File(e.path)); + }); + final list = await Future.wait(futures); + imgList.addAll(list); + print(imgList); + SmartDialog.dismiss(); + SmartDialog.showToast('上传相册成功'); + + setState(() { + + }); + + } + } catch (e) { + print('选择图片失败: $e'); + // 更友好的错误提示 + if (e.toString().contains('permission') || e.toString().contains('权限')) { + SmartDialog.showToast('相册权限被拒绝,请在设置中允许访问相册'); + } else { + SmartDialog.showToast('选择图片失败,请重试'); + } + } + } + + // 通用权限申请 + Future _ensurePermission(Permission permission, {String? denyToast}) async { + var status = await permission.status; + if (status.isGranted) return true; + + if (status.isDenied || status.isRestricted || status.isLimited) { + status = await permission.request(); + if (status.isGranted) return true; + if (denyToast != null) SmartDialog.showToast(denyToast); + return false; + } + + if (status.isPermanentlyDenied) { + if (denyToast != null) SmartDialog.showToast('$denyToast,前往系统设置开启'); + // 延迟弹设置,避免与弹窗冲突 + Future.delayed(const Duration(milliseconds: 300), openAppSettings); + return false; + } + return false; + } + + + // 处理选中的图片 + Future processSelectedImage(File imageFile) async { + try { + // 显示加载提示 + SmartDialog.showLoading(msg: '上传相册中...'); + String objectName = '${DateUtil.getNowDateMs()}.${imageFile.path.split('.').last}'; + String imageUrl = await OSSManager.instance.uploadFile(imageFile.readAsBytesSync(), objectName); + print('上传成功,图片URL: $imageUrl'); + imgList.add(imageUrl); + SmartDialog.dismiss(); + SmartDialog.showToast('相册上传成功'); + + setState(() { + + }); + + } catch (e) { + SmartDialog.dismiss(); + print('处理图片失败: $e'); + SmartDialog.showToast('上传相册失败,请重试'); + } + } + // 处理选中的图片 + Future processSelectedMoreImage(File imageFile) async { + try { + // 显示加载提示 + SmartDialog.showLoading(msg: '上传相册中...'); + String objectName = '${DateUtil.getNowDateMs()}.${imageFile.path.split('.').last}'; + String imageUrl = await OSSManager.instance.uploadFile(imageFile.readAsBytesSync(), objectName); + print('上传成功,图片URL: $imageUrl'); + return imageUrl; + } catch (e) { + SmartDialog.dismiss(); + print('处理图片失败: $e'); + SmartDialog.showToast('上传相册失败,请重试'); + return ""; + } + } + + + void toggleEmojiPanel() { isEmojiVisible = !isEmojiVisible; FocusManager.instance.primaryFocus?.unfocus(); @@ -125,6 +401,30 @@ class _SendTimelineState extends State { return widgets; } + sendTimeLine() async { + try { + final response = await _homeApi.userCreatePost({ + "content": messageController.value.text, + "mediaUrls": imgList.isNotEmpty ? imgList.join(",") : "", + "topicTags": "" + }); + if (response.data.isSuccess) { + + SmartDialog.showToast('帖子已发布成功,待审核通过后可见'); + Get.back(); + } else { + + // 响应失败,抛出异常 + throw Exception(response.data.message ?? '获取数据失败'); + } + } catch(e){ + print('帖子发布失败: $e'); + SmartDialog.showToast('帖子发布失败'); + rethrow; + + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -145,7 +445,9 @@ class _SendTimelineState extends State { ), ), ), - ),), + ).onTap((){ + sendTimeLine(); + }),), body: Container( padding: EdgeInsets.symmetric(horizontal: 17.w, vertical: 10.w), child: Column( @@ -208,12 +510,46 @@ class _SendTimelineState extends State { ], ), ), + if(imgList.length == 1) CachedNetworkImage( + imageUrl: imgList[0], + width: 341.w, + height: 341.w, + ), + if(imgList.length == 2) Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ...imgList.map((e){ + return CachedNetworkImage( + imageUrl: e, + width: 165.w, + height: 165.w, + fit: BoxFit.cover, + ); + }), + ], + ), + if(imgList.length > 2) Wrap( + spacing: 13.w, + runSpacing: 13.w, + children: [ + ...imgList.map((e){ + return CachedNetworkImage( + imageUrl: e, + width: 105.w, + height: 105.w, + fit: BoxFit.cover, + ); + }), + ], + ), Row( children: [ Image.asset( Assets.imagesImg, width: 20.w, - ), + ).onTap(() { + _showAvatarPopup(); + }), SizedBox(width: 25.w,), Image.asset( Assets.imagesEmoji, diff --git a/lib/pages/home/timeline_info.dart b/lib/pages/home/timeline_info.dart index 698a5fb..67d8c01 100644 --- a/lib/pages/home/timeline_info.dart +++ b/lib/pages/home/timeline_info.dart @@ -1,258 +1,440 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:dating_touchme_app/components/page_appbar.dart'; +import 'package:dating_touchme_app/controller/home/timeline_info_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/post_comment_data.dart'; +import 'package:dating_touchme_app/pages/home/report_page.dart'; +import 'package:dating_touchme_app/pages/home/user_information_page.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'; import 'package:tdesign_flutter/tdesign_flutter.dart'; -class TimelineInfo extends StatefulWidget { - const TimelineInfo({super.key}); +class TimelineInfo extends StatelessWidget { + final String id; + const TimelineInfo({super.key, required this.id}); @override - State createState() => _TimelineInfoState(); -} - -class _TimelineInfoState extends State { + Widget build(BuildContext context) { + return GetX( + init: TimelineInfoController(id: id), + builder: (controller) { + return Scaffold( + appBar: PageAppbar(title: "详情"), + body: Stack( + children: [ + if(controller.item.value.id != null && controller.item.value.id != "") EasyRefresh( + controller: controller.listRefreshController, + 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.commentList.clear(); + await controller.getPostData(); + await controller.getCommentData(); + controller.listRefreshController.finishRefresh(IndicatorResult.success); + controller.listRefreshController.finishLoad(IndicatorResult.none); + }, + // 上拉加载更多 + onLoad: () async { + print('推荐列表上拉加载被触发, hasMore: '); + controller.page.value += 1; + controller.getCommentData(); + }, + child: SingleChildScrollView( + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 16.w, + vertical: 10.w + ), - bool showInput = false; + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height - MediaQuery.of(context).padding.top, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(40.w)), + child: CachedNetworkImage( + imageUrl: controller.item.value.profilePhoto ?? "", + width: 40.w, + height: 40.w, + fit: BoxFit.cover, + ), + ).onTap((){ + Get.to(() => UserInformationPage(miId: controller.item.value.miId ?? "", userId: controller.item.value.userId ?? "",)); + }), + SizedBox(width: 8.w,), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + controller.item.value.nickName ?? "", + style: TextStyle( + fontSize: 13.w, + fontWeight: FontWeight.w500 + ), + ), + Text( + controller.item.value.createTime ?? "", + style: TextStyle( + fontSize: 11.w, + color: const Color.fromRGBO(51, 51, 51, .6), + fontWeight: FontWeight.w500 + ), + ) + ], + ) + ], + ), + PopupMenuButton( + tooltip: "", + padding: EdgeInsets.zero, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + color: Colors.white, + elevation: 8, + offset: Offset(0, 32.w), // 相对按钮下移一点 + itemBuilder: (context) => [ + const PopupMenuItem(value: 'report', child: Text('举报')), + ], + onSelected: (v) { + if (v == 'report') { + print("举报"); + Get.to(() => ReportPage()); + } + }, + child: Icon( + Icons.keyboard_control, + size: 15.w, + color: const Color.fromRGBO(51, 51, 51, 1), + ), // 你的小圆按钮 + ), + ], + ), + Container( + margin: EdgeInsets.symmetric(vertical: 11.w), + child: !controller.item.value.content!.contains('[emoji:') ? Text( - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PageAppbar(title: "详情"), - body: Stack( - children: [ - SingleChildScrollView( - child: Container( - padding: EdgeInsets.symmetric( - horizontal: 16.w, - vertical: 10.w - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Image.asset( - Assets.imagesUserAvatar, - width: 40.w, - height: 40.w, + controller.item.value.content ?? "", + ) : Wrap( + children: controller.buildInputContentWidgets(), ), - SizedBox(width: 8.w,), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "刘美玲", - style: TextStyle( - fontSize: 13.w, - fontWeight: FontWeight.w500 + ), + + if(controller.imgList.length == 1) CachedNetworkImage( + imageUrl: controller.imgList[0], + width: 341.w, + height: 341.w, + ), + if(controller.imgList.length == 2) Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ...controller.imgList.map((e){ + return CachedNetworkImage( + imageUrl: e, + width: 165.w, + height: 165.w, + fit: BoxFit.cover, + ); + }), + ], + ), + if(controller.imgList.length > 2) Wrap( + spacing: 13.w, + runSpacing: 13.w, + children: [ + ...controller.imgList.map((e){ + return CachedNetworkImage( + imageUrl: e, + width: 105.w, + height: 105.w, + fit: BoxFit.cover, + ); + }), + ], + ), + SizedBox(height: 15.w,), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + children: [ + Image.asset( + (controller.item.value.isLiked ?? false) ? Assets.imagesLikeActive : Assets.imagesLikeIcon, + width: 14.w, + height: 12.w, ), - ), - Text( - "15:16", - style: TextStyle( - fontSize: 11.w, - color: const Color.fromRGBO(51, 51, 51, .6), - fontWeight: FontWeight.w500 + SizedBox(width: 6.w,), + Text( + "${controller.item.value.likeCount ?? 0}", + style: TextStyle( + fontSize: 11.w, + color: const Color.fromRGBO(144, 144, 144, .6) + ), + ) + ], + ).onTap((){ + controller.likePost(); + }), + SizedBox(width: 33.w,), + Row( + children: [ + Image.asset( + Assets.imagesCommentIcon, + width: 15.w, + height: 15.w, ), - ) - ], - ) - ], - ), - PopupMenuButton( - tooltip: "", - padding: EdgeInsets.zero, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), - color: Colors.white, - elevation: 8, - offset: Offset(0, 32.w), // 相对按钮下移一点 - itemBuilder: (context) => [ - const PopupMenuItem(value: 'report', child: Text('举报')), - ], - onSelected: (v) { - if (v == 'report') { - print("举报"); - } - }, - child: Icon( - Icons.keyboard_control, - size: 15.w, - color: const Color.fromRGBO(51, 51, 51, 1), - ), // 你的小圆按钮 - ), - ], - ), - Container( - margin: EdgeInsets.symmetric(vertical: 11.w), - child: Text( - "你总顾及别人,那谁来顾及你。你总顾及别人,那谁来顾及你。你总顾及别人,那谁来顾及你。你总顾及别人,那谁来顾及你。" + SizedBox(width: 6.w,), + Text( + "${controller.item.value.commentCount ?? 0}", + style: TextStyle( + fontSize: 11.w, + color: const Color.fromRGBO(144, 144, 144, .6) + ), + ) + ], + ).onTap((){ + controller.showInput.value = true; + + }), + ], + ), + SizedBox(height: 18.w,), + Text( + "全部评论(${controller.item.value.commentCount ?? 0})", + style: TextStyle( + fontSize: 13.w, + color: const Color.fromRGBO(144, 144, 144, 1), + fontWeight: FontWeight.w500 + ), + ), + SizedBox(height: 20.w,), + + ...controller.commentList.map((e){ + return CommentItem(item: e, controller: controller,); + }), + ], ), ), - Image.asset( - Assets.imagesRoseBanner, - width: 343.w, - ), - SizedBox(height: 15.w,), - Row( - mainAxisAlignment: MainAxisAlignment.end, + ), + ), + if(controller.showInput.value) Positioned.fill( + child: Container( + color: const Color.fromRGBO(0, 0, 0, .4), + ).onTap((){ + controller.showInput.value = false; + + }), + ), + if(controller.showInput.value) Positioned( + left: 0, + bottom: 0, + child: Container( + width: 375.w, + height: 60.w, + color: Colors.white, + padding: EdgeInsets.all(10.w), + child: Row( children: [ - Row( - children: [ - Image.asset( - Assets.imagesLikeIcon, - width: 14.w, - height: 12.w, + Expanded( + child: Container( + decoration: BoxDecoration( + color: const Color.fromRGBO(247, 247, 247, 1), + borderRadius: BorderRadius.all(Radius.circular(40.w)) ), - SizedBox(width: 6.w,), - Text( - "47", + child: TextField( + controller: controller.messageController.value, + autofocus: true, style: TextStyle( - fontSize: 11.w, - color: const Color.fromRGBO(144, 144, 144, .6) + fontSize: ScreenUtil().setWidth(14), + height: 1 ), - ) - ], + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric( + vertical: 0, + horizontal: 17.w + ), + hintText: "请输入评论", + + border: const OutlineInputBorder( + borderSide: BorderSide.none, // 这将移除边框 // 可选:设置圆角 + ), + // 如果你希望聚焦时和未聚焦时都没有边框,也可以设置 focusedBorder 和 enabledBorder + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(4.0)), + ), + ), + onChanged: (value){ + controller.message.value = value; + }, + ), + ), ), - SizedBox(width: 33.w,), - Row( - children: [ - Image.asset( - Assets.imagesCommentIcon, - width: 15.w, - height: 15.w, + Container( + width: 60.w, + height: 30.w, + + margin: EdgeInsets.only(left: 15.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(30.w)), + gradient: LinearGradient( + begin: Alignment.centerLeft, // 0%:左边开始 + end: Alignment.centerRight, // 100%:右边结束 + colors: [ + Color.fromRGBO(131, 89, 255, 1), // 紫色 + Color.fromRGBO(77, 127, 231, 1), // 中间淡蓝 + Color.fromRGBO(61, 138, 224, 1), // 右侧深蓝 + ], + stops: [0.0, 0.7753, 1.0], // 对应 CSS 百分比:0%、77.53%、100% ), - SizedBox(width: 6.w,), - Text( - "23", + ), + child: Center( + child: Text( + "发送", style: TextStyle( - fontSize: 11.w, - color: const Color.fromRGBO(144, 144, 144, .6) + fontSize: 12.w, + color: Colors.white ), - ) - ], + ), + ), ).onTap((){ - showInput = true; - setState(() { - - }); - }), + controller.sendComment(); + }) ], ), - SizedBox(height: 18.w,), - Text( - "全部评论(23)", - style: TextStyle( - fontSize: 13.w, - color: const Color.fromRGBO(144, 144, 144, 1), - fontWeight: FontWeight.w500 - ), - ), - SizedBox(height: 20.w,), - CommentItem(), - CommentItem(), - CommentItem(), - CommentItem(), - CommentItem(), - CommentItem(), - CommentItem(), - ], - ), - ), + ), + ) + ], ), - if(showInput) Positioned.fill( - child: Container( - color: const Color.fromRGBO(0, 0, 0, .4), - ).onTap((){ - showInput = false; - setState(() { + ); + }, + ); + } +} - }); - }), - ), - if(showInput) Positioned( - left: 0, - bottom: 0, - child: Container( - width: 375.w, - height: 60.w, - color: Colors.white, - padding: EdgeInsets.all(10.w), - child: Row( - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: const Color.fromRGBO(247, 247, 247, 1), - borderRadius: BorderRadius.all(Radius.circular(40.w)) - ), - child: TextField( - // controller: _codeController, - autofocus: true, - style: TextStyle( - fontSize: ScreenUtil().setWidth(14), - height: 1 - ), - decoration: InputDecoration( - contentPadding: EdgeInsets.symmetric( - vertical: 0, - horizontal: 17.w - ), - hintText: "请输入评论", - border: const OutlineInputBorder( - borderSide: BorderSide.none, // 这将移除边框 // 可选:设置圆角 - ), - // 如果你希望聚焦时和未聚焦时都没有边框,也可以设置 focusedBorder 和 enabledBorder - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(4.0)), - ), - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(4.0)), - ), - ), - onChanged: (value){ +class CommentItem extends StatefulWidget { + final Records item; + final TimelineInfoController controller; + const CommentItem({super.key, required this.item, required this.controller}); - }, + @override + State createState() => _CommentItemState(); +} + +class _CommentItemState extends State { + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 20.w), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(40.w)), + child: CachedNetworkImage( + imageUrl: widget.item.profilePhoto ?? "", + width: 40.w, + height: 40.w, + fit: BoxFit.cover, ), - ), - ), - Container( - width: 60.w, - height: 30.w, - - margin: EdgeInsets.only(left: 15.w), - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(30.w)), - gradient: LinearGradient( - begin: Alignment.centerLeft, // 0%:左边开始 - end: Alignment.centerRight, // 100%:右边结束 - colors: [ - Color.fromRGBO(131, 89, 255, 1), // 紫色 - Color.fromRGBO(77, 127, 231, 1), // 中间淡蓝 - Color.fromRGBO(61, 138, 224, 1), // 右侧深蓝 + ).onTap((){ + Get.to(() => UserInformationPage(miId: widget.item.miId ?? "", userId: widget.item.userId ?? "",)); + }), + SizedBox(width: 8.w,), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.item.nickName ?? "", + style: TextStyle( + fontSize: 13.w, + color: const Color.fromRGBO(144, 144, 144, 1), + ), + ), + SizedBox(height: 5.w,), + SizedBox( + child: Text( + widget.item.content ?? "", + style: TextStyle( + fontSize: 13.w, + fontWeight: FontWeight.w500 + ), + ), + ), + SizedBox(height: 5.w,), + Text( + "${widget.item.createTime}·回复", + style: TextStyle( + fontSize: 11.w, + color: const Color.fromRGBO(51, 51, 51, .6), + ), + ).onTap((){ + widget.controller.parentId.value = widget.item.id ?? "0"; + widget.controller.showInput.value = true; + }), ], - stops: [0.0, 0.7753, 1.0], // 对应 CSS 百分比:0%、77.53%、100% - ), - ), - child: Center( - child: Text( - "发送", - style: TextStyle( - fontSize: 12.w, - color: Colors.white - ), ), - ), - ) - ], + ) + ], + ), ), + // Image.asset( + // Assets.imagesLikeIcon, + // width: 14.w, + // ) + ], + ), + Container( + padding: EdgeInsets.only(left: 48.w), + margin: EdgeInsets.only(top: 10.w), + child: Column( + children: [ + ...widget.item.childPostCommentList?.map((e){ + return SecondCommentItem(item: e,); + }) ?? [], + + ], ), ) ], @@ -261,14 +443,15 @@ class _TimelineInfoState extends State { } } -class CommentItem extends StatefulWidget { - const CommentItem({super.key}); +class SecondCommentItem extends StatefulWidget { + final ChildPostCommentList item; + const SecondCommentItem({super.key, required this.item}); @override - State createState() => _CommentItemState(); + State createState() => _SecondCommentItemState(); } -class _CommentItemState extends State { +class _SecondCommentItemState extends State { @override Widget build(BuildContext context) { return Container( @@ -280,18 +463,24 @@ class _CommentItemState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Image.asset( - Assets.imagesUserAvatar, - width: 40.w, - height: 40.w, - ), + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(40.w)), + child: CachedNetworkImage( + imageUrl: widget.item.profilePhoto ?? "", + width: 40.w, + height: 40.w, + fit: BoxFit.cover, + ), + ).onTap((){ + Get.to(() => UserInformationPage(miId: widget.item.miId ?? "", userId: widget.item.userId ?? "",)); + }), SizedBox(width: 8.w,), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "刘美玲", + widget.item.nickName ?? "", style: TextStyle( fontSize: 13.w, color: const Color.fromRGBO(144, 144, 144, 1), @@ -300,7 +489,7 @@ class _CommentItemState extends State { SizedBox(height: 5.w,), SizedBox( child: Text( - "看起来还不错!看起来还不错!看起来还不错!看起来还不错!看起来还不错!看起来还不错!看起来还不错!看起来还不错!", + widget.item.content ?? "", style: TextStyle( fontSize: 13.w, fontWeight: FontWeight.w500 @@ -309,7 +498,7 @@ class _CommentItemState extends State { ), SizedBox(height: 5.w,), Text( - "15:16·回复", + "${widget.item.createTime}", style: TextStyle( fontSize: 11.w, color: const Color.fromRGBO(51, 51, 51, .6), @@ -330,4 +519,3 @@ class _CommentItemState extends State { ); } } - diff --git a/lib/pages/home/timeline_item.dart b/lib/pages/home/timeline_item.dart index a9afaf0..ec5f884 100644 --- a/lib/pages/home/timeline_item.dart +++ b/lib/pages/home/timeline_item.dart @@ -1,47 +1,137 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:dating_touchme_app/config/emoji_config.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/post_data.dart'; +import 'package:dating_touchme_app/pages/home/report_page.dart'; import 'package:dating_touchme_app/pages/home/timeline_info.dart'; +import 'package:dating_touchme_app/pages/home/user_information_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; class TimelineItem extends StatefulWidget { - const TimelineItem({super.key}); + final Records item; + const TimelineItem({super.key, required this.item}); @override State createState() => _TimelineItemState(); } class _TimelineItemState extends State { + + + /// 构建输入框内容(文本+表情) + List buildInputContentWidgets() { + final List 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: 2.w), + 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; + } + + List imgList = []; + + @override + void initState() { + super.initState(); + getImgList(); + } + + getImgList(){ + if(widget.item.mediaUrls != null && widget.item.mediaUrls != ""){ + imgList = widget.item.mediaUrls!.split(","); + + setState(() { + + }); + } + } + @override Widget build(BuildContext context) { return Container( margin: EdgeInsets.only(bottom: 15.w), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ - Image.asset( - Assets.imagesUserAvatar, - width: 40.w, - height: 40.w, - ), + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(40.w)), + child: CachedNetworkImage( + imageUrl: widget.item.profilePhoto ?? "", + width: 40.w, + height: 40.w, + fit: BoxFit.cover, + ), + ).onTap((){ + Get.to(() => UserInformationPage(miId: widget.item.miId ?? "", userId: widget.item.userId ?? "",)); + }), SizedBox(width: 8.w,), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "刘美玲", + widget.item.nickName ?? "", style: TextStyle( fontSize: 13.w, fontWeight: FontWeight.w500 ), ), Text( - "15:16", + widget.item.createTime ?? "", style: TextStyle( fontSize: 11.w, color: const Color.fromRGBO(51, 51, 51, .6), @@ -66,6 +156,7 @@ class _TimelineItemState extends State { onSelected: (v) { if (v == 'report') { print("举报"); + Get.to(() => ReportPage()); } }, child: Icon( @@ -79,13 +170,44 @@ class _TimelineItemState extends State { ), Container( margin: EdgeInsets.symmetric(vertical: 11.w), - child: Text( - "你总顾及别人,那谁来顾及你。你总顾及别人,那谁来顾及你。你总顾及别人,那谁来顾及你。你总顾及别人,那谁来顾及你。" + child: !widget.item.content!.contains('[emoji:') ? Text( + widget.item.content ?? "" + ) : Wrap( + children: buildInputContentWidgets(), ), ), - Image.asset( - Assets.imagesRoseBanner, - width: 375.w - 24, + + if(imgList.length == 1) CachedNetworkImage( + imageUrl: imgList[0], + width: 341.w, + height: 341.w, + ), + if(imgList.length == 2) Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ...imgList.map((e){ + return CachedNetworkImage( + imageUrl: e, + width: 165.w, + height: 165.w, + fit: BoxFit.cover, + ); + }), + ], + ), + if(imgList.length > 2) Wrap( + spacing: 13.w, + runSpacing: 13.w, + children: [ + ...imgList.map((e){ + return CachedNetworkImage( + imageUrl: e, + width: 105.w, + height: 105.w, + fit: BoxFit.cover, + ); + }), + ], ), SizedBox(height: 14.w,), Row( @@ -94,13 +216,13 @@ class _TimelineItemState extends State { Row( children: [ Image.asset( - Assets.imagesLikeIcon, + (widget.item.isLiked ?? false) ? Assets.imagesLikeActive : Assets.imagesLikeIcon, width: 14.w, height: 12.w, ), SizedBox(width: 6.w,), Text( - "47", + "${widget.item.likeCount ?? 0}", style: TextStyle( fontSize: 11.w, color: const Color.fromRGBO(144, 144, 144, .6) @@ -118,7 +240,7 @@ class _TimelineItemState extends State { ), SizedBox(width: 6.w,), Text( - "23", + "${widget.item.commentCount ?? 0}", style: TextStyle( fontSize: 11.w, color: const Color.fromRGBO(144, 144, 144, .6) @@ -131,7 +253,7 @@ class _TimelineItemState extends State { ], ), ).onTap((){ - Get.to(() => TimelineInfo()); + Get.to(() => TimelineInfo(id: widget.item.id ?? "",)); }); } } diff --git a/lib/pages/home/timeline_page.dart b/lib/pages/home/timeline_page.dart index b2ac638..16bd9ac 100644 --- a/lib/pages/home/timeline_page.dart +++ b/lib/pages/home/timeline_page.dart @@ -1,3 +1,4 @@ +import 'package:dating_touchme_app/controller/home/timeline_controller.dart'; import 'package:dating_touchme_app/extension/ex_widget.dart'; import 'package:dating_touchme_app/generated/assets.dart'; import 'package:dating_touchme_app/pages/home/recommend_window.dart'; @@ -23,15 +24,15 @@ class _TimelinePageState extends State void initState() { super.initState(); // 确保 HomeController 已注册 - if (!Get.isRegistered()) { - Get.put(HomeController()); + if (!Get.isRegistered()) { + Get.put(TimelineController()); } } @override Widget build(BuildContext context) { super.build(context); - return GetBuilder( + return GetBuilder( builder: (controller) { return Stack( children: [ @@ -78,7 +79,7 @@ class _TimelinePageState extends State ); } - PreferredSizeWidget _buildAppBar(HomeController controller) { + PreferredSizeWidget _buildAppBar(TimelineController controller) { return AppBar( backgroundColor: Colors.transparent, elevation: 0, @@ -104,7 +105,7 @@ class _TimelinePageState extends State Widget _buildTabButton({ required String title, required int index, - required HomeController controller, + required TimelineController controller, }) { final bool selected = controller.topTab.value == index; return GestureDetector( diff --git a/lib/pages/main/main_page.dart b/lib/pages/main/main_page.dart index 568d6ff..0e060cd 100644 --- a/lib/pages/main/main_page.dart +++ b/lib/pages/main/main_page.dart @@ -103,8 +103,8 @@ class _MainPageState extends State { navigationTabs: [ tabItem('首页', Assets.imagesHomePre, Assets.imagesHomeNol, 0), tabItem('广场', Assets.imagesDiscoverPre, Assets.imagesDiscoverNol, 1), - tabItem('消息', Assets.imagesMessagePre, Assets.imagesMessageNol, 1), - tabItem('我的', Assets.imagesMinePre, Assets.imagesMineNol, 2), + tabItem('消息', Assets.imagesMessagePre, Assets.imagesMessageNol, 2), + tabItem('我的', Assets.imagesMinePre, Assets.imagesMineNol, 3), ] ), diff --git a/pubspec.yaml b/pubspec.yaml index ed6163b..0781d83 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,7 +65,7 @@ dependencies: audioplayers: ^6.5.1 video_thumbnail: ^0.5.3 # 视频缩略图生成 fluwx: ^5.7.5 -# # tobias: ^5.3.1 + # # tobias: ^5.3.1 agora_rtc_engine: ^6.5.3 agora_rtm: ^2.2.5 agora_token_generator: ^1.0.0 @@ -78,7 +78,7 @@ dependencies: pinput: ^6.0.1 im_flutter_sdk: 4.15.2 webview_flutter: ^4.13.0 - + dev_dependencies: flutter_test: sdk: flutter @@ -143,4 +143,4 @@ flutter_launcher_icons: ios: true image_path: "assets/images/app_logo.jpg" min_sdk_android: 21 - remove_alpha_ios: true + remove_alpha_ios: true \ No newline at end of file