diff --git a/lib/features/auth/presentation/bloc/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit.dart index 7762739a..52a2b9f3 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit.dart @@ -23,12 +23,11 @@ class AuthCubit extends Cubit { final TcpClient tcp; final AppUserCubit appCubit; final NetMessageDispatcher dispatcher; - final AuthTcpDatasource _authTcpDatasource; + final AuthTcpDatasource _authTcpDatasource; StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期 - AuthCubit(this.storage, this.tcp, this.appCubit, this.dispatcher, this._authTcpDatasource) - : super(AuthInitial()) { + AuthCubit(this.storage, this.tcp, this.appCubit, this.dispatcher, this._authTcpDatasource) : super(AuthInitial()) { // Cubit 一启动就开始监听 TCP 的“自动逻辑” _listenToAuthResponse(); } @@ -53,7 +52,7 @@ class AuthCubit extends Cubit { Future loginSuccess(UserEntity user) async { await storage.saveUser(user); await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT); - await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数 + await _authTcpDatasource.sendAuthPacket(); //包括发送认证包和获取列表和切换函数 tcp.startHeartbeat(interval: const Duration(seconds: 4)); //启动心跳 appCubit.setAuth(user); emit(AuthAuthenticated(user)); @@ -75,7 +74,7 @@ class AuthCubit extends Cubit { // 假设 0x12 是踢下线或多设备登录提醒 _kickOutSub = dispatcher.onJsonMessage(0x12).listen((json) { // 如果后端发来指令确认需要退出 - // print('>>> [AUTH] 收到 0x12: $json'); // + // print('>>> [AUTH] 收到 0x12: $json'); // logout(); }); } diff --git a/lib/features/devices/presentation/bloc/devices_cubit.dart b/lib/features/devices/presentation/bloc/devices_cubit.dart index 96482e2b..8f3e9328 100644 --- a/lib/features/devices/presentation/bloc/devices_cubit.dart +++ b/lib/features/devices/presentation/bloc/devices_cubit.dart @@ -45,7 +45,8 @@ class DevicesCubit extends Cubit { this._selectWorkRecordUseCase, this._saveWorkRecordUseCase, this._generatePathUseCase, - this._routePlanningUseCase, this._bindDeviceUseCase + this._routePlanningUseCase, + this._bindDeviceUseCase, ) : super(const DevicesState()); Future unbindDevice(String deviceId, String deviceName) async { @@ -126,24 +127,42 @@ class DevicesCubit extends Cubit { } } + // 获取所有设备列表 // 获取所有设备列表 Future fetchAllDevices(String username) async { emit(state.copyWith(isLoading: true)); + try { - // 网络请求获取列表 + // 🔥 关键步骤1:记录刷新前的选中设备标识(用 deviceName 作为唯一标识) + final String? oldSelectedDeviceName = state.selectedDevice?.deviceName; + + // 网络请求获取新列表 var resultEither = await _getUserDeviceUseCase.call(GetUserDeviceParams(username)); - resultEither.fold( - (failure) => emit( + resultEither.fold((failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message)), (deviceList) { + // 🔥 关键步骤2:匹配新列表中对应的旧选中设备 + DeviceEntity? newSelectedDevice; + if (oldSelectedDeviceName != null && deviceList.isNotEmpty) { + // 在新列表中查找和旧选中设备名称一致的设备 + newSelectedDevice = deviceList.firstWhere( + (device) => device.deviceName == oldSelectedDeviceName, + // 如果找不到(如设备已解绑),返回 null + orElse: () => deviceList.first, // 兜底:选中第一个 + ); + } else { + // 无旧选中设备,默认选中第一个 + newSelectedDevice = deviceList.isNotEmpty ? deviceList.first : null; + } + + // 🔥 关键步骤3:更新状态,使用匹配后的选中设备 + emit( state.copyWith( + devices: deviceList, + selectedDevice: newSelectedDevice, // 保留旧选中设备 isLoading: false, - errorMessage: failure.message, // ), - ), - (deviceList) { - emit(state.copyWith(devices: deviceList, selectedDevice: deviceList.isNotEmpty ? deviceList.first : null, isLoading: false)); - }, - ); + ); + }); } catch (e) { emit(state.copyWith(isLoading: false, errorMessage: e.toString())); } @@ -178,6 +197,7 @@ class DevicesCubit extends Cubit { selectDevice(device); }, (r) => emit(state.copyWith(isLoading: false, selectedDevice: device))); } + /// 绑定设备 Future bindDevice(String deviceId, String deviceAlias) async { emit(state.copyWith(isLoading: true, errorMessage: '')); @@ -186,34 +206,23 @@ class DevicesCubit extends Cubit { final result = await _bindDeviceUseCase.call(params); result.fold( // 失败处理 - (failure) => emit(state.copyWith( - isLoading: false, - errorMessage: failure.message ?? '绑定设备失败', - )), + (failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '绑定设备失败')), // 成功处理(返回 int 状态码) - (successCode) { + (successCode) { if (successCode == 1) { emit(state.copyWith(isLoading: false, errorMessage: '')); // 绑定成功后刷新设备列表 - //fetchAllDevices(state.?.username ?? ''); + //fetchAllDevices(state.?.username ?? ''); } else { - emit(state.copyWith( - isLoading: false, - errorMessage: '绑定失败:状态码 $successCode', - )); + emit(state.copyWith(isLoading: false, errorMessage: '绑定失败:状态码 $successCode')); } }, ); } catch (e) { - emit(state.copyWith( - isLoading: false, - errorMessage: '绑定异常:${e.toString()}', - )); + emit(state.copyWith(isLoading: false, errorMessage: '绑定异常:${e.toString()}')); } } - - /// 获取设备位置 Future getDeviceLocation(DeviceEntity device) async { emit(state.copyWith(isLoading: true)); @@ -282,60 +291,35 @@ class DevicesCubit extends Cubit { result.fold((failure) => emit(state.copyWith(errorMessage: failure.message)), (pathData) => emit(state.copyWith(generatedPath: pathData))); } -// 开始路径规划 + + // 开始路径规划 Future startRoutePlanning(Queue locationQueue) async { emit(state.copyWith(isLoading: true)); final result = await _routePlanningUseCase.startRoutePlanning(locationQueue); result.fold( - (failure) => emit(state.copyWith( - isLoading: false, - errorMessage: failure.message ?? '路径规划启动失败', - )), - (_) => emit(state.copyWith( - isLoading: false, - errorMessage: '', - )), + (failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '路径规划启动失败')), + (_) => emit(state.copyWith(isLoading: false, errorMessage: '')), ); } -// 暂停 + // 暂停 Future pauseRoutePlanning() async { emit(state.copyWith(isLoading: true)); final result = await _routePlanningUseCase.pauseRPWork(); - result.fold( - (failure) => emit(state.copyWith( - isLoading: false, - errorMessage: failure.message ?? '暂停失败', - )), - (_) => emit(state.copyWith(isLoading: false)), - ); + result.fold((failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '暂停失败')), (_) => emit(state.copyWith(isLoading: false))); } -// 恢复 + // 恢复 Future resumeRoutePlanning() async { emit(state.copyWith(isLoading: true)); final result = await _routePlanningUseCase.resumeRPWork(); - result.fold( - (failure) => emit(state.copyWith( - isLoading: false, - errorMessage: failure.message ?? '恢复失败', - )), - (_) => emit(state.copyWith(isLoading: false)), - ); + result.fold((failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '恢复失败')), (_) => emit(state.copyWith(isLoading: false))); } -// 停止 + // 停止 Future stopRoutePlanning() async { emit(state.copyWith(isLoading: true)); final result = await _routePlanningUseCase.stopRoutePlanning(); - result.fold( - (failure) => emit(state.copyWith( - isLoading: false, - errorMessage: failure.message ?? '停止失败', - )), - (_) => emit(state.copyWith(isLoading: false)), - ); + result.fold((failure) => emit(state.copyWith(isLoading: false, errorMessage: failure.message ?? '停止失败')), (_) => emit(state.copyWith(isLoading: false))); } - - } diff --git a/lib/features/home/presentation/widgets/ImmersionHeader.dart b/lib/features/home/presentation/widgets/ImmersionHeader.dart index e0c54f99..e9ac0713 100644 --- a/lib/features/home/presentation/widgets/ImmersionHeader.dart +++ b/lib/features/home/presentation/widgets/ImmersionHeader.dart @@ -52,14 +52,7 @@ class ImmersionHeader extends StatelessWidget { // 机器大图 Transform.translate( offset: const Offset(30, 80), // 正数向右移动(例如 20 像素),负数向左 - child: Center( - child: Image.asset( - 'assets/images/car.png', - width: 330, - height: 190, - fit: BoxFit.contain, - ), - ), + child: Center(child: Image.asset('assets/images/car.png', width: 330, height: 190, fit: BoxFit.contain)), ), // 顶部状态信息 @@ -78,28 +71,12 @@ class ImmersionHeader extends StatelessWidget { mainAxisSize: MainAxisSize.min, // 尽可能收缩高度 children: [ // 设备名称和电量 - Text( - _formatName(device.displayName), - style: GoogleFonts.roboto( - fontSize: 22, - fontWeight: FontWeight.w900, - ), - ), + Text(_formatName(device.displayName), style: GoogleFonts.roboto(fontSize: 22, fontWeight: FontWeight.w900)), const SizedBox(height: 4), Row( children: [ - Text( - '${device.onlineStatus == 1 ? "100" : "--"} %', - style: GoogleFonts.roboto( - fontSize: 24, - fontWeight: FontWeight.w900, - ), - ), - const Icon( - Icons.chevron_right, - size: 20, - color: Colors.grey, - ), + Text('${device.onlineStatus == 1 ? "100" : "--"} %', style: GoogleFonts.roboto(fontSize: 24, fontWeight: FontWeight.w900)), + const Icon(Icons.chevron_right, size: 20, color: Colors.grey), ], ), @@ -123,9 +100,7 @@ class ImmersionHeader extends StatelessWidget { _buildCircleIcon(Icons.sync, () { // 1. 触发 Cubit 请求最新设备列表 // 假设你的 username 存储在 AuthCubit 或类似的全局状态中 - final username = - context.read().state.user?.username ?? - ""; + final username = context.read().state.user?.username ?? ""; context.read().fetchAllDevices(username); // 2. 弹出窗口(窗口内部会根据状态显示转圈或列表) @@ -151,22 +126,14 @@ class ImmersionHeader extends StatelessWidget { // 在 offWhite 背景下,标签用纯白色会显得更精致 color: Colors.white, borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 4), - ], + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.02), blurRadius: 4)], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - CircleAvatar( - radius: 3, - backgroundColor: isOnline ? Colors.green : Colors.grey, - ), + CircleAvatar(radius: 3, backgroundColor: isOnline ? Colors.green : Colors.grey), const SizedBox(width: 6), - Text( - isOnline ? "在线" : "离线", - style: const TextStyle(fontSize: 12, color: Colors.black54), - ), + Text(isOnline ? "在线" : "离线", style: const TextStyle(fontSize: 12, color: Colors.black54)), ], ), ); @@ -177,10 +144,7 @@ class ImmersionHeader extends StatelessWidget { return Container( width: 120, height: 6, - decoration: BoxDecoration( - color: Colors.blueAccent, - borderRadius: BorderRadius.circular(5), - ), + decoration: BoxDecoration(color: Colors.blueAccent, borderRadius: BorderRadius.circular(5)), ); } @@ -205,11 +169,7 @@ class ImmersionHeader extends StatelessWidget { ), ], ), - child: Icon( - icon, - size: 30, - color: isAccent ? Colors.blue : Colors.black54, - ), + child: Icon(icon, size: 30, color: isAccent ? Colors.blue : Colors.black54), ), ); } @@ -235,21 +195,11 @@ class ImmersionHeader extends StatelessWidget { margin: const EdgeInsets.symmetric(vertical: 12), width: 36, height: 5, - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(10), - ), - ), - const Text( - "切换设备", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(10)), ), + const Text("切换设备", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 10), - Container( - height: 0.5, - margin: const EdgeInsets.symmetric(horizontal: 20), - color: Colors.grey.withOpacity(0.1), - ), + Container(height: 0.5, margin: const EdgeInsets.symmetric(horizontal: 20), color: Colors.grey.withOpacity(0.1)), // 💡 动态内容区 Expanded( @@ -264,10 +214,7 @@ class ImmersionHeader extends StatelessWidget { children: [ const CircularProgressIndicator(color: Colors.blue), const SizedBox(height: 16), - Text( - "加载中...", - style: TextStyle(color: Colors.grey[600]), - ), + Text("加载中...", style: TextStyle(color: Colors.grey[600])), ], ), ); @@ -275,26 +222,26 @@ class ImmersionHeader extends StatelessWidget { // 2. 列表展示 if (state.devices.isEmpty) { - return const Center(child: Text("暂无可用设备")); + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.description_outlined, size: 60, color: Colors.grey), + SizedBox(height: 16), + Text('暂无设备', style: TextStyle(fontSize: 16, color: Colors.grey)), + ], + ), + ); } return ListView.builder( // 💡 底部留出安全距离,防止最后一条滚不上来 - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom + 20, - top: 10, - ), + padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom + 20, top: 10), itemCount: state.devices.length, itemBuilder: (context, index) { // 传入当前选中的 ID 方便做 UI 区分 - final isSelected = - state.selectedDevice?.deviceName == - state.devices[index].deviceName; - return _buildDeviceItem( - state.devices[index], - context, - isSelected, - ); + final isSelected = state.selectedDevice?.deviceName == state.devices[index].deviceName; + return _buildDeviceItem(state.devices[index], context, isSelected); }, ); }, @@ -307,11 +254,7 @@ class ImmersionHeader extends StatelessWidget { ); } - Widget _buildDeviceItem( - DeviceEntity device, - BuildContext context, - bool isSelected, - ) { + Widget _buildDeviceItem(DeviceEntity device, BuildContext context, bool isSelected) { return GestureDetector( onTap: () { print("跳转到详情页: ${device.deviceAlias}"); @@ -323,29 +266,14 @@ class ImmersionHeader extends StatelessWidget { decoration: BoxDecoration( color: Colors.white, // 💡 如果是当前选中设备,边框颜色略深 - border: Border.all( - color: isSelected - ? Colors.blue.withOpacity(0.5) - : Colors.grey.withOpacity(0.3), - width: isSelected ? 1.5 : 1, - ), + border: Border.all(color: isSelected ? Colors.blue.withOpacity(0.5) : Colors.grey.withOpacity(0.3), width: isSelected ? 1.5 : 1), borderRadius: BorderRadius.circular(12), - boxShadow: [ - if (isSelected) - BoxShadow(color: Colors.blue.withOpacity(0.05), blurRadius: 4), - ], + boxShadow: [if (isSelected) BoxShadow(color: Colors.blue.withOpacity(0.05), blurRadius: 4)], ), child: Row( children: [ // 设备图片 - Transform.translate( - offset: const Offset(10, 0), - child: Image.asset( - 'assets/images/car.png', - width: 100, - height: 80, - ), - ), + Transform.translate(offset: const Offset(10, 0), child: Image.asset('assets/images/car.png', width: 100, height: 80)), const SizedBox(width: 12), // 设备信息 Expanded( @@ -354,17 +282,11 @@ class ImmersionHeader extends StatelessWidget { children: [ Row( children: [ - Icon( - Icons.circle, - color: device.isOnline ? Colors.green : Colors.grey, - size: 12, - ), + Icon(Icons.circle, color: device.isOnline ? Colors.green : Colors.grey, size: 12), const SizedBox(width: 4), Flexible( child: Text( - (device.deviceAlias?.trim() ?? '').isEmpty - ? '未知设备' - : device.deviceAlias!, + (device.deviceAlias?.trim() ?? '').isEmpty ? '未知设备' : device.deviceAlias!, style: const TextStyle(fontWeight: FontWeight.bold), // 关键属性:处理文本溢出 maxLines: 2, // 最多显示2行 @@ -375,10 +297,7 @@ class ImmersionHeader extends StatelessWidget { ], ), const SizedBox(height: 6), - const Text( - "点击查看详情", - style: TextStyle(color: Colors.grey, fontSize: 12), - ), + const Text("点击查看详情", style: TextStyle(color: Colors.grey, fontSize: 12)), ], ), ), @@ -390,9 +309,7 @@ class ImmersionHeader extends StatelessWidget { ? null : () async { // 💡 执行切换逻辑 - await context.read().switchDevice( - device, - ); + await context.read().switchDevice(device); // 切换成功后,UI 会自动更新(因为 BlocBuilder 在监听),我们可以关闭弹窗 if (context.mounted) { Navigator.pop(context); @@ -408,10 +325,7 @@ class ImmersionHeader extends StatelessWidget { const SizedBox(height: 8), CCPrimaryButton( onPressed: () { - context.push( - RoutePaths.machineDetails, - extra: device, - ); // 把当前点击的这个设备对象传过去 + context.push(RoutePaths.machineDetails, extra: device); // 把当前点击的这个设备对象传过去 print("通过按钮进入详情,设备名称:$device"); }, diff --git a/lib/main.dart b/lib/main.dart index 2f130e47..6f40b851 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -69,11 +69,7 @@ class MyApp extends StatelessWidget { BlocProvider(create: (_) => sl()), // 其他 Cubit... ], - child: MaterialApp.router( - title: 'Maibu Satabot', - theme: AppTheme.lightTheme, - routerConfig: sl(), - ), + child: MaterialApp.router(title: 'Maibu Satabot', theme: AppTheme.lightTheme, routerConfig: sl()), ); } }