Browse Source

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

新增全屏文本编辑器组件,当输入内容过长时自动显示展开按钮
优化输入框布局和高度限制,添加字符计数功能
weicu
lxm 1 year ago
parent
commit
c0a2546309
  1. 42
      lib/modules/agent/controllers/agent_controller.dart
  2. 514
      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 isFullScreenEditMode = false.obs; // 是否处于全屏编辑模式
final shouldShowExpandButton = false.obs; // 是否显示展开按钮
// 流事件订阅
StreamSubscription<AgentServiceEvent>? _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<void> sendMessageFromFullScreen() async {
await sendMessage();
closeFullScreenEdit();
}
// 加载聊天历史记录
Future<void> _loadChatHistory() async {
try {

514
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<String>(
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<String>(
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<String>(
icon: Icon(
Icons.add_photo_alternate_outlined,
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 22,
),
PopupMenuItem<String>(
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<String>(
value: 'camera',
child: Row(
children: [
Icon(Icons.camera_alt,
color: primaryColor, size: 20),
const SizedBox(width: 8),
const Text('拍照',
style: TextStyle(fontSize: 14)),
],
),
),
PopupMenuItem<String>(
value: 'gallery',
child: Row(
children: [
Icon(Icons.photo_library,
color: primaryColor, size: 20),
const SizedBox(width: 8),
const Text('从相册选择',
style: TextStyle(fontSize: 14)),
],
),
),
],
),
);
}
}),
],
),
),
],
),

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(
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<ProfileController> {
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<ProfileController> {
child: TextField(
controller: nameController,
autofocus: true,
textAlign: TextAlign.right, // 右对齐,与原昵称显示保持一致
textAlign: TextAlign.center, // 右对齐,与原昵称显示保持一致
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[600],

Loading…
Cancel
Save