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 '../../../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(); |
|||
} |
|||
} |
|||
@ -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, // 连接成功 |
|||
failed // 连接失败 |
|||
// 连接状态枚举 |
|||
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(); |
|||
|
|||
// 模拟定期获取电池状态 |
|||
_simulateBatteryUpdates(); |
|||
} |
|||
|
|||
// 初始化蓝牙服务 |
|||
void _initBluetoothService() { |
|||
try { |
|||
if (Get.isRegistered<BluetoothService>()) { |
|||
_bluetoothService = Get.find<BluetoothService>(); |
|||
|
|||
@override |
|||
void onClose() { |
|||
_isDisposed = true; |
|||
stopSearchingAnimation(); |
|||
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; |
|||
_animationTimer = Timer.periodic(const Duration(milliseconds: 500), (timer) { |
|||
if (_isDisposed || _connectStatus.value != ConnectStatus.searching) { |
|||
timer.cancel(); |
|||
return; |
|||
_updateConnectionStatus(); |
|||
} |
|||
|
|||
// 显示连接成功消息 |
|||
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; |
|||
_searchDots.value = '.' * count; |
|||
}); |
|||
} 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() { |
|||
// 在实际应用中,这里应该从设备获取真实的电池状态 |
|||
// 这里只是模拟随机波动 |
|||
} |
|||
|
|||
// 探索AI功能 |
|||
void exploreAIFeatures() { |
|||
Get.toNamed(Routes.explore); |
|||
} |
|||
|
|||
// 更新固件 |
|||
void updateFirmware() { |
|||
Get.snackbar( |
|||
'提示', |
|||
'开始下载固件更新...', |
|||
snackPosition: SnackPosition.TOP, |
|||
); |
|||
|
|||
// 模拟更新过程 |
|||
// 在真实应用中,这里应该有固件更新的实际逻辑 |
|||
} |
|||
|
|||
// 打开降噪功能 |
|||
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 stopSearchingAnimation() { |
|||
_animationTimer?.cancel(); |
|||
_animationTimer = null; |
|||
if (!_isDisposed) { |
|||
_searchDots.value = ''; |
|||
// 重新扫描设备 |
|||
void rescanDevices() { |
|||
if (_bluetoothService != null) { |
|||
Get.toNamed(Routes.pairing); |
|||
} else { |
|||
Get.snackbar( |
|||
'提示', |
|||
'蓝牙服务不可用', |
|||
snackPosition: SnackPosition.TOP, |
|||
); |
|||
} |
|||
} |
|||
|
|||
Future<void> connectDevice() async { |
|||
if (_connectStatus.value == ConnectStatus.searching) return; |
|||
// 获取设备名称 |
|||
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'] ?? ''; |
|||
} |
|||
|
|||
_connectStatus.value = ConnectStatus.searching; |
|||
startSearchingAnimation(); |
|||
// 获取设备信号强度 |
|||
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 { |
|||
// 模拟搜索和连接过程 |
|||
await Future.delayed(const Duration(seconds: 3)); |
|||
// 检查蓝牙是否可用 |
|||
if (_bluetoothService == null) { |
|||
Get.snackbar( |
|||
'提示', |
|||
'蓝牙服务不可用', |
|||
snackPosition: SnackPosition.TOP, |
|||
backgroundColor: Colors.red[100], |
|||
colorText: Colors.red[800], |
|||
); |
|||
return; |
|||
} |
|||
|
|||
// 设置正在刷新状态 |
|||
_connectionStatus.value = ConnectionStatus.connecting; |
|||
|
|||
if (_isDisposed) return; // Check if controller is disposed |
|||
if (_bluetoothService!.selectedDevice != null) { |
|||
// 如果有选中的设备,刷新设备信息 |
|||
_deviceInfo.assignAll(_bluetoothService!.getConnectedDeviceInfo()); |
|||
_updateConnectionStatus(); |
|||
|
|||
// 模拟随机成功或失败 |
|||
if (DateTime.now().millisecondsSinceEpoch % 2 == 0) { |
|||
_connectStatus.value = ConnectStatus.connected; |
|||
if (!_isDisposed) { |
|||
await Future.delayed(const Duration(seconds: 2)); // 显示成功状态2秒 |
|||
// 检查刷新后的连接状态 |
|||
if (_isConnected.value) { |
|||
Get.snackbar( |
|||
'Success', |
|||
'Device connected successfully', |
|||
'提示', |
|||
'设备信息已更新', |
|||
snackPosition: SnackPosition.TOP, |
|||
backgroundColor: Colors.green.withOpacity(0.1), |
|||
colorText: Colors.green, |
|||
duration: const Duration(seconds: 3), |
|||
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 { |
|||
_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) { |
|||
if (!_isDisposed) { |
|||
_connectStatus.value = ConnectStatus.failed; |
|||
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; |
|||
} |
|||
Logger.error('刷新设备信息失败: $e'); |
|||
_connectionStatus.value = ConnectionStatus.failed; |
|||
_connectionErrorMsg.value = '刷新设备信息失败: ${e.toString()}'; |
|||
|
|||
Get.snackbar( |
|||
'提示', |
|||
'刷新设备信息失败', |
|||
snackPosition: SnackPosition.TOP, |
|||
backgroundColor: Colors.red[100], |
|||
colorText: Colors.red[800], |
|||
); |
|||
} |
|||
} |
|||
|
|||
void retryConnect() { |
|||
if (_isDisposed) return; |
|||
_connectStatus.value = ConnectStatus.initial; |
|||
connectDevice(); |
|||
// 处理连接失败 |
|||
String getConnectionStatusText() { |
|||
switch (_connectionStatus.value) { |
|||
case ConnectionStatus.connected: |
|||
return "已连接"; |
|||
case ConnectionStatus.connecting: |
|||
return "连接中..."; |
|||
case ConnectionStatus.disconnected: |
|||
return "未连接"; |
|||
case ConnectionStatus.failed: |
|||
return "连接失败"; |
|||
} |
|||
} |
|||
|
|||
void disconnectDevice() { |
|||
if (_isDisposed) return; |
|||
_connectStatus.value = ConnectStatus.initial; |
|||
Get.snackbar( |
|||
'Disconnected', |
|||
'Device has been disconnected', |
|||
snackPosition: SnackPosition.TOP, |
|||
backgroundColor: Colors.blue.withOpacity(0.1), |
|||
colorText: Colors.blue, |
|||
duration: const Duration(seconds: 3), |
|||
); |
|||
// 获取连接状态颜色 |
|||
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; |
|||
} |
|||
} |
|||
} |
|||
@ -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