修复切换设备每次 都选中在第一个

This commit is contained in:
mmc
2026-03-05 10:01:02 +08:00
parent 612b128f7a
commit 22c62006af
4 changed files with 86 additions and 193 deletions

View File

@@ -23,12 +23,11 @@ class AuthCubit extends Cubit<AuthState> {
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<AuthState> {
Future<void> 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<AuthState> {
// 假设 0x12 是踢下线或多设备登录提醒
_kickOutSub = dispatcher.onJsonMessage(0x12).listen((json) {
// 如果后端发来指令确认需要退出
// print('>>> [AUTH] 收到 0x12: $json'); //
// print('>>> [AUTH] 收到 0x12: $json'); //
logout();
});
}

View File

@@ -45,7 +45,8 @@ class DevicesCubit extends Cubit<DevicesState> {
this._selectWorkRecordUseCase,
this._saveWorkRecordUseCase,
this._generatePathUseCase,
this._routePlanningUseCase, this._bindDeviceUseCase
this._routePlanningUseCase,
this._bindDeviceUseCase,
) : super(const DevicesState());
Future<void> unbindDevice(String deviceId, String deviceName) async {
@@ -126,24 +127,42 @@ class DevicesCubit extends Cubit<DevicesState> {
}
}
// 获取所有设备列表
// 获取所有设备列表
Future<void> 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<DevicesState> {
selectDevice(device);
}, (r) => emit(state.copyWith(isLoading: false, selectedDevice: device)));
}
/// 绑定设备
Future<void> bindDevice(String deviceId, String deviceAlias) async {
emit(state.copyWith(isLoading: true, errorMessage: ''));
@@ -186,34 +206,23 @@ class DevicesCubit extends Cubit<DevicesState> {
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<void> getDeviceLocation(DeviceEntity device) async {
emit(state.copyWith(isLoading: true));
@@ -282,60 +291,35 @@ class DevicesCubit extends Cubit<DevicesState> {
result.fold((failure) => emit(state.copyWith(errorMessage: failure.message)), (pathData) => emit(state.copyWith(generatedPath: pathData)));
}
// 开始路径规划
// 开始路径规划
Future<void> startRoutePlanning(Queue<DeviceAddPathPointModel> 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<void> 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<void> 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<void> 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)));
}
}

View File

@@ -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<AppUserCubit>().state.user?.username ??
"";
final username = context.read<AppUserCubit>().state.user?.username ?? "";
context.read<DevicesCubit>().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<DevicesCubit>().switchDevice(
device,
);
await context.read<DevicesCubit>().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");
},

View File

@@ -69,11 +69,7 @@ class MyApp extends StatelessWidget {
BlocProvider<DeviceStatusBloc>(create: (_) => sl<DeviceStatusBloc>()),
// 其他 Cubit...
],
child: MaterialApp.router(
title: 'Maibu Satabot',
theme: AppTheme.lightTheme,
routerConfig: sl<GoRouter>(),
),
child: MaterialApp.router(title: 'Maibu Satabot', theme: AppTheme.lightTheme, routerConfig: sl<GoRouter>()),
);
}
}