24 changed files with 5137 additions and 457 deletions
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -1,32 +1,170 @@ |
|||||
import 'package:get/get.dart'; |
import 'package:get/get.dart'; |
||||
import '../../../data/providers/agent_provider.dart'; |
import 'package:flutter/material.dart'; |
||||
import '../../../data/services/voice_interaction_service.dart'; |
|
||||
class ChatItem { |
|
||||
final String id; |
|
||||
|
|
||||
ChatItem({ |
class AIFeature { |
||||
required this.id, |
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 { |
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() { |
void search() { |
||||
super.onInit(); |
if (searchController.text.isEmpty) return; |
||||
_loadInitialChats(); |
|
||||
|
Get.snackbar( |
||||
|
'搜索', |
||||
|
'正在搜索: ${searchController.text}', |
||||
|
snackPosition: SnackPosition.TOP, |
||||
|
); |
||||
|
|
||||
|
// 实际应用中,这里应该实现真正的搜索逻辑 |
||||
|
searchController.clear(); |
||||
} |
} |
||||
|
|
||||
void _loadInitialChats() { |
// 点击AI功能 |
||||
final agents = AgentProvider.getAllAgents(); |
void onAIFeatureTap(int index) { |
||||
_chatList.assignAll( |
if (index < 0 || index >= aiFeatures.length) return; |
||||
agents.map((agent) => ChatItem(id: agent.id)).toList(), |
|
||||
|
Get.snackbar( |
||||
|
'功能', |
||||
|
'您选择了: ${aiFeatures[index].title}', |
||||
|
snackPosition: SnackPosition.TOP, |
||||
); |
); |
||||
|
|
||||
|
// 实际应用中,这里应该跳转到对应的功能页面 |
||||
} |
} |
||||
|
|
||||
|
// 点击功能场景 |
||||
|
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(); |
||||
|
} |
||||
} |
} |
||||
@ -1,131 +1,467 @@ |
|||||
import 'package:get/get.dart'; |
import 'package:get/get.dart'; |
||||
import 'dart:async'; |
import '../../../core/routes/app_routes.dart'; |
||||
import 'dart:io'; |
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 'package:flutter/material.dart'; |
||||
import '../../../data/services/voice_interaction_service.dart'; |
|
||||
|
|
||||
enum ConnectStatus { |
// 连接状态枚举 |
||||
initial, // 初始状态,显示添加设备 |
enum ConnectionStatus { |
||||
searching, // 搜索中 |
disconnected, // 未连接 |
||||
connected, // 连接成功 |
connecting, // 连接中 |
||||
failed // 连接失败 |
connected, // 已连接 |
||||
|
failed // 连接失败 |
||||
} |
} |
||||
|
|
||||
class HomeController extends GetxController { |
class HomeController extends GetxController { |
||||
static HomeController get to => Get.find(); |
static HomeController get to => Get.find(); |
||||
|
|
||||
final _connectStatus = ConnectStatus.initial.obs; |
// 设备连接状态 |
||||
final _searchDots = ''.obs; |
final _isConnected = false.obs; |
||||
Timer? _animationTimer; |
bool get isConnected => _isConnected.value; |
||||
bool _isDisposed = false; |
|
||||
|
// 连接状态 |
||||
|
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; |
||||
|
|
||||
ConnectStatus get connectStatus => _connectStatus.value; |
|
||||
String get searchDots => _searchDots.value; |
|
||||
|
|
||||
@override |
@override |
||||
void onInit() { |
void onInit() { |
||||
super.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(); |
||||
|
|
||||
|
// 模拟定期获取电池状态 |
||||
|
_simulateBatteryUpdates(); |
||||
} |
} |
||||
|
|
||||
|
// 初始化蓝牙服务 |
||||
@override |
void _initBluetoothService() { |
||||
void onClose() { |
try { |
||||
_isDisposed = true; |
if (Get.isRegistered<BluetoothService>()) { |
||||
stopSearchingAnimation(); |
_bluetoothService = Get.find<BluetoothService>(); |
||||
super.onClose(); |
|
||||
|
// 如果已经有连接的设备但没有设备信息 |
||||
|
if (_deviceInfo.isEmpty && _bluetoothService!.selectedDevice != null) { |
||||
|
_deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo()); |
||||
|
_updateConnectionStatus(); |
||||
|
} else if (_deviceInfo.isEmpty) { |
||||
|
// 如果没有设备信息,尝试获取已配对设备 |
||||
|
_checkPairedDevices(); |
||||
|
} |
||||
|
|
||||
|
// 监听连接状态变化 |
||||
|
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; |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
Logger.error('获取蓝牙服务失败: $e'); |
||||
|
_connectionStatus.value = ConnectionStatus.failed; |
||||
|
_connectionErrorMsg.value = '获取蓝牙服务失败'; |
||||
|
} |
||||
} |
} |
||||
|
|
||||
void startSearchingAnimation() { |
// 处理连接状态变化 |
||||
stopSearchingAnimation(); // Clear any existing timer |
void _processConnectionStateChange(BluetoothConnectionState state) { |
||||
|
_deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo()); |
||||
|
|
||||
if (_isDisposed) return; |
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; |
||||
|
} |
||||
|
|
||||
int count = 0; |
_updateConnectionStatus(); |
||||
_animationTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) { |
} |
||||
if (_isDisposed || _connectStatus.value != ConnectStatus.searching) { |
|
||||
timer.cancel(); |
// 显示连接成功消息 |
||||
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; |
||||
|
} |
||||
|
|
||||
|
// 获取已配对设备 |
||||
|
final pairedDevices = await _bluetoothService!.getPairedDevices(); |
||||
|
if (pairedDevices.isEmpty) { |
||||
|
Logger.info('没有找到已配对设备'); |
||||
|
_connectionStatus.value = ConnectionStatus.disconnected; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 遍历配对设备,查找已连接的设备 |
||||
|
for (var device in pairedDevices) { |
||||
|
try { |
||||
|
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; |
||||
|
} |
||||
} |
} |
||||
count = (count + 1) % 4; |
} catch (e) { |
||||
_searchDots.value = '.' * count; |
Logger.error('检查已配对设备失败: $e'); |
||||
}); |
_connectionStatus.value = ConnectionStatus.failed; |
||||
|
_connectionErrorMsg.value = '检查设备失败: ${e.toString()}'; |
||||
|
} |
||||
} |
} |
||||
|
|
||||
void stopSearchingAnimation() { |
// 更新连接状态 |
||||
_animationTimer?.cancel(); |
void _updateConnectionStatus() { |
||||
_animationTimer = null; |
if (_deviceInfo.containsKey('connected')) { |
||||
if (!_isDisposed) { |
_isConnected.value = _deviceInfo['connected'] ?? false; |
||||
_searchDots.value = ''; |
|
||||
|
// 如果设备信息显示已连接,但状态不是已连接,则更新状态 |
||||
|
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; |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
Future<void> connectDevice() async { |
// 模拟电池状态更新 |
||||
if (_connectStatus.value == ConnectStatus.searching) return; |
void _simulateBatteryUpdates() { |
||||
|
// 在实际应用中,这里应该从设备获取真实的电池状态 |
||||
|
// 这里只是模拟随机波动 |
||||
|
} |
||||
|
|
||||
|
// 探索AI功能 |
||||
|
void exploreAIFeatures() { |
||||
|
Get.toNamed(Routes.explore); |
||||
|
} |
||||
|
|
||||
|
// 更新固件 |
||||
|
void updateFirmware() { |
||||
|
Get.snackbar( |
||||
|
'提示', |
||||
|
'开始下载固件更新...', |
||||
|
snackPosition: SnackPosition.TOP, |
||||
|
); |
||||
|
|
||||
_connectStatus.value = ConnectStatus.searching; |
// 模拟更新过程 |
||||
startSearchingAnimation(); |
// 在真实应用中,这里应该有固件更新的实际逻辑 |
||||
|
} |
||||
|
|
||||
|
// 打开降噪功能 |
||||
|
void toggleNoiseReduction() { |
||||
|
Get.snackbar( |
||||
|
'提示', |
||||
|
'降噪功能已开启', |
||||
|
snackPosition: SnackPosition.TOP, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 音效设置 |
||||
|
void openSoundEffects() { |
||||
|
Get.snackbar( |
||||
|
'提示', |
||||
|
'音效设置功能尚未实现', |
||||
|
snackPosition: SnackPosition.TOP, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 环境音功能 |
||||
|
void toggleAmbientSound() { |
||||
|
Get.snackbar( |
||||
|
'提示', |
||||
|
'环境音模式已开启', |
||||
|
snackPosition: SnackPosition.TOP, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 收藏的设置 |
||||
|
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'] ?? '未命名设备'; |
||||
|
} |
||||
|
|
||||
|
// 获取设备ID |
||||
|
String getDeviceId() { |
||||
|
if (_deviceInfo.isEmpty || _deviceInfo['device'] == null) { |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
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 { |
try { |
||||
// 模拟搜索和连接过程 |
// 检查蓝牙是否可用 |
||||
await Future.delayed(const Duration(seconds: 3)); |
if (_bluetoothService == null) { |
||||
|
Get.snackbar( |
||||
|
'提示', |
||||
|
'蓝牙服务不可用', |
||||
|
snackPosition: SnackPosition.TOP, |
||||
|
backgroundColor: Colors.red[100], |
||||
|
colorText: Colors.red[800], |
||||
|
); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
if (_isDisposed) return; // Check if controller is disposed |
// 设置正在刷新状态 |
||||
|
_connectionStatus.value = ConnectionStatus.connecting; |
||||
|
|
||||
// 模拟随机成功或失败 |
if (_bluetoothService!.selectedDevice != null) { |
||||
if (DateTime.now().millisecondsSinceEpoch % 2 == 0) { |
// 如果有选中的设备,刷新设备信息 |
||||
_connectStatus.value = ConnectStatus.connected; |
_deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo()); |
||||
if (!_isDisposed) { |
_updateConnectionStatus(); |
||||
await Future.delayed(const Duration(seconds: 2)); // 显示成功状态2秒 |
|
||||
|
// 检查刷新后的连接状态 |
||||
|
if (_isConnected.value) { |
||||
|
Get.snackbar( |
||||
|
'提示', |
||||
|
'设备信息已更新', |
||||
|
snackPosition: SnackPosition.TOP, |
||||
|
backgroundColor: Colors.green[100], |
||||
|
colorText: Colors.green[800], |
||||
|
); |
||||
|
} else { |
||||
|
// 设备可能已断开连接 |
||||
|
_connectionStatus.value = ConnectionStatus.disconnected; |
||||
Get.snackbar( |
Get.snackbar( |
||||
'Success', |
'提示', |
||||
'Device connected successfully', |
'设备已断开连接', |
||||
snackPosition: SnackPosition.TOP, |
snackPosition: SnackPosition.TOP, |
||||
backgroundColor: Colors.green.withOpacity(0.1), |
backgroundColor: Colors.amber[100], |
||||
colorText: Colors.green, |
colorText: Colors.amber[800], |
||||
duration: const Duration(seconds: 3), |
|
||||
); |
); |
||||
|
|
||||
|
// 尝试重新连接 |
||||
|
await _checkPairedDevices(); |
||||
} |
} |
||||
} else { |
} else { |
||||
_connectStatus.value = ConnectStatus.failed; |
// 如果没有选中的设备,尝试连接已配对设备 |
||||
|
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) { |
} catch (e) { |
||||
if (!_isDisposed) { |
Logger.error('刷新设备信息失败: $e'); |
||||
_connectStatus.value = ConnectStatus.failed; |
_connectionStatus.value = ConnectionStatus.failed; |
||||
Get.snackbar( |
_connectionErrorMsg.value = '刷新设备信息失败: ${e.toString()}'; |
||||
'Error', |
|
||||
'Failed to connect to device', |
Get.snackbar( |
||||
snackPosition: SnackPosition.TOP, |
'提示', |
||||
backgroundColor: Colors.red.withOpacity(0.1), |
'刷新设备信息失败', |
||||
colorText: Colors.red, |
snackPosition: SnackPosition.TOP, |
||||
duration: const Duration(seconds: 3), |
backgroundColor: Colors.red[100], |
||||
); |
colorText: Colors.red[800], |
||||
} |
); |
||||
} finally { |
|
||||
if (!_isDisposed && _connectStatus.value == ConnectStatus.searching) { |
|
||||
stopSearchingAnimation(); |
|
||||
_connectStatus.value = ConnectStatus.failed; |
|
||||
} |
|
||||
} |
} |
||||
} |
} |
||||
|
|
||||
void retryConnect() { |
// 处理连接失败 |
||||
if (_isDisposed) return; |
String getConnectionStatusText() { |
||||
_connectStatus.value = ConnectStatus.initial; |
switch (_connectionStatus.value) { |
||||
connectDevice(); |
case ConnectionStatus.connected: |
||||
|
return "已连接"; |
||||
|
case ConnectionStatus.connecting: |
||||
|
return "连接中..."; |
||||
|
case ConnectionStatus.disconnected: |
||||
|
return "未连接"; |
||||
|
case ConnectionStatus.failed: |
||||
|
return "连接失败"; |
||||
|
} |
||||
} |
} |
||||
|
|
||||
void disconnectDevice() { |
// 获取连接状态颜色 |
||||
if (_isDisposed) return; |
Color getConnectionStatusColor() { |
||||
_connectStatus.value = ConnectStatus.initial; |
switch (_connectionStatus.value) { |
||||
Get.snackbar( |
case ConnectionStatus.connected: |
||||
'Disconnected', |
return Colors.green; |
||||
'Device has been disconnected', |
case ConnectionStatus.connecting: |
||||
snackPosition: SnackPosition.TOP, |
return Colors.blue; |
||||
backgroundColor: Colors.blue.withOpacity(0.1), |
case ConnectionStatus.disconnected: |
||||
colorText: Colors.blue, |
return Colors.red; |
||||
duration: const Duration(seconds: 3), |
case ConnectionStatus.failed: |
||||
); |
return Colors.red; |
||||
|
} |
||||
} |
} |
||||
} |
} |
||||
@ -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()); |
||||
|
} |
||||
|
} |
||||
@ -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, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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()); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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(), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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'); |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -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()); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -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], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue