29 changed files with 4566 additions and 352 deletions
Split View
Diff Options
-
BINassets/images/comment_icon.png
-
BINassets/images/like_active.png
-
BINassets/images/like_icon.png
-
BINassets/images/publish.png
-
2lib/controller/home/home_controller.dart
-
227lib/controller/home/report_controller.dart
-
107lib/controller/home/send_timeline_controller.dart
-
176lib/controller/home/timeline_controller.dart
-
236lib/controller/home/timeline_info_controller.dart
-
67lib/controller/home/timeline_trend_controller.dart
-
2lib/controller/message/chat_settings_controller.dart
-
4lib/generated/assets.dart
-
149lib/model/home/post_comment_data.dart
-
103lib/model/home/post_data.dart
-
91lib/model/home/trend_data.dart
-
46lib/network/api_urls.dart
-
60lib/network/home_api.dart
-
353lib/network/home_api.g.dart
-
159lib/pages/home/all_timeline.dart
-
5lib/pages/home/home_page.dart
-
92lib/pages/home/recommend_window.dart
-
830lib/pages/home/report_page.dart
-
734lib/pages/home/send_timeline.dart
-
636lib/pages/home/timeline_info.dart
-
365lib/pages/home/timeline_item.dart
-
170lib/pages/home/timeline_page.dart
-
248lib/pages/home/timeline_trend.dart
-
54lib/pages/home/timeline_window.dart
-
2lib/pages/home/user_information_page.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 = <String>[].obs; |
|||
|
|||
late final HomeApi _homeApi; |
|||
|
|||
final isClick = false.obs; |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
_homeApi = Get.find<HomeApi>(); |
|||
} |
|||
|
|||
// 选择头像 - 业务逻辑处理 |
|||
Future<void> 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<void> 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<void> 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<XFile>? 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<bool> _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<void> 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<String> 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; |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -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<Widget> buildInputContentWidgets() { |
|||
final List<Widget> 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; |
|||
} |
|||
|
|||
} |
|||
@ -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 = <MarriageData>[].obs; |
|||
// 同城列表数据 |
|||
final nearbyFeed = <MarriageData>[].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 = <Records>[].obs; |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
// 从全局依赖中获取HomeApi |
|||
_homeApi = Get.find<HomeApi>(); |
|||
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<MarriageData> getFeedListByTab(int tabIndex) { |
|||
return tabIndex == 0 ? List.from(recommendFeed) : List.from(nearbyFeed); |
|||
} |
|||
|
|||
/// 私有方法:获取婚姻数据(统一的数据获取逻辑) |
|||
/// 返回包含records(数据列表)、current(当前页)、pages(总页数)的Map |
|||
Future<Map<String, dynamic>> _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': <MarriageData>[], |
|||
'current': pageNum, |
|||
'pages': 1, |
|||
'total': 0, |
|||
'size': pageSize, |
|||
}; |
|||
} |
|||
|
|||
// data 是 PaginatedResponse<dynamic>,直接使用其属性 |
|||
// records 中的每个项是 dynamic,需要转换为 MarriageData |
|||
final allRecords = paginatedData.records |
|||
.map((item) => MarriageData.fromJson(item as Map<String, dynamic>)) |
|||
.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); |
|||
} |
|||
} |
|||
@ -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 = <String>[].obs; |
|||
|
|||
|
|||
final commentList = <pcd.Records>[].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<HomeApi>(); |
|||
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<Widget> buildInputContentWidgets() { |
|||
final List<Widget> 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; |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
import 'package:dating_touchme_app/network/home_api.dart'; |
|||
import 'package:easy_refresh/easy_refresh.dart'; |
|||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; |
|||
import 'package:get/get.dart'; |
|||
|
|||
import '../../model/home/trend_data.dart'; |
|||
|
|||
class TimelineTrendController extends GetxController { |
|||
|
|||
final trendList = <Records>[].obs; |
|||
|
|||
final page = 1.obs; |
|||
final size = 10.obs; |
|||
|
|||
late final EasyRefreshController listRefreshController; |
|||
|
|||
late final HomeApi _homeApi; |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
listRefreshController = EasyRefreshController( |
|||
controlFinishRefresh: true, |
|||
controlFinishLoad: true, |
|||
); |
|||
// 从全局依赖中获取HomeApi |
|||
_homeApi = Get.find<HomeApi>(); |
|||
|
|||
getTrendData(); |
|||
|
|||
} |
|||
|
|||
|
|||
|
|||
getTrendData() async { |
|||
try { |
|||
final response = await _homeApi.userPageOwnPostDynamic( |
|||
pageNum: page.value, |
|||
pageSize: size.value |
|||
); |
|||
if (response.data.isSuccess && response.data.data != null) { |
|||
final data = response.data.data?.records ?? []; |
|||
|
|||
trendList.addAll(data); |
|||
|
|||
if((data.length ?? 0) == size.value){ |
|||
|
|||
listRefreshController.finishLoad(IndicatorResult.success); |
|||
} else { |
|||
listRefreshController.finishLoad(IndicatorResult.noMore); |
|||
} |
|||
|
|||
} else { |
|||
|
|||
// 响应失败,抛出异常 |
|||
throw Exception(response.data.message ?? '获取数据失败'); |
|||
} |
|||
} catch(e){ |
|||
print('详情获取失败: $e'); |
|||
SmartDialog.showToast('动态失败'); |
|||
rethrow; |
|||
|
|||
} |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,149 @@ |
|||
class PostCommentData { |
|||
List<Records>? records; |
|||
int? total; |
|||
int? size; |
|||
int? current; |
|||
int? pages; |
|||
|
|||
PostCommentData( |
|||
{this.records, this.total, this.size, this.current, this.pages}); |
|||
|
|||
PostCommentData.fromJson(Map<String, dynamic> json) { |
|||
if (json['records'] != null) { |
|||
records = <Records>[]; |
|||
json['records'].forEach((v) { |
|||
records!.add(new Records.fromJson(v)); |
|||
}); |
|||
} |
|||
total = json['total']; |
|||
size = json['size']; |
|||
current = json['current']; |
|||
pages = json['pages']; |
|||
} |
|||
|
|||
Map<String, dynamic> toJson() { |
|||
final Map<String, dynamic> data = new Map<String, dynamic>(); |
|||
if (this.records != null) { |
|||
data['records'] = this.records!.map((v) => v.toJson()).toList(); |
|||
} |
|||
data['total'] = this.total; |
|||
data['size'] = this.size; |
|||
data['current'] = this.current; |
|||
data['pages'] = this.pages; |
|||
return data; |
|||
} |
|||
} |
|||
|
|||
class Records { |
|||
String? id; |
|||
String? userId; |
|||
String? miId; |
|||
String? nickName; |
|||
int? genderCode; |
|||
String? profilePhoto; |
|||
String? parentId; |
|||
String? content; |
|||
String? createTime; |
|||
List<ChildPostCommentList>? 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<String, dynamic> 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 = <ChildPostCommentList>[]; |
|||
json['childPostCommentList'].forEach((v) { |
|||
childPostCommentList!.add(new ChildPostCommentList.fromJson(v)); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
Map<String, dynamic> toJson() { |
|||
final Map<String, dynamic> data = new Map<String, dynamic>(); |
|||
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<String, dynamic> 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<String, dynamic> toJson() { |
|||
final Map<String, dynamic> data = new Map<String, dynamic>(); |
|||
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; |
|||
} |
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
class PostData { |
|||
List<Records>? records; |
|||
int? total; |
|||
int? size; |
|||
int? current; |
|||
int? pages; |
|||
|
|||
PostData({this.records, this.total, this.size, this.current, this.pages}); |
|||
|
|||
PostData.fromJson(Map<String, dynamic> json) { |
|||
if (json['records'] != null) { |
|||
records = <Records>[]; |
|||
json['records'].forEach((v) { |
|||
records!.add(new Records.fromJson(v)); |
|||
}); |
|||
} |
|||
total = json['total']; |
|||
size = json['size']; |
|||
current = json['current']; |
|||
pages = json['pages']; |
|||
} |
|||
|
|||
Map<String, dynamic> toJson() { |
|||
final Map<String, dynamic> data = new Map<String, dynamic>(); |
|||
if (this.records != null) { |
|||
data['records'] = this.records!.map((v) => v.toJson()).toList(); |
|||
} |
|||
data['total'] = this.total; |
|||
data['size'] = this.size; |
|||
data['current'] = this.current; |
|||
data['pages'] = this.pages; |
|||
return data; |
|||
} |
|||
} |
|||
|
|||
class Records { |
|||
String? 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<String, dynamic> 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<String, dynamic> toJson() { |
|||
final Map<String, dynamic> data = new Map<String, dynamic>(); |
|||
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; |
|||
} |
|||
} |
|||
@ -0,0 +1,91 @@ |
|||
class TrendData { |
|||
List<Records>? records; |
|||
int? total; |
|||
int? size; |
|||
int? current; |
|||
int? pages; |
|||
|
|||
TrendData({this.records, this.total, this.size, this.current, this.pages}); |
|||
|
|||
TrendData.fromJson(Map<String, dynamic> json) { |
|||
if (json['records'] != null) { |
|||
records = <Records>[]; |
|||
json['records'].forEach((v) { |
|||
records!.add(new Records.fromJson(v)); |
|||
}); |
|||
} |
|||
total = json['total']; |
|||
size = json['size']; |
|||
current = json['current']; |
|||
pages = json['pages']; |
|||
} |
|||
|
|||
Map<String, dynamic> toJson() { |
|||
final Map<String, dynamic> data = new Map<String, dynamic>(); |
|||
if (this.records != null) { |
|||
data['records'] = this.records!.map((v) => v.toJson()).toList(); |
|||
} |
|||
data['total'] = this.total; |
|||
data['size'] = this.size; |
|||
data['current'] = this.current; |
|||
data['pages'] = this.pages; |
|||
return data; |
|||
} |
|||
} |
|||
|
|||
class Records { |
|||
String? postId; |
|||
int? operationType; |
|||
String? postCommentContent; |
|||
String? userId; |
|||
String? miId; |
|||
String? nickName; |
|||
String? profilePhoto; |
|||
String? content; |
|||
String? mediaUrls; |
|||
String? topicTags; |
|||
String? createTime; |
|||
|
|||
Records( |
|||
{this.postId, |
|||
this.operationType, |
|||
this.postCommentContent, |
|||
this.userId, |
|||
this.miId, |
|||
this.nickName, |
|||
this.profilePhoto, |
|||
this.content, |
|||
this.mediaUrls, |
|||
this.topicTags, |
|||
this.createTime}); |
|||
|
|||
Records.fromJson(Map<String, dynamic> json) { |
|||
postId = json['postId']; |
|||
operationType = json['operationType']; |
|||
postCommentContent = json['postCommentContent']; |
|||
userId = json['userId']; |
|||
miId = json['miId']; |
|||
nickName = json['nickName']; |
|||
profilePhoto = json['profilePhoto']; |
|||
content = json['content']; |
|||
mediaUrls = json['mediaUrls']; |
|||
topicTags = json['topicTags']; |
|||
createTime = json['createTime']; |
|||
} |
|||
|
|||
Map<String, dynamic> toJson() { |
|||
final Map<String, dynamic> data = new Map<String, dynamic>(); |
|||
data['postId'] = this.postId; |
|||
data['operationType'] = this.operationType; |
|||
data['postCommentContent'] = this.postCommentContent; |
|||
data['userId'] = this.userId; |
|||
data['miId'] = this.miId; |
|||
data['nickName'] = this.nickName; |
|||
data['profilePhoto'] = this.profilePhoto; |
|||
data['content'] = this.content; |
|||
data['mediaUrls'] = this.mediaUrls; |
|||
data['topicTags'] = this.topicTags; |
|||
data['createTime'] = this.createTime; |
|||
return data; |
|||
} |
|||
} |
|||
@ -0,0 +1,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<AllTimeline> createState() => _AllTimelineState(); |
|||
} |
|||
|
|||
class _AllTimelineState extends State<AllTimeline> |
|||
with AutomaticKeepAliveClientMixin { |
|||
late final TimelineController controller; |
|||
late final EasyRefreshController _refreshController; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
// 确保 TimelineController 已注册 |
|||
if (!Get.isRegistered<TimelineController>()) { |
|||
Get.put(TimelineController()); |
|||
} |
|||
controller = Get.find<TimelineController>(); |
|||
_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; |
|||
} |
|||
@ -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<RecommendWindow> createState() => _RecommendWindowState(); |
|||
} |
|||
|
|||
class _RecommendWindowState extends State<RecommendWindow> with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { |
|||
|
|||
|
|||
late TabController tabController; |
|||
final HomeController controller = Get.find<HomeController>(); |
|||
|
|||
@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; |
|||
} |
|||
|
|||
|
|||
@ -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<ReportPage> createState() => _ReportPageState(); |
|||
} |
|||
|
|||
class _ReportPageState extends State<ReportPage> { |
|||
|
|||
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<ReportController>( |
|||
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 |
|||
), |
|||
), |
|||
), |
|||
) |
|||
], |
|||
), |
|||
), |
|||
), |
|||
), |
|||
); |
|||
}, |
|||
); |
|||
} |
|||
} |
|||
|
|||
@ -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<SendTimeline> createState() => _SendTimelineState(); |
|||
} |
|||
|
|||
class _SendTimelineState extends State<SendTimeline> { |
|||
|
|||
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<HomeApi>(); |
|||
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<void> 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<void> 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<void> 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<XFile>? 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<bool> _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<void> 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<String> 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<Widget> buildInputContentWidgets() { |
|||
final List<Widget> 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, |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
} |
|||
|
|||
@ -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<TimelineInfoController>( |
|||
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<String>( |
|||
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<CommentItem> createState() => _CommentItemState(); |
|||
} |
|||
|
|||
class _CommentItemState extends State<CommentItem> { |
|||
@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<SecondCommentItem> createState() => _SecondCommentItemState(); |
|||
} |
|||
|
|||
class _SecondCommentItemState extends State<SecondCommentItem> { |
|||
@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, |
|||
// ) |
|||
], |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -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<TimelineItem> createState() => _TimelineItemState(); |
|||
} |
|||
|
|||
class _TimelineItemState extends State<TimelineItem> { |
|||
|
|||
|
|||
/// 构建输入框内容(文本+表情) |
|||
List<Widget> buildInputContentWidgets() { |
|||
final List<Widget> widgets = []; |
|||
final text = widget.item.content ?? ""; |
|||
final RegExp emojiRegex = RegExp(r'\[emoji:(\d+)\]'); |
|||
|
|||
int lastMatchEnd = 0; |
|||
final matches = emojiRegex.allMatches(text); |
|||
|
|||
for (final match in matches) { |
|||
// 添加表情之前的文本 |
|||
if (match.start > lastMatchEnd) { |
|||
final textPart = text.substring(lastMatchEnd, match.start); |
|||
widgets.add( |
|||
Text( |
|||
textPart, |
|||
style: TextStyle(fontSize: 14.sp, color: Colors.black), |
|||
), |
|||
); |
|||
} |
|||
|
|||
// 添加表情图片 |
|||
final emojiId = match.group(1); |
|||
if (emojiId != null) { |
|||
final emoji = EmojiConfig.getEmojiById(emojiId); |
|||
if (emoji != null) { |
|||
widgets.add( |
|||
Padding( |
|||
padding: EdgeInsets.symmetric(horizontal: 0), |
|||
child: Image.asset( |
|||
emoji.path, |
|||
width: 24.w, |
|||
height: 24.w, |
|||
fit: BoxFit.contain, |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
|
|||
lastMatchEnd = match.end; |
|||
} |
|||
|
|||
// 添加剩余的文本 |
|||
if (lastMatchEnd < text.length) { |
|||
final textPart = text.substring(lastMatchEnd); |
|||
widgets.add( |
|||
Text( |
|||
textPart, |
|||
style: TextStyle(fontSize: 14.sp, color: Colors.black), |
|||
), |
|||
); |
|||
} |
|||
|
|||
return widgets; |
|||
} |
|||
|
|||
List<String> imgList = []; |
|||
|
|||
late final HomeApi _homeApi; |
|||
|
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
|
|||
|
|||
_homeApi = Get.find<HomeApi>(); |
|||
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<String>( |
|||
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(() { |
|||
|
|||
}); |
|||
}); |
|||
}); |
|||
} |
|||
} |
|||
@ -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<TimelinePage> createState() => _TimelinePageState(); |
|||
} |
|||
|
|||
class _TimelinePageState extends State<TimelinePage> |
|||
with AutomaticKeepAliveClientMixin { |
|||
@override |
|||
void initState() { |
|||
super.initState(); |
|||
// 确保 HomeController 已注册 |
|||
if (!Get.isRegistered<TimelineController>()) { |
|||
Get.put(TimelineController()); |
|||
} |
|||
// 确保 HomeController 已注册 |
|||
if (!Get.isRegistered<HomeController>()) { |
|||
Get.put(HomeController()); |
|||
} |
|||
|
|||
} |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
super.build(context); |
|||
return GetBuilder<TimelineController>( |
|||
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; |
|||
} |
|||
@ -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<TimelineTrendController>( |
|||
init: TimelineTrendController(), |
|||
builder: (controller){ |
|||
return Scaffold( |
|||
appBar: PageAppbar(title: "互动通知"), |
|||
body: EasyRefresh( |
|||
header: const ClassicHeader( |
|||
dragText: '下拉刷新', |
|||
armedText: '释放刷新', |
|||
readyText: '刷新中...', |
|||
processingText: '刷新中...', |
|||
processedText: '刷新完成', |
|||
failedText: '刷新失败', |
|||
noMoreText: '没有更多数据', |
|||
showMessage: false |
|||
), |
|||
footer: ClassicFooter( |
|||
dragText: '上拉加载', |
|||
armedText: '释放加载', |
|||
readyText: '加载中...', |
|||
processingText: '加载中...', |
|||
processedText: '加载完成', |
|||
failedText: '加载失败', |
|||
noMoreText: '没有更多数据', |
|||
showMessage: false |
|||
), |
|||
// 下拉刷新 |
|||
onRefresh: () async { |
|||
print('推荐列表下拉刷新被触发'); |
|||
controller.page.value = 1; |
|||
controller.trendList.clear(); |
|||
await controller.getTrendData(); |
|||
controller.listRefreshController.finishRefresh(IndicatorResult.success); |
|||
controller.listRefreshController.finishLoad(IndicatorResult.none); |
|||
}, |
|||
// 上拉加载更多 |
|||
onLoad: () async { |
|||
print('推荐列表上拉加载被触发, hasMore: '); |
|||
controller.page.value += 1; |
|||
controller.getTrendData(); |
|||
}, |
|||
child: ListView.separated( |
|||
// 关键:始终允许滚动,即使内容不足 |
|||
// 移除顶部 padding,让刷新指示器可以正确显示在 AppBar 下方 |
|||
padding: EdgeInsets.only(left: 12, right: 12), |
|||
itemBuilder: (context, index){ |
|||
|
|||
if (controller.trendList.isEmpty) { |
|||
return Center( |
|||
child: Column( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
Text('暂无数据'), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
return TrendItem(item: controller.trendList[index]); |
|||
}, |
|||
|
|||
separatorBuilder: (context, index) { |
|||
// 空状态或加载状态时不显示分隔符 |
|||
// if (controller.postList.isEmpty) { |
|||
// return const SizedBox.shrink(); |
|||
// } |
|||
return const SizedBox(height: 12); |
|||
}, |
|||
itemCount: controller.trendList.isEmpty ? 1 : controller.trendList.length, |
|||
), |
|||
), |
|||
); |
|||
}, |
|||
); |
|||
} |
|||
} |
|||
|
|||
class TrendItem extends StatefulWidget { |
|||
final Records item; |
|||
const TrendItem({super.key, required this.item}); |
|||
|
|||
@override |
|||
State<TrendItem> createState() => _TrendItemState(); |
|||
} |
|||
|
|||
class _TrendItemState extends State<TrendItem> { |
|||
|
|||
/// 构建输入框内容(文本+表情) |
|||
List<Widget> buildInputContentWidgets() { |
|||
final List<Widget> widgets = []; |
|||
final text = widget.item.content ?? ""; |
|||
final RegExp emojiRegex = RegExp(r'\[emoji:(\d+)\]'); |
|||
|
|||
int lastMatchEnd = 0; |
|||
final matches = emojiRegex.allMatches(text); |
|||
|
|||
for (final match in matches) { |
|||
// 添加表情之前的文本 |
|||
if (match.start > lastMatchEnd) { |
|||
final textPart = text.substring(lastMatchEnd, match.start); |
|||
widgets.add( |
|||
Text( |
|||
textPart, |
|||
style: TextStyle(fontSize: 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 ?? "")); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
@ -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<TimelineWindow> createState() => _TimelineWindowState(); |
|||
} |
|||
|
|||
class _TimelineWindowState extends State<TimelineWindow> with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { |
|||
|
|||
|
|||
late TabController tabController; |
|||
final HomeController controller = Get.find<HomeController>(); |
|||
|
|||
@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; |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save