diff --git a/assets/images/comment_icon.png b/assets/images/comment_icon.png new file mode 100644 index 0000000..7be1757 Binary files /dev/null and b/assets/images/comment_icon.png differ diff --git a/assets/images/like_active.png b/assets/images/like_active.png new file mode 100644 index 0000000..46d14dd Binary files /dev/null and b/assets/images/like_active.png differ diff --git a/assets/images/like_icon.png b/assets/images/like_icon.png new file mode 100644 index 0000000..48aebfc Binary files /dev/null and b/assets/images/like_icon.png differ diff --git a/assets/images/publish.png b/assets/images/publish.png new file mode 100644 index 0000000..c35eb3b Binary files /dev/null and b/assets/images/publish.png differ 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("拉黑"); }