From b36987fee1120bf7c3a214fc3b1fc2dda0a740b3 Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 7 Dec 2025 20:51:59 -0800 Subject: [PATCH] feat(mobile-app): implement profile editing with backend API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add updateProfile() and getMyProfile() methods to AccountService - Load real user data (nickname, avatar) in EditProfilePage - Call PUT /user/update-profile API to save nickname changes - Display SVG avatar from backend - Add loading state while fetching user data 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../lib/core/services/account_service.dart | 75 +++++++++++ .../presentation/pages/edit_profile_page.dart | 119 ++++++++++++------ 2 files changed, 153 insertions(+), 41 deletions(-) diff --git a/frontend/mobile-app/lib/core/services/account_service.dart b/frontend/mobile-app/lib/core/services/account_service.dart index e9e8e7a7..c9fcecb2 100644 --- a/frontend/mobile-app/lib/core/services/account_service.dart +++ b/frontend/mobile-app/lib/core/services/account_service.dart @@ -770,6 +770,81 @@ class AccountService { debugPrint('$_tag _saveRecoverAccountData() - 恢复账号数据保存完成'); } + /// 更新用户资料 + /// + /// [nickname] - 新昵称(可选) + /// [avatarUrl] - 新头像URL/SVG(可选) + Future updateProfile({String? nickname, String? avatarUrl}) async { + debugPrint('$_tag updateProfile() - 开始更新用户资料'); + debugPrint('$_tag updateProfile() - nickname: $nickname, avatarUrl长度: ${avatarUrl?.length ?? 0}'); + + if (nickname == null && avatarUrl == null) { + debugPrint('$_tag updateProfile() - 无需更新,参数为空'); + return; + } + + try { + // 构建请求数据 + final Map data = {}; + if (nickname != null) { + data['nickname'] = nickname; + } + if (avatarUrl != null) { + data['avatarUrl'] = avatarUrl; + } + + // 调用 API + debugPrint('$_tag updateProfile() - 调用 PUT /user/update-profile'); + final response = await _apiClient.put('/user/update-profile', data: data); + debugPrint('$_tag updateProfile() - API 响应状态码: ${response.statusCode}'); + + // 更新本地存储 + if (nickname != null) { + debugPrint('$_tag updateProfile() - 更新本地 username: $nickname'); + await _secureStorage.write(key: StorageKeys.username, value: nickname); + } + if (avatarUrl != null) { + debugPrint('$_tag updateProfile() - 更新本地 avatarSvg'); + await _secureStorage.write(key: StorageKeys.avatarSvg, value: avatarUrl); + } + + debugPrint('$_tag updateProfile() - 用户资料更新完成'); + } on ApiException catch (e) { + debugPrint('$_tag updateProfile() - API 异常: $e'); + rethrow; + } catch (e, stackTrace) { + debugPrint('$_tag updateProfile() - 未知异常: $e'); + debugPrint('$_tag updateProfile() - 堆栈: $stackTrace'); + throw ApiException('更新用户资料失败: $e'); + } + } + + /// 获取我的资料(从服务器) + Future> getMyProfile() async { + debugPrint('$_tag getMyProfile() - 获取我的资料'); + + try { + final response = await _apiClient.get('/user/my-profile'); + debugPrint('$_tag getMyProfile() - API 响应状态码: ${response.statusCode}'); + + if (response.data == null) { + throw const ApiException('获取资料失败: 空响应'); + } + + final responseData = response.data as Map; + final data = responseData['data'] as Map; + debugPrint('$_tag getMyProfile() - 获取成功'); + return data; + } on ApiException catch (e) { + debugPrint('$_tag getMyProfile() - API 异常: $e'); + rethrow; + } catch (e, stackTrace) { + debugPrint('$_tag getMyProfile() - 未知异常: $e'); + debugPrint('$_tag getMyProfile() - 堆栈: $stackTrace'); + throw ApiException('获取资料失败: $e'); + } + } + /// 登出 Future logout() async { debugPrint('$_tag logout() - 开始登出,清除所有数据'); diff --git a/frontend/mobile-app/lib/features/profile/presentation/pages/edit_profile_page.dart b/frontend/mobile-app/lib/features/profile/presentation/pages/edit_profile_page.dart index adf9ecf9..acd8480d 100644 --- a/frontend/mobile-app/lib/features/profile/presentation/pages/edit_profile_page.dart +++ b/frontend/mobile-app/lib/features/profile/presentation/pages/edit_profile_page.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; +import '../../../../core/di/injection_container.dart'; /// 编辑资料页面 - 允许用户修改头像和昵称 /// 包含头像选择底部弹窗(拍照、从相册选择、删除头像) @@ -15,17 +17,39 @@ class _EditProfilePageState extends ConsumerState { // 昵称控制器 final TextEditingController _nicknameController = TextEditingController(); - // 当前头像路径(模拟数据) - String? _avatarPath; + // 当前头像SVG + String? _avatarSvg; + + // 原始昵称(用于判断是否修改) + String? _originalNickname; // 是否正在保存 bool _isSaving = false; + // 是否正在加载 + bool _isLoading = true; + @override void initState() { super.initState(); - // 初始化昵称(模拟数据) - _nicknameController.text = '社区园丁'; + _loadUserData(); + } + + /// 加载用户数据 + Future _loadUserData() async { + final accountService = ref.read(accountServiceProvider); + + final username = await accountService.getUsername(); + final avatarSvg = await accountService.getAvatarSvg(); + + if (mounted) { + setState(() { + _nicknameController.text = username ?? ''; + _originalNickname = username; + _avatarSvg = avatarSvg; + _isLoading = false; + }); + } } @override @@ -93,7 +117,9 @@ class _EditProfilePageState extends ConsumerState { /// 保存资料 Future _saveProfile() async { - if (_nicknameController.text.trim().isEmpty) { + final newNickname = _nicknameController.text.trim(); + + if (newNickname.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('昵称不能为空'), @@ -103,13 +129,29 @@ class _EditProfilePageState extends ConsumerState { return; } + // 检查是否有修改 + final nicknameChanged = newNickname != _originalNickname; + if (!nicknameChanged) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('未做任何修改'), + backgroundColor: Color(0xFFD4AF37), + ), + ); + return; + } + setState(() { _isSaving = true; }); try { - // TODO: 调用API保存资料 - await Future.delayed(const Duration(seconds: 1)); + final accountService = ref.read(accountServiceProvider); + + // 调用API更新资料 + await accountService.updateProfile( + nickname: nicknameChanged ? newNickname : null, + ); if (!mounted) return; @@ -120,12 +162,12 @@ class _EditProfilePageState extends ConsumerState { ), ); - context.pop(); + context.pop(true); // 返回 true 表示有更新 } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('保存失败: $e'), + content: Text('保存失败: ${e.toString().replaceAll('Exception: ', '')}'), backgroundColor: Colors.red, ), ); @@ -162,18 +204,24 @@ class _EditProfilePageState extends ConsumerState { _buildAppBar(), // 内容区域 Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - const SizedBox(height: 40), - // 头像区域 - _buildAvatarSection(), - const SizedBox(height: 40), - // 昵称输入区域 - _buildNicknameSection(), - ], - ), - ), + child: _isLoading + ? const Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Color(0xFFD4AF37)), + ), + ) + : SingleChildScrollView( + child: Column( + children: [ + const SizedBox(height: 40), + // 头像区域 + _buildAvatarSection(), + const SizedBox(height: 40), + // 昵称输入区域 + _buildNicknameSection(), + ], + ), + ), ), // 底部保存按钮 _buildSaveButton(), @@ -246,28 +294,17 @@ class _EditProfilePageState extends ConsumerState { ), ), child: ClipOval( - child: _avatarPath != null - ? Image.asset( - _avatarPath!, + child: _avatarSvg != null + ? SvgPicture.string( + _avatarSvg!, + width: 128, + height: 128, fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return const Icon( - Icons.person, - size: 64, - color: Color(0xFF8B5A2B), - ); - }, ) - : Image.asset( - 'assets/images/Image-Border@2x.png', - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return const Icon( - Icons.person, - size: 64, - color: Color(0xFF8B5A2B), - ); - }, + : const Icon( + Icons.person, + size: 64, + color: Color(0xFF8B5A2B), ), ), ),