import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:dating_touchme_app/components/page_appbar.dart'; import 'package:dating_touchme_app/config/emoji_config.dart'; import 'package:dating_touchme_app/controller/home/send_timeline_controller.dart'; import 'package:dating_touchme_app/extension/ex_widget.dart'; import 'package:dating_touchme_app/generated/assets.dart'; import 'package:dating_touchme_app/network/home_api.dart'; import 'package:dating_touchme_app/oss/oss_manager.dart'; import 'package:dating_touchme_app/widget/emoji_panel.dart'; import 'package:flustars/flustars.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; class SendTimeline extends StatefulWidget { const SendTimeline({super.key}); @override State createState() => _SendTimelineState(); } class _SendTimelineState extends State { String title = ""; String message = ''; final TextEditingController messageController = TextEditingController(); final FocusNode focusNode = FocusNode(); bool isEmojiVisible = false; List imgList = []; late final HomeApi _homeApi; bool isClick = false; @override void initState() { super.initState(); _homeApi = Get.find(); focusNode.addListener(() { if (focusNode.hasFocus) { // 输入框获得焦点(键盘弹起),关闭所有控制面板 isEmojiVisible = false; setState(() { }); } }); } @override void dispose() { super.dispose(); focusNode.dispose(); } void _showAvatarPopup(){ Navigator.of(Get.context!).push( TDSlidePopupRoute( slideTransitionFrom: SlideTransitionFrom.bottom, builder: (context) { return Container( height: 176, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( topLeft: Radius.circular(12.0), topRight: Radius.circular(12.0), ), ), child: Column( children: [ ClipRRect( borderRadius: BorderRadius.only( topLeft: Radius.circular(12.0), topRight: Radius.circular(12.0), ), child: TDCell( arrow: false, titleWidget: Center( child: Text('拍照', style: TextStyle(fontSize: 16.w, color: const Color.fromRGBO(51, 51, 51, 1))), ), style: TDCellStyle( padding: EdgeInsets.all(TDTheme.of(context).spacer16), clickBackgroundColor: TDTheme.of(context).bgColorContainerHover, cardBorderRadius: BorderRadius.only( topLeft: Radius.circular(12.0), topRight: Radius.circular(12.0), ) ), onClick: (cell) async{ Navigator.pop(context); if(9 - imgList.length == 1){ await handleCameraCapture(); } else { if(imgList.length >= 9){ SmartDialog.showToast('超出数量限制,请先删除再尝试上传'); return; } await handleCameraCapture(); } }, ), ), const TDDivider(), TDCell( arrow: false, titleWidget: Center( child: Text('从相册选择'), ), onClick: (cell) async{ Navigator.pop(context); if(9 - imgList.length == 1){ await handleGallerySelection(); } else { if(imgList.length >= 9){ SmartDialog.showToast('超出数量限制,请先删除再尝试上传'); return; } await handleMultiGallerySelection(); } }, ), Expanded( child: Container( color: Color(0xFFF3F3F3), ), ), TDCell( arrow: false, titleWidget: Center( child: Text('取消'), ), onClick: (cell){ Navigator.pop(context); }, ), ], ), ); }), ); } // 选择头像 - 业务逻辑处理 Future handleCameraCapture() async { try { // 请求相机权限 final ok = await _ensurePermission( Permission.camera, denyToast: '相机权限被拒绝,请在设置中允许访问相机', ); if (!ok) return; // 请求麦克风权限(部分设备拍照/录像会一并请求建议预授权) await _ensurePermission(Permission.microphone, denyToast: '麦克风权限被拒绝'); // 权限通过后拍照 final ImagePicker picker = ImagePicker(); final XFile? photo = await picker.pickImage(source: ImageSource.camera); if (photo != null) { await processSelectedImage(File(photo.path)); } } catch (e) { print('拍照失败: $e'); // 更友好的错误提示 if (e.toString().contains('permission') || e.toString().contains('权限')) { SmartDialog.showToast('相机权限被拒绝,请在设置中允许访问相机'); } else if (e.toString().contains('camera') || e.toString().contains('相机')) { SmartDialog.showToast('设备没有可用的相机'); } else { SmartDialog.showToast('拍照失败,请重试'); } } } Future handleGallerySelection() async { try { // 请求相册/照片权限 // final ok = await _ensurePermission( // Permission.photos, // // Android 上 photos 等价于 storage/mediaLibrary,permission_handler 会映射 // denyToast: '相册权限被拒绝,请在设置中允许访问相册', // ); // if (!ok) return; // 从相册选择图片 final ImagePicker picker = ImagePicker(); final XFile? image = await picker.pickImage(source: ImageSource.gallery); if (image != null) { await processSelectedImage(File(image.path)); } } catch (e) { print('选择图片失败: $e'); // 更友好的错误提示 if (e.toString().contains('permission') || e.toString().contains('权限')) { SmartDialog.showToast('相册权限被拒绝,请在设置中允许访问相册'); } else { SmartDialog.showToast('选择图片失败,请重试'); } } } Future handleMultiGallerySelection() async { try { // 请求相册/照片权限 // final ok = await _ensurePermission( // Permission.photos, // // Android 上 photos 等价于 storage/mediaLibrary,permission_handler 会映射 // denyToast: '相册权限被拒绝,请在设置中允许访问相册', // ); // if (!ok) return; // 从相册选择图片 final ImagePicker picker = ImagePicker(); final List? image = await picker.pickMultiImage(limit: 9 - imgList.length); if (image != null) { final futures = image.map((e){ return processSelectedMoreImage(File(e.path)); }); final list = await Future.wait(futures); imgList.addAll(list); print(imgList); SmartDialog.dismiss(); SmartDialog.showToast('上传相册成功'); setState(() { }); } } catch (e) { print('选择图片失败: $e'); // 更友好的错误提示 if (e.toString().contains('permission') || e.toString().contains('权限')) { SmartDialog.showToast('相册权限被拒绝,请在设置中允许访问相册'); } else { SmartDialog.showToast('选择图片失败,请重试'); } } } // 通用权限申请 Future _ensurePermission(Permission permission, {String? denyToast}) async { var status = await permission.status; if (status.isGranted) return true; if (status.isDenied || status.isRestricted || status.isLimited) { status = await permission.request(); if (status.isGranted) return true; if (denyToast != null) SmartDialog.showToast(denyToast); return false; } if (status.isPermanentlyDenied) { if (denyToast != null) SmartDialog.showToast('$denyToast,前往系统设置开启'); // 延迟弹设置,避免与弹窗冲突 Future.delayed(const Duration(milliseconds: 300), openAppSettings); return false; } return false; } // 处理选中的图片 Future processSelectedImage(File imageFile) async { try { // 显示加载提示 SmartDialog.showLoading(msg: '上传相册中...'); String objectName = '${DateUtil.getNowDateMs()}.${imageFile.path.split('.').last}'; String imageUrl = await OSSManager.instance.uploadFile(imageFile.readAsBytesSync(), objectName); print('上传成功,图片URL: $imageUrl'); imgList.add(imageUrl); SmartDialog.dismiss(); SmartDialog.showToast('相册上传成功'); setState(() { }); } catch (e) { SmartDialog.dismiss(); print('处理图片失败: $e'); SmartDialog.showToast('上传相册失败,请重试'); } } // 处理选中的图片 Future processSelectedMoreImage(File imageFile) async { try { // 显示加载提示 SmartDialog.showLoading(msg: '上传相册中...'); String objectName = '${DateUtil.getNowDateMs()}.${imageFile.path.split('.').last}'; String imageUrl = await OSSManager.instance.uploadFile(imageFile.readAsBytesSync(), objectName); print('上传成功,图片URL: $imageUrl'); return imageUrl; } catch (e) { SmartDialog.dismiss(); print('处理图片失败: $e'); SmartDialog.showToast('上传相册失败,请重试'); return ""; } } void toggleEmojiPanel() { isEmojiVisible = !isEmojiVisible; FocusManager.instance.primaryFocus?.unfocus(); setState(() { }); } void handleEmojiSelected(EmojiItem emoji) { // 将表情添加到输入框 final currentText = messageController.text; final emojiText = '[emoji:${emoji.id}]'; messageController.text = currentText + emojiText; // 将光标移到末尾 messageController.selection = TextSelection.fromPosition( TextPosition(offset: messageController.text.length), ); setState(() {}); // 刷新显示 } /// 构建输入框内容(文本+表情) List buildInputContentWidgets() { final List widgets = []; final text = messageController.value.text; final RegExp emojiRegex = RegExp(r'\[emoji:(\d+)\]'); int lastMatchEnd = 0; final matches = emojiRegex.allMatches(text); for (final match in matches) { // 添加表情之前的文本 if (match.start > lastMatchEnd) { final textPart = text.substring(lastMatchEnd, match.start); widgets.add( Text( textPart, style: TextStyle(fontSize: 14.sp, color: Colors.black), ), ); } // 添加表情图片 final emojiId = match.group(1); if (emojiId != null) { final emoji = EmojiConfig.getEmojiById(emojiId); if (emoji != null) { widgets.add( Padding( padding: EdgeInsets.symmetric(horizontal: 0), child: Image.asset( emoji.path, width: 24.w, height: 24.w, fit: BoxFit.contain, ), ), ); } } lastMatchEnd = match.end; } // 添加剩余的文本 if (lastMatchEnd < text.length) { final textPart = text.substring(lastMatchEnd); widgets.add( Text( textPart, style: TextStyle(fontSize: 14.sp, color: Colors.black), ), ); } return widgets; } sendTimeLine() async { try { if(isClick) return; isClick = true; if(messageController.value.text == ""){ SmartDialog.showToast('请填写帖子内容'); return; } final response = await _homeApi.userCreatePost({ "content": messageController.value.text, "mediaUrls": imgList.isNotEmpty ? imgList.join(",") : "", "topicTags": "" }); if (response.data.isSuccess) { SmartDialog.showToast('帖子已发布成功,待审核通过后可见'); Get.back(); } else { // 响应失败,抛出异常 throw Exception(response.data.message ?? '获取数据失败'); } } catch(e){ print('帖子发布失败: $e'); SmartDialog.showToast('帖子发布失败'); rethrow; } finally { isClick = false; } } @override Widget build(BuildContext context) { return Scaffold( appBar: PageAppbar(title: "", right: Container( width: 53.w, height: 26.w, margin: EdgeInsets.only(right: 17.w), decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(8.w)), color: const Color.fromRGBO(117, 98, 249, 1) ), child: Center( child: Text( "发送", style: TextStyle( fontSize: 13.w, color: Colors.white ), ), ), ).onTap((){ sendTimeLine(); }),), body: 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, ), 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.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, ), 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, ), 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(() { }); }), ) ], ); }), ], ), Row( children: [ Image.asset( Assets.imagesImg, width: 20.w, ).onTap(() { _showAvatarPopup(); }), SizedBox(width: 25.w,), Image.asset( Assets.imagesEmoji, width: 18.w, ).onTap(toggleEmojiPanel) ], ), // 表情面板 EmojiPanel( isVisible: isEmojiVisible, onEmojiSelected: handleEmojiSelected, ), ], ), ), ); } }