10 changed files with 1323 additions and 831 deletions
@ -1,11 +1,11 @@ |
|||
import 'package:get/get.dart'; |
|||
import '../controllers/agent_controller.dart'; |
|||
import '../controllers/report_controller.dart'; |
|||
import '../controllers/message_action_controller.dart'; |
|||
|
|||
class AgentBinding extends Bindings { |
|||
@override |
|||
void dependencies() { |
|||
Get.lazyPut<AgentController>(() => AgentController()); |
|||
Get.lazyPut<ReportController>(() => ReportController()); |
|||
Get.lazyPut<MessageActionController>(() => MessageActionController()); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,402 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:flutter/services.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import 'package:haptic_feedback/haptic_feedback.dart'; |
|||
import '../../../core/theme/app_colors.dart'; |
|||
import '../../../data/models/message_model.dart'; |
|||
import '../../../data/services/network/api.dart'; |
|||
|
|||
/// 消息操作控制器 |
|||
/// 负责处理消息长按、复制、举报等操作 |
|||
class MessageActionController extends GetxController { |
|||
// 当前举报的消息 |
|||
final reportingMessage = Rxn<Message>(); |
|||
|
|||
// 选择的举报原因 |
|||
List<String> selectedReasons = []; |
|||
|
|||
// 举报描述 |
|||
final reportDescription = ''.obs; |
|||
|
|||
/// 处理消息长按事件 |
|||
void onLongPressMessage( |
|||
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: _buildMenuItems(context, isDarkMode, message), |
|||
); |
|||
} |
|||
|
|||
/// 构建弹出菜单项 |
|||
List<PopupMenuEntry> _buildMenuItems( |
|||
BuildContext context, |
|||
bool isDarkMode, |
|||
Message message, |
|||
) { |
|||
final List<PopupMenuEntry> items = [ |
|||
// 复制选项 |
|||
_buildPopupMenuItem( |
|||
Icon( |
|||
Icons.copy, |
|||
size: 20.w, |
|||
color: isDarkMode ? Colors.white : Colors.black87, |
|||
), |
|||
'复制', |
|||
isDarkMode ? Colors.white : Colors.black87, |
|||
() => _copyMessage(message), |
|||
), |
|||
]; |
|||
|
|||
// 如果不是用户消息,添加举报选项 |
|||
if (!message.isUser) { |
|||
items.addAll([ |
|||
// 分隔线 |
|||
PopupMenuItem( |
|||
height: 1, |
|||
enabled: false, |
|||
child: Divider( |
|||
height: 1, |
|||
thickness: 0.8, |
|||
indent: 0, |
|||
endIndent: 0, |
|||
color: isDarkMode ? Colors.grey[700] : Colors.grey[300], |
|||
), |
|||
), |
|||
// 举报选项 |
|||
_buildPopupMenuItem( |
|||
Icon( |
|||
Icons.report, |
|||
size: 20.w, |
|||
color: isDarkMode ? Colors.red[700] : Colors.red, |
|||
), |
|||
'举报'.tr, |
|||
isDarkMode ? Colors.red[700]! : Colors.red, |
|||
() => _showReportForm(context, isDarkMode, message), |
|||
), |
|||
]); |
|||
} |
|||
|
|||
return items; |
|||
} |
|||
|
|||
/// 构建弹出菜单项 |
|||
PopupMenuEntry _buildPopupMenuItem( |
|||
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, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
/// 复制消息内容 |
|||
void _copyMessage(Message message) { |
|||
Clipboard.setData(ClipboardData(text: message.text)).then((_) { |
|||
Get.snackbar( |
|||
'success'.tr, |
|||
'复制成功', |
|||
backgroundColor: Colors.blue.withOpacity(0.8), |
|||
colorText: Colors.white, |
|||
); |
|||
}); |
|||
} |
|||
|
|||
/// 显示举报表单 |
|||
void _showReportForm(BuildContext context, bool isDarkMode, Message message) { |
|||
// 初始化举报状态 |
|||
initReport(message); |
|||
|
|||
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), |
|||
); |
|||
}, |
|||
); |
|||
} |
|||
|
|||
/// 初始化举报状态 |
|||
void initReport(Message message) { |
|||
reportingMessage.value = message; |
|||
selectedReasons.clear(); |
|||
reportDescription.value = ''; |
|||
} |
|||
|
|||
/// 切换举报原因选择状态 |
|||
void toggleReason(String reason) { |
|||
if (selectedReasons.contains(reason)) { |
|||
selectedReasons.remove(reason); |
|||
} else { |
|||
selectedReasons.add(reason); |
|||
} |
|||
update(); |
|||
} |
|||
|
|||
/// 更新举报描述 |
|||
void updateDescription(String text) { |
|||
reportDescription.value = text; |
|||
} |
|||
|
|||
/// 提交举报 |
|||
Future<void> submitReport() async { |
|||
if (reportingMessage.value == null) return; |
|||
|
|||
// 构建举报数据 |
|||
final reportData = { |
|||
'describe': reportDescription.value, |
|||
'message': [], |
|||
'reportContext': reportingMessage.value!.text, |
|||
'reporttype': selectedReasons.toString(), |
|||
}; |
|||
|
|||
try { |
|||
// 调用API提交举报 |
|||
var response = await Api.reportAiContent(reportData); |
|||
|
|||
if (response != null) { |
|||
print('提交举报响应: $response'); |
|||
} |
|||
|
|||
// 提交成功后重置状态 |
|||
selectedReasons.clear(); |
|||
reportDescription.value = ''; |
|||
reportingMessage.value = null; |
|||
|
|||
// 关闭举报表单 |
|||
Navigator.of(Get.context!).pop(); |
|||
|
|||
// 显示成功通知 |
|||
Get.snackbar( |
|||
'提交成功', |
|||
'举报已提交,我们会尽快处理', |
|||
backgroundColor: Colors.green.withOpacity(0.8), |
|||
colorText: Colors.white, |
|||
duration: const Duration(seconds: 2), |
|||
); |
|||
} catch (e) { |
|||
// 显示错误通知 |
|||
Get.snackbar( |
|||
'提交失败', |
|||
'网络错误,请稍后重试', |
|||
backgroundColor: Colors.red.withOpacity(0.8), |
|||
colorText: Colors.white, |
|||
duration: const Duration(seconds: 2), |
|||
); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// 举报表单组件 |
|||
class _ReportForm extends StatelessWidget { |
|||
final bool isDarkMode; |
|||
final MessageActionController controller = Get.find<MessageActionController>(); |
|||
|
|||
_ReportForm({required this.isDarkMode}); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Container( |
|||
padding: EdgeInsets.all(16.w), |
|||
child: SingleChildScrollView( |
|||
child: GetBuilder<MessageActionController>( |
|||
builder: (_) => Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
crossAxisAlignment: CrossAxisAlignment.stretch, |
|||
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<MessageActionController>( |
|||
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( |
|||
padding: EdgeInsets.all(12.w), |
|||
decoration: BoxDecoration( |
|||
color: isDarkMode ? Colors.grey[800] : Colors.grey[200], |
|||
borderRadius: BorderRadius.circular(8.r), |
|||
), |
|||
child: TextField( |
|||
maxLines: 3, |
|||
onChanged: controller.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: () => controller.submitReport(), |
|||
), |
|||
|
|||
SizedBox(height: 8.h), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
/// 构建举报原因芯片 |
|||
Widget _buildReportChip( |
|||
String label, |
|||
bool isDarkMode, |
|||
MessageActionController controller, |
|||
) { |
|||
final isSelected = controller.selectedReasons.contains(label); |
|||
|
|||
return GestureDetector( |
|||
onTap: () => controller.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( |
|||
label, |
|||
style: TextStyle( |
|||
color: isSelected |
|||
? Colors.white |
|||
: (isDarkMode ? Colors.white : Colors.black87), |
|||
fontSize: 14.sp, |
|||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, |
|||
), |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -1,80 +0,0 @@ |
|||
import 'package:get/get.dart'; |
|||
//import 'agent_controller.dart'; // 确保导入Message模型 |
|||
import 'package:flutter/material.dart'; |
|||
import '../../../data/services/network/api.dart'; |
|||
import '../../../data/models/message_model.dart'; |
|||
|
|||
class ReportController extends GetxController { |
|||
// 当前举报的消息 |
|||
final reportingMessage = Rxn<Message>(); |
|||
|
|||
// 选择的举报原因 |
|||
List<String> selectedReasons = []; |
|||
|
|||
// 举报描述 |
|||
final reportDescription = ''.obs; |
|||
|
|||
// 初始化举报状态 |
|||
void initReport(Message message) { |
|||
reportingMessage.value = message; |
|||
selectedReasons.clear(); |
|||
reportDescription.value = ''; |
|||
} |
|||
|
|||
// 切换举报原因选择状态 |
|||
void toggleReason(String reason) { |
|||
if (selectedReasons.contains(reason)) { |
|||
selectedReasons.remove(reason); |
|||
} else { |
|||
selectedReasons.add(reason); |
|||
} |
|||
update(); |
|||
} |
|||
|
|||
// 更新举报描述 |
|||
void updateDescription(String text) { |
|||
reportDescription.value = text; |
|||
} |
|||
|
|||
// 提交举报 |
|||
Future<void> submitReport() async { |
|||
if (reportingMessage.value == null) return; |
|||
|
|||
// 构建举报数据 |
|||
final reportData = { |
|||
'describe': reportDescription.value, |
|||
// 将Message对象转换为可序列化的Map |
|||
'message': [], |
|||
// 同样处理reportContext |
|||
'reportContext': reportingMessage.value!.text, |
|||
|
|||
'reporttype': selectedReasons.toString(), |
|||
}; |
|||
|
|||
print('提交举报数据: ${reportData}'); |
|||
|
|||
//调用API提交举报: |
|||
var response = await Api.reportAiContent(reportData); |
|||
|
|||
if (response != null) { |
|||
print('提交举报响应: $response'); |
|||
} |
|||
|
|||
// 提交成功后重置状态 |
|||
selectedReasons.clear(); |
|||
reportDescription.value = ''; |
|||
reportingMessage.value = null; |
|||
|
|||
// 关闭举报表单 |
|||
Navigator.of(Get.context!).pop(); |
|||
|
|||
// 显示成功通知 |
|||
Get.snackbar( |
|||
'提交成功', |
|||
'举报已提交,我们会尽快处理', |
|||
backgroundColor: Colors.green.withOpacity(0.8), |
|||
colorText: Colors.white, |
|||
duration: const Duration(seconds: 2), |
|||
); |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,143 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import '../controllers/agent_controller.dart'; |
|||
|
|||
/// 声音柱动画组件 |
|||
/// 用于显示语音输入时的音频波形效果 |
|||
class SoundBarWidget extends StatefulWidget { |
|||
final double delay; |
|||
final bool isActive; |
|||
|
|||
const SoundBarWidget({ |
|||
Key? key, |
|||
required this.delay, |
|||
required this.isActive, |
|||
}) : super(key: key); |
|||
|
|||
@override |
|||
State<SoundBarWidget> createState() => _SoundBarWidgetState(); |
|||
} |
|||
|
|||
class _SoundBarWidgetState extends State<SoundBarWidget> |
|||
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(SoundBarWidget 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), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
|
|||
/// 声音柱组合组件 |
|||
/// 包含多个声音柱的组合显示 |
|||
class SoundBarsWidget extends StatelessWidget { |
|||
final bool isActive; |
|||
final int barCount; |
|||
|
|||
const SoundBarsWidget({ |
|||
Key? key, |
|||
required this.isActive, |
|||
this.barCount = 10, |
|||
}) : super(key: key); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Row( |
|||
mainAxisAlignment: MainAxisAlignment.start, |
|||
children: List.generate(barCount, (index) { |
|||
return Padding( |
|||
padding: const EdgeInsets.symmetric(horizontal: 1), |
|||
child: SoundBarWidget( |
|||
delay: index * 0.1, |
|||
isActive: isActive, |
|||
), |
|||
); |
|||
}), |
|||
); |
|||
} |
|||
} |
|||
|
|||
/// 带控制器集成的声音柱组件 |
|||
/// 自动从AgentController获取状态 |
|||
class AgentSoundBarsWidget extends StatelessWidget { |
|||
final int barCount; |
|||
|
|||
const AgentSoundBarsWidget({ |
|||
Key? key, |
|||
this.barCount = 10, |
|||
}) : super(key: key); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
return Obx(() { |
|||
final controller = Get.find<AgentController>(); |
|||
return SoundBarsWidget( |
|||
isActive: controller.isListening.value, |
|||
barCount: barCount, |
|||
); |
|||
}); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue