集成接口 开始执行任务的领域层和数据层和UI层的开发啊(待测试)

集成功能 暂停 取消功能 恢复功能的接口的领域层和数据层的开发 下一步待集成到页面上
优化功能,优化了接口异常和未知异常对页面渲染的影响对用户的体验的不好情况。具体通过弹窗友好提示!
优化更新了tcp指示灯点击出现机器状态中添加选中设备的编号 方便用户使用的明白明了!。
优化更新关闭了tcp重连操作内链条中的获取用户设备和切换设备操作项。
调整了获取无人机状态信息的更新频率 为14秒一次
This commit is contained in:
2026-06-16 17:34:00 +08:00
parent 52fe17c0d5
commit 71aea48d9f
61 changed files with 2896 additions and 1648 deletions

View File

@@ -76,12 +76,12 @@ class PositionState extends Equatable {
/// UAV详情实体(用于详情页面API返回的数据)
class UAVDetailEntity extends Equatable {
final String deviceSn; // 设备序列号(无人机序列号)
final String gatewaySn; // 网关序列号
final String callsign; // 机场呼号/名称
final String droneCallsign; // 无人机呼号
final int onlineStatus; // 机场在线状态 (1:在线, 0:离线)
final int droneOnlineStatus; // 无人机在线状态
final String deviceSn;
final String gatewaySn;
final String callsign;
final String droneCallsign;
final int onlineStatus;
final int droneOnlineStatus;
final double? latitude;
final double? longitude;
final double? capacityPercent;
@@ -173,7 +173,9 @@ class UAVDetailEntity extends Equatable {
: null,
droneCameraList: json['drone_camera_list'] != null
? (json['drone_camera_list'] as List)
.map((item) => CameraInfo.fromJson(item as Map<String, dynamic>))
.map(
(item) => CameraInfo.fromJson(item as Map<String, dynamic>),
)
.toList()
: null,
orgId: json['orgId'],
@@ -333,7 +335,9 @@ class DroneStationEntity extends Equatable {
: null,
droneCameraList: json['drone_camera_list'] != null
? (json['drone_camera_list'] as List)
.map((item) => CameraInfo.fromJson(item as Map<String, dynamic>))
.map(
(item) => CameraInfo.fromJson(item as Map<String, dynamic>),
)
.toList()
: null,
orgId: json['orgId'] ?? 0,

View File

@@ -5,7 +5,6 @@ class FlightTaskDetailEntity {
final String taskType;
final String status;
final String sn;
final String droneSn; // 无人机序列号
final String waylineUuid;
final String beginAt;
final String endAt;
@@ -27,7 +26,6 @@ class FlightTaskDetailEntity {
required this.taskType,
required this.status,
required this.sn,
required this.droneSn,
required this.waylineUuid,
required this.beginAt,
required this.endAt,
@@ -47,26 +45,18 @@ class FlightTaskDetailEntity {
factory FlightTaskDetailEntity.fromJson(Map<String, dynamic> json) {
// 安全处理 folder_info 嵌套结构
final rawFolderInfo = json['folder_info'];
final Map<String, dynamic> folderInfo = (rawFolderInfo is Map)
? Map<String, dynamic>.from(rawFolderInfo)
final Map<String, dynamic> folderInfo = (rawFolderInfo is Map)
? Map<String, dynamic>.from(rawFolderInfo)
: <String, dynamic>{};
print(
'🔍 [FlightTaskDetailEntity] folder_info 类型: ${rawFolderInfo.runtimeType}',
);
print('🔍 [FlightTaskDetailEntity] folder_info 类型: ${rawFolderInfo.runtimeType}');
print('🔍 [FlightTaskDetailEntity] folder_info 值: $rawFolderInfo');
return FlightTaskDetailEntity(
name: json['name'] ?? '',
uuid: json['uuid'] ?? '',
taskType: json['task_type'] ?? '',
status: json['status'] ?? '',
sn: json['sn'] ?? '',
droneSn:
json['drone_sn'] ??
json['device_sn'] ??
json['droneSn'] ??
json['deviceSn'] ??
'',
waylineUuid: json['wayline_uuid'] ?? '',
beginAt: json['begin_at'] ?? '',
endAt: json['end_at'] ?? '',
@@ -99,7 +89,6 @@ class FlightTaskDetailEntity {
'task_type': taskType,
'status': status,
'sn': sn,
'drone_sn': droneSn,
'wayline_uuid': waylineUuid,
'begin_at': beginAt,
'end_at': endAt,

View File

@@ -1,5 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/usecases/get_device_status_data_usecase.dart';
import '../../../../../core/network/error_handler.dart';
import 'device_status_event.dart';
import 'device_status_state.dart';
@@ -31,7 +32,10 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
siteId: event.siteId,
));
} catch (e) {
emit(DeviceStatusError(e.toString()));
emit(DeviceStatusError(
message: ErrorHandler.getErrorMessage(e),
shouldShowError: true, // 🔥 标记需要显示弹窗
));
}
}
@@ -55,7 +59,10 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
devices: response.devices,
));
} catch (e) {
emit(DeviceStatusError(e.toString()));
emit(DeviceStatusError(
message: ErrorHandler.getErrorMessage(e),
shouldShowError: true, // 🔥 标记需要显示弹窗
));
}
}
}

View File

@@ -54,11 +54,15 @@ class DeviceStatusLoaded extends DeviceStatusState {
class DeviceStatusError extends DeviceStatusState {
final String message;
final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗
const DeviceStatusError(this.message);
const DeviceStatusError({
required this.message,
this.shouldShowError = false, // 默认 false
});
@override
List<Object?> get props => [message];
List<Object?> get props => [message, shouldShowError];
}
class DeviceStatusEmpty extends DeviceStatusState {

View File

@@ -2,6 +2,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/usecases/get_drone_station_list_usecase.dart';
import '../../domain/usecases/get_video_stream_usecase.dart';
import '../../domain/usecases/get_uav_video_stream_usecase.dart';
import '../../../../../core/network/error_handler.dart';
import 'drone_station_event.dart';
import 'drone_station_state.dart';
@@ -33,7 +34,10 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
final result = await getDroneStationListUseCase(event.siteId);
result.fold(
(failure) => emit(DroneStationError(failure.message)),
(failure) => emit(DroneStationError(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
)),
(stations) => emit(DroneStationLoaded(stations)),
);
}
@@ -46,7 +50,10 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
final result = await getDroneStationListUseCase(event.siteId);
result.fold(
(failure) => emit(DroneStationError(failure.message)),
(failure) => emit(DroneStationError(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
)),
(stations) => emit(DroneStationLoaded(stations)),
);
}
@@ -61,7 +68,10 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
final result = await getUAVDetailUseCase(event.gatewaySn, event.deviceSn);
result.fold(
(failure) => emit(UAVDetailError(failure.message)),
(failure) => emit(UAVDetailError(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
)),
(detail) => emit(UAVDetailLoaded(detail)),
);
}
@@ -79,7 +89,10 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
);
result.fold(
(failure) => emit(VideoStreamError(failure.message)),
(failure) => emit(VideoStreamError(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
)),
(videoStream) => emit(VideoStreamLoaded(videoStream, event.cameraPosition)),
);
}
@@ -100,7 +113,10 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
);
result.fold(
(failure) => emit(UavVideoStreamError(failure.message)),
(failure) => emit(UavVideoStreamError(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
)),
(videoStream) => emit(UavVideoStreamLoaded(
videoStream: videoStream,
cameraIndex: event.cameraIndex,

View File

@@ -29,11 +29,15 @@ class DroneStationLoaded extends DroneStationState {
class DroneStationError extends DroneStationState {
final String message;
final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗
const DroneStationError(this.message);
const DroneStationError({
required this.message,
this.shouldShowError = false, // 默认 false
});
@override
List<Object?> get props => [message];
List<Object?> get props => [message, shouldShowError];
}
class UAVDetailLoading extends DroneStationState {
@@ -51,11 +55,15 @@ class UAVDetailLoaded extends DroneStationState {
class UAVDetailError extends DroneStationState {
final String message;
final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗
const UAVDetailError(this.message);
const UAVDetailError({
required this.message,
this.shouldShowError = false, // 默认 false
});
@override
List<Object?> get props => [message];
List<Object?> get props => [message, shouldShowError];
}
class VideoStreamLoading extends DroneStationState {
@@ -74,11 +82,15 @@ class VideoStreamLoaded extends DroneStationState {
class VideoStreamError extends DroneStationState {
final String message;
final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗
const VideoStreamError(this.message);
const VideoStreamError({
required this.message,
this.shouldShowError = false, // 默认 false
});
@override
List<Object?> get props => [message];
List<Object?> get props => [message, shouldShowError];
}
/// 无人机实时视频流加载状态
@@ -105,9 +117,13 @@ class UavVideoStreamLoaded extends DroneStationState {
/// 无人机实时视频流加载失败状态
class UavVideoStreamError extends DroneStationState {
final String message;
final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗
const UavVideoStreamError(this.message);
const UavVideoStreamError({
required this.message,
this.shouldShowError = false, // 默认 false
});
@override
List<Object?> get props => [message];
List<Object?> get props => [message, shouldShowError];
}

View File

@@ -1,6 +1,7 @@
import 'package:dio/dio.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../core/consts/http_api_consts.dart';
import '../../../../../core/network/error_handler.dart';
import '../../data/models/robot_data_model.dart';
import 'robot_list_event.dart';
import 'robot_list_state.dart';
@@ -19,7 +20,10 @@ class RobotListBloc extends Bloc<RobotListEvent, RobotListState> {
Emitter<RobotListState> emit,
) async {
if (event.siteId == null) {
emit(const RobotListError('请先选择场站'));
emit(const RobotListError(
message: '请先选择场站',
shouldShowError: true,
));
return;
}
@@ -53,7 +57,10 @@ class RobotListBloc extends Bloc<RobotListEvent, RobotListState> {
siteId: event.siteId,
));
} catch (e) {
emit(RobotListError(e.toString()));
emit(RobotListError(
message: ErrorHandler.getErrorMessage(e),
shouldShowError: true, // 🔥 标记需要显示弹窗
));
}
}
@@ -65,7 +72,10 @@ class RobotListBloc extends Bloc<RobotListEvent, RobotListState> {
final currentState = state as RobotListLoaded;
if (currentState.siteId == null) {
emit(const RobotListError('请先选择场站'));
emit(const RobotListError(
message: '请先选择场站',
shouldShowError: true,
));
return;
}
@@ -94,7 +104,10 @@ class RobotListBloc extends Bloc<RobotListEvent, RobotListState> {
emit(currentState.copyWith(robots: robots));
} catch (e) {
emit(RobotListError(e.toString()));
emit(RobotListError(
message: ErrorHandler.getErrorMessage(e),
shouldShowError: true, // 🔥 标记需要显示弹窗
));
}
}
}

View File

@@ -45,9 +45,13 @@ class RobotListLoaded extends RobotListState {
class RobotListError extends RobotListState {
final String message;
final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗
const RobotListError(this.message);
const RobotListError({
required this.message,
this.shouldShowError = false, // 默认 false
});
@override
List<Object?> get props => [message];
List<Object?> get props => [message, shouldShowError];
}

View File

@@ -1,107 +0,0 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import '../model/robot_status_model.dart';
import '../service/robot_status_service.dart';
import '../manager/float_bar_manager.dart';
/// 悬浮条事件
abstract class FloatBarEvent {}
/// 切换折叠/展开状态事件
class ToggleExpandEvent extends FloatBarEvent {}
/// 更新状态数据事件
class UpdateStatusEvent extends FloatBarEvent {
final RobotStatusModel status;
UpdateStatusEvent(this.status);
}
/// 悬浮条状态
abstract class FloatBarState {}
/// 折叠状态
class FloatBarCollapsedState extends FloatBarState {
final RobotStatusModel status;
FloatBarCollapsedState(this.status);
}
/// 展开状态
class FloatBarExpandedState extends FloatBarState {
final RobotStatusModel status;
FloatBarExpandedState(this.status);
}
/// 悬浮条Bloc
class FloatBarBloc extends Bloc<FloatBarEvent, FloatBarState> {
final RobotStatusService _statusService;
final FloatBarManager? _floatBarManager;
StreamSubscription? _statusSubscription;
FloatBarBloc(this._statusService, [this._floatBarManager])
: super(FloatBarCollapsedState(_statusService.currentStatus)) {
// 监听服务层数据流
_startListening();
on<ToggleExpandEvent>(_handleToggleExpand);
on<UpdateStatusEvent>(_handleUpdateStatus);
}
/// 开始监听服务层数据
void _startListening() {
_statusSubscription?.cancel();
_statusSubscription = _statusService.statusStream.listen((status) {
add(UpdateStatusEvent(status));
});
}
/// 处理切换折叠/展开
void _handleToggleExpand(
ToggleExpandEvent event,
Emitter<FloatBarState> emit,
) {
final currentState = state;
if (currentState is FloatBarCollapsedState) {
emit(FloatBarExpandedState(currentState.status));
} else if (currentState is FloatBarExpandedState) {
emit(FloatBarCollapsedState(currentState.status));
}
// 触发UI刷新(使用 ?. 处理空安全)
_floatBarManager?.refresh();
}
/// 处理状态数据更新
void _handleUpdateStatus(
UpdateStatusEvent event,
Emitter<FloatBarState> emit,
) {
final currentState = state;
if (currentState is FloatBarCollapsedState) {
emit(FloatBarCollapsedState(event.status));
} else if (currentState is FloatBarExpandedState) {
emit(FloatBarExpandedState(event.status));
}
// 触发UI刷新(使用 ?. 处理空安全)
_floatBarManager?.refresh();
}
/// 启动状态服务
void startService() {
_statusService.start();
}
/// 停止状态服务
void stopService() {
_statusService.stop();
}
@override
Future<void> close() {
_statusSubscription?.cancel();
_statusService.stop();
return super.close();
}
}

View File

@@ -1,62 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// 悬浮条设置服务 - 简化版
/// 使用静态变量存储状态,确保全局同步
class FloatBarSettingService {
final SharedPreferences _prefs;
static const String _key = 'float_bar_enabled';
/// 🔥 静态实例引用
static FloatBarSettingService? _instance;
/// 🔥 静态状态变量 - 所有组件共享
static bool _isEnabled = true;
/// 🔥 静态 ValueNotifier - 用于通知UI变化
static final ValueNotifier<bool> _settingNotifier = ValueNotifier<bool>(true);
FloatBarSettingService(this._prefs) {
_instance = this;
// 从持久化读取初始状态
_isEnabled = _prefs.getBool(_key) ?? true;
_settingNotifier.value = _isEnabled;
print('✅ [FloatBarSettingService] 初始化完成,初始状态: $_isEnabled');
}
/// 获取静态实例
static FloatBarSettingService? get instance => _instance;
/// 获取 ValueNotifier
static ValueNotifier<bool> get settingNotifier => _settingNotifier;
/// 获取当前是否启用(直接从静态变量读取)
static bool get isEnabled => _isEnabled;
/// 设置是否启用
static Future<void> setEnabled(bool enabled) async {
print('🔍 [FloatBarSettingService] setEnabled 被调用,新值: $enabled');
// 1. 更新静态变量
_isEnabled = enabled;
print('🔍 [FloatBarSettingService] 静态变量已更新: $_isEnabled');
// 2. 更新 ValueNotifier(通知所有监听者)
_settingNotifier.value = enabled;
print(
'🔍 [FloatBarSettingService] ValueNotifier 已更新: ${_settingNotifier.value}',
);
// 3. 持久化到 SharedPreferences
final instance = _instance;
if (instance != null) {
await instance._prefs.setBool(_key, enabled);
print('🔍 [FloatBarSettingService] 已保存到 SharedPreferences');
}
}
/// 切换开关
static Future<void> toggle() async {
await setEnabled(!_isEnabled);
}
}

View File

@@ -1,24 +0,0 @@
import 'package:flutter/foundation.dart';
/// 悬浮条控制器 - 极简版
/// 使用静态变量管理全局状态
class FloatBarController {
/// 🔥 是否显示悬浮条
static bool isVisible = true;
/// 🔥 状态变化通知器
static final ValueNotifier<bool> visibilityNotifier = ValueNotifier<bool>(true);
/// 设置显示/隐藏
static void setVisible(bool visible) {
if (isVisible != visible) {
isVisible = visible;
visibilityNotifier.value = visible;
}
}
/// 切换显示状态
static void toggle() {
setVisible(!isVisible);
}
}

View File

@@ -1,69 +0,0 @@
import 'package:flutter/material.dart';
import '../view/float_bar_widget.dart';
/// 全局悬浮条管理器
/// 单例模式,负责管理OverlayEntry的创建、显示、隐藏和刷新
class FloatBarManager {
static final FloatBarManager _instance = FloatBarManager._internal();
factory FloatBarManager() => _instance;
FloatBarManager._internal();
/// OverlayEntry实例
OverlayEntry? _overlayEntry;
/// 是否已初始化
bool _isInitialized = false;
/// 全局上下文
BuildContext? _globalContext;
/// 初始化管理器,保存全局上下文
void initialize(BuildContext context) {
if (_isInitialized) return;
_globalContext = context;
_isInitialized = true;
}
/// 显示悬浮条
void show() {
if (!_isInitialized || _globalContext == null) {
throw Exception('FloatBarManager has not been initialized!');
}
if (_overlayEntry != null) {
// 已有浮层,先移除再重新创建
hide();
}
_overlayEntry = OverlayEntry(
builder: (context) => const FloatBarWidget(),
);
Overlay.of(_globalContext!)?.insert(_overlayEntry!);
}
/// 隐藏悬浮条
void hide() {
if (_overlayEntry != null) {
_overlayEntry!.remove();
_overlayEntry = null;
}
}
/// 强制刷新UI
void refresh() {
_overlayEntry?.markNeedsBuild();
}
/// 检查浮层是否显示中
bool get isVisible => _overlayEntry != null;
/// 释放资源
void dispose() {
hide();
_globalContext = null;
_isInitialized = false;
}
}

View File

@@ -1,104 +0,0 @@
import 'package:equatable/equatable.dart';
/// 机器人状态数据模型
class RobotStatusModel extends Equatable {
/// 任务名称
final String taskName;
/// 电量百分比
final int battery;
/// 设备状态:idle/running/charging/error
final String status;
/// 信号强度
final int signal;
/// 当前位置
final String location;
/// 速度
final double speed;
/// 温度
final int temperature;
/// 运行时间
final String runTime;
const RobotStatusModel({
this.taskName = '未知任务',
this.battery = 100,
this.status = 'idle',
this.signal = 100,
this.location = '未知位置',
this.speed = 0.0,
this.temperature = 25,
this.runTime = '00:00:00',
});
/// 创建副本
RobotStatusModel copyWith({
String? taskName,
int? battery,
String? status,
int? signal,
String? location,
double? speed,
int? temperature,
String? runTime,
}) {
return RobotStatusModel(
taskName: taskName ?? this.taskName,
battery: battery ?? this.battery,
status: status ?? this.status,
signal: signal ?? this.signal,
location: location ?? this.location,
speed: speed ?? this.speed,
temperature: temperature ?? this.temperature,
runTime: runTime ?? this.runTime,
);
}
/// 状态描述文本
String get statusText {
switch (status) {
case 'running':
return '运行中';
case 'charging':
return '充电中';
case 'error':
return '故障';
case 'idle':
default:
return '待机';
}
}
/// 状态颜色
String get statusColor {
switch (status) {
case 'running':
return '#00C853';
case 'charging':
return '#03DAC6';
case 'error':
return '#FF5252';
case 'idle':
default:
return '#9E9E9E';
}
}
@override
List<Object?> get props => [
taskName,
battery,
status,
signal,
location,
speed,
temperature,
runTime,
];
}

View File

@@ -1,101 +0,0 @@
import 'dart:async';
import 'dart:math';
import '../model/robot_status_model.dart';
/// 机器人状态服务
/// 负责模拟设备状态推送,实际项目中应替换为真实的TCP/接口对接
class RobotStatusService {
static final RobotStatusService _instance = RobotStatusService._internal();
factory RobotStatusService() => _instance;
RobotStatusService._internal();
/// 状态数据流控制器
final StreamController<RobotStatusModel> _statusController =
StreamController.broadcast();
/// 当前状态
RobotStatusModel _currentStatus = const RobotStatusModel();
/// 模拟定时器
Timer? _timer;
/// 状态数据流
Stream<RobotStatusModel> get statusStream => _statusController.stream;
/// 获取当前状态
RobotStatusModel get currentStatus => _currentStatus;
/// 启动状态推送
void start() {
if (_timer != null) return;
// 立即发送初始状态
_statusController.add(_currentStatus);
// 模拟每3秒更新一次状态
_timer = Timer.periodic(const Duration(seconds: 3), (timer) {
_simulateStatusUpdate();
});
}
/// 停止状态推送
void stop() {
_timer?.cancel();
_timer = null;
}
/// 手动更新状态(用于外部触发更新)
void updateStatus(RobotStatusModel status) {
_currentStatus = status;
_statusController.add(status);
}
/// 模拟状态更新
void _simulateStatusUpdate() {
final random = Random();
final statuses = ['idle', 'running', 'charging', 'error'];
_currentStatus = _currentStatus.copyWith(
battery: max(0, _currentStatus.battery + random.nextInt(3) - 1),
status: random.nextDouble() > 0.95 ? statuses[random.nextInt(statuses.length)] : _currentStatus.status,
signal: min(100, max(0, _currentStatus.signal + random.nextInt(5) - 2)),
speed: _currentStatus.status == 'running' ? random.nextDouble() * 5 : 0,
temperature: min(50, max(20, _currentStatus.temperature + random.nextInt(3) - 1)),
runTime: _updateRunTime(),
);
_statusController.add(_currentStatus);
}
/// 更新运行时间
String _updateRunTime() {
if (_currentStatus.status != 'running') {
return _currentStatus.runTime;
}
final parts = _currentStatus.runTime.split(':');
int hours = int.parse(parts[0]);
int minutes = int.parse(parts[1]);
int seconds = int.parse(parts[2]);
seconds++;
if (seconds >= 60) {
seconds = 0;
minutes++;
}
if (minutes >= 60) {
minutes = 0;
hours++;
}
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
/// 释放资源
void dispose() {
stop();
_statusController.close();
}
}

View File

@@ -1,123 +0,0 @@
import 'package:flutter/material.dart';
import 'float_bar_controller.dart';
/// 简单悬浮条组件
class SimpleFloatBar extends StatefulWidget {
const SimpleFloatBar({super.key});
@override
State<SimpleFloatBar> createState() => _SimpleFloatBarState();
}
class _SimpleFloatBarState extends State<SimpleFloatBar> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 100),
child: GestureDetector(
onTap: () => setState(() => _isExpanded = !_isExpanded),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
height: _isExpanded ? 200 : 56,
width: double.infinity,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFFE8F5E9), Color(0xFFFFFFFF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.green.withOpacity(0.2), width: 1),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.12),
blurRadius: 12,
offset: const Offset(0, 4),
spreadRadius: 2,
),
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 20,
offset: const Offset(0, 8),
spreadRadius: 1,
),
],
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: _isExpanded
? SingleChildScrollView(child: _buildExpanded())
: _buildCollapsed(),
),
),
),
);
}
Widget _buildCollapsed() {
return Row(children: [
Container(width: 10, height: 10, decoration: BoxDecoration(color: Colors.green, borderRadius: BorderRadius.circular(5))),
const SizedBox(width: 12),
const Expanded(child: Text('设备运行中', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black87))),
const SizedBox(width: 12),
Row(children: const [Icon(Icons.battery_full, size: 18, color: Colors.grey), SizedBox(width: 4), Text('85%', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500))]),
const SizedBox(width: 8),
const Icon(Icons.keyboard_arrow_up, size: 20, color: Colors.grey),
const SizedBox(width: 8),
_buildCloseButton(),
]);
}
Widget _buildExpanded() {
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Container(width: 10, height: 10, decoration: BoxDecoration(color: Colors.green, borderRadius: BorderRadius.circular(5))),
const SizedBox(width: 8),
Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration(color: Colors.green.withOpacity(0.1), borderRadius: BorderRadius.circular(4)), child: const Text('运行中', style: TextStyle(fontSize: 12, color: Colors.green, fontWeight: FontWeight.w500))),
const SizedBox(width: 12),
const Expanded(child: Text('设备运行中', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.black87))),
const Icon(Icons.keyboard_arrow_down, size: 20, color: Colors.grey),
const SizedBox(width: 8),
_buildCloseButton(),
]),
const SizedBox(height: 16),
Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [
_infoItem(Icons.battery_full, '电量', '85%'),
_infoItem(Icons.signal_cellular_alt, '信号', '100%'),
_infoItem(Icons.speed, '速度', '0.0m/s'),
_infoItem(Icons.thermostat, '温度', '25°C'),
]),
const SizedBox(height: 12),
Row(children: const [
Icon(Icons.location_on, size: 14, color: Colors.grey),
SizedBox(width: 4),
Expanded(child: Text('北京市朝阳区', style: TextStyle(fontSize: 12, color: Colors.grey))),
SizedBox(width: 12),
Icon(Icons.timer, size: 14, color: Colors.grey),
SizedBox(width: 4),
Text('02:35:18', style: TextStyle(fontSize: 12, color: Colors.grey)),
]),
]);
}
Widget _buildCloseButton() {
return GestureDetector(
onTap: () => FloatBarController.setVisible(false),
child: Container(padding: const EdgeInsets.all(4), decoration: BoxDecoration(color: Colors.grey.withOpacity(0.1), borderRadius: BorderRadius.circular(8)), child: const Icon(Icons.close, size: 16, color: Colors.grey)),
);
}
Widget _infoItem(IconData icon, String label, String value) {
return Column(mainAxisSize: MainAxisSize.min, children: [
Icon(icon, size: 20, color: Colors.grey),
const SizedBox(height: 4),
Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)),
const SizedBox(height: 2),
Text(value, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
]);
}
}

View File

@@ -1,350 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/float_bar_bloc.dart';
import '../model/robot_status_model.dart';
import '../service/robot_status_service.dart';
import '../cubit/float_bar_setting_cubit.dart';
/// 悬浮条UI组件
/// 内部独立管理Bloc,不依赖外部注入
class FloatBarWidget extends StatefulWidget {
const FloatBarWidget({super.key});
@override
State<FloatBarWidget> createState() => _FloatBarWidgetState();
}
class _FloatBarWidgetState extends State<FloatBarWidget> {
late final FloatBarBloc _bloc;
@override
void initState() {
super.initState();
debugPrint('🔥 [FloatBarWidget] initState - 开始创建 Bloc');
// 内部创建Bloc并启动服务(不需要FloatBarManager,因为我们使用Stack方式)
_bloc = FloatBarBloc(RobotStatusService());
_bloc.startService();
debugPrint('✅ [FloatBarWidget] Bloc 已创建并启动服务');
}
@override
void dispose() {
debugPrint('🔥 [FloatBarWidget] dispose - 关闭 Bloc');
_bloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
debugPrint('🔥 [FloatBarWidget] build - 渲染悬浮条');
return BlocProvider.value(value: _bloc, child: const _FloatBarContent());
}
}
/// 悬浮条内容组件
class _FloatBarContent extends StatelessWidget {
const _FloatBarContent();
@override
Widget build(BuildContext context) {
return BlocBuilder<FloatBarBloc, FloatBarState>(
builder: (context, state) {
final isExpanded = state is FloatBarExpandedState;
final status = state is FloatBarCollapsedState
? state.status
: (state as FloatBarExpandedState).status;
debugPrint(
'🔥 [FloatBarContent] build - isExpanded: $isExpanded, status: ${status.taskName}',
);
return Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 100), // 底部留出 Tab 栏空间
child: GestureDetector(
onTap: () {
debugPrint('🔥 [FloatBarContent] 点击悬浮条');
context.read<FloatBarBloc>().add(ToggleExpandEvent());
},
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
height: isExpanded ? 180 : 56,
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFFE8F5E9), // 浅绿色
Color(0xFFFFFFFF), // 白色
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.green.withOpacity(0.2),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.12),
blurRadius: 12,
offset: const Offset(0, 4),
spreadRadius: 2,
),
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 20,
offset: const Offset(0, 8),
spreadRadius: 1,
),
BoxShadow(
color: Colors.white.withOpacity(0.6),
blurRadius: 8,
offset: const Offset(0, -2),
spreadRadius: -2,
),
],
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: isExpanded
? _buildExpandedContent(status)
: _buildCollapsedContent(status),
),
),
),
);
},
);
}
/// 折叠态内容
Widget _buildCollapsedContent(RobotStatusModel status) {
return Row(
children: [
// 状态指示灯
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: _parseColor(status.statusColor),
borderRadius: BorderRadius.circular(5),
),
),
const SizedBox(width: 12),
// 任务名称
Expanded(
child: Text(
status.taskName,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 12),
// 电量
Row(
children: [
Icon(
status.battery > 20 ? Icons.battery_full : Icons.battery_alert,
size: 18,
color: status.battery > 20 ? Colors.grey : Colors.red,
),
const SizedBox(width: 4),
Text(
'${status.battery}%',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: status.battery > 20 ? Colors.black87 : Colors.red,
),
),
],
),
const SizedBox(width: 8),
// 展开箭头
const Icon(Icons.keyboard_arrow_up, size: 20, color: Colors.grey),
const SizedBox(width: 8),
// 关闭按钮
GestureDetector(
onTap: () {
debugPrint('🔥 [FloatBarContent] 点击关闭按钮');
// 🔥 使用静态方法关闭悬浮条
FloatBarSettingService.setEnabled(false);
},
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.close, size: 16, color: Colors.grey),
),
),
],
);
}
/// 展开态内容
Widget _buildExpandedContent(RobotStatusModel status) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 顶部:状态+任务名+折叠按钮
Row(
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: _parseColor(status.statusColor),
borderRadius: BorderRadius.circular(5),
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: _parseColor(status.statusColor).withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
status.statusText,
style: TextStyle(
fontSize: 12,
color: _parseColor(status.statusColor),
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
status.taskName,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.keyboard_arrow_down, size: 20, color: Colors.grey),
const SizedBox(width: 8),
// 关闭按钮
GestureDetector(
onTap: () {
debugPrint('🔥 [FloatBarContent] 点击关闭按钮(展开态)');
// 🔥 使用静态方法关闭悬浮条
FloatBarSettingService.setEnabled(false);
},
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.close, size: 16, color: Colors.grey),
),
),
],
),
const SizedBox(height: 16),
// 中间:详细信息网格
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildInfoItem(
status.battery > 20 ? Icons.battery_full : Icons.battery_alert,
'电量',
'${status.battery}%',
status.battery > 20 ? Colors.black87 : Colors.red,
),
_buildInfoItem(
Icons.signal_cellular_alt,
'信号',
'${status.signal}%',
Colors.black87,
),
_buildInfoItem(
Icons.speed,
'速度',
'${status.speed.toStringAsFixed(1)}m/s',
Colors.black87,
),
_buildInfoItem(
Icons.thermostat,
'温度',
'${status.temperature}°C',
Colors.black87,
),
],
),
const SizedBox(height: 12),
// 底部:位置+时间
Row(
children: [
const Icon(Icons.location_on, size: 14, color: Colors.grey),
const SizedBox(width: 4),
Expanded(
child: Text(
status.location,
style: const TextStyle(fontSize: 12, color: Colors.grey),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 12),
const Icon(Icons.timer, size: 14, color: Colors.grey),
const SizedBox(width: 4),
Text(
status.runTime,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
],
);
}
/// 信息项组件
Widget _buildInfoItem(
IconData icon,
String label,
String value,
Color valueColor,
) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 20, color: Colors.grey),
const SizedBox(height: 4),
Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)),
const SizedBox(height: 2),
Text(
value,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: valueColor,
),
),
],
);
}
/// 解析颜色字符串
Color _parseColor(String colorStr) {
try {
return Color(int.parse(colorStr.replaceFirst('#', '0xFF')));
} catch (_) {
return Colors.grey;
}
}
}

View File

@@ -17,6 +17,7 @@ import '../widgets/device_item_widget.dart';
import '../widgets/drone_station_item_card.dart';
import 'robot_list_page.dart';
import 'drone_station_detail_page.dart';
import '../../../../remote_control/presentation/bloc/remote_control_cubit.dart';
/// 设备状态页面 - 使用 BLoC 模式
class DeviceStatusPage extends StatelessWidget {
@@ -50,22 +51,35 @@ class DeviceStatusView extends StatelessWidget {
child: Scaffold(
backgroundColor: const Color(0xFFF7F7F7),
body: SafeArea(
child:
BlocBuilder<
DeviceListBloc.DeviceStatusBloc,
DeviceListState.DeviceStatusState
>(
builder: (context, state) {
return Column(
children: [
_buildAppBar(context),
_buildSearchBar(context),
_buildTypeFilterBar(context),
Expanded(child: _buildContent(context, state)),
],
);
},
),
child: BlocConsumer<
DeviceListBloc.DeviceStatusBloc,
DeviceListState.DeviceStatusState
>(
listener: (context, state) {
// 🔥 监听错误状态,显示友好提示
if (state is DeviceListState.DeviceStatusError &&
state.shouldShowError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
backgroundColor: Colors.orange,
),
);
}
},
builder: (context, state) {
return Column(
children: [
_buildAppBar(context),
_buildSearchBar(context),
_buildTypeFilterBar(context),
Expanded(child: _buildContent(context, state)),
],
);
},
),
),
),
);
@@ -264,33 +278,11 @@ class DeviceStatusView extends StatelessWidget {
);
}
// 🔥 错误状态不再显示全屏错误页面,而是通过 listener 显示 SnackBar
// 页面保持当前内容,用户可以继续操作
if (state is DeviceListState.DeviceStatusError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: Color(0xFF86909C)),
const SizedBox(height: 16),
Text(
state.message,
style: const TextStyle(fontSize: 14, color: Color(0xFF4E5969)),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.read<DeviceListBloc.DeviceStatusBloc>().add(
const DeviceListEvent.DeviceStatusLoadData(),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF165DFF),
foregroundColor: Colors.white,
),
child: const Text('重试'),
),
],
),
);
// 如果是从 Loaded 状态变成 Error,返回空容器保持页面
return const SizedBox.shrink();
}
if (state is DeviceListState.DeviceStatusLoaded) {
@@ -375,7 +367,20 @@ class DeviceStatusView extends StatelessWidget {
return BlocProvider(
create: (_) =>
sl<DroneStationBloc>()..add(DroneStationLoadData(selectedSite.id)),
child: BlocBuilder<DroneStationBloc, DroneStationState>(
child: BlocConsumer<DroneStationBloc, DroneStationState>(
listener: (context, state) {
// 🔥 监听错误状态,显示友好提示
if (state is DroneStationError && state.shouldShowError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
backgroundColor: Colors.orange,
),
);
}
},
builder: (context, state) {
if (state is DroneStationLoading) {
return const Center(
@@ -383,40 +388,10 @@ class DeviceStatusView extends StatelessWidget {
);
}
// 🔥 错误状态不再显示全屏错误页面,而是通过 listener 显示 SnackBar
if (state is DroneStationError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 48,
color: Color(0xFF86909C),
),
const SizedBox(height: 16),
Text(
state.message,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF4E5969),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.read<DroneStationBloc>().add(
DroneStationLoadData(selectedSite.id),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF165DFF),
foregroundColor: Colors.white,
),
child: const Text('重试'),
),
],
),
);
// 返回空容器保持页面
return const SizedBox.shrink();
}
if (state is DroneStationLoaded) {
@@ -572,8 +547,11 @@ class DeviceStatusView extends StatelessWidget {
),
),
builder: (BuildContext context) {
return BlocProvider.value(
value: context.read<DeviceStatusBloc>(),
return MultiBlocProvider(
providers: [
BlocProvider.value(value: context.read<DeviceStatusBloc>()),
BlocProvider.value(value: sl<RemoteControlCubit>()),
],
child: const DeviceStatusModal(),
);
},

View File

@@ -11,9 +11,8 @@ import '../../domain/usecases/update_flight_task_status_usecase.dart';
/// 无人机任务与航线控制页面
class DroneMissionControlPage extends StatefulWidget {
final List<FlightTaskEntity>? selectedTasks;
final String? droneSn; // 无人机序列号
const DroneMissionControlPage({super.key, this.selectedTasks, this.droneSn});
const DroneMissionControlPage({super.key, this.selectedTasks});
@override
State<DroneMissionControlPage> createState() =>
@@ -23,12 +22,7 @@ class DroneMissionControlPage extends StatefulWidget {
class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
FlightTaskEntity? _listTask; // 列表数据
FlightTaskDetailEntity? _detailTask; // 详情数据
String? _droneSn; // 无人机序列号
bool _isLoading = false;
bool _isReturningHome = false; // 是否正在返航
bool _isPausing = false; // 是否正在暂停
bool _isReturnHomeLoading = false; // 返航命令是否正在执行
bool _isPauseLoading = false; // 暂停命令是否正在执行
final Dio _dio = Dio();
@override
@@ -38,7 +32,6 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
_listTask = widget.selectedTasks!.first;
_loadTaskDetail();
}
_droneSn = widget.droneSn; // 初始化无人机序列号
}
Future<void> _loadTaskDetail() async {
@@ -78,7 +71,6 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
final detailData = jsonData['data'];
print('🔍 [DroneMissionControl] data 字段类型: ${detailData.runtimeType}');
print('🔍 [DroneMissionControl] data 字段完整内容: $detailData');
// 确保 data 也是 Map
final Map<String, dynamic> detailMap = (detailData is Map)
@@ -126,7 +118,7 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
print('❌ [DroneMissionControl] 执行任务失败: ${failure.message}');
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('下发指令失败')));
).showSnackBar(SnackBar(content: Text(failure.message)));
},
(data) {
print('✅ [DroneMissionControl] 任务执行成功: $data');
@@ -139,105 +131,7 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
print('❌ [DroneMissionControl] 执行任务异常: $e');
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('下发指令失败')));
}
}
// 返航/取消返航
Future<void> _toggleReturnHome() async {
if (_detailTask == null || _detailTask!.sn.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('设备SN不存在')));
return;
}
setState(() => _isReturnHomeLoading = true);
try {
final command = _isReturningHome ? 'return_home_cancel' : 'return_home';
print('🔍 [DroneMissionControl] 发送返航命令: $command, deviceSn: $_droneSn');
final response = await _dio.post(
HttpApiConsts.flightTaskCommand,
data: {'command': command, 'deviceSn': _droneSn},
);
print('📦 [DroneMissionControl] 返航响应: ${response.data}');
if (response.statusCode == 200) {
final responseData = response.data;
if (responseData['code'] == 0) {
setState(() => _isReturningHome = !_isReturningHome);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(_isReturningHome ? '已下发返航命令' : '已取消返航')),
);
}
} else {
throw Exception(responseData['message'] ?? '操作失败');
}
} else {
throw Exception('请求失败');
}
} catch (e) {
print('❌ [DroneMissionControl] 返航操作失败: $e');
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('下发指令失败')));
}
} finally {
setState(() => _isReturnHomeLoading = false);
}
}
// 暂停/取消暂停
Future<void> _togglePause() async {
if (_detailTask == null || _detailTask!.sn.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('设备SN不存在')));
return;
}
setState(() => _isPauseLoading = true);
try {
final command = _isPausing ? 'flighttask_recovery' : 'flighttask_pause';
print('🔍 [DroneMissionControl] 发送暂停命令: $command, deviceSn: $_droneSn');
final response = await _dio.post(
HttpApiConsts.flightTaskCommand,
data: {'command': command, 'deviceSn': _droneSn},
);
print('📦 [DroneMissionControl] 暂停响应: ${response.data}');
if (response.statusCode == 200) {
final responseData = response.data;
if (responseData['code'] == 0) {
setState(() => _isPausing = !_isPausing);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(_isPausing ? '已下发暂停命令' : '已取消暂停')),
);
}
} else {
throw Exception(responseData['message'] ?? '操作失败');
}
} else {
throw Exception('请求失败');
}
} catch (e) {
print('❌ [DroneMissionControl] 暂停操作失败: $e');
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('下发指令失败')));
}
} finally {
setState(() => _isPauseLoading = false);
).showSnackBar(SnackBar(content: Text('执行任务失败: $e')));
}
}
@@ -362,7 +256,7 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
],
),
const SizedBox(height: 12),
_buildInfoRow('网关序列号', task.sn),
_buildInfoRow('设备序列号', task.sn),
const SizedBox(height: 12),
_buildInfoRow('任务类型', task.taskType),
const SizedBox(height: 12),
@@ -628,7 +522,7 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: _isPauseLoading ? null : _togglePause,
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFF7D00),
foregroundColor: Colors.white,
@@ -638,28 +532,16 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
),
elevation: 0,
),
child: _isPauseLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
_isPausing ? '取消暂停' : '暂停任务',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
child: const Text(
'暂停任务',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton(
onPressed: _isReturnHomeLoading ? null : _toggleReturnHome,
onPressed: () {},
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF4E5969),
side: const BorderSide(color: Color(0xFFC9CDD4)),
@@ -668,19 +550,10 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
borderRadius: BorderRadius.circular(8),
),
),
child: _isReturnHomeLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(
_isReturningHome ? '取消返航' : '返航降落',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
child: const Text(
'返航降落',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
),
),
),
],

View File

@@ -28,8 +28,10 @@ class DroneStationDetailPage extends StatefulWidget {
class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
late DroneStationBloc _bloc;
UAVDetailEntity? _detail; // 无人机详情数据
String? _droneSn; // 无人机序列号
// 无人机详情数据
UAVDetailEntity? _detail;
String? _droneSn;
// 悬浮视频监控状态
bool showFloatingMonitor = false;
@@ -55,7 +57,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
// 加载超时计时器
Timer? _floatingLoadingTimer;
static const _floatingLoadingTimeout = Duration(seconds: 15);
// 无人机状态轮询计时器
Timer? _droneStatusPollingTimer;
@@ -69,7 +71,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
deviceSn: widget.station.deviceSn,
),
);
// 启动无人机状态轮询(每5秒刷新一次)
_startDroneStatusPolling();
}
@@ -81,15 +83,15 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
debugPrint(' - 是否已显示: $showFloatingMonitor');
debugPrint(' - 无人机在线: ${detail.droneOnlineStatus}');
debugPrint(' - 机场摄像头: ${detail.gatewayCameraList?.length ?? 0}');
if (!_isFloatingMonitorEnabled || showFloatingMonitor) {
debugPrint('❌ 不满足条件,退出检查');
return; // 开关关闭或已显示,不执行
}
// 无人机在线且有机场摄像头,自动打开悬浮窗
if (detail.droneOnlineStatus == 1 &&
detail.gatewayCameraList != null &&
if (detail.droneOnlineStatus == 1 &&
detail.gatewayCameraList != null &&
detail.gatewayCameraList!.isNotEmpty) {
debugPrint('✅ 检测到无人机在线,自动打开悬浮窗');
_loadFloatingVideoStream();
@@ -109,13 +111,30 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
/// 启动无人机状态轮询
void _startDroneStatusPolling() {
// 每5秒刷新一次无人机状态
_droneStatusPollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
if (!mounted) {
timer.cancel();
return;
_scheduleDroneStatusPoll();
}
/// 根据无人机状态动态调整轮询周期
void _scheduleDroneStatusPoll() {
if (!mounted) return;
// 检查当前无人机状态
Duration interval = const Duration(seconds: 60); // 默认60秒
final currentState = _bloc.state;
if (currentState is UAVDetailLoaded) {
if (currentState.detail.droneOnlineStatus == 1) {
// 无人机在线时,每15秒轮询一次
interval = const Duration(seconds: 15);
} else {
// 无人机离线时,每60秒轮询一次(降低频率)
interval = const Duration(seconds: 60);
}
}
_droneStatusPollingTimer?.cancel();
_droneStatusPollingTimer = Timer(interval, () {
if (!mounted) return;
debugPrint('🔄 定时刷新无人机状态...');
_bloc.add(
UAVDetailLoad(
@@ -123,17 +142,20 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
deviceSn: widget.station.deviceSn,
),
);
// 如果悬浮窗开启且无人机上线,自动显示悬浮窗
if (_isFloatingMonitorEnabled) {
final currentState = _bloc.state;
if (currentState is UAVDetailLoaded &&
currentState.detail.droneOnlineStatus == 1 &&
final state = _bloc.state;
if (state is UAVDetailLoaded &&
state.detail.droneOnlineStatus == 1 &&
!showFloatingMonitor) {
debugPrint('✅ 无人机已上线,自动打开悬浮窗');
_loadFloatingVideoStream();
}
}
// 重新调度下一次轮询(动态周期)
_scheduleDroneStatusPoll();
});
}
@@ -203,10 +225,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
_initFloatingRtcEngine();
} else if (state is VideoStreamError) {
_floatingLoadingTimer?.cancel();
setState(() {
_floatingErrorMessage = state.message;
_isFloatingLoading = false;
});
setState(() {});
}
},
builder: (context, state) {
@@ -674,7 +693,6 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
),
),
),
const SizedBox(height: 12),
// 悬浮观看功能已禁用
// GestureDetector(
// onTap: () {
@@ -739,10 +757,8 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DroneMissionControlPage(
selectedTasks: tasks,
droneSn: _droneSn,
),
builder: (context) =>
DroneMissionControlPage(selectedTasks: tasks),
),
);
}

View File

@@ -46,7 +46,20 @@ class _RobotListViewState extends State<RobotListView> {
@override
Widget build(BuildContext context) {
return BlocBuilder<RobotListBloc, RobotListState>(
return BlocConsumer<RobotListBloc, RobotListState>(
listener: (context, state) {
// 🔥 监听错误状态,显示友好提示
if (state is RobotListError && state.shouldShowError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
backgroundColor: Colors.orange,
),
);
}
},
builder: (context, state) {
if (state is RobotListLoading) {
return const Center(
@@ -56,27 +69,10 @@ class _RobotListViewState extends State<RobotListView> {
);
}
// 🔥 错误状态不再显示全屏错误页面,而是通过 listener 显示 SnackBar
if (state is RobotListError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 48,
color: Color(0xFFF53F3F),
),
const SizedBox(height: 16),
Text(
'加载失败: ${state.message}',
style: const TextStyle(
fontSize: 14,
color: Color(0xFF4E5969),
),
),
],
),
);
// 返回空容器保持页面
return const SizedBox.shrink();
}
if (state is RobotListLoaded) {
@@ -480,9 +476,11 @@ class _RobotListViewState extends State<RobotListView> {
status: robot.status,
battery: robot.battery,
task: robot.task,
onTap: () {
onTap: () async {
// final logger = sl<ILoggerService>();
debugPrint('📱 [选中机器人] name: ${robot.name}, id: ${robot.id}, type: ${robot.type}, status: ${robot.status}, battery: ${robot.battery}, task: ${robot.task}');
debugPrint('🔴🔴🔴 [选中机器人] ========== 点击事件触发 ==========');
debugPrint('🔴🔴🔴 [选中机器人] name: ${robot.name}, id: ${robot.id}, type: ${robot.type}');
debugPrint('🔴🔴🔴 [选中机器人] status: ${robot.status}, battery: ${robot.battery}, task: ${robot.task}');
// 1. 将当前机器人设置为全局待控制设备(用 robot.name 作为 deviceName)
final device = DeviceEntity(
@@ -495,9 +493,11 @@ class _RobotListViewState extends State<RobotListView> {
onlineStatus: robot.status == '在线' ? 1 : 0,
);
debugPrint('🎯 [RobotListPage] 准备调用 setTargetDevice...');
// 🔥 使用 GetIt 直接获取 RemoteControlCubit 单例
final remoteCubit = GetIt.I<RemoteControlCubit>();
remoteCubit.setTargetDevice(device);
remoteCubit.setTargetDevice(device); // 🔥 TCP连接在后台异步执行
debugPrint('✅ [RobotListPage] setTargetDevice 已调用(TCP连接中)');
// 2. 跳转到机器人控制页面
final robotMap = {
@@ -509,6 +509,7 @@ class _RobotListViewState extends State<RobotListView> {
'task': robot.task,
};
debugPrint('🚀 [RobotListPage] 准备跳转页面...');
Navigator.push(
context,
MaterialPageRoute(

View File

@@ -3,9 +3,7 @@ import '../pages/drone_mission_control_page.dart';
/// 无人机机场与设备状态组件
class DroneStationStatusWidget extends StatelessWidget {
final String? droneSn; // 无人机序列号
const DroneStationStatusWidget({super.key, this.droneSn});
const DroneStationStatusWidget({super.key});
@override
Widget build(BuildContext context) {
@@ -222,8 +220,7 @@ class DroneStationStatusWidget extends StatelessWidget {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DroneMissionControlPage(droneSn: droneSn),
builder: (context) => const DroneMissionControlPage(),
),
);
},

View File

@@ -6,6 +6,7 @@ import 'package:maibu_satabot_v2/features/v2/home/domain/usecases/get_home_data_
import 'package:maibu_satabot_v2/features/v2/home/domain/usecases/get_site_list_usecase.dart';
import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_event.dart';
import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_state.dart';
import '../../../../../core/network/error_handler.dart';
class HomeV2Bloc extends Bloc<HomeV2Event, HomeV2State> {
final GetHomeDataUseCase getHomeDataUseCase;
@@ -27,7 +28,10 @@ class HomeV2Bloc extends Bloc<HomeV2Event, HomeV2State> {
final user = appUserCubit.state.user;
if (user == null) {
emit(const HomeV2Error('用户未登录'));
emit(const HomeV2Error(
message: '用户未登录',
shouldShowError: true,
));
return;
}
@@ -36,7 +40,10 @@ class HomeV2Bloc extends Bloc<HomeV2Event, HomeV2State> {
final siteResult = await getSiteListUseCase(user.orgId); // 使用用户的 orgId
homeResult.fold(
(failure) => emit(HomeV2Error(failure.message)),
(failure) => emit(HomeV2Error(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
)),
(homeData) {
List<SiteEntity> sites = [];
SiteEntity? selectedSite;
@@ -87,7 +94,10 @@ class HomeV2Bloc extends Bloc<HomeV2Event, HomeV2State> {
final homeResult = await getHomeDataUseCase(const NoParams());
homeResult.fold(
(failure) => emit(HomeV2Error(failure.message)),
(failure) => emit(HomeV2Error(
message: ErrorHandler.getErrorMessage(failure.message),
shouldShowError: true, // 🔥 标记需要显示弹窗
)),
(homeData) {
emit(HomeV2Loaded(
homeData: homeData,

View File

@@ -50,9 +50,13 @@ class HomeV2Loaded extends HomeV2State {
class HomeV2Error extends HomeV2State {
final String message;
final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗
const HomeV2Error(this.message);
const HomeV2Error({
required this.message,
this.shouldShowError = false, // 默认 false
});
@override
List<Object?> get props => [message];
List<Object?> get props => [message, shouldShowError];
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:maibu_satabot_v2/core/di/injection.dart';
import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_bloc.dart';
import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_event.dart';
@@ -15,6 +16,8 @@ import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/tcp_statu
import 'package:maibu_satabot_v2/components/device_status_modal.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
import '../../../../remote_control/presentation/bloc/remote_control_cubit.dart';
class HomeV2Page extends StatefulWidget {
const HomeV2Page({super.key});
@@ -35,29 +38,26 @@ class _HomeV2PageState extends State<HomeV2Page> {
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F7FA),
body: BlocBuilder<HomeV2Bloc, HomeV2State>(
builder: (context, state) {
if (state is HomeV2Error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
size: 48,
color: Color(0xFF86909C),
),
const SizedBox(height: 16),
Text(state.message),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => _bloc.add(const HomeV2LoadData()),
child: const Text('重试'),
),
],
body: BlocConsumer<HomeV2Bloc, HomeV2State>(
listener: (context, state) {
// 🔥 监听错误状态,显示友好提示
if (state is HomeV2Error && state.shouldShowError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.message),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
backgroundColor: Colors.orange,
),
);
}
},
builder: (context, state) {
// 🔥 错误状态不再显示全屏错误页面,而是通过 listener 显示 SnackBar
if (state is HomeV2Error) {
// 返回空容器保持页面
return const SizedBox.shrink();
}
if (state is HomeV2Loaded) {
return RefreshIndicator(
@@ -345,8 +345,15 @@ class _HomeV2PageState extends State<HomeV2Page> {
),
),
builder: (BuildContext context) {
return BlocProvider.value(
value: context.read<DeviceStatusBloc>(),
return MultiBlocProvider(
providers: [
BlocProvider.value(
value: context.read<DeviceStatusBloc>(),
),
BlocProvider.value(
value: GetIt.I<RemoteControlCubit>(), // 🔥 注入 RemoteControlCubit
),
],
child: const DeviceStatusModal(),
);
},

View File

@@ -86,29 +86,33 @@ class _TcpStatusIndicatorState extends State<TcpStatusIndicator>
message: tooltip,
child: InkWell(
onTap: widget.onTap,
borderRadius: BorderRadius.circular(6),
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
final opacity = shouldAnimate ? _animation.value : 1.0;
return Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color.withOpacity(opacity),
shape: BoxShape.circle,
boxShadow: state.status == TcpConnectionStatus.connected
? [
BoxShadow(
color: Colors.green.withOpacity(0.5 * opacity),
blurRadius: 6 * opacity,
spreadRadius: 2 * opacity,
),
]
: [],
),
);
},
borderRadius: BorderRadius.circular(12),
// 🔥 增大点击热区
child: Container(
padding: const EdgeInsets.all(8),
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
final opacity = shouldAnimate ? _animation.value : 1.0;
return Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color.withOpacity(opacity),
shape: BoxShape.circle,
boxShadow: state.status == TcpConnectionStatus.connected
? [
BoxShadow(
color: Colors.green.withOpacity(0.5 * opacity),
blurRadius: 6 * opacity,
spreadRadius: 2 * opacity,
),
]
: [],
),
);
},
),
),
),
);

View File

@@ -8,9 +8,6 @@ import 'package:maibu_satabot_v2/core/router/route_paths.dart';
import 'package:maibu_satabot_v2/core/storage/user_storage.dart';
import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart';
import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart';
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/float_bar_controller.dart';
final sl = GetIt.instance;
/// 系统设置综合页面
class SystemSettingsPage extends StatefulWidget {
@@ -21,17 +18,6 @@ class SystemSettingsPage extends StatefulWidget {
}
class _SystemSettingsPageState extends State<SystemSettingsPage> {
/// 悬浮条开关状态
bool _floatBarEnabled = true;
@override
void initState() {
super.initState();
// 初始化时获取当前状态(使用静态属性)
_floatBarEnabled = FloatBarController.isVisible;
print('🔍 [设置页面] initState,初始状态: $_floatBarEnabled');
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -59,9 +45,6 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
// Tab 设置
_buildTabSettingsSection(),
const SizedBox(height: 12),
// 悬浮条设置
_buildFloatBarSection(),
const SizedBox(height: 12),
// 语言设置
_buildLanguageSection(),
const SizedBox(height: 12),
@@ -105,9 +88,9 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
if (state.runtimeType.toString() != 'TabConfigLoaded') {
return const Center(child: CircularProgressIndicator());
}
final tabs = state.config.items;
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
@@ -181,18 +164,14 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
AppLocalizations.of(context).translate('my.chinese'),
'zh',
locale.languageCode == 'zh',
() => context.read<LocaleCubit>().setLocale(
const Locale('zh', 'CN'),
),
() => context.read<LocaleCubit>().setLocale(const Locale('zh', 'CN')),
),
const SizedBox(height: 12),
_buildLanguageOption(
AppLocalizations.of(context).translate('my.english'),
'en',
locale.languageCode == 'en',
() => context.read<LocaleCubit>().setLocale(
const Locale('en', 'US'),
),
() => context.read<LocaleCubit>().setLocale(const Locale('en', 'US')),
),
],
);
@@ -203,25 +182,16 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
);
}
Widget _buildLanguageOption(
String label,
String code,
bool isSelected,
VoidCallback onTap,
) {
Widget _buildLanguageOption(String label, String code, bool isSelected, VoidCallback onTap) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF165DFF).withOpacity(0.1)
: Colors.transparent,
color: isSelected ? const Color(0xFF165DFF).withOpacity(0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected
? const Color(0xFF165DFF)
: const Color(0xFFE5E6EB),
color: isSelected ? const Color(0xFF165DFF) : const Color(0xFFE5E6EB),
width: 1,
),
),
@@ -232,9 +202,7 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
label,
style: TextStyle(
fontSize: 14,
color: isSelected
? const Color(0xFF165DFF)
: const Color(0xFF1D2129),
color: isSelected ? const Color(0xFF165DFF) : const Color(0xFF1D2129),
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
@@ -284,94 +252,6 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
);
}
/// 悬浮条设置
Widget _buildFloatBarSection() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x0D000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'悬浮条设置',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
),
),
const SizedBox(height: 16),
_buildFloatBarSwitch(),
],
),
);
}
/// 悬浮条开关
Widget _buildFloatBarSwitch() {
print(
'🔍 [设置页面] _buildFloatBarSwitch 被调用,_floatBarEnabled: $_floatBarEnabled',
);
return Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'显示悬浮条',
style: TextStyle(fontSize: 14, color: Color(0xFF1D2129)),
),
SizedBox(height: 4),
Text(
'在Tab页面显示设备状态悬浮条',
style: TextStyle(fontSize: 12, color: Color(0xFF8F959E)),
),
],
),
),
Switch(
value: _floatBarEnabled,
onChanged: (value) {
print('🔍 [设置页面] 开关被点击,新值: $value,当前状态: $_floatBarEnabled');
// 🔥 立即更新本地状态
setState(() {
_floatBarEnabled = value;
});
print('🔍 [设置页面] 本地状态已更新为: $_floatBarEnabled');
// 🔥 使用静态方法设置新值
FloatBarController.setVisible(value);
print('🔍 [设置页面] setVisible 已调用完成');
// 显示提示
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(value ? '悬浮条已开启' : '悬浮条已关闭'),
duration: const Duration(seconds: 2),
),
);
}
},
activeColor: const Color(0xFF165DFF),
),
],
);
}
/// 显示退出登录确认对话框
void _showLogoutDialog(BuildContext context) {
showDialog(

View File

@@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../../../core/network/tcp/tcp_client.dart';
import '../../../home/domain/entities/site_entity.dart';
class SiteState {
@@ -48,8 +51,26 @@ class SiteCubit extends Cubit<SiteState> {
/// 选择场站(持久化)
void selectSite(SiteEntity site) {
debugPrint('🏭 [SiteCubit] ========== 切换场站 ==========');
debugPrint('🏭 [SiteCubit] 从 ${state.selectedSite?.siteName ?? "无"} 切换到 ${site.siteName}');
// 🔥 关键修复:只有切换到不同场站时才断开TCP
if (state.selectedSite?.id != site.id) {
final tcpClient = GetIt.I<TcpClient>();
if (tcpClient.isConnected) {
debugPrint('🛑 [SiteCubit] 检测到TCP已连接,正在断开...');
tcpClient.disconnect();
debugPrint('✅ [SiteCubit] TCP已断开,防止场站间设备数据混淆');
} else {
debugPrint('ℹ️ [SiteCubit] TCP未连接,无需断开');
}
} else {
debugPrint('✅ [SiteCubit] 相同场站,保持TCP连接状态');
}
sharedPreferences.setInt(_selectedSiteIdKey, site.id);
emit(state.copyWith(selectedSite: site));
debugPrint('✅ [SiteCubit] 场站切换完成');
}
/// 清除选中场站