Browse Source

feat(agent): 添加全屏编辑模式支持长文本输入

新增全屏文本编辑器组件,当输入内容过长时自动显示展开按钮
优化输入框布局和高度限制,添加字符计数功能
weicu
lxm 1 year ago
parent
commit
c0a2546309
  1. 42
      lib/modules/agent/controllers/agent_controller.dart
  2. 88
      lib/modules/agent/views/agent_view.dart
  3. 190
      lib/modules/agent/widgets/full_screen_editor.dart
  4. 6
      lib/modules/profile/views/profile_view.dart

42
lib/modules/agent/controllers/agent_controller.dart

@ -71,6 +71,10 @@ class AgentController extends GetxController {
// 输入状态控制 - 用于动态显示发送按钮 // 输入状态控制 - 用于动态显示发送按钮
final hasTextInput = false.obs; // 是否有文本输入 final hasTextInput = false.obs; // 是否有文本输入
// 全屏编辑相关状态
final isFullScreenEditMode = false.obs; // 是否处于全屏编辑模式
final shouldShowExpandButton = false.obs; // 是否显示展开按钮
// 流事件订阅 // 流事件订阅
StreamSubscription<AgentServiceEvent>? _eventSubscription; StreamSubscription<AgentServiceEvent>? _eventSubscription;
@ -187,9 +191,47 @@ class AgentController extends GetxController {
textController.addListener(() { textController.addListener(() {
final text = textController.text.trim(); final text = textController.text.trim();
hasTextInput.value = text.isNotEmpty; 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<void> sendMessageFromFullScreen() async {
await sendMessage();
closeFullScreenEdit();
}
// 加载聊天历史记录 // 加载聊天历史记录
Future<void> _loadChatHistory() async { Future<void> _loadChatHistory() async {
try { try {

88
lib/modules/agent/views/agent_view.dart

@ -4,6 +4,7 @@ import 'package:get/get.dart';
import '../controllers/agent_controller.dart'; import '../controllers/agent_controller.dart';
import '../controllers/report_controller.dart'; import '../controllers/report_controller.dart';
import 'message_bubble.dart'; import 'message_bubble.dart';
import '../widgets/full_screen_editor.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
@ -287,8 +288,10 @@ class AgentView extends StatelessWidget {
} }
}), }),
// 输入控件行 // 输入控件行 - 使用IntrinsicHeight确保按钮与输入框底部对齐
Row( IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.end, // 所有元素底部对齐
children: [ children: [
// 键盘/语音切换按钮 // 键盘/语音切换按钮
Obx(() { Obx(() {
@ -325,10 +328,14 @@ class AgentView extends StatelessWidget {
final isTextMode = controller.isTextInputMode.value; final isTextMode = controller.isTextInputMode.value;
if (isTextMode) { if (isTextMode) {
// 文本输入模式 - 注意观察图片模式时的高度变化 // 文本输入模式 - 限制最大高度并只向上扩展
return Container( return Container(
//height: 44, constraints: const BoxConstraints(
padding: const EdgeInsets.symmetric(horizontal: 16), minHeight: 44, // 最小高度
maxHeight: 120, // 最大高度限制,防止挤压按钮
),
padding:
const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode color: isDarkMode
? Colors.grey[800] ? Colors.grey[800]
@ -343,6 +350,8 @@ class AgentView extends StatelessWidget {
), ),
), ),
child: Row( child: Row(
crossAxisAlignment:
CrossAxisAlignment.end, // 对齐到底部,实现向上扩展
children: [ children: [
Expanded( Expanded(
child: Obx(() { child: Obx(() {
@ -352,7 +361,8 @@ class AgentView extends StatelessWidget {
return TextField( return TextField(
controller: controller.textController, controller: controller.textController,
maxLines: isImageActive ? 2 : null, maxLines:
isImageActive ? 3 : 5, // 设置合理的最大行数
minLines: isImageActive ? 2 : 1, minLines: isImageActive ? 2 : 1,
keyboardType: keyboardType:
TextInputType.multiline, // 启用多行键盘 TextInputType.multiline, // 启用多行键盘
@ -383,7 +393,9 @@ class AgentView extends StatelessWidget {
), ),
), ),
contentPadding: EdgeInsets.symmetric( contentPadding: EdgeInsets.symmetric(
vertical: isImageActive ? 12 : 0), vertical: isImageActive
? 12
: 8), // 调整垂直内边距
), ),
onSubmitted: (_) => onSubmitted: (_) =>
controller.sendMessage(), controller.sendMessage(),
@ -471,15 +483,59 @@ class AgentView extends StatelessWidget {
const SizedBox(width: 8), const SizedBox(width: 8),
// 动态按钮:图片选择按钮 或 发送按钮 // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮
Obx(() { Obx(() {
final hasText = controller.hasTextInput.value; final hasText = controller.hasTextInput.value;
final hasImage = controller.isImageInputActive.value; final hasImage = controller.isImageInputActive.value;
final shouldShowSend = hasText || hasImage; final shouldShowSend = hasText || hasImage;
final shouldShowExpand =
controller.shouldShowExpandButton.value;
if (shouldShowSend) { if (shouldShowSend) {
// 显示发送按钮 // 显示发送按钮(可能还有展开按钮)
return GestureDetector( 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(), onTap: () => controller.sendMessage(),
child: Container( child: Container(
width: 44, width: 44,
@ -494,6 +550,8 @@ class AgentView extends StatelessWidget {
size: 22, size: 22,
), ),
), ),
),
],
); );
} else { } else {
// 显示图片选择按钮 // 显示图片选择按钮
@ -501,14 +559,17 @@ class AgentView extends StatelessWidget {
width: 44, width: 44,
height: 44, height: 44,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200], color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius: BorderRadius.circular(22), borderRadius: BorderRadius.circular(22),
), ),
child: PopupMenuButton<String>( child: PopupMenuButton<String>(
icon: Icon( icon: Icon(
Icons.add_photo_alternate_outlined, Icons.add_photo_alternate_outlined,
color: color: isDarkMode
isDarkMode ? Colors.grey[300] : Colors.grey[700], ? Colors.grey[300]
: Colors.grey[700],
size: 22, size: 22,
), ),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
@ -554,6 +615,7 @@ class AgentView extends StatelessWidget {
}), }),
], ],
), ),
),
], ],
), ),
), ),

190
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<FullScreenEditor> createState() => _FullScreenEditorState();
}
class _FullScreenEditorState extends State<FullScreenEditor> {
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<TextEditingValue>(
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,
),
),
),
),
],
),
);
}
}

6
lib/modules/profile/views/profile_view.dart

@ -327,7 +327,7 @@ class EditProfileView extends GetView<ProfileController> {
Row( Row(
children: [ children: [
Obx(() => CircleAvatar( Obx(() => CircleAvatar(
radius: 30.r, // 调整头像大小以适应行高 radius: 26.r, // 调整头像大小以适应行高
backgroundImage: controller.localAvatar.value.isNotEmpty backgroundImage: controller.localAvatar.value.isNotEmpty
? FileImage(File(controller.localAvatar.value)) ? FileImage(File(controller.localAvatar.value))
: (controller.userInfo.value.avatar.isNotEmpty : (controller.userInfo.value.avatar.isNotEmpty
@ -444,7 +444,7 @@ class EditProfileView extends GetView<ProfileController> {
borderRadius: BorderRadius.circular(16.r), borderRadius: BorderRadius.circular(16.r),
), ),
child: Icon( child: Icon(
Icons.edit, Icons.chevron_right,
size: 16.w, size: 16.w,
color: isDarkMode ? Colors.white70 : Colors.grey[500], color: isDarkMode ? Colors.white70 : Colors.grey[500],
), ),
@ -504,7 +504,7 @@ class EditProfileView extends GetView<ProfileController> {
child: TextField( child: TextField(
controller: nameController, controller: nameController,
autofocus: true, autofocus: true,
textAlign: TextAlign.right, // 右对齐,与原昵称显示保持一致 textAlign: TextAlign.center, // 右对齐,与原昵称显示保持一致
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[600], color: isDarkMode ? Colors.white70 : Colors.grey[600],

Loading…
Cancel
Save