29 changed files with 4566 additions and 352 deletions
Unified 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/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/material.dart'; |
||||
import 'package:flutter_screenutil/flutter_screenutil.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'; |
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( |
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( |
Container( |
||||
width: 80.w, |
|
||||
height: 80.w, |
|
||||
|
width: 325.w, |
||||
|
height: 45.w, |
||||
decoration: BoxDecoration( |
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: 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