diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index b4e58d19d..c8729d062 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -71,6 +71,10 @@ class AgentController extends GetxController { // 输入状态控制 - 用于动态显示发送按钮 final hasTextInput = false.obs; // 是否有文本输入 + // 全屏编辑相关状态 + final isFullScreenEditMode = false.obs; // 是否处于全屏编辑模式 + final shouldShowExpandButton = false.obs; // 是否显示展开按钮 + // 流事件订阅 StreamSubscription? _eventSubscription; @@ -187,9 +191,47 @@ class AgentController extends GetxController { textController.addListener(() { final text = textController.text.trim(); hasTextInput.value = text.isNotEmpty; + + // 检查是否需要显示展开按钮 + _checkShouldShowExpandButton(); }); } + // 检查是否需要显示展开按钮 + void _checkShouldShowExpandButton() { + final text = textController.text; + if (text.isEmpty) { + shouldShowExpandButton.value = false; + return; + } + + // 计算文本行数 + final lines = text.split('\n'); + final maxLines = isImageInputActive.value ? 3 : 5; + + // 简单估算:如果换行符数量超过maxLines-1,或者单行文本过长,显示展开按钮 + final hasMultipleLines = lines.length > maxLines; + final hasLongLine = lines.any((line) => line.length > 50); // 估算单行字符数 + + shouldShowExpandButton.value = hasMultipleLines || hasLongLine; + } + + // 打开全屏编辑模式 + void openFullScreenEdit() { + isFullScreenEditMode.value = true; + } + + // 关闭全屏编辑模式 + void closeFullScreenEdit() { + isFullScreenEditMode.value = false; + } + + // 从全屏编辑模式发送消息 + Future sendMessageFromFullScreen() async { + await sendMessage(); + closeFullScreenEdit(); + } + // 加载聊天历史记录 Future _loadChatHistory() async { try { diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index 5df5a3374..e22175383 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -4,6 +4,7 @@ import 'package:get/get.dart'; import '../controllers/agent_controller.dart'; import '../controllers/report_controller.dart'; import 'message_bubble.dart'; +import '../widgets/full_screen_editor.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../../core/theme/app_colors.dart'; @@ -287,124 +288,52 @@ class AgentView extends StatelessWidget { } }), - // 输入控件行 - Row( - 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(() { + // 输入控件行 - 使用IntrinsicHeight确保按钮与输入框底部对齐 + IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, // 所有元素底部对齐 + children: [ + // 键盘/语音切换按钮 + Obx(() { final isTextMode = controller.isTextInputMode.value; - - if (isTextMode) { - // 文本输入模式 - 注意观察图片模式时的高度变化 - return Container( - //height: 44, - padding: const EdgeInsets.symmetric(horizontal: 16), + 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), - border: Border.all( - color: isDarkMode - ? Colors.grey[ - 800]! // 修改: 从 Colors.grey[700] 改为 Colors.grey[800] - : Colors.grey.withOpacity(0.1), - width: 1.0, - ), ), - child: Row( - children: [ - Expanded( - child: Obx(() { - // 图片模式下使用更高的输入框 - final isImageActive = - controller.isImageInputActive.value; - - return TextField( - controller: controller.textController, - maxLines: isImageActive ? 2 : null, - 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 : 0), - ), - onSubmitted: (_) => - controller.sendMessage(), - ); - }), - ), - ], + child: Icon( + isTextMode ? Icons.mic_none : Icons.keyboard, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 22, ), - ); - } else { - // 语音输入模式 - 始终显示波形图 - return GestureDetector( - onTap: () { - if (controller.isListening.value) { - controller.stopVoiceInput(); - } else { - controller.startVoiceInput(); - } - }, - child: Container( - height: 50, + ), + ); + }), + + 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( @@ -414,145 +343,278 @@ class AgentView extends StatelessWidget { borderRadius: BorderRadius.circular(22), border: Border.all( color: isDarkMode - ? Colors.grey[800]! + ? 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: 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, + 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 { + // 语音输入模式 - 始终显示波形图 + 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), + const SizedBox(width: 8), - // 动态按钮:图片选择按钮 或 发送按钮 - Obx(() { - final hasText = controller.hasTextInput.value; - final hasImage = controller.isImageInputActive.value; - final shouldShowSend = hasText || hasImage; + // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 + Obx(() { + final hasText = controller.hasTextInput.value; + final hasImage = controller.isImageInputActive.value; + final shouldShowSend = hasText || hasImage; + final shouldShowExpand = + controller.shouldShowExpandButton.value; - if (shouldShowSend) { - // 显示发送按钮 - return GestureDetector( - onTap: () => controller.sendMessage(), - child: Container( + 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 Container( width: 44, height: 44, decoration: BoxDecoration( - color: primaryColor, + color: isDarkMode + ? Colors.grey[800] + : Colors.grey[200], borderRadius: BorderRadius.circular(22), ), - 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( - children: [ - Icon(Icons.camera_alt, - color: primaryColor, size: 20), - const SizedBox(width: 8), - const Text('拍照', - style: TextStyle(fontSize: 14)), - ], - ), + child: PopupMenuButton( + icon: Icon( + Icons.add_photo_alternate_outlined, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 22, ), - PopupMenuItem( - value: 'gallery', - child: Row( - children: [ - Icon(Icons.photo_library, - color: primaryColor, size: 20), - const SizedBox(width: 8), - const Text('从相册选择', - style: TextStyle(fontSize: 14)), - ], - ), + 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( + children: [ + Icon(Icons.camera_alt, + color: primaryColor, size: 20), + const SizedBox(width: 8), + const Text('拍照', + style: TextStyle(fontSize: 14)), + ], + ), + ), + PopupMenuItem( + value: 'gallery', + child: Row( + children: [ + Icon(Icons.photo_library, + color: primaryColor, size: 20), + const SizedBox(width: 8), + const Text('从相册选择', + style: TextStyle(fontSize: 14)), + ], + ), + ), + ], + ), + ); + } + }), + ], + ), ), ], ), diff --git a/lib/modules/agent/widgets/full_screen_editor.dart b/lib/modules/agent/widgets/full_screen_editor.dart new file mode 100644 index 000000000..902067c54 --- /dev/null +++ b/lib/modules/agent/widgets/full_screen_editor.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import '../controllers/agent_controller.dart'; + +/// 底部弹出式文本编辑器组件 +class FullScreenEditor extends StatefulWidget { + const FullScreenEditor({super.key}); + + @override + State createState() => _FullScreenEditorState(); +} + +class _FullScreenEditorState extends State { + late TextEditingController _textController; + late FocusNode _focusNode; + final AgentController controller = AgentController.to; + + @override + void initState() { + super.initState(); + // 使用与主界面相同的文本控制器,保持内容同步 + _textController = controller.textController; + _focusNode = FocusNode(); + + // 延迟聚焦,确保界面完全加载后再聚焦 + WidgetsBinding.instance.addPostFrameCallback((_) { + _focusNode.requestFocus(); + }); + } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + final primaryColor = Theme.of(context).primaryColor; + final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; // 获取键盘高度 + + return Container( + height: MediaQuery.of(context).size.height * 0.95, // 占屏幕高度的90% + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20.r), + topRight: Radius.circular(20.r), + ), + ), + child: Stack( + children: [ + // 主要内容区域 + Column( + children: [ + // 主要编辑区域 + Expanded( + child: Container( + padding: EdgeInsets.fromLTRB(16.w, 60.w, 16.w, 16.w), + child: TextField( + controller: _textController, + focusNode: _focusNode, + maxLines: null, + expands: true, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.newline, + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.white : Colors.black87, + height: 1.5, + ), + decoration: InputDecoration( + hintText: '在这里输入你的消息...', + hintStyle: TextStyle( + color: isDarkMode ? Colors.white54 : Colors.grey[500], + fontSize: 16.sp, + ), + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + ), + ), + ), + ), + + // 底部操作区域 + Container( + margin: + EdgeInsets.only(bottom: keyboardHeight), // 关键修改:添加键盘高度的边距 + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.grey[50], + border: Border( + top: BorderSide( + color: isDarkMode ? Colors.grey[700]! : Colors.grey[200]!, + width: 1, + ), + ), + ), + child: Row( + children: [ + // 字符计数 + Expanded( + child: ValueListenableBuilder( + valueListenable: _textController, + builder: (context, value, child) { + return Text( + '${value.text.length} 字符', + style: TextStyle( + color: isDarkMode + ? Colors.white60 + : Colors.grey[600], + fontSize: 14.sp, + ), + ); + }, + ), + ), + + // 发送按钮 - 保持与主界面一致的样式 + Obx(() { + final hasText = controller.hasTextInput.value; + return GestureDetector( + onTap: hasText + ? () async { + await controller.sendMessageFromFullScreen(); + if (context.mounted) { + Navigator.of(context).pop(); + } + } + : null, + child: Container( + width: 44.w, + height: 44.h, + decoration: BoxDecoration( + color: hasText + ? primaryColor + : (isDarkMode + ? Colors.grey[800] + : Colors.grey[300]), + borderRadius: BorderRadius.circular(22.r), + ), + child: Icon( + Icons.send, + color: hasText + ? Colors.white + : (isDarkMode + ? Colors.grey[600] + : Colors.grey[500]), + size: 22.sp, + ), + ), + ); + }), + ], + ), + ), + ], + ), + + // 右上角关闭按钮 + Positioned( + top: 16.h, + right: 16.w, + child: GestureDetector( + onTap: () { + controller.closeFullScreenEdit(); + Navigator.of(context).pop(); + }, + child: Container( + width: 32.w, + height: 32.h, + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.grey[200], + borderRadius: BorderRadius.circular(16.r), + ), + child: Icon( + Icons.close, + color: isDarkMode ? Colors.white : Colors.black87, + size: 20.sp, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/profile/views/profile_view.dart b/lib/modules/profile/views/profile_view.dart index 76ed99bea..41666b35c 100644 --- a/lib/modules/profile/views/profile_view.dart +++ b/lib/modules/profile/views/profile_view.dart @@ -327,7 +327,7 @@ class EditProfileView extends GetView { Row( children: [ Obx(() => CircleAvatar( - radius: 30.r, // 调整头像大小以适应行高 + radius: 26.r, // 调整头像大小以适应行高 backgroundImage: controller.localAvatar.value.isNotEmpty ? FileImage(File(controller.localAvatar.value)) : (controller.userInfo.value.avatar.isNotEmpty @@ -444,7 +444,7 @@ class EditProfileView extends GetView { borderRadius: BorderRadius.circular(16.r), ), child: Icon( - Icons.edit, + Icons.chevron_right, size: 16.w, color: isDarkMode ? Colors.white70 : Colors.grey[500], ), @@ -504,7 +504,7 @@ class EditProfileView extends GetView { child: TextField( controller: nameController, autofocus: true, - textAlign: TextAlign.right, // 右对齐,与原昵称显示保持一致 + textAlign: TextAlign.center, // 右对齐,与原昵称显示保持一致 style: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.white70 : Colors.grey[600],