507 lines
19 KiB
Dart
507 lines
19 KiB
Dart
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;
|
||
}
|
||
}
|
||
}
|