From f7025b9a446c4f784c4ec44013e99d347af936d7 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 27 Jun 2025 16:46:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(agent):=20=E6=B7=BB=E5=8A=A0=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E9=80=89=E9=A1=B9=E5=B1=95=E5=BC=80=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=92=8C=E9=94=AE=E7=9B=98=E5=8F=AF=E8=A7=81=E6=80=A7=E6=8E=A7?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增图片选择选项的展开/收缩功能,并添加键盘可见性状态管理 当键盘显示时自动收缩图片选项,避免界面重叠 --- .gitignore | 1 + .../agent/controllers/agent_controller.dart | 58 ++ lib/modules/agent/views/agent_view.dart | 753 +++++++++++------- 3 files changed, 509 insertions(+), 303 deletions(-) diff --git a/.gitignore b/.gitignore index b4a242c24..63e1880cc 100644 --- a/.gitignore +++ b/.gitignore @@ -222,3 +222,4 @@ fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output fastlane/readme.md +.trae/rules/project_rules.md diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index c8729d062..3648b0639 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -75,6 +75,12 @@ class AgentController extends GetxController { final isFullScreenEditMode = false.obs; // 是否处于全屏编辑模式 final shouldShowExpandButton = false.obs; // 是否显示展开按钮 + // 图片选择选项展开状态 + final isImageOptionsExpanded = false.obs; // 是否展开图片选择选项 + + // 键盘状态 + final isKeyboardVisible = false.obs; // 键盘是否可见 + // 流事件订阅 StreamSubscription? _eventSubscription; @@ -898,6 +904,58 @@ class AgentController extends GetxController { isImageInputActive.value = false; } + // 切换图片选项展开状态 + void toggleImageOptions() { + if (isImageOptionsExpanded.value) { + // 如果当前已展开,则收缩 + collapseImageOptions(); + } else { + // 如果当前未展开,则展开并关闭键盘 + if (isKeyboardVisible.value) { + hideKeyboard(); + // 延迟展开选项,等待键盘完全关闭 + Future.delayed(const Duration(milliseconds: 200), () { + // 再次检查键盘状态,确保键盘已关闭 + if (!isKeyboardVisible.value) { + isImageOptionsExpanded.value = true; + } else { + // 如果键盘还没关闭,再等一会儿 + Future.delayed(const Duration(milliseconds: 100), () { + if (!isKeyboardVisible.value) { + isImageOptionsExpanded.value = true; + } + }); + } + }); + } else { + // 键盘未显示,直接展开 + isImageOptionsExpanded.value = true; + } + } + } + + // 收缩图片选项 + void collapseImageOptions() { + isImageOptionsExpanded.value = false; + } + + // 更新键盘可见状态 + void updateKeyboardVisibility(bool isVisible) { + final wasVisible = isKeyboardVisible.value; + isKeyboardVisible.value = isVisible; + + // 只有当键盘从隐藏变为显示时,才收缩图片选项 + // 避免键盘关闭时误触发收缩逻辑 + if (!wasVisible && isVisible && isImageOptionsExpanded.value) { + collapseImageOptions(); + } + } + + // 隐藏键盘 + void hideKeyboard() { + FocusManager.instance.primaryFocus?.unfocus(); + } + // 使用预设提示词处理图片 void useImagePrompt(String prompt) { // 确保selectedImagePath.value不为null diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index e22175383..a754a1cca 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -11,12 +11,45 @@ import '../../../core/theme/app_colors.dart'; import 'package:haptic_feedback/haptic_feedback.dart'; import '../../../data/models/message_model.dart'; -class AgentView extends StatelessWidget { +class AgentView extends StatefulWidget { const AgentView({Key? key}) : super(key: key); + @override + State createState() => _AgentViewState(); +} + +class _AgentViewState extends State with WidgetsBindingObserver { + late AgentController controller; + double _keyboardHeight = 0; + + @override + void initState() { + super.initState(); + controller = Get.find(); + WidgetsBinding.instance.addObserver(this); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeMetrics() { + super.didChangeMetrics(); + final bottomInset = View.of(context).viewInsets.bottom; + final newKeyboardHeight = bottomInset / View.of(context).devicePixelRatio; + + if (_keyboardHeight != newKeyboardHeight) { + _keyboardHeight = newKeyboardHeight; + // 更新键盘可见状态 + controller.updateKeyboardVisibility(_keyboardHeight > 0); + } + } + @override Widget build(BuildContext context) { - final controller = Get.find(); final theme = Theme.of(context); final primaryColor = theme.primaryColor; final backgroundColor = theme.scaffoldBackgroundColor; @@ -288,333 +321,391 @@ class AgentView extends StatelessWidget { } }), - // 输入控件行 - 使用IntrinsicHeight确保按钮与输入框底部对齐 - IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, // 所有元素底部对齐 - children: [ - // 键盘/语音切换按钮 - Obx(() { - final isTextMode = controller.isTextInputMode.value; - return GestureDetector( - onTap: () { - controller.toggleInputMode(); - }, - child: Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - borderRadius: BorderRadius.circular(22), - ), - child: Icon( - isTextMode ? Icons.mic_none : Icons.keyboard, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - size: 22, - ), - ), - ); - }), - - const SizedBox(width: 8), - - // 文本输入框或语音波形图 - Expanded( - child: Obx(() { - final isTextMode = controller.isTextInputMode.value; - - if (isTextMode) { - // 文本输入模式 - 限制最大高度并只向上扩展 - return Container( - constraints: const BoxConstraints( - minHeight: 44, // 最小高度 - maxHeight: 120, // 最大高度限制,防止挤压按钮 - ), - padding: - const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - borderRadius: BorderRadius.circular(22), - border: Border.all( - color: isDarkMode - ? Colors.grey[ - 800]! // 修改: 从 Colors.grey[700] 改为 Colors.grey[800] - : Colors.grey.withOpacity(0.1), - width: 1.0, - ), - ), - child: Row( - crossAxisAlignment: - CrossAxisAlignment.end, // 对齐到底部,实现向上扩展 - children: [ - Expanded( - child: Obx(() { - // 图片模式下使用更高的输入框 - final isImageActive = - controller.isImageInputActive.value; - - return TextField( - controller: controller.textController, - maxLines: - isImageActive ? 3 : 5, // 设置合理的最大行数 - minLines: isImageActive ? 2 : 1, - keyboardType: - TextInputType.multiline, // 启用多行键盘 - textInputAction: - TextInputAction.newline, // 支持回车换行 - style: TextStyle( - fontSize: 15, - color: isDarkMode - ? Colors.white - : Colors.grey[800], - ), - decoration: InputDecoration( - hintText: isImageActive - ? '添加对图片的问题或描述...' - : '输入消息...', - hintStyle: TextStyle( - color: Colors.grey[500], - fontSize: 15, - ), - filled: false, - border: InputBorder.none, - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: isDarkMode - ? Colors.grey[800]! - : Colors.grey[200]!, - width: 1.0, - ), - ), - contentPadding: EdgeInsets.symmetric( - vertical: isImageActive - ? 12 - : 8), // 调整垂直内边距 - ), - onSubmitted: (_) => - controller.sendMessage(), - ); - }), - ), - ], - ), - ); - } else { - // 语音输入模式 - 始终显示波形图 + // 输入控件区域 - 包含输入行和展开的选项 + Column( + children: [ + // 输入控件行 - 使用IntrinsicHeight确保按钮与输入框底部对齐 + IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, // 所有元素底部对齐 + children: [ + // 键盘/语音切换按钮 + Obx(() { + final isTextMode = controller.isTextInputMode.value; return GestureDetector( onTap: () { - if (controller.isListening.value) { - controller.stopVoiceInput(); - } else { - controller.startVoiceInput(); - } + controller.toggleInputMode(); }, child: Container( - height: 50, - padding: - const EdgeInsets.symmetric(horizontal: 16), + width: 44, + height: 44, decoration: BoxDecoration( color: isDarkMode ? Colors.grey[800] : Colors.grey[200], borderRadius: BorderRadius.circular(22), - border: Border.all( - color: isDarkMode - ? Colors.grey[800]! - : Colors.grey.withOpacity(0.1), - width: 1.0, - ), ), - child: Row( - children: [ - // 语音波形图 - 始终显示 - Expanded( - child: Row( - mainAxisAlignment: - MainAxisAlignment.start, - children: [ - ...List.generate(10, (index) { - return _buildSoundBar(index); - }), - const Spacer(), - // "正在聆听"文字 - Obx(() { - final isListening = - controller.isListening.value; - return Row( - children: [ - Icon( - Icons.mic, - size: 14, - color: isListening - ? primaryColor - : Colors.grey[500], - ), - const SizedBox(width: 4), - Text( - '正在聆听', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: isListening - ? primaryColor - : Colors.grey[500], - ), - ), - ], - ); - }), - ], - ), - ), - ], + child: Icon( + isTextMode ? Icons.mic_none : Icons.keyboard, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 22, ), ), ); - } - }), - ), + }), - const SizedBox(width: 8), + const SizedBox(width: 8), - // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 - Obx(() { - final hasText = controller.hasTextInput.value; - final hasImage = controller.isImageInputActive.value; - final shouldShowSend = hasText || hasImage; - final shouldShowExpand = - controller.shouldShowExpandButton.value; + // 文本输入框或语音波形图 + Expanded( + child: Obx(() { + final isTextMode = + controller.isTextInputMode.value; - if (shouldShowSend) { - // 显示发送按钮(可能还有展开按钮) - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 展开按钮(在发送按钮上方) - if (shouldShowExpand && hasText) - Padding( - padding: const EdgeInsets.only(bottom: 40), - child: GestureDetector( - onTap: () { - // 打开底部弹出编辑模式 - controller.openFullScreenEdit(); - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => - const FullScreenEditor(), - ).then((_) { - // 弹出页关闭时的回调 - controller.closeFullScreenEdit(); - }); - }, - child: Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[700] - : Colors.grey[300], - borderRadius: BorderRadius.circular(16), - ), - child: Icon( - Icons.expand_less, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - size: 20, - ), - ), + if (isTextMode) { + // 文本输入模式 - 限制最大高度并只向上扩展 + return Container( + constraints: const BoxConstraints( + minHeight: 44, // 最小高度 + maxHeight: 120, // 最大高度限制,防止挤压按钮 ), - ), - // 发送按钮 - GestureDetector( - onTap: () => controller.sendMessage(), - child: Container( - width: 44, - height: 44, + padding: const EdgeInsets.symmetric( + horizontal: 16), decoration: BoxDecoration( - color: primaryColor, + color: isDarkMode + ? Colors.grey[800] + : Colors.grey[200], borderRadius: BorderRadius.circular(22), + border: Border.all( + color: isDarkMode + ? Colors.grey[ + 800]! // 修改: 从 Colors.grey[700] 改为 Colors.grey[800] + : Colors.grey.withOpacity(0.1), + width: 1.0, + ), ), - child: const Icon( - Icons.send, - color: Colors.white, - size: 22, - ), - ), - ), - ], - ); - } else { - // 显示图片选择按钮 - return Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - borderRadius: BorderRadius.circular(22), - ), - child: PopupMenuButton( - icon: Icon( - Icons.add_photo_alternate_outlined, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - size: 22, - ), - padding: EdgeInsets.zero, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - onSelected: (value) async { - if (value == 'camera') { - await controller.takePhoto(); - } else if (value == 'gallery') { - await controller.pickImage(); - } - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: 'camera', child: Row( + crossAxisAlignment: + CrossAxisAlignment.end, // 对齐到底部,实现向上扩展 children: [ - Icon(Icons.camera_alt, - color: primaryColor, size: 20), - const SizedBox(width: 8), - const Text('拍照', - style: TextStyle(fontSize: 14)), + Expanded( + child: Obx(() { + // 图片模式下使用更高的输入框 + final isImageActive = controller + .isImageInputActive.value; + + return TextField( + controller: + controller.textController, + maxLines: isImageActive + ? 3 + : 5, // 设置合理的最大行数 + minLines: isImageActive ? 2 : 1, + keyboardType: TextInputType + .multiline, // 启用多行键盘 + textInputAction: TextInputAction + .newline, // 支持回车换行 + onTap: () { + // 当点击输入框时,如果图片选项已展开,则收缩 + if (controller + .isImageOptionsExpanded + .value) { + controller + .collapseImageOptions(); + } + }, + style: TextStyle( + fontSize: 15, + color: isDarkMode + ? Colors.white + : Colors.grey[800], + ), + decoration: InputDecoration( + hintText: isImageActive + ? '添加对图片的问题或描述...' + : '输入消息...', + hintStyle: TextStyle( + color: Colors.grey[500], + fontSize: 15, + ), + filled: false, + border: InputBorder.none, + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: isDarkMode + ? Colors.grey[800]! + : Colors.grey[200]!, + width: 1.0, + ), + ), + contentPadding: + EdgeInsets.symmetric( + vertical: isImageActive + ? 12 + : 8), // 调整垂直内边距 + ), + onSubmitted: (_) => + controller.sendMessage(), + ); + }), + ), ], ), - ), - PopupMenuItem( - value: 'gallery', - child: Row( - children: [ - Icon(Icons.photo_library, - color: primaryColor, size: 20), - const SizedBox(width: 8), - const Text('从相册选择', - style: TextStyle(fontSize: 14)), - ], + ); + } else { + // 语音输入模式 - 始终显示波形图 + return GestureDetector( + onTap: () { + if (controller.isListening.value) { + controller.stopVoiceInput(); + } else { + controller.startVoiceInput(); + } + }, + child: Container( + height: 50, + padding: const EdgeInsets.symmetric( + horizontal: 16), + decoration: BoxDecoration( + color: isDarkMode + ? Colors.grey[800] + : Colors.grey[200], + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: isDarkMode + ? Colors.grey[800]! + : Colors.grey.withOpacity(0.1), + width: 1.0, + ), + ), + child: Row( + children: [ + // 语音波形图 - 始终显示 + Expanded( + child: Row( + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + ...List.generate(10, (index) { + return _buildSoundBar(index); + }), + const Spacer(), + // "正在聆听"文字 + Obx(() { + final isListening = controller + .isListening.value; + return Row( + children: [ + Icon( + Icons.mic, + size: 14, + color: isListening + ? primaryColor + : Colors.grey[500], + ), + const SizedBox(width: 4), + Text( + '正在聆听', + style: TextStyle( + fontSize: 13, + fontWeight: + FontWeight.w500, + color: isListening + ? primaryColor + : Colors.grey[500], + ), + ), + ], + ); + }), + ], + ), + ), + ], + ), ), + ); + } + }), + ), + + const SizedBox(width: 8), + + // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 + Obx(() { + final hasText = controller.hasTextInput.value; + final hasImage = + controller.isImageInputActive.value; + final shouldShowSend = hasText || hasImage; + final shouldShowExpand = + controller.shouldShowExpandButton.value; + + if (shouldShowSend) { + // 显示发送按钮(可能还有展开按钮) + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 展开按钮(在发送按钮上方) + if (shouldShowExpand && hasText) + Padding( + padding: + const EdgeInsets.only(bottom: 40), + child: GestureDetector( + onTap: () { + // 打开底部弹出编辑模式 + controller.openFullScreenEdit(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => + const FullScreenEditor(), + ).then((_) { + // 弹出页关闭时的回调 + controller.closeFullScreenEdit(); + }); + }, + child: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.grey[700] + : Colors.grey[300], + borderRadius: + BorderRadius.circular(16), + ), + child: Icon( + Icons.expand_less, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 20, + ), + ), + ), + ), + // 发送按钮 + GestureDetector( + onTap: () => controller.sendMessage(), + child: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: primaryColor, + borderRadius: BorderRadius.circular(22), + ), + child: const Icon( + Icons.send, + color: Colors.white, + size: 22, + ), + ), + ), + ], + ); + } else { + // 显示'+'选择按钮 + return Obx(() { + final isExpanded = + controller.isImageOptionsExpanded.value; + return GestureDetector( + onTap: () { + controller.toggleImageOptions(); + }, + child: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.grey[800] + : Colors.grey[200], + borderRadius: BorderRadius.circular(22), + ), + child: Icon( + isExpanded ? Icons.close : Icons.add, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 22, + ), + ), + ); + }); + } + }), + ], + ), + ), + + // 展开的'+'选择选项 + Obx(() { + final isExpanded = + controller.isImageOptionsExpanded.value; + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + 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: '拍照', + onTap: () async { + //controller.collapseImageOptions(); + await controller.takePhoto(); + }, + isDarkMode: isDarkMode, + ), + // 相册按钮 + _buildPlusOptionButton( + context: context, + icon: Icons.photo_library, + label: '相册', + onTap: () async { + //controller.collapseImageOptions(); + await controller.pickImage(); + }, + isDarkMode: isDarkMode, + ), + // 文件按钮(预留位置) + // _buildPlusOptionButton( + // context: context, + // icon: Icons.folder, + // label: '文件', + // onTap: () { + // //controller.collapseImageOptions(); + // // TODO: 实现文件选择功能 + // }, + // isDarkMode: isDarkMode, + // ), + // 通话按钮(预留位置) + _buildPlusOptionButton( + context: context, + icon: Icons.phone, + label: '打电话', + onTap: () { + //controller.collapseImageOptions(); + // TODO: 实现通话功能 + }, + isDarkMode: isDarkMode, + ), + ], ), - ], - ), - ); - } - }), - ], - ), + ) + : const SizedBox.shrink(), + ); + }), + ], ), ], ), @@ -792,6 +883,62 @@ class AgentView extends StatelessWidget { )), ); } + + // 构建"+"号展开选项按钮 + Widget _buildPlusOptionButton({ + required BuildContext context, + required IconData icon, + required String label, + required VoidCallback onTap, + required bool isDarkMode, + }) { + final primaryColor = Theme.of(context).primaryColor; + + return GestureDetector( + onTap: onTap, + child: SizedBox( + // 调整为一行四个按钮的宽度,考虑屏幕宽度和间距 + width: (1.sw - 100.w) / 4, // 屏幕宽度减去左右边距,除以4个按钮 + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 正方形图标容器 - 增大尺寸 + Container( + width: 50.w, // 调整为44.w以适应布局 + height: 50.w, // 调整为44.w以适应布局 + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.grey[100], + borderRadius: BorderRadius.circular(12.r), // 调整为10.r + border: Border.all( + color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!, + width: 1, + ), + ), + child: Icon( + icon, + color: primaryColor, + size: 20.sp, // 调整为18.sp + ), + ), + SizedBox(height: 4.h), // 调整为3.h + // 文字标签 + Text( + label, + style: TextStyle( + fontSize: 10.sp, // 调整为9.sp + color: isDarkMode ? Colors.grey[300] : Colors.grey[700], + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + } } class _ReportForm extends StatelessWidget {