You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
915 lines
34 KiB
915 lines
34 KiB
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_intro/flutter_intro.dart';
|
|
import 'package:get/get.dart';
|
|
import '../../../core/widgets/intro_overlay_builder.dart';
|
|
import '../../../data/models/channel_app_model.dart';
|
|
import '../../../data/services/version_update_service.dart';
|
|
import '../controllers/agent_controller.dart';
|
|
import '../controllers/message_action_controller.dart';
|
|
import 'message_bubble.dart';
|
|
import '../widgets/sound_bar_widget.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
|
|
class AgentView extends StatefulWidget {
|
|
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;
|
|
|
|
void _showTtsSpeedDialog() {
|
|
final primaryColor = Theme.of(context).primaryColor;
|
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
|
final RxInt tempRate = controller.ttsRatePercent.value.obs;
|
|
|
|
Get.dialog(
|
|
AlertDialog(
|
|
backgroundColor: isDarkMode ? Colors.grey[850] : Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12.w),
|
|
),
|
|
title: Text(
|
|
'ttsSpeed'.tr, // 语速
|
|
style: TextStyle(fontSize: 16.sp),
|
|
),
|
|
content: Obx(() {
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
'ttsSpeedDefaultHint'.tr, // 默认 +25%
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 12.h),
|
|
Text(
|
|
'+${tempRate.value}%',
|
|
style: TextStyle(
|
|
fontSize: 20.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: primaryColor,
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Slider(
|
|
value: tempRate.value.toDouble(),
|
|
min: 0,
|
|
max: 100,
|
|
divisions: 20,
|
|
label: '+${tempRate.value}%',
|
|
activeColor: primaryColor,
|
|
onChanged: (value) {
|
|
tempRate.value = value.round();
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Get.back(),
|
|
child: Text('cancel'.tr),
|
|
),
|
|
TextButton(
|
|
onPressed: () async {
|
|
Get.back();
|
|
await controller.updateTtsRate(tempRate.value);
|
|
},
|
|
child: Text('confirm'.tr),
|
|
),
|
|
],
|
|
),
|
|
barrierDismissible: true,
|
|
);
|
|
}
|
|
|
|
@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
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final primaryColor = theme.primaryColor;
|
|
final backgroundColor = theme.scaffoldBackgroundColor;
|
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
|
final isOfficialChannel = Get.isRegistered<VersionUpdateService>() &&
|
|
VersionUpdateService.to.cachedChannel == UserChannel.OFFICIAL;
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
elevation: 1,
|
|
backgroundColor: backgroundColor,
|
|
leading: IconButton(
|
|
icon: Icon(Icons.arrow_back_ios,
|
|
color: isDarkMode ? Colors.white : Colors.black87, size: 20.sp),
|
|
onPressed: () => Get.back(),
|
|
),
|
|
titleSpacing: 0,
|
|
title: Row(
|
|
children: [
|
|
Container(
|
|
width: 40.w,
|
|
height: 40.w,
|
|
decoration: BoxDecoration(
|
|
color: primaryColor,
|
|
borderRadius: BorderRadius.circular(20.w),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.1),
|
|
blurRadius: 3.w,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
child: Icon(
|
|
Icons.support_agent,
|
|
color: Colors.white,
|
|
size: 22.sp,
|
|
),
|
|
),
|
|
SizedBox(width: 12.w),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
// 机器人名字(品牌名,不做多语言翻译)
|
|
'Emai',
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: isDarkMode ? Colors.white : Colors.black87,
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 6.w,
|
|
height: 6.w,
|
|
decoration: const BoxDecoration(
|
|
color: Colors.green,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
SizedBox(width: 4.w),
|
|
Text(
|
|
'online'.tr,
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
color: Colors.green,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
// Obx(() {
|
|
// final vipexptime = controller.vipExpiryTime.value;
|
|
// DateTime? expiry;
|
|
// if (vipexptime > 0) {
|
|
// expiry = vipexptime > 1000000000000
|
|
// ? DateTime.fromMillisecondsSinceEpoch(vipexptime)
|
|
// : DateTime.fromMillisecondsSinceEpoch(
|
|
// vipexptime * 1000);
|
|
// }
|
|
// final text = expiry != null
|
|
// ? '${'expiryLabel'.tr}${DateFormat('yyyy-MM-dd').format(expiry)}'
|
|
// : 'notActivated'.tr;
|
|
// return Text(
|
|
// text,
|
|
// style: TextStyle(
|
|
// fontSize: 12.sp,
|
|
// color: Colors.green,
|
|
// fontWeight: FontWeight.w500,
|
|
// ),
|
|
// );
|
|
// }),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
// TTS开关
|
|
Obx(() => IconButton(
|
|
icon: Icon(
|
|
controller.isTtsEnabled.value
|
|
? Icons.volume_up
|
|
: Icons.volume_off,
|
|
size: 24.sp,
|
|
color: controller.isTtsEnabled.value
|
|
? primaryColor
|
|
: isDarkMode
|
|
? Colors.grey[400]
|
|
: Colors.grey,
|
|
),
|
|
onPressed: () => controller.toggleTts(),
|
|
)),
|
|
|
|
// 更多选项
|
|
Container(
|
|
width: 34.w,
|
|
height: 34.w,
|
|
margin: EdgeInsets.only(right: 8.w),
|
|
decoration: BoxDecoration(
|
|
color: isDarkMode
|
|
? Colors.white.withValues(alpha: 0.1)
|
|
: Colors.grey[100],
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
if (!isDarkMode)
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.05),
|
|
blurRadius: 2.w,
|
|
spreadRadius: 0.5.w,
|
|
),
|
|
],
|
|
),
|
|
child: PopupMenuButton(
|
|
icon: Icon(Icons.more_vert,
|
|
color: isDarkMode ? Colors.white : Colors.black87,
|
|
size: 20.sp),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12.w),
|
|
),
|
|
color: isDarkMode ? Colors.grey[800] : Colors.white,
|
|
itemBuilder: (context) => [
|
|
if (isOfficialChannel)
|
|
PopupMenuItem(
|
|
child: Obx(() => Row(
|
|
children: [
|
|
Icon(
|
|
controller.isShowToolInterruptEnabled.value
|
|
? Icons.build_circle
|
|
: Icons.build_circle_outlined,
|
|
color: controller.isShowToolInterruptEnabled.value
|
|
? primaryColor
|
|
: (isDarkMode
|
|
? Colors.grey[400]
|
|
: Colors.grey),
|
|
size: 20.sp,
|
|
),
|
|
SizedBox(width: 8.w),
|
|
Text(
|
|
"显示工具",
|
|
style: TextStyle(fontSize: 14.sp),
|
|
),
|
|
const Spacer(),
|
|
Switch(
|
|
value:
|
|
controller.isShowToolInterruptEnabled.value,
|
|
onChanged: (value) =>
|
|
controller.toggleShowAIToolInterrupt(),
|
|
activeColor: primaryColor,
|
|
materialTapTargetSize:
|
|
MaterialTapTargetSize.shrinkWrap,
|
|
),
|
|
],
|
|
)),
|
|
onTap: () => controller.toggleTtsInterrupt(),
|
|
),
|
|
|
|
// 播报打断开关选项
|
|
PopupMenuItem(
|
|
child: Obx(() => Row(
|
|
children: [
|
|
Icon(
|
|
controller.isTtsInterruptEnabled.value
|
|
? Icons.voice_over_off
|
|
: Icons.voice_over_off_outlined,
|
|
color: controller.isTtsInterruptEnabled.value
|
|
? primaryColor
|
|
: (isDarkMode ? Colors.grey[400] : Colors.grey),
|
|
size: 20.sp,
|
|
),
|
|
SizedBox(width: 8.w),
|
|
Text(
|
|
//'ttsInterruptToggle'.tr,
|
|
"voice_interrupt".tr, // 语音打断
|
|
style: TextStyle(fontSize: 14.sp),
|
|
),
|
|
const Spacer(),
|
|
Switch(
|
|
value: controller.isTtsInterruptEnabled.value,
|
|
onChanged: (value) =>
|
|
controller.toggleTtsInterrupt(),
|
|
activeColor: primaryColor,
|
|
materialTapTargetSize:
|
|
MaterialTapTargetSize.shrinkWrap,
|
|
),
|
|
],
|
|
)),
|
|
onTap: () => controller.toggleTtsInterrupt(),
|
|
),
|
|
|
|
PopupMenuItem(
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.speed, color: primaryColor, size: 20.sp),
|
|
SizedBox(width: 8.w),
|
|
Text('ttsSpeed'.tr, // 语速
|
|
style: TextStyle(fontSize: 14.sp)),
|
|
],
|
|
),
|
|
onTap: () => Future.delayed(
|
|
const Duration(milliseconds: 100),
|
|
() => _showTtsSpeedDialog(),
|
|
),
|
|
),
|
|
|
|
// 清除聊天记录选项
|
|
PopupMenuItem(
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.delete_outline,
|
|
color: primaryColor, size: 20.sp),
|
|
SizedBox(width: 8.w),
|
|
Text('clearChatHistory'.tr,
|
|
style: TextStyle(fontSize: 14.sp)),
|
|
],
|
|
),
|
|
onTap: () => Future.delayed(
|
|
const Duration(milliseconds: 100),
|
|
() => controller.clearChat(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
body: SafeArea(
|
|
top: false,
|
|
child: Stack(
|
|
children: [
|
|
IntroStepBuilder(
|
|
group: 'ai',
|
|
order: 3,
|
|
getOverlayPosition: IntroOverlayBuilder.overlayPosition,
|
|
overlayBuilder: (params) {
|
|
return Obx(
|
|
() => IntroOverlayBuilder.homeBuilder(
|
|
params,
|
|
false,
|
|
title: controller.newAIReplyCompleted.value
|
|
? 'aiReplyCompleted'.tr
|
|
: 'aiThinking'.tr,
|
|
onNext: controller.newAIReplyCompleted.value ? () {} : null,
|
|
),
|
|
);
|
|
},
|
|
builder: (BuildContext context, GlobalKey key) => SizedBox(
|
|
key: key,
|
|
),
|
|
),
|
|
// 主要聊天界面(占满整个 body,底部按钮浮在其上)
|
|
GestureDetector(
|
|
onTap: () {
|
|
// 点击聊天区域时,收起键盘并切换工具显示状态
|
|
FocusScope.of(context).unfocus();
|
|
if (controller.isPlusOptionsExpanded.value) {
|
|
controller.collapsePlusOptions();
|
|
}
|
|
},
|
|
child: Obx(() {
|
|
return Align(
|
|
alignment: Alignment.topCenter,
|
|
child: ListView.builder(
|
|
controller: controller.scrollController,
|
|
shrinkWrap: true,
|
|
reverse: true,
|
|
padding: EdgeInsets.only(
|
|
top: 16.h,
|
|
// 预留浮动通话/挂断按钮的高度,避免最后一条消息被遮挡
|
|
bottom: 140.h,
|
|
left: 16.w,
|
|
right: 16.w,
|
|
),
|
|
itemCount: controller.messages.length +
|
|
(controller.hasMoreMessages.value ? 1 : 0),
|
|
itemBuilder: (context, index) {
|
|
// 如果是最后一项且有更多消息,显示加载指示器
|
|
if (index == controller.messages.length &&
|
|
controller.hasMoreMessages.value) {
|
|
return _buildLoadMoreIndicator();
|
|
}
|
|
|
|
final message = controller
|
|
.messages[controller.messages.length - 1 - index];
|
|
return index == 0
|
|
? IntroStepBuilder(
|
|
group: 'ai',
|
|
order: 4,
|
|
getOverlayPosition:
|
|
IntroOverlayBuilder.overlayPosition,
|
|
overlayBuilder: (params) {
|
|
return IntroOverlayBuilder.aiBuilder(
|
|
params,
|
|
'${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['endKey']}'
|
|
.tr,
|
|
onFinish: () async {
|
|
if (controller.noviceGuideService.aiStep <
|
|
controller.noviceGuideService
|
|
.aiStepsList.length -
|
|
1) {
|
|
await Future.delayed(
|
|
const Duration(milliseconds: 300));
|
|
controller.noviceGuideService.aiStep++;
|
|
Intro.of(Get.context!)
|
|
.start(group: 'ai', reset: true);
|
|
}
|
|
},
|
|
onTap: () async {
|
|
params.onFinish();
|
|
if (controller.noviceGuideService.aiStep <
|
|
controller.noviceGuideService
|
|
.aiStepsList.length -
|
|
1) {
|
|
await Future.delayed(
|
|
const Duration(milliseconds: 300));
|
|
controller.noviceGuideService.aiStep++;
|
|
Intro.of(Get.context!)
|
|
.start(group: 'ai', reset: true);
|
|
}
|
|
},
|
|
);
|
|
},
|
|
builder: (BuildContext context, GlobalKey key) =>
|
|
Padding(
|
|
key: key,
|
|
padding: EdgeInsets.only(bottom: 8.h),
|
|
child: GestureDetector(
|
|
onLongPressStart:
|
|
(LongPressStartDetails details) {
|
|
messageActionController.onLongPressMessage(
|
|
context, isDarkMode, details, message);
|
|
},
|
|
child: MessageBubble(
|
|
sessionid: message.sessionid,
|
|
isUser: message.isUser,
|
|
message: message.text,
|
|
timestamp: message.timestamp,
|
|
isRecognizing: message.isRecognizing,
|
|
hasImage: message.hasImage,
|
|
imagePath: message.imagePath,
|
|
meta: message.meta,
|
|
functionCalls: message.functionCalls,
|
|
isToolCallsExpanded:
|
|
message.isToolCallsExpanded,
|
|
),
|
|
),
|
|
),
|
|
)
|
|
: Padding(
|
|
padding: EdgeInsets.only(bottom: 8.h),
|
|
child: GestureDetector(
|
|
onLongPressStart:
|
|
(LongPressStartDetails details) {
|
|
messageActionController.onLongPressMessage(
|
|
context, isDarkMode, details, message);
|
|
},
|
|
child: MessageBubble(
|
|
sessionid: message.sessionid,
|
|
isUser: message.isUser,
|
|
message: message.text,
|
|
timestamp: message.timestamp,
|
|
isRecognizing: message.isRecognizing,
|
|
hasImage: message.hasImage,
|
|
imagePath: message.imagePath,
|
|
meta: message.meta,
|
|
functionCalls: message.functionCalls,
|
|
isToolCallsExpanded:
|
|
message.isToolCallsExpanded,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
// 底部输入栏:仿微信「键盘/语音」双模式切换,只保留打字与语音输入
|
|
Align(
|
|
alignment: Alignment.bottomCenter,
|
|
child: Obx(() {
|
|
if (controller.isDeviceCallMode.value) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
return _buildInputBar(context, isDarkMode);
|
|
}),
|
|
),
|
|
|
|
// 打电话模式覆盖层
|
|
Obx(() {
|
|
if (controller.isPhoneCallMode.value) {
|
|
return _buildPhoneCallOverlay(context, isDarkMode);
|
|
}
|
|
return const SizedBox.shrink();
|
|
}),
|
|
|
|
// 设备通话模式覆盖层
|
|
Obx(() {
|
|
if (controller.isDeviceCallMode.value) {
|
|
return _buildDeviceCallOverlay(context, isDarkMode);
|
|
}
|
|
return const SizedBox.shrink();
|
|
}),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 底部输入栏:仿微信的「键盘 / 语音」双模式。
|
|
// 左侧圆形按钮切换模式:文本模式显示麦克风图标(点它切到语音),
|
|
// 语音模式显示键盘图标(点它切回打字)。中间随模式变成输入框或「按住 说话」。
|
|
// 只做打字 + 语音输入两件事,不含表情/图片/更多面板。
|
|
Widget _buildInputBar(BuildContext context, bool isDarkMode) {
|
|
final Color barBg = isDarkMode ? const Color(0xFF1C1C1E) : Colors.white;
|
|
final Color fieldBg =
|
|
isDarkMode ? const Color(0xFF2C2C2E) : const Color(0xFFF2F3F5);
|
|
final Color iconColor = isDarkMode ? Colors.white70 : Colors.black54;
|
|
final Color textColor = isDarkMode ? Colors.white : Colors.black87;
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: barBg,
|
|
border: Border(
|
|
top: BorderSide(
|
|
color:
|
|
isDarkMode ? const Color(0xFF2C2C2E) : const Color(0xFFE5E5E5),
|
|
width: 0.5,
|
|
),
|
|
),
|
|
),
|
|
// 外层 body 已经包了 SafeArea(top:false),这里不再重复处理底部安全区
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
// 模式切换按钮
|
|
GestureDetector(
|
|
onTap: controller.toggleInputMode,
|
|
behavior: HitTestBehavior.opaque,
|
|
child: Padding(
|
|
padding: EdgeInsets.only(right: 8.w, bottom: 4.h),
|
|
child: Icon(
|
|
controller.isTextInputMode.value
|
|
? Icons.keyboard_voice_outlined
|
|
: Icons.keyboard_outlined,
|
|
size: 28.sp,
|
|
color: iconColor,
|
|
),
|
|
),
|
|
),
|
|
// 中间:输入框 / 按住说话
|
|
Expanded(
|
|
child: controller.isTextInputMode.value
|
|
? _buildTextField(fieldBg, textColor)
|
|
: _buildHoldToTalkButton(fieldBg, textColor),
|
|
),
|
|
// 发送按钮:仅文本模式且有内容时出现
|
|
if (controller.isTextInputMode.value &&
|
|
controller.hasInputText.value)
|
|
Padding(
|
|
padding: EdgeInsets.only(left: 8.w, bottom: 2.h),
|
|
child: GestureDetector(
|
|
onTap: controller.sendMessage,
|
|
behavior: HitTestBehavior.opaque,
|
|
child: Container(
|
|
width: 40.h,
|
|
height: 40.h,
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF07C160), // 微信绿
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.arrow_upward_rounded,
|
|
size: 22.sp,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTextField(Color fieldBg, Color textColor) {
|
|
return Container(
|
|
constraints: BoxConstraints(minHeight: 40.h, maxHeight: 120.h),
|
|
decoration: BoxDecoration(
|
|
color: fieldBg,
|
|
borderRadius: BorderRadius.circular(6.r),
|
|
),
|
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.h),
|
|
child: TextField(
|
|
controller: controller.textController,
|
|
style: TextStyle(fontSize: 15.sp, color: textColor),
|
|
minLines: 1,
|
|
maxLines: 4,
|
|
textInputAction: TextInputAction.send,
|
|
onSubmitted: (_) => controller.sendMessage(),
|
|
decoration: const InputDecoration(
|
|
isDense: true,
|
|
border: InputBorder.none,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 按住说话:按下开始识别、松开结束。识别结果由 AgentService 事件回流成消息,
|
|
/// 与原来语音链路一致,这里只负责手势与视觉反馈。
|
|
Widget _buildHoldToTalkButton(Color fieldBg, Color textColor) {
|
|
return GestureDetector(
|
|
onLongPressStart: (_) => controller.startVoiceInput(),
|
|
onLongPressEnd: (_) => controller.stopVoiceInput(),
|
|
onLongPressCancel: () => controller.stopVoiceInput(),
|
|
behavior: HitTestBehavior.opaque,
|
|
child: Obx(() {
|
|
final talking = controller.isListening.value;
|
|
return Container(
|
|
height: 40.h,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: talking ? const Color(0xFFD9D9D9) : fieldBg,
|
|
borderRadius: BorderRadius.circular(6.r),
|
|
),
|
|
child: Text(
|
|
talking ? 'releaseToSend'.tr : 'holdToTalk'.tr,
|
|
style: TextStyle(
|
|
fontSize: 15.sp,
|
|
fontWeight: FontWeight.w500,
|
|
color: talking ? Colors.black87 : textColor,
|
|
),
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
}
|
|
|
|
// 构建打电话模式覆盖层
|
|
Widget _buildPhoneCallOverlay(BuildContext context, bool isDarkMode) {
|
|
return Container(
|
|
color: Colors.transparent, // 设置整个覆盖层为透明
|
|
child: Column(
|
|
children: [
|
|
// 中间空白区域 - 完全透明
|
|
const Expanded(
|
|
child: SizedBox(), // 空的占位符,完全透明
|
|
),
|
|
|
|
// 底部控制按钮 - 包含音频波纹
|
|
Container(
|
|
color: isDarkMode ? Colors.black : Colors.white,
|
|
child: _buildPhoneCallControls(isDarkMode),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// 构建打电话模式控制按钮
|
|
Widget _buildPhoneCallControls(bool isDarkMode) {
|
|
return Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 32.w, vertical: 24.h),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
crossAxisAlignment: CrossAxisAlignment.center, // 确保垂直居中对齐
|
|
children: [
|
|
// 录音控制按钮(左侧) - 控制录音输入功能
|
|
Obx(() => _buildPhoneControlButton(
|
|
icon: controller.isListening.value
|
|
? Icons.mic // 录音中显示麦克风图标
|
|
: Icons.mic_off, // 未录音显示麦克风关闭图标
|
|
onTap: () => controller.togglePhoneCallRecording(),
|
|
isDarkMode: isDarkMode,
|
|
isActive: controller.isListening.value,
|
|
// backgroundColor: controller.isListening.value
|
|
// ? Colors.red // 录音中显示红色背景
|
|
// : Theme.of(context).primaryColor, // 未录音显示主题色
|
|
)),
|
|
|
|
// 音频波纹组件(中间) - 仅显示动态波形效果,无其他UI元素
|
|
Container(
|
|
width: 120.w, // 设置固定宽度
|
|
height: 64.h, // 与按钮高度一致
|
|
alignment: Alignment.center, // 确保波纹居中
|
|
child: const AgentSoundBarsWidget(barCount: 12), // 适应宽度的波纹条数
|
|
),
|
|
|
|
// 关闭打电话模式按钮(右侧) - 退出打电话模式
|
|
_buildPhoneControlButton(
|
|
icon: Icons.close,
|
|
onTap: () => controller.endPhoneCall(),
|
|
isDarkMode: isDarkMode,
|
|
isActive: false,
|
|
//backgroundColor: Colors.red,
|
|
iconColor: Colors.redAccent,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// 构建打电话控制按钮
|
|
Widget _buildPhoneControlButton({
|
|
required IconData icon,
|
|
required VoidCallback onTap,
|
|
required bool isDarkMode,
|
|
required bool isActive,
|
|
Color? backgroundColor,
|
|
Color? iconColor,
|
|
}) {
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Container(
|
|
width: 64.w, // 按钮尺寸
|
|
height: 64.h,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: backgroundColor ??
|
|
(isDarkMode ? Colors.grey[800] : Colors.grey[250]),
|
|
border: Border.all(color: Colors.blue, width: 2.w),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: (backgroundColor ?? Colors.grey).withValues(alpha: 0.3),
|
|
blurRadius: 8.w,
|
|
spreadRadius: 1.w,
|
|
offset: Offset(0, 2.h),
|
|
),
|
|
],
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
color: iconColor ?? (isDarkMode ? Colors.white : Colors.black),
|
|
size: 28.sp, // 图标尺寸
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ─── 设备通话覆盖层(接听后仅一个挂断按钮,悬浮在聊天界面之上)─────────────────────
|
|
Widget _buildDeviceCallOverlay(BuildContext context, bool isDarkMode) {
|
|
return Align(
|
|
alignment: Alignment.bottomCenter,
|
|
child: Padding(
|
|
padding: EdgeInsets.only(bottom: 16.h),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
GestureDetector(
|
|
onTap: () => controller.endDeviceCall(),
|
|
child: Container(
|
|
width: 68.w,
|
|
height: 68.w,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFEF4444),
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFFEF4444).withValues(alpha: 0.35),
|
|
blurRadius: 16.w,
|
|
offset: Offset(0, 6.h),
|
|
),
|
|
],
|
|
),
|
|
child: Transform.rotate(
|
|
angle: 2.356,
|
|
child: Icon(
|
|
Icons.call_end,
|
|
color: Colors.white,
|
|
size: 30.sp,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Text(
|
|
'挂断',
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 构建加载更多指示器
|
|
Widget _buildLoadMoreIndicator() {
|
|
return Obx(
|
|
() {
|
|
if (controller.isLoadingMore.value) {
|
|
// 正在加载状态
|
|
return Container(
|
|
padding: EdgeInsets.symmetric(vertical: 16.h),
|
|
child: Center(
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
SizedBox(
|
|
width: 16.w,
|
|
height: 16.h,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2.w,
|
|
),
|
|
),
|
|
SizedBox(width: 8.w),
|
|
Text(
|
|
'loadMoreMessages'.tr,
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
} else if (controller.isLoadingMoreError.value) {
|
|
// 加载失败状态
|
|
return Container(
|
|
padding: EdgeInsets.symmetric(vertical: 16.h),
|
|
child: Center(
|
|
child: GestureDetector(
|
|
onTap: () => controller.loadMoreMessages(),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.refresh,
|
|
size: 16.sp,
|
|
color: Colors.grey,
|
|
),
|
|
SizedBox(width: 8.w),
|
|
Text(
|
|
'loadFailedClickRetry'.tr,
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
} else {
|
|
// 默认状态(有更多消息但未加载)
|
|
return Container(
|
|
padding: EdgeInsets.symmetric(vertical: 8.h),
|
|
child: Center(
|
|
child: Text(
|
|
'swipeUpForMore'.tr,
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|