feat(mobile-app): implement profile editing with backend API

- 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 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-07 20:51:59 -08:00
parent 1f852d1fca
commit b36987fee1
2 changed files with 153 additions and 41 deletions

View File

@ -770,6 +770,81 @@ class AccountService {
debugPrint('$_tag _saveRecoverAccountData() - 恢复账号数据保存完成');
}
///
///
/// [nickname] -
/// [avatarUrl] - URL/SVG
Future<void> 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<String, dynamic> 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<Map<String, dynamic>> 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<String, dynamic>;
final data = responseData['data'] as Map<String, dynamic>;
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<void> logout() async {
debugPrint('$_tag logout() - 开始登出,清除所有数据');

View File

@ -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<EditProfilePage> {
//
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<void> _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<EditProfilePage> {
///
Future<void> _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<EditProfilePage> {
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<EditProfilePage> {
),
);
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<EditProfilePage> {
_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>(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<EditProfilePage> {
),
),
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),
),
),
),