|
|
|
@ -28,6 +28,13 @@ class ProfileController extends GetxController { |
|
|
|
final Rx<User> userInfo = User.instance.obs; // 响应式用户信息对象,使用单例模式确保全局唯一性 |
|
|
|
final RxString localAvatar = ''.obs; // 本地头像路径,便于响应式更新UI |
|
|
|
final RxBool hasEdits = false.obs; // 是否有编辑 |
|
|
|
final RxBool isUpdating = false.obs; // 是否正在更新用户信息 |
|
|
|
final RxBool isEditingNickname = false.obs; // 是否正在编辑昵称(内联编辑状态) |
|
|
|
|
|
|
|
// 用于乐观更新的原始状态备份 |
|
|
|
String _originalName = ''; |
|
|
|
String _originalAvatar = ''; |
|
|
|
String _originalLocalAvatar = ''; |
|
|
|
|
|
|
|
// 使用响应式变量 |
|
|
|
final currentLanguage = 'zh'.obs; |
|
|
|
@ -35,6 +42,33 @@ class ProfileController extends GetxController { |
|
|
|
// 账号服务 |
|
|
|
late final AccountService _accountService; |
|
|
|
|
|
|
|
// 错误处理工具方法 |
|
|
|
String _getErrorMessage(dynamic error) { |
|
|
|
final errorStr = error.toString().toLowerCase(); |
|
|
|
|
|
|
|
if (errorStr.contains('network') || |
|
|
|
errorStr.contains('socket') || |
|
|
|
errorStr.contains('timeout') || |
|
|
|
errorStr.contains('connection')) { |
|
|
|
return '网络连接失败,请检查网络设置后重试'; |
|
|
|
} else if (errorStr.contains('server') || |
|
|
|
errorStr.contains('500') || |
|
|
|
errorStr.contains('502') || |
|
|
|
errorStr.contains('503')) { |
|
|
|
return '服务器暂时无法响应,请稍后重试'; |
|
|
|
} else if (errorStr.contains('401') || errorStr.contains('unauthorized')) { |
|
|
|
return '登录已过期,请重新登录'; |
|
|
|
} else if (errorStr.contains('403') || errorStr.contains('forbidden')) { |
|
|
|
return '没有权限执行此操作'; |
|
|
|
} else if (errorStr.contains('404')) { |
|
|
|
return '请求的资源不存在'; |
|
|
|
} else if (errorStr.contains('file') || errorStr.contains('image')) { |
|
|
|
return '图片处理失败,请重新选择图片'; |
|
|
|
} else { |
|
|
|
return '操作失败,请重试'; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@override |
|
|
|
void onInit() { |
|
|
|
super.onInit(); |
|
|
|
@ -158,6 +192,8 @@ class ProfileController extends GetxController { |
|
|
|
// 更新用户信息到后端 |
|
|
|
Future<void> _syncWithBackend() async { |
|
|
|
try { |
|
|
|
Logger().d('开始同步用户信息到后端'); |
|
|
|
|
|
|
|
// 创建FormData对象用于上传 |
|
|
|
dio.FormData formData = dio.FormData(); |
|
|
|
|
|
|
|
@ -182,10 +218,12 @@ class ProfileController extends GetxController { |
|
|
|
); |
|
|
|
Logger().d('已添加头像文件到请求'); |
|
|
|
} else { |
|
|
|
Logger().e('头像文件不存在'); |
|
|
|
Logger().e('头像文件不存在: ${localAvatar.value}'); |
|
|
|
throw Exception('头像文件不存在'); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
Logger().e('读取头像文件失败: $e'); |
|
|
|
throw Exception('读取头像文件失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -194,25 +232,53 @@ class ProfileController extends GetxController { |
|
|
|
// 调用API,使用FormData |
|
|
|
final response = await Api.updateUserSetting(formData); |
|
|
|
Logger().d('--更新用户信息响应: $response'); |
|
|
|
if (response != null) { |
|
|
|
|
|
|
|
if (response != null && response is Map<String, dynamic>) { |
|
|
|
// 检查响应是否包含错误 |
|
|
|
if (response.containsKey('error') || response.containsKey('msg')) { |
|
|
|
final errorMsg = response['error'] ?? response['msg'] ?? '服务器返回错误'; |
|
|
|
Logger().e('服务器返回错误: $errorMsg'); |
|
|
|
throw Exception('服务器错误: $errorMsg'); |
|
|
|
} |
|
|
|
|
|
|
|
Logger().d('更新成功: $response'); |
|
|
|
Get.snackbar('成功', '同步到服务器成功'); |
|
|
|
|
|
|
|
// 更新用户信息为服务器返回的最新数据 |
|
|
|
userInfo.update((u) { |
|
|
|
if (u != null) { |
|
|
|
u.name = response['name'] ?? ''; |
|
|
|
u.avatar = response['avatar'] ?? ''; |
|
|
|
u.name = response['name'] ?? u.name; |
|
|
|
u.avatar = response['avatar'] ?? u.avatar; |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 如果服务器返回了新的头像URL,下载并保存到本地 |
|
|
|
if (response['avatar'] != null && response['avatar'].toString().isNotEmpty) { |
|
|
|
final serverAvatar = response['avatar'].toString(); |
|
|
|
if (serverAvatar.startsWith('http')) { |
|
|
|
// 异步下载新头像,不阻塞当前流程 |
|
|
|
_downloadAndSaveAvatar(serverAvatar); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
Logger().d('更新后的用户信息: ${userInfo.value.toJson()}'); |
|
|
|
} else { |
|
|
|
Logger().e('更新失败: $response'); |
|
|
|
Get.snackbar('错误', '同步到服务器失败,请重试'); |
|
|
|
return; |
|
|
|
Logger().e('服务器响应为空或格式错误: $response'); |
|
|
|
throw Exception('服务器响应异常'); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
Logger().e('同步到后端失败: $e'); |
|
|
|
Get.snackbar('Error', '同步到服务器失败,请重试'); |
|
|
|
throw e; |
|
|
|
|
|
|
|
// 根据错误类型抛出更具体的异常 |
|
|
|
if (e.toString().contains('SocketException') || |
|
|
|
e.toString().contains('TimeoutException') || |
|
|
|
e.toString().contains('network')) { |
|
|
|
throw Exception('network: 网络连接失败,请检查网络设置'); |
|
|
|
} else if (e.toString().contains('500') || |
|
|
|
e.toString().contains('server')) { |
|
|
|
throw Exception('server: 服务器内部错误,请稍后重试'); |
|
|
|
} else { |
|
|
|
throw Exception('unknown: ${e.toString()}'); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -221,12 +287,127 @@ class ProfileController extends GetxController { |
|
|
|
if (userJson != null) { |
|
|
|
userInfo.value = User.instance; |
|
|
|
Logger().d('从本地加载用户信息: $userJson'); |
|
|
|
|
|
|
|
// 加载本地头像路径并验证文件有效性 |
|
|
|
final savedLocalAvatar = _storage.read('local_avatar'); |
|
|
|
if (savedLocalAvatar != null && savedLocalAvatar.toString().isNotEmpty) { |
|
|
|
final avatarPath = savedLocalAvatar.toString(); |
|
|
|
// 验证文件是否存在 |
|
|
|
final file = File(avatarPath); |
|
|
|
file.exists().then((exists) { |
|
|
|
if (exists) { |
|
|
|
localAvatar.value = avatarPath; |
|
|
|
Logger().d('加载本地头像路径: ${localAvatar.value}'); |
|
|
|
} else { |
|
|
|
// 文件不存在,清除无效路径 |
|
|
|
localAvatar.value = ''; |
|
|
|
_storage.remove('local_avatar'); |
|
|
|
Logger().w('本地头像文件不存在,已清除路径: $avatarPath'); |
|
|
|
} |
|
|
|
}); |
|
|
|
} else { |
|
|
|
// 确保本地头像路径为空 |
|
|
|
localAvatar.value = ''; |
|
|
|
Logger().d('没有本地头像路径'); |
|
|
|
} |
|
|
|
|
|
|
|
// 保存原始状态用于回滚 |
|
|
|
_saveOriginalState(); |
|
|
|
|
|
|
|
// 如果有远程头像地址,下载并保存到本地 |
|
|
|
if (userInfo.value.avatar.isNotEmpty) { |
|
|
|
Logger().d('远程头像地址: ${userInfo.value.avatar}'); |
|
|
|
_downloadAndSaveAvatar(userInfo.value.avatar); |
|
|
|
} |
|
|
|
|
|
|
|
// 确保状态一致性 |
|
|
|
_ensureStateConsistency(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 确保本地状态与服务器状态的一致性 |
|
|
|
void _ensureStateConsistency() { |
|
|
|
// 检查本地头像文件是否存在 |
|
|
|
if (localAvatar.value.isNotEmpty) { |
|
|
|
final file = File(localAvatar.value); |
|
|
|
file.exists().then((exists) { |
|
|
|
if (!exists) { |
|
|
|
Logger().w('本地头像文件不存在,清除本地头像路径: ${localAvatar.value}'); |
|
|
|
localAvatar.value = ''; |
|
|
|
_storage.remove('local_avatar'); |
|
|
|
|
|
|
|
// 如果有远程头像,重新下载 |
|
|
|
if (userInfo.value.avatar.isNotEmpty) { |
|
|
|
_downloadAndSaveAvatar(userInfo.value.avatar); |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 保存原始状态用于乐观更新回滚 |
|
|
|
void _saveOriginalState() { |
|
|
|
_originalName = userInfo.value.name; |
|
|
|
_originalAvatar = userInfo.value.avatar; |
|
|
|
_originalLocalAvatar = localAvatar.value; |
|
|
|
Logger().d('已保存原始状态 - 名称: $_originalName, 头像: $_originalAvatar, 本地头像: $_originalLocalAvatar'); |
|
|
|
} |
|
|
|
|
|
|
|
// 回滚到原始状态 |
|
|
|
Future<void> _rollbackToOriginalState() async { |
|
|
|
Logger().d('开始回滚到原始状态'); |
|
|
|
|
|
|
|
// 回滚内存中的状态 |
|
|
|
userInfo.update((user) { |
|
|
|
if (user != null) { |
|
|
|
user.name = _originalName; |
|
|
|
user.avatar = _originalAvatar; |
|
|
|
} |
|
|
|
}); |
|
|
|
localAvatar.value = _originalLocalAvatar; |
|
|
|
|
|
|
|
// 重置编辑状态 |
|
|
|
hasEdits.value = false; |
|
|
|
|
|
|
|
// 关键修复:回滚后立即同步到本地存储 |
|
|
|
try { |
|
|
|
await _syncLocalStorage(); |
|
|
|
Logger().d('回滚状态已同步到本地存储'); |
|
|
|
} catch (e) { |
|
|
|
Logger().e('回滚状态同步到本地存储失败: $e'); |
|
|
|
} |
|
|
|
|
|
|
|
Logger().d('已回滚到原始状态 - 名称: $_originalName, 头像: $_originalAvatar, 本地头像: $_originalLocalAvatar'); |
|
|
|
} |
|
|
|
|
|
|
|
// 开始编辑昵称(内联编辑) |
|
|
|
void startEditingNickname() { |
|
|
|
isEditingNickname.value = true; |
|
|
|
Logger().d('开始内联编辑昵称'); |
|
|
|
} |
|
|
|
|
|
|
|
// 完成编辑昵称(内联编辑) |
|
|
|
void finishEditingNickname(String newName) { |
|
|
|
isEditingNickname.value = false; |
|
|
|
|
|
|
|
if (newName.trim().isNotEmpty && newName.trim() != _originalName) { |
|
|
|
// 乐观更新 |
|
|
|
userInfo.update((user) { |
|
|
|
if (user != null) { |
|
|
|
user.name = newName.trim(); |
|
|
|
} |
|
|
|
}); |
|
|
|
hasEdits.value = true; |
|
|
|
Logger().d('昵称内联编辑完成,新名称: ${newName.trim()}'); |
|
|
|
} else { |
|
|
|
Logger().d('昵称内联编辑取消或无变化'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 取消编辑昵称(内联编辑) |
|
|
|
void cancelEditingNickname() { |
|
|
|
isEditingNickname.value = false; |
|
|
|
Logger().d('取消内联编辑昵称'); |
|
|
|
} |
|
|
|
|
|
|
|
// 下载并保存远程头像 |
|
|
|
@ -273,6 +454,29 @@ class ProfileController extends GetxController { |
|
|
|
|
|
|
|
Future<void> saveUserInfo() async { |
|
|
|
await _storage.write(userKey, userInfo.value.toJson()); |
|
|
|
Logger().d('用户信息已保存到本地存储'); |
|
|
|
} |
|
|
|
|
|
|
|
// 同步所有相关状态到本地存储 |
|
|
|
Future<void> _syncLocalStorage() async { |
|
|
|
try { |
|
|
|
// 保存用户信息 |
|
|
|
await saveUserInfo(); |
|
|
|
|
|
|
|
// 同步本地头像路径 |
|
|
|
if (localAvatar.value.isNotEmpty) { |
|
|
|
await _storage.write('local_avatar', localAvatar.value); |
|
|
|
Logger().d('本地头像路径已保存: ${localAvatar.value}'); |
|
|
|
} else { |
|
|
|
// 如果本地头像为空,清除存储中的路径 |
|
|
|
await _storage.remove('local_avatar'); |
|
|
|
Logger().d('本地头像路径已清除'); |
|
|
|
} |
|
|
|
|
|
|
|
Logger().d('所有状态已同步到本地存储'); |
|
|
|
} catch (e) { |
|
|
|
Logger().e('同步本地存储失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 更新用户头像 |
|
|
|
@ -285,82 +489,152 @@ class ProfileController extends GetxController { |
|
|
|
); |
|
|
|
|
|
|
|
if (image != null) { |
|
|
|
// 检查文件大小 |
|
|
|
final file = File(image.path); |
|
|
|
var fileSize = await file.length(); |
|
|
|
var quality = 85; |
|
|
|
|
|
|
|
// 如果文件大于1MB,进行压缩 |
|
|
|
while (fileSize > 1024 * 1024 && quality > 30) { |
|
|
|
final compressedImage = await FlutterImageCompress.compressAndGetFile( |
|
|
|
file.absolute.path, |
|
|
|
file.absolute.path, // 覆盖原文件 |
|
|
|
quality: quality, |
|
|
|
minWidth: 800, |
|
|
|
minHeight: 800, |
|
|
|
); |
|
|
|
try { |
|
|
|
// 保存当前状态用于可能的回滚 |
|
|
|
_saveOriginalState(); |
|
|
|
|
|
|
|
// 检查文件大小 |
|
|
|
final file = File(image.path); |
|
|
|
var fileSize = await file.length(); |
|
|
|
var quality = 85; |
|
|
|
|
|
|
|
// 如果文件大于1MB,进行压缩 |
|
|
|
while (fileSize > 1024 * 1024 && quality > 30) { |
|
|
|
final compressedImage = await FlutterImageCompress.compressAndGetFile( |
|
|
|
file.absolute.path, |
|
|
|
file.absolute.path, // 覆盖原文件 |
|
|
|
quality: quality, |
|
|
|
minWidth: 800, |
|
|
|
minHeight: 800, |
|
|
|
); |
|
|
|
|
|
|
|
if (compressedImage == null) { |
|
|
|
Get.snackbar('错误', '图片压缩失败'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (compressedImage == null) { |
|
|
|
Get.snackbar('错误', '图片压缩失败'); |
|
|
|
return; |
|
|
|
//更新文件大小和质量 |
|
|
|
fileSize = await compressedImage.length(); |
|
|
|
quality -= 5; |
|
|
|
Logger().d('压缩后文件大小:$fileSize'); |
|
|
|
} |
|
|
|
|
|
|
|
//更新文件大小和质量 |
|
|
|
fileSize = await compressedImage.length(); |
|
|
|
quality -= 5; |
|
|
|
Logger().d('压缩后文件大小:$fileSize'); |
|
|
|
} |
|
|
|
|
|
|
|
userInfo.update((user) { |
|
|
|
if (user != null) { |
|
|
|
user.avatar = image.path; |
|
|
|
} |
|
|
|
}); |
|
|
|
// 乐观更新:立即更新UI |
|
|
|
userInfo.update((user) { |
|
|
|
if (user != null) { |
|
|
|
user.avatar = image.path; |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
// 更新本地头像路径 |
|
|
|
localAvatar.value = image.path; |
|
|
|
await _storage.write('local_avatar', image.path); |
|
|
|
// await saveUserInfo(); //保存到本地 |
|
|
|
//await _syncWithBackend(); //同步到后端 |
|
|
|
|
|
|
|
// 通知 SettingsController 更新 |
|
|
|
// if (Get.isRegistered<SettingsController>()) { |
|
|
|
// Get.find<SettingsController>().loadUserInfo(); |
|
|
|
// } |
|
|
|
hasEdits.value = true; //表示有编辑 |
|
|
|
// 更新本地头像路径 |
|
|
|
localAvatar.value = image.path; |
|
|
|
await _storage.write('local_avatar', image.path); |
|
|
|
hasEdits.value = true; //表示有编辑 |
|
|
|
|
|
|
|
Logger().d('头像已乐观更新,等待用户保存'); |
|
|
|
} catch (e) { |
|
|
|
Logger().e('更新头像失败: $e'); |
|
|
|
final errorMessage = _getErrorMessage(e); |
|
|
|
Get.snackbar('头像更新失败', errorMessage, |
|
|
|
duration: const Duration(seconds: 3), |
|
|
|
snackPosition: SnackPosition.TOP, |
|
|
|
); |
|
|
|
// 发生错误时回滚状态 |
|
|
|
await _rollbackToOriginalState(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 更新用户名 |
|
|
|
// 更新用户名和头像(保存操作) |
|
|
|
Future<void> updateUserNameAndAvatar(String name) async { |
|
|
|
if (name.trim().isEmpty) { |
|
|
|
Logger().e('无效名称: $name'); |
|
|
|
Get.snackbar('错误', '用户名不能为空'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 显示加载状态 |
|
|
|
isUpdating.value = true; |
|
|
|
|
|
|
|
try { |
|
|
|
userInfo.update((user) { |
|
|
|
if (user != null) { |
|
|
|
user.name = name.trim(); |
|
|
|
Logger().d('名称更新为: ${user.name}'); |
|
|
|
} |
|
|
|
}); |
|
|
|
Logger().d('开始保存用户信息 - 名称: ${name.trim()}, 本地头像: ${localAvatar.value}'); |
|
|
|
|
|
|
|
//await saveUserInfo(); //保存到本地 |
|
|
|
await _syncWithBackend(); //同步到后端 |
|
|
|
// 同步到后端 |
|
|
|
await _syncWithBackend(); |
|
|
|
|
|
|
|
//同步更新 SettingsController |
|
|
|
// 同步成功后更新原始状态和本地存储 |
|
|
|
_saveOriginalState(); |
|
|
|
await _syncLocalStorage(); |
|
|
|
|
|
|
|
// 同步更新 SettingsController |
|
|
|
if (Get.isRegistered<SettingsController>()) { |
|
|
|
final settingsController = Get.find<SettingsController>(); |
|
|
|
settingsController.userInfo.value.name = userInfo.value.name; |
|
|
|
settingsController.userInfo.value.avatar = |
|
|
|
'${userInfo.value.avatar}?time=${DateTime.now().millisecondsSinceEpoch}'; |
|
|
|
//settingsController.loadUserInfo(); |
|
|
|
settingsController.userInfo.refresh(); |
|
|
|
} |
|
|
|
|
|
|
|
// 重置编辑状态 |
|
|
|
hasEdits.value = false; |
|
|
|
|
|
|
|
Logger().d('用户信息保存成功'); |
|
|
|
|
|
|
|
// 显示成功提示 |
|
|
|
Get.snackbar( |
|
|
|
'保存成功', |
|
|
|
'个人信息已更新', |
|
|
|
duration: const Duration(seconds: 3), |
|
|
|
snackPosition: SnackPosition.TOP, |
|
|
|
backgroundColor: Colors.green.withOpacity(0.1), |
|
|
|
colorText: Colors.green.shade700, |
|
|
|
icon: Icon( |
|
|
|
Icons.check_circle_outline, |
|
|
|
color: Colors.green.shade700, |
|
|
|
), |
|
|
|
margin: const EdgeInsets.all(16), |
|
|
|
borderRadius: 8, |
|
|
|
isDismissible: true, |
|
|
|
forwardAnimationCurve: Curves.easeOutBack, |
|
|
|
); |
|
|
|
|
|
|
|
Logger().d('成功提示已显示'); |
|
|
|
|
|
|
|
// 备用成功提示机制 - 使用ScaffoldMessenger确保用户看到 |
|
|
|
Future.delayed(const Duration(milliseconds: 100), () { |
|
|
|
if (Get.context != null) { |
|
|
|
ScaffoldMessenger.of(Get.context!).showSnackBar( |
|
|
|
const SnackBar( |
|
|
|
content: Row( |
|
|
|
children: [ |
|
|
|
Icon(Icons.check_circle, color: Colors.white), |
|
|
|
SizedBox(width: 8), |
|
|
|
Text('个人信息已更新'), |
|
|
|
], |
|
|
|
), |
|
|
|
backgroundColor: Colors.green, |
|
|
|
duration: Duration(seconds: 2), |
|
|
|
behavior: SnackBarBehavior.floating, |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
} catch (e) { |
|
|
|
Logger().e('更新用户名和头像错误: $e'); |
|
|
|
Get.snackbar('错误', '更新用户名和头像失败'); |
|
|
|
|
|
|
|
// 同步失败时回滚到原始状态 |
|
|
|
await _rollbackToOriginalState(); |
|
|
|
|
|
|
|
// 显示错误提示 |
|
|
|
final errorMessage = _getErrorMessage(e); |
|
|
|
Get.snackbar('保存失败', errorMessage, |
|
|
|
duration: const Duration(seconds: 4), |
|
|
|
snackPosition: SnackPosition.TOP, |
|
|
|
); |
|
|
|
} finally { |
|
|
|
// 隐藏加载状态 |
|
|
|
isUpdating.value = false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|