import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; import 'package:image/image.dart' as img; import 'package:image_picker/image_picker.dart'; import 'package:news_app/constant/color_res.dart'; import 'package:news_app/model/user_model.dart'; import 'package:news_app/util/log.util.dart'; import 'package:news_app/util/toast_util.dart'; import 'package:news_app/widget/my_txt.dart'; import 'package:path_provider/path_provider.dart'; import '../../main.dart'; import '../../util/permission_util.dart'; import '../../widget/load_image.dart'; /// @author: bo.zeng /// @email: cnhbwds@gmail.com /// @date: 2025 2025/4/9 16:00 /// @description: class EditProfilePage extends ConsumerStatefulWidget { const EditProfilePage({super.key}); @override ConsumerState createState() => _EditProfilePageState(); } class _EditProfilePageState extends ConsumerState { final TextEditingController _nicknameController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController(); @override void dispose() { _nicknameController.dispose(); _descriptionController.dispose(); super.dispose(); } // 从相册选择图片 Future _pickImageFromGallery() async { final XFile? pickedFile = await ImagePicker().pickImage( source: ImageSource.gallery, imageQuality: 80, // 原生压缩(可选) ); consoleLog(pickedFile); if (pickedFile != null) { _uploadToServer(pickedFile); } } // 从相机拍照 Future _pickImageFromCamera() async { final XFile? pickedFile = await ImagePicker().pickImage( source: ImageSource.camera, imageQuality: 80, // 原生压缩(可选) ); consoleLog(pickedFile); if (pickedFile != null) { _uploadToServer(pickedFile); } } Future saveToFile(String text) async { final dir = await getTemporaryDirectory(); final file = File('${dir.path}/base64_output.txt'); await file.writeAsString(text); consoleLog('已保存到: ${file.path}'); } // 示例上传方法 void _uploadToServer(XFile? pickedFile) async { if (pickedFile != null) { final String? base64Image = await pickCompressAndConvertToBase64( pickedFile, ); // saveToFile(base64Image ?? ""); if (base64Image != null) { bool result = await ref .read(globalUserProvider.notifier) .fetchUserAvatarInfo(base64Image: base64Image); if (!mounted) return; if (result) { context.pop(); } } } else { consoleLog("获取相册错误"); } } // 2. 压缩图片 Future compressImage( XFile file, { int maxWidth = 200, int quality = 80, }) async { try { // 读取原始图片数据 final originalBytes = await file.readAsBytes(); // 解码图片 final originalImage = img.decodeImage(originalBytes); if (originalImage == null) return null; // 计算缩放比例 int width = originalImage.width; int height = originalImage.height; if (width > maxWidth) { height = (height * maxWidth / width).toInt(); width = maxWidth; } // 缩放图片 final resizedImage = img.copyResize( originalImage, width: width, height: height, ); // 获取临时目录 final tempDir = await getTemporaryDirectory(); final compressedFile = File( '${tempDir.path}/compressed_${DateTime.now().millisecondsSinceEpoch}.jpg', ); // 保存压缩后的图片 await compressedFile.writeAsBytes( img.encodeJpg(resizedImage, quality: quality), ); return compressedFile; } catch (e) { debugPrint('图片压缩错误: $e'); return null; } } Future convertImageToBase64WithPrefix(File imageFile) async { try { // 1. 读取图片文件 final bytes = await imageFile.readAsBytes(); // 2. 获取图片格式(根据文件扩展名判断) final format = _getImageFormat(imageFile.path); if (format == null) return null; // 3. 编码为Base64 final base64Str = base64Encode(bytes); // 4. 添加数据URI前缀 return base64Str; // return 'data:image/$format;base64,$base64Str'; } catch (e) { consoleLog('转换失败: $e'); return null; } } /// 获取图片格式 String? _getImageFormat(String filePath) { final extension = filePath.split('.').last.toLowerCase(); switch (extension) { case 'png': return 'png'; case 'jpg': case 'jpeg': return 'jpeg'; case 'gif': return 'gif'; case 'webp': return 'webp'; default: return null; } } // 完整流程:选择图片 -> 压缩 -> 转Base64 Future pickCompressAndConvertToBase64(XFile pickedFile) async { try { // 2. 压缩图片 final File? compressedFile = await compressImage(pickedFile); if (compressedFile == null) return null; // 3. 转换为Base64 final String? base64String = await convertImageToBase64WithPrefix( compressedFile, ); // 删除临时文件 await compressedFile.delete(); return base64String; } catch (e) { debugPrint('完整流程错误: $e'); return null; } } void saveAction(BuildContext context, WidgetRef ref) async { if (_nicknameController.text.isEmpty) { showToast("昵称不能为空"); return; } if (_descriptionController.text.isEmpty) { showToast("描述不能为空"); return; } bool result = await ref .read(globalUserProvider.notifier) .fetchUpdateUserInfo( nickname: _nicknameController.text, description: _descriptionController.text, ); if (result && context.mounted) { context.pop(); } } void _changeAvatar(BuildContext context) { showDialog( useRootNavigator: true, barrierDismissible: true, // 点击背景可关闭 context: context, builder: (context) { return SimpleDialog( title: myTxt(text: "请选择头像来源"), children: [ SimpleDialogOption( onPressed: () { Navigator.pop(context); _pickImageFromCamera(); }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: const Text('拍照'), ), ), SimpleDialogOption( onPressed: () { Navigator.pop(context); _pickImageFromGallery(); }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: const Text('从相册选择'), ), ), ], ); }, ); } @override Widget build(BuildContext context) { UserModel user = ref.watch(globalUserProvider); _nicknameController.text = user.nickName ?? ""; _descriptionController.text = user.description ?? ""; return Scaffold( resizeToAvoidBottomInset: false, backgroundColor: Colors.white, appBar: AppBar( title: myTxt( text: '个人资料', fontSize: 18.sp, fontWeight: FontWeight.bold, ), actions: [ TextButton( child: Text( '保存', style: TextStyle(color: Colors.black, fontSize: 16), ), onPressed: () { saveAction(context, ref); }, ), ], centerTitle: true, ), body: Container( padding: EdgeInsets.all(20.w), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ // Avatar section GestureDetector( onTap: () async { if (Platform.isIOS) { _changeAvatar(context); } else { // Handle avatar change bool granted = await PermissionManager.requestCameraPermission(); if (granted) { // 执行拍照逻辑 if (context.mounted) _changeAvatar(context); } else { // 提示用户权限被拒绝 showToast("需要开启存储权限"); } } }, child: Column( children: [ Container( width: 80.w, height: 80.w, decoration: BoxDecoration( border: Border.all(color: Colors.white, width: 2.w), borderRadius: BorderRadius.circular(33.r), ), child: ClipOval( child: LoadImage( user.avatar ?? '', width: 80.w, height: 80.w, holderImg: "user_avatar", ), ), ), const SizedBox(height: 8), Text( '点击更换头像', style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: color666666), ), ], ), ), SizedBox(height: 24.h), // Nickname section ProfileSection( title: '昵称', content: user.nickName ?? "", maxLines: 1, controller: _nicknameController, ), SizedBox(height: 24.h), // Bio section ProfileSection( title: '个人简介', content: user.description ?? "", maxLines: 5, controller: _descriptionController, ), ], ), ), ); } } class ProfileSection extends StatelessWidget { final String title; final String content; final int maxLines; final TextEditingController controller; const ProfileSection({ super.key, required this.title, required this.content, required this.maxLines, required this.controller, }); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), TextField( maxLines: maxLines, controller: controller, decoration: InputDecoration( border: InputBorder.none, filled: true, fillColor: Colors.grey[200], hintText: content, hintStyle: Theme.of( context, ).textTheme.bodyMedium?.copyWith(color: Colors.grey), ), ), ], ); } }