Browse Source

light

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
90c9f03e86
  1. 3
      android/app/src/main/AndroidManifest.xml
  2. 4
      lib/app/translations/app_translations.dart
  3. 28
      lib/core/routes/app_pages.dart
  4. 4
      lib/core/routes/app_routes.dart
  5. 6
      lib/core/widgets/common_bottom_nav.dart
  6. 300
      lib/data/services/bluetooth_service.dart
  7. 14
      lib/main.dart
  8. 172
      lib/modules/explore/controllers/explore_controller.dart
  9. 555
      lib/modules/explore/views/explore_view.dart
  10. 492
      lib/modules/home/controllers/home_controller.dart
  11. 732
      lib/modules/home/views/home_view.dart
  12. 9
      lib/modules/login/bindings/login_binding.dart
  13. 246
      lib/modules/login/controllers/login_controller.dart
  14. 628
      lib/modules/login/views/login_view.dart
  15. 9
      lib/modules/pairing/bindings/pairing_binding.dart
  16. 295
      lib/modules/pairing/controllers/pairing_controller.dart
  17. 344
      lib/modules/pairing/views/pairing_view.dart
  18. 11
      lib/modules/settings/bindings/settings_binding.dart
  19. 66
      lib/modules/settings/controllers/settings_controller.dart
  20. 1158
      lib/modules/settings/views/settings_view.dart
  21. 9
      lib/modules/splash/bindings/splash_binding.dart
  22. 83
      lib/modules/splash/controllers/splash_controller.dart
  23. 256
      lib/modules/splash/views/splash_view.dart
  24. 2
      pubspec.yaml

3
android/app/src/main/AndroidManifest.xml

@ -11,6 +11,9 @@
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- Android 13+ 通知权限 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- 添加存储权限 -->

4
lib/app/translations/app_translations.dart

@ -36,8 +36,9 @@ class AppTranslations extends Translations {
'check_update': '检查更新',
'clear_cache': '清除缓存',
'home': '首页',
'explore': '探索',
'explore': '发现',
'profile': '我的',
'device': '设备',
},
'en_US': {
// General
@ -74,6 +75,7 @@ class AppTranslations extends Translations {
'home': 'Home',
'explore': 'Explore',
'profile': 'Profile',
'device': 'Device',
},
};
}

28
lib/core/routes/app_pages.dart

@ -22,10 +22,33 @@ import '../../modules/translation/views/translation_view.dart';
import '../../modules/translation/bindings/translation_binding.dart';
import '../../modules/translation/views/translation_history_view.dart';
import '../../modules/translation/bindings/translation_history_binding.dart';
import '../../modules/login/views/login_view.dart';
import '../../modules/login/bindings/login_binding.dart';
import '../../modules/splash/views/splash_view.dart';
import '../../modules/splash/bindings/splash_binding.dart';
import '../../modules/pairing/views/pairing_view.dart';
import '../../modules/pairing/bindings/pairing_binding.dart';
import './app_routes.dart';
import '../../modules/settings/views/settings_view.dart';
import '../../modules/settings/bindings/settings_binding.dart';
abstract class AppPages {
static final pages = [
GetPage(
name: Routes.splash,
page: () => const SplashView(),
binding: SplashBinding(),
),
GetPage(
name: Routes.login,
page: () => const LoginView(),
binding: LoginBinding(),
),
GetPage(
name: Routes.pairing,
page: () => const PairingView(),
binding: PairingBinding(),
),
GetPage(
name: Routes.initial,
page: () => const HomeView(),
@ -101,5 +124,10 @@ abstract class AppPages {
page: () => const TranslationHistoryView(),
binding: TranslationHistoryBinding(),
),
GetPage(
name: Routes.settings,
page: () => const SettingsView(),
binding: SettingsBinding(),
),
];
}

4
lib/core/routes/app_routes.dart

@ -1,9 +1,13 @@
abstract class Routes {
static const splash = '/splash';
static const login = '/login';
static const pairing = '/pairing';
static const initial = '/home';
static const home = '/home';
static const explore = '/explore';
static const chat = '/chat';
static const profile = '/profile';
static const settings = '/settings';
static const speechDemo = '/speech_demo';
static const TTS_TEST = '/tts_test';
static const ASR_TEST = '/asr_test';

6
lib/core/widgets/common_bottom_nav.dart

@ -24,7 +24,7 @@ class CommonBottomNav extends StatelessWidget {
Get.offAllNamed(Routes.explore);
break;
case 2:
Get.offAllNamed(Routes.profile);
Get.offAllNamed(Routes.settings);
break;
}
},
@ -38,8 +38,8 @@ class CommonBottomNav extends StatelessWidget {
label: 'explore'.tr,
),
BottomNavigationBarItem(
icon: const Icon(Icons.person),
label: 'profile'.tr,
icon: const Icon(Icons.settings),
label: 'settings'.tr,
),
],
);

300
lib/data/services/bluetooth_service.dart

@ -0,0 +1,300 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:get/get.dart';
import '../../core/utils/logger.dart';
import 'dart:io' show Platform;
import 'package:permission_handler/permission_handler.dart';
/// 蓝牙设备服务,用于发现和管理蓝牙设备
class BluetoothService extends GetxService {
static BluetoothService get to => Get.find<BluetoothService>();
// 单例实例
static final BluetoothService _instance = BluetoothService._internal();
factory BluetoothService() => _instance;
BluetoothService._internal();
// 设备状态
final _isAvailable = false.obs;
final _isOn = false.obs;
// 已连接的设备
final _connectedDevices = <BluetoothDevice>[].obs;
// 扫描状态
final _isScanning = false.obs;
// 扫描结果
final _scanResults = <ScanResult>[].obs;
// 当前选中的设备
final _selectedDevice = Rx<BluetoothDevice?>(null);
// 连接状态
final _connectionState = Rx<BluetoothConnectionState>(BluetoothConnectionState.disconnected);
// 订阅
StreamSubscription? _stateSubscription;
StreamSubscription? _scanSubscription;
StreamSubscription? _connectionSubscription;
// 权限状态
final _permissionsGranted = false.obs;
bool get permissionsGranted => _permissionsGranted.value;
// Getters
bool get isAvailable => _isAvailable.value;
bool get isOn => _isOn.value;
bool get isScanning => _isScanning.value;
List<BluetoothDevice> get connectedDevices => _connectedDevices;
List<ScanResult> get scanResults => _scanResults;
BluetoothDevice? get selectedDevice => _selectedDevice.value;
BluetoothConnectionState get connectionState => _connectionState.value;
// Observable getters (for reactive programming)
RxBool get isAvailableObs => _isAvailable;
RxBool get isOnObs => _isOn;
RxList<BluetoothDevice> get connectedDevicesObs => _connectedDevices;
RxList<ScanResult> get scanResultsObs => _scanResults;
Rx<BluetoothDevice?> get selectedDeviceObs => _selectedDevice;
Rx<BluetoothConnectionState> get connectionStateObs => _connectionState;
/// 初始化蓝牙服务
Future<BluetoothService> init() async {
try {
// 不在初始化时检查权限,改为在需要时才检查
// await _checkAndRequestPermissions();
// 监听蓝牙状态
_stateSubscription = FlutterBluePlus.adapterState.listen((state) {
_isOn.value = state == BluetoothAdapterState.on;
_isAvailable.value = state != BluetoothAdapterState.unavailable;
if (state == BluetoothAdapterState.on) {
// 蓝牙打开时,获取已连接设备,但不请求权限
// 只有在用户主动扫描时才请求权限
_getConnectedDevices();
} else if (state == BluetoothAdapterState.off) {
// 蓝牙关闭时,清空设备列表
_connectedDevices.clear();
_scanResults.clear();
_selectedDevice.value = null;
}
});
// 检查当前蓝牙状态,如果已开启则获取设备
if (await FlutterBluePlus.isOn) {
_isOn.value = true;
_isAvailable.value = true;
_getConnectedDevices();
}
return this;
} catch (e) {
Logger.error('初始化蓝牙服务失败: $e');
return this;
}
}
/// 检查并请求蓝牙所需权限
Future<bool> _checkAndRequestPermissions() async {
// 暂时跳过权限检查,直接返回true
_permissionsGranted.value = true;
return true;
}
/// 再次请求权限
Future<bool> requestPermissions() async {
// 暂时跳过权限检查,直接返回true
_permissionsGranted.value = true;
return true;
}
/// 获取已连接的设备
Future<void> _getConnectedDevices() async {
try {
// 获取已连接的设备
List<BluetoothDevice> devices = await FlutterBluePlus.connectedSystemDevices;
_connectedDevices.value = devices;
// 如果有已连接的设备但没有选中设备,则选中第一个
if (_connectedDevices.isNotEmpty && _selectedDevice.value == null) {
_selectedDevice.value = _connectedDevices[0];
_monitorConnectionState(_selectedDevice.value!);
}
Logger.info('已连接设备数量: ${_connectedDevices.length}');
} catch (e) {
Logger.error('获取已连接设备失败: $e');
}
}
/// 获取系统中已配对的设备
Future<List<BluetoothDevice>> getPairedDevices() async {
try {
// 获取已配对的设备(系统已知设备)
List<BluetoothDevice> systemDevices = await FlutterBluePlus.bondedDevices;
return systemDevices;
} catch (e) {
Logger.error('获取已配对设备失败: $e');
return [];
}
}
/// 开始扫描附近的蓝牙设备
Future<void> startScan({Duration? timeout}) async {
if (_isScanning.value) return;
// 清空旧的扫描结果
_scanResults.clear();
try {
// 开始扫描
_scanSubscription?.cancel();
_scanSubscription = FlutterBluePlus.scanResults.listen((results) {
_scanResults.value = results;
});
// 设置扫描中状态
_isScanning.value = true;
// 开始扫描
await FlutterBluePlus.startScan(
timeout: timeout ?? const Duration(seconds: 10),
);
// 扫描完成后更新状态
_isScanning.value = false;
Logger.info('蓝牙扫描完成,发现 ${_scanResults.length} 个设备');
} catch (e) {
_isScanning.value = false;
Logger.error('蓝牙扫描失败: $e');
}
}
/// 停止扫描
Future<void> stopScan() async {
if (!_isScanning.value) return;
try {
await FlutterBluePlus.stopScan();
_isScanning.value = false;
} catch (e) {
Logger.error('停止蓝牙扫描失败: $e');
}
}
/// 连接到指定设备
Future<bool> connectToDevice(BluetoothDevice device) async {
if (_selectedDevice.value == device) {
// 如果是当前选中设备,检查连接状态
if (_connectionState.value == BluetoothConnectionState.connected) {
Logger.info('设备 ${device.platformName} 已经连接,无需重新连接');
return true;
}
}
try {
// 检查设备是否已连接
bool isAlreadyConnected = false;
// 检查设备连接状态
BluetoothConnectionState state = await device.connectionState.first;
if (state == BluetoothConnectionState.connected) {
Logger.info('设备 ${device.platformName} 已处于连接状态');
isAlreadyConnected = true;
}
// 如果设备已连接,直接更新状态而不尝试连接
if (isAlreadyConnected) {
// 断开当前跟踪的设备(如果有)
if (_selectedDevice.value != null && _selectedDevice.value != device) {
_connectionSubscription?.cancel();
}
// 更新状态
_selectedDevice.value = device;
_monitorConnectionState(device);
return true;
}
// 断开当前连接
await disconnectFromDevice();
// 连接新设备
await device.connect();
// 更新状态
_selectedDevice.value = device;
_monitorConnectionState(device);
return true;
} catch (e) {
Logger.error('连接蓝牙设备失败: $e');
return false;
}
}
/// 断开与当前设备的连接
Future<bool> disconnectFromDevice() async {
if (_selectedDevice.value == null) return true;
try {
await _selectedDevice.value!.disconnect();
_connectionSubscription?.cancel();
_connectionSubscription = null;
_selectedDevice.value = null;
return true;
} catch (e) {
Logger.error('断开蓝牙设备连接失败: $e');
return false;
}
}
/// 监控连接状态
void _monitorConnectionState(BluetoothDevice device) {
_connectionSubscription?.cancel();
_connectionSubscription = device.connectionState.listen((state) {
_connectionState.value = state;
if (state == BluetoothConnectionState.disconnected) {
_selectedDevice.value = null;
}
});
}
/// 获取已连接设备的信息
Map<String, dynamic> getConnectedDeviceInfo() {
if (_selectedDevice.value == null) {
return {
'connected': false,
'device': null,
};
}
return {
'connected': _connectionState.value == BluetoothConnectionState.connected,
'device': {
'name': _selectedDevice.value!.platformName.isEmpty ?
'未命名设备' : _selectedDevice.value!.platformName,
'id': _selectedDevice.value!.remoteId.str,
'isConnected': _connectionState.value == BluetoothConnectionState.connected,
'rssi': _scanResults
.firstWhereOrNull((r) => r.device.remoteId == _selectedDevice.value!.remoteId)
?.rssi
}
};
}
@override
void onClose() {
_stateSubscription?.cancel();
_scanSubscription?.cancel();
_connectionSubscription?.cancel();
disconnectFromDevice();
super.onClose();
}
}

14
lib/main.dart

@ -12,8 +12,18 @@ import 'app/translations/app_translations.dart';
import 'data/services/voice_interaction_service.dart';
void main() async {
// 确保Flutter绑定初始化
WidgetsFlutterBinding.ensureInitialized();
// 先运行应用,显示启动页面
runApp(const MainApp());
// 然后异步执行其他初始化操作
_initializeApp();
}
// 异步初始化应用
Future<void> _initializeApp() async {
// 设置窗口大小(仅在 macOS 下生效)
if (Platform.isMacOS) {
const double width = 375.0;
@ -46,7 +56,7 @@ void main() async {
// AudioService 将在 AudioServiceManager 中初始化
// 不再需要在这里调用 AudioService.init
runApp(const MainApp());
// 其他可能的初始化操作...
}
class MainApp extends StatelessWidget {
@ -66,7 +76,7 @@ class MainApp extends StatelessWidget {
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialBinding: InitialBinding(),
initialRoute: Routes.initial,
initialRoute: Routes.splash,
getPages: AppPages.pages,
debugShowCheckedModeBanner: false,
translations: AppTranslations(),

172
lib/modules/explore/controllers/explore_controller.dart

@ -1,32 +1,170 @@
import 'package:get/get.dart';
import '../../../data/providers/agent_provider.dart';
import '../../../data/services/voice_interaction_service.dart';
class ChatItem {
final String id;
import 'package:flutter/material.dart';
ChatItem({
required this.id,
class AIFeature {
final String title;
final String description;
final IconData icon;
final Color startColor;
final Color endColor;
final Color iconBgColor;
final Color iconColor;
AIFeature({
required this.title,
required this.description,
required this.icon,
required this.startColor,
required this.endColor,
required this.iconBgColor,
required this.iconColor,
});
}
class FunctionScene {
final String title;
final String description;
final IconData icon;
final Color startColor;
final Color endColor;
final Color iconColor;
FunctionScene({
required this.title,
required this.description,
required this.icon,
required this.startColor,
required this.endColor,
required this.iconColor,
});
}
class ExploreController extends GetxController {
static ExploreController get to => Get.find();
// 搜索关键词
final searchController = TextEditingController();
final _chatList = <ChatItem>[].obs;
// AI功能列表
final List<AIFeature> aiFeatures = [
AIFeature(
title: '同声翻译',
description: '实时翻译40+种语言',
icon: Icons.translate,
startColor: Colors.blue.shade100,
endColor: Colors.blue.shade200,
iconBgColor: Colors.blue.withOpacity(0.2),
iconColor: Colors.blue.shade600,
),
AIFeature(
title: 'AI女友',
description: '智能陪伴,随时聊天',
icon: Icons.favorite,
startColor: Colors.pink.shade100,
endColor: Colors.pink.shade200,
iconBgColor: Colors.pink.withOpacity(0.2),
iconColor: Colors.pink.shade600,
),
AIFeature(
title: '音乐创作',
description: '用AI创作专属音乐',
icon: Icons.music_note,
startColor: Colors.green.shade100,
endColor: Colors.green.shade200,
iconBgColor: Colors.green.withOpacity(0.2),
iconColor: Colors.green.shade600,
),
AIFeature(
title: '声音克隆',
description: '创建你的数字声音',
icon: Icons.mic,
startColor: Colors.amber.shade100,
endColor: Colors.amber.shade200,
iconBgColor: Colors.amber.withOpacity(0.2),
iconColor: Colors.amber.shade600,
),
];
List<ChatItem> get chatList => _chatList;
// 热门功能场景列表
final List<FunctionScene> functionScenes = [
FunctionScene(
title: '商务会议助手',
description: '实时记录会议纪要,多语言翻译',
icon: Icons.people,
startColor: Colors.blue.shade200,
endColor: Colors.blue.shade300,
iconColor: Colors.blue.shade600,
),
FunctionScene(
title: '语言学习助手',
description: '发音纠正,实时练习口语',
icon: Icons.school,
startColor: Colors.green.shade200,
endColor: Colors.green.shade300,
iconColor: Colors.green.shade600,
),
FunctionScene(
title: '旅行翻译助手',
description: '实时语音翻译,旅游景点介绍',
icon: Icons.language,
startColor: Colors.amber.shade200,
endColor: Colors.amber.shade300,
iconColor: Colors.amber.shade600,
),
];
@override
void onInit() {
super.onInit();
_loadInitialChats();
// 搜索功能
void search() {
if (searchController.text.isEmpty) return;
Get.snackbar(
'搜索',
'正在搜索: ${searchController.text}',
snackPosition: SnackPosition.TOP,
);
// 实际应用中,这里应该实现真正的搜索逻辑
searchController.clear();
}
// 点击AI功能
void onAIFeatureTap(int index) {
if (index < 0 || index >= aiFeatures.length) return;
Get.snackbar(
'功能',
'您选择了: ${aiFeatures[index].title}',
snackPosition: SnackPosition.TOP,
);
// 实际应用中,这里应该跳转到对应的功能页面
}
void _loadInitialChats() {
final agents = AgentProvider.getAllAgents();
_chatList.assignAll(
agents.map((agent) => ChatItem(id: agent.id)).toList(),
// 点击功能场景
void onFunctionSceneTap(int index) {
if (index < 0 || index >= functionScenes.length) return;
Get.snackbar(
'场景',
'您选择了: ${functionScenes[index].title}',
snackPosition: SnackPosition.TOP,
);
// 实际应用中,这里应该跳转到对应的场景页面
}
// 尝试新功能
void tryNewFeature() {
Get.snackbar(
'新功能',
'您选择了尝试AI歌手模式',
snackPosition: SnackPosition.TOP,
);
// 实际应用中,这里应该跳转到新功能页面
}
@override
void onClose() {
searchController.dispose();
super.onClose();
}
}

555
lib/modules/explore/views/explore_view.dart

@ -1,111 +1,312 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:ui';
import '../controllers/explore_controller.dart';
import '../../chat/controllers/chat_controller.dart';
import '../../chat/views/chat_view.dart';
import '../../../data/providers/agent_provider.dart';
import '../../../core/widgets/common_bottom_nav.dart';
import '../../../core/routes/app_routes.dart';
class ExploreView extends GetView<ExploreController> {
const ExploreView({Key? key}) : super(key: key);
void _onChatItemTap(ChatItem item) {
// 获取对应的Agent
final agent = AgentProvider.getAgentById(item.id);
if (agent == null) {
Get.snackbar(
'Error',
'Agent not found',
snackPosition: SnackPosition.BOTTOM,
);
return;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
body: SafeArea(
child: Column(
children: [
// 标题和搜索
_buildHeaderAndSearch(),
// 检查是否是同声翻译官
if (agent.id == 'simultaneous_interpreter') {
// 导航到翻译页面,并传递代理信息
Get.toNamed(Routes.translation, arguments: {
'agentId': agent.id,
'agentName': agent.name,
'agentAvatar': agent.avatarUrl,
});
return;
}
// 主要内容(可滚动)
Expanded(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 80.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 特色功能
_buildFeaturedCard(),
// 导航到聊天页面,让 binding 来处理控制器的创建
Get.toNamed('/chat', arguments: {
'agentId': agent.id,
'agentName': agent.name,
'agentVoice': agent.voice,
'agentAvatar': agent.avatarUrl,
'agentSubtitle': agent.description,
'systemPrompt': agent.systemPrompt,
'welcomeMessage': agent.welcomeMessage,
'playVoiceOnEnter': false,
});
}
// AI功能列表
SizedBox(height: 24.h),
_buildAIFeaturesSection(),
Widget _buildChatItem(ChatItem item) {
// 获取对应的Agent
final agent = AgentProvider.getAgentById(item.id);
if (agent == null) return const SizedBox.shrink();
// 热门功能场景
SizedBox(height: 24.h),
_buildFunctionScenesSection(),
return InkWell(
onTap: () => _onChatItemTap(item),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
child: Row(
children: [
// 头像
CircleAvatar(
radius: 25.r,
backgroundImage: AssetImage(agent.avatarUrl),
backgroundColor: const Color(0xFFEEF4FF),
),
SizedBox(width: 12.w),
// 聊天内容
Expanded(
// 新功能推荐
SizedBox(height: 24.h),
_buildNewFeatureSection(),
],
),
),
),
),
],
),
),
bottomNavigationBar: const CommonBottomNav(currentIndex: 1),
);
}
// 标题和搜索
Widget _buildHeaderAndSearch() {
return Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 名称行
// 标题和菜单按钮
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
agent.name,
'发现',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
fontSize: 24.sp,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
if (agent.tag == '推荐') ...[
SizedBox(width: 8.w),
Container(
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h),
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: const Color(0xFFE8F5E9),
borderRadius: BorderRadius.circular(4.r),
color: Colors.grey[200],
shape: BoxShape.circle,
),
child: Text(
'推荐',
child: IconButton(
icon: Icon(Icons.more_vert, size: 16.sp),
padding: EdgeInsets.zero,
onPressed: () {},
),
),
],
),
// 搜索框
SizedBox(height: 16.h),
Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12.r),
),
child: TextField(
controller: controller.searchController,
decoration: InputDecoration(
hintText: '搜索AI功能...',
hintStyle: TextStyle(
fontSize: 14.sp,
color: Colors.grey[400],
),
prefixIcon: Icon(
Icons.search,
size: 20.sp,
color: Colors.grey[400],
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 12.h),
),
textInputAction: TextInputAction.search,
onSubmitted: (_) => controller.search(),
),
),
],
),
);
}
// 特色功能卡片
Widget _buildFeaturedCard() {
return Container(
height: 180.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Stack(
children: [
// 背景图片
ClipRRect(
borderRadius: BorderRadius.circular(24.r),
child: Container(
color: Colors.blue.shade900, // 预设颜色,图片加载失败时显示
child: Image.network(
'https://images.unsplash.com/photo-1526378722484-bd91ca387e72?ixlib=rb-4.0.3&auto=format&fit=crop&w=1000&q=80',
width: double.infinity,
height: 180.h,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.blue.shade900,
width: double.infinity,
height: 180.h,
child: Icon(
Icons.image_not_supported,
color: Colors.white,
size: 40.sp,
),
);
},
),
),
),
// 渐变遮罩
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24.r),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.1),
Colors.black.withOpacity(0.6),
],
),
),
),
// 文本内容
Padding(
padding: EdgeInsets.all(24.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'智能耳机,无限可能',
style: TextStyle(
fontSize: 12.sp,
color: const Color(0xFF4CAF50),
fontSize: 22.sp,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 4.h),
Text(
'探索DeepVoice提供的AI功能',
style: TextStyle(
fontSize: 14.sp,
color: Colors.white.withOpacity(0.8),
),
),
],
),
),
],
),
);
}
// AI功能部分
Widget _buildAIFeaturesSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'AI功能',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.grey[800],
),
),
SizedBox(height: 16.h),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16.w,
mainAxisSpacing: 16.h,
childAspectRatio: 1.0,
),
itemCount: controller.aiFeatures.length,
itemBuilder: (context, index) {
final feature = controller.aiFeatures[index];
return _buildAIFeatureItem(feature, index);
},
),
],
);
}
// AI功能项
Widget _buildAIFeatureItem(AIFeature feature, int index) {
return GestureDetector(
onTap: () => controller.onAIFeatureTap(index),
child: Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
feature.startColor,
feature.endColor,
],
),
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Stack(
children: [
// 图标圆圈
Align(
alignment: Alignment.topLeft,
child: Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
color: feature.iconColor.withOpacity(0.2),
shape: BoxShape.circle,
),
child: Icon(
feature.icon,
size: 24.sp,
color: feature.iconColor,
),
),
),
// 文本内容
Padding(
padding: EdgeInsets.only(top: 58.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 标题
Text(
feature.title,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.grey[800],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 4.h),
// 描述
Text(
agent.description,
feature.description,
style: TextStyle(
fontSize: 14.sp,
fontSize: 13.sp,
color: Colors.grey[600],
),
maxLines: 1,
@ -120,53 +321,191 @@ class ExploreView extends GetView<ExploreController> {
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
title: Text(
'对话',
// 热门功能场景部分
Widget _buildFunctionScenesSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'热门功能场景',
style: TextStyle(
fontSize: 17.sp,
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.black,
color: Colors.grey[800],
),
),
centerTitle: true,
actions: [
IconButton(
icon: Icon(
Icons.search,
color: Colors.black,
size: 24.sp,
SizedBox(height: 16.h),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.functionScenes.length,
itemBuilder: (context, index) {
final scene = controller.functionScenes[index];
return Padding(
padding: EdgeInsets.only(bottom: 16.h),
child: _buildFunctionSceneItem(scene, index),
);
},
),
onPressed: () {},
],
);
}
// 功能场景项
Widget _buildFunctionSceneItem(FunctionScene scene, int index) {
return GestureDetector(
onTap: () => controller.onFunctionSceneTap(index),
child: Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
IconButton(
icon: Icon(
Icons.add_circle_outline,
color: Colors.black,
],
),
child: Row(
children: [
// 图标
Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
scene.startColor,
scene.endColor,
],
),
borderRadius: BorderRadius.circular(12.r),
),
child: Icon(
scene.icon,
size: 24.sp,
color: scene.iconColor,
),
),
SizedBox(width: 16.w),
// 文本内容
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
scene.title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.grey[800],
),
),
SizedBox(height: 4.h),
Text(
scene.description,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[600],
),
onPressed: () {},
),
SizedBox(width: 4.w),
],
),
body: Obx(() => ListView.separated(
itemCount: controller.chatList.length,
separatorBuilder: (context, index) => Divider(
height: 1,
indent: 16.w,
endIndent: 16.w,
color: const Color(0xFFEEEEEE),
),
itemBuilder: (context, index) => _buildChatItem(controller.chatList[index]),
)),
bottomNavigationBar: const CommonBottomNav(currentIndex: 1),
],
),
),
);
}
// 新功能推荐部分
Widget _buildNewFeatureSection() {
return Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.purple.shade100,
Colors.purple.shade200,
],
),
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 图标
Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
color: Colors.purple.shade200,
borderRadius: BorderRadius.circular(12.r),
),
child: Icon(
Icons.lightbulb_outline,
size: 24.sp,
color: Colors.purple.shade600,
),
),
SizedBox(height: 16.h),
// 标题
Text(
'AI歌手模式',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
SizedBox(height: 8.h),
// 描述
Text(
'用你喜欢的歌手声音演唱任何歌曲,甚至是你自己创作的歌曲',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[600],
),
),
SizedBox(height: 16.h),
// 按钮
ElevatedButton(
onPressed: controller.tryNewFeature,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple.shade500,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
child: Text(
'立即尝试',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
}

492
lib/modules/home/controllers/home_controller.dart

@ -1,131 +1,467 @@
import 'package:get/get.dart';
import 'dart:async';
import 'dart:io';
import '../../../core/routes/app_routes.dart';
import '../../../data/services/bluetooth_service.dart';
import '../../../core/utils/logger.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart' hide BluetoothService;
import 'package:flutter/material.dart';
import '../../../data/services/voice_interaction_service.dart';
enum ConnectStatus {
initial, // 初始状态,显示添加设备
searching, // 搜索中
connected, // 连接成功
// 连接状态枚举
enum ConnectionStatus {
disconnected, // 未连接
connecting, // 连接中
connected, // 已连接
failed // 连接失败
}
class HomeController extends GetxController {
static HomeController get to => Get.find();
final _connectStatus = ConnectStatus.initial.obs;
final _searchDots = ''.obs;
Timer? _animationTimer;
bool _isDisposed = false;
// 设备连接状态
final _isConnected = false.obs;
bool get isConnected => _isConnected.value;
ConnectStatus get connectStatus => _connectStatus.value;
String get searchDots => _searchDots.value;
// 连接状态
final _connectionStatus = ConnectionStatus.disconnected.obs;
ConnectionStatus get connectionStatus => _connectionStatus.value;
// 电池状态
final _leftEarbudBattery = 75.obs;
final _rightEarbudBattery = 52.obs;
final _caseBattery = 92.obs;
int get leftEarbudBattery => _leftEarbudBattery.value;
int get rightEarbudBattery => _rightEarbudBattery.value;
int get caseBattery => _caseBattery.value;
// 固件版本
final String firmwareVersion = 'v2.4.1';
// 蓝牙设备信息
final _deviceInfo = <String, dynamic>{}.obs;
Map<String, dynamic> get deviceInfo => _deviceInfo.value;
// 蓝牙服务
BluetoothService? _bluetoothService;
// 连接错误信息
final _connectionErrorMsg = ''.obs;
String get connectionErrorMsg => _connectionErrorMsg.value;
@override
void onInit() {
super.onInit();
_connectStatus.value = ConnectStatus.initial;
VoiceInteractionService.to.initialize();
// 检查是否有传入的蓝牙设备信息
if (Get.arguments != null && Get.arguments is Map<String, dynamic>) {
_deviceInfo.assignAll(Get.arguments);
_updateConnectionStatus();
}
// 获取蓝牙服务
_initBluetoothService();
@override
void onClose() {
_isDisposed = true;
stopSearchingAnimation();
super.onClose();
// 模拟定期获取电池状态
_simulateBatteryUpdates();
}
void startSearchingAnimation() {
stopSearchingAnimation(); // Clear any existing timer
// 初始化蓝牙服务
void _initBluetoothService() {
try {
if (Get.isRegistered<BluetoothService>()) {
_bluetoothService = Get.find<BluetoothService>();
if (_isDisposed) return;
// 如果已经有连接的设备但没有设备信息
if (_deviceInfo.isEmpty && _bluetoothService!.selectedDevice != null) {
_deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo());
_updateConnectionStatus();
} else if (_deviceInfo.isEmpty) {
// 如果没有设备信息,尝试获取已配对设备
_checkPairedDevices();
}
int count = 0;
_animationTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
if (_isDisposed || _connectStatus.value != ConnectStatus.searching) {
timer.cancel();
return;
// 监听连接状态变化
ever(_bluetoothService!.connectionStateObs, (state) {
_processConnectionStateChange(state);
});
// 监听选中设备变化
ever(_bluetoothService!.selectedDeviceObs, (device) {
if (device != null) {
_deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo());
_updateConnectionStatus();
} else if (_isConnected.value) {
// 如果设备断开连接,更新状态
_isConnected.value = false;
_deviceInfo.clear();
_connectionStatus.value = ConnectionStatus.disconnected;
}
count = (count + 1) % 4;
_searchDots.value = '.' * count;
});
}
} catch (e) {
Logger.error('获取蓝牙服务失败: $e');
_connectionStatus.value = ConnectionStatus.failed;
_connectionErrorMsg.value = '获取蓝牙服务失败';
}
}
void stopSearchingAnimation() {
_animationTimer?.cancel();
_animationTimer = null;
if (!_isDisposed) {
_searchDots.value = '';
// 处理连接状态变化
void _processConnectionStateChange(BluetoothConnectionState state) {
_deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo());
switch (state) {
case BluetoothConnectionState.connected:
_connectionStatus.value = ConnectionStatus.connected;
_connectionErrorMsg.value = '';
_showConnectionSuccessMessage();
break;
case BluetoothConnectionState.disconnected:
_connectionStatus.value = ConnectionStatus.disconnected;
break;
case BluetoothConnectionState.connecting:
_connectionStatus.value = ConnectionStatus.connecting;
break;
default:
// 其他状态暂不处理
break;
}
_updateConnectionStatus();
}
Future<void> connectDevice() async {
if (_connectStatus.value == ConnectStatus.searching) return;
// 显示连接成功消息
void _showConnectionSuccessMessage() {
if (_bluetoothService?.selectedDevice != null) {
String deviceName = _bluetoothService!.selectedDevice!.platformName.isEmpty ?
'未命名设备' : _bluetoothService!.selectedDevice!.platformName;
Get.snackbar(
'连接成功',
'已成功连接到设备: $deviceName',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green[100],
colorText: Colors.green[800],
duration: const Duration(seconds: 2),
);
}
}
// 尝试查找并连接已配对设备
Future<void> _checkPairedDevices() async {
try {
if (_bluetoothService != null) {
// 更新状态为正在连接
_connectionStatus.value = ConnectionStatus.connecting;
// 如果已有选中的设备并且已连接,直接使用
if (_bluetoothService!.selectedDevice != null &&
_bluetoothService!.connectionState == BluetoothConnectionState.connected) {
Logger.info('使用已连接的设备: ${_bluetoothService!.selectedDevice!.platformName}');
_deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo());
_connectionStatus.value = ConnectionStatus.connected;
_updateConnectionStatus();
return;
}
_connectStatus.value = ConnectStatus.searching;
startSearchingAnimation();
// 获取已配对设备
final pairedDevices = await _bluetoothService!.getPairedDevices();
if (pairedDevices.isEmpty) {
Logger.info('没有找到已配对设备');
_connectionStatus.value = ConnectionStatus.disconnected;
return;
}
// 遍历配对设备,查找已连接的设备
for (var device in pairedDevices) {
try {
// 模拟搜索和连接过程
await Future.delayed(const Duration(seconds: 3));
final state = await device.connectionState.first;
if (state == BluetoothConnectionState.connected) {
// 找到已连接的设备,更新UI
final success = await _bluetoothService!.connectToDevice(device);
if (success) {
Logger.info('使用已连接的已配对设备: ${device.platformName}');
_deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo());
_connectionStatus.value = ConnectionStatus.connected;
_updateConnectionStatus();
return;
}
}
} catch (e) {
// 忽略单个设备检查错误
Logger.error('检查设备连接状态失败: $e');
}
}
// 如果没有已连接的设备,尝试连接第一个配对设备
if (pairedDevices.isNotEmpty) {
Logger.info('尝试连接第一个配对设备: ${pairedDevices.first.platformName}');
final success = await _bluetoothService!.connectToDevice(pairedDevices.first);
if (success) {
Logger.info('自动连接到已配对设备: ${pairedDevices.first.platformName}');
_deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo());
_connectionStatus.value = ConnectionStatus.connected;
_updateConnectionStatus();
} else {
// 连接失败
_connectionStatus.value = ConnectionStatus.failed;
_connectionErrorMsg.value = '连接设备失败,请重试';
}
} else {
_connectionStatus.value = ConnectionStatus.disconnected;
}
}
} catch (e) {
Logger.error('检查已配对设备失败: $e');
_connectionStatus.value = ConnectionStatus.failed;
_connectionErrorMsg.value = '检查设备失败: ${e.toString()}';
}
}
// 更新连接状态
void _updateConnectionStatus() {
if (_deviceInfo.containsKey('connected')) {
_isConnected.value = _deviceInfo['connected'] ?? false;
// 如果设备信息显示已连接,但状态不是已连接,则更新状态
if (_isConnected.value && _connectionStatus.value != ConnectionStatus.connected) {
_connectionStatus.value = ConnectionStatus.connected;
} else if (!_isConnected.value && _connectionStatus.value == ConnectionStatus.connected) {
// 如果设备显示未连接,但状态是已连接,则更新状态
_connectionStatus.value = ConnectionStatus.disconnected;
}
} else {
_isConnected.value = false;
if (_connectionStatus.value == ConnectionStatus.connected) {
_connectionStatus.value = ConnectionStatus.disconnected;
}
}
}
// 模拟电池状态更新
void _simulateBatteryUpdates() {
// 在实际应用中,这里应该从设备获取真实的电池状态
// 这里只是模拟随机波动
}
if (_isDisposed) return; // Check if controller is disposed
// 探索AI功能
void exploreAIFeatures() {
Get.toNamed(Routes.explore);
}
// 模拟随机成功或失败
if (DateTime.now().millisecondsSinceEpoch % 2 == 0) {
_connectStatus.value = ConnectStatus.connected;
if (!_isDisposed) {
await Future.delayed(const Duration(seconds: 2)); // 显示成功状态2秒
// 更新固件
void updateFirmware() {
Get.snackbar(
'Success',
'Device connected successfully',
'提示',
'开始下载固件更新...',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green.withOpacity(0.1),
colorText: Colors.green,
duration: const Duration(seconds: 3),
);
// 模拟更新过程
// 在真实应用中,这里应该有固件更新的实际逻辑
}
} else {
_connectStatus.value = ConnectStatus.failed;
// 打开降噪功能
void toggleNoiseReduction() {
Get.snackbar(
'提示',
'降噪功能已开启',
snackPosition: SnackPosition.TOP,
);
}
} catch (e) {
if (!_isDisposed) {
_connectStatus.value = ConnectStatus.failed;
// 音效设置
void openSoundEffects() {
Get.snackbar(
'提示',
'音效设置功能尚未实现',
snackPosition: SnackPosition.TOP,
);
}
// 环境音功能
void toggleAmbientSound() {
Get.snackbar(
'Error',
'Failed to connect to device',
'提示',
'环境音模式已开启',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
duration: const Duration(seconds: 3),
);
}
} finally {
if (!_isDisposed && _connectStatus.value == ConnectStatus.searching) {
stopSearchingAnimation();
_connectStatus.value = ConnectStatus.failed;
// 收藏的设置
void openFavorites() {
Get.snackbar(
'提示',
'收藏功能尚未实现',
snackPosition: SnackPosition.TOP,
);
}
// 打开设置页面
void openSettings() {
Get.toNamed(Routes.settings);
}
// 重新扫描设备
void rescanDevices() {
if (_bluetoothService != null) {
Get.toNamed(Routes.pairing);
} else {
Get.snackbar(
'提示',
'蓝牙服务不可用',
snackPosition: SnackPosition.TOP,
);
}
}
// 获取设备名称
String getDeviceName() {
if (_deviceInfo.isEmpty || _deviceInfo['device'] == null) {
return '未连接设备';
}
final device = _deviceInfo['device'] as Map<String, dynamic>;
return device['name'] ?? '未命名设备';
}
void retryConnect() {
if (_isDisposed) return;
_connectStatus.value = ConnectStatus.initial;
connectDevice();
// 获取设备ID
String getDeviceId() {
if (_deviceInfo.isEmpty || _deviceInfo['device'] == null) {
return '';
}
void disconnectDevice() {
if (_isDisposed) return;
_connectStatus.value = ConnectStatus.initial;
final device = _deviceInfo['device'] as Map<String, dynamic>;
return device['id'] ?? '';
}
// 获取设备信号强度
String getSignalStrength() {
if (_deviceInfo.isEmpty || _deviceInfo['device'] == null) {
return '无信号';
}
final device = _deviceInfo['device'] as Map<String, dynamic>;
final rssi = device['rssi'];
if (rssi == null) {
return '未知';
}
// RSSI通常在-100到0之间,值越大信号越强
if (rssi > -60) {
return '强';
} else if (rssi > -80) {
return '中';
} else {
return '弱';
}
}
// 刷新设备信息
Future<void> refreshDeviceInfo() async {
try {
// 检查蓝牙是否可用
if (_bluetoothService == null) {
Get.snackbar(
'Disconnected',
'Device has been disconnected',
'提示',
'蓝牙服务不可用',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.blue.withOpacity(0.1),
colorText: Colors.blue,
duration: const Duration(seconds: 3),
backgroundColor: Colors.red[100],
colorText: Colors.red[800],
);
return;
}
// 设置正在刷新状态
_connectionStatus.value = ConnectionStatus.connecting;
if (_bluetoothService!.selectedDevice != null) {
// 如果有选中的设备,刷新设备信息
_deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo());
_updateConnectionStatus();
// 检查刷新后的连接状态
if (_isConnected.value) {
Get.snackbar(
'提示',
'设备信息已更新',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green[100],
colorText: Colors.green[800],
);
} else {
// 设备可能已断开连接
_connectionStatus.value = ConnectionStatus.disconnected;
Get.snackbar(
'提示',
'设备已断开连接',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.amber[100],
colorText: Colors.amber[800],
);
// 尝试重新连接
await _checkPairedDevices();
}
} else {
// 如果没有选中的设备,尝试连接已配对设备
await _checkPairedDevices();
if (_connectionStatus.value == ConnectionStatus.failed) {
Get.snackbar(
'提示',
_connectionErrorMsg.value.isNotEmpty ? _connectionErrorMsg.value : '连接设备失败',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red[100],
colorText: Colors.red[800],
);
} else if (_connectionStatus.value == ConnectionStatus.disconnected) {
Get.snackbar(
'提示',
'未找到已连接设备,请连接设备',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.amber[100],
colorText: Colors.amber[800],
);
}
}
} catch (e) {
Logger.error('刷新设备信息失败: $e');
_connectionStatus.value = ConnectionStatus.failed;
_connectionErrorMsg.value = '刷新设备信息失败: ${e.toString()}';
Get.snackbar(
'提示',
'刷新设备信息失败',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red[100],
colorText: Colors.red[800],
);
}
}
// 处理连接失败
String getConnectionStatusText() {
switch (_connectionStatus.value) {
case ConnectionStatus.connected:
return "已连接";
case ConnectionStatus.connecting:
return "连接中...";
case ConnectionStatus.disconnected:
return "未连接";
case ConnectionStatus.failed:
return "连接失败";
}
}
// 获取连接状态颜色
Color getConnectionStatusColor() {
switch (_connectionStatus.value) {
case ConnectionStatus.connected:
return Colors.green;
case ConnectionStatus.connecting:
return Colors.blue;
case ConnectionStatus.disconnected:
return Colors.red;
case ConnectionStatus.failed:
return Colors.red;
}
}
}

732
lib/modules/home/views/home_view.dart

@ -3,281 +3,685 @@ import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/home_controller.dart';
import '../../../core/widgets/common_bottom_nav.dart';
import '../../../core/routes/app_routes.dart';
// 导入枚举
import '../controllers/home_controller.dart' show ConnectionStatus;
class HomeView extends GetView<HomeController> {
const HomeView({Key? key}) : super(key: key);
Widget _buildStatusWidget() {
return Obx(() {
switch (controller.connectStatus) {
case ConnectStatus.searching:
return Column(
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
body: SafeArea(
child: Column(
children: [
Text(
'searching'.tr + controller.searchDots,
style: TextStyle(
fontSize: 16.sp,
color: Colors.grey[600],
// 状态栏和标题区域
_buildHeader(),
// 主要内容区域(可滚动)
Expanded(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 耳机图片
_buildEarbudsImage(),
// 设备状态卡片
SizedBox(height: 16.h),
_buildDeviceStatusCard(),
// AI功能部分
SizedBox(height: 16.h),
_buildAIFunctionsCard(),
// 固件更新提示
SizedBox(height: 16.h),
_buildFirmwareUpdateCard(),
// 快速功能访问
SizedBox(height: 24.h),
_buildQuickFunctions(),
],
),
),
),
),
],
),
),
bottomNavigationBar: const CommonBottomNav(currentIndex: 0),
);
}
// 页面顶部:状态栏和标题
Widget _buildHeader() {
return Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
child: Column(
children: [
// 删除状态栏
// 标题区域
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'searching_hint'.tr,
'DeepVoice Pro',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[400],
height: 2,
fontSize: 20.sp,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: 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.more_horiz, size: 16.sp, color: Colors.grey[600]),
padding: EdgeInsets.zero,
onPressed: () {
// 打开更多选项菜单
},
),
),
],
),
],
),
);
case ConnectStatus.connected:
return Column(
}
// 耳机图片
Widget _buildEarbudsImage() {
return Container(
height: 180.h,
alignment: Alignment.center,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.r),
child: Container(
color: Colors.white,
padding: EdgeInsets.all(16.w),
child: Icon(
Icons.headset,
size: 120.sp,
color: Colors.blueGrey,
),
),
),
),
);
}
// 设备状态卡片
Widget _buildDeviceStatusCard() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
padding: EdgeInsets.all(16.w),
child: Column(
children: [
// 状态标题行
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Icon(
Icons.check_circle,
color: Colors.green,
size: 20.sp,
),
SizedBox(width: 8.w),
Text(
'connected'.tr,
'设备状态',
style: TextStyle(
fontSize: 16.sp,
color: Colors.green,
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: Colors.grey[700],
),
),
],
Obx(() => Row(
children: [
Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: controller.getConnectionStatusColor(),
shape: BoxShape.circle,
),
),
SizedBox(width: 4.w),
Text(
'device_ready'.tr,
controller.getConnectionStatusText(),
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[400],
height: 2,
fontSize: 12.sp,
color: controller.getConnectionStatusColor(),
),
),
// 添加刷新按钮
SizedBox(width: 8.w),
InkWell(
onTap: controller.refreshDeviceInfo,
child: Icon(
Icons.refresh,
size: 16.sp,
color: Colors.grey[600],
),
),
],
);
case ConnectStatus.failed:
)),
],
),
// 设备信息
SizedBox(height: 12.h),
Obx(() {
// 如果连接失败且有错误信息,显示错误提示
if (controller.connectionStatus == ConnectionStatus.failed &&
controller.connectionErrorMsg.isNotEmpty) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Colors.red[200]!),
),
child: Row(
children: [
Icon(
Icons.error,
color: Colors.red[400],
size: 20.sp,
Icons.error_outline,
color: Colors.red,
size: 16.sp,
),
SizedBox(width: 8.w),
Text(
'connection_failed'.tr,
Expanded(
child: Text(
controller.connectionErrorMsg,
style: TextStyle(
fontSize: 16.sp,
color: Colors.red[400],
fontWeight: FontWeight.w500,
fontSize: 12.sp,
color: Colors.red[800],
),
),
),
],
),
),
SizedBox(height: 12.h),
// 连接按钮
ElevatedButton(
onPressed: controller.rescanDevices,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
minimumSize: Size(double.infinity, 36.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text('重新连接'),
),
],
);
}
// 如果正在连接中,显示连接中提示
if (controller.connectionStatus == ConnectionStatus.connecting) {
return Container(
padding: EdgeInsets.symmetric(vertical: 16.h),
child: Center(
child: Column(
children: [
SizedBox(
width: 24.w,
height: 24.w,
child: CircularProgressIndicator(
strokeWidth: 2.w,
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
),
),
SizedBox(height: 8.h),
Text(
'connection_failed_hint'.tr,
textAlign: TextAlign.center,
'正在连接设备...',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[500],
height: 1.8,
fontSize: 12.sp,
color: Colors.blue,
),
),
],
),
),
);
default:
return const SizedBox.shrink();
}
});
}
Widget _buildActionButton() {
return Obx(() {
switch (controller.connectStatus) {
case ConnectStatus.searching:
return Container(
width: double.infinity,
height: 56.h,
margin: EdgeInsets.symmetric(horizontal: 20.w),
return controller.isConnected
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'已连接到:${controller.getDeviceName()}',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: 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(
'信号: ${controller.getSignalStrength()}',
style: TextStyle(
fontSize: 10.sp,
color: Colors.blue[700],
),
),
),
],
),
SizedBox(height: 8.h),
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 6.h),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(8.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'设备ID:${controller.getDeviceId()}',
style: TextStyle(
fontSize: 10.sp,
color: Colors.grey[500],
),
),
SizedBox(height: 4.h),
Text(
'固件版本:${controller.firmwareVersion}',
style: TextStyle(
fontSize: 10.sp,
color: Colors.grey[500],
),
),
],
),
),
],
)
: Container(
padding: EdgeInsets.symmetric(vertical: 8.h),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(28.r),
borderRadius: BorderRadius.circular(8.r),
),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20.w,
height: 20.w,
child: const CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF4A6FE5)),
),
Icon(
Icons.bluetooth_disabled,
size: 14.sp,
color: Colors.grey[700],
),
SizedBox(width: 12.w),
SizedBox(width: 4.w),
Text(
'searching'.tr,
'未连接设备,点击下方按钮连接',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.grey[400],
fontSize: 12.sp,
color: Colors.grey[700],
),
),
],
),
),
);
case ConnectStatus.failed:
return TextButton(
onPressed: controller.retryConnect,
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 40.w, vertical: 12.h),
backgroundColor: Colors.blue[50],
}),
// 连接按钮(仅在未连接时显示)
SizedBox(height: 12.h),
Obx(() {
if (controller.connectionStatus == ConnectionStatus.disconnected) {
return ElevatedButton(
onPressed: controller.rescanDevices,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
minimumSize: Size(double.infinity, 36.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28.r),
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text(
'retry'.tr,
style: TextStyle(
fontSize: 18.sp,
color: const Color(0xFF4A6FE5),
fontWeight: FontWeight.w600,
child: Text('连接设备'),
);
} else {
return SizedBox();
}
}),
// 电池状态
SizedBox(height: 12.h),
Obx(() => controller.isConnected
? Row(
children: [
// 左耳机
Expanded(
child: _buildBatteryItem('左耳机', controller.leftEarbudBattery, Icons.headset),
),
SizedBox(width: 12.w),
// 右耳机
Expanded(
child: _buildBatteryItem('右耳机', controller.rightEarbudBattery, Icons.headset),
),
SizedBox(width: 12.w),
// 充电盒
Expanded(
child: _buildBatteryItem('充电盒', controller.caseBattery, Icons.cases_outlined),
),
],
)
: const SizedBox()),
],
),
);
case ConnectStatus.connected:
}
// 电池状态项
Widget _buildBatteryItem(String label, int percentage, IconData icon) {
Color batteryColor;
IconData batteryIcon;
// 根据电量设置颜色和图标
if (percentage >= 70) {
batteryColor = Colors.green;
batteryIcon = Icons.battery_full;
} else if (percentage >= 30) {
batteryColor = Colors.amber;
batteryIcon = Icons.battery_5_bar;
} else {
batteryColor = Colors.red;
batteryIcon = Icons.battery_1_bar;
}
return Container(
width: double.infinity,
height: 56.h,
margin: EdgeInsets.symmetric(horizontal: 20.w),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF4CAF50), Color(0xFF66BB6A)],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: BorderRadius.circular(28.r),
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: const Color(0xFF4CAF50).withOpacity(0.3),
offset: const Offset(0, 4),
blurRadius: 12,
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: controller.disconnectDevice,
borderRadius: BorderRadius.circular(28.r),
child: Center(
child: Text(
'disconnect'.tr,
padding: EdgeInsets.all(12.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 24.sp, color: Colors.grey[700]),
SizedBox(height: 8.h),
Text(
label,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.5,
fontSize: 12.sp,
color: Colors.grey[500],
),
),
SizedBox(height: 4.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(batteryIcon, size: 14.sp, color: batteryColor),
SizedBox(width: 4.w),
Text(
'$percentage%',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: Colors.grey[700],
),
),
],
),
],
),
);
default:
}
// AI功能卡片
Widget _buildAIFunctionsCard() {
return Container(
width: double.infinity,
height: 56.h,
margin: EdgeInsets.symmetric(horizontal: 20.w),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF4A6FE5), Color(0xFF6B8FF7)],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.blue.shade100,
Colors.purple.shade100,
],
),
borderRadius: BorderRadius.circular(28.r),
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: const Color(0xFF4A6FE5).withOpacity(0.3),
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
blurRadius: 12,
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: controller.connectDevice,
borderRadius: BorderRadius.circular(28.r),
child: Center(
child: Text(
'add_device'.tr,
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'AI助手',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
letterSpacing: 0.5,
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: Colors.grey[800],
),
),
SizedBox(height: 4.h),
Text(
'探索DeepVoice的智能功能',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[600],
),
),
],
),
ElevatedButton(
onPressed: controller.exploreAIFeatures,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[600],
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24.r),
),
elevation: 2,
),
child: Text(
'探索',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Column(
// 固件更新卡片
Widget _buildFirmwareUpdateCard() {
return Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: Colors.amber[100],
shape: BoxShape.circle,
),
child: Icon(
Icons.download,
size: 20.sp,
color: Colors.amber[600],
),
),
SizedBox(width: 12.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 40.h),
Text(
'app_name'.tr,
'固件更新',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold,
color: Colors.black87,
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: Colors.grey[800],
),
),
SizedBox(height: 40.h),
Image.asset(
'assets/images/headphone.png',
width: 200.w,
height: 200.w,
fit: BoxFit.contain,
Text(
'${controller.firmwareVersion}可用',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[500],
),
Expanded(
child: Center(
child: _buildStatusWidget(),
),
],
),
_buildActionButton(),
SizedBox(height: 40.h),
],
),
ElevatedButton(
onPressed: controller.updateFirmware,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[200],
foregroundColor: Colors.grey[700],
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24.r),
),
elevation: 0,
),
bottomNavigationBar: const CommonBottomNav(currentIndex: 0),
child: Text(
'更新',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
),
),
],
),
);
}
// 快速功能访问
Widget _buildQuickFunctions() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildQuickFunctionItem('降噪', Icons.volume_off, Colors.blue[600]!, Colors.blue[100]!, controller.toggleNoiseReduction),
_buildQuickFunctionItem('音效', Icons.music_note, Colors.purple[600]!, Colors.purple[100]!, controller.openSoundEffects),
_buildQuickFunctionItem('环境音', Icons.eco, Colors.green[600]!, Colors.green[100]!, controller.toggleAmbientSound),
_buildQuickFunctionItem('收藏', Icons.favorite, Colors.red[600]!, Colors.red[100]!, controller.openFavorites),
],
);
}
// 快速功能项
Widget _buildQuickFunctionItem(String label, IconData icon, Color iconColor, Color bgColor, VoidCallback onTap) {
return Column(
children: [
Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
color: bgColor,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: IconButton(
icon: Icon(icon, color: iconColor),
onPressed: onTap,
),
),
SizedBox(height: 4.h),
Text(
label,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[700],
),
),
],
);
}
}

9
lib/modules/login/bindings/login_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/login_controller.dart';
class LoginBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<LoginController>(() => LoginController());
}
}

246
lib/modules/login/controllers/login_controller.dart

@ -0,0 +1,246 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import '../../../core/routes/app_routes.dart';
enum LoginViewState {
welcome, // 欢迎页面
verification // 验证码页面
}
class LoginController extends GetxController {
static LoginController get to => Get.find();
// 状态管理
final _viewState = LoginViewState.welcome.obs;
final _isLoading = false.obs;
final _countdownSeconds = 0.obs;
final _contactValue = ''.obs;
final _displayContact = ''.obs;
final _verificationCode = ['', '', '', ''].obs;
// Getters
LoginViewState get viewState => _viewState.value;
bool get isLoading => _isLoading.value;
int get countdownSeconds => _countdownSeconds.value;
String get contactValue => _contactValue.value;
String get displayContact => _displayContact.value;
List<String> get verificationCode => _verificationCode;
// 文本控制器
final contactController = TextEditingController();
final verificationCodeController = TextEditingController();
@override
void onInit() {
super.onInit();
_viewState.value = LoginViewState.welcome;
}
@override
void onClose() {
contactController.dispose();
verificationCodeController.dispose();
super.onClose();
}
// 获取验证码
void getVerificationCode() {
if (contactController.text.isEmpty) {
Get.snackbar(
'提示',
'请输入手机号码或邮箱',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
return;
}
_contactValue.value = contactController.text;
_formatDisplayContact();
_startCountdown();
_sendVerificationCode();
}
// 使用验证码直接登录
void loginWithVerificationCode() {
//测试期间暂时关闭验证码登录
// if (contactController.text.isEmpty) {
// Get.snackbar(
// '提示',
// '请输入手机号码或邮箱',
// snackPosition: SnackPosition.TOP,
// backgroundColor: Colors.red.withOpacity(0.1),
// colorText: Colors.red,
// );
// return;
// }
// if (verificationCodeController.text.isEmpty) {
// Get.snackbar(
// '提示',
// '请输入验证码',
// snackPosition: SnackPosition.TOP,
// backgroundColor: Colors.red.withOpacity(0.1),
// colorText: Colors.red,
// );
// return;
// }
// if (verificationCodeController.text.length < 6) {
// Get.snackbar(
// '提示',
// '请输入完整的验证码',
// snackPosition: SnackPosition.TOP,
// backgroundColor: Colors.red.withOpacity(0.1),
// colorText: Colors.red,
// );
// return;
// }
_isLoading.value = true;
// 模拟验证过程
Future.delayed(const Duration(seconds: 2), () {
_isLoading.value = false;
// 验证成功,跳转到配对页面
Get.offAllNamed(Routes.pairing);
});
}
// 切换到验证码页面
void showVerificationView() {
if (contactController.text.isEmpty) {
Get.snackbar(
'提示',
'请输入手机号码或邮箱',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
return;
}
_contactValue.value = contactController.text;
_formatDisplayContact();
_viewState.value = LoginViewState.verification;
_startCountdown();
_sendVerificationCode();
}
// 返回欢迎页面
void showWelcomeView() {
_viewState.value = LoginViewState.welcome;
_resetVerificationCode();
}
// 格式化显示的联系方式(隐藏部分字符)
void _formatDisplayContact() {
String contact = _contactValue.value;
if (contact.contains('@')) { // 邮箱
final parts = contact.split('@');
if (parts[0].length > 3) {
_displayContact.value = parts[0].substring(0, 3) + '****@' + parts[1];
} else {
_displayContact.value = contact;
}
} else if (RegExp(r'^\d+$').hasMatch(contact)) { // 手机号
if (contact.length > 7) {
_displayContact.value = '+86 ' + contact.substring(0, 3) + '****' + contact.substring(contact.length - 4);
} else {
_displayContact.value = contact;
}
} else {
_displayContact.value = contact;
}
}
// 开始倒计时
void _startCountdown() {
_countdownSeconds.value = 60;
Future.doWhile(() async {
await Future.delayed(const Duration(seconds: 1));
_countdownSeconds.value--;
return _countdownSeconds.value > 0;
});
}
// 发送验证码
void _sendVerificationCode() {
// 这里应该是实际发送验证码的逻辑
// 为了演示,我们只是模拟发送
}
// 重新发送验证码
void resendVerificationCode() {
if (_countdownSeconds.value > 0) return;
_startCountdown();
_sendVerificationCode();
}
// 更新验证码输入
void updateVerificationCode(int index, String value) {
if (index >= 0 && index < 4) {
var newList = [..._verificationCode];
newList[index] = value;
_verificationCode.value = newList;
}
}
// 重置验证码
void _resetVerificationCode() {
_verificationCode.value = ['', '', '', ''];
}
// 检查验证码是否完整
bool isVerificationCodeComplete() {
return !_verificationCode.contains('');
}
// 登录
Future<void> login() async {
// 暂时关闭验证码验证
if (!isVerificationCodeComplete()) {
Get.snackbar(
'提示',
'请输入完整的验证码',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
return;
}
_isLoading.value = true;
try {
// 模拟验证过程
await Future.delayed(const Duration(seconds: 2));
// 验证成功,跳转到配对页面
Get.offAllNamed(Routes.pairing);
} catch (e) {
Get.snackbar(
'错误',
'登录失败,请重试',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
} finally {
_isLoading.value = false;
}
}
// 使用密码登录
void usePasswordLogin() {
// 这里可以实现密码登录的逻辑
Get.snackbar(
'提示',
'密码登录功能尚未实现',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.blue.withOpacity(0.1),
colorText: Colors.blue,
);
}
}

628
lib/modules/login/views/login_view.dart

@ -0,0 +1,628 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import '../controllers/login_controller.dart';
class LoginView extends GetView<LoginController> {
const LoginView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
body: SafeArea(
child: Obx(() => controller.viewState == LoginViewState.welcome
? _buildWelcomeView()
: _buildVerificationView(context)),
),
);
}
// 欢迎页面
Widget _buildWelcomeView() {
return Padding(
padding: EdgeInsets.all(24.w),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
children: [
// 状态栏
_buildStatusBar(),
// 应用标志和标题
SizedBox(height: 32.h),
_buildAppLogo(),
// 欢迎文本
SizedBox(height: 32.h),
_buildWelcomeText(),
// 输入框和继续按钮
SizedBox(height: 24.h),
_buildInputField(),
SizedBox(height: 16.h),
_buildVerificationInputField(),
SizedBox(height: 16.h),
_buildContinueButton(),
// 社交媒体登录
SizedBox(height: 32.h),
_buildSocialLoginSection(),
// 用户协议
SizedBox(height: 40.h),
_buildTermsAndPrivacy(),
SizedBox(height: 24.h),
],
),
),
);
}
// 验证码页面
Widget _buildVerificationView(BuildContext context) {
return Padding(
padding: EdgeInsets.all(24.w),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 返回按钮
_buildBackButton(),
// 验证码标题
SizedBox(height: 24.h),
_buildVerificationTitle(),
// 验证码输入框
SizedBox(height: 32.h),
_buildVerificationInputs(context),
// 重新发送验证码
SizedBox(height: 16.h),
_buildResendButton(),
// 登录按钮
SizedBox(height: 32.h),
_buildLoginButton(),
// 使用密码登录
SizedBox(height: 16.h),
_buildUsePasswordButton(),
SizedBox(height: 24.h),
],
),
),
);
}
// 状态栏
Widget _buildStatusBar() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'9:41',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[800],
),
),
Row(
children: [
Icon(Icons.signal_cellular_4_bar, size: 12.sp, color: Colors.grey[800]),
SizedBox(width: 4.w),
Icon(Icons.wifi, size: 12.sp, color: Colors.grey[800]),
SizedBox(width: 4.w),
Icon(Icons.battery_full, size: 12.sp, color: Colors.grey[800]),
],
),
],
);
}
// 应用标志和标题
Widget _buildAppLogo() {
return Column(
children: [
Container(
width: 80.w,
height: 80.w,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.blue, Colors.purple],
),
borderRadius: BorderRadius.circular(24.r),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Icon(
Icons.headset,
size: 40.sp,
color: Colors.white,
),
),
SizedBox(height: 16.h),
Text(
'DeepVoice',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
SizedBox(height: 8.h),
Text(
'连接世界,超越声音',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[500],
),
),
],
);
}
// 欢迎文本
Widget _buildWelcomeText() {
return Column(
children: [
Text(
'欢迎使用',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.w600,
color: Colors.grey[800],
),
),
SizedBox(height: 4.h),
Text(
'请使用手机号或邮箱登录/注册',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[500],
),
),
],
);
}
// 输入框
Widget _buildInputField() {
return Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 4.h),
child: Row(
children: [
Icon(
Icons.person_outline,
color: Colors.grey[400],
size: 20.sp,
),
SizedBox(width: 12.w),
Expanded(
child: TextField(
controller: controller.contactController,
decoration: InputDecoration(
hintText: '手机号码或邮箱',
hintStyle: TextStyle(
fontSize: 14.sp,
color: Colors.grey[400],
),
border: InputBorder.none,
),
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[700],
),
),
),
],
),
),
);
}
// 验证码输入框
Widget _buildVerificationInputField() {
return Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Padding(
padding: EdgeInsets.only(left: 16.w, right: 4.w, top: 4.h, bottom: 4.h),
child: Row(
children: [
Icon(
Icons.lock_outline,
color: Colors.grey[400],
size: 20.sp,
),
SizedBox(width: 12.w),
Expanded(
child: TextField(
controller: controller.verificationCodeController,
keyboardType: TextInputType.number,
maxLength: 6,
decoration: InputDecoration(
hintText: '验证码',
hintStyle: TextStyle(
fontSize: 14.sp,
color: Colors.grey[400],
),
border: InputBorder.none,
counterText: '',
),
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[700],
),
),
),
Obx(() => TextButton(
onPressed: controller.countdownSeconds > 0
? null
: controller.getVerificationCode,
style: TextButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 16.w),
foregroundColor: Colors.blue[600],
backgroundColor: Colors.transparent,
disabledForegroundColor: Colors.grey[400],
),
child: Text(
controller.countdownSeconds > 0
? '${controller.countdownSeconds}s后重新获取'
: '获取验证码',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
),
),
)),
],
),
),
);
}
// 继续按钮
Widget _buildContinueButton() {
return SizedBox(
width: double.infinity,
child: Obx(() => ElevatedButton(
onPressed: controller.isLoading ? null : controller.loginWithVerificationCode,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[600],
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
elevation: 2,
),
child: controller.isLoading
? SizedBox(
width: 20.w,
height: 20.w,
child: const CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
'登录',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
)),
);
}
// 社交媒体登录部分
Widget _buildSocialLoginSection() {
return Column(
children: [
Row(
children: [
Expanded(child: Divider(color: Colors.grey[300])),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Text(
'或使用以下方式登录',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[500],
),
),
),
Expanded(child: Divider(color: Colors.grey[300])),
],
),
SizedBox(height: 24.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildSocialButton(FontAwesomeIcons.google, Colors.grey[700]!),
SizedBox(width: 24.w),
_buildSocialButton(FontAwesomeIcons.weixin, Colors.green),
],
),
],
);
}
// 社交媒体按钮
Widget _buildSocialButton(IconData icon, Color color) {
return Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(24.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: IconButton(
icon: Icon(icon, color: color, size: 20.sp),
onPressed: () {},
),
);
}
// 用户协议和隐私政策
Widget _buildTermsAndPrivacy() {
return Column(
children: [
Text(
'登录/注册即表示你同意',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[500],
),
),
SizedBox(height: 4.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: () {},
child: Text(
'《用户协议》',
style: TextStyle(
fontSize: 12.sp,
color: Colors.blue[600],
),
),
),
Text(
' 和 ',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[500],
),
),
GestureDetector(
onTap: () {},
child: Text(
'《隐私政策》',
style: TextStyle(
fontSize: 12.sp,
color: Colors.blue[600],
),
),
),
],
),
],
);
}
// 返回按钮
Widget _buildBackButton() {
return Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: IconButton(
icon: Icon(Icons.arrow_back, color: Colors.grey[500], size: 20.sp),
onPressed: controller.showWelcomeView,
),
);
}
// 验证码标题
Widget _buildVerificationTitle() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'输入验证码',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.w600,
color: Colors.grey[800],
),
),
SizedBox(height: 8.h),
Obx(() => Text(
'验证码已发送至 ${controller.displayContact}',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[500],
),
)),
],
);
}
// 验证码输入框
Widget _buildVerificationInputs(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(4, (index) => _buildVerificationInput(index, context)),
);
}
// 单个验证码输入框
Widget _buildVerificationInput(int index, BuildContext context) {
return Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
color: Colors.grey[100]!.withOpacity(0.5),
borderRadius: BorderRadius.circular(16.r),
border: Border.all(
color: Colors.grey[300]!,
width: 1,
),
),
child: Center(
child: TextField(
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
maxLength: 1,
onChanged: (value) {
controller.updateVerificationCode(index, value);
if (value.isNotEmpty && index < 3) {
FocusScope.of(context).nextFocus();
}
},
decoration: const InputDecoration(
counterText: '',
border: InputBorder.none,
),
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w600,
color: Colors.grey[800],
),
),
),
);
}
// 重新发送验证码按钮
Widget _buildResendButton() {
return Center(
child: Obx(() => TextButton.icon(
onPressed: controller.countdownSeconds > 0
? null
: controller.resendVerificationCode,
icon: Icon(
Icons.refresh,
size: 14.sp,
color: controller.countdownSeconds > 0
? Colors.grey[400]
: Colors.blue[600],
),
label: Text(
controller.countdownSeconds > 0
? '重新发送验证码 (${controller.countdownSeconds}s)'
: '重新发送验证码',
style: TextStyle(
fontSize: 14.sp,
color: controller.countdownSeconds > 0
? Colors.grey[400]
: Colors.blue[600],
),
),
)),
);
}
// 登录按钮
Widget _buildLoginButton() {
return SizedBox(
width: double.infinity,
child: Obx(() => ElevatedButton(
onPressed: controller.isLoading ? null : controller.login,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[600],
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
elevation: 2,
),
child: controller.isLoading
? SizedBox(
width: 20.w,
height: 20.w,
child: const CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
'登录',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
)),
);
}
// 使用密码登录按钮
Widget _buildUsePasswordButton() {
return Center(
child: TextButton(
onPressed: controller.usePasswordLogin,
child: Text(
'使用密码登录',
style: TextStyle(
fontSize: 14.sp,
color: Colors.blue[600],
),
),
),
);
}
}

9
lib/modules/pairing/bindings/pairing_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/pairing_controller.dart';
class PairingBinding extends Bindings {
@override
void dependencies() {
Get.put<PairingController>(PairingController());
}
}

295
lib/modules/pairing/controllers/pairing_controller.dart

@ -0,0 +1,295 @@
import 'dart:async';
import 'package:get/get.dart';
import '../../../core/routes/app_routes.dart';
import '../../../data/services/bluetooth_service.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart' hide BluetoothService;
import '../../../core/utils/logger.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter/material.dart';
enum SearchStatus {
searching, // 正在搜索设备
found, // 找到设备
failed, // 搜索失败
noPermission, // 缺少权限
}
class PairingController extends GetxController {
// 搜索状态
final _searchStatus = SearchStatus.searching.obs;
SearchStatus get searchStatus => _searchStatus.value;
// 搜索动画的点
final _searchDots = ''.obs;
String get searchDots => _searchDots.value;
// 动画定时器
Timer? _animationTimer;
// 蓝牙服务
late BluetoothService _bluetoothService;
// 设备信息
final _deviceInfo = Rx<Map<String, dynamic>>({});
Map<String, dynamic> get deviceInfo => _deviceInfo.value;
// 蓝牙状态
final _isBluetoothAvailable = false.obs;
final _isBluetoothOn = false.obs;
bool get isBluetoothAvailable => _isBluetoothAvailable.value;
bool get isBluetoothOn => _isBluetoothOn.value;
// 错误消息
final _errorMessage = ''.obs;
String get errorMessage => _errorMessage.value;
@override
void onInit() {
super.onInit();
// 启动搜索动画
_startSearchingAnimation();
// 延迟初始化蓝牙服务,避免在应用启动时就请求权限
// 改为延迟初始化,给UI足够时间渲染
Future.delayed(const Duration(milliseconds: 500), () {
_initBluetoothService();
});
}
@override
void onClose() {
_stopSearchingAnimation();
super.onClose();
}
// 初始化蓝牙服务
Future<void> _initBluetoothService() async {
try {
// 获取蓝牙服务实例
if (Get.isRegistered<BluetoothService>()) {
_bluetoothService = Get.find<BluetoothService>();
} else {
_bluetoothService = await Get.put(BluetoothService()).init();
}
// 监听蓝牙状态变化
ever(_bluetoothService.isAvailableObs, (bool available) {
_isBluetoothAvailable.value = available;
});
ever(_bluetoothService.isOnObs, (bool on) {
_isBluetoothOn.value = on;
// 如果蓝牙开启,开始扫描设备
if (on) {
// 延迟开始扫描,避免权限请求过早
Future.delayed(const Duration(milliseconds: 300), () {
_startScanningDevices();
});
} else {
_errorMessage.value = '请打开蓝牙以搜索设备';
_searchStatus.value = SearchStatus.failed;
_stopSearchingAnimation();
}
});
// 如果蓝牙已经开启,直接开始扫描
if (_bluetoothService.isOn) {
_isBluetoothOn.value = true;
// 延迟开始扫描,避免权限请求过早
Future.delayed(const Duration(milliseconds: 300), () {
_startScanningDevices();
});
} else {
_errorMessage.value = '请打开蓝牙以搜索设备';
}
} catch (e) {
Logger.error('初始化蓝牙服务失败: $e');
_errorMessage.value = '初始化蓝牙服务失败';
_searchStatus.value = SearchStatus.failed;
_stopSearchingAnimation();
}
}
// 启动搜索动画
void _startSearchingAnimation() {
_stopSearchingAnimation(); // 清除已有的定时器
int count = 0;
_animationTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) {
count = (count + 1) % 4;
_searchDots.value = '.' * count;
});
}
// 停止搜索动画
void _stopSearchingAnimation() {
_animationTimer?.cancel();
_animationTimer = null;
_searchDots.value = '';
}
// 请求必要权限
Future<bool> _requestPermissions() async {
// 暂时跳过权限检查,直接返回true
return true;
}
// 打开应用设置
void goToAppSettings() async {
try {
await openAppSettings();
} catch (e) {
Logger.error('打开应用设置失败: $e');
Get.snackbar(
'提示',
'无法打开应用设置,请手动打开系统设置授予权限',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red[100],
colorText: Colors.red[800],
duration: const Duration(seconds: 3),
);
}
}
// 开始扫描蓝牙设备
Future<void> _startScanningDevices() async {
try {
// 先检查是否已有连接的设备
if (_bluetoothService.connectedDevices.isNotEmpty) {
// 有已连接的设备,直接使用
_deviceFound(_bluetoothService.connectedDevices.first);
return;
}
// 检查是否有已配对的设备
if (await _checkPairedDevices()) {
// 已找到并成功连接到配对设备
return;
}
// 搜索新设备
_searchStatus.value = SearchStatus.searching;
await _bluetoothService.startScan(timeout: const Duration(seconds: 10));
// 检查扫描结果
if (_bluetoothService.scanResults.isNotEmpty) {
// 找到设备,选择信号最强的一个
final strongestSignal = _bluetoothService.scanResults
.reduce((a, b) => (a.rssi > b.rssi) ? a : b);
// 连接设备
final success = await _bluetoothService.connectToDevice(strongestSignal.device);
if (success) {
_deviceFound(strongestSignal.device);
} else {
_errorMessage.value = '连接设备失败,请重试';
_searchStatus.value = SearchStatus.failed;
_stopSearchingAnimation();
}
} else {
// 未找到设备
_errorMessage.value = '未找到蓝牙设备,请确保设备已开启并在附近';
_searchStatus.value = SearchStatus.failed;
_stopSearchingAnimation();
}
} catch (e) {
Logger.error('扫描蓝牙设备失败: $e');
_errorMessage.value = '扫描蓝牙设备失败: ${e.toString()}';
_searchStatus.value = SearchStatus.failed;
_stopSearchingAnimation();
}
}
// 检查已配对设备
Future<bool> _checkPairedDevices() async {
try {
// 先检查当前是否有设备已连接
if (_bluetoothService.selectedDevice != null &&
_bluetoothService.connectionState == BluetoothConnectionState.connected) {
Logger.info('已有设备连接: ${_bluetoothService.selectedDevice!.platformName}');
_deviceFound(_bluetoothService.selectedDevice!);
return true;
}
List<BluetoothDevice> pairedDevices = await _bluetoothService.getPairedDevices();
if (pairedDevices.isEmpty) {
Logger.info('没有找到已配对设备');
return false;
}
// 遍历配对设备,检查是否有设备已连接
for (BluetoothDevice device in pairedDevices) {
try {
BluetoothConnectionState state = await device.connectionState.first;
if (state == BluetoothConnectionState.connected) {
Logger.info('发现已连接的设备: ${device.platformName}');
// 设置为当前设备但不需要连接
final success = await _bluetoothService.connectToDevice(device);
if (success) {
_deviceFound(device);
return true;
}
}
} catch (e) {
// 忽略单个设备的连接状态检查错误,继续检查下一个
Logger.error('检查设备连接状态失败: $e');
}
}
// 如果没有找到已连接的设备,尝试连接第一个配对设备
Logger.info('尝试连接配对设备: ${pairedDevices.first.platformName}');
final success = await _bluetoothService.connectToDevice(pairedDevices.first);
if (success) {
_deviceFound(pairedDevices.first);
return true;
} else {
// 连接失败,继续搜索新设备
Logger.info('已配对设备连接失败,继续搜索新设备');
return false;
}
} catch (e) {
Logger.error('检查已配对设备失败: $e');
return false;
}
}
// 设备被找到时的处理
void _deviceFound(BluetoothDevice device) {
_searchStatus.value = SearchStatus.found;
_stopSearchingAnimation();
// 获取设备信息
_deviceInfo.value = _bluetoothService.getConnectedDeviceInfo();
// 延迟后跳转到主页,同时传递设备信息
Timer(const Duration(seconds: 1), () {
Get.offAllNamed(Routes.home, arguments: _deviceInfo.value);
});
}
// 手动连接
void manualConnect() {
if (_searchStatus.value == SearchStatus.searching) {
// 如果正在搜索,则停止搜索
_bluetoothService.stopScan();
}
// 重新开始搜索
_startSearchingAnimation();
_searchStatus.value = SearchStatus.searching;
_startScanningDevices();
}
// 稍后再说
void skipConnecting() {
if (_searchStatus.value == SearchStatus.searching) {
// 如果正在搜索,则停止搜索
_bluetoothService.stopScan();
}
Get.offAllNamed(Routes.home);
}
}

344
lib/modules/pairing/views/pairing_view.dart

@ -0,0 +1,344 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/pairing_controller.dart';
import '../../../core/widgets/common_bottom_nav.dart';
class PairingView extends GetView<PairingController> {
const PairingView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[50],
body: SafeArea(
child: Stack(
children: [
// 主要内容
_buildMainContent(),
],
),
),
bottomNavigationBar: Opacity(
opacity: 0.5, // 保持半透明效果
child: const CommonBottomNav(currentIndex: 0),
),
);
}
// 主要内容
Widget _buildMainContent() {
return Padding(
padding: EdgeInsets.fromLTRB(24.w, 40.h, 24.w, 24.h),
child: Column(
children: [
// 标题部分
_buildHeader(),
// 设备动画
SizedBox(height: 40.h),
_buildDeviceAnimation(),
// 搜索状态
SizedBox(height: 40.h),
_buildSearchStatus(),
// 底部按钮
const Spacer(),
_buildBottomButtons(),
SizedBox(height: 80.h), // 留出底部导航的空间
],
),
);
}
// 标题部分
Widget _buildHeader() {
return Column(
children: [
Text(
'DeepVoice',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
SizedBox(height: 8.h),
Text(
'连接世界,超越声音',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[500],
),
),
],
);
}
// 设备动画
Widget _buildDeviceAnimation() {
return SizedBox(
width: 240.w,
height: 240.w,
child: Stack(
alignment: Alignment.center,
children: [
// 脉动动画背景
_buildPulseBackground(),
// 耳机图片
_buildHeadphoneImage(),
// 旋转加载指示器
_buildLoadingIndicator(),
],
),
);
}
// 脉动背景
Widget _buildPulseBackground() {
return TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.8, end: 1.1),
duration: const Duration(seconds: 2),
curve: Curves.easeInOut,
builder: (context, value, child) {
return Transform.scale(
scale: value,
child: Container(
width: 200.w,
height: 200.w,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.blue.withOpacity(0.2),
Colors.purple.withOpacity(0.2),
],
),
shape: BoxShape.circle,
),
),
);
},
);
}
// 耳机图片
Widget _buildHeadphoneImage() {
return Container(
width: 140.w,
height: 140.w,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.blue,
Colors.purple,
],
),
shape: BoxShape.circle,
),
child: Icon(
Icons.headset,
size: 80.sp,
color: Colors.white,
),
);
}
// 加载指示器
Widget _buildLoadingIndicator() {
return SizedBox(
width: 180.w,
height: 180.w,
child: CircularProgressIndicator(
strokeWidth: 4.w,
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue),
backgroundColor: Colors.blue.withOpacity(0.1),
),
);
}
// 搜索状态
Widget _buildSearchStatus() {
return Obx(() {
switch (controller.searchStatus) {
case SearchStatus.searching:
return Column(
children: [
Text(
'寻找设备中${controller.searchDots}',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.grey[800],
),
),
SizedBox(height: 8.h),
Text(
'请打开耳机充电盒,将耳机放入您的耳朵',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[500],
),
),
],
);
case SearchStatus.found:
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.check_circle, color: Colors.green, size: 20.sp),
SizedBox(width: 8.w),
Text(
'设备连接成功',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.green,
),
),
],
),
SizedBox(height: 8.h),
Text(
'正在跳转到主页...',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[500],
),
),
],
);
case SearchStatus.noPermission:
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.no_encryption, color: Colors.orange, size: 20.sp),
SizedBox(width: 8.w),
Text(
'缺少必要权限',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.orange,
),
),
],
),
SizedBox(height: 8.h),
Text(
controller.errorMessage,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
SizedBox(height: 16.h),
ElevatedButton(
onPressed: () => controller.goToAppSettings(),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text('打开设置'),
),
],
);
case SearchStatus.failed:
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error, color: Colors.red, size: 20.sp),
SizedBox(width: 8.w),
Text(
'未找到设备',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.red,
),
),
],
),
SizedBox(height: 8.h),
Text(
controller.errorMessage.isEmpty ?
'请确保耳机已开机并在附近' : controller.errorMessage,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
],
);
default:
return SizedBox();
}
});
}
// 底部按钮
Widget _buildBottomButtons() {
return Column(
children: [
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: controller.manualConnect,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[200],
foregroundColor: Colors.grey[800],
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
elevation: 0,
),
child: Text(
'手动连接',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
),
),
),
),
SizedBox(height: 12.h),
SizedBox(
width: double.infinity,
child: TextButton(
onPressed: controller.skipConnecting,
style: TextButton.styleFrom(
foregroundColor: Colors.blue[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
),
child: Text(
'稍后再说',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
),
),
),
),
],
);
}
}

11
lib/modules/settings/bindings/settings_binding.dart

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import '../controllers/settings_controller.dart';
class SettingsBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<SettingsController>(
() => SettingsController(),
);
}
}

66
lib/modules/settings/controllers/settings_controller.dart

@ -0,0 +1,66 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
class SettingsController extends GetxController {
// 噪声消除开关状态
final RxBool noiseReductionEnabled = true.obs;
// 环境音增强模式
final RxString ambientSoundMode = '中等'.obs;
final List<String> availableAmbientModes = ['关闭', '低', '中等', '高'];
// 立体声平衡值
final RxDouble stereoBalance = 0.0.obs; // -1.0 (左) 到 1.0 (右), 0 为中间
// AI助理默认模式
final RxString aiAssistantMode = '通用助手'.obs;
final List<String> availableAiModes = ['通用助手', '翻译助手', '学习助手', '交际助手'];
// AI声音选择
final RxString aiVoice = '默认女声'.obs;
final List<String> availableVoices = ['默认女声', '默认男声', '温柔女声', '磁性男声', '活力女声'];
// 翻译语言对
final RxString sourceLanguage = '中文'.obs;
final RxString targetLanguage = '英文'.obs;
final List<String> availableLanguages = ['中文', '英文', '日文', '韩文', '法文', '德文', '西班牙文', '俄文'];
void toggleNoiseReduction(bool value) {
noiseReductionEnabled.value = value;
}
void updateAmbientSoundMode(String mode) {
ambientSoundMode.value = mode;
}
void updateStereoBalance(double value) {
stereoBalance.value = value;
}
void updateAiAssistantMode(String mode) {
aiAssistantMode.value = mode;
}
void updateAiVoice(String voice) {
aiVoice.value = voice;
}
void setSourceLanguage(String language) {
sourceLanguage.value = language;
}
void setTargetLanguage(String language) {
targetLanguage.value = language;
}
void swapLanguages() {
final temp = sourceLanguage.value;
sourceLanguage.value = targetLanguage.value;
targetLanguage.value = temp;
}
void signOut() {
// 在此实现退出登录逻辑
Get.offAllNamed('/login');
}
}

1158
lib/modules/settings/views/settings_view.dart

File diff suppressed because it is too large

9
lib/modules/splash/bindings/splash_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/splash_controller.dart';
class SplashBinding extends Bindings {
@override
void dependencies() {
Get.put<SplashController>(SplashController());
}
}

83
lib/modules/splash/controllers/splash_controller.dart

@ -0,0 +1,83 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../core/routes/app_routes.dart';
class SplashController extends GetxController {
// 进度条动画控制
final _loadingProgress = 0.0.obs;
double get loadingProgress => _loadingProgress.value;
// 版本号
final String version = 'v1.0.0';
// 资源加载状态
final _isResourceLoaded = false.obs;
bool get isResourceLoaded => _isResourceLoaded.value;
@override
void onInit() {
super.onInit();
// 确保页面先渲染,再执行其他操作
WidgetsBinding.instance.addPostFrameCallback((_) {
// 启动进度条动画
_startLoadingAnimation();
// 异步加载应用资源
_loadAppResources();
});
}
// 进度条动画
void _startLoadingAnimation() {
// 模拟0.5秒的加载进度
Timer.periodic(const Duration(milliseconds: 5), (timer) {
if (_loadingProgress.value < 1.0) {
_loadingProgress.value += 0.06;
} else {
timer.cancel();
// 当进度条动画完成且资源加载完毕时,跳转到登录页面
if (_isResourceLoaded.value) {
_navigateToLogin();
}
}
});
}
// 异步加载应用资源
Future<void> _loadAppResources() async {
try {
// 在这里可以添加应用初始化需要的资源加载
// 例如:预加载图片、初始化数据库、加载配置等
// 模拟耗时操作
await Future.delayed(const Duration(milliseconds: 100));
// TODO: 在此处添加实际的资源加载代码
// 标记资源加载完成
_isResourceLoaded.value = true;
// 如果进度条已经走完,则跳转到登录页面
if (_loadingProgress.value >= 1.0) {
_navigateToLogin();
}
} catch (e) {
// 处理资源加载错误
// 即使加载失败,也应该允许用户继续使用应用
_isResourceLoaded.value = true;
// 记录错误日志
print('资源加载错误: $e');
}
}
// 跳转到登录页面
void _navigateToLogin() {
// 添加一个小延迟,确保UI完全渲染
Future.delayed(const Duration(milliseconds: 200), () {
Get.offAllNamed(Routes.login);
});
}
}

256
lib/modules/splash/views/splash_view.dart

@ -0,0 +1,256 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/splash_controller.dart';
class SplashView extends GetView<SplashController> {
const SplashView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F7),
body: Stack(
children: [
// 状态栏
_buildStatusBar(),
// 主内容
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo动画
_buildAnimatedLogo(),
// 应用名称
_buildAppTitle(),
// 标语
_buildSlogan(),
// 加载进度条
SizedBox(height: 40.h),
_buildLoadingBar(),
// 额外信息
SizedBox(height: 16.h),
_buildExtraInfo(),
],
),
),
// 版本号
_buildVersionInfo(),
],
),
);
}
// 状态栏
Widget _buildStatusBar() {
return Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'9:41',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[800],
),
),
Row(
children: [
Icon(Icons.signal_cellular_4_bar, size: 12.sp, color: Colors.grey[800]),
SizedBox(width: 4.w),
Icon(Icons.wifi, size: 12.sp, color: Colors.grey[800]),
SizedBox(width: 4.w),
Icon(Icons.battery_full, size: 12.sp, color: Colors.grey[800]),
],
),
],
),
),
),
);
}
// 动画Logo
Widget _buildAnimatedLogo() {
return TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.8, end: 1.1),
duration: const Duration(seconds: 2),
curve: Curves.easeInOut,
builder: (context, value, child) {
// 确保opacity值不超过1.0
final opacity = value <= 1.0 ? value : 1.0;
return Transform.scale(
scale: value,
child: AnimatedOpacity(
opacity: opacity,
duration: const Duration(milliseconds: 500),
child: child,
),
);
},
child: Container(
width: 96.w,
height: 96.w,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.blue, Colors.purple],
),
borderRadius: BorderRadius.circular(24.r),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.3),
blurRadius: 15,
offset: const Offset(0, 8),
spreadRadius: 3,
),
],
),
child: Icon(
Icons.headset,
size: 48.sp,
color: Colors.white,
),
),
);
}
// 应用标题
Widget _buildAppTitle() {
return TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 1500),
curve: Curves.easeOut,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Padding(
padding: EdgeInsets.only(top: 24.h),
child: Text(
'DeepVoice',
style: TextStyle(
fontSize: 28.sp,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
),
);
},
);
}
// 标语
Widget _buildSlogan() {
return TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 1500),
curve: Curves.easeOut,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Padding(
padding: EdgeInsets.only(top: 8.h),
child: Text(
'连接世界,岂止声音',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[600],
),
),
),
);
},
);
}
// 加载进度条
Widget _buildLoadingBar() {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 80.w),
child: Stack(
children: [
Container(
height: 4.h,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(2.r),
),
),
Obx(() {
final progress = controller.loadingProgress;
return LayoutBuilder(
builder: (context, constraints) {
return Container(
height: 4.h,
width: constraints.maxWidth * progress,
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [Colors.blue, Colors.purple],
),
borderRadius: BorderRadius.circular(2.r),
),
);
},
);
}),
],
),
);
}
// 额外信息
Widget _buildExtraInfo() {
return TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0.0, end: 1.0),
duration: const Duration(milliseconds: 1500),
curve: Curves.easeOut,
builder: (context, value, child) {
return Opacity(
opacity: value,
child: Text(
'AI语音专家 · 随时随地',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[400],
),
),
);
},
);
}
// 版本信息
Widget _buildVersionInfo() {
return Positioned(
bottom: 32.h,
left: 0,
right: 0,
child: Center(
child: Text(
controller.version,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[400],
),
),
),
);
}
}

2
pubspec.yaml

@ -29,6 +29,7 @@ dependencies:
crypto: ^3.0.3
flutter_tts: ^4.2.2
logger: ^2.0.2+1
flutter_blue_plus: ^1.31.16
# shared_preferences: ^2.2.2 # 移除,使用 get_storage 代替
# speech_to_text: ^7.0.0 # 移除,使用自定义的 ASR 服务
window_size:
@ -43,6 +44,7 @@ dependencies:
share_plus: ^10.0.0
image_picker: ^1.1.2
audio_waveforms: ^1.3.0
font_awesome_flutter: ^10.8.0
dev_dependencies:
flutter_test:

Loading…
Cancel
Save