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.
 
 
 
 
 
 

520 lines
19 KiB

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/home_controller.dart';
import '../../../routes/app_routes.dart';
import 'widgets/calendar/calendar_card.dart';
import 'widgets/calendar/calendar_task_list.dart';
import '../controllers/calendar_controller.dart';
// 首页视图
class HomeView extends GetView<HomeController> {
const HomeView({Key? key}) : super(key: key);
// 功能卡片模块(语音记录/翻译/通话录音等)折叠态下的目标可视高度:
// 固定预算而非内容自然高度,避免卡片变多时撑爆首页固定布局;
// 展示不下的部分(如通话录音)在模块内部滚动查看。
static const double _extrasTargetHeight = 420;
@override
Widget build(BuildContext context) {
final bool isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: Colors.transparent,
// 背景:顶部「左淡蓝·中浅蓝·右淡暖」横向晕染,向下渐隐到白(取自参考图真实色值)
body: Stack(
children: [
// 1) 顶部横向色带(浅色模式)/ 深色竖直渐变(深色模式)
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: isDarkMode
? const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF0A0F1A), Color(0xFF000000)],
)
: const LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Color(0xFFD4EAFF), // 左:淡蓝
Color(0xFFE6F3FE), // 中:浅蓝
Color(0xFFFBF2F0), // 右:淡暖(浅奶粉)
],
),
),
),
),
// 2) 竖直方向把顶部色带渐隐到白(仅浅色模式)
if (!isDarkMode)
const Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0x00FFFFFF), // 顶部透明:露出横向色带
Color(0x00FFFFFF),
Color(0xFFFFFFFF), // 向下渐变为白
],
stops: [0.0, 0.15, 0.42],
),
),
),
),
// 内容(固定布局:日历展开时任务列表铺满并覆盖下方翻译卡片/AI)
SafeArea(
child: Padding(
// 横向不留白:任务列表需铺满到屏幕边(在屏幕边裁切,阴影完整);
// 其余元素各自加 16 横向内边距对齐
padding: EdgeInsets.only(top: 16.w, bottom: 8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 顶部标题栏(固定)
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: _buildAppBar(context, isDarkMode),
),
SizedBox(height: 16.h),
// 日历 + 任务列表 + 翻译卡片/AI(随日历折叠/展开切换)
Expanded(child: _buildCalendarArea(isDarkMode)),
],
),
),
),
],
),
);
}
// 日历区域:日历卡片 + 任务列表(Expanded) + 翻译卡片/AI(随展开收起)
Widget _buildCalendarArea(bool isDarkMode) {
final cal = Get.find<CalendarController>();
return LayoutBuilder(
builder: (context, constraints) {
final double h = constraints.maxHeight;
// 极小高度(超大字体/超小屏)兜底:退回可滚动布局,避免溢出
if (h < 360.h) {
return _buildCalendarScrollFallback(isDarkMode);
}
// 月历网格最大高度夹取:留出间距 + 最小任务列表 + 卡片其余高度,避免 Expanded 负高
final double maxBody = h - 16.h - 96.h - 130.h;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: CalendarCard(maxBodyHeight: maxBody > 0 ? maxBody : null),
),
SizedBox(height: 16.h),
// 任务列表:填满任务区(连续高度,便于折叠/展开两层按下拉距离交叉淡入淡出)。
const Expanded(child: CalendarTaskList()),
// 翻译卡片 + AI:随日历展开(dragProgress→0)从底部收起、被覆盖。
// 用固定目标高度(非内容自然高度)做插值:卡片模块内容一旦变多(如新增
// "通话录音")就不会撑爆这个不滚动的整屏布局;模块内部改用可滚动容器
// 兜底——展示不下的卡片(如通话录音)靠上下滑动查看,而不是让布局溢出。
Obx(() => ClipRect(
child: SizedBox(
height: _extrasTargetHeight.h * cal.dragProgress.value,
child: IgnorePointer(
ignoring: cal.dragProgress.value < 1.0,
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 16.w),
physics: const BouncingScrollPhysics(),
child: _buildExtrasBlock(isDarkMode),
),
),
),
)),
],
);
},
);
}
// 翻译功能卡片(折叠态显示在任务列表下方;展开态被收起覆盖)
// AI 球已改为全局悬浮球(core/widgets/global_ai_orb.dart),不再嵌在首页里
Widget _buildExtrasBlock(bool isDarkMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 16.h),
_buildFeatureGrid(isDarkMode),
SizedBox(height: 24.h),
],
);
}
// 极小屏兜底:可滚动布局(任务列表给有限高度)
Widget _buildCalendarScrollFallback(bool isDarkMode) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: const CalendarCard(),
),
SizedBox(height: 16.h),
SizedBox(height: 280.h, child: const CalendarTaskList()),
SizedBox(height: 16.h),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: _buildFeatureGrid(isDarkMode),
),
SizedBox(height: 24.h),
],
),
);
}
// 顶部标题栏:左侧设备管理,右侧“语音打断”开关 + 设置入口
Widget _buildAppBar(BuildContext context, bool isDarkMode) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 左侧:设备管理入口
_buildDeviceManager(isDarkMode),
// 右侧:标语 + 设置图标
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'YM开启科技人生',
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white70 : Colors.grey[700],
),
),
SizedBox(width: 8.w),
_buildSettingsEntry(isDarkMode),
],
),
],
);
}
// 左侧设备管理:未连接显示“添加设备”,已连接显示电量
Widget _buildDeviceManager(bool isDarkMode) {
final Color pillBg = isDarkMode ? const Color(0xFF1A1A1A) : Colors.white;
final Color textColor = isDarkMode ? Colors.white : const Color(0xFF222222);
return Obx(() {
final bool connected = controller.isFullyConnectedRx.value;
return GestureDetector(
behavior: HitTestBehavior.opaque,
// 未连接 → 扫描页;已连接 → 我的设备
onTap: () => Get.toNamed(connected ? Routes.myDevices : Routes.devices),
child: Container(
padding: EdgeInsets.fromLTRB(6.w, 6.h, 14.w, 6.h),
decoration: BoxDecoration(
color: pillBg,
borderRadius: BorderRadius.circular(24.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isDarkMode ? 0.25 : 0.08),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
// 蓝色圆形图标
Container(
width: 26.w,
height: 26.w,
decoration: const BoxDecoration(
color: Color(0xFF3B82F6),
shape: BoxShape.circle,
),
child: Icon(
connected ? Icons.battery_full : Icons.add,
color: Colors.white,
size: connected ? 16.sp : 18.sp,
),
),
SizedBox(width: 8.w),
Text(
connected ? controller.getBatteryText() : 'addDevice'.tr,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.bold,
color: textColor,
),
),
],
),
),
);
});
}
// 右侧“语音打断”开关(保留原有逻辑与样式)
// 设置入口:头像/人形图标,点击进入设置页
Widget _buildSettingsEntry(bool isDarkMode) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => Get.toNamed(Routes.settings),
child: Container(
width: 36.w,
height: 36.w,
decoration: BoxDecoration(
color: isDarkMode
? const Color(0xFF2A2A2A)
: Colors.white.withOpacity(0.7),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isDarkMode ? 0.25 : 0.08),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Icon(
Icons.person,
size: 22.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[700],
),
),
);
}
// 功能卡片区:顶部(通话录音/现场录音,依赖设备连接) + 左1大(语音记录助手) +
// 右2(同声翻译/面对面翻译) + 下2(通话翻译/多媒体翻译)
Widget _buildFeatureGrid(bool isDarkMode) {
const double smallH = 84; // 小卡高度
const double gap = 12; // 卡片间距
return Column(
children: [
// 顶区:通话录音 + 现场录音(依赖设备连接,未连接时置灰/透明)
Obx(() {
final bool connected = controller.isFullyConnectedRx.value;
return Row(
children: [
Expanded(
child: _buildFeatureCard(
title: '通话录音',
icon: Icons.fiber_manual_record,
gradient: const [Color(0xFFEF5350), Color(0xFFE53935)],
height: smallH.h,
isDarkMode: isDarkMode,
enabled: connected,
onTap: controller.openCallRecording,
),
),
SizedBox(width: gap.w),
Expanded(
child: _buildFeatureCard(
title: '现场录音',
icon: Icons.mic,
gradient: const [Color(0xFF26C6DA), Color(0xFF00838F)],
height: smallH.h,
isDarkMode: isDarkMode,
onTap: controller.openFieldRecording,
),
),
],
);
}),
SizedBox(height: gap.h),
// 上区:左侧高卡 + 右侧两张卡
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: _buildFeatureCard(
title: '语音记录助手',
subtitle: '实时记录各种场景语音',
icon: Icons.groups_2,
gradient: const [Color(0xFFAB47BC), Color(0xFFEC407A)],
height: (smallH * 2 + gap).h,
iconOnTop: true,
isDarkMode: isDarkMode,
onTap: controller.openMeet, // → 会议(Routes.meeting)
),
),
SizedBox(width: gap.w),
Expanded(
child: Column(
children: [
_buildFeatureCard(
title: '同声翻译',
icon: Icons.record_voice_over,
gradient: const [Color(0xFF4CAF50), Color(0xFF009688)],
height: smallH.h,
isDarkMode: isDarkMode,
onTap: () =>
controller.openTranslationFeature('simultaneous'),
),
SizedBox(height: gap.h),
_buildFeatureCard(
title: '面对面翻译',
icon: Icons.forum,
gradient: const [Color(0xFF42A5F5), Color(0xFF1E88E5)],
height: smallH.h,
isDarkMode: isDarkMode,
onTap: () =>
controller.openTranslationFeature('faceToFace'),
),
],
),
),
],
),
SizedBox(height: gap.h),
// 下区:通话翻译 + 多媒体翻译(依赖设备连接,未连接时置灰/透明)
Obx(() {
final bool connected = controller.isFullyConnectedRx.value;
return Row(
children: [
Expanded(
child: _buildFeatureCard(
title: '通话翻译',
icon: Icons.phone_in_talk,
gradient: const [Color(0xFFFFA726), Color(0xFFFB8C00)],
height: smallH.h,
isDarkMode: isDarkMode,
enabled: connected,
onTap: () => controller.openTranslationFeature('call'),
),
),
SizedBox(width: gap.w),
Expanded(
child: _buildFeatureCard(
title: '多媒体翻译',
icon: Icons.photo_camera,
gradient: const [Color(0xFF5C6BC0), Color(0xFF3949AB)],
height: smallH.h,
isDarkMode: isDarkMode,
onTap: () => controller.openTranslationFeature('audioVideo'),
),
),
],
);
}),
],
);
}
// 通用功能卡片
Widget _buildFeatureCard({
required String title,
String? subtitle,
required IconData icon,
required List<Color> gradient,
required double height,
required bool isDarkMode,
required VoidCallback onTap,
bool iconOnTop = false,
bool enabled = true, // false 时整卡变透明(未连接设备)
}) {
final Color cardBg = isDarkMode ? const Color(0xFF1A1A1A) : Colors.white;
final Color titleColor =
isDarkMode ? Colors.white : const Color(0xFF222222);
final Color subColor =
isDarkMode ? Colors.white60 : const Color(0xFF888888);
final Widget iconBox = Container(
width: 42.w,
height: 42.w,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: gradient,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12.r),
),
child: Icon(icon, color: Colors.white, size: 22.sp),
);
return Opacity(
opacity: enabled ? 1.0 : 0.4, // 未连接:整卡变透明(仍可点击 → 弹"请连接设备")
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
height: height,
padding: EdgeInsets.all(14.w),
decoration: BoxDecoration(
color: cardBg,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isDarkMode ? 0.06 : 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: iconOnTop
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
iconBox,
const Spacer(),
Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
color: titleColor,
),
),
if (subtitle != null) ...[
SizedBox(height: 4.h),
Text(
subtitle,
style: TextStyle(fontSize: 12.sp, color: subColor),
),
],
],
)
: Row(
children: [
iconBox,
SizedBox(width: 12.w),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.bold,
color: titleColor,
),
),
if (subtitle != null) ...[
SizedBox(height: 2.h),
Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style:
TextStyle(fontSize: 11.sp, color: subColor),
),
],
],
),
),
],
),
),
),
);
}
}