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() - 恢复账号数据保存完成');
|
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 {
|
Future<void> logout() async {
|
||||||
debugPrint('$_tag logout() - 开始登出,清除所有数据');
|
debugPrint('$_tag logout() - 开始登出,清除所有数据');
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:go_router/go_router.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();
|
final TextEditingController _nicknameController = TextEditingController();
|
||||||
|
|
||||||
// 当前头像路径(模拟数据)
|
// 当前头像SVG
|
||||||
String? _avatarPath;
|
String? _avatarSvg;
|
||||||
|
|
||||||
|
// 原始昵称(用于判断是否修改)
|
||||||
|
String? _originalNickname;
|
||||||
|
|
||||||
// 是否正在保存
|
// 是否正在保存
|
||||||
bool _isSaving = false;
|
bool _isSaving = false;
|
||||||
|
|
||||||
|
// 是否正在加载
|
||||||
|
bool _isLoading = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// 初始化昵称(模拟数据)
|
_loadUserData();
|
||||||
_nicknameController.text = '社区园丁';
|
}
|
||||||
|
|
||||||
|
/// 加载用户数据
|
||||||
|
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
|
@override
|
||||||
|
|
@ -93,7 +117,9 @@ class _EditProfilePageState extends ConsumerState<EditProfilePage> {
|
||||||
|
|
||||||
/// 保存资料
|
/// 保存资料
|
||||||
Future<void> _saveProfile() async {
|
Future<void> _saveProfile() async {
|
||||||
if (_nicknameController.text.trim().isEmpty) {
|
final newNickname = _nicknameController.text.trim();
|
||||||
|
|
||||||
|
if (newNickname.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('昵称不能为空'),
|
content: Text('昵称不能为空'),
|
||||||
|
|
@ -103,13 +129,29 @@ class _EditProfilePageState extends ConsumerState<EditProfilePage> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否有修改
|
||||||
|
final nicknameChanged = newNickname != _originalNickname;
|
||||||
|
if (!nicknameChanged) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('未做任何修改'),
|
||||||
|
backgroundColor: Color(0xFFD4AF37),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isSaving = true;
|
_isSaving = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: 调用API保存资料
|
final accountService = ref.read(accountServiceProvider);
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
|
||||||
|
// 调用API更新资料
|
||||||
|
await accountService.updateProfile(
|
||||||
|
nickname: nicknameChanged ? newNickname : null,
|
||||||
|
);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
|
|
@ -120,12 +162,12 @@ class _EditProfilePageState extends ConsumerState<EditProfilePage> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
context.pop();
|
context.pop(true); // 返回 true 表示有更新
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('保存失败: $e'),
|
content: Text('保存失败: ${e.toString().replaceAll('Exception: ', '')}'),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -162,18 +204,24 @@ class _EditProfilePageState extends ConsumerState<EditProfilePage> {
|
||||||
_buildAppBar(),
|
_buildAppBar(),
|
||||||
// 内容区域
|
// 内容区域
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: _isLoading
|
||||||
child: Column(
|
? const Center(
|
||||||
children: [
|
child: CircularProgressIndicator(
|
||||||
const SizedBox(height: 40),
|
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
|
||||||
// 头像区域
|
),
|
||||||
_buildAvatarSection(),
|
)
|
||||||
const SizedBox(height: 40),
|
: SingleChildScrollView(
|
||||||
// 昵称输入区域
|
child: Column(
|
||||||
_buildNicknameSection(),
|
children: [
|
||||||
],
|
const SizedBox(height: 40),
|
||||||
),
|
// 头像区域
|
||||||
),
|
_buildAvatarSection(),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
// 昵称输入区域
|
||||||
|
_buildNicknameSection(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
// 底部保存按钮
|
// 底部保存按钮
|
||||||
_buildSaveButton(),
|
_buildSaveButton(),
|
||||||
|
|
@ -246,28 +294,17 @@ class _EditProfilePageState extends ConsumerState<EditProfilePage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: ClipOval(
|
child: ClipOval(
|
||||||
child: _avatarPath != null
|
child: _avatarSvg != null
|
||||||
? Image.asset(
|
? SvgPicture.string(
|
||||||
_avatarPath!,
|
_avatarSvg!,
|
||||||
|
width: 128,
|
||||||
|
height: 128,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
|
||||||
return const Icon(
|
|
||||||
Icons.person,
|
|
||||||
size: 64,
|
|
||||||
color: Color(0xFF8B5A2B),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
: Image.asset(
|
: const Icon(
|
||||||
'assets/images/Image-Border@2x.png',
|
Icons.person,
|
||||||
fit: BoxFit.cover,
|
size: 64,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
color: Color(0xFF8B5A2B),
|
||||||
return const Icon(
|
|
||||||
Icons.person,
|
|
||||||
size: 64,
|
|
||||||
color: Color(0xFF8B5A2B),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue