| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- 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<EditProfilePage> createState() => _EditProfilePageState();
- }
- class _EditProfilePageState extends ConsumerState<EditProfilePage> {
- final TextEditingController _nicknameController = TextEditingController();
- final TextEditingController _descriptionController = TextEditingController();
- @override
- void dispose() {
- _nicknameController.dispose();
- _descriptionController.dispose();
- super.dispose();
- }
- // 从相册选择图片
- Future<void> _pickImageFromGallery() async {
- final XFile? pickedFile = await ImagePicker().pickImage(
- source: ImageSource.gallery,
- imageQuality: 80, // 原生压缩(可选)
- );
- consoleLog(pickedFile);
- if (pickedFile != null) {
- _uploadToServer(pickedFile);
- }
- }
- // 从相机拍照
- Future<void> _pickImageFromCamera() async {
- final XFile? pickedFile = await ImagePicker().pickImage(
- source: ImageSource.camera,
- imageQuality: 80, // 原生压缩(可选)
- );
- consoleLog(pickedFile);
- if (pickedFile != null) {
- _uploadToServer(pickedFile);
- }
- }
- Future<void> 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<File?> 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<String?> 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<String?> 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),
- ),
- ),
- ],
- );
- }
- }
|