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.

508 lines
16 KiB

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/mobile_elf_controller.dart';
class MobileElfView extends GetView<MobileElfController> {
const MobileElfView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final bool isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDark ? Colors.black : const Color(0xFFF9FAFC),
appBar: _buildAppBar(isDark),
body: Obx(() {
final err = controller.credentialError.value;
if (err.isNotEmpty) {
return Center(
child: Padding(
padding: EdgeInsets.all(24.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.error_outline, size: 48.sp, color: Colors.red),
SizedBox(height: 12.h),
Text(err,
textAlign: TextAlign.center,
style: TextStyle(fontSize: 13.sp, color: Colors.red)),
SizedBox(height: 16.h),
ElevatedButton(
onPressed: controller.retryCredentials,
child: const Text('重试'),
),
],
),
),
);
}
return Column(
children: [
_buildStatusBanner(),
Expanded(child: _buildMessageList(isDark)),
_buildBottomBar(isDark),
],
);
}),
);
}
PreferredSizeWidget _buildAppBar(bool isDark) {
return AppBar(
backgroundColor: isDark ? const Color(0xFF1A1A1A) : Colors.white,
elevation: 0.5,
leading: IconButton(
icon: Icon(Icons.arrow_back_ios,
size: 18.sp,
color: isDark ? Colors.white : Colors.grey[800]),
onPressed: () => Get.back(),
),
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF0091EA), Color(0xFF00BFA5)],
),
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(Icons.cell_tower, color: Colors.white, size: 18.sp),
),
SizedBox(width: 8.w),
Text(
'移动精灵',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.grey[800],
),
),
],
),
actions: [
Obx(() => Padding(
padding: EdgeInsets.only(right: 16.w),
child: _buildStatusDot(),
)),
],
);
}
Widget _buildStatusDot() {
final color = _statusColor(controller.status.value);
return Row(
children: [
Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(color: color.withOpacity(0.4), blurRadius: 4)
],
),
),
SizedBox(width: 4.w),
Text(
_statusLabel(controller.status.value),
style: TextStyle(fontSize: 11.sp, color: color),
),
],
);
}
Widget _buildStatusBanner() {
return Obx(() {
final s = controller.status.value;
if (s == MobileElfStatus.idle) return const SizedBox.shrink();
final color = _statusColor(s);
final label = _statusBannerLabel(s);
return Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 6.h),
color: color.withOpacity(0.12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (s == MobileElfStatus.thinking || s == MobileElfStatus.connecting)
SizedBox(
width: 12.w,
height: 12.w,
child: CircularProgressIndicator(
strokeWidth: 1.5,
color: color,
),
),
if (s == MobileElfStatus.recording)
Icon(Icons.mic, size: 14.sp, color: color),
if (s == MobileElfStatus.speaking)
Icon(Icons.volume_up, size: 14.sp, color: color),
if (s == MobileElfStatus.error)
Icon(Icons.error_outline, size: 14.sp, color: color),
SizedBox(width: 6.w),
Text(label,
style: TextStyle(
fontSize: 12.sp,
color: color,
fontWeight: FontWeight.w500)),
],
),
);
});
}
Widget _buildMessageList(bool isDark) {
return Obx(() {
if (controller.messages.isEmpty) {
return _buildEmptyHint(isDark);
}
return ListView.builder(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
reverse: false,
itemCount: controller.messages.length,
itemBuilder: (_, i) => _buildBubble(controller.messages[i], isDark),
);
});
}
Widget _buildEmptyHint(bool isDark) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 72.w,
height: 72.w,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF0091EA), Color(0xFF00BFA5)],
),
shape: BoxShape.circle,
),
child: Icon(Icons.cell_tower, color: Colors.white, size: 36.sp),
),
SizedBox(height: 16.h),
Text(
'移动精灵',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.grey[800],
),
),
SizedBox(height: 8.h),
Text(
'按住麦克风按钮开始语音对话',
style: TextStyle(
fontSize: 13.sp,
color: isDark ? Colors.grey[400] : Colors.grey[500],
),
),
],
),
);
}
Widget _buildBubble(MobileElfMessage msg, bool isDark) {
final isUser = msg.isUser;
return Padding(
padding: EdgeInsets.only(bottom: 12.h),
child: Row(
mainAxisAlignment:
isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (!isUser) ...[
_buildAvatar(isUser: false),
SizedBox(width: 8.w),
],
Flexible(
child: Container(
padding:
EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.h),
decoration: BoxDecoration(
color: isUser
? const Color(0xFF0091EA)
: (isDark
? const Color(0xFF2A2A2A)
: Colors.white),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.r),
topRight: Radius.circular(16.r),
bottomLeft:
isUser ? Radius.circular(16.r) : Radius.circular(4.r),
bottomRight:
isUser ? Radius.circular(4.r) : Radius.circular(16.r),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.06),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
msg.text,
style: TextStyle(
fontSize: 14.sp,
color: isUser
? Colors.white
: (isDark ? Colors.white : Colors.grey[800]),
height: 1.4,
),
),
if (!isUser && msg.ttsUrl != null) ...[
SizedBox(height: 6.h),
GestureDetector(
onTap: () => controller.playTts(msg.ttsUrl!),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.play_circle_outline,
size: 16.sp,
color: const Color(0xFF0091EA),
),
SizedBox(width: 4.w),
Text(
'重播语音',
style: TextStyle(
fontSize: 11.sp,
color: const Color(0xFF0091EA),
),
),
],
),
),
],
],
),
),
),
if (isUser) ...[
SizedBox(width: 8.w),
_buildAvatar(isUser: true),
],
],
),
);
}
Widget _buildAvatar({required bool isUser}) {
return Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: isUser
? const Color(0xFF0091EA).withOpacity(0.15)
: const Color(0xFF00BFA5).withOpacity(0.15),
shape: BoxShape.circle,
),
child: Icon(
isUser ? Icons.person : Icons.cell_tower,
size: 16.sp,
color: isUser ? const Color(0xFF0091EA) : const Color(0xFF00BFA5),
),
);
}
Widget _buildBottomBar(bool isDark) {
return Container(
padding: EdgeInsets.fromLTRB(16.w, 8.h, 16.w, 24.h),
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1A1A1A) : Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 实时识别字幕
Obx(() {
final text = controller.realtimeText.value;
if (text.isEmpty) return const SizedBox.shrink();
return Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: Text(
text,
style: TextStyle(
fontSize: 13.sp,
color: isDark ? Colors.grey[400] : Colors.grey[500],
fontStyle: FontStyle.italic,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
);
}),
// 错误提示
Obx(() {
final err = controller.errorMsg.value;
if (err.isEmpty) return const SizedBox.shrink();
return Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: Text(
err,
style: TextStyle(
fontSize: 12.sp, color: Colors.red[400]),
textAlign: TextAlign.center,
),
);
}),
// 麦克风按钮
Obx(() => _buildMicButton(controller.status.value)),
],
),
);
}
Widget _buildMicButton(MobileElfStatus status) {
final bool isThinking = status == MobileElfStatus.thinking ||
status == MobileElfStatus.connecting;
final Color btnColor = _statusColor(status);
final IconData icon = _micIcon(status);
final String label = _micLabel(status);
return Column(
children: [
GestureDetector(
onTap: isThinking ? null : controller.onMicTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 68.w,
height: 68.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isThinking
? Colors.grey.withOpacity(0.2)
: btnColor.withOpacity(0.15),
border: Border.all(
color: isThinking ? Colors.grey.withOpacity(0.3) : btnColor,
width: 2,
),
),
child: isThinking
? Center(
child: SizedBox(
width: 24.w,
height: 24.w,
child: CircularProgressIndicator(
strokeWidth: 2.5, color: Colors.grey),
),
)
: Icon(icon, size: 30.sp, color: btnColor),
),
),
SizedBox(height: 6.h),
Text(
label,
style: TextStyle(
fontSize: 11.sp,
color: isThinking ? Colors.grey : btnColor,
),
),
],
);
}
// ── 辅助 ──────────────────────────────────────────────────────────────────
Color _statusColor(MobileElfStatus s) {
switch (s) {
case MobileElfStatus.connecting:
return Colors.orange;
case MobileElfStatus.recording:
return const Color(0xFF0066CC);
case MobileElfStatus.thinking:
return Colors.orange;
case MobileElfStatus.speaking:
return const Color(0xFF4CAF50);
case MobileElfStatus.error:
return Colors.red;
case MobileElfStatus.idle:
return const Color(0xFF0091EA);
}
}
String _statusLabel(MobileElfStatus s) {
switch (s) {
case MobileElfStatus.connecting:
return '连接中';
case MobileElfStatus.recording:
return '录音中';
case MobileElfStatus.thinking:
return '思考中';
case MobileElfStatus.speaking:
return '播报中';
case MobileElfStatus.error:
return '错误';
case MobileElfStatus.idle:
return '就绪';
}
}
String _statusBannerLabel(MobileElfStatus s) {
switch (s) {
case MobileElfStatus.connecting:
return '正在连接移动精灵...';
case MobileElfStatus.recording:
return '正在聆听,请说话';
case MobileElfStatus.thinking:
return 'AI 思考中,请稍候';
case MobileElfStatus.speaking:
return '正在播报回答';
case MobileElfStatus.error:
return controller.errorMsg.value;
case MobileElfStatus.idle:
return '';
}
}
IconData _micIcon(MobileElfStatus s) {
switch (s) {
case MobileElfStatus.recording:
return Icons.stop_circle_outlined;
case MobileElfStatus.speaking:
return Icons.stop_circle_outlined;
case MobileElfStatus.error:
return Icons.refresh;
default:
return Icons.mic;
}
}
String _micLabel(MobileElfStatus s) {
switch (s) {
case MobileElfStatus.recording:
return '点击停止';
case MobileElfStatus.speaking:
return '点击停止播报';
case MobileElfStatus.error:
return '重试';
case MobileElfStatus.thinking:
return '思考中...';
case MobileElfStatus.connecting:
return '连接中...';
default:
return '点击说话';
}
}
}