构建初步的设备运行悬浮状态栏,已实现初步的的悬浮和开关功能。

优化了登录链条适配多种用户的登录操作
This commit is contained in:
2026-06-12 15:00:44 +08:00
parent b08e1e0b57
commit 52fe17c0d5
14 changed files with 1238 additions and 28 deletions

View File

@@ -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(

View File

@@ -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));

View File

@@ -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] ========== 目标设备设置完成 ==========');
}
/// 🔥 清除待控制设备(退出远程控制页时调用)

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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,
];
}

View File

@@ -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();
}
}

View File

@@ -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)),
]);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();

View File

@@ -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(

View File

@@ -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(