机器状态数据和地图切换,系统配置页面
This commit is contained in:
133
lib/components/route_type_picker.dart
Normal file
133
lib/components/route_type_picker.dart
Normal file
@@ -0,0 +1,133 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/enum.dart';
|
||||
|
||||
/// 保存打点前的「规划类型」选择框:对应后端 workRecord.routeType(必填)。
|
||||
///
|
||||
/// 后端该字段缺失时保存接口直接报「未选择规划类型」。新版「地图与区域编辑」
|
||||
/// 与旧版路径规划共用 /iot/workRecord/add,所以两边共用这一个弹窗,
|
||||
/// 保证取值与交互一致。返回 null 表示用户取消。
|
||||
///
|
||||
/// [maxWidth]:横屏页传较小值(如 240),避免默认 AlertDialog 撑满整屏;
|
||||
/// [insetPadding]:横屏页收窄两侧留白;
|
||||
/// [initial]:默认选中项,默认「割草任务」。
|
||||
Future<RouteType?> showRouteTypePicker(
|
||||
BuildContext context, {
|
||||
String title = '选择规划类型',
|
||||
RouteType? initial,
|
||||
double? maxWidth,
|
||||
EdgeInsets? insetPadding,
|
||||
}) {
|
||||
return showDialog<RouteType>(
|
||||
context: context,
|
||||
builder: (dialogContext) => _RouteTypeDialog(
|
||||
title: title,
|
||||
initial: initial ?? RouteType.mowingTask,
|
||||
maxWidth: maxWidth,
|
||||
insetPadding: insetPadding,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _RouteTypeDialog extends StatefulWidget {
|
||||
const _RouteTypeDialog({
|
||||
required this.title,
|
||||
required this.initial,
|
||||
this.maxWidth,
|
||||
this.insetPadding,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final RouteType initial;
|
||||
final double? maxWidth;
|
||||
final EdgeInsets? insetPadding;
|
||||
|
||||
@override
|
||||
State<_RouteTypeDialog> createState() => _RouteTypeDialogState();
|
||||
}
|
||||
|
||||
class _RouteTypeDialogState extends State<_RouteTypeDialog> {
|
||||
late RouteType _picked = widget.initial;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color accent = Theme.of(context).colorScheme.primary;
|
||||
return AlertDialog(
|
||||
insetPadding: widget.insetPadding ??
|
||||
const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0),
|
||||
title: Text(widget.title, style: const TextStyle(fontSize: 18)),
|
||||
// 横屏可视高度只有 300dp 上下(刘海机型更少):标题/内容/按钮的
|
||||
// 上下留白全部压紧,再加下面的可滚动兼底,避免 RenderFlex 溢出
|
||||
titlePadding: const EdgeInsets.fromLTRB(24, 14, 24, 4),
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 4, 24, 0),
|
||||
actionsPadding: const EdgeInsets.fromLTRB(24, 4, 24, 10),
|
||||
content: SizedBox(
|
||||
width: widget.maxWidth,
|
||||
// 兼容极矮屏幕(横屏 + 挖孔 + 系统栏):内容超高时滚动,不再顶破弹窗
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final RouteType type in RouteType.values)
|
||||
_buildOption(type, accent),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(_picked),
|
||||
child: Text(
|
||||
'确定',
|
||||
style: TextStyle(
|
||||
color: accent,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOption(RouteType type, Color accent) {
|
||||
final bool selected = type == _picked;
|
||||
return InkWell(
|
||||
onTap: () => setState(() => _picked = type),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
// 自绘单选圈:不用 RadioListTile,避开其 groupValue/onChanged 已废弃的告警
|
||||
Container(
|
||||
width: 18,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: selected ? accent : Colors.grey.shade400,
|
||||
width: selected ? 6 : 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
type.label,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
color: selected ? accent : null,
|
||||
fontWeight: selected ? FontWeight.w600 : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -549,8 +549,32 @@ class TcpClient {
|
||||
_socket!.add(data);
|
||||
}
|
||||
|
||||
void sendRaw(int cmd, List<int> payload) {
|
||||
if (_socket == null) return;
|
||||
/// 🔥 断线丢帧日志节流时间戳:控制循环每 200ms 就会撞一次「未连接」,
|
||||
/// 全量打印会淹掉日志文件,故 5 秒内只记一次(丢帧本身仍如实返回 false)。
|
||||
DateTime? _lastSendDropLogAt;
|
||||
|
||||
/// 把一帧协议数据写入 socket。
|
||||
///
|
||||
/// 返回 `false` = 这一帧**根本没交给系统 socket**(未连接,或写入抛异常)。
|
||||
/// 🔥 原来的 void 版本在 `_socket == null` 时静默 return,调用方完全无从知晓:
|
||||
/// `stopAllMovement()` 连发的 30 帧停止指令会在断线窗口里一帧都不出网,
|
||||
/// 而代码以为「已发送」——设备只保留了断开前最后一帧运动指令,正是
|
||||
/// 「松手了车还在走」的一条隐藏通路。现在必须由调用方判断结果自行兜底。
|
||||
bool sendRaw(int cmd, List<int> payload) {
|
||||
if (_socket == null) {
|
||||
final now = DateTime.now();
|
||||
if (_lastSendDropLogAt == null ||
|
||||
now.difference(_lastSendDropLogAt!) >= const Duration(seconds: 5)) {
|
||||
_lastSendDropLogAt = now;
|
||||
_logger.logWithLevel(
|
||||
'🔗 [联调-TCP] sendRaw ❌未连接(_socket=null),本帧丢弃 '
|
||||
'cmd=0x${cmd.toRadixString(16).padLeft(2, '0')}(同类丢弃 5s 内不再重复打印)',
|
||||
level: 'ERROR',
|
||||
shouldLog: true,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
final builder = BytesBuilder()
|
||||
..addByte(0xAB)
|
||||
@@ -573,12 +597,14 @@ class TcpClient {
|
||||
'len=${packet.length} frameHex=${_hex(packet)}',
|
||||
shouldLog: true,
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
_logger.logWithLevel(
|
||||
'🔗 [联调-TCP] sendRaw ❌写入socket失败 cmd=0x${cmd.toRadixString(16).padLeft(2, '0')} err=$e',
|
||||
level: 'ERROR',
|
||||
shouldLog: true,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
/// 一键返航方案的领域模型:返航点 + 线上禁行区 + 场站上下文。
|
||||
/// 只描述业务数据,后端字段名(size_x / source / tempObstaclePoints 等)
|
||||
/// 全部留给 data 层网关映射,页面与领域层不感知。
|
||||
|
||||
/// 单个返航点:坐标 + 绑定的设备 SN(MC700-…)+ 名称
|
||||
class ReturnRestPoint {
|
||||
const ReturnRestPoint({
|
||||
required this.position,
|
||||
required this.alias,
|
||||
this.bindDeviceId,
|
||||
});
|
||||
|
||||
final LatLng position;
|
||||
final String alias;
|
||||
final String? bindDeviceId;
|
||||
}
|
||||
|
||||
/// 单个障碍(禁行区):中心 + 尺寸(米) + 形状 + 名称 + 来源
|
||||
class ReturnObstaclePoint {
|
||||
const ReturnObstaclePoint({
|
||||
required this.center,
|
||||
required this.alias,
|
||||
this.shapeType = 'rect',
|
||||
this.sizeX = 0,
|
||||
this.sizeY = 0,
|
||||
this.source = 'platform',
|
||||
});
|
||||
|
||||
final LatLng center;
|
||||
final String alias;
|
||||
final String shapeType; // rect / circle
|
||||
final double sizeX; // 宽(米,对应经度方向)
|
||||
final double sizeY; // 高(米,对应纬度方向)
|
||||
final String source; // 默认 platform
|
||||
}
|
||||
|
||||
/// 一次返航方案提交(对应 /system/siteMapInfo/save)
|
||||
class ReturnPlanDraft {
|
||||
const ReturnPlanDraft({
|
||||
required this.siteId,
|
||||
required this.orgId,
|
||||
this.restPoints = const <ReturnRestPoint>[],
|
||||
this.obstacles = const <ReturnObstaclePoint>[],
|
||||
this.id,
|
||||
});
|
||||
|
||||
/// siteMapInfo 主键:更新已有方案时传,新建传 null(省略由后端插入)
|
||||
final int? id;
|
||||
final int siteId;
|
||||
final int orgId;
|
||||
final List<ReturnRestPoint> restPoints;
|
||||
final List<ReturnObstaclePoint> obstacles;
|
||||
|
||||
bool get isEmpty => restPoints.isEmpty && obstacles.isEmpty;
|
||||
}
|
||||
@@ -37,7 +37,10 @@ class RunningStatusEntity extends Equatable {
|
||||
// === 扩展信息 (字段 18-23) ===
|
||||
final String timestamp; // [18] 时间戳 (格式:20-1-11-31 08:00:00)
|
||||
final String knifeCuttingSpeed; // [19] 割刀速度
|
||||
final String controlMode; // [20] 控制模式
|
||||
/// [20] 控制模式。**空串 = 后端从未给过**(UI 显示「未知」);
|
||||
/// 协议值 0=未知 1=本地 2=自主任务 3=远程遥控。
|
||||
/// 以前默认写死 '0',把「没数据」也当成一个有效档位,UI 无从分辨。
|
||||
final String controlMode;
|
||||
final String battery; // [21] 电量百分比
|
||||
final String workingArea; // [22] 工作面积
|
||||
final String obstacleFlag; // [23] 障碍物标志
|
||||
@@ -69,7 +72,7 @@ class RunningStatusEntity extends Equatable {
|
||||
this.longitude = 0.0,
|
||||
this.timestamp = '',
|
||||
this.knifeCuttingSpeed = '0',
|
||||
this.controlMode = '0',
|
||||
this.controlMode = '',
|
||||
this.battery = '0',
|
||||
this.workingArea = '0',
|
||||
this.obstacleFlag = '0',
|
||||
@@ -143,7 +146,8 @@ class RunningStatusEntity extends Equatable {
|
||||
// 扩展信息 (保持原始字符串,由调用方决定如何解析)
|
||||
timestamp: fields[18].trim(),
|
||||
knifeCuttingSpeed: fields[19].trim().isNotEmpty ? fields[19].trim() : '0',
|
||||
controlMode: fields[20].trim().isNotEmpty ? fields[20].trim() : '0',
|
||||
// 空串原样保留:表示后端这一帧没带该字段,UI 据此显示「未知」
|
||||
controlMode: fields[20].trim(),
|
||||
battery: fields[21].trim().isNotEmpty ? fields[21].trim() : '0',
|
||||
workingArea: fields[22].trim().isNotEmpty ? fields[22].trim() : '0',
|
||||
obstacleFlag: fields[23].trim().isNotEmpty ? fields[23].trim() : '0',
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../../../core/domain/usecases/base_usecase.dart';
|
||||
import '../../../../core/error/failure.dart';
|
||||
import '../entities/device_schedule_plan_entity.dart';
|
||||
import '../repositories/device_task_repository.dart';
|
||||
|
||||
/// 创建设备定时任务(定时作业计划,按天/周/月循环触发)。
|
||||
///
|
||||
/// 提交前先执行实体的本地校验(与后端入参规则逐条对齐,
|
||||
/// 含「生效区间内必须存在可执行日期」的业务强校验),
|
||||
/// 本地能拦的直接 Left,不再浪费一次失败往返。
|
||||
class SaveDeviceSchedulePlanUseCase
|
||||
implements BaseUseCase<bool, SaveDeviceSchedulePlanParams> {
|
||||
final DeviceTaskRepository repository;
|
||||
|
||||
SaveDeviceSchedulePlanUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, bool>> call(
|
||||
SaveDeviceSchedulePlanParams params,
|
||||
) async {
|
||||
final error = params.plan.getValidationError();
|
||||
if (error != null) return Left(Failure(error));
|
||||
return await repository.saveDeviceSchedulePlan(plan: params.plan);
|
||||
}
|
||||
}
|
||||
|
||||
class SaveDeviceSchedulePlanParams {
|
||||
final DeviceSchedulePlanEntity plan;
|
||||
|
||||
SaveDeviceSchedulePlanParams({required this.plan});
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/network/net_message_dispatcher.dart';
|
||||
import '../../../../core/network/protocol_decoder.dart';
|
||||
import '../../../../core/network/tcp/tcp_client.dart';
|
||||
import '../../../../components/control_mode_text.dart';
|
||||
import '../../../auth/presentation/bloc/auth_cubit.dart';
|
||||
import '../../../devices/presentation/bloc/devices_cubit.dart';
|
||||
import '../../../devices/presentation/bloc/device_status_bloc.dart';
|
||||
@@ -800,7 +801,7 @@ class _RunningStatusPageState extends State<RunningStatusPage> with WidgetsBindi
|
||||
TableRow(children: [_buildTableCell(AppLocalizations.of(context).translate('running_status.battery') + "(%)"), _buildTableCell(status.battery)]),
|
||||
TableRow(children: [_buildTableCell(AppLocalizations.of(context).translate('running_status.chip_temp') + "(°C)"), _buildTableCell(status.chipTemp.toStringAsFixed(2))]),
|
||||
TableRow(children: [_buildTableCell(AppLocalizations.of(context).translate('running_status.knife_speed') + "(rpm)"), _buildTableCell(status.knifeCuttingSpeed)]),
|
||||
TableRow(children: [_buildTableCell(AppLocalizations.of(context).translate('running_status.control_mode')), _buildTableCell(AppLocalizations.of(context).translate(_getControlModeKey(int.parse(status.controlMode))))]),
|
||||
TableRow(children: [_buildTableCell(AppLocalizations.of(context).translate('running_status.control_mode')), _buildTableCell(controlModeDisplayText(AppLocalizations.of(context), status.controlMode))]),
|
||||
TableRow(children: [_buildTableCell(AppLocalizations.of(context).translate('running_status.longitude') + "(°)"), _buildTableCell(status.longitude.toStringAsFixed(6))]),
|
||||
TableRow(children: [_buildTableCell(AppLocalizations.of(context).translate('running_status.latitude') + "(°)"), _buildTableCell(status.latitude.toStringAsFixed(6))]),
|
||||
],
|
||||
@@ -867,15 +868,9 @@ class _RunningStatusPageState extends State<RunningStatusPage> with WidgetsBindi
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助方法:根据控制模式值返回翻译键
|
||||
String _getControlModeKey(int modeValue) {
|
||||
switch (modeValue) {
|
||||
case 3:
|
||||
return 'running_status.remote_control';
|
||||
default:
|
||||
return 'running_status.local_control';
|
||||
}
|
||||
}
|
||||
// 控制模式文案已下沉到 lib/components/control_mode_text.dart(与设备数据监控共用)。
|
||||
// 原来此处是 int.parse(status.controlMode) + default 一律当「本地控制」:
|
||||
// 后端一发非数字直接抛 FormatException,发空值(没给)也会显示成「本地控制」。
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -47,11 +47,13 @@ import 'package:maibu_satabot_v2/features/home/presentation/widgets/BottomDirect
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/commonFn.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/enum.dart';
|
||||
import 'package:maibu_satabot_v2/components/input_confirm_dialog.dart';
|
||||
import 'package:maibu_satabot_v2/components/route_type_picker.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/tracepoint.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/CenterLocation.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/HeadingPointer.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/dji_tile_layer.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/geoserver_suzhou_layer.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/app_base_map_layer.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/obsToast.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/startpoint_area.dart';
|
||||
import 'package:image/image.dart' as _image; // 注意命名空间冲突,使用 as img
|
||||
@@ -904,8 +906,14 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
}
|
||||
}
|
||||
|
||||
void _showSavePlotDialog() {
|
||||
void _showSavePlotDialog() async {
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
// 后端 workRecord.routeType 必填(与新版「地图与区域编辑」共用同一接口):
|
||||
// 先选规划类型,再输地块名称;取消则不进入保存。
|
||||
final RouteType? routeType = await showRouteTypePicker(context);
|
||||
if (routeType == null || !mounted) return;
|
||||
|
||||
showInputConfirmDialog(
|
||||
context: context,
|
||||
title: loc.translate('route_planning.save_plot'), // 自定义标题
|
||||
@@ -947,10 +955,10 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
// 4. 处理截图结果并保存
|
||||
if (mounted) {
|
||||
if (imgBase64 != null) {
|
||||
_savePlotData(plotName, imgBase64); // 传入 Base64
|
||||
_savePlotData(plotName, imgBase64, routeType.code); // 传入 Base64
|
||||
} else {
|
||||
// 截图失败时,允许空图片保存(可根据业务调整为强制失败)
|
||||
_savePlotData(plotName, null);
|
||||
_savePlotData(plotName, null, routeType.code);
|
||||
_showPageToast(
|
||||
message:
|
||||
loc.translate('route_planning.save_success') +
|
||||
@@ -1179,7 +1187,11 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
debugPrint('📤 [保存地块-请求详情] ================= END =================');
|
||||
}
|
||||
|
||||
Future<void> _savePlotData(String plotName, String? imgBase64) async {
|
||||
Future<void> _savePlotData(
|
||||
String plotName,
|
||||
String? imgBase64,
|
||||
String routeType,
|
||||
) async {
|
||||
debugPrint('🔍 [保存地块] ====== _savePlotData 被调用, plotName=$plotName ======');
|
||||
final loc = AppLocalizations.of(context);
|
||||
// 🔥 V2 适配:使用 SiteCubit 获取站点ID(从 v2 首页场站选择获取,有默认值)
|
||||
@@ -1247,7 +1259,12 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
? WorkMode.bow.modeCode
|
||||
: WorkMode.custom.modeCode;
|
||||
workRecord['mode'] = modeCode;
|
||||
debugPrint('📤 [保存地块] workRecord 附加字段: orgId=$orgId, mode=$modeCode');
|
||||
// 🔥 后端必填的规划类型(Java 枚举 RouteType.name()),与 mode 平级;
|
||||
// 缺失时后端直接返回「未选择规划类型」
|
||||
workRecord['routeType'] = routeType;
|
||||
debugPrint(
|
||||
'📤 [保存地块] workRecord 附加字段: orgId=$orgId, mode=$modeCode, routeType=$routeType',
|
||||
);
|
||||
final String workRecordJson = jsonEncode(workRecord);
|
||||
final http.MultipartRequest request = http.MultipartRequest(
|
||||
'POST',
|
||||
@@ -2309,15 +2326,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
onTap: (_, __) {}, // 空实现,禁用地图点击响应
|
||||
),
|
||||
children: [
|
||||
/// ESRI World Imagery(WGS84 坐标系,全球高清卫星图)
|
||||
/// 替代高德地图(GCJ02),统一坐标系,无需坐标转换
|
||||
TileLayer(
|
||||
urlTemplate:
|
||||
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||||
userAgentPackageName: 'com.example.app',
|
||||
minZoom: 0,
|
||||
maxZoom: 19,
|
||||
),
|
||||
/// 底图:中文=ESRI(境内稳定),非中文=Google(英文地名,不可达自动回退)
|
||||
const AppBaseMapLayer(maxZoom: 22),
|
||||
|
||||
/// GeoServer 全部场站无人机高清影像叠加层(WGS84 基准,无偏移)
|
||||
/// 图层范围优先取自 GeoServer GetCapabilities(动态),失败回退内置兜底
|
||||
|
||||
@@ -55,9 +55,12 @@ class _RightJoystickAreaState extends State<RightJoystickArea> {
|
||||
color: const Color(0xE5333333),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Text(
|
||||
'暂未收到该机器的推送,不能控制',
|
||||
style: TextStyle(
|
||||
child: Text(
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
).translate('remote_control.joystick_no_data'),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ 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/components/control_mode_text.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';
|
||||
@@ -317,14 +318,14 @@ class TopStatusBar extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildControlModeChip(String mode, BuildContext context) {
|
||||
String displayText;
|
||||
if (mode == '') {
|
||||
displayText = AppLocalizations.of(
|
||||
context,
|
||||
).translate('remote_control.mode_none');
|
||||
} else {
|
||||
displayText = mode;
|
||||
}
|
||||
// mode 为协议原始值(0未知 / 1本地 / 2自主任务 / 3远程遥控),
|
||||
// 文案统一走 lib/components/control_mode_text.dart,与「设备数据监控」同一口径。
|
||||
// 以前此处把 '' 当「无模式」、又把 0 归到「本地模式」,两者都不对:
|
||||
// 现在 '' = 后端没给 = 未知,'0' = 未知(协议本意),'2' = 自主任务。
|
||||
// 清空缓存时 cubit 塞的是 '--' 占位,保持原样不动。
|
||||
final String displayText = mode == '--'
|
||||
? mode
|
||||
: controlModeDisplayText(AppLocalizations.of(context), mode);
|
||||
// switch (mode) {
|
||||
// case "蓝牙模式":
|
||||
// displayText = '蓝牙模式';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -317,7 +317,9 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
if (state is UpdateUpToDate) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('当前已是最新版本'),
|
||||
content: Text(
|
||||
AppLocalizations.of(context).translate('my_v2.up_to_date'),
|
||||
),
|
||||
backgroundColor: context.appColors.success,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
@@ -326,6 +328,7 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
},
|
||||
builder: (context, state) {
|
||||
final isChecking = state is UpdateChecking;
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -333,7 +336,7 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'版本更新',
|
||||
l10n.translate('my_v2.version_update'),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -342,7 +345,12 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'当前版本: $_currentVersion ($_currentVersionCode)',
|
||||
l10n
|
||||
.translate('my_v2.current_version')
|
||||
.replaceAll(
|
||||
'%s',
|
||||
'$_currentVersion ($_currentVersionCode)',
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: context.appColors.textTertiary,
|
||||
@@ -350,7 +358,9 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
isChecking ? '正在检查更新...' : '点击检查是否有新版本',
|
||||
isChecking
|
||||
? l10n.translate('my_v2.checking_update')
|
||||
: l10n.translate('my_v2.tap_to_check'),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: context.appColors.textTertiary,
|
||||
@@ -379,9 +389,9 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
color: const Color(0xFF165DFF),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'检查更新',
|
||||
style: TextStyle(
|
||||
child: Text(
|
||||
l10n.translate('my_v2.check_update'),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../devices/domain/entities/device_schedule_plan_entity.dart';
|
||||
import '../../../../devices/domain/usecases/save_device_schedule_plan_usecase.dart';
|
||||
|
||||
/// 提交状态
|
||||
enum SchedulePlanCreateStatus { idle, submitting, success, failure }
|
||||
|
||||
/// 新建作业计划页的提交状态。
|
||||
///
|
||||
/// 表单字段(设备/路线/周期规则/区间)与自动生成的计划名称由页面提供,
|
||||
/// 由页面持有;本 Cubit 只负责「提交创建」这一条接口链路的状态:
|
||||
/// idle → submitting → success / failure(message)。
|
||||
class SchedulePlanCreateState {
|
||||
const SchedulePlanCreateState({
|
||||
this.status = SchedulePlanCreateStatus.idle,
|
||||
this.message,
|
||||
});
|
||||
|
||||
final SchedulePlanCreateStatus status;
|
||||
|
||||
/// 失败原因(本地校验文案或后端 msg 透传),仅 failure 时有值
|
||||
final String? message;
|
||||
|
||||
bool get isSubmitting => status == SchedulePlanCreateStatus.submitting;
|
||||
|
||||
SchedulePlanCreateState copyWith({
|
||||
SchedulePlanCreateStatus? status,
|
||||
String? message,
|
||||
}) {
|
||||
return SchedulePlanCreateState(
|
||||
status: status ?? this.status,
|
||||
message: message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建定时作业计划:页面只调 submit,链路走
|
||||
/// UseCase → Repository → DataSource(POST /iot/deviceTask/save)。
|
||||
class SchedulePlanCreateCubit extends Cubit<SchedulePlanCreateState> {
|
||||
SchedulePlanCreateCubit(this._savePlanUseCase)
|
||||
: super(const SchedulePlanCreateState());
|
||||
|
||||
final SaveDeviceSchedulePlanUseCase _savePlanUseCase;
|
||||
|
||||
/// 提交创建。实体本地校验(含后端业务强校验)已在 UseCase 内前置拦截,
|
||||
/// 这里不重复校验、也不直连任何 Repository/接口。
|
||||
Future<void> submit(DeviceSchedulePlanEntity plan) async {
|
||||
if (state.isSubmitting) return;
|
||||
debugPrint(
|
||||
'[新建计划] 开始提交 siteId=${plan.siteId}, routeId=${plan.routeId}, '
|
||||
'type=${plan.type.name}',
|
||||
);
|
||||
emit(state.copyWith(status: SchedulePlanCreateStatus.submitting));
|
||||
try {
|
||||
final result = await _savePlanUseCase(
|
||||
SaveDeviceSchedulePlanParams(plan: plan),
|
||||
);
|
||||
if (isClosed) return;
|
||||
emit(
|
||||
result.fold(
|
||||
(failure) {
|
||||
debugPrint('[新建计划] 提交未成功(校验或接口失败):${failure.message}');
|
||||
return state.copyWith(
|
||||
status: SchedulePlanCreateStatus.failure,
|
||||
message: failure.message,
|
||||
);
|
||||
},
|
||||
(saved) {
|
||||
debugPrint('[新建计划] 保存结果:$saved');
|
||||
return SchedulePlanCreateState(
|
||||
status: saved
|
||||
? SchedulePlanCreateStatus.success
|
||||
: SchedulePlanCreateStatus.failure,
|
||||
message: saved ? null : '创建计划失败,请重试',
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
debugPrint('[新建计划] 提交异常:$error');
|
||||
debugPrintStack(stackTrace: stackTrace);
|
||||
if (isClosed) return;
|
||||
emit(
|
||||
const SchedulePlanCreateState(
|
||||
status: SchedulePlanCreateStatus.failure,
|
||||
message: '创建计划失败,请重试',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// failure 被提示后回到 idle,允许修改表单再次提交
|
||||
void clearFailure() {
|
||||
if (state.status != SchedulePlanCreateStatus.failure) return;
|
||||
emit(state.copyWith(status: SchedulePlanCreateStatus.idle));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user