|
|
@ -2,21 +2,53 @@ import 'dart:io'; |
|
|
import 'package:flutter/material.dart'; |
|
|
import 'package:flutter/material.dart'; |
|
|
import 'package:get/get.dart'; |
|
|
import 'package:get/get.dart'; |
|
|
import '../controllers/agent_controller.dart'; |
|
|
import '../controllers/agent_controller.dart'; |
|
|
import '../controllers/report_controller.dart'; |
|
|
import '../controllers/message_action_controller.dart'; |
|
|
import 'message_bubble.dart'; |
|
|
import 'message_bubble.dart'; |
|
|
import '../widgets/full_screen_editor.dart'; |
|
|
import '../widgets/full_screen_editor.dart'; |
|
|
import 'package:flutter/services.dart'; |
|
|
import '../widgets/sound_bar_widget.dart'; |
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|
|
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); |
|
|
const AgentView({Key? key}) : super(key: key); |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
|
State<AgentView> createState() => _AgentViewState(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
class _AgentViewState extends State<AgentView> with WidgetsBindingObserver { |
|
|
|
|
|
late AgentController controller; |
|
|
|
|
|
late MessageActionController messageActionController; |
|
|
|
|
|
double _keyboardHeight = 0; |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
|
void initState() { |
|
|
|
|
|
super.initState(); |
|
|
|
|
|
controller = Get.find<AgentController>(); |
|
|
|
|
|
messageActionController = Get.find<MessageActionController>(); |
|
|
|
|
|
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 |
|
|
@override |
|
|
Widget build(BuildContext context) { |
|
|
Widget build(BuildContext context) { |
|
|
final controller = Get.find<AgentController>(); |
|
|
|
|
|
final theme = Theme.of(context); |
|
|
final theme = Theme.of(context); |
|
|
final primaryColor = theme.primaryColor; |
|
|
final primaryColor = theme.primaryColor; |
|
|
final backgroundColor = theme.scaffoldBackgroundColor; |
|
|
final backgroundColor = theme.scaffoldBackgroundColor; |
|
|
@ -172,7 +204,7 @@ class AgentView extends StatelessWidget { |
|
|
padding: const EdgeInsets.only(bottom: 8), |
|
|
padding: const EdgeInsets.only(bottom: 8), |
|
|
child: GestureDetector( |
|
|
child: GestureDetector( |
|
|
onLongPressStart: (LongPressStartDetails details) { |
|
|
onLongPressStart: (LongPressStartDetails details) { |
|
|
_onLongPressMessageContent( |
|
|
messageActionController.onLongPressMessage( |
|
|
context, isDarkMode, details, message); |
|
|
context, isDarkMode, details, message); |
|
|
}, |
|
|
}, |
|
|
child: MessageBubble( |
|
|
child: MessageBubble( |
|
|
@ -288,6 +320,9 @@ class AgentView extends StatelessWidget { |
|
|
} |
|
|
} |
|
|
}), |
|
|
}), |
|
|
|
|
|
|
|
|
|
|
|
// 输入控件区域 - 包含输入行和展开的选项 |
|
|
|
|
|
Column( |
|
|
|
|
|
children: [ |
|
|
// 输入控件行 - 使用IntrinsicHeight确保按钮与输入框底部对齐 |
|
|
// 输入控件行 - 使用IntrinsicHeight确保按钮与输入框底部对齐 |
|
|
IntrinsicHeight( |
|
|
IntrinsicHeight( |
|
|
child: Row( |
|
|
child: Row( |
|
|
@ -325,7 +360,8 @@ class AgentView extends StatelessWidget { |
|
|
// 文本输入框或语音波形图 |
|
|
// 文本输入框或语音波形图 |
|
|
Expanded( |
|
|
Expanded( |
|
|
child: Obx(() { |
|
|
child: Obx(() { |
|
|
final isTextMode = controller.isTextInputMode.value; |
|
|
final isTextMode = |
|
|
|
|
|
controller.isTextInputMode.value; |
|
|
|
|
|
|
|
|
if (isTextMode) { |
|
|
if (isTextMode) { |
|
|
// 文本输入模式 - 限制最大高度并只向上扩展 |
|
|
// 文本输入模式 - 限制最大高度并只向上扩展 |
|
|
@ -334,8 +370,8 @@ class AgentView extends StatelessWidget { |
|
|
minHeight: 44, // 最小高度 |
|
|
minHeight: 44, // 最小高度 |
|
|
maxHeight: 120, // 最大高度限制,防止挤压按钮 |
|
|
maxHeight: 120, // 最大高度限制,防止挤压按钮 |
|
|
), |
|
|
), |
|
|
padding: |
|
|
padding: const EdgeInsets.symmetric( |
|
|
const EdgeInsets.symmetric(horizontal: 16), |
|
|
horizontal: 16), |
|
|
decoration: BoxDecoration( |
|
|
decoration: BoxDecoration( |
|
|
color: isDarkMode |
|
|
color: isDarkMode |
|
|
? Colors.grey[800] |
|
|
? Colors.grey[800] |
|
|
@ -356,18 +392,29 @@ class AgentView extends StatelessWidget { |
|
|
Expanded( |
|
|
Expanded( |
|
|
child: Obx(() { |
|
|
child: Obx(() { |
|
|
// 图片模式下使用更高的输入框 |
|
|
// 图片模式下使用更高的输入框 |
|
|
final isImageActive = |
|
|
final isImageActive = controller |
|
|
controller.isImageInputActive.value; |
|
|
.isImageInputActive.value; |
|
|
|
|
|
|
|
|
return TextField( |
|
|
return TextField( |
|
|
controller: controller.textController, |
|
|
controller: |
|
|
maxLines: |
|
|
controller.textController, |
|
|
isImageActive ? 3 : 5, // 设置合理的最大行数 |
|
|
maxLines: isImageActive |
|
|
|
|
|
? 3 |
|
|
|
|
|
: 5, // 设置合理的最大行数 |
|
|
minLines: isImageActive ? 2 : 1, |
|
|
minLines: isImageActive ? 2 : 1, |
|
|
keyboardType: |
|
|
keyboardType: TextInputType |
|
|
TextInputType.multiline, // 启用多行键盘 |
|
|
.multiline, // 启用多行键盘 |
|
|
textInputAction: |
|
|
textInputAction: TextInputAction |
|
|
TextInputAction.newline, // 支持回车换行 |
|
|
.newline, // 支持回车换行 |
|
|
|
|
|
onTap: () { |
|
|
|
|
|
// 当点击输入框时,如果图片选项已展开,则收缩 |
|
|
|
|
|
if (controller |
|
|
|
|
|
.isImageOptionsExpanded |
|
|
|
|
|
.value) { |
|
|
|
|
|
controller |
|
|
|
|
|
.collapseImageOptions(); |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
style: TextStyle( |
|
|
style: TextStyle( |
|
|
fontSize: 15, |
|
|
fontSize: 15, |
|
|
color: isDarkMode |
|
|
color: isDarkMode |
|
|
@ -392,7 +439,8 @@ class AgentView extends StatelessWidget { |
|
|
width: 1.0, |
|
|
width: 1.0, |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
contentPadding: EdgeInsets.symmetric( |
|
|
contentPadding: |
|
|
|
|
|
EdgeInsets.symmetric( |
|
|
vertical: isImageActive |
|
|
vertical: isImageActive |
|
|
? 12 |
|
|
? 12 |
|
|
: 8), // 调整垂直内边距 |
|
|
: 8), // 调整垂直内边距 |
|
|
@ -417,8 +465,8 @@ class AgentView extends StatelessWidget { |
|
|
}, |
|
|
}, |
|
|
child: Container( |
|
|
child: Container( |
|
|
height: 50, |
|
|
height: 50, |
|
|
padding: |
|
|
padding: const EdgeInsets.symmetric( |
|
|
const EdgeInsets.symmetric(horizontal: 16), |
|
|
horizontal: 16), |
|
|
decoration: BoxDecoration( |
|
|
decoration: BoxDecoration( |
|
|
color: isDarkMode |
|
|
color: isDarkMode |
|
|
? Colors.grey[800] |
|
|
? Colors.grey[800] |
|
|
@ -439,14 +487,12 @@ class AgentView extends StatelessWidget { |
|
|
mainAxisAlignment: |
|
|
mainAxisAlignment: |
|
|
MainAxisAlignment.start, |
|
|
MainAxisAlignment.start, |
|
|
children: [ |
|
|
children: [ |
|
|
...List.generate(10, (index) { |
|
|
const AgentSoundBarsWidget(), |
|
|
return _buildSoundBar(index); |
|
|
|
|
|
}), |
|
|
|
|
|
const Spacer(), |
|
|
const Spacer(), |
|
|
// "正在聆听"文字 |
|
|
// "正在聆听"文字 |
|
|
Obx(() { |
|
|
Obx(() { |
|
|
final isListening = |
|
|
final isListening = controller |
|
|
controller.isListening.value; |
|
|
.isListening.value; |
|
|
return Row( |
|
|
return Row( |
|
|
children: [ |
|
|
children: [ |
|
|
Icon( |
|
|
Icon( |
|
|
@ -461,7 +507,8 @@ class AgentView extends StatelessWidget { |
|
|
'正在聆听', |
|
|
'正在聆听', |
|
|
style: TextStyle( |
|
|
style: TextStyle( |
|
|
fontSize: 13, |
|
|
fontSize: 13, |
|
|
fontWeight: FontWeight.w500, |
|
|
fontWeight: |
|
|
|
|
|
FontWeight.w500, |
|
|
color: isListening |
|
|
color: isListening |
|
|
? primaryColor |
|
|
? primaryColor |
|
|
: Colors.grey[500], |
|
|
: Colors.grey[500], |
|
|
@ -486,7 +533,8 @@ class AgentView extends StatelessWidget { |
|
|
// 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 |
|
|
// 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 |
|
|
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 = |
|
|
final shouldShowExpand = |
|
|
controller.shouldShowExpandButton.value; |
|
|
controller.shouldShowExpandButton.value; |
|
|
@ -499,7 +547,8 @@ class AgentView extends StatelessWidget { |
|
|
// 展开按钮(在发送按钮上方) |
|
|
// 展开按钮(在发送按钮上方) |
|
|
if (shouldShowExpand && hasText) |
|
|
if (shouldShowExpand && hasText) |
|
|
Padding( |
|
|
Padding( |
|
|
padding: const EdgeInsets.only(bottom: 40), |
|
|
padding: |
|
|
|
|
|
const EdgeInsets.only(bottom: 40), |
|
|
child: GestureDetector( |
|
|
child: GestureDetector( |
|
|
onTap: () { |
|
|
onTap: () { |
|
|
// 打开底部弹出编辑模式 |
|
|
// 打开底部弹出编辑模式 |
|
|
@ -522,7 +571,8 @@ class AgentView extends StatelessWidget { |
|
|
color: isDarkMode |
|
|
color: isDarkMode |
|
|
? Colors.grey[700] |
|
|
? Colors.grey[700] |
|
|
: Colors.grey[300], |
|
|
: Colors.grey[300], |
|
|
borderRadius: BorderRadius.circular(16), |
|
|
borderRadius: |
|
|
|
|
|
BorderRadius.circular(16), |
|
|
), |
|
|
), |
|
|
child: Icon( |
|
|
child: Icon( |
|
|
Icons.expand_less, |
|
|
Icons.expand_less, |
|
|
@ -554,8 +604,15 @@ class AgentView extends StatelessWidget { |
|
|
], |
|
|
], |
|
|
); |
|
|
); |
|
|
} else { |
|
|
} else { |
|
|
// 显示图片选择按钮 |
|
|
// 显示'+'选择按钮 |
|
|
return Container( |
|
|
return Obx(() { |
|
|
|
|
|
final isExpanded = |
|
|
|
|
|
controller.isImageOptionsExpanded.value; |
|
|
|
|
|
return GestureDetector( |
|
|
|
|
|
onTap: () { |
|
|
|
|
|
controller.toggleImageOptions(); |
|
|
|
|
|
}, |
|
|
|
|
|
child: Container( |
|
|
width: 44, |
|
|
width: 44, |
|
|
height: 44, |
|
|
height: 44, |
|
|
decoration: BoxDecoration( |
|
|
decoration: BoxDecoration( |
|
|
@ -564,58 +621,89 @@ class AgentView extends StatelessWidget { |
|
|
: Colors.grey[200], |
|
|
: Colors.grey[200], |
|
|
borderRadius: BorderRadius.circular(22), |
|
|
borderRadius: BorderRadius.circular(22), |
|
|
), |
|
|
), |
|
|
child: PopupMenuButton<String>( |
|
|
child: Icon( |
|
|
icon: Icon( |
|
|
isExpanded ? Icons.close : Icons.add, |
|
|
Icons.add_photo_alternate_outlined, |
|
|
|
|
|
color: isDarkMode |
|
|
color: isDarkMode |
|
|
? Colors.grey[300] |
|
|
? Colors.grey[300] |
|
|
: Colors.grey[700], |
|
|
: Colors.grey[700], |
|
|
size: 22, |
|
|
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)), |
|
|
|
|
|
], |
|
|
], |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
PopupMenuItem<String>( |
|
|
|
|
|
value: 'gallery', |
|
|
// 展开的'+'选择选项 |
|
|
|
|
|
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( |
|
|
child: Row( |
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 改为均匀分布 |
|
|
children: [ |
|
|
children: [ |
|
|
Icon(Icons.photo_library, |
|
|
// 相机按钮 |
|
|
color: primaryColor, size: 20), |
|
|
_buildPlusOptionButton( |
|
|
const SizedBox(width: 8), |
|
|
context: context, |
|
|
const Text('从相册选择', |
|
|
icon: Icons.camera_alt, |
|
|
style: TextStyle(fontSize: 14)), |
|
|
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(), |
|
|
); |
|
|
); |
|
|
} |
|
|
|
|
|
}), |
|
|
}), |
|
|
], |
|
|
], |
|
|
), |
|
|
), |
|
|
), |
|
|
|
|
|
], |
|
|
], |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
@ -624,130 +712,11 @@ class AgentView extends StatelessWidget { |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
void _onLongPressMessageContent(BuildContext context, bool isDarkMode, |
|
|
|
|
|
LongPressStartDetails details, Message message) { |
|
|
|
|
|
Haptics.vibrate(HapticsType.success); |
|
|
|
|
|
RenderBox? renderBox = |
|
|
|
|
|
Overlay.of(context).context.findRenderObject() as RenderBox; |
|
|
|
|
|
final RelativeRect position = RelativeRect.fromRect( |
|
|
|
|
|
Rect.fromLTRB( |
|
|
|
|
|
details.globalPosition.dx, |
|
|
|
|
|
details.globalPosition.dy, |
|
|
|
|
|
details.globalPosition.dx + 110, // 菜单显示位置X轴坐标 |
|
|
|
|
|
details.globalPosition.dy - 40, // 菜单显示位置Y轴坐标 |
|
|
|
|
|
), |
|
|
|
|
|
Offset.zero & renderBox.size, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
showMenu( |
|
|
|
|
|
context: context, |
|
|
|
|
|
position: position, |
|
|
|
|
|
color: isDarkMode ? AppColors.cardBackground : Colors.white, |
|
|
|
|
|
shape: RoundedRectangleBorder( |
|
|
|
|
|
borderRadius: BorderRadius.circular(12.r), |
|
|
|
|
|
), |
|
|
|
|
|
items: [ |
|
|
|
|
|
_popupMenuEntry( |
|
|
|
|
|
Icon( |
|
|
|
|
|
Icons.copy, |
|
|
|
|
|
size: 20.w, |
|
|
|
|
|
color: isDarkMode ? Colors.white : Colors.black87, |
|
|
|
|
|
), |
|
|
|
|
|
'复制', |
|
|
|
|
|
isDarkMode ? Colors.white : Colors.black87, |
|
|
|
|
|
() { |
|
|
|
|
|
Clipboard.setData( |
|
|
|
|
|
ClipboardData( |
|
|
|
|
|
text: message.text, |
|
|
|
|
|
), |
|
|
|
|
|
).then((value) => Get.snackbar( |
|
|
|
|
|
'success'.tr, // 成功 |
|
|
|
|
|
'复制成功', |
|
|
|
|
|
backgroundColor: Colors.blue.withOpacity(0.8), |
|
|
|
|
|
colorText: Colors.white, |
|
|
|
|
|
)); |
|
|
|
|
|
}, |
|
|
|
|
|
), |
|
|
|
|
|
if (!message.isUser) ...[ |
|
|
|
|
|
// 自定义分隔线样式 |
|
|
|
|
|
PopupMenuItem( |
|
|
|
|
|
height: 1, // 控制间距 |
|
|
|
|
|
enabled: false, |
|
|
|
|
|
child: Divider( |
|
|
|
|
|
height: 1, |
|
|
|
|
|
thickness: 0.8, |
|
|
|
|
|
indent: 0, |
|
|
|
|
|
endIndent: 0, |
|
|
|
|
|
color: isDarkMode ? Colors.grey[700] : Colors.grey[300], |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
_popupMenuEntry( |
|
|
|
|
|
Icon( |
|
|
|
|
|
Icons.report, |
|
|
|
|
|
size: 20.w, |
|
|
|
|
|
color: isDarkMode ? Colors.red[700] : Colors.red, |
|
|
|
|
|
), |
|
|
|
|
|
'举报'.tr, // 举报 |
|
|
|
|
|
isDarkMode ? Colors.red[700]! : Colors.red, |
|
|
|
|
|
() { |
|
|
|
|
|
// 实现举报功能 |
|
|
|
|
|
final reportController = Get.find<ReportController>(); |
|
|
|
|
|
reportController.initReport(message); |
|
|
|
|
|
|
|
|
|
|
|
_showReportForm(context, isDarkMode); |
|
|
|
|
|
}, |
|
|
|
|
|
), |
|
|
|
|
|
] |
|
|
|
|
|
], |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 更新_showReportForm方法 |
|
|
|
|
|
void _showReportForm(BuildContext context, bool isDarkMode) { |
|
|
|
|
|
showModalBottomSheet( |
|
|
|
|
|
context: context, |
|
|
|
|
|
isScrollControlled: true, |
|
|
|
|
|
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white, |
|
|
|
|
|
shape: RoundedRectangleBorder( |
|
|
|
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16.r)), |
|
|
|
|
|
), |
|
|
|
|
|
builder: (BuildContext context) { |
|
|
|
|
|
return Padding( |
|
|
|
|
|
padding: EdgeInsets.only( |
|
|
|
|
|
bottom: MediaQuery.of(context).viewInsets.bottom, |
|
|
|
|
|
), |
|
|
|
|
|
child: _ReportForm(isDarkMode: isDarkMode), |
|
|
|
|
|
); |
|
|
|
|
|
}, |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
PopupMenuEntry _popupMenuEntry( |
|
|
|
|
|
Icon icon, |
|
|
|
|
|
String title, |
|
|
|
|
|
Color color, |
|
|
|
|
|
Function() onTap, |
|
|
|
|
|
) { |
|
|
|
|
|
return PopupMenuItem( |
|
|
|
|
|
onTap: onTap, |
|
|
|
|
|
child: SizedBox( |
|
|
|
|
|
width: 140.w, |
|
|
|
|
|
child: Row( |
|
|
|
|
|
children: [ |
|
|
|
|
|
icon, |
|
|
|
|
|
8.horizontalSpace, |
|
|
|
|
|
Text( |
|
|
|
|
|
title, |
|
|
|
|
|
style: TextStyle( |
|
|
|
|
|
color: color, |
|
|
|
|
|
fontSize: 14.sp, |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
], |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 构建提示选项芯片 |
|
|
// 构建提示选项芯片 |
|
|
Widget _buildPromptChip({ |
|
|
Widget _buildPromptChip({ |
|
|
@ -782,245 +751,65 @@ class AgentView extends StatelessWidget { |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 构建声音柱 |
|
|
|
|
|
Widget _buildSoundBar(int index) { |
|
|
|
|
|
return Padding( |
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 1), |
|
|
|
|
|
child: Obx(() => _SoundBar( |
|
|
|
|
|
delay: index * 0.1, |
|
|
|
|
|
isActive: Get.find<AgentController>().isListening.value, |
|
|
|
|
|
)), |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
class _ReportForm extends StatelessWidget { |
|
|
|
|
|
final bool isDarkMode; |
|
|
|
|
|
final ReportController reportController = Get.find<ReportController>(); |
|
|
|
|
|
|
|
|
|
|
|
_ReportForm({required this.isDarkMode}); |
|
|
// 构建"+"号展开选项按钮 |
|
|
|
|
|
Widget _buildPlusOptionButton({ |
|
|
|
|
|
required BuildContext context, |
|
|
|
|
|
required IconData icon, |
|
|
|
|
|
required String label, |
|
|
|
|
|
required VoidCallback onTap, |
|
|
|
|
|
required bool isDarkMode, |
|
|
|
|
|
}) { |
|
|
|
|
|
final primaryColor = Theme.of(context).primaryColor; |
|
|
|
|
|
|
|
|
@override |
|
|
return GestureDetector( |
|
|
Widget build(BuildContext context) { |
|
|
onTap: onTap, |
|
|
return Container( |
|
|
child: SizedBox( |
|
|
padding: EdgeInsets.all(16.w), |
|
|
// 调整为一行四个按钮的宽度,考虑屏幕宽度和间距 |
|
|
child: SingleChildScrollView( |
|
|
width: (1.sw - 100.w) / 4, // 屏幕宽度减去左右边距,除以4个按钮 |
|
|
child: GetBuilder<ReportController>( |
|
|
child: Column( |
|
|
builder: (_) => Column( |
|
|
|
|
|
mainAxisSize: MainAxisSize.min, |
|
|
mainAxisSize: MainAxisSize.min, |
|
|
crossAxisAlignment: CrossAxisAlignment.stretch, |
|
|
mainAxisAlignment: MainAxisAlignment.center, |
|
|
children: [ |
|
|
children: [ |
|
|
Stack( |
|
|
// 正方形图标容器 - 增大尺寸 |
|
|
alignment: Alignment.center, |
|
|
|
|
|
children: [ |
|
|
|
|
|
Center( |
|
|
|
|
|
child: Text( |
|
|
|
|
|
'举报', |
|
|
|
|
|
style: TextStyle( |
|
|
|
|
|
fontSize: 18.sp, |
|
|
|
|
|
fontWeight: FontWeight.bold, |
|
|
|
|
|
color: isDarkMode ? Colors.white : Colors.black, |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
Positioned( |
|
|
|
|
|
right: 0, |
|
|
|
|
|
child: IconButton( |
|
|
|
|
|
icon: Icon( |
|
|
|
|
|
Icons.close, |
|
|
|
|
|
color: isDarkMode ? Colors.grey[400] : Colors.black54, |
|
|
|
|
|
), |
|
|
|
|
|
onPressed: () => Navigator.pop(context), |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
], |
|
|
|
|
|
), |
|
|
|
|
|
Container( |
|
|
|
|
|
padding: EdgeInsets.symmetric(horizontal: 12.w), |
|
|
|
|
|
margin: EdgeInsets.only(bottom: 10.w), |
|
|
|
|
|
child: Divider( |
|
|
|
|
|
height: 1, |
|
|
|
|
|
color: Colors.grey[300], |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
GetBuilder<ReportController>( |
|
|
|
|
|
builder: (controller) { |
|
|
|
|
|
return Wrap( |
|
|
|
|
|
spacing: 8.w, |
|
|
|
|
|
runSpacing: 8.h, |
|
|
|
|
|
children: [ |
|
|
|
|
|
_buildReportChip('有害/不安全', isDarkMode, controller), |
|
|
|
|
|
_buildReportChip('色情低俗', isDarkMode, controller), |
|
|
|
|
|
_buildReportChip('误导攻击', isDarkMode, controller), |
|
|
|
|
|
_buildReportChip('违法犯罪', isDarkMode, controller), |
|
|
|
|
|
_buildReportChip('隐私相关', isDarkMode, controller), |
|
|
|
|
|
_buildReportChip('侵犯我的著作权、名誉权等', isDarkMode, controller), |
|
|
|
|
|
_buildReportChip('其他', isDarkMode, controller), |
|
|
|
|
|
], |
|
|
|
|
|
); |
|
|
|
|
|
}, |
|
|
|
|
|
), |
|
|
|
|
|
SizedBox(height: 16.h), |
|
|
|
|
|
Container( |
|
|
Container( |
|
|
padding: EdgeInsets.all(12.w), |
|
|
width: 50.w, // 调整为44.w以适应布局 |
|
|
|
|
|
height: 50.w, // 调整为44.w以适应布局 |
|
|
decoration: BoxDecoration( |
|
|
decoration: BoxDecoration( |
|
|
color: isDarkMode ? Colors.grey[800] : Colors.grey[200], |
|
|
color: isDarkMode ? Colors.grey[800] : Colors.grey[100], |
|
|
borderRadius: BorderRadius.circular(8.r), |
|
|
borderRadius: BorderRadius.circular(12.r), // 调整为10.r |
|
|
), |
|
|
border: Border.all( |
|
|
child: TextField( |
|
|
color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!, |
|
|
maxLines: 3, |
|
|
width: 1, |
|
|
onChanged: reportController.updateDescription, |
|
|
|
|
|
style: TextStyle( |
|
|
|
|
|
color: isDarkMode ? Colors.white : Colors.black, |
|
|
|
|
|
), |
|
|
|
|
|
decoration: InputDecoration( |
|
|
|
|
|
hintText: '我们想知道你举报的原因,你可以描述你遇到的问题。', |
|
|
|
|
|
hintStyle: TextStyle( |
|
|
|
|
|
color: isDarkMode ? Colors.white70 : Colors.black54, |
|
|
|
|
|
fontSize: 14.sp, |
|
|
|
|
|
), |
|
|
|
|
|
border: InputBorder.none, |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
SizedBox(height: 14.h), |
|
|
|
|
|
ElevatedButton( |
|
|
|
|
|
style: ElevatedButton.styleFrom( |
|
|
|
|
|
backgroundColor: Colors.blue, |
|
|
|
|
|
foregroundColor: Colors.white, |
|
|
|
|
|
padding: EdgeInsets.symmetric(vertical: 12.h), |
|
|
|
|
|
shape: RoundedRectangleBorder( |
|
|
|
|
|
borderRadius: BorderRadius.circular(8.r), |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
|
|
|
child: Text('提交'), |
|
|
|
|
|
onPressed: () => reportController.submitReport(), |
|
|
|
|
|
), |
|
|
|
|
|
SizedBox(height: 8.h), |
|
|
|
|
|
], |
|
|
|
|
|
), |
|
|
|
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
); |
|
|
child: Icon( |
|
|
} |
|
|
icon, |
|
|
|
|
|
color: primaryColor, |
|
|
Widget _buildReportChip( |
|
|
size: 20.sp, // 调整为18.sp |
|
|
String label, bool isDarkMode, ReportController controller) { |
|
|
|
|
|
final isSelected = reportController.selectedReasons.contains(label); |
|
|
|
|
|
|
|
|
|
|
|
return GestureDetector( |
|
|
|
|
|
onTap: () => reportController.toggleReason(label), |
|
|
|
|
|
child: Container( |
|
|
|
|
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h), |
|
|
|
|
|
decoration: BoxDecoration( |
|
|
|
|
|
color: isSelected |
|
|
|
|
|
? Colors.red |
|
|
|
|
|
: (isDarkMode ? Colors.grey[800] : Colors.grey[200]), |
|
|
|
|
|
borderRadius: BorderRadius.circular(16.r), |
|
|
|
|
|
border: Border.all( |
|
|
|
|
|
color: isSelected |
|
|
|
|
|
? Colors.red[700]! |
|
|
|
|
|
: (isDarkMode ? Colors.grey[700]! : Colors.grey[300]!), |
|
|
|
|
|
width: isSelected ? 1.0 : 0.5, |
|
|
|
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
child: Text( |
|
|
SizedBox(height: 4.h), // 调整为3.h |
|
|
|
|
|
// 文字标签 |
|
|
|
|
|
Text( |
|
|
label, |
|
|
label, |
|
|
style: TextStyle( |
|
|
style: TextStyle( |
|
|
color: isSelected |
|
|
fontSize: 10.sp, // 调整为9.sp |
|
|
? Colors.white |
|
|
color: isDarkMode ? Colors.grey[300] : Colors.grey[700], |
|
|
: (isDarkMode ? Colors.white : Colors.black87), |
|
|
fontWeight: FontWeight.w500, |
|
|
fontSize: 14.sp, |
|
|
|
|
|
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, |
|
|
|
|
|
), |
|
|
), |
|
|
|
|
|
textAlign: TextAlign.center, |
|
|
|
|
|
maxLines: 1, |
|
|
|
|
|
overflow: TextOverflow.ellipsis, |
|
|
), |
|
|
), |
|
|
|
|
|
], |
|
|
), |
|
|
), |
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 声音柱动画组件 |
|
|
|
|
|
class _SoundBar extends StatefulWidget { |
|
|
|
|
|
final double delay; |
|
|
|
|
|
final bool isActive; |
|
|
|
|
|
|
|
|
|
|
|
const _SoundBar({ |
|
|
|
|
|
required this.delay, |
|
|
|
|
|
required this.isActive, |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
|
_SoundBarState createState() => _SoundBarState(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
class _SoundBarState extends State<_SoundBar> |
|
|
|
|
|
with SingleTickerProviderStateMixin { |
|
|
|
|
|
late AnimationController _controller; |
|
|
|
|
|
late Animation<double> _animation; |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
|
void initState() { |
|
|
|
|
|
super.initState(); |
|
|
|
|
|
|
|
|
|
|
|
_controller = AnimationController( |
|
|
|
|
|
vsync: this, |
|
|
|
|
|
duration: const Duration(milliseconds: 500), |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
_animation = Tween<double>(begin: 3, end: 18).animate( |
|
|
|
|
|
CurvedAnimation( |
|
|
|
|
|
parent: _controller, |
|
|
|
|
|
curve: Curves.easeInOut, |
|
|
|
|
|
), |
|
|
), |
|
|
)..addListener(() { |
|
|
); |
|
|
setState(() {}); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
_startOrStopAnimation(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
|
void didUpdateWidget(_SoundBar oldWidget) { |
|
|
|
|
|
super.didUpdateWidget(oldWidget); |
|
|
|
|
|
if (widget.isActive != oldWidget.isActive) { |
|
|
|
|
|
_startOrStopAnimation(); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
void _startOrStopAnimation() { |
|
|
|
|
|
if (widget.isActive) { |
|
|
|
|
|
Future.delayed(Duration(milliseconds: (widget.delay * 1000).toInt()), () { |
|
|
|
|
|
if (mounted && widget.isActive) { |
|
|
|
|
|
_controller.repeat(reverse: true); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
} else { |
|
|
|
|
|
_controller.reset(); |
|
|
|
|
|
// 设置为小高度的静态状态 |
|
|
|
|
|
_controller.animateTo(0.1, duration: const Duration(milliseconds: 300)); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
|
void dispose() { |
|
|
|
|
|
_controller.dispose(); |
|
|
|
|
|
super.dispose(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
|
Widget build(BuildContext context) { |
|
|
|
|
|
final primaryColor = Theme.of(context).primaryColor; |
|
|
|
|
|
|
|
|
|
|
|
return Container( |
|
|
|
|
|
width: 2.5, |
|
|
|
|
|
height: widget.isActive ? _animation.value : 3, |
|
|
|
|
|
margin: const EdgeInsets.symmetric(horizontal: 1), |
|
|
|
|
|
decoration: BoxDecoration( |
|
|
|
|
|
color: widget.isActive |
|
|
|
|
|
? primaryColor.withOpacity(0.8) |
|
|
|
|
|
: Colors.grey.withOpacity(0.4), |
|
|
|
|
|
borderRadius: BorderRadius.circular(1.5), |
|
|
|
|
|
), |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|