Files
feature-next-arch/lib/features/remote_control/presentation/widgets/top_status_bar.dart
Songzex 7c01229b9e 1:修复远程遥控控制状态显示不正确的问题
2:更换端口映射为测试服的8081-59003,9001-59004
2026-08-26 14:23:20 +08:00

507 lines
19 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:go_router/go_router.dart';
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/control_mode.dart';
import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/remote_control_state.dart';
import 'package:maibu_satabot_v2/features/remote_control/presentation/widgets/status_chip.dart';
import 'package:cc_ui_kit/cc_ui_kit.dart';
import '../../../../core/di/injection.dart';
import '../../../../core/logging/i_logger_service.dart';
import '../../../devices/domain/entities/device_entity.dart';
import '../../../devices/presentation/bloc/devices_cubit.dart';
import '../../data/models/running_status_model.dart';
import '../bloc/remote_control_cubit.dart';
class TopStatusBar extends StatelessWidget {
/// 🔥 从父组件传入状态,不依赖 context.watch
final RemoteControlState remoteState;
const TopStatusBar({super.key, required this.remoteState});
@override
Widget build(BuildContext context) {
// 🔥 添加调试日志:确认接收到的状态值
//debugPrint('🔍 [TopStatusBar] build - isLocked=${remoteState.isLocked}');
// 监听全局设备状态 (DevicesCubit 需要 watch,因为设备选择可能变化)
// 🔥 使用 try-catch 保护,防止 BlocProvider 缺失导致整个 widget 树崩溃(白屏)
DeviceEntity? device;
try {
final deviceState = context.watch<DevicesCubit>().state;
device = deviceState.selectedDevice;
} catch (e) {
debugPrint('⚠️ [TopStatusBar] 读取 DevicesCubit 失败: $e');
// DevicesCubit 不可用时使用 targetDevice 作为降级方案
try {
device = context.read<RemoteControlCubit>().state.targetDevice;
} catch (e2) {
debugPrint('⚠️ [TopStatusBar] 读取 targetDevice 也失败: $e2');
}
}
var _remoteControlCubit = context.read<RemoteControlCubit>();
ControlMode _parseControlMode(String modeString) {
switch (modeString.toUpperCase()) {
case 'BLE':
return ControlMode.BLE;
case 'LOCAL':
return ControlMode.LOCAL;
case 'TCP':
return ControlMode.TCP;
case 'NONE':
return ControlMode.NONE;
case 'OTHER':
return ControlMode.OTHER;
default:
return ControlMode.NONE;
}
}
return Padding(
padding: const EdgeInsets.fromLTRB(1, 12, 1, 12),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: constraints.maxWidth),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 1. 返回按钮 (对应 SimpleSmallFunctionButton)
_buildIconButton("assets/svgs/remote_back.svg", () {
// 🔥 兼容两种导航栈:GoRouter 和 Navigator
if (Navigator.of(context).canPop()) {
Navigator.of(context).pop();
} else {
context.pop();
}
}),
const SizedBox(width: 8),
// 2. 控制状态 (对应 StatusChipLeft)
StatusChip(
text: remoteState.hasPermission
? AppLocalizations.of(
context,
).translate('remote_control.controlling')
: AppLocalizations.of(
context,
).translate('remote_control.not_controlling'),
color: remoteState.hasPermission
? const Color(0xFF1DB954)
: Colors.red,
icon: Icons.eighteen_mp,
breathing: !remoteState.hasPermission,
onTap: () {
if (!remoteState.hasPermission) {
// 🔥 点击后发起抢占:查权限 + 无权限则发 TCP switch_control
debugPrint('🔑 [TopStatusBar] 👆 用户手动点击“未在控制”,请求抢占控制权');
final targetDevice =
_remoteControlCubit.state.targetDevice;
if (targetDevice != null) {
_remoteControlCubit.requestControlPermissionS(
targetDevice.deviceName,
'app',
source: '手动点击',
grabControl: true,
);
} else {
debugPrint('❌ [TopStatusBar] targetDevice 为空,无法请求权限');
}
}
},
),
const SizedBox(width: 8),
// 3. 锁定状态 (对应 SmallFunctionButton)
_buildIconButton(
remoteState.isLocked
? "assets/svgs/remote_lock.svg"
: "assets/svgs/remote_lock_open.svg",
() => context.read<RemoteControlCubit>().toggleLock(),
isSelected: remoteState.isLocked,
),
const SizedBox(width: 8),
// 4. 刷新按钮
_buildIconButton("assets/svgs/remote_refresh.svg", () {}),
// const SizedBox(width: 8),
// 5. 火技能按钮 (对应 SmallFunctionButton)
//_buildIconButton("assets/svgs/fire.svg", () {}),
_buildSliderBox(
AppLocalizations.of(
context,
).translate('remote_control.fire_skill'),
false,
context,
),
// const Spacer(), // 移除 Spacer,改用 MainAxisAlignment.spaceBetween
_buildExpandIconButton(
iconPath: "assets/svgs/remote_expand.svg",
selectedIconPath: "assets/svgs/remote_unexpand.svg",
isSelected: remoteState.topRightIsExpanded,
onTap: () {
_remoteControlCubit.toggleTopLeftExpand();
},
),
AnimatedSize(
duration: const Duration(milliseconds: 100),
curve: Curves.easeInOut,
child: Row(
children: !remoteState.topRightIsExpanded
? [
const SizedBox(width: 8),
_buildSwitchIconButton(
iconPath:
"assets/svgs/remote_recognition_off.svg",
selectedIconPath:
"assets/svgs/remote_recognition_on.svg",
isSelected: remoteState.obstacleRecognitionFlag,
onTap: () {
_remoteControlCubit
.toggleObstacleRecognition();
},
),
const SizedBox(width: 8),
_buildSwitchIconButton(
iconPath:
"assets/svgs/remote_video_left_off.svg",
selectedIconPath:
"assets/svgs/remote_video_left.svg",
isSelected: remoteState.showLeftPip,
onTap: () {
_remoteControlCubit.toggleLeftPip();
},
),
const SizedBox(width: 8),
_buildSwitchIconButton(
iconPath:
"assets/svgs/remote_video_right_off.svg",
selectedIconPath:
"assets/svgs/remote_video_right.svg",
isSelected: remoteState.showRightPip,
onTap: () {
_remoteControlCubit.toggleRightPip();
},
),
const SizedBox(width: 8),
_buildControlModeChip(
remoteState.runningStatusModel.controlMode,
context,
),
const SizedBox(width: 8),
_buildVoltageChip(
remoteState.runningStatusModel.voltage,
),
const SizedBox(width: 8),
_buildPingChip(remoteState.ping),
const SizedBox(width: 8),
]
: [], // 折叠时数组为空
),
),
// TODO
// _buildControlModeChip( _parseControlMode(remoteState.runningStatusModel.controlMode)),
// const SizedBox(width: 8),
//_buildVoltageChip(remoteState.voltage),
// const SizedBox(width: 8),
// 5. 信号延迟 (对应 pingStatusChip)
//_buildPingChip(remoteState.ping),
// const SizedBox(width: 8),
// 6. 电量 (对应 StatusChipRight)
_buildBatteryChip(remoteState.battery),
const SizedBox(width: 8),
],
),
),
);
},
),
);
}
Widget _buildIconButton(
String svgAsset, // 改为 String 路径,例如 'assets/icons/stop.svg'
VoidCallback onTap, {
bool isSelected = false,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: isSelected
? const Color(0xCC0078D4)
: Colors.grey.withOpacity(0.4),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white.withOpacity(0.3), width: 0.5),
),
padding: const EdgeInsets.all(7),
child: SvgPicture.asset(
svgAsset,
// 这里可以使用 colorFilter 来改变 SVG 的颜色(如果需要根据选中状态变色)
colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
),
);
}
Widget _buildSwitchIconButton({
required String iconPath, // 默认图标路径
required String selectedIconPath, // 选中后的图标路径
required VoidCallback onTap, // 点击事件
bool isSelected = false, // 是否选中状态
double size = 32, // 按钮尺寸
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
// 增加颜色切换动画
width: size,
height: size,
decoration: BoxDecoration(
// 选中时为深蓝色,未选中时为半透明灰色
color: isSelected
? const Color(0xFF0078D4)
: Colors.grey.withOpacity(0.4),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isSelected
? Colors.transparent
: Colors.white.withOpacity(0.3),
width: 0.5,
),
boxShadow: isSelected
? [
BoxShadow(
color: const Color(0xFF0078D4).withOpacity(0.4),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: null,
),
padding: const EdgeInsets.all(7),
child: SvgPicture.asset(
// 根据状态切换图片
isSelected ? selectedIconPath : iconPath,
colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
),
);
}
Widget _buildPingChip(int ping) {
Color color = ping < 100
? Colors.green
: (ping < 200 ? Colors.orange : Colors.red);
return StatusChip(
text: "$ping ms",
color: color.withValues(alpha: 0.6),
svgColor: color,
icon: Icons.network_check,
);
}
Widget _buildControlModeChip(String mode, BuildContext context) {
String displayText;
if (mode == '') {
displayText = AppLocalizations.of(
context,
).translate('remote_control.mode_none');
} else {
displayText = mode;
}
// switch (mode) {
// case "蓝牙模式":
// displayText = '蓝牙模式';
// break;
// case "本地模式":
// displayText = '本地模式';
// break;
// case "TCP模式":
// displayText = 'TCP模式';
// break;
// case "":
// displayText = '无模式';
// break;
// case "其他模式":
// displayText = '其他模式';
// break;
// default:
// displayText = '未知模式';
// }
return StatusChip(
text: displayText,
color: Colors.blue.withValues(alpha: 0.6),
svgColor: Colors.blue,
svgPath: "assets/svgs/remote_net.svg",
);
}
Widget _buildExpandIconButton({
required String iconPath, // 默认图标路径
required String selectedIconPath, // 选中后的图标路径
required VoidCallback onTap, // 点击事件
bool isSelected = false, // 是否选中状态
double size = 28, // 按钮尺寸
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
// 增加颜色切换动画
width: size,
height: size,
decoration: BoxDecoration(
// 选中时为深蓝色,未选中时为半透明灰色
color: Colors.grey.withOpacity(0.4),
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.white.withOpacity(0.3), width: 0.5),
boxShadow: null,
),
padding: const EdgeInsets.all(7),
child: SvgPicture.asset(
// 根据状态切换图片
isSelected ? selectedIconPath : iconPath,
colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
),
),
);
}
Widget _buildVoltageChip(String voltage) {
return StatusChip(
text: "$voltage V",
color: Color(0xFFFACC15).withValues(alpha: 0.6),
svgColor: Color(0xFFFACC15),
svgPath: "assets/svgs/remote_zap.svg",
);
}
Widget _buildBatteryChip(int level) {
// 1. 根据数值判断图片路径 (逻辑与之前一致)
String svgPath;
Color color;
if (level >= 90) {
svgPath = "assets/svgs/remote_battery_full.svg";
color = Colors.green;
} else if (level >= 50) {
svgPath = "assets/svgs/remote_battery_middle.svg";
color = Colors.greenAccent;
} else if (level >= 20) {
svgPath = "assets/svgs/remote_battery_low.svg";
color = Colors.orange;
} else if (level >= 10) {
svgPath = "assets/svgs/remote_battery_warning.svg";
color = Colors.redAccent;
} else {
svgPath = "assets/svgs/remote_battery_none.svg";
color = Colors.red;
}
// 2. 使用 StatusChip 包装,但 icon 传入 SvgPicture
return StatusChip(
text: "$level%",
color: color.withValues(alpha: 0.5),
svgColor: color,
// 如果你的 StatusChip 已经改为接受 Widget,则直接传 SvgPicture
// 如果 StatusChip 目前只接受 IconData,你需要按我之前的建议给它加个 leading 参数
svgPath: svgPath,
);
}
Widget _buildSliderBox(String label, bool isLeft, BuildContext context) {
// 1. 核心尺寸调整:足够宽的容器解决拥挤,高度匹配状态栏
const double boxWidth = 66; // 水平宽度放大,容纳左右图标
const double boxHeight = 32; // 高度和其他按钮保持一致
const double iconSize = 20; // 图标尺寸放大,避免过小拥挤
return SizedBox(
width: boxWidth,
height: boxHeight,
child: Center(
// 2. 关键:旋转组件将竖向CCExpandSlider转为水平(无direction参数的替代方案)
child: RotatedBox(
quarterTurns: 1, // 旋转90度(竖向→横向),若方向反了可改为3
child: SizedBox(
// 旋转后宽高互换,这里给CCExpandSlider足够的显示空间
width: boxHeight, // 旋转后对应原高度
height: boxWidth, // 旋转后对应原宽度(足够长,不拥挤)
child: CCExpandSlider(
label: " ",
// 3. 图标适配旋转后的方向(仍用上下箭头,旋转后变为左右)因为外边旋转了 90度 参数传递也要转换
svgStart:
"assets/svgs/remote_flame_plus.svg", // 旋转后→左箭头assets/svgs/remote_flame_minus.svg
svgCenter: "assets/svgs/remote_flame.svg",
svgEnd:
"assets/svgs/remote_flame_minus.svg", // 旋转后→右箭头assets/svgs/remote_flame_plus.svg
// 4. 放大图标尺寸,解决拥挤
iconSize: iconSize,
// 保留原有业务逻辑
onStart: () => _handleSliderAction(
label,
"end",
context,
), //_handleSliderAction(label, "start", context)
onCenter: () => _handleSliderAction(label, "center", context),
onEnd: () => _handleSliderAction(
label,
"start",
context,
), //_handleSliderAction(label, "end", context)
),
),
),
),
);
}
// 🔥 新增:统一的业务逻辑分发方法
void _handleSliderAction(String label, String action, BuildContext context) {
debugPrint('🎯 [Slider 业务] label: $label, action: $action');
// 🔥 根据 label 区分不同的设备
_handleChassisAction(action, context);
}
// 🔥 点火的业务逻辑(左滑块)
void _handleChassisAction(String action, BuildContext context) {
final cubit = context.read<RemoteControlCubit>();
switch (action) {
case "start":
debugPrint('2熄火');
// TODO: 发送 🔥点火指令
cubit.sendFireCommand(2);
break;
case "center":
debugPrint('️0熄火');
// TODO: 发送点火停止指令
cubit.sendFireCommand(0);
break;
case "end":
debugPrint('1点火)');
// TODO: 发送熄火后退指令
cubit.sendFireCommand(1);
break;
}
}
}