From 7beabed98cd64c4b30d33f580ecabba2b2155f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=AD=90=E8=B4=A4?= Date: Tue, 10 Mar 2026 18:57:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=88=E5=B9=B6=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/images/comment_icon.png | Bin 0 -> 1000 bytes assets/images/like_active.png | Bin 0 -> 613 bytes assets/images/like_icon.png | Bin 0 -> 824 bytes assets/images/publish.png | Bin 0 -> 12447 bytes lib/controller/home/home_controller.dart | 2 + lib/controller/home/report_controller.dart | 227 +++++ .../home/send_timeline_controller.dart | 107 +++ lib/controller/home/timeline_controller.dart | 176 ++++ .../home/timeline_info_controller.dart | 236 +++++ .../home/timeline_trend_controller.dart | 67 ++ .../message/chat_settings_controller.dart | 2 +- lib/generated/assets.dart | 4 + lib/model/home/post_comment_data.dart | 149 ++++ lib/model/home/post_data.dart | 103 +++ lib/model/home/trend_data.dart | 91 ++ lib/network/api_urls.dart | 46 + lib/network/home_api.dart | 60 ++ lib/network/home_api.g.dart | 353 ++++++++ lib/pages/home/all_timeline.dart | 159 ++++ lib/pages/home/home_page.dart | 5 +- lib/pages/home/recommend_window.dart | 92 ++ lib/pages/home/report_page.dart | 830 ++++++++++-------- lib/pages/home/send_timeline.dart | 734 ++++++++++++++++ lib/pages/home/timeline_info.dart | 636 ++++++++++++++ lib/pages/home/timeline_item.dart | 365 ++++++++ lib/pages/home/timeline_page.dart | 170 ++++ lib/pages/home/timeline_trend.dart | 248 ++++++ lib/pages/home/timeline_window.dart | 54 ++ lib/pages/home/user_information_page.dart | 2 +- 29 files changed, 4566 insertions(+), 352 deletions(-) create mode 100644 assets/images/comment_icon.png create mode 100644 assets/images/like_active.png create mode 100644 assets/images/like_icon.png create mode 100644 assets/images/publish.png create mode 100644 lib/controller/home/report_controller.dart create mode 100644 lib/controller/home/send_timeline_controller.dart create mode 100644 lib/controller/home/timeline_controller.dart create mode 100644 lib/controller/home/timeline_info_controller.dart create mode 100644 lib/controller/home/timeline_trend_controller.dart create mode 100644 lib/model/home/post_comment_data.dart create mode 100644 lib/model/home/post_data.dart create mode 100644 lib/model/home/trend_data.dart create mode 100644 lib/pages/home/all_timeline.dart create mode 100644 lib/pages/home/recommend_window.dart create mode 100644 lib/pages/home/send_timeline.dart create mode 100644 lib/pages/home/timeline_info.dart create mode 100644 lib/pages/home/timeline_item.dart create mode 100644 lib/pages/home/timeline_page.dart create mode 100644 lib/pages/home/timeline_trend.dart create mode 100644 lib/pages/home/timeline_window.dart diff --git a/assets/images/comment_icon.png b/assets/images/comment_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7be1757b7e1c99a1c3dca02ca3f2d0a9badd89bb GIT binary patch literal 1000 zcmV>P)Px#1am@3R0s$N2z&@+hyVZsl1W5CRA_^OFvHtps+wT?aU zA3OGVG5`Yu0|Nv51I9EWA_f2~X;0!1MNx>DhC#xKs6@m`%#AumNGZ=k9zrYOJr-=z z`T;ZwCnAQ3m&v-dA(Z&Csg6_lu8OW8@;pCwU9<`h5G|yXU!%Jg2_hb;pRB1~)5u$e zbIvbaMx~Tzu*Pbd8s8wf#Mgv3Eo5!poo&%HSb3h;bY^BjgXrVFiUt74^ZZ!N#u`La znq`?$f!P{{?vdNV`950)GxJq|+xOXOu#dDI^{jG%f#n@>8ES%v?$2hkd!g-YcINZ> zK}Z`lNWxdI_KsT&d5vf;E-oH(&VL}{_07%Af^+_%ZQCl->GUZP{X)d+tE;QOMD#wS zy={xFY=Hpa7cN3>xLhtz0pPGw^kO=l9=Ynf$g=D;B0f=n4-j#Detv$~*vJoCxrggQ zBoEDkEXy85*m-0YESJj=JHl7NJJkb`wHsvFza)mGOkziqTKIRfV6j;IjIi^~EEtVO zF?O6J6`tq$sg&|90DK03PXO?IJRbj!u=7$%`3?ZS0Ki88cy@Af@+W0qM=6Z~02D>> zCXt<2DeSmNJ=J%HLHJ$c&I(VyxK(}rbq+VF?6^oe;wp%7iHH+KoT%Wy+IDQ`>2>LP zsm;*V(;|cp@q}=MwCPrR2fQq>Eco6oQD( zY>^!G&QHJyNw*0V#{@%sbm6j=obw}a3t@=px>lpBcwZ-rTT{d{gf7F`ovrFcQIu!XOM7VM31?cdw3*%n!!aF(J|UE^6itzO^C=Tqe})*4oySc(NG z(Sd^jxWDcyoLLcV7Pl2oi*pVTuNx|GryVX@WgqhL*J+V)Q7!c~b94zRlT=@{BB2p$+17#JAXBmM)` WEZMmIC=|^A0000Px#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/assets/images/like_icon.png b/assets/images/like_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..48aebfcdb516136b3acf380a88cc45d9a63d8437 GIT binary patch literal 824 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 YW$-jezHZ{>Cj&r9PEEE>+9K@#18n)`Q2+n{ literal 0 HcmV?d00001 diff --git a/lib/controller/home/home_controller.dart b/lib/controller/home/home_controller.dart index 8723c8e..2dd90b5 100644 --- a/lib/controller/home/home_controller.dart +++ b/lib/controller/home/home_controller.dart @@ -33,6 +33,8 @@ class HomeController extends GetxController { // 从GetX依赖注入中获取HomeApi实例 late final HomeApi _homeApi; + final timelineTab = 0.obs; + @override void onInit() { super.onInit(); diff --git a/lib/controller/home/report_controller.dart b/lib/controller/home/report_controller.dart new file mode 100644 index 0000000..4fa33d9 --- /dev/null +++ b/lib/controller/home/report_controller.dart @@ -0,0 +1,227 @@ +import 'dart:io'; + +import 'package:dating_touchme_app/network/home_api.dart'; +import 'package:dating_touchme_app/oss/oss_manager.dart'; +import 'package:flustars/flustars.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class ReportController extends GetxController { + final String id; + ReportController({required this.id}); + + + final checked = 1.obs; + + final message = ''.obs; + + final blockUser = false.obs; + + final messageController = TextEditingController().obs; + + final imgList = [].obs; + + late final HomeApi _homeApi; + + final isClick = false.obs; + + @override + void onInit() { + super.onInit(); + _homeApi = Get.find(); + } + + // 选择头像 - 业务逻辑处理 + 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('上传相册成功'); + + + } + } 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('相册上传成功'); + + + } 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 ""; + } + } + + sendReport() async { + try { + if(isClick.value) return; + isClick.value = true; + final response = await _homeApi.userReportPost({ + "id": id, + "reportPicUrls": imgList.isNotEmpty ? imgList.join(",") : "", + "reportContent": message.value, + "reportReason": checked.value + }); + if (response.data.isSuccess) { + + SmartDialog.showToast('举报已提交成功'); + Get.back(); + } else { + + // 响应失败,抛出异常 + throw Exception(response.data.message ?? '获取数据失败'); + } + } catch(e){ + print('举报提交失败: $e'); + SmartDialog.showToast('举报提交失败'); + rethrow; + + } finally { + + isClick.value = false; + } + } + +} \ No newline at end of file diff --git a/lib/controller/home/send_timeline_controller.dart b/lib/controller/home/send_timeline_controller.dart new file mode 100644 index 0000000..f29a20e --- /dev/null +++ b/lib/controller/home/send_timeline_controller.dart @@ -0,0 +1,107 @@ +import 'package:dating_touchme_app/config/emoji_config.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +class SendTimelineController extends GetxController { + final title = "".obs; + final message = ''.obs; + final TextEditingController messageController = TextEditingController(); + + final focusNode = FocusNode().obs; + + final isEmojiVisible = false.obs; + + @override + void onInit() { + super.onInit(); + focusNode.value.addListener(() { + if (focusNode.value.hasFocus) { + // 输入框获得焦点(键盘弹起),关闭所有控制面板 + isEmojiVisible.value = false; + } + }); + } + + @override + void onClose() { + super.onClose(); + focusNode.value.dispose(); + } + + + void toggleEmojiPanel() { + isEmojiVisible.value = !isEmojiVisible.value; + FocusManager.instance.primaryFocus?.unfocus(); + } + + void handleEmojiSelected(EmojiItem emoji) { + // 将表情添加到输入框 + final currentText = messageController.text; + final emojiText = '[emoji:${emoji.id}]'; + messageController.text = currentText + emojiText; + // 将光标移到末尾 + messageController.selection = TextSelection.fromPosition( + TextPosition(offset: messageController.text.length), + ); + } + + + /// 构建输入框内容(文本+表情) + List buildInputContentWidgets() { + final List widgets = []; + final text = messageController.value.text; + 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/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..25c238d --- /dev/null +++ b/lib/controller/home/timeline_info_controller.dart @@ -0,0 +1,236 @@ +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; + + final focusNode = FocusNode().obs; + + @override + void onInit() { + super.onInit(); + listRefreshController = EasyRefreshController( + controlFinishRefresh: true, + controlFinishLoad: true, + ); + // 从全局依赖中获取HomeApi + _homeApi = Get.find(); + getPostData(); + getCommentData(); + + } + + @override + void onClose() { + super.onClose(); + focusNode.value.dispose(); + } + + + 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; + + FocusScope.of(Get.context!).unfocus(); + 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: 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; + } +} \ No newline at end of file diff --git a/lib/controller/home/timeline_trend_controller.dart b/lib/controller/home/timeline_trend_controller.dart new file mode 100644 index 0000000..fad7335 --- /dev/null +++ b/lib/controller/home/timeline_trend_controller.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 = [].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(); + + 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; + + } + } + + +} \ No newline at end of file diff --git a/lib/controller/message/chat_settings_controller.dart b/lib/controller/message/chat_settings_controller.dart index 272a1b1..2eed191 100644 --- a/lib/controller/message/chat_settings_controller.dart +++ b/lib/controller/message/chat_settings_controller.dart @@ -494,7 +494,7 @@ class ChatSettingsController extends GetxController { /// 举报用户 void reportUser() { // 跳转到举报页面 - Get.to(() => ReportPage()); + Get.to(() => ReportPage(id: "",)); } /// 跳转到用户主页 diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart index 824af94..41676bd 100644 --- a/lib/generated/assets.dart +++ b/lib/generated/assets.dart @@ -98,6 +98,7 @@ class Assets { static const String imagesCheck = 'assets/images/check.png'; static const String imagesCloseArrow = 'assets/images/close_arrow.png'; static const String imagesCloseLive = 'assets/images/close_live.png'; + static const String imagesCommentIcon = 'assets/images/comment_icon.png'; static const String imagesConnectHistoryIcon = 'assets/images/connect_history_icon.png'; static const String imagesCustomer = 'assets/images/customer.png'; static const String imagesDailyTasks = 'assets/images/daily_tasks.png'; @@ -138,6 +139,8 @@ class Assets { static const String imagesJoinRoomIcon = 'assets/images/join_room_icon.png'; static const String imagesKickUser = 'assets/images/kick_user.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'; static const String imagesLocationIcon = 'assets/images/location_icon.png'; @@ -179,6 +182,7 @@ class Assets { static const String imagesPlatVoiceMessageSelf = 'assets/images/plat_voice_message_self.png'; static const String imagesPlayIcon = 'assets/images/play_icon.png'; static const String imagesPlayer = 'assets/images/player.png'; + static const String imagesPublish = 'assets/images/publish.png'; static const String imagesQuestionIcon = 'assets/images/question_icon.png'; static const String imagesRealChecked = 'assets/images/real_checked.png'; static const String imagesRealName = 'assets/images/real_name.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/model/home/trend_data.dart b/lib/model/home/trend_data.dart new file mode 100644 index 0000000..1fdd47d --- /dev/null +++ b/lib/model/home/trend_data.dart @@ -0,0 +1,91 @@ +class TrendData { + List? records; + int? total; + int? size; + int? current; + int? pages; + + TrendData({this.records, this.total, this.size, this.current, this.pages}); + + TrendData.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? 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 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 toJson() { + final Map data = new Map(); + 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; + } +} diff --git a/lib/network/api_urls.dart b/lib/network/api_urls.dart index e71d784..8e6e088 100644 --- a/lib/network/api_urls.dart +++ b/lib/network/api_urls.dart @@ -183,4 +183,50 @@ class ApiUrls { static const String userGetSysInfo = 'dating-agency-uec/user/get/sys-info'; + + 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'; + + static const String userReportPost = + 'dating-agency-service/user/report/post'; + + static const String userGetSiteActivityPage = + 'dating-agency-service/user/get/site/activity/page'; + + static const String userGetSiteActivityDetails = + 'dating-agency-service/user/get/site/activity/details'; + + static const String userPageAuthorPost = + 'dating-agency-service/user/page/author-post'; + + static const String userPageOwnPostDynamic = + 'dating-agency-service/user/page/own-post-dynamic'; + + static const String userParticipateInSiteActivity = + 'dating-agency-service/user/participate/in/site/qulianlian-activity'; + + static const String userQuitSiteActivity = + 'dating-agency-service/user/quit/site/activity'; + + static const String userPageBannerByCustomer = + 'dating-agency-service/user/page/banner/by/customer'; + + static const String userPageDongwoMatchmakerMarriageInformation = + 'dating-agency-service/user/page/dongwo/matchmaker-marriage-information'; + } diff --git a/lib/network/home_api.dart b/lib/network/home_api.dart index 782f9b1..169d796 100644 --- a/lib/network/home_api.dart +++ b/lib/network/home_api.dart @@ -1,3 +1,6 @@ +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/model/home/trend_data.dart' hide Records; import 'package:dating_touchme_app/model/live/matchmaker_task.dart'; import 'package:dating_touchme_app/network/api_urls.dart'; import 'package:dating_touchme_app/network/response_model.dart'; @@ -28,4 +31,61 @@ abstract class HomeApi { }); + @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, + ); + + @POST(ApiUrls.userReportPost) + Future>> userReportPost( + @Body() Map data, + ); + + + + @GET(ApiUrls.userPageOwnPostDynamic) + Future>> userPageOwnPostDynamic({ + @Query('pageNum') required int pageNum, + @Query('pageSize') required int pageSize, + }); + + @POST(ApiUrls.userParticipateInSiteActivity) + Future>> userParticipateInSiteActivity( + @Body() Map data, + ); + + @POST(ApiUrls.userQuitSiteActivity) + Future>> userQuitSiteActivity( + @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 29b6943..365f921 100644 --- a/lib/network/home_api.g.dart +++ b/lib/network/home_api.g.dart @@ -98,6 +98,359 @@ 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; + } + + @override + Future>> userReportPost( + 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/report/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>> userPageOwnPostDynamic({ + 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/own-post-dynamic', + 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) => TrendData.fromJson(json as Map), + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future>> userParticipateInSiteActivity( + 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/participate/in/site/qulianlian-activity', + 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>> userQuitSiteActivity( + 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/quit/site/activity', + 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 new file mode 100644 index 0000000..a6cbf0e --- /dev/null +++ b/lib/pages/home/all_timeline.dart @@ -0,0 +1,159 @@ +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'; +import 'package:get/get.dart'; + +/// 推荐列表 Tab +class AllTimeline extends StatefulWidget { + const AllTimeline({super.key}); + + @override + State createState() => _AllTimelineState(); +} + +class _AllTimelineState extends State + with AutomaticKeepAliveClientMixin { + late final TimelineController controller; + late final EasyRefreshController _refreshController; + + @override + void initState() { + super.initState(); + // 确保 TimelineController 已注册 + if (!Get.isRegistered()) { + Get.put(TimelineController()); + } + controller = Get.find(); + _refreshController = EasyRefreshController(controlFinishRefresh: true, controlFinishLoad: true); + } + + @override + void dispose() { + _refreshController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Obx(() { + if (controller.recommendIsLoading.value && controller.postList.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('加载数据中...'), + ], + ), + ); + } + return EasyRefresh( + controller: _refreshController, + 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('推荐列表下拉刷新被触发'); + try { + controller.page.value = 1; + controller.postList.clear(); + await controller.loadPostList(); + print( '推荐列表刷新完成, hasMore: $controller.recommendHasMore.value'); + _refreshController.finishRefresh(); + _refreshController.resetFooter(); + } catch (e) { + print('推荐列表刷新失败: $e'); + _refreshController.finishRefresh(IndicatorResult.fail); + } + }, + // 上拉加载更多 + onLoad: () async { + print('推荐列表上拉加载被触发, hasMore: $controller.recommendHasMore.value'); + try { + controller.page.value += 1; + await controller.loadPostList(); + // 完成加载,根据是否有更多数据决定 + if (controller.hasMore.value) { + _refreshController.finishLoad(IndicatorResult.success); + print('推荐列表加载更多成功'); + } else { + _refreshController.finishLoad(IndicatorResult.noMore); + print('推荐列表没有更多数据了'); + } + } catch (e) { + print('推荐列表加载更多失败: $e'); + _refreshController.finishLoad(IndicatorResult.fail); + } + }, + child: ListView.separated( + // 关键:始终允许滚动,即使内容不足 + // 移除顶部 padding,让刷新指示器可以正确显示在 AppBar 下方 + padding: EdgeInsets.only(left: 12, right: 12), + itemBuilder: (context, index) { + // 空数据状态 + if (controller.postList.isEmpty && index == 0) { + // 使用足够的高度确保可以滚动 + if (controller.recommendIsLoading.value) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text('加载数据中...'), + ], + ), + ); + } else { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('暂无数据'), + ], + ), + ); + } + } + // 数据项 + final item = controller.postList[index]; + return TimelineItem(item: item,); + }, + separatorBuilder: (context, index) { + // 空状态或加载状态时不显示分隔符 + if (controller.postList.isEmpty) { + return const SizedBox.shrink(); + } + return const SizedBox(height: 12); + }, + // 至少显示一个 item(用于显示加载或空状态) + itemCount: controller.postList.isEmpty ? 1 : controller.postList.length, + ) + ); + }); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 3bf5cd6..39046cd 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -1,4 +1,5 @@ import 'package:dating_touchme_app/generated/assets.dart'; +import 'package:dating_touchme_app/pages/home/all_timeline.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:dating_touchme_app/controller/home/home_controller.dart'; @@ -63,7 +64,7 @@ class _HomePageState extends State // 推荐列表 RecommendTab(), // 同城列表 - NearbyTab(), + AllTimeline(), ], ); }), @@ -87,7 +88,7 @@ class _HomePageState extends State children: [ _buildTabButton(title: '推荐', index: 0, controller: controller), const SizedBox(width: 28), - _buildTabButton(title: '同城', index: 1, controller: controller), + _buildTabButton(title: '广场', index: 1, controller: controller), ], ), bottom: const PreferredSize( diff --git a/lib/pages/home/recommend_window.dart b/lib/pages/home/recommend_window.dart new file mode 100644 index 0000000..fd4f170 --- /dev/null +++ b/lib/pages/home/recommend_window.dart @@ -0,0 +1,92 @@ +import 'package:dating_touchme_app/controller/home/home_controller.dart'; +import 'package:dating_touchme_app/pages/home/nearby_tab.dart'; +import 'package:dating_touchme_app/pages/home/recommend_tab.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +class RecommendWindow extends StatefulWidget { + const RecommendWindow({super.key}); + + @override + State createState() => _RecommendWindowState(); +} + +class _RecommendWindowState extends State with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { + + + late TabController tabController; + final HomeController controller = Get.find(); + + @override + void initState() { + super.initState(); + + tabController = TabController(length: 2, vsync: this); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Column( + children: [ + // TDTabBar( + // tabs: [ + // TDTab( + // child: Padding( + // padding: EdgeInsets.only(right: 16, left: 16), + // child: Text('推荐'), + // ), + // ), + // TDTab( + // child: Padding( + // padding: EdgeInsets.only(right: 16, left: 16), + // child: Text('同城'), + // ), + // ), + // ], + // backgroundColor: Colors.transparent, + // labelPadding: const EdgeInsets.only(right: 4, top: 10, bottom: 10, left: 4), + // selectedBgColor: const Color.fromRGBO(108, 105, 244, 1), + // unSelectedBgColor: Colors.transparent, + // labelColor: Colors.white, + // dividerHeight: 0, + // tabAlignment: TabAlignment.start, + // outlineType: TDTabBarOutlineType.capsule, + // controller: tabController, + // showIndicator: false, + // isScrollable: true, + // onTap: (index) async { + // print('相亲页面 Tab: $index'); + // if (controller.selectedTabIndex.value != index) { + // controller.setSelectedTabIndex(index); + // // 确保状态更新后刷新UI + // controller.update(); + // } + // }, + // ), + + Expanded( + child: Obx(() { + // 使用 IndexedStack 保持两个列表的状态,根据当前选中的标签显示对应的列表 + return IndexedStack( + index: controller.selectedTabIndex.value, + children: const [ + // 推荐列表 + RecommendTab(), + // 同城列表 + NearbyTab(), + ], + ); + }), + ), + ], + ); + } + + + @override + bool get wantKeepAlive => true; +} + + diff --git a/lib/pages/home/report_page.dart b/lib/pages/home/report_page.dart index 9403d74..56cc797 100644 --- a/lib/pages/home/report_page.dart +++ b/lib/pages/home/report_page.dart @@ -1,394 +1,528 @@ +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/report_controller.dart'; +import 'package:dating_touchme_app/extension/ex_widget.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:tdesign_flutter/tdesign_flutter.dart'; -class ReportPage extends StatefulWidget { - const ReportPage({super.key}); +class ReportPage extends StatelessWidget { + final String id; + const ReportPage({super.key, required this.id}); - @override - State createState() => _ReportPageState(); -} -class _ReportPageState extends State { - int checked = 0; + void _showAvatarPopup(ReportController controller){ + 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); - String message = ''; + if(9 - controller.imgList.length == 1){ - bool blockUser = false; + await controller.handleCameraCapture(); + } else { + if(controller.imgList.length >= 9){ - final TextEditingController _messageController = TextEditingController(); + SmartDialog.showToast('超出数量限制,请先删除再尝试上传'); + return; + } + await controller.handleCameraCapture(); + } + }, + ), + ), + const TDDivider(), + TDCell( + arrow: false, + titleWidget: Center( + child: Text('从相册选择'), + ), + onClick: (cell) async{ + Navigator.pop(context); + if(9 - controller.imgList.length == 1){ + await controller.handleGallerySelection(); + } else { + if(controller.imgList.length >= 9){ - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PageAppbar(title: "举报中心"), - body: SingleChildScrollView( - child: Container( - padding: EdgeInsets.only( - top: 6.w, - right: 10.w, - left: 20.w - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - "请选择举报的原因", - style: TextStyle( - fontSize: 12.w, - color: const Color.fromRGBO(144, 144, 144, 1) + SmartDialog.showToast('超出数量限制,请先删除再尝试上传'); + return; + } + await controller.handleMultiGallerySelection(); + } + }, + ), + Expanded( + child: Container( + color: Color(0xFFF3F3F3), + ), + ), + TDCell( + arrow: false, + titleWidget: Center( + child: Text('取消'), ), + onClick: (cell){ + Navigator.pop(context); + }, ), ], ), - SizedBox(height: 9.w ,), - SizedBox( - height: 32.w, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "资料作假", - style: TextStyle( - fontSize: 12.w, - color: const Color.fromRGBO(51, 51, 51, 1), - fontWeight: FontWeight.w500 - ), - ), - Checkbox( - value: checked == 1, - onChanged: (value) { - checked = 1; - }, - activeColor: const Color.fromRGBO(117, 98, 249, 1), - side: const BorderSide(color: Colors.grey), - shape: const CircleBorder(), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - ], - ), + ); + }), + ); + } + + @override + Widget build(BuildContext context) { + return GetX( + init: ReportController(id: id), + builder: (controller){ + return Scaffold( + appBar: PageAppbar(title: "举报中心"), + body: SingleChildScrollView( + child: Container( + padding: EdgeInsets.only( + top: 6.w, + right: 10.w, + left: 20.w ), - SizedBox( - height: 32.w, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "色情低俗", - style: TextStyle( - fontSize: 13.w, - color: const Color.fromRGBO(51, 51, 51, 1), - fontWeight: FontWeight.w500 + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "请选择举报的原因", + style: TextStyle( + fontSize: 12.w, + color: const Color.fromRGBO(144, 144, 144, 1) + ), ), + ], + ), + SizedBox(height: 9.w ,), + SizedBox( + height: 32.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "资料作假", + style: TextStyle( + fontSize: 12.w, + color: const Color.fromRGBO(51, 51, 51, 1), + fontWeight: FontWeight.w500 + ), + ), + Checkbox( + value: controller.checked.value == 1, + onChanged: (value) { + controller.checked.value = 1; + + }, + activeColor: const Color.fromRGBO(117, 98, 249, 1), + side: const BorderSide(color: Colors.grey), + shape: const CircleBorder(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ], ), - Checkbox( - value: checked == 1, - onChanged: (value) { - checked = 1; - }, - activeColor: const Color.fromRGBO(117, 98, 249, 1), - side: const BorderSide(color: Colors.grey), - shape: const CircleBorder(), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + SizedBox( + height: 32.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "色情低俗", + style: TextStyle( + fontSize: 13.w, + color: const Color.fromRGBO(51, 51, 51, 1), + fontWeight: FontWeight.w500 + ), + ), + Checkbox( + value: controller.checked.value == 2, + onChanged: (value) { + controller.checked.value = 2; + + }, + activeColor: const Color.fromRGBO(117, 98, 249, 1), + side: const BorderSide(color: Colors.grey), + shape: const CircleBorder(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ], ), - ], - ), - ), - SizedBox( - height: 32.w, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "涉政/涉独", - style: TextStyle( - fontSize: 13.w, - color: const Color.fromRGBO(51, 51, 51, 1), - fontWeight: FontWeight.w500 - ), + ), + SizedBox( + height: 32.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "涉政/涉独", + style: TextStyle( + fontSize: 13.w, + color: const Color.fromRGBO(51, 51, 51, 1), + fontWeight: FontWeight.w500 + ), + ), + Checkbox( + value: controller.checked.value == 3, + onChanged: (value) { + controller.checked.value = 3; + + }, + activeColor: const Color.fromRGBO(117, 98, 249, 1), + side: const BorderSide(color: Colors.grey), + shape: const CircleBorder(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ], ), - Checkbox( - value: checked == 1, - onChanged: (value) { - checked = 1; - }, - activeColor: const Color.fromRGBO(117, 98, 249, 1), - side: const BorderSide(color: Colors.grey), - shape: const CircleBorder(), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + SizedBox( + height: 32.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "违法违禁", + style: TextStyle( + fontSize: 13.w, + color: const Color.fromRGBO(51, 51, 51, 1), + fontWeight: FontWeight.w500 + ), + ), + Checkbox( + value: controller.checked.value == 4, + onChanged: (value) { + controller.checked.value = 4; + + }, + activeColor: const Color.fromRGBO(117, 98, 249, 1), + side: const BorderSide(color: Colors.grey), + shape: const CircleBorder(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ], ), - ], - ), - ), - SizedBox( - height: 32.w, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "违法违禁", - style: TextStyle( - fontSize: 13.w, - color: const Color.fromRGBO(51, 51, 51, 1), - fontWeight: FontWeight.w500 - ), + ), + SizedBox( + height: 32.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "未成年相关", + style: TextStyle( + fontSize: 13.w, + color: const Color.fromRGBO(51, 51, 51, 1), + fontWeight: FontWeight.w500 + ), + ), + Checkbox( + value: controller.checked.value == 5, + onChanged: (value) { + controller.checked.value = 5; + + }, + activeColor: const Color.fromRGBO(117, 98, 249, 1), + side: const BorderSide(color: Colors.grey), + shape: const CircleBorder(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ], ), - Checkbox( - value: checked == 1, - onChanged: (value) { - checked = 1; - }, - activeColor: const Color.fromRGBO(117, 98, 249, 1), - side: const BorderSide(color: Colors.grey), - shape: const CircleBorder(), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + SizedBox( + height: 32.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "欺诈/广告/引导第三方交易", + style: TextStyle( + fontSize: 13.w, + color: const Color.fromRGBO(51, 51, 51, 1), + fontWeight: FontWeight.w500 + ), + ), + Checkbox( + value: controller.checked.value == 6, + onChanged: (value) { + controller.checked.value = 6; + + }, + activeColor: const Color.fromRGBO(117, 98, 249, 1), + side: const BorderSide(color: Colors.grey), + shape: const CircleBorder(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ], ), - ], - ), - ), - SizedBox( - height: 32.w, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "未成年相关", - style: TextStyle( - fontSize: 13.w, - color: const Color.fromRGBO(51, 51, 51, 1), - fontWeight: FontWeight.w500 - ), + ), + SizedBox( + height: 32.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "恶意骚扰/侮辱谩骂", + style: TextStyle( + fontSize: 13.w, + color: const Color.fromRGBO(51, 51, 51, 1), + fontWeight: FontWeight.w500 + ), + ), + Checkbox( + value: controller.checked.value == 7, + onChanged: (value) { + controller.checked.value = 7; + + }, + activeColor: const Color.fromRGBO(117, 98, 249, 1), + side: const BorderSide(color: Colors.grey), + shape: const CircleBorder(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ], ), - Checkbox( - value: checked == 1, - onChanged: (value) { - checked = 1; - }, - activeColor: const Color.fromRGBO(117, 98, 249, 1), - side: const BorderSide(color: Colors.grey), - shape: const CircleBorder(), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + SizedBox( + height: 32.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "其他", + style: TextStyle( + fontSize: 13.w, + color: const Color.fromRGBO(51, 51, 51, 1), + fontWeight: FontWeight.w500 + ), + ), + Checkbox( + value: controller.checked.value == 8, + onChanged: (value) { + controller.checked.value = 8; + + }, + activeColor: const Color.fromRGBO(117, 98, 249, 1), + side: const BorderSide(color: Colors.grey), + shape: const CircleBorder(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ], ), - ], - ), - ), - SizedBox( - height: 32.w, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "欺诈/广告/引导第三方交易", - style: TextStyle( - fontSize: 13.w, - color: const Color.fromRGBO(51, 51, 51, 1), - fontWeight: FontWeight.w500 + ), + SizedBox(height: 29.w ,), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "图片证据(选填)", + style: TextStyle( + fontSize: 13.w, + color: const Color.fromRGBO(144, 144, 144, 1) + ), ), - ), - Checkbox( - value: checked == 1, - onChanged: (value) { - checked = 1; - }, - activeColor: const Color.fromRGBO(117, 98, 249, 1), - side: const BorderSide(color: Colors.grey), - shape: const CircleBorder(), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - ], - ), - ), - SizedBox( - height: 32.w, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "恶意骚扰/侮辱谩骂", - style: TextStyle( - fontSize: 13.w, - color: const Color.fromRGBO(51, 51, 51, 1), - fontWeight: FontWeight.w500 + ], + ), + SizedBox(height: 13.w ,), + Wrap( + spacing: 10.w, + runSpacing: 10.w, + children: [ + ...controller.imgList.map((e){ + return Stack( + children: [ + CachedNetworkImage( + imageUrl: e, + width: 80.w, + height: 80.w, + fit: BoxFit.cover, + ), + Positioned( + left: 5.w, + top: 5.w, + child: Container( + width: 20.w, + height: 20.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(20.w)), + color: const Color.fromRGBO(0, 0, 0, .3) + ), + child: Icon( + Icons.close, + size: 20.w, + ), + ).onTap((){ + controller.imgList.remove(e); + + }), + ) + ], + ); + }), + Container( + width: 80.w, + height: 80.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.w)), + border: Border.all(width: 1, color: const Color.fromRGBO(224, 224, 224, 1)) + ), + child: Center( + child: Icon( + Icons.add, + size: 13.w, + color: const Color.fromRGBO(144, 144, 144, 1), + ), + ), + ).onTap((){ + _showAvatarPopup(controller); + }) + ], + ), + SizedBox(height: 15.w ,), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + "投诉内容(选填)", + style: TextStyle( + fontSize: 13.w, + color: const Color.fromRGBO(144, 144, 144, 1) + ), ), + ], + ), + Container( + padding: EdgeInsets.all(17.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.w)), + border: Border.all(width: 1, color: const Color.fromRGBO(224, 224, 224, 1)) ), - Checkbox( - value: checked == 1, - onChanged: (value) { - checked = 1; - }, - activeColor: const Color.fromRGBO(117, 98, 249, 1), - side: const BorderSide(color: Colors.grey), - shape: const CircleBorder(), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - ], - ), - ), - SizedBox( - height: 32.w, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "其他", + child: TextField( + controller: controller.messageController.value, + minLines: 3, // 多行 + maxLines: 3, // 自适应高度 style: TextStyle( - fontSize: 13.w, - color: const Color.fromRGBO(51, 51, 51, 1), - fontWeight: FontWeight.w500 + fontSize: ScreenUtil().setWidth(12), + height: 1 ), - ), - Checkbox( - value: checked == 1, - onChanged: (value) { - checked = 1; + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric( + vertical: 0, + horizontal: 0 + ), + 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; }, - activeColor: const Color.fromRGBO(117, 98, 249, 1), - side: const BorderSide(color: Colors.grey), - shape: const CircleBorder(), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - ], - ), - ), - SizedBox(height: 29.w ,), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - "图片证据(选填)", - style: TextStyle( - fontSize: 13.w, - color: const Color.fromRGBO(144, 144, 144, 1) ), ), - ], - ), - SizedBox(height: 13.w ,), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ + SizedBox(height: 16.w ,), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "同时加入黑名单", + style: TextStyle( + fontSize: 13.w, + color: const Color.fromRGBO(51, 51, 51, 1), + fontWeight: FontWeight.w500 + ), + ), + TDSwitch( + isOn: controller.blockUser.value, + trackOnColor: const Color.fromRGBO(117, 98, 249, 1), + onChanged: (bool e){ + print(e); + controller.blockUser.value = e; + return e; + }, + ), + ], + ), + SizedBox(height: 53.w ,), Container( - width: 80.w, - height: 80.w, + width: 325.w, + height: 45.w, decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(8.w)), - border: Border.all(width: 1, color: const Color.fromRGBO(224, 224, 224, 1)) + borderRadius: BorderRadius.all(Radius.circular(45.w)), + color: Color.fromRGBO(117, 98, 249, controller.checked.value != 0 ? 1 : .6) ), child: Center( - child: Icon( - Icons.add, - size: 13.w, - color: const Color.fromRGBO(144, 144, 144, 1), + child: Text( + "提交", + style: TextStyle( + fontSize: 18.w, + color: Colors.white, + fontWeight: FontWeight.w500 + ), ), ), - ) - ], - ), - SizedBox(height: 15.w ,), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - "投诉内容(选填)", - style: TextStyle( - fontSize: 13.w, - color: const Color.fromRGBO(144, 144, 144, 1) - ), - ), + ).onTap((){ + controller.sendReport(); + }) ], ), - Container( - padding: EdgeInsets.all(17.w), - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(8.w)), - border: Border.all(width: 1, color: const Color.fromRGBO(224, 224, 224, 1)) - ), - child: TextField( - controller: _messageController, - minLines: 3, // 多行 - maxLines: 3, // 自适应高度 - style: TextStyle( - fontSize: ScreenUtil().setWidth(12), - height: 1 - ), - decoration: InputDecoration( - contentPadding: EdgeInsets.symmetric( - vertical: 0, - horizontal: 0 - ), - 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){ - message = value; - }, - ), - ), - SizedBox(height: 16.w ,), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "同时加入黑名单", - style: TextStyle( - fontSize: 13.w, - color: const Color.fromRGBO(51, 51, 51, 1), - fontWeight: FontWeight.w500 - ), - ), - TDSwitch( - isOn: blockUser, - trackOnColor: const Color.fromRGBO(117, 98, 249, 1), - onChanged: (bool e){ - print(e); - blockUser = e; - setState(() { - - }); - return e; - }, - ), - ], - ), - SizedBox(height: 53.w ,), - Container( - width: 325.w, - height: 45.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(45.w)), - color: Color.fromRGBO(117, 98, 249, checked != 0 ? 1 : .6) - ), - child: Center( - child: Text( - "提交", - style: TextStyle( - fontSize: 18.w, - color: Colors.white, - fontWeight: FontWeight.w500 - ), - ), - ), - ) - ], + ), ), - ), - ), + ); + }, ); } } + diff --git a/lib/pages/home/send_timeline.dart b/lib/pages/home/send_timeline.dart new file mode 100644 index 0000000..ac26d13 --- /dev/null +++ b/lib/pages/home/send_timeline.dart @@ -0,0 +1,734 @@ +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: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}); + + @override + State createState() => _SendTimelineState(); +} + +class _SendTimelineState extends State { + + String title = ""; + String message = ''; + final TextEditingController messageController = TextEditingController(); + + final FocusNode focusNode = FocusNode(); + + bool isEmojiVisible = false; + + List imgList = []; + + + late final HomeApi _homeApi; + + bool isClick = false; + + @override + void initState() { + super.initState(); + + _homeApi = Get.find(); + focusNode.addListener(() { + if (focusNode.hasFocus) { + // 输入框获得焦点(键盘弹起),关闭所有控制面板 + isEmojiVisible = false; + setState(() { + + }); + } + }); + } + + @override + void dispose() { + super.dispose(); + focusNode.dispose(); + } + + + + 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 && image.isNotEmpty) { + 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(); + setState(() { + + }); + } + + void handleEmojiSelected(EmojiItem emoji) { + // 将表情添加到输入框 + final currentText = messageController.text; + final emojiText = '[emoji:${emoji.id}]'; + messageController.text = currentText + emojiText; + // 将光标移到末尾 + messageController.selection = TextSelection.fromPosition( + TextPosition(offset: messageController.text.length), + ); + setState(() {}); // 刷新显示 + } + + + /// 构建输入框内容(文本+表情) + List buildInputContentWidgets() { + final List widgets = []; + final text = messageController.value.text; + 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; + } + + sendTimeLine() async { + try { + if(isClick) return; + isClick = true; + if(messageController.value.text == ""){ + SmartDialog.showToast('请填写帖子内容'); + return; + } + 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; + + } finally { + + isClick = false; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: PageAppbar(title: "", right: Container( + width: 53.w, + height: 26.w, + margin: EdgeInsets.only(right: 17.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.w)), + color: const Color.fromRGBO(117, 98, 249, 1) + ), + child: Center( + child: Text( + "发布", + style: TextStyle( + fontSize: 13.w, + color: Colors.white + ), + ), + ), + ).onTap((){ + sendTimeLine(); + }),), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 70.w, + height: 70.w, + margin: EdgeInsets.only( + left: 15.w, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(9.w)), + border: Border.all(width: 1.w, color: const Color.fromRGBO(224, 224, 224, 1)) + ), + child: Center( + child: Icon( + Icons.add, + size: 35.w, + color: const Color.fromRGBO(224, 224, 224, 1) + ), + ), + ).onTap(() { + _showAvatarPopup(); + }), + SizedBox(height: 25.w,), + Container( + margin: EdgeInsets.symmetric(horizontal: 15.w,), + width: 345.w, + height: 1.w, + color: const Color.fromRGBO(224, 224, 224, 1), + ), + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 17.w, vertical: 10.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Stack( + children: [ + TextField( + controller: messageController, + + focusNode: focusNode, + minLines: 1, + maxLines: null, // 关键 + style: TextStyle( + fontSize: 14.sp, + color: messageController.text.contains('[emoji:') + ? Colors.transparent + : Colors.black, + ), + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric( + vertical: 0, + horizontal: 0 + ), + hintText: "勇敢表达吧,你的有趣,总有人懂。", + hintStyle: TextStyle( + fontSize: 14.sp, + color: Colors.grey, + ), + border: const OutlineInputBorder( + borderSide: BorderSide.none, // 这将移除边框 // 可选:设置圆角 + ), + // 如果你希望聚焦时和未聚焦时都没有边框,也可以设置 focusedBorder 和 enabledBorder + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(8.0)), + ), + ), + onChanged: (value){ + setState(() { + + }); + }, + ), + if (messageController.text.contains('[emoji:')) + Positioned.fill( + child: IgnorePointer( + child: SingleChildScrollView( + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: buildInputContentWidgets(), + ), + ), + ), + ), + ], + ), + ), + if(imgList.length == 1) Stack( + children: [ + CachedNetworkImage( + imageUrl: imgList[0], + width: 341.w, + height: 341.w, + fit: BoxFit.cover, + + + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + errorWidget: (context, url, error) => Image.asset( + Assets.imagesUserAvatar, + width: 341.w, + height: 341.w, + fit: BoxFit.cover, + ), + ), + Positioned( + left: 5.w, + top: 5.w, + child: Container( + width: 20.w, + height: 20.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(20.w)), + color: const Color.fromRGBO(0, 0, 0, .3) + ), + child: Icon( + Icons.close, + size: 20.w, + color: Colors.white, + ), + ).onTap((){ + imgList.clear(); + setState(() { + + }); + }), + ) + ], + ), + if(imgList.length == 2) Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ...imgList.map((e){ + return Stack( + children: [ + CachedNetworkImage( + imageUrl: e, + width: 165.w, + height: 165.w, + fit: BoxFit.cover, + + + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + errorWidget: (context, url, error) => Image.asset( + Assets.imagesUserAvatar, + width: 165.w, + height: 165.w, + fit: BoxFit.cover, + ), + ), + Positioned( + left: 5.w, + top: 5.w, + child: Container( + width: 20.w, + height: 20.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(20.w)), + color: const Color.fromRGBO(0, 0, 0, .3) + ), + child: Icon( + Icons.close, + size: 20.w, + ), + ).onTap((){ + imgList.remove(e); + setState(() { + + }); + }), + ) + ], + ); + }), + ], + ), + if(imgList.length > 2) Wrap( + spacing: 13.w, + runSpacing: 13.w, + children: [ + ...imgList.map((e){ + return Stack( + children: [ + CachedNetworkImage( + imageUrl: e, + width: 105.w, + height: 105.w, + fit: BoxFit.cover, + + + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + errorWidget: (context, url, error) => Image.asset( + Assets.imagesUserAvatar, + width: 105.w, + height: 105.w, + fit: BoxFit.cover, + ), + ), + Positioned( + left: 5.w, + top: 5.w, + child: Container( + width: 20.w, + height: 20.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(20.w)), + color: const Color.fromRGBO(0, 0, 0, .3) + ), + child: Icon( + Icons.close, + size: 20.w, + ), + ).onTap((){ + imgList.remove(e); + setState(() { + + }); + }), + ) + ], + ); + }), + ], + ), + + + ], + ), + ), + ), + + // 表情面板 + EmojiPanel( + isVisible: isEmojiVisible, + onEmojiSelected: handleEmojiSelected, + ), + ], + ), + ); + } +} + diff --git a/lib/pages/home/timeline_info.dart b/lib/pages/home/timeline_info.dart new file mode 100644 index 0000000..3c5b25a --- /dev/null +++ b/lib/pages/home/timeline_info.dart @@ -0,0 +1,636 @@ +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/foundation.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 StatelessWidget { + final String id; + const TimelineInfo({super.key, required this.id}); + @override + Widget build(BuildContext context) { + return GetX( + init: TimelineInfoController(id: id), + builder: (controller) { + return PopScope( + canPop: false, + onPopInvoked: (didPop) { + if (didPop) return; + Get.back(result: controller.item.value); + }, + child: Scaffold( + appBar: AppBar( + leading: BackButton( + onPressed: () { + Get.back(result: controller.item.value); + }, + ), + backgroundColor: const Color.fromRGBO(255, 255, 255, 1), + surfaceTintColor: const Color.fromRGBO(255, 255, 255, 1), + centerTitle: true, + title: Text( + "详情", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: const Color.fromRGBO(51, 51, 51, 1) + ), + ), + actions: [ + Container( + margin: EdgeInsets.only(right: 14.w), + child: 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(id: id,)); + } + }, + child: Icon( + Icons.keyboard_control, + size: 24.w, + color: const Color.fromRGBO(51, 51, 51, 1), + ), // 你的小圆按钮 + ), + ) + ], + ), + 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 + ), + + 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 ?? "")); + }), + 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 + ), + ) + ], + ) + ], + ), + + ], + ), + Container( + margin: EdgeInsets.symmetric(vertical: 11.w), + child: !controller.item.value.content!.contains('[emoji:') ? Text( + + controller.item.value.content ?? "", + ) : Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: controller.buildInputContentWidgets(), + ), + ), + + if(controller.imgList.length == 1) CachedNetworkImage( + imageUrl: controller.imgList[0], + width: 341.w, + height: 341.w, + fit: BoxFit.cover, + + + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + errorWidget: (context, url, error) => Image.asset( + Assets.imagesUserAvatar, + width: 341.w, + height: 341.w, + fit: BoxFit.cover, + ), + ).onTap((){ + TDImageViewer.showImageViewer(context: context, images: controller.imgList); + }), + 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, + + + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + errorWidget: (context, url, error) => Image.asset( + Assets.imagesUserAvatar, + width: 165.w, + height: 165.w, + fit: BoxFit.cover, + ), + ).onTap((){ + TDImageViewer.showImageViewer(context: context, images: controller.imgList); + }); + }), + ], + ), + 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, + + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + errorWidget: (context, url, error) => Image.asset( + Assets.imagesUserAvatar, + width: 105.w, + height: 105.w, + fit: BoxFit.cover, + ), + ).onTap((){ + TDImageViewer.showImageViewer(context: context, images: controller.imgList); + }); + }), + ], + ), + 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, + ), + 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, + ), + 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.focusNode.value.requestFocus(); + + // controller.update(); + + }), + ], + ), + 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,); + }), + ], + ), + ), + ), + ), + // if(controller.showInput.value) Positioned.fill( + // child: Container( + // color: const Color.fromRGBO(0, 0, 0, .4), + // ).onTap((){ + // controller.showInput.value = false; + // FocusScope.of(context).unfocus(); + // + // }), + // ), + ListenableBuilder( + listenable: controller.focusNode.value, // 直接监听焦点节点 + builder: (context, child) { + // 只有当焦点状态改变时,这段 builder 才会运行 + return Visibility( + visible: controller.focusNode.value.hasFocus, + child: GestureDetector( + onTap: () { + + FocusScope.of(context).unfocus(); + controller.parentId.value = "0"; + }, + child: Container( + color: const Color.fromRGBO(0, 0, 0, .4), + // 这里放遮罩层的内容 + ), + ), + ); + }, + ), + 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( + focusNode: controller.focusNode.value, + controller: controller.messageController.value, + 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){ + controller.message.value = value; + }, + ), + ), + ), + 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% + ), + ), + child: Center( + child: Text( + "发送", + style: TextStyle( + fontSize: 12.w, + color: Colors.white + ), + ), + ), + ).onTap((){ + controller.sendComment(); + }) + ], + ), + ), + ) + ], + ), + ), + ); + }, + ); + } +} + + +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, + ), + ).onTap((){ + Get.to(() => UserInformationPage(miId: widget.item.miId ?? "")); + }), + 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.focusNode.value.requestFocus(); + + }), + ], + ), + ) + ], + ), + ), + // 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,); + }) ?? [], + + ], + ), + ) + ], + ), + ); + } +} + +class SecondCommentItem extends StatefulWidget { + final ChildPostCommentList item; + const SecondCommentItem({super.key, required this.item}); + + @override + State createState() => _SecondCommentItemState(); +} + +class _SecondCommentItemState extends State { + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 20.w), + child: 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, + ), + ).onTap((){ + Get.to(() => UserInformationPage(miId: widget.item.miId ?? "")); + }), + 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), + ), + ), + ], + ), + ) + ], + ), + ), + // Image.asset( + // Assets.imagesLikeIcon, + // width: 14.w, + // ) + ], + ), + ); + } +} diff --git a/lib/pages/home/timeline_item.dart b/lib/pages/home/timeline_item.dart new file mode 100644 index 0000000..7306b08 --- /dev/null +++ b/lib/pages/home/timeline_item.dart @@ -0,0 +1,365 @@ +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/network/home_api.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:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +class TimelineItem extends StatefulWidget { + 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: 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; + } + + List imgList = []; + + late final HomeApi _homeApi; + + @override + void initState() { + super.initState(); + + + _homeApi = Get.find(); + getImgList(); + } + + getImgList(){ + if(widget.item.mediaUrls != null && widget.item.mediaUrls != ""){ + imgList = widget.item.mediaUrls!.split(","); + + setState(() { + + }); + } + } + + + + likePost() async { + try { + final response = await _homeApi.userLikePost({ + "id": widget.item.id, + "isLiked": !(widget.item.isLiked ?? false), + }); + if (response.data.isSuccess) { + if(widget.item.isLiked ?? false){ + SmartDialog.showToast('取消点赞成功'); + widget.item.likeCount = widget.item.likeCount! - 1; + } else { + SmartDialog.showToast('点赞成功'); + widget.item.likeCount = widget.item.likeCount! + 1; + } + widget.item.isLiked = !(widget.item.isLiked ?? false); + setState(() { + + }); + } else { + + // 响应失败,抛出异常 + throw Exception(response.data.message ?? '获取数据失败'); + } + } catch(e){ + print('帖子发布失败: $e'); + SmartDialog.showToast('帖子发布失败'); + rethrow; + + } + } + + @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: [ + 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 ?? "")); + }), + SizedBox(width: 8.w,), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.item.nickName ?? "", + style: TextStyle( + fontSize: 13.w, + fontWeight: FontWeight.w500 + ), + ), + Text( + widget.item.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(id: widget.item.id ?? "",)); + } + }, + child: Icon( + Icons.keyboard_control, + size: 15.w, + color: const Color.fromRGBO(51, 51, 51, 1), + ), // 你的小圆按钮 + ), + + ], + ), + Container( + margin: EdgeInsets.symmetric(vertical: 11.w), + child: !widget.item.content!.contains('[emoji:') ? Text( + widget.item.content ?? "" + ) : Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: buildInputContentWidgets(), + ), + ), + + if(imgList.length == 1) CachedNetworkImage( + imageUrl: imgList[0], + width: 341.w, + height: 341.w, + fit: BoxFit.cover, + + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + errorWidget: (context, url, error) => Image.asset( + Assets.imagesUserAvatar, + width: 341.w, + height: 341.w, + fit: BoxFit.cover, + ), + ).onTap((){ + TDImageViewer.showImageViewer(context: context, images: imgList); + }), + 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, + + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + errorWidget: (context, url, error) => Image.asset( + Assets.imagesUserAvatar, + width: 165.w, + height: 165.w, + fit: BoxFit.cover, + ), + ).onTap((){ + TDImageViewer.showImageViewer(context: context, images: imgList); + }); + }), + ], + ), + 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, + + imageBuilder: (context, imageProvider) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: imageProvider, + fit: BoxFit.cover, + ), + ), + ), + errorWidget: (context, url, error) => Image.asset( + Assets.imagesUserAvatar, + width: 105.w, + height: 105.w, + fit: BoxFit.cover, + ), + ).onTap((){ + TDImageViewer.showImageViewer(context: context, images: imgList); + }); + }), + ], + ), + SizedBox(height: 14.w,), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + children: [ + Image.asset( + (widget.item.isLiked ?? false) ? Assets.imagesLikeActive : Assets.imagesLikeIcon, + width: 14.w, + height: 12.w, + ), + SizedBox(width: 6.w,), + Text( + "${widget.item.likeCount ?? 0}", + style: TextStyle( + fontSize: 11.w, + color: const Color.fromRGBO(144, 144, 144, .6) + ), + ) + ], + ).onTap((){ + likePost(); + }), + SizedBox(width: 33.w,), + Row( + children: [ + Image.asset( + Assets.imagesCommentIcon, + width: 15.w, + height: 15.w, + ), + SizedBox(width: 6.w,), + Text( + "${widget.item.commentCount ?? 0}", + style: TextStyle( + fontSize: 11.w, + color: const Color.fromRGBO(144, 144, 144, .6) + ), + ) + ], + ), + ], + ) + ], + ), + ).onTap((){ + Get.to(() => TimelineInfo(id: widget.item.id ?? "",))?.then((e){ + widget.item.likeCount = e.likeCount; + widget.item.isLiked = e.isLiked; + widget.item.commentCount = e.commentCount; + setState(() { + + }); + }); + }); + } +} diff --git a/lib/pages/home/timeline_page.dart b/lib/pages/home/timeline_page.dart new file mode 100644 index 0000000..3ee4562 --- /dev/null +++ b/lib/pages/home/timeline_page.dart @@ -0,0 +1,170 @@ +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'; +import 'package:dating_touchme_app/pages/home/send_timeline.dart'; +import 'package:dating_touchme_app/pages/home/timeline_trend.dart'; +import 'package:dating_touchme_app/pages/home/timeline_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:dating_touchme_app/controller/home/home_controller.dart'; +import 'package:dating_touchme_app/pages/home/recommend_tab.dart'; +import 'package:dating_touchme_app/pages/home/nearby_tab.dart'; + +class TimelinePage extends StatefulWidget { + final Function goMessage; + const TimelinePage({super.key, required this.goMessage}); + + @override + State createState() => _TimelinePageState(); +} + +class _TimelinePageState extends State + with AutomaticKeepAliveClientMixin { + @override + void initState() { + super.initState(); + // 确保 HomeController 已注册 + if (!Get.isRegistered()) { + Get.put(TimelineController()); + } + // 确保 HomeController 已注册 + if (!Get.isRegistered()) { + Get.put(HomeController()); + } + + } + + @override + Widget build(BuildContext context) { + super.build(context); + return GetBuilder( + builder: (controller) { + return Stack( + children: [ + Positioned.fill( + child: Container( + color: Colors.white, + ), + ), + // 背景图 - 覆盖整个屏幕包括状态栏和导航栏 + Image.asset( + Assets.imagesBgInformation, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + ), + Scaffold( + backgroundColor: Colors.transparent, + appBar: _buildAppBar(controller), + body: Stack( + children: [ + Obx(() { + // 使用 IndexedStack 保持两个列表的状态,根据当前选中的标签显示对应的列表 + return IndexedStack( + index: controller.topTab.value, + children: [ + // // 推荐列表 + // RecommendWindow(), + // 同城列表 + const TimelineWindow(), + Container() + ], + ); + }), + Positioned( + bottom: 44.w, + right: 3.w, + child: Image.asset( + Assets.imagesPublish, + width: 60.w, + ).onTap((){ + Get.to(() => SendTimeline()); + }), + ) + ], + ), + ), + ], + ); + }, + ); + } + + PreferredSizeWidget _buildAppBar(TimelineController controller) { + return AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + centerTitle: true, + toolbarHeight: 56, + titleSpacing: 0, + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + // _buildTabButton(title: '推荐', index: 0, controller: controller), + // const SizedBox(width: 28), + _buildTabButton(title: '广场', index: 0, controller: controller), + ], + ), + bottom: const PreferredSize( + preferredSize: Size.fromHeight(4), + child: SizedBox(height: 4), + ), + actions: [ + Container( + margin: EdgeInsets.only(right: 15), + child: Icon( + Icons.email_outlined, + size: 19, + ), + ).onTap((){ + widget.goMessage(); + // Get.to(() => TimelineTrend()); + }) + ], + ); + } + + Widget _buildTabButton({ + required String title, + required int index, + required TimelineController controller, + }) { + final bool selected = controller.topTab.value == index; + return GestureDetector( + onTap: () { + print('Tab $index clicked'); + if (controller.topTab.value != index) { + controller.setTopTab(index); + // 确保状态更新后刷新UI + controller.update(); + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + title, + style: TextStyle( + fontSize: selected ? 19 : 17, + fontWeight: selected ? FontWeight.w700 : FontWeight.w400, + color: selected + ? const Color(0xFF333333) + : const Color(0xFF999999), + ), + ), + const SizedBox(height: 6), + selected + ? Image.asset(Assets.imagesTabChangeIcon, width: 32, height: 8) + : const SizedBox(height: 8), + ], + ), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/pages/home/timeline_trend.dart b/lib/pages/home/timeline_trend.dart new file mode 100644 index 0000000..539578c --- /dev/null +++ b/lib/pages/home/timeline_trend.dart @@ -0,0 +1,248 @@ +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( + 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 createState() => _TrendItemState(); +} + +class _TrendItemState 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: 11.w, 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: 18.w, + height: 18.w, + fit: BoxFit.contain, + ), + ), + ); + } + } + + lastMatchEnd = match.end; + } + + // 添加剩余的文本 + if (lastMatchEnd < text.length) { + final textPart = text.substring(lastMatchEnd); + widgets.add( + Text( + textPart, + style: TextStyle(fontSize: 11.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: 12.w, + fontWeight: FontWeight.w500, + color: const Color.fromRGBO(144, 144, 144, 1) + ), + ), + if(widget.item.operationType == 1)Text( + "赞了你的动态", + style: TextStyle( + fontSize: 13.w, + fontWeight: FontWeight.w500 + ), + ), + if(widget.item.operationType == 2)Text( + widget.item.postCommentContent ?? "", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13.w, + fontWeight: FontWeight.w500 + ), + ), + SizedBox(height: 15.w,), + Text( + widget.item.createTime ?? "", + style: TextStyle( + fontSize: 12.w, + color: const Color.fromRGBO(144, 144, 144, .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(240, 240, 240, 1) + ), + child: !widget.item.content!.contains('[emoji:') ? Text( + widget.item.content ?? "", + overflow: TextOverflow.ellipsis, + maxLines: 4, + style: TextStyle( + fontSize: 11.w + ), + ) : ClipRect( + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: buildInputContentWidgets(), + ), + ), + ) + ], + ).onTap((){ + Get.to(() => TimelineInfo(id: widget.item.postId ?? "")); + }); + } +} + diff --git a/lib/pages/home/timeline_window.dart b/lib/pages/home/timeline_window.dart new file mode 100644 index 0000000..bbf72d9 --- /dev/null +++ b/lib/pages/home/timeline_window.dart @@ -0,0 +1,54 @@ +import 'package:dating_touchme_app/controller/home/home_controller.dart'; +import 'package:dating_touchme_app/pages/home/all_timeline.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 TimelineWindow extends StatefulWidget { + const TimelineWindow({super.key}); + + @override + State createState() => _TimelineWindowState(); +} + +class _TimelineWindowState extends State with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { + + + late TabController tabController; + final HomeController controller = Get.find(); + + @override + void initState() { + super.initState(); + print(111); + tabController = TabController(length: 2, vsync: this); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Column( + children: [ + Expanded( + child: Obx(() { + // 使用 IndexedStack 保持两个列表的状态,根据当前选中的标签显示对应的列表 + return IndexedStack( + index: controller.timelineTab.value, + children: const [ + // 推荐列表 + AllTimeline(), + // 同城列表 + SizedBox(), + ], + ); + }), + ), + ], + ); + } + + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/pages/home/user_information_page.dart b/lib/pages/home/user_information_page.dart index cce57c7..2879dce 100644 --- a/lib/pages/home/user_information_page.dart +++ b/lib/pages/home/user_information_page.dart @@ -427,7 +427,7 @@ class UserInformationPage extends StatelessWidget { onSelected: (v) { if (v == 'report') { print("举报"); - Get.to(() => ReportPage()); + Get.to(() => ReportPage(id: "",)); } else if (v == 'block') { print("拉黑"); }