构建初步的设备运行悬浮状态栏,已实现初步的的悬浮和开关功能。
优化了登录链条适配多种用户的登录操作
This commit is contained in:
@@ -17,6 +17,7 @@ import 'package:maibu_satabot_v2/features/my/repository/my_repository.dart';
|
||||
import 'package:maibu_satabot_v2/features/my/repository/my_repository_impl.dart';
|
||||
import 'package:maibu_satabot_v2/features/my/usecases/updatename_usecase.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/cubit/float_bar_setting_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/data/repositories/remote_control_repository_impl.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/domain/repositories/remote_control_repository.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/domain/usecase/diff_steer_usecase.dart';
|
||||
@@ -368,6 +369,11 @@ Future<void> init() async {
|
||||
// Tab 配置 Cubit (单例)
|
||||
sl.registerLazySingleton(() => TabConfigCubit(sl<SharedPreferences>()));
|
||||
|
||||
// 🔥 悬浮条设置服务 (单例)
|
||||
sl.registerLazySingleton(
|
||||
() => FloatBarSettingService(sl<SharedPreferences>()),
|
||||
);
|
||||
|
||||
sl.registerLazySingleton(() => GetDeviceLocationUseCase(sl()));
|
||||
sl.registerLazySingleton(
|
||||
() => DevicesCubit(
|
||||
@@ -399,7 +405,16 @@ Future<void> init() async {
|
||||
);
|
||||
|
||||
// 🔥 RemoteControlCubit 注入 DeviceStatusBloc(工厂模式,每次新建)
|
||||
sl.registerFactory(() => RemoteControlCubit(sl(), sl(), sl(), sl(), sl()));
|
||||
sl.registerFactory(
|
||||
() => RemoteControlCubit(
|
||||
sl(), // RemoteControlRepository
|
||||
sl(), // RequestControlPermissionUseCase
|
||||
sl(), // DeviceRepository
|
||||
sl<TcpClient>(), // TcpClient
|
||||
sl(), // NetMessageDispatcher
|
||||
sl(), // DeviceStatusBloc
|
||||
),
|
||||
);
|
||||
|
||||
// 🔥 PermissionRequestBloc 用于首页权限弹窗(单例,通过 NetMessageDispatcher 监听)
|
||||
sl.registerLazySingleton(
|
||||
|
||||
@@ -61,17 +61,17 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
);
|
||||
|
||||
if (user != null) {
|
||||
// 🔥 冷启动时重新初始化 TCP 连接
|
||||
await tcp.initializeTcp(
|
||||
host: TCPConsts.TCP_IP,
|
||||
port: TCPConsts.TCP_PORT,
|
||||
);
|
||||
// 🔥 冷启动时不再自动连接TCP,改为选择设备时再连接
|
||||
// await tcp.initializeTcp(
|
||||
// host: TCPConsts.TCP_IP,
|
||||
// port: TCPConsts.TCP_PORT,
|
||||
// );
|
||||
|
||||
// 2. 同步全局 App 状态
|
||||
appCubit.setAuth(user);
|
||||
// 3. 进入已登录状态
|
||||
emit(AuthAuthenticated(user));
|
||||
logger.logWithLevel('✅ [AUTH] 应用启动 - 已恢复登录状态并建立TCP连接', level: 'INFO');
|
||||
logger.logWithLevel('✅ [AUTH] 应用启动 - 已恢复登录状态(TCP将在选择设备时连接)', level: 'INFO');
|
||||
} else {
|
||||
logger.logWithLevel('⚠️ [AUTH] 应用启动 - 无本地缓存,进入未登录状态', level: 'INFO');
|
||||
emit(AuthUnauthenticated());
|
||||
@@ -86,8 +86,8 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
Future<void> loginSuccess(UserEntity user) async {
|
||||
await storage.saveUser(user);
|
||||
|
||||
// 🔥 使用封装的TCP初始化方法:连接 + 认证 + 心跳
|
||||
await tcp.initializeTcp(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
||||
// 🔥 登录后不再立即连接TCP,改为选择设备时再连接
|
||||
// await tcp.initializeTcp(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT);
|
||||
|
||||
appCubit.setAuth(user);
|
||||
emit(AuthAuthenticated(user));
|
||||
|
||||
@@ -8,6 +8,8 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
|
||||
import 'package:maibu_satabot_v2/core/network/tcp/tcp_client.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/repositories/device_repository.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
|
||||
@@ -27,6 +29,7 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
final RemoteControlRepository _repository;
|
||||
final RequestControlPermissionUseCase _requestControlPermissionUseCase;
|
||||
final DeviceRepository _deviceRepository; // 🔥 注入设备仓库
|
||||
final TcpClient tcpClient; // 🔥 注入TCP客户端
|
||||
Timer? _timer;
|
||||
StreamSubscription? _kickOutSub; // 新增:用于管理监听生命周期
|
||||
final NetMessageDispatcher dispatcher;
|
||||
@@ -56,6 +59,7 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
this._repository,
|
||||
this._requestControlPermissionUseCase,
|
||||
this._deviceRepository, // 🔥 注入
|
||||
this.tcpClient, // 🔥 注入TCP客户端
|
||||
this.dispatcher,
|
||||
this.deviceStatusBloc, // 🔥 注入
|
||||
) : super(
|
||||
@@ -937,25 +941,49 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
}
|
||||
|
||||
/// 🔥 设置待控制的设备(从机器人列表点击进入时调用)
|
||||
void setTargetDevice(DeviceEntity device) {
|
||||
// debugPrint('🎯 [RemoteControl] 设置待控制设备 ${device.deviceName}');
|
||||
// _logger.logWithLevel('🎯 [RemoteControl] 设置待控制设备 ${device.deviceName}');
|
||||
void setTargetDevice(DeviceEntity device) async {
|
||||
debugPrint('🎯 [RemoteControl] ========== 开始设置目标设备 ==========');
|
||||
debugPrint('🎯 [RemoteControl] 设备名称: ${device.deviceName}');
|
||||
debugPrint('🎯 [RemoteControl] 当前TCP状态: ${tcpClient.isConnected ? "已连接" : "未连接"}');
|
||||
|
||||
// 🔥 关键修改:选择设备时才连接TCP
|
||||
if (!tcpClient.isConnected) {
|
||||
debugPrint('🔌 [RemoteControl] TCP未连接,开始建立连接...');
|
||||
try {
|
||||
debugPrint('🔌 [RemoteControl] 调用 connectBySwitch,目标: ${device.deviceName}');
|
||||
await tcpClient.connectBySwitch(
|
||||
host: TCPConsts.TCP_IP,
|
||||
port: TCPConsts.TCP_PORT,
|
||||
deviceName: device.deviceName,
|
||||
);
|
||||
debugPrint('✅ [RemoteControl] TCP连接成功!');
|
||||
debugPrint('✅ [RemoteControl] 认证流程已完成(包含发送0x03认证包和设备订阅)');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [RemoteControl] TCP连接失败: $e');
|
||||
debugPrint('⚠️ [RemoteControl] 将继续执行,允许用户进入页面(但无实时数据)');
|
||||
// 即使TCP连接失败,也继续设置设备,允许用户进入页面
|
||||
}
|
||||
} else {
|
||||
debugPrint('✅ [RemoteControl] TCP已连接,跳过连接步骤');
|
||||
debugPrint('🔄 [RemoteControl] 将直接切换设备订阅');
|
||||
}
|
||||
|
||||
// 🔥 通知后端订阅该设备
|
||||
debugPrint('📡 [RemoteControl] 开始HTTP切换设备订阅...');
|
||||
_deviceRepository.switchDevice("app", device.deviceName).then((result) {
|
||||
result.fold(
|
||||
(failure) {
|
||||
// debugPrint('>>> [RemoteControl] 切换设备失败: ${failure.message}');
|
||||
// _logger.logWithLevel('>>> [RemoteControl] 切换设备失败: ${failure.message}');
|
||||
debugPrint('❌ [RemoteControl] HTTP切换设备失败: ${failure.message}');
|
||||
},
|
||||
(success) {
|
||||
// debugPrint('>>> [RemoteControl] 切换设备成功, code: $success');
|
||||
// _logger.logWithLevel('>>> [RemoteControl] 切换设备成功, code: $success');
|
||||
debugPrint('✅ [RemoteControl] HTTP切换设备成功, code: $success');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
debugPrint('📦 [RemoteControl] 更新targetDevice状态');
|
||||
emit(state.copyWith(targetDevice: device));
|
||||
debugPrint('🎯 [RemoteControl] ========== 目标设备设置完成 ==========');
|
||||
}
|
||||
|
||||
/// 🔥 清除待控制设备(退出远程控制页时调用)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
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,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
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)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
bool showFloatingMonitor = false;
|
||||
bool isFloatingIndoor = true;
|
||||
Offset floatingPosition = const Offset(20, 200);
|
||||
bool _isFloatingMonitorEnabled = true; // 悬浮窗默认开启
|
||||
|
||||
// 视频流状态
|
||||
VideoStreamEntity? _floatingVideoStream;
|
||||
@@ -73,6 +74,30 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
_startDroneStatusPolling();
|
||||
}
|
||||
|
||||
/// 检查并自动打开悬浮窗
|
||||
void _checkAndShowFloatingMonitor(UAVDetailEntity detail) {
|
||||
debugPrint('🔍 检查悬浮窗条件:');
|
||||
debugPrint(' - 开关状态: $_isFloatingMonitorEnabled');
|
||||
debugPrint(' - 是否已显示: $showFloatingMonitor');
|
||||
debugPrint(' - 无人机在线: ${detail.droneOnlineStatus}');
|
||||
debugPrint(' - 机场摄像头: ${detail.gatewayCameraList?.length ?? 0}');
|
||||
|
||||
if (!_isFloatingMonitorEnabled || showFloatingMonitor) {
|
||||
debugPrint('❌ 不满足条件,退出检查');
|
||||
return; // 开关关闭或已显示,不执行
|
||||
}
|
||||
|
||||
// 无人机在线且有机场摄像头,自动打开悬浮窗
|
||||
if (detail.droneOnlineStatus == 1 &&
|
||||
detail.gatewayCameraList != null &&
|
||||
detail.gatewayCameraList!.isNotEmpty) {
|
||||
debugPrint('✅ 检测到无人机在线,自动打开悬浮窗');
|
||||
_loadFloatingVideoStream();
|
||||
} else {
|
||||
debugPrint('❌ 无人机离线或无摄像头');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_bloc.close();
|
||||
@@ -98,6 +123,17 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
deviceSn: widget.station.deviceSn,
|
||||
),
|
||||
);
|
||||
|
||||
// 如果悬浮窗开启且无人机上线,自动显示悬浮窗
|
||||
if (_isFloatingMonitorEnabled) {
|
||||
final currentState = _bloc.state;
|
||||
if (currentState is UAVDetailLoaded &&
|
||||
currentState.detail.droneOnlineStatus == 1 &&
|
||||
!showFloatingMonitor) {
|
||||
debugPrint('✅ 无人机已上线,自动打开悬浮窗');
|
||||
_loadFloatingVideoStream();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -219,6 +255,8 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
}
|
||||
|
||||
if (state is UAVDetailLoaded) {
|
||||
// 检查是否自动打开悬浮窗
|
||||
_checkAndShowFloatingMonitor(state.detail);
|
||||
return _buildContent(state.detail);
|
||||
}
|
||||
|
||||
@@ -836,7 +874,10 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
_destroyFloatingRtcEngine();
|
||||
setState(() => showFloatingMonitor = false);
|
||||
setState(() {
|
||||
showFloatingMonitor = false;
|
||||
_isFloatingMonitorEnabled = false; // 关闭开关
|
||||
});
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
@@ -965,6 +1006,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
_isFloatingLoading = true;
|
||||
_floatingErrorMessage = null;
|
||||
showFloatingMonitor = true;
|
||||
_isFloatingMonitorEnabled = true; // 开启开关
|
||||
});
|
||||
|
||||
_destroyFloatingRtcEngine();
|
||||
|
||||
@@ -8,6 +8,9 @@ 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 {
|
||||
@@ -18,6 +21,17 @@ 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(
|
||||
@@ -45,6 +59,9 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
// Tab 设置
|
||||
_buildTabSettingsSection(),
|
||||
const SizedBox(height: 12),
|
||||
// 悬浮条设置
|
||||
_buildFloatBarSection(),
|
||||
const SizedBox(height: 12),
|
||||
// 语言设置
|
||||
_buildLanguageSection(),
|
||||
const SizedBox(height: 12),
|
||||
@@ -164,14 +181,18 @@ 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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -182,16 +203,25 @@ 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,
|
||||
),
|
||||
),
|
||||
@@ -202,7 +232,9 @@ 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,
|
||||
),
|
||||
),
|
||||
@@ -252,6 +284,94 @@ 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(
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_patcher/flutter_patcher.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
// 悬浮条相关
|
||||
import 'features/v2/device_list/presentation/float_bar/float_bar_controller.dart';
|
||||
import 'features/v2/device_list/presentation/float_bar/simple_float_bar.dart';
|
||||
|
||||
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/core/infrastructure/logging/app_bloc_observer.dart';
|
||||
import 'package:maibu_satabot_v2/core/logging/i_logger_service.dart';
|
||||
@@ -19,8 +23,13 @@ import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/main_container/presentation/main_wrapper.dart';
|
||||
// 🔥 导入悬浮条管理器和设置服务
|
||||
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/manager/float_bar_manager.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/view/float_bar_widget.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/cubit/float_bar_setting_cubit.dart';
|
||||
|
||||
import 'core/di/injection.dart';
|
||||
import 'core/router/route_paths.dart';
|
||||
import 'core/localization/app_localizations.dart';
|
||||
import 'core/localization/locale_cubit.dart';
|
||||
import 'features/auth/presentation/bloc/auth_state.dart';
|
||||
@@ -28,6 +37,9 @@ import 'features/auth/presentation/bloc/login_cubit.dart';
|
||||
import 'features/devices/presentation/bloc/device_status_bloc.dart';
|
||||
import 'features/home/presentation/bloc/permission_request_bloc.dart';
|
||||
|
||||
// 🔥 全局 GetIt 实例
|
||||
final sl = GetIt.instance;
|
||||
|
||||
// 🔥 定义全局 Navigator Key
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
@@ -38,10 +50,17 @@ void main() async {
|
||||
|
||||
await FlutterPatcher.init();
|
||||
await init();
|
||||
|
||||
final logger = sl<ILoggerService>();
|
||||
await logger.init();
|
||||
Bloc.observer = AppBlocObserver(logger);
|
||||
|
||||
// 🔥 确保 FloatBarSettingService 被初始化,这样 settingNotifier 才不会是 null
|
||||
sl<FloatBarSettingService>();
|
||||
debugPrint(
|
||||
'🔍 [FloatBar] FloatBarSettingService 已初始化,settingNotifier: ${FloatBarSettingService.settingNotifier}',
|
||||
);
|
||||
|
||||
// ⚠️ 注意:不要在启动时清除补丁版本记录!
|
||||
// 补丁版本记录只在整包更新成功后才清除
|
||||
// 如果在启动时清除,会导致差量更新后划掉App再进入时循环更新
|
||||
@@ -102,7 +121,23 @@ class MyApp extends StatelessWidget {
|
||||
theme: AppTheme.lightTheme,
|
||||
routerConfig: sl<GoRouter>(),
|
||||
builder: (context, child) {
|
||||
return _LifecycleListener(child: _UpdateChecker(child: child!));
|
||||
// 🔥 初始化悬浮条管理器并显示悬浮条
|
||||
debugPrint(
|
||||
'======== [FloatBar] MaterialApp.builder START ========',
|
||||
);
|
||||
debugPrint('🔍 [FloatBar] child: $child');
|
||||
debugPrint(
|
||||
'🔍 [FloatBar] settingNotifier: ${FloatBarSettingService.settingNotifier}',
|
||||
);
|
||||
debugPrint(
|
||||
'🔍 [FloatBar] isEnabled: ${FloatBarSettingService.isEnabled}',
|
||||
);
|
||||
debugPrint(
|
||||
'======== [FloatBar] MaterialApp.builder END ========',
|
||||
);
|
||||
return _FloatBarInitializer(
|
||||
child: _LifecycleListener(child: _UpdateChecker(child: child!)),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -111,6 +146,35 @@ class MyApp extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 悬浮条初始化组件 - 极简版
|
||||
class _FloatBarInitializer extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const _FloatBarInitializer({required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: FloatBarController.visibilityNotifier,
|
||||
builder: (context, isVisible, child) {
|
||||
return Stack(
|
||||
children: [
|
||||
child!,
|
||||
if (isVisible)
|
||||
const Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: SimpleFloatBar(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LifecycleListener extends StatefulWidget {
|
||||
final Widget? child;
|
||||
const _LifecycleListener({required this.child});
|
||||
@@ -247,6 +311,7 @@ class _UpdateCheckerState extends State<_UpdateChecker> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 💡 选择下载方式
|
||||
const Text(
|
||||
'💡 选择下载方式',
|
||||
style: TextStyle(
|
||||
|
||||
Reference in New Issue
Block a user