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.

968 lines
31 KiB

import 'package:flutter/material.dart';
import 'package:flutter_intro/flutter_intro.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../core/widgets/intro_overlay_builder.dart';
import '../controllers/home_controller.dart';
import '../../../routes/app_routes.dart';
import '../../../core/widgets/common_bottom_nav.dart';
// 呼吸灯动画组件
class AnimatedPulsing extends StatefulWidget {
const AnimatedPulsing({Key? key}) : super(key: key);
@override
_AnimatedPulsingState createState() => _AnimatedPulsingState();
}
class _AnimatedPulsingState extends State<AnimatedPulsing>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true);
_animation = Tween<double>(begin: 0.5, end: 0.8).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Opacity(
opacity: _animation.value,
child: child,
);
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.r),
gradient: RadialGradient(
colors: [
Colors.white.withOpacity(0.3),
Colors.transparent,
],
center: Alignment.topRight,
radius: 0.8,
stops: const [0.0, 1.0],
),
),
),
);
}
}
// 首页视图
class HomeView extends GetView<HomeController> {
const HomeView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : const Color(0xFFF9FAFC),
body: SafeArea(
child: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 顶部标题栏
_buildAppBar(isDarkMode),
// 顶部图片
_buildEarbudsImage(),
SizedBox(height: 20.h),
// 设备状态卡片
IntroStepBuilder(
group: 'home',
order: 1,
onWidgetLoad: () {
controller.noviceGuideService.startHomeGuide();
},
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
'欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言',
onNext: () {
controller.noviceGuideService.startWakeupListen();
},
);
},
builder: (BuildContext context, GlobalKey key) => SizedBox(
key: key,
child: _buildDeviceStatusCard(isDarkMode),
),
),
SizedBox(height: 20.h),
IntroStepBuilder(
group: 'home',
order: 2,
getOverlayPosition: ({
required Offset offset,
required Size screenSize,
required Size size,
}) {
return OverlayPosition(
top: 200.h,
width: screenSize.width,
crossAxisAlignment: CrossAxisAlignment.end,
);
},
overlayBuilder: (params) {
return Obx(
() => IntroOverlayBuilder.homeBuilder(
params,
controller.noviceGuideService.isWakeup.value,
onNext: () {
controller.noviceGuideService.closeWakeupListen();
},
),
);
},
builder: (BuildContext context, GlobalKey key) => SizedBox(
key: key,
),
),
// 热门AI体验
_buildSectionTitle(isDarkMode),
SizedBox(height: 16.h),
_buildPremiumAILayout(),
],
),
),
),
bottomNavigationBar: const CommonBottomNav(currentIndex: 0),
);
}
// 顶部标题栏
Widget _buildAppBar(bool isDarkMode) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Voitrans',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.grey[800],
),
),
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: IconButton(
icon: Icon(Icons.settings_outlined,
size: 16.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[600]),
padding: EdgeInsets.zero,
onPressed: () {
Get.toNamed(Routes.settings);
},
),
),
],
);
}
// 耳机图片
Widget _buildEarbudsImage() {
final bool isDarkMode = Get.isDarkMode;
return SizedBox(
height: 220.h,
child: PageView(
children: [
// Center(
// child: ClipRRect(
// // 添加 ClipRRect 来实现圆角
// borderRadius: BorderRadius.circular(16.r), // 使用与其他部件一致的圆角大小
// child: SizedBox(
// width: double.infinity,
// height: 200.h,
// child: Image.asset(
// isDarkMode
// ? 'assets/images/robot_light.png'
// : 'assets/images/robot_light.png',
// fit: BoxFit.fitWidth,
// ),
// ),
// ),
// ),
Center(
child: ClipRRect(
// 添加 ClipRRect
borderRadius: BorderRadius.circular(16.r),
child: Image.asset(
isDarkMode
? 'assets/images/headphone_new3.png'
: 'assets/images/headphone_new4.png',
height: 220.h,
fit: BoxFit.contain,
),
),
),
// Center(
// child: ClipRRect(
// // 添加 ClipRRect
// borderRadius: BorderRadius.circular(16.r),
// child: SizedBox(
// width: double.infinity,
// height: 200.h,
// child: Image.asset(
// isDarkMode
// ? 'assets/images/glasses_light.png'
// : 'assets/images/glasses_light.png',
// fit: BoxFit.fitWidth,
// ),
// ),
// ),
// ),
],
),
);
}
// 设备状态卡片
Widget _buildDeviceStatusCard(bool isDarkMode) {
return Obx(() {
// 使用连接状态作为key,强制在状态改变时重建widget
final isConnected = controller.isFullyConnectedRx.value;
return Container(
key: ValueKey('device_status_$isConnected'),
decoration: BoxDecoration(
color: isDarkMode ? const Color(0xFF1A1A1A) : Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(isDarkMode ? 0.05 : 0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
padding: EdgeInsets.fromLTRB(16.w, 16.h, 16.w, 16.h),
child: Column(
children: [
// 状态标题行
_buildDeviceStatusHeader(isDarkMode),
// 设备信息
SizedBox(height: 12.h),
Obx(() {
// 移除连接失败和连接中的状态判断,直接根据是否完全连接来判断
// 使用响应式访问确保UI更新
return controller.isFullyConnectedRx.value
? _buildConnectedDeviceInfo(isDarkMode) //已连接设备信息
: _buildDisconnectedInfo(isDarkMode); //未连接提示
}),
// 连接按钮(仅在未连接时显示)
SizedBox(height: 12.h),
Obx(() {
// 使用响应式访问确保UI更新
if (!controller.isFullyConnectedRx.value) {
return _buildConnectButton();
} else {
return _buildDeviceButton(isDarkMode);
}
}),
],
),
);
});
}
// 设备状态头部
Widget _buildDeviceStatusHeader(bool isDarkMode) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.headset_outlined,
size: 18.sp,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
),
SizedBox(width: 8.w),
Text(
'deviceStatus'.tr, // 设备状态
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
),
),
],
),
Obx(() => Row(
children: [
Container(
//状态指示灯
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: controller.getConnectionStatusColor(),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: controller
.getConnectionStatusColor()
.withOpacity(0.3),
blurRadius: 4,
spreadRadius: 1,
),
],
),
),
SizedBox(width: 4.w),
Text(
controller.getConnectionStatusText(),
style: TextStyle(
fontSize: 12.sp,
color: controller.getConnectionStatusColor(),
),
),
// 添加刷新按钮
SizedBox(width: 8.w),
InkWell(
onTap: () => controller.refreshDeviceConnection(),
borderRadius: BorderRadius.circular(12.r),
child: Container(
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
shape: BoxShape.circle,
),
child: Icon(
Icons.refresh,
size: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
),
),
],
)),
],
);
}
// 连接错误部分
// 已连接设备信息(新增设备名称 + 信号强度 + 电池信息)
Widget _buildConnectedDeviceInfo(bool isDarkMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'${'connectedTo'.tr}:${controller.getDeviceName()}',
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.grey[300] : Colors.grey[700],
),
overflow: TextOverflow.ellipsis,
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.r),
),
child: Text(
'${'signal'.tr}: ${controller.getSignalStrength()}',
style: TextStyle(
fontSize: 11.sp,
color: Colors.blue[700],
),
),
),
],
),
SizedBox(height: 12.h),
_buildBatteryInfoGrid(isDarkMode),
],
);
}
// 设备管理按钮
Widget _buildDeviceButton(bool isDarkMode) {
return ElevatedButton(
onPressed: () {
Get.toNamed(Routes.devices); // 跳转到设备管理页面
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
minimumSize: Size(double.infinity, 44.h),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.r),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.device_hub, size: 16.sp), // 设备图标
SizedBox(width: 8.w),
Text(
'deviceManagement'.tr,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
// 电池信息网格布局
Widget _buildBatteryInfoGrid(bool isDarkMode) {
return Obx(() {
// 获取各部分电池电量
final leftBattery = controller.leftEarBattery;
final rightBattery = controller.rightEarBattery;
final caseBattery = controller.caseBattery;
// 获取充电状态
final leftEarCharging = controller.leftEarCharging;
final rightEarCharging = controller.rightEarCharging;
final caseCharging = controller.caseCharging;
// 判断是否显示充电盒:充电盒电量大于0且设备已连接
final shouldShowCase =
caseBattery.value > 0 && controller.isFullyConnectedRx.value;
// 使用更紧凑的布局
return Container(
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 4.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.grey[50],
borderRadius: BorderRadius.circular(12.r),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 左耳机
_buildCompactBatteryItem(
label: 'left'.tr, // 左
batteryLevel: leftBattery.value,
isCharging: leftEarCharging.value,
isDarkMode: isDarkMode,
),
// 分隔线
Container(
height: 30.h,
width: 1,
color: isDarkMode ? Colors.grey[800] : Colors.grey[300],
),
// 右耳机
_buildCompactBatteryItem(
label: 'right'.tr, // 右
batteryLevel: rightBattery.value,
isCharging: rightEarCharging.value,
isDarkMode: isDarkMode,
),
// 只有在充电盒电量大于0且设备已连接时才显示充电盒
if (shouldShowCase) ...[
// 分隔线
Container(
height: 30.h,
width: 1,
color: isDarkMode ? Colors.grey[800] : Colors.grey[300],
),
// 充电盒
_buildCompactBatteryItem(
label: '充电盒',
batteryLevel: caseBattery.value,
isCharging: caseCharging.value,
isDarkMode: isDarkMode,
),
]
],
),
);
});
}
// 紧凑型电池项
Widget _buildCompactBatteryItem({
required String label,
required int batteryLevel,
required bool isDarkMode,
bool isCharging = false,
}) {
// 获取电池图标和颜色
IconData batteryIcon;
Color batteryColor;
if (batteryLevel > 75) {
batteryIcon = Icons.battery_full;
batteryColor = Colors.green[600]!;
} else if (batteryLevel > 50) {
batteryIcon = Icons.battery_6_bar;
batteryColor = Colors.green[600]!;
} else if (batteryLevel > 25) {
batteryIcon = Icons.battery_4_bar;
batteryColor = Colors.orange[600]!;
} else if (batteryLevel > 10) {
batteryIcon = Icons.battery_3_bar;
batteryColor = Colors.orange[600]!;
} else {
batteryIcon = Icons.battery_alert;
batteryColor = Colors.red[600]!;
}
bool isBatteryZero = batteryLevel == 0;
return Expanded(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 6.h),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 标签
Text(
label,
style: TextStyle(
fontSize: 11.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontWeight: FontWeight.normal,
),
textAlign: TextAlign.center,
),
SizedBox(height: 4.h),
// 电池指示器 - 当电量为0时,显示横线
isBatteryZero
? Container(
width: 20.w, // 横线宽度
height: 12.sp, // 设置与图标相同的高度,确保对齐
alignment: Alignment.center, // 居中对齐
child: Container(
width: 10.w,
height: 1.5.h, // 实际横线高度
color: isDarkMode
? Colors.grey[600]
: Colors.grey[400], // 横线颜色
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
batteryIcon,
size: 12.sp,
color: batteryColor,
),
SizedBox(width: 2.w),
Text(
'$batteryLevel%',
style: TextStyle(
fontSize: 11.sp,
fontWeight: FontWeight.w500,
color: batteryColor,
),
),
if (isCharging)
Icon(
Icons.bolt,
size: 10.sp,
color: Colors.green[600],
),
],
),
],
),
),
);
}
// 设备详情卡片
// 未连接信息
Widget _buildDisconnectedInfo(bool isDarkMode) {
return Container(
padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 12.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
borderRadius: BorderRadius.circular(10.r),
border: Border.all(
color: isDarkMode ? Colors.grey[800]! : Colors.grey[300]!,
width: 1,
),
),
child: IntrinsicHeight(
child: Row(
mainAxisAlignment: MainAxisAlignment.center, // 整体居中对齐
crossAxisAlignment: CrossAxisAlignment.start, // 顶部对齐,适应多行文本
children: [
// 图标容器,与文本第一行基线对齐
Padding(
padding: EdgeInsets.only(top: 1.h), // 微调图标位置,与文本第一行基线对齐
child: Icon(
Icons.bluetooth_disabled,
size: 14.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
),
),
SizedBox(width: 6.w), // 图标和文本间距
// 文本容器,使用Flexible确保自动换行且保持居中效果
Flexible(
child: Text(
'disconnectedDevice'.tr, // 未连接设备,点击下方按钮连接
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
height: 1.4, // 启用行高,提升多行文本可读性
),
textAlign: TextAlign.center, // 文本居中对齐
softWrap: true, // 启用软换行
overflow: TextOverflow.visible, // 确保文本完全显示
),
),
],
),
),
);
}
// 连接按钮
Widget _buildConnectButton() {
return ElevatedButton(
onPressed: controller.rescanDevices,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
minimumSize: Size(double.infinity, 44.h),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.r),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.bluetooth_searching, size: 16.sp),
SizedBox(width: 8.w),
Text(
'connectDevice'.tr, // 连接设备
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
// 热门AI体验标题
Widget _buildSectionTitle(bool isDarkMode) {
return Row(
children: [
Text(
'topAiExperiences'.tr, // 热门AI体验
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.grey[800],
),
),
SizedBox(width: 8.w),
Icon(
Icons.star,
color: Colors.amber,
size: 20.w,
),
],
);
}
// 优质AI体验布局(左1右2)
Widget _buildPremiumAILayout() {
final bool isDarkMode = Get.isDarkMode;
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 左侧大卡片 - AI助理
Expanded(
flex: 1,
child: IntroStepBuilder(
group: 'home',
order: 3,
onHighlightWidgetTap: () {
Intro.of(Get.context!).dispose();
controller.exploreAIFeatures();
},
overlayBuilder: (params) {
return IntroOverlayBuilder.aiBuilder(
params,
'太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧',
);
},
builder: (BuildContext context, GlobalKey key) => SizedBox(
key: key,
child: _buildFeatureCard(
title: 'aiAssistant'.tr, // AI全能助手
description: 'aiAssistantInfo'.tr, // 24小时专属顾问,解答问题、提供建议
icon: Icons.assistant,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF0066CC).withOpacity(0.3),
Color(0xFF5E5CE6).withOpacity(0.3),
],
),
onTap: controller.exploreAIFeatures,
iconOnTop: true,
height: 175.h, // 增加高度
isDarkMode: isDarkMode,
iconColor: const Color(0xFF0066CC),
),
),
),
),
SizedBox(width: 16.w),
// 右侧两个卡片
Expanded(
flex: 1,
child: Column(
children: [
// 右上卡片 - 同声翻译
_buildFeatureCard(
title: 'simultaneousInterpretation'.tr, // 同声传译
description:
'simultaneousInterpretationInfo'.tr, // 80+语言实时翻译,出国无忧
icon: Icons.translate,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
const Color(0xFF4CAF50).withOpacity(0.3),
const Color(0xFF009688).withOpacity(0.3),
],
),
onTap: controller.openTranslation,
height: 83.h, // 增加高度
isDarkMode: isDarkMode,
iconColor: const Color(0xFF4CAF50),
),
SizedBox(height: 5.h), // 减少间距
// 右下卡片 -
// 右下卡片 - 会议助手
_buildFeatureCard(
title: 'conferenceAssistant'.tr, // 商务会议助手
description: 'conferenceAssistantInfo'.tr, // 实时记录会议纪要,多语言翻译
icon: Icons.people,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFFab47bc).withOpacity(0.3), // 薰衣草紫
Color(0xFFec407a).withOpacity(0.3), // 品红
],
),
onTap: () {
Get.toNamed(Routes.meeting);
},
height: 85.h, // 增加高度
isDarkMode: isDarkMode,
iconColor: const Color(0xFFd81b60), // 品红
),
],
),
),
],
);
}
// 特性卡片构建方法
Widget _buildFeatureCard({
required String title,
required String description,
required IconData icon,
required LinearGradient gradient,
required VoidCallback onTap,
double? height,
bool iconOnTop = false,
bool isDarkMode = false,
Color iconColor = Colors.white,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
height: height ?? 175.h,
padding: EdgeInsets.all(14.w),
decoration: BoxDecoration(
gradient: gradient,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.grey.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Stack(
children: [
// 图标
if (iconOnTop)
// 大卡片布局
Align(
alignment: Alignment.topLeft,
child: Container(
width: 45.w,
height: 45.w,
decoration: BoxDecoration(
color: iconColor.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: iconColor,
size: 24.w,
),
),
)
else
// 小卡片布局 - 图标
Align(
alignment: Alignment.topLeft,
child: Container(
width: 34.w,
height: 34.w,
decoration: BoxDecoration(
color: iconColor.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
icon,
color: iconColor,
size: 18.w,
),
),
),
// 文本内容
if (iconOnTop)
// 大卡片布局 - 文本
Padding(
padding: EdgeInsets.only(top: 55.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.grey[800],
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4.h),
Flexible(
child: Text(
description,
style: TextStyle(
color:
isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontSize: 12.sp,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
),
],
),
)
else
// 小卡片布局 - 文本
Padding(
padding: EdgeInsets.only(left: 44.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.grey[800],
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2.h),
Flexible(
child: Text(
description,
style: TextStyle(
color:
isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontSize: 10.sp,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
),
),
);
}
}