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:
parent
1f852d1fca
commit
b36987fee1
|
|
@ -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() - 开始登出,清除所有数据');
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in New Issue