diff --git a/lib/modules/meeting/views/meeting_mine_view.dart b/lib/modules/meeting/views/meeting_mine_view.dart index ae0337965..c84d61cf1 100644 --- a/lib/modules/meeting/views/meeting_mine_view.dart +++ b/lib/modules/meeting/views/meeting_mine_view.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; -import 'dart:io'; import '../../../routes/app_routes.dart'; import '../../profile/controllers/profile_controller.dart'; +import '../../profile/views/unified_avatar.dart'; import '../controllers/meeting_mine_controller.dart'; import 'bottomSheet/statistics_bottom_sheet.dart'; @@ -104,32 +104,17 @@ class MeetingMineView extends GetView { onTap: () => Get.toNamed(Routes.profileMeeting), // 跳转到个人信息页面 child: Row( children: [ - // 统一头像显示逻辑:本地头像 > 网络头像 > 默认图标 - Obx(() => Container( - width: 50.w, - height: 50.w, - margin: EdgeInsets.only(right: 10.w), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(50.w), - ), - child: CircleAvatar( - radius: 25.w, - backgroundImage: profileCtrl.localAvatar.value.isNotEmpty - ? FileImage(File(profileCtrl.localAvatar.value)) - : (profileCtrl.userInfo.value.avatar.isNotEmpty - ? NetworkImage(profileCtrl.userInfo.value.avatar) - : null) as ImageProvider?, - backgroundColor: Colors.grey[200], - child: (profileCtrl.localAvatar.value.isEmpty && - profileCtrl.userInfo.value.avatar.isEmpty) - ? Icon( - Icons.person, - size: 25.r, - color: Colors.grey[600], - ) - : null, - ), - )), + // 使用统一头像组件,避免闪烁问题 + Container( + margin: EdgeInsets.only(right: 10.w), + child: UnifiedAvatar( + radius: 25.w, + backgroundColor: Colors.grey[200], + iconColor: Colors.grey[600], + iconSize: 25.r, + isDarkMode: false, + ), + ), Expanded( child: Obx( () => Text( diff --git a/lib/modules/profile/controllers/profile_controller.dart b/lib/modules/profile/controllers/profile_controller.dart index ae71fea8b..e679fecb7 100644 --- a/lib/modules/profile/controllers/profile_controller.dart +++ b/lib/modules/profile/controllers/profile_controller.dart @@ -31,6 +31,7 @@ class ProfileController extends GetxController { final RxBool hasEdits = false.obs; // 是否有编辑 final RxBool isUpdating = false.obs; // 是否正在更新用户信息 final RxBool isEditingNickname = false.obs; // 是否正在编辑昵称(内联编辑状态) + final RxBool isAvatarReady = false.obs; // 头像数据是否已准备好,用于防止闪烁 //音乐播放管理 final _userportraitManager = Get.find(); // 用于乐观更新的原始状态备份 @@ -289,19 +290,23 @@ class ProfileController extends GetxController { } } - void loadUserInfo() { + void loadUserInfo() async { final userJson = _storage.read(userKey); if (userJson != null) { userInfo.value = User.instance; Logger().d('从本地加载用户信息: $userJson'); - // 加载本地头像路径并验证文件有效性 + // 重置头像准备状态 + isAvatarReady.value = false; + + // 同步加载本地头像路径并验证文件有效性 - 修复闪烁问题 final savedLocalAvatar = _storage.read('local_avatar'); if (savedLocalAvatar != null && savedLocalAvatar.toString().isNotEmpty) { final avatarPath = savedLocalAvatar.toString(); - // 验证文件是否存在 + // 同步验证文件是否存在,避免异步导致的闪烁 final file = File(avatarPath); - file.exists().then((exists) { + try { + final exists = await file.exists(); if (exists) { localAvatar.value = avatarPath; Logger().d('加载本地头像路径: ${localAvatar.value}'); @@ -311,18 +316,26 @@ class ProfileController extends GetxController { _storage.remove('local_avatar'); Logger().w('本地头像文件不存在,已清除路径: $avatarPath'); } - }); + } catch (e) { + // 文件访问异常,清除无效路径 + localAvatar.value = ''; + _storage.remove('local_avatar'); + Logger().e('验证本地头像文件时发生异常: $e'); + } } else { // 确保本地头像路径为空 localAvatar.value = ''; Logger().d('没有本地头像路径'); } + // 头像数据准备完成 + isAvatarReady.value = true; + // 保存原始状态用于回滚 _saveOriginalState(); - // 如果有远程头像地址,下载并保存到本地 - if (userInfo.value.avatar.isNotEmpty) { + // 如果有远程头像地址且本地头像不存在,才下载并保存到本地 + if (userInfo.value.avatar.isNotEmpty && localAvatar.value.isEmpty) { Logger().d('远程头像地址: ${userInfo.value.avatar}'); _downloadAndSaveAvatar(userInfo.value.avatar); } @@ -333,11 +346,12 @@ class ProfileController extends GetxController { } // 确保本地状态与服务器状态的一致性 - void _ensureStateConsistency() { - // 检查本地头像文件是否存在 + void _ensureStateConsistency() async { + // 同步检查本地头像文件是否存在 - 修复闪烁问题 if (localAvatar.value.isNotEmpty) { final file = File(localAvatar.value); - file.exists().then((exists) { + try { + final exists = await file.exists(); if (!exists) { Logger().w('本地头像文件不存在,清除本地头像路径: ${localAvatar.value}'); localAvatar.value = ''; @@ -348,7 +362,11 @@ class ProfileController extends GetxController { _downloadAndSaveAvatar(userInfo.value.avatar); } } - }); + } catch (e) { + Logger().e('检查本地头像文件一致性时发生异常: $e'); + localAvatar.value = ''; + _storage.remove('local_avatar'); + } } } diff --git a/lib/modules/profile/views/profile_meeting_view.dart b/lib/modules/profile/views/profile_meeting_view.dart index a2bcad1c1..ccf7d29a1 100644 --- a/lib/modules/profile/views/profile_meeting_view.dart +++ b/lib/modules/profile/views/profile_meeting_view.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../controllers/profile_controller.dart'; +import 'unified_avatar.dart'; class ProfileMeetingView extends GetView { ProfileMeetingView({super.key}); @@ -100,15 +101,15 @@ class ProfileMeetingView extends GetView { SizedBox(height: 16.h), _buildNicknameItem(isDarkMode), Divider( - height: 1.h, + height: 0.5.h, color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), _buildLoginMethodItem(isDarkMode), Divider( - height: 1.h, + height: 0.5.h, color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), _buildManageLoginItem(isDarkMode), Divider( - height: 1.h, + height: 0.5.h, color: isDarkMode ? Colors.grey[800] : Colors.grey[200]), SizedBox(height: 32.h), _buildLogoutButton(isDarkMode), @@ -126,55 +127,43 @@ class ProfileMeetingView extends GetView { return Center( child: Column( children: [ - Obx(() => GestureDetector( - onTap: () => controller.updateUserAvatar(), - child: Stack( - children: [ - CircleAvatar( - radius: 40.r, - backgroundImage: controller.localAvatar.value.isNotEmpty - ? FileImage(File(controller.localAvatar.value)) - : (controller.userInfo.value.avatar.isNotEmpty - ? NetworkImage(controller.userInfo.value.avatar) - : null) as ImageProvider?, - backgroundColor: - isDarkMode ? Colors.grey[800] : Colors.grey[200], - child: (controller.localAvatar.value.isEmpty && - controller.userInfo.value.avatar.isEmpty) - ? Icon( - Icons.person, - size: 30.r, - color: isDarkMode - ? Colors.grey[400] - : Colors.grey[600], - ) - : null, - ), - Positioned( - bottom: 0, - right: 0, - child: Container( - width: 22.w, - height: 22.w, - decoration: BoxDecoration( - color: - isDarkMode ? Colors.grey[600] : Colors.grey[350], - shape: BoxShape.circle, - border: Border.all( - color: isDarkMode ? Colors.black : Colors.white, - width: 0.w, - ), - ), - child: Icon( - Icons.camera_alt, - size: 14.w, - color: Colors.black, - ), + // 使用统一头像组件,避免闪烁问题 + GestureDetector( + onTap: () => controller.updateUserAvatar(), + child: Stack( + children: [ + UnifiedAvatar( + radius: 40.r, + backgroundColor: + isDarkMode ? Colors.grey[800] : Colors.grey[200], + iconColor: isDarkMode ? Colors.grey[400] : Colors.grey[600], + iconSize: 30.r, + isDarkMode: isDarkMode, + ), + Positioned( + bottom: 0, + right: 0, + child: Container( + width: 22.w, + height: 22.w, + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[600] : Colors.grey[350], + shape: BoxShape.circle, + border: Border.all( + color: isDarkMode ? Colors.black : Colors.white, + width: 0.w, ), ), - ], + child: Icon( + Icons.camera_alt, + size: 14.w, + color: Colors.black, + ), + ), ), - )), + ], + ), + ), ], ), ); @@ -336,9 +325,8 @@ class ProfileMeetingView extends GetView { child: OutlinedButton( onPressed: () => _showLogoutDialog(isDarkMode), style: OutlinedButton.styleFrom( - backgroundColor: isDarkMode ? Colors.grey[900] : Colors.grey[50], - side: BorderSide( - color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!), + backgroundColor: isDarkMode ? Colors.grey[700] : Colors.grey[200], + side: BorderSide.none, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.r), ), @@ -358,10 +346,11 @@ class ProfileMeetingView extends GetView { return SizedBox( width: double.infinity, height: 48.h, - child: ElevatedButton( + child: OutlinedButton( onPressed: () => _showDeleteAccountDialog(isDarkMode), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, + style: OutlinedButton.styleFrom( + side: BorderSide.none, + backgroundColor: isDarkMode ? Colors.grey[700] : Colors.grey[200], shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.r), ), @@ -370,7 +359,7 @@ class ProfileMeetingView extends GetView { '删除账号', style: TextStyle( fontSize: 16.sp, - color: Colors.white, + color: Colors.red[600], ), ), ), @@ -378,11 +367,25 @@ class ProfileMeetingView extends GetView { } Widget _buildWarningText() { - return Text( - '警告:您的账号信息和所有文件将被永久删除。', - style: TextStyle( - fontSize: 12.sp, - color: Colors.red, + return Text.rich( + TextSpan( + children: [ + TextSpan( + text: '警告:', + style: TextStyle( + fontSize: 12.sp, // 稍微大一点的字号 + color: Colors.red[800], // 更深的红色 + //fontStyle: FontStyle.italic, // 斜体 + ), + ), + TextSpan( + text: '您的账号信息和所有文件将被永久删除。', + style: TextStyle( + fontSize: 11.sp, + color: Colors.grey[600], + ), + ), + ], ), textAlign: TextAlign.center, ); @@ -464,7 +467,7 @@ class ProfileMeetingView extends GetView { controller.deleteAccount(); }, child: Text( - 'comfirm'.tr, + 'confirm'.tr, style: const TextStyle( color: Colors.red, ), diff --git a/lib/modules/profile/views/profile_view.dart b/lib/modules/profile/views/profile_view.dart index 69d3f5e22..dd3f1dea3 100644 --- a/lib/modules/profile/views/profile_view.dart +++ b/lib/modules/profile/views/profile_view.dart @@ -7,6 +7,7 @@ import '../../../routes/app_routes.dart'; import 'dart:io'; import 'dart:ui'; import '../../common/webview_page.dart'; +import 'unified_avatar.dart'; // ProfileView: 用户设置页面 // class ProfileView extends GetView { @@ -326,20 +327,14 @@ class EditProfileView extends GetView { // --- 右侧内容保持不变 --- Row( children: [ - Obx(() => CircleAvatar( - radius: 26.r, // 调整头像大小以适应行高 - backgroundImage: controller.localAvatar.value.isNotEmpty - ? FileImage(File(controller.localAvatar.value)) - : (controller.userInfo.value.avatar.isNotEmpty - ? NetworkImage(controller.userInfo.value.avatar) - : null) as ImageProvider?, - backgroundColor: Colors.grey[200], - child: (controller.localAvatar.value.isEmpty && - controller.userInfo.value.avatar.isEmpty) - ? Icon(Icons.person, - size: 20.r, color: Colors.grey) // 默认图标 - : null, // 添加背景色防止透明图片问题 - )), + // 使用统一头像组件,避免闪烁问题 + UnifiedAvatar( + radius: 26.r, + backgroundColor: Colors.grey[200], + iconColor: Colors.grey, + iconSize: 20.r, + isDarkMode: isDarkMode, + ), SizedBox(width: 8.w), Container( width: 32.w, diff --git a/lib/modules/profile/views/unified_avatar.dart b/lib/modules/profile/views/unified_avatar.dart new file mode 100644 index 000000000..d25a8a8f4 --- /dev/null +++ b/lib/modules/profile/views/unified_avatar.dart @@ -0,0 +1,137 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import '../controllers/profile_controller.dart'; + +/// 统一头像显示组件 +/// 确保所有页面的头像显示逻辑一致,避免闪烁问题 +class UnifiedAvatar extends StatelessWidget { + final double radius; + final Color? backgroundColor; + final Color? iconColor; + final double? iconSize; + final bool isDarkMode; + final VoidCallback? onTap; + + const UnifiedAvatar({ + super.key, + required this.radius, + this.backgroundColor, + this.iconColor, + this.iconSize, + this.isDarkMode = false, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final ProfileController profileCtrl = Get.find(); + + return GestureDetector( + onTap: onTap, + child: Obx(() { + // 等待头像数据准备完成,避免闪烁 + if (!profileCtrl.isAvatarReady.value) { + return _buildLoadingAvatar(); + } + + return _buildAvatar(profileCtrl); + }), + ); + } + + /// 构建加载中的头像占位符 + Widget _buildLoadingAvatar() { + return CircleAvatar( + radius: radius, + backgroundColor: + backgroundColor ?? (isDarkMode ? Colors.grey[800] : Colors.grey[200]), + child: Icon( + Icons.person, + size: iconSize ?? (radius * 0.6), + color: iconColor ?? (isDarkMode ? Colors.grey[400] : Colors.grey[600]), + ), + ); + } + + /// 构建实际头像 + Widget _buildAvatar(ProfileController profileCtrl) { + ImageProvider? imageProvider; + + // 统一头像显示优先级:本地头像 > 网络头像 > 默认图标 + if (profileCtrl.localAvatar.value.isNotEmpty) { + imageProvider = FileImage(File(profileCtrl.localAvatar.value)); + } else if (profileCtrl.userInfo.value.avatar.isNotEmpty) { + imageProvider = NetworkImage(profileCtrl.userInfo.value.avatar); + } + + return CircleAvatar( + radius: radius, + backgroundImage: imageProvider, + backgroundColor: + backgroundColor ?? (isDarkMode ? Colors.grey[800] : Colors.grey[200]), + child: (profileCtrl.localAvatar.value.isEmpty && + profileCtrl.userInfo.value.avatar.isEmpty) + ? Icon( + Icons.person, + size: iconSize ?? (radius * 0.6), + color: iconColor ?? + (isDarkMode ? Colors.grey[400] : Colors.grey[600]), + ) + : null, + ); + } +} + +/// 带容器装饰的统一头像组件 +class UnifiedAvatarWithContainer extends StatelessWidget { + final double width; + final double height; + final double radius; + final Color? backgroundColor; + final Color? iconColor; + final double? iconSize; + final bool isDarkMode; + final VoidCallback? onTap; + final EdgeInsetsGeometry? margin; + final List? boxShadow; + + const UnifiedAvatarWithContainer({ + super.key, + required this.width, + required this.height, + required this.radius, + this.backgroundColor, + this.iconColor, + this.iconSize, + this.isDarkMode = false, + this.onTap, + this.margin, + this.boxShadow, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: width, + height: height, + margin: margin, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(radius), + boxShadow: boxShadow, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: UnifiedAvatar( + radius: radius / 2, + backgroundColor: backgroundColor, + iconColor: iconColor, + iconSize: iconSize, + isDarkMode: isDarkMode, + onTap: onTap, + ), + ), + ); + } +} diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index f3beae7e6..2b4955f9f 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -3,14 +3,15 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'dart:ui'; +import 'dart:io'; import '../../../core/theme/app_colors.dart'; import '../controllers/settings_controller.dart'; import '../../../core/widgets/common_bottom_nav.dart'; import '../../../routes/app_routes.dart'; //import '../../../data/models/user_Info.dart'; -import 'dart:io'; import '../../common/webview_page.dart'; import '../../../modules/profile/controllers/profile_controller.dart'; +import '../../profile/views/unified_avatar.dart'; class SettingsView extends GetView { const SettingsView({Key? key}) : super(key: key); @@ -98,46 +99,24 @@ class SettingsView extends GetView { : ImageFilter.blur(sigmaX: 0, sigmaY: 0), child: Row( children: [ - Obx(() => GestureDetector( - //onTap: ()=>controller.updateUserAvatar(), - child: Container( - //用户头像部分 - width: 60.w, - height: 60.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30.r), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: ClipRRect( - // 裁剪圆形头像 - borderRadius: BorderRadius.circular(30.r), - child: CircleAvatar( - radius: 30.r, - // 统一头像显示优先级:本地头像 > 网络头像 > 默认图标 - backgroundImage: profileCtrl.localAvatar.value.isNotEmpty - ? FileImage(File(profileCtrl.localAvatar.value)) - : (profileCtrl.userInfo.value.avatar.isNotEmpty - ? NetworkImage(profileCtrl.userInfo.value.avatar) - : null) as ImageProvider?, - backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[200], - child: (profileCtrl.localAvatar.value.isEmpty && - profileCtrl.userInfo.value.avatar.isEmpty) - ? Icon( - Icons.person, - size: 30.r, - color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - ) - : null, - ), - ), - ), - )), + // 使用统一头像组件,避免闪烁问题 + UnifiedAvatarWithContainer( + width: 60.w, + height: 60.w, + radius: 30.r, + backgroundColor: + isDarkMode ? Colors.grey[800] : Colors.grey[200], + iconColor: isDarkMode ? Colors.grey[400] : Colors.grey[600], + iconSize: 30.r, + isDarkMode: isDarkMode, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), SizedBox(width: 16.w), Expanded( // 占据剩余空间