From 5e9b74af65af112f895cf67781b151607e336d73 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Wed, 16 Jul 2025 16:06:11 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(profile):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=A4=B4=E5=83=8F=E7=BB=84=E4=BB=B6=E8=A7=A3?= =?UTF-8?q?=E5=86=B3=E9=97=AA=E7=83=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建 UnifiedAvatar 组件统一处理所有头像显示逻辑 - 在 ProfileController 中添加 isAvatarReady 状态防止闪烁 - 修改多个视图使用新的统一头像组件 - 优化头像加载流程,确保本地和远程头像一致性 --- .../meeting/views/meeting_mine_view.dart | 39 ++--- .../controllers/profile_controller.dart | 40 +++-- .../profile/views/profile_meeting_view.dart | 127 ++++++++-------- lib/modules/profile/views/profile_view.dart | 23 ++- lib/modules/profile/views/unified_avatar.dart | 137 ++++++++++++++++++ lib/modules/settings/views/settings_view.dart | 61 +++----- 6 files changed, 272 insertions(+), 155 deletions(-) create mode 100644 lib/modules/profile/views/unified_avatar.dart 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( // 占据剩余空间 From 4a824f9fa19e61cf79d2299d7d6ce4426fc627de Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Wed, 16 Jul 2025 17:22:20 +0800 Subject: [PATCH 2/5] =?UTF-8?q?refactor(agent=5Fview):=20=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E5=93=8D=E5=BA=94=E5=BC=8F=E5=8D=95=E4=BD=8D=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=E5=9B=BA=E5=AE=9A=E6=95=B0=E5=80=BC=E4=BB=A5=E9=80=82?= =?UTF-8?q?=E9=85=8D=E4=B8=8D=E5=90=8C=E5=B1=8F=E5=B9=95=E5=B0=BA=E5=AF=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/agent/views/agent_view.dart | 237 ++++++++++++------------ 1 file changed, 120 insertions(+), 117 deletions(-) diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index ed13c720a..ac8a8d5fc 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -61,40 +61,40 @@ class _AgentViewState extends State with WidgetsBindingObserver { backgroundColor: backgroundColor, leading: IconButton( icon: Icon(Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black87, size: 20), + color: isDarkMode ? Colors.white : Colors.black87, size: 20.sp), onPressed: () => Get.back(), ), titleSpacing: 0, title: Row( children: [ Container( - width: 40, - height: 40, + width: 40.w, + height: 40.w, decoration: BoxDecoration( color: primaryColor, - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(20.w), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), - blurRadius: 3, + blurRadius: 3.w, offset: const Offset(0, 1), ), ], ), - child: const Icon( + child: Icon( Icons.smart_toy_outlined, color: Colors.white, - size: 22, + size: 22.sp, ), ), - const SizedBox(width: 12), + SizedBox(width: 12.w), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'XiaoYan'.tr, style: TextStyle( - fontSize: 16, + fontSize: 16.sp, fontWeight: FontWeight.bold, color: isDarkMode ? Colors.white : Colors.black87, ), @@ -102,18 +102,18 @@ class _AgentViewState extends State with WidgetsBindingObserver { Row( children: [ Container( - width: 6, - height: 6, + width: 6.w, + height: 6.w, decoration: const BoxDecoration( color: Colors.green, shape: BoxShape.circle, ), ), - const SizedBox(width: 4), + SizedBox(width: 4.w), Text( 'online'.tr, style: TextStyle( - fontSize: 12, + fontSize: 12.sp, color: Colors.green, fontWeight: FontWeight.w500, ), @@ -131,6 +131,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { controller.isTtsEnabled.value ? Icons.volume_up : Icons.volume_off, + size: 24.sp, color: controller.isTtsEnabled.value ? primaryColor : isDarkMode @@ -142,9 +143,9 @@ class _AgentViewState extends State with WidgetsBindingObserver { // 更多选项 Container( - width: 34, - height: 34, - margin: const EdgeInsets.only(right: 8), + width: 34.w, + height: 34.w, + margin: EdgeInsets.only(right: 8.w), decoration: BoxDecoration( color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], @@ -153,25 +154,28 @@ class _AgentViewState extends State with WidgetsBindingObserver { if (!isDarkMode) BoxShadow( color: Colors.black.withOpacity(0.05), - blurRadius: 2, - spreadRadius: 0.5, + blurRadius: 2.w, + spreadRadius: 0.5.w, ), ], ), child: PopupMenuButton( icon: Icon(Icons.more_vert, - color: isDarkMode ? Colors.white : Colors.black87, size: 20), + color: isDarkMode ? Colors.white : Colors.black87, + size: 20.sp), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(12.w), ), color: isDarkMode ? Colors.grey[800] : Colors.white, itemBuilder: (context) => [ PopupMenuItem( child: Row( children: [ - Icon(Icons.delete_outline, color: primaryColor, size: 20), - const SizedBox(width: 8), - Text('clearChatHistory'.tr, style: TextStyle(fontSize: 14)), + Icon(Icons.delete_outline, + color: primaryColor, size: 20.sp), + SizedBox(width: 8.w), + Text('clearChatHistory'.tr, + style: TextStyle(fontSize: 14.sp)), ], ), onTap: () => Future.delayed( @@ -198,8 +202,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { controller: controller.scrollController, shrinkWrap: true, reverse: true, - padding: const EdgeInsets.symmetric( - vertical: 16, horizontal: 16), + padding: EdgeInsets.symmetric( + vertical: 16.h, horizontal: 16.w), itemCount: controller.messages.length + (controller.hasMoreMessages.value ? 1 : 0), itemBuilder: (context, index) { @@ -212,7 +216,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { final message = controller .messages[controller.messages.length - 1 - index]; return Padding( - padding: const EdgeInsets.only(bottom: 8), + padding: EdgeInsets.only(bottom: 8.h), child: GestureDetector( onLongPressStart: (LongPressStartDetails details) { @@ -237,21 +241,20 @@ class _AgentViewState extends State with WidgetsBindingObserver { // 输入区域 Container( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), decoration: BoxDecoration( color: backgroundColor, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.06), - blurRadius: 8, + blurRadius: 8.w, offset: const Offset(0, -1), ), ], border: Border( top: BorderSide( color: Colors.grey.withOpacity(0.15), - width: 1, + width: 1.w, ), ), ), @@ -269,29 +272,29 @@ class _AgentViewState extends State with WidgetsBindingObserver { Stack( children: [ ClipRRect( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(12.w), child: Image.file( File(controller.selectedImagePath.value!), - height: 150, + height: 150.h, width: double.infinity, fit: BoxFit.cover, ), ), Positioned( - top: 8, - right: 8, + top: 8.h, + right: 8.w, child: GestureDetector( onTap: () => controller.clearImageInput(), child: Container( - padding: const EdgeInsets.all(4), + padding: EdgeInsets.all(4.w), decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), shape: BoxShape.circle, ), - child: const Icon( + child: Icon( Icons.close, color: Colors.white, - size: 16, + size: 16.sp, ), ), ), @@ -300,34 +303,34 @@ class _AgentViewState extends State with WidgetsBindingObserver { ), // 预设提示选项 - const SizedBox(height: 12), + SizedBox(height: 12.h), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildPromptChip( context: context, - label: 'summarizeImageContent'.tr,//总结图片内容 + label: 'summarizeImageContent'.tr, //总结图片内容 onTap: () => controller.summarizeImage(), ), - const SizedBox(width: 8), + SizedBox(width: 8.w), _buildPromptChip( context: context, - label: 'extractImageText'.tr,//抽取图像文字 + label: 'extractImageText'.tr, //抽取图像文字 onTap: () => controller.extractTextFromImage(), ), - const SizedBox(width: 8), + SizedBox(width: 8.w), _buildPromptChip( context: context, - label: 'translateImageText'.tr,//翻译图像文字 + label: 'translateImageText'.tr, //翻译图像文字 onTap: () => controller.translateImageText(), ), ], ), ), - const SizedBox(height: 12), + SizedBox(height: 12.h), ], ); } else { @@ -353,8 +356,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { controller.toggleInputMode(); }, child: Container( - width: 44, - height: 44, + width: 44.w, + height: 44.w, decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] @@ -368,13 +371,13 @@ class _AgentViewState extends State with WidgetsBindingObserver { color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - size: 22, + size: 22.sp, ), ), ); }), - const SizedBox(width: 8), + SizedBox(width: 8.w), // 文本输入框或语音波形图 Expanded( @@ -385,23 +388,23 @@ class _AgentViewState extends State with WidgetsBindingObserver { if (controller.isTextInputMode.value) { // 文本输入模式 - 限制最大高度并只向上扩展 return Container( - constraints: const BoxConstraints( - minHeight: 44, // 最小高度 - maxHeight: 120, // 最大高度限制,防止挤压按钮 + constraints: BoxConstraints( + minHeight: 44.h, + maxHeight: 120.h, ), - padding: const EdgeInsets.symmetric( - horizontal: 16), + padding: EdgeInsets.symmetric( + horizontal: 16.w), decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[200], - borderRadius: BorderRadius.circular(22), + borderRadius: + BorderRadius.circular(22.w), border: Border.all( color: isDarkMode - ? Colors.grey[ - 800]! // 修改: 从 Colors.grey[700] 改为 Colors.grey[800] + ? Colors.grey[800]! : Colors.grey.withOpacity(0.1), - width: 1.0, + width: 1.w, ), ), child: Row( @@ -435,7 +438,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { } }, style: TextStyle( - fontSize: 15, + fontSize: 15.sp, color: isDarkMode ? Colors.white : Colors.grey[800], @@ -448,7 +451,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { .tr, // 输入消息... hintStyle: TextStyle( color: Colors.grey[500], - fontSize: 15, + fontSize: 15.sp, ), filled: false, border: InputBorder.none, @@ -458,14 +461,15 @@ class _AgentViewState extends State with WidgetsBindingObserver { color: isDarkMode ? Colors.grey[800]! : Colors.grey[200]!, - width: 1.0, + width: 1.w, ), ), contentPadding: EdgeInsets.symmetric( - vertical: isImageActive - ? 12 - : 8), // 调整垂直内边距 + vertical: + isImageActive + ? 12.h + : 8.h), ), onSubmitted: (_) => controller.sendMessage(), @@ -525,9 +529,9 @@ class _AgentViewState extends State with WidgetsBindingObserver { controller.isListening.value; return Container( - height: 50, - padding: const EdgeInsets.symmetric( - horizontal: 16), + height: 50.h, + padding: EdgeInsets.symmetric( + horizontal: 16.w), decoration: BoxDecoration( color: isPushToTalkActive ? (isDarkMode @@ -537,7 +541,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { ? Colors.grey[800] : Colors.grey[200]), borderRadius: - BorderRadius.circular(22), + BorderRadius.circular(22.w), border: Border.all( color: isPushToTalkActive ? Colors.blue @@ -546,8 +550,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { : Colors.grey .withOpacity(0.1)), width: isPushToTalkActive - ? 2.0 - : 1.0, + ? 2.w + : 1.w, ), ), child: Row( @@ -572,7 +576,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { isPushToTalkActive ? Icons.mic : Icons.mic_none, - size: 16, + size: 16.sp, color: isPushToTalkActive ? Colors.blue : Colors.grey[500], @@ -607,7 +611,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { }), ), - const SizedBox(width: 8), + SizedBox(width: 8.w), // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 Obx(() { @@ -627,7 +631,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { if (shouldShowExpand && hasText) Padding( padding: - const EdgeInsets.only(bottom: 40), + EdgeInsets.only(bottom: 40.h), child: GestureDetector( onTap: () { // 打开底部弹出编辑模式 @@ -646,21 +650,21 @@ class _AgentViewState extends State with WidgetsBindingObserver { }); }, child: Container( - width: 32, - height: 32, + width: 32.w, + height: 32.w, decoration: BoxDecoration( color: isDarkMode ? Colors.grey[700] : Colors.grey[300], borderRadius: - BorderRadius.circular(16), + BorderRadius.circular(16.w), ), child: Icon( Icons.expand_less, color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - size: 20, + size: 20.sp, ), ), ), @@ -669,17 +673,17 @@ class _AgentViewState extends State with WidgetsBindingObserver { GestureDetector( onTap: () => controller.sendMessage(), child: Container( - width: 44, - height: 44, + width: 44.w, + height: 44.w, decoration: BoxDecoration( color: primaryColor, borderRadius: - BorderRadius.circular(22), + BorderRadius.circular(22.w), ), - child: const Icon( + child: Icon( Icons.send, color: Colors.white, - size: 22, + size: 22.sp, ), ), ), @@ -695,21 +699,21 @@ class _AgentViewState extends State with WidgetsBindingObserver { controller.togglePlusOptions(); }, child: Container( - width: 44, - height: 44, + width: 44.w, + height: 44.w, decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[200], borderRadius: - BorderRadius.circular(22), + BorderRadius.circular(22.w), ), child: Icon( isExpanded ? Icons.close : Icons.add, color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - size: 22, + size: 22.sp, ), ), ); @@ -731,8 +735,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { child: isExpanded ? Container( margin: EdgeInsets.only(top: 8.h), - padding: EdgeInsets.symmetric( - horizontal: 16.w), // 添加左右边距 + padding: + EdgeInsets.symmetric(horizontal: 16.w), child: Row( mainAxisAlignment: MainAxisAlignment .spaceEvenly, // 改为均匀分布 @@ -813,25 +817,24 @@ class _AgentViewState extends State with WidgetsBindingObserver { required String label, required VoidCallback onTap, }) { - final theme = Theme.of(context); - final primaryColor = theme.primaryColor; + final primaryColor = Theme.of(context).primaryColor; return GestureDetector( onTap: onTap, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), decoration: BoxDecoration( color: primaryColor.withOpacity(0.08), - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(16.w), border: Border.all( color: primaryColor.withOpacity(0.2), - width: 1, + width: 1.w, ), ), child: Text( label, style: TextStyle( - fontSize: 13, + fontSize: 13.sp, color: primaryColor, fontWeight: FontWeight.w500, ), @@ -865,10 +868,10 @@ class _AgentViewState extends State with WidgetsBindingObserver { height: 50.w, // 调整为44.w以适应布局 decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[100], - borderRadius: BorderRadius.circular(12.r), // 调整为10.r + borderRadius: BorderRadius.circular(12.w), border: Border.all( color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!, - width: 1, + width: 1.w, ), ), child: Icon( @@ -1085,8 +1088,8 @@ class _AgentViewState extends State with WidgetsBindingObserver { BoxShadow( color: (isActive ? Colors.red : Colors.green) .withOpacity(0.3), - blurRadius: 12.r, - spreadRadius: 2.r, + blurRadius: 12.w, + spreadRadius: 2.w, ), ], ), @@ -1148,23 +1151,23 @@ class _AgentViewState extends State with WidgetsBindingObserver { if (controller.isLoadingMore.value) { // 正在加载状态 return Container( - padding: const EdgeInsets.symmetric(vertical: 16), + padding: EdgeInsets.symmetric(vertical: 16.h), child: Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox( - width: 16, - height: 16, + SizedBox( + width: 16.w, + height: 16.h, child: CircularProgressIndicator( - strokeWidth: 2, + strokeWidth: 2.w, ), ), - const SizedBox(width: 8), + SizedBox(width: 8.w), Text( - 'loadMoreMessages'.tr, // 加载更多消息... - style: const TextStyle( - fontSize: 14, + 'loadMoreMessages'.tr, + style: TextStyle( + fontSize: 14.sp, color: Colors.grey, ), ), @@ -1175,23 +1178,23 @@ class _AgentViewState extends State with WidgetsBindingObserver { } else if (controller.isLoadingMoreError.value) { // 加载失败状态 return Container( - padding: const EdgeInsets.symmetric(vertical: 16), + padding: EdgeInsets.symmetric(vertical: 16.h), child: Center( child: GestureDetector( onTap: () => controller.loadMoreMessages(), child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon( + Icon( Icons.refresh, - size: 16, + size: 16.sp, color: Colors.grey, ), - const SizedBox(width: 8), + SizedBox(width: 8.w), Text( - 'loadFailedClickRetry'.tr, // 加载失败,点击重试 - style: const TextStyle( - fontSize: 14, + 'loadFailedClickRetry'.tr, + style: TextStyle( + fontSize: 14.sp, color: Colors.grey, ), ), @@ -1203,12 +1206,12 @@ class _AgentViewState extends State with WidgetsBindingObserver { } else { // 默认状态(有更多消息但未加载) return Container( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: EdgeInsets.symmetric(vertical: 8.h), child: Center( child: Text( - 'swipeUpForMore'.tr, // 向上滑动查看更多 - style: const TextStyle( - fontSize: 12, + 'swipeUpForMore'.tr, + style: TextStyle( + fontSize: 12.sp, color: Colors.grey, ), ), From fd54772946c325e36083dfe329b4287b1d295fdc Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Wed, 16 Jul 2025 18:11:16 +0800 Subject: [PATCH 3/5] =?UTF-8?q?refactor(agent=5Fview):=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=89=A9=E5=B1=95=E9=80=89=E9=A1=B9=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E7=9A=84=E5=B8=83=E5=B1=80=E5=92=8C=E6=BB=9A=E5=8A=A8=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将Row改为Wrap布局实现按钮自动换行 添加水平滚动确保在小屏幕设备上可用 调整容器高度和边距以改善视觉效果 --- lib/modules/agent/views/agent_view.dart | 124 +++++++++++++----------- 1 file changed, 70 insertions(+), 54 deletions(-) diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index ac8a8d5fc..7e048d2fe 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../controllers/agent_controller.dart'; @@ -731,62 +732,77 @@ class _AgentViewState extends State with WidgetsBindingObserver { return AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, - height: isExpanded ? 120.h : 0, // 调整高度以适应按钮尺寸 + height: isExpanded ? 100.h : 0, // 调整高度以适应按钮尺寸 child: isExpanded - ? Container( - margin: EdgeInsets.only(top: 8.h), - padding: - EdgeInsets.symmetric(horizontal: 16.w), - child: Row( - mainAxisAlignment: MainAxisAlignment - .spaceEvenly, // 改为均匀分布 - children: [ - // 相机按钮 - _buildPlusOptionButton( - context: context, - icon: Icons.camera_alt, - label: 'takePhoto'.tr, // 拍照 - onTap: () async { - //controller.collapsePlusOptions(); - await controller.takePhoto(); - }, - isDarkMode: isDarkMode, - ), - // 相册按钮 - _buildPlusOptionButton( - context: context, - icon: Icons.photo_library, - label: 'photoAlbum'.tr, // 相册 - onTap: () async { - //controller.collapsePlusOptions(); - await controller.pickImage(); - }, - isDarkMode: isDarkMode, - ), - // 文件按钮(预留位置) - // _buildPlusOptionButton( - // context: context, - // icon: Icons.folder, - // label: '文件', - // onTap: () { - // //controller.collapsePlusOptions(); - // // TODO: 实现文件选择功能 - // }, - // isDarkMode: isDarkMode, - // ), - // 通话按钮 - _buildPlusOptionButton( - context: context, - icon: Icons.phone, - label: 'makeCall'.tr, // 打电话 - onTap: () { - // 启动打电话模式 - controller.startPhoneCall(); - }, - isDarkMode: isDarkMode, + ? LayoutBuilder( + // 添加LayoutBuilder获取实际约束 + builder: (context, constraints) { + return SingleChildScrollView( + // 确保内容可滚动 + scrollDirection: + Axis.horizontal, // 水平滚动 + child: Container( + margin: EdgeInsets.only(top: 16.h), + padding: EdgeInsets.symmetric( + horizontal: 16.w), + // 根据可用宽度调整容器宽度 + width: max(constraints.maxWidth, + 300.w), // 最小300宽 + child: Wrap( + // 使用Wrap替代Row,自动换行 + alignment: WrapAlignment.center, + spacing: 26.w, // 按钮水平间距 + runSpacing: 8.h, // 按钮垂直间距 + children: [ + // 相机按钮 + _buildPlusOptionButton( + context: context, + icon: Icons.camera_alt, + label: 'takePhoto'.tr, // 拍照 + onTap: () async { + //controller.collapsePlusOptions(); + await controller.takePhoto(); + }, + isDarkMode: isDarkMode, + ), + // 相册按钮 + _buildPlusOptionButton( + context: context, + icon: Icons.photo_library, + label: 'photoAlbum'.tr, // 相册 + onTap: () async { + //controller.collapsePlusOptions(); + await controller.pickImage(); + }, + isDarkMode: isDarkMode, + ), + // 文件按钮(预留位置) + // _buildPlusOptionButton( + // context: context, + // icon: Icons.folder, + // label: '文件', + // onTap: () { + // //controller.collapsePlusOptions(); + // // TODO: 实现文件选择功能 + // }, + // isDarkMode: isDarkMode, + // ), + // 通话按钮 + _buildPlusOptionButton( + context: context, + icon: Icons.phone, + label: 'makeCall'.tr, // 打电话 + onTap: () { + // 启动打电话模式 + controller.startPhoneCall(); + }, + isDarkMode: isDarkMode, + ), + ], + ), ), - ], - ), + ); + }, ) : const SizedBox.shrink(), ); From 3186494cdf2f236adbced92dd9b600cfd706a517 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Thu, 17 Jul 2025 10:43:21 +0800 Subject: [PATCH 4/5] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=96=B0=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E8=B7=AF=E5=BE=84=E5=92=8C=E5=AF=BC=E5=85=A5=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=EF=BC=8C=E5=88=A0=E9=99=A4=E6=97=A0=E7=94=A8=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将splash路由路径从'/'改为'/splash'以提高可读性 - 修正user_portrait服务中的导入路径 - 删除已不再需要的测试文件 --- lib/data/services/user_portrait.dart | 2 +- lib/routes/app_routes.dart | 2 +- .../user_portrait_method_channel_test.dart | 27 ----------------- .../test/user_portrait_test.dart | 29 ------------------- 4 files changed, 2 insertions(+), 58 deletions(-) delete mode 100644 local_plugins/user_portrait/test/user_portrait_method_channel_test.dart delete mode 100644 local_plugins/user_portrait/test/user_portrait_test.dart diff --git a/lib/data/services/user_portrait.dart b/lib/data/services/user_portrait.dart index 08372b638..d4e521299 100644 --- a/lib/data/services/user_portrait.dart +++ b/lib/data/services/user_portrait.dart @@ -1,4 +1,4 @@ -import 'package:deep_voice/core/utils/logger.dart'; +import '/core/utils/logger.dart'; import 'package:get/get.dart'; import 'package:user_portrait/user_portrait.dart'; diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index e700acb92..3559da01e 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -1,6 +1,6 @@ abstract class Routes { // 认证相关 - static const splash = '/'; + static const splash = '/splash'; static const login = '/login'; static const pairing = '/pairing'; diff --git a/local_plugins/user_portrait/test/user_portrait_method_channel_test.dart b/local_plugins/user_portrait/test/user_portrait_method_channel_test.dart deleted file mode 100644 index 7a2dd7e32..000000000 --- a/local_plugins/user_portrait/test/user_portrait_method_channel_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:user_portrait/user_portrait_method_channel.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - MethodChannelUserPortrait platform = MethodChannelUserPortrait(); - const MethodChannel channel = MethodChannel('user_portrait'); - - setUp(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( - channel, - (MethodCall methodCall) async { - return '42'; - }, - ); - }); - - tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); - }); - - test('getPlatformVersion', () async { - expect(await platform.getPlatformVersion(), '42'); - }); -} diff --git a/local_plugins/user_portrait/test/user_portrait_test.dart b/local_plugins/user_portrait/test/user_portrait_test.dart deleted file mode 100644 index 02c2ecd8a..000000000 --- a/local_plugins/user_portrait/test/user_portrait_test.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:user_portrait/user_portrait.dart'; -import 'package:user_portrait/user_portrait_platform_interface.dart'; -import 'package:user_portrait/user_portrait_method_channel.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -class MockUserPortraitPlatform - with MockPlatformInterfaceMixin - implements UserPortraitPlatform { - - @override - Future getPlatformVersion() => Future.value('42'); -} - -void main() { - final UserPortraitPlatform initialPlatform = UserPortraitPlatform.instance; - - test('$MethodChannelUserPortrait is the default instance', () { - expect(initialPlatform, isInstanceOf()); - }); - - test('getPlatformVersion', () async { - UserPortrait userPortraitPlugin = UserPortrait(); - MockUserPortraitPlatform fakePlatform = MockUserPortraitPlatform(); - UserPortraitPlatform.instance = fakePlatform; - - expect(await userPortraitPlugin.getPlatformVersion(), '42'); - }); -} From 038a67d0776a80c87adb5a24f93f9e45883276b6 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Thu, 17 Jul 2025 14:46:31 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat(explore):=20=E4=BC=98=E5=8C=96AI?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增商务会议助手功能模块,包括控制器逻辑和视图布局 - 重构AI功能区域为2x2网格布局,商务会议助手占据下方整行 - 添加宽版AI功能项组件,支持不同尺寸的展示需求 - 实现商务会议助手点击跳转逻辑 --- .../controllers/explore_controller.dart | 55 ++++ lib/modules/explore/views/explore_view.dart | 302 ++++++++++++++++-- 2 files changed, 327 insertions(+), 30 deletions(-) diff --git a/lib/modules/explore/controllers/explore_controller.dart b/lib/modules/explore/controllers/explore_controller.dart index 067e4b6af..f09d1e1bd 100644 --- a/lib/modules/explore/controllers/explore_controller.dart +++ b/lib/modules/explore/controllers/explore_controller.dart @@ -76,6 +76,17 @@ class ExploreController extends GetxController { targetRoute: Routes.translation, ), // AIFeature( + // title: 'conferenceAssistant'.tr, // 商务会议助手 + // description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译 + // icon: Icons.people, + // startColor: const Color(0xFFab47bc), + // endColor: const Color(0xFFec407a), + // iconBgColor: const Color(0xFFd81b60).withOpacity(0.2), + // iconColor: const Color(0xFFd81b60), + // agentId: 'meeting_assistant', + // targetRoute: Routes.meeting, + // ), + // AIFeature( // title: 'aiGirlfriend'.tr, // 亲子陪伴 // description: 'aiGirlfriendInfo'.tr, // 智能陪伴,随时聊天 // icon: Icons.favorite, @@ -99,6 +110,20 @@ class ExploreController extends GetxController { // ), ]; + final List aiFeaturesWide = [ + AIFeature( + title: 'conferenceAssistant'.tr, // 商务会议助手 + description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译 + icon: Icons.people, + startColor: const Color(0xFFab47bc), + endColor: const Color(0xFFec407a), + iconBgColor: const Color(0xFFd81b60).withOpacity(0.2), + iconColor: const Color(0xFFd81b60), + agentId: 'meeting_assistant', + targetRoute: Routes.meeting, + ), + ]; + // 热门功能场景列表 final List functionScenes = [ FunctionScene( @@ -177,6 +202,36 @@ class ExploreController extends GetxController { Get.toNamed(feature.targetRoute); } else if (feature.targetRoute == Routes.agent) { Get.toNamed(feature.targetRoute); + } else if (feature.targetRoute == Routes.meeting) { + // 跳转到会议页面 + Get.toNamed(feature.targetRoute); + } + } + + // 点击AI功能(宽版本) + void onAIFeatureWideTap(int index) { + if (index < 0 || index >= aiFeaturesWide.length) return; + + final feature = aiFeaturesWide[index]; + + // 直接根据目标路由进行跳转 + if (feature.targetRoute == Routes.meeting) { + Get.toNamed(feature.targetRoute); + } else if (feature.targetRoute == Routes.chat) { + final agent = AgentProvider.getAgentById(feature.agentId); + if (agent != null) { + Get.toNamed(feature.targetRoute, arguments: { + 'agentId': agent.id, + 'agentName': agent.name, + 'agentVoice': agent.voice, + 'agentAvatar': agent.avatarUrl, + 'agentSubtitle': agent.description, + 'systemPrompt': agent.systemPrompt, + 'welcomeMessage': agent.welcomeMessage, + }); + } + } else if (feature.targetRoute.isNotEmpty) { + Get.toNamed(feature.targetRoute); } } diff --git a/lib/modules/explore/views/explore_view.dart b/lib/modules/explore/views/explore_view.dart index 9f5250a69..3f0fee73c 100644 --- a/lib/modules/explore/views/explore_view.dart +++ b/lib/modules/explore/views/explore_view.dart @@ -65,11 +65,11 @@ class ExploreView extends GetView { // AI功能列表 SizedBox(height: 24.h), - _buildAIFeaturesSection(isDarkMode), + _buildCustomAILayout(isDarkMode), // 热门功能场景 SizedBox(height: 24.h), - _buildFunctionScenesSection(isDarkMode), + //_buildFunctionScenesSection(isDarkMode), // 新功能推荐 SizedBox(height: 24.h), @@ -250,8 +250,23 @@ class ExploreView extends GetView { ); } - // AI功能部分 - Widget _buildAIFeaturesSection(bool isDarkMode) { + // 自定义AI功能布局:2x2网格,商务会议助手占据下方两个网格位置 + Widget _buildCustomAILayout(bool isDarkMode) { + // 找到AI全能助手 + final aiAssistantIndex = controller.aiFeatures.indexWhere( + (feature) => feature.agentId == 'personal_assistant', + ); + + // 找到同声传译 + final translationIndex = controller.aiFeatures.indexWhere( + (feature) => feature.agentId == 'simultaneous_interpreter', + ); + + // 找到商务会议助手(在aiFeaturesWide中) + final meetingFeature = controller.aiFeaturesWide.isNotEmpty + ? controller.aiFeaturesWide.first + : null; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -264,20 +279,56 @@ class ExploreView extends GetView { ), ), SizedBox(height: 16.h), - GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 16.w, - mainAxisSpacing: 16.h, - childAspectRatio: 1.0, - ), - itemCount: controller.aiFeatures.length, - itemBuilder: (context, index) { - final feature = controller.aiFeatures[index]; - return _buildAIFeatureItem(feature, index, isDarkMode); - }, + + // 2x2网格布局 + Column( + children: [ + // 上层:AI全能助手(左)和同声传译(右) + Row( + children: [ + // 左上角:AI全能助手 + if (aiAssistantIndex != -1) + Expanded( + child: AspectRatio( + aspectRatio: 1.0, // 正方形 + child: _buildAIFeatureItem( + controller.aiFeatures[aiAssistantIndex], + aiAssistantIndex, + isDarkMode, + ), + ), + ), + + SizedBox(width: 16.w), + + // 右上角:同声传译 + if (translationIndex != -1) + Expanded( + child: AspectRatio( + aspectRatio: 1.0, // 正方形 + child: _buildAIFeatureItem( + controller.aiFeatures[translationIndex], + translationIndex, + isDarkMode, + ), + ), + ), + ], + ), + + SizedBox(height: 16.h), + + // 下层:商务会议助手(横跨整行) + if (meetingFeature != null) + AspectRatio( + aspectRatio: 2.0, // 宽高比2:1,横跨两个网格位置 + child: _buildAIFeatureItemWideForMeeting( + meetingFeature, + 0, // 使用固定索引,因为这是aiFeaturesWide中的项目 + isDarkMode, + ), + ), + ], ), ], ); @@ -366,20 +417,211 @@ class ExploreView extends GetView { ); } +//整行布局 + Widget _buildAIFeaturesWideSection(bool isDarkMode) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Text( + // 'aiFunction'.tr, // AI功能 + // style: TextStyle( + // fontSize: 18.sp, + // fontWeight: FontWeight.w600, + // color: isDarkMode ? Colors.white : Colors.grey[800], + // ), + // ), + // SizedBox(height: 16.h), + // 直接使用 Column 包裹宽版卡片 + Column( + children: controller.aiFeaturesWide.map((feature) { + return Padding( + padding: EdgeInsets.only(bottom: 16.h), // 卡片间距 + child: _buildAIFeatureItemWide( + feature, + controller.aiFeatures.indexOf(feature), + isDarkMode, + ), + ); + }).toList(), + ), + ], + ); +} + + // AI功能项(宽版本,用于横跨整行的AI全能助手) + Widget _buildAIFeatureItemWide(AIFeature feature, int index, bool isDarkMode) { + return GestureDetector( + onTap: () => controller.onAIFeatureTap(index), + child: Container( + padding: EdgeInsets.all(20.w), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + feature.startColor.withOpacity(isDarkMode ? 0.3 : 0.3), + feature.endColor.withOpacity(isDarkMode ? 0.3 : 0.3), + ], + ), + borderRadius: BorderRadius.circular(20.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.grey.withOpacity(0.1), + blurRadius: 8.w, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + // 图标圆圈 + Container( + width: 48.w, + height: 48.w, + decoration: BoxDecoration( + color: feature.iconColor.withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + child: Icon( + feature.icon, + size: 24.sp, + color: feature.iconColor, + ), + ), + SizedBox(width: 16.w), + // 文本内容 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 标题 + Text( + feature.title, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.grey[800], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 4.h), + // 描述 + Text( + feature.description, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ); + } + + // AI功能项(宽版本,专门用于商务会议助手) + Widget _buildAIFeatureItemWideForMeeting(AIFeature feature, int index, bool isDarkMode) { + return GestureDetector( + onTap: () => controller.onAIFeatureWideTap(index), + child: Container( + padding: EdgeInsets.all(20.w), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + feature.startColor.withValues(alpha: isDarkMode ? 0.3 : 0.3), + feature.endColor.withValues(alpha: isDarkMode ? 0.3 : 0.3), + ], + ), + borderRadius: BorderRadius.circular(20.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + // 图标圆圈 + Container( + width: 48.w, + height: 48.w, + decoration: BoxDecoration( + color: feature.iconColor.withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + child: Icon( + feature.icon, + size: 24.sp, + color: feature.iconColor, + ), + ), + SizedBox(width: 16.w), + // 文本内容 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 标题 + Text( + feature.title, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.w600, + color: isDarkMode ? Colors.white : Colors.grey[800], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 4.h), + // 描述 + Text( + feature.description, + style: TextStyle( + fontSize: 14.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + ); + } + // 热门功能场景部分 Widget _buildFunctionScenesSection(bool isDarkMode) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'popularFunction'.tr, // 热门功能场景 - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.grey[800], - ), - ), - SizedBox(height: 16.h), + // Text( + // 'popularFunction'.tr, // 热门功能场景 + // style: TextStyle( + // fontSize: 18.sp, + // fontWeight: FontWeight.w600, + // color: isDarkMode ? Colors.white : Colors.grey[800], + // ), + // ), + // SizedBox(height: 16.h), ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -404,13 +646,13 @@ class ExploreView extends GetView { child: Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.grey[100], + color: isDarkMode ? Colors.white.withValues(alpha: 0.05) : Colors.grey[100], borderRadius: BorderRadius.circular(16.r), boxShadow: [ BoxShadow( color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.grey.withOpacity(0.1), + ? Colors.black.withValues(alpha: 0.2) + : Colors.grey.withValues(alpha: 0.1), blurRadius: 4, offset: const Offset(0, 2), ),