From 84eec7da96df1db75e540ef57f732e6787fa68da Mon Sep 17 00:00:00 2001 From: Songzex <2402265378@qq.com> Date: Fri, 6 Nov 2026 08:47:36 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=BA=E5=99=A8=E5=88=97=E8=A1=A8=E5=92=8C?= =?UTF-8?q?=E8=BF=9C=E7=A8=8B=E5=96=8A=E8=AF=9D=E7=AD=89ui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/models/robot_data_model.dart | 95 ++ .../device_list/data/voice_preset_store.dart | 7 +- .../usecases/search_device_usecase.dart | 24 + .../usecases/update_device_alias_usecase.dart | 32 + .../presentation/bloc/robot_list_bloc.dart | 105 +++ .../pages/robot_app_power_page_v2.dart | 272 ++++-- .../pages/robot_autonomous_work_page_v2.dart | 487 ++++++++--- .../pages/robot_fault_center_page_v2.dart | 180 ++-- .../pages/robot_firmware_page_v2.dart | 149 ++-- .../pages/robot_full_info_page_v2.dart | 159 +++- .../pages/robot_light_sound_page_v2.dart | 162 +++- .../presentation/pages/robot_list_page.dart | 55 +- .../pages/robot_location_page_v2.dart | 107 ++- .../pages/robot_network_location_page_v2.dart | 151 +++- .../pages/robot_ownership_page_v2.dart | 810 ++++++++++++++---- .../pages/robot_run_record_page_v2.dart | 137 ++- .../pages/robot_sensor_page_v2.dart | 201 +++-- .../pages/robot_voice_preset_page_v2.dart | 170 ++-- .../autonomous/work_record_panels.dart | 408 ++------- .../widgets/autonomous/work_route_panels.dart | 32 +- .../widgets/robot_control_panel.dart | 27 +- .../presentation/widgets/robot_item_card.dart | 18 + .../widgets/robot_summary_card_v2.dart | 5 +- .../presentation/widgets/robot_task_info.dart | 5 +- .../widgets/voice_broadcast_sheet.dart | 78 +- 25 files changed, 2662 insertions(+), 1214 deletions(-) create mode 100644 lib/features/v2/device_list/domain/usecases/search_device_usecase.dart create mode 100644 lib/features/v2/device_list/domain/usecases/update_device_alias_usecase.dart diff --git a/lib/features/v2/device_list/data/models/robot_data_model.dart b/lib/features/v2/device_list/data/models/robot_data_model.dart index 1c53cd98..fb2e6f27 100644 --- a/lib/features/v2/device_list/data/models/robot_data_model.dart +++ b/lib/features/v2/device_list/data/models/robot_data_model.dart @@ -1,3 +1,7 @@ +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; + +import '../../domain/entities/device_model_info.dart'; + /// 机器人数据模型 class RobotDataModel { final String name; @@ -11,6 +15,28 @@ class RobotDataModel { final String task; final bool hasHost; + /// 产品型号(getSiteList 返回,如「割草机产品MC700」) + final String? productName; + + /// 机型编码(getSiteList 返回,如 MC700PRO)—— 列表机型展示以此为准 + final String? productModel; + + /// 设备类型(getSiteList 返回的 deviceType) + /// + /// 展示位置在版本号(PRO / PLUS / STD)之前:`MC700 · PRO 旗舰版`。 + final String? deviceType; + + /// 归属信息:当前挂在哪个组织 / 场站 / 负责人下。 + /// + /// 后端 getSiteList 不一定回传这些字段,因此全部可空: + /// 拿不到时页面显示「未知」,不编造。 + final int? orgId; + final String? orgName; + final int? siteId; + final String? siteName; + final int? userId; + final String? userName; + const RobotDataModel({ required this.name, required this.id, @@ -22,6 +48,15 @@ class RobotDataModel { required this.battery, required this.task, this.hasHost = false, + this.productName, + this.productModel, + this.deviceType, + this.orgId, + this.orgName, + this.siteId, + this.siteName, + this.userId, + this.userName, }); /// 从 JSON 创建数据模型 @@ -49,9 +84,60 @@ class RobotDataModel { battery: batteryValue, task: json['task'] ?? json['currentTask'] ?? '待机中', hasHost: json['hasHost'] as bool? ?? false, + productName: _pickStr(json['productName']), + productModel: _pickStr(json['productModel']), + deviceType: _pickStr(json['deviceType'] ?? json['device_type']), + orgId: _pickInt(json, const ['orgId', 'org_id', 'organizationId']), + orgName: _pickStr( + json['orgName'] ?? json['org_name'] ?? json['organizationName'], + ), + siteId: _pickInt(json, const ['siteId', 'site_id']), + siteName: _pickStr(json['siteName'] ?? json['site_name']), + userId: _pickInt(json, const [ + 'userId', + 'user_id', + 'ownerId', + 'owner_id', + 'managerId', + ]), + userName: _pickStr( + json['userName'] ?? + json['user_name'] ?? + json['ownerName'] ?? + json['nickName'] ?? + json['managerName'], + ), ); } + /// 多候选 key 里取第一个能转成整数的值 + static int? _pickInt(Map json, List keys) { + for (final key in keys) { + final value = json[key]; + if (value is int) return value; + if (value is num) return value.toInt(); + if (value is String) { + final parsed = int.tryParse(value.trim()); + if (parsed != null) return parsed; + } + } + return null; + } + + /// 机型信息(系列 + 版本),由 productModel 解析 + DeviceModelInfo get modelInfo => DeviceModelInfo.parse(productModel); + + /// 列表卡「型号 · 版本」文案:`MC700 · PRO 旗舰版` + /// productModel 为 null / 空串时统一显示未知; + /// deviceType 排在版本号之前。 + String modelLabelOf(AppLocalizations l10n) => + modelInfo.labelOf(l10n, deviceType: deviceType); + + static String? _pickStr(dynamic value) { + final s = value?.toString().trim() ?? ''; + return s.isEmpty ? null : s; + } + /// 转换为 JSON Map toJson() { return { @@ -64,6 +150,15 @@ class RobotDataModel { 'statusCode': statusCode, 'battery': battery, 'task': task, + 'productName': productName, + 'productModel': productModel, + 'deviceType': deviceType, + 'orgId': orgId, + 'orgName': orgName, + 'siteId': siteId, + 'siteName': siteName, + 'userId': userId, + 'userName': userName, }; } diff --git a/lib/features/v2/device_list/data/voice_preset_store.dart b/lib/features/v2/device_list/data/voice_preset_store.dart index 09794365..dcc00050 100644 --- a/lib/features/v2/device_list/data/voice_preset_store.dart +++ b/lib/features/v2/device_list/data/voice_preset_store.dart @@ -52,7 +52,12 @@ class VoicePresetStore { VoicePreset(id: 'default_polite', name: '礼貌提示', text: '请注意避让,感谢配合。'), ]; - static Future> loadPresets(String deviceName) async { + /// 首次进入时落盘一份出厂默认;[fallback] 允许调用方传入已本地化的默认值 + static Future> loadPresets( + String deviceName, { + List? fallback, + }) async { + final defaults = fallback ?? VoicePresetStore.defaults; final prefs = await SharedPreferences.getInstance(); final raw = prefs.getString(_keyOf(deviceName)); if (raw == null || raw.isEmpty) { diff --git a/lib/features/v2/device_list/domain/usecases/search_device_usecase.dart b/lib/features/v2/device_list/domain/usecases/search_device_usecase.dart new file mode 100644 index 00000000..925a4965 --- /dev/null +++ b/lib/features/v2/device_list/domain/usecases/search_device_usecase.dart @@ -0,0 +1,24 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart'; + +import '../repositories/device_repository.dart'; + +/// 一键寻车用例(设备控制 → 一键寻车 → 启动寻车提示) +/// +/// 对应后端 GET /iot/host/searchDevice,query 参数 deviceId 传 +/// 设备编码(deviceName,MC700- 开头),不是 getSiteList 的数字主键 id。 +/// 返回 true 表示指令已成功下发。 +class SearchDeviceUseCase { + final DeviceRepository repository; + + const SearchDeviceUseCase({required this.repository}); + + /// 入参非法(空编码)时返回 message 为空的 Failure,由 UI 层套用本地化提示。 + Future> execute({required String deviceCode}) async { + final code = deviceCode.trim(); + if (code.isEmpty) { + return Left(Failure('')); + } + return await repository.searchDevice(deviceCode: code); + } +} diff --git a/lib/features/v2/device_list/domain/usecases/update_device_alias_usecase.dart b/lib/features/v2/device_list/domain/usecases/update_device_alias_usecase.dart new file mode 100644 index 00000000..b0075b8a --- /dev/null +++ b/lib/features/v2/device_list/domain/usecases/update_device_alias_usecase.dart @@ -0,0 +1,32 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:maibu_satabot_v2/core/error/failure.dart'; + +import '../repositories/device_repository.dart'; + +/// 修改设备别名用例(设备列表卡片名称旁的铅笔图标) +/// +/// 只写后端 deviceAlias 字段,deviceName(出厂序列号)保持不变。 +/// 列表展示口径:deviceAlias 为空(首次绑定为 null)就回退显示 deviceName, +/// 因此这里拒绝空别名,避免把已有别名写成空串导致列表直接露出长序列号。 +class UpdateDeviceAliasUseCase { + final DeviceRepository repository; + + const UpdateDeviceAliasUseCase({required this.repository}); + + /// 返回 true 表示后端已成功更新别名。 + /// 入参非法时返回 message 为空的 Failure,由 UI 层套用本地化提示。 + Future> execute({ + required String deviceId, + required String deviceAlias, + }) async { + final id = deviceId.trim(); + final alias = deviceAlias.trim(); + if (id.isEmpty || alias.isEmpty) { + return Left(Failure('')); + } + return await repository.updateDeviceAlias( + deviceId: id, + deviceAlias: alias, + ); + } +} diff --git a/lib/features/v2/device_list/presentation/bloc/robot_list_bloc.dart b/lib/features/v2/device_list/presentation/bloc/robot_list_bloc.dart index 256d0d07..8f82fe55 100644 --- a/lib/features/v2/device_list/presentation/bloc/robot_list_bloc.dart +++ b/lib/features/v2/device_list/presentation/bloc/robot_list_bloc.dart @@ -1,8 +1,10 @@ import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../../core/consts/http_api_consts.dart'; import '../../../../../core/network/error_handler.dart'; import '../../data/models/robot_data_model.dart'; +import '../../domain/entities/device_model_info.dart'; import 'robot_list_event.dart'; import 'robot_list_state.dart'; @@ -50,6 +52,7 @@ class RobotListBloc extends Bloc { } final List rows = responseData['rows'] ?? []; + _logProductModel(rows, scene: 'load', siteId: event.siteId); final robots = rows.map((item) => RobotDataModel.fromJson(item)).toList(); emit(RobotListLoaded( @@ -100,6 +103,11 @@ class RobotListBloc extends Bloc { } final List rows = responseData['rows'] ?? []; + _logProductModel( + rows, + scene: 'refresh', + siteId: currentState.siteId, + ); final robots = rows.map((item) => RobotDataModel.fromJson(item)).toList(); emit(currentState.copyWith(robots: robots)); @@ -112,6 +120,103 @@ class RobotListBloc extends Bloc { } } + /// 🔍 专项日志:只核查 getSiteList 返回体里是否带 productModel 字段 + /// 整体打印 row 时内容过长会被截断,这里只摘取关键字段 + 型号相关候选字段 + void _logProductModel( + List rows, { + required String scene, + required int? siteId, + }) { + debugPrint( + '>>> [getSiteList/$scene] siteId=$siteId 返回 ${rows.length} 条', + ); + if (rows.isEmpty) { + debugPrint('>>> [getSiteList/$scene] rows 为空,无法判断 productModel 是否存在'); + return; + } + for (int i = 0; i < rows.length; i++) { + final row = rows[i]; + if (row is! Map) { + debugPrint( + '>>> [getSiteList/$scene] rows[$i] 非 Map 结构: ${row.runtimeType}', + ); + continue; + } + final json = Map.from(row); + final head = + 'deviceId=${json['deviceId']} name=${json['deviceName'] ?? json['name']}'; + if (json.containsKey('productModel')) { + final value = json['productModel']; + // 同时输出解析结果,方便确认机型(STD/PLUS/PRO)是否识别正确 + final info = DeviceModelInfo.parse(value?.toString()); + debugPrint( + '>>> [getSiteList/$scene] rows[$i] $head 有 productModel=$value ' + '(${value.runtimeType}) → 解析: series=${info.series} edition=${info.edition.name}', + ); + } else { + // 字段缺失时列出所有“疑似型号”字段,便于确认后端真实字段名 + final candidates = json.entries + .where((e) => _isModelLikeKey(e.key)) + .map((e) => '${e.key}=${e.value}') + .join(', '); + debugPrint( + '>>> [getSiteList/$scene] rows[$i] $head 无 productModel 字段;型号候选字段: ' + '${candidates.isEmpty ? '无' : candidates}', + ); + // 彻底核实:打印顶层全部字段名,并递归扫描子对象/数组 + _printChunked( + '>>> [getSiteList/$scene] rows[$i] 全部顶层字段(${json.length}): ', + json.keys.join('|'), + ); + final nested = _findKeyPaths(json, 'productModel'); + debugPrint( + '>>> [getSiteList/$scene] rows[$i] 嵌套层 productModel 命中: ' + '${nested.isEmpty ? '无(子对象/数组均未找到)' : nested.join(', ')}', + ); + } + } + } + + /// 递归查找指定 key(含子对象与数组),返回“路径=值”列表 + List _findKeyPaths(dynamic node, String key, [String path = '']) { + final hits = []; + if (node is Map) { + node.forEach((k, v) { + final p = path.isEmpty ? '$k' : '$path.$k'; + if (k.toString() == key) { + hits.add('$p=$v'); + } else { + hits.addAll(_findKeyPaths(v, key, p)); + } + }); + } else if (node is List) { + for (int i = 0; i < node.length; i++) { + hits.addAll(_findKeyPaths(node[i], key, '$path[$i]')); + } + } + return hits; + } + + /// 长文本分段打印,避免 debugPrint 单条过长被截断 + void _printChunked(String prefix, String content) { + const chunk = 700; + var head = prefix; + for (int start = 0; start < content.length; start += chunk) { + final end = + start + chunk < content.length ? start + chunk : content.length; + debugPrint('$head${content.substring(start, end)}'); + head = ' ↳ '; + } + } + + bool _isModelLikeKey(String key) { + final k = key.toLowerCase(); + return k.contains('model') || + k.contains('product') || + k.contains('type') || + k.contains('version'); + } + void _onChangeType( RobotListChangeType event, Emitter emit, diff --git a/lib/features/v2/device_list/presentation/pages/robot_app_power_page_v2.dart b/lib/features/v2/device_list/presentation/pages/robot_app_power_page_v2.dart index 6f61a50f..eb6f1131 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_app_power_page_v2.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_app_power_page_v2.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:maibu_satabot_v2/components/capsule_toast.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import '../../data/models/robot_data_model.dart'; import '../widgets/device_confirm_dialog.dart'; @@ -11,12 +12,14 @@ import '../widgets/voice_broadcast_sheet.dart'; /// 一条联动操作(远程开机后才解锁) class _LinkageItem { final IconData icon; + + /// 名称语言包 key final String label; - /// 主操作文案 + /// 主操作文案语言包 key final String action; - /// 次操作文案,null 表示只有一个操作(如急停) + /// 次操作文案语言包 key,null 表示只有一个操作(如急停) final String? secondary; /// 主操作是否红色危险态 @@ -94,115 +97,144 @@ class _RobotAppPowerPageV2State extends State { bool get _online => widget.robot.isOnline; - /// 联动操作清单,顺序与设计稿一致 + /// 联动操作清单,顺序与设计稿一致;label/action/secondary 均存语言包 key static const List<_LinkageItem> _linkages = [ _LinkageItem( icon: Icons.content_cut, - label: '割刀启停', - action: '启动', - secondary: '停止', + label: 'robot_power_v2.lk_blade', + action: 'robot_power_v2.act_start', + secondary: 'robot_power_v2.act_stop', ), _LinkageItem( icon: Icons.unfold_more_rounded, - label: '底盘升降', - action: '升起', - secondary: '降下', + label: 'robot_power_v2.lk_chassis', + action: 'robot_power_v2.act_raise', + secondary: 'robot_power_v2.act_lower', ), _LinkageItem( icon: Icons.emergency_outlined, - label: '一键急停', - action: '急停', + label: 'robot_power_v2.lk_estop', + action: 'robot_power_v2.act_estop', danger: true, ), _LinkageItem( icon: Icons.lightbulb_outline, - label: '灯光', - action: '开启', - secondary: '关闭', + label: 'robot_power_v2.lk_light', + action: 'robot_power_v2.act_on', + secondary: 'robot_power_v2.act_off', ), _LinkageItem( icon: Icons.campaign_outlined, - label: '鸣笛/语音', - action: '鸣笛', - secondary: '语音播报', + label: 'robot_power_v2.lk_horn', + action: 'robot_power_v2.act_horn', + secondary: 'robot_power_v2.act_voice', voiceSecondary: true, ), ]; /// 通道未接入时的统一提示(状态仍按设计稿切换,便于验收解锁态) - void _channelTip(String action) => CapsuleToast.show( - '$action指令通道未接入(需遥控控制权与点火位下发),当前状态仅作界面预览', - showCheck: false, - ); + void _channelTip(String actionKey) { + final l10n = AppLocalizations.of(context); + CapsuleToast.show( + l10n + .translate('robot_power_v2.toast_channel_tip') + .replaceAll('%s', l10n.translate(actionKey)), + showCheck: false, + ); + } /// 远程开机:先弹安全确认(离线也先弹,避免点了没反应),确认后再校验下发条件 Future _powerOn() async { + final l10n = AppLocalizations.of(context); if (_powered) { - CapsuleToast.show('设备已开机', showCheck: false); + CapsuleToast.show( + l10n.translate('robot_power_v2.toast_already_on'), + showCheck: false, + ); return; } final ok = await showDeviceConfirmDialog( context, icon: Icons.power_settings_new_rounded, - title: '远程开机确认', - message: '确认向机器人发送远程开机指令?开机成功后,才可启用割刀、底盘升降、急停等联动操作。', - note: '请确认设备周边安全、网络正常。', - confirmText: '确认开机', + title: l10n.translate('robot_power_v2.power_on_title'), + message: l10n.translate('robot_power_v2.power_on_msg'), + note: l10n.translate('robot_power_v2.power_on_note'), + confirmText: l10n.translate('robot_power_v2.power_on_ok'), ); if (!ok || !mounted) return; if (!_online) { - CapsuleToast.show('设备离线,开机指令未下发', showCheck: false); + CapsuleToast.show( + AppLocalizations.of(context).translate('robot_power_v2.toast_offline_on'), + showCheck: false, + ); return; } if (widget.onPowerOn != null) { widget.onPowerOn!(); } else { - _channelTip('远程开机'); + _channelTip('robot_power_v2.btn_power_on'); } setState(() => _powered = true); } /// 远程关机:同样先弹确认,确认后再校验下发条件 Future _powerOff() async { + final l10n = AppLocalizations.of(context); if (!_powered) { - CapsuleToast.show('设备当前已关机', showCheck: false); + CapsuleToast.show( + l10n.translate('robot_power_v2.toast_already_off'), + showCheck: false, + ); return; } final ok = await showDeviceConfirmDialog( context, icon: Icons.power_settings_new_rounded, - title: '远程关机确认', - message: '确认向机器人发送远程关机指令?关机后割刀、底盘、灯光、鸣笛等联动功能将立即失效。', - note: '请确认设备已停止作业并处于安全位置。', - confirmText: '确认关机', + title: l10n.translate('robot_power_v2.power_off_title'), + message: l10n.translate('robot_power_v2.power_off_msg'), + note: l10n.translate('robot_power_v2.power_off_note'), + confirmText: l10n.translate('robot_power_v2.power_off_ok'), danger: true, ); if (!ok || !mounted) return; if (!_online) { - CapsuleToast.show('设备离线,关机指令未下发', showCheck: false); + CapsuleToast.show( + AppLocalizations.of( + context, + ).translate('robot_power_v2.toast_offline_off'), + showCheck: false, + ); return; } if (widget.onPowerOff != null) { widget.onPowerOff!(); } else { - _channelTip('远程关机'); + _channelTip('robot_power_v2.btn_power_off'); } setState(() => _powered = false); } /// 联动操作统一入口:未开机 → 锁定提示;已开机 → 回调或明确未接入 - void _linkage(String action) { + void _linkage(String actionKey) { + final l10n = AppLocalizations.of(context); + final action = l10n.translate(actionKey); if (!_powered) { - CapsuleToast.show('请先远程开机,开机后方可执行$action', showCheck: false); + CapsuleToast.show( + l10n.translate('robot_power_v2.toast_need_power').replaceAll('%s', action), + showCheck: false, + ); return; } if (widget.onLinkageAction != null) { widget.onLinkageAction!(action); return; } - CapsuleToast.show('$action指令通道未接入', showCheck: false); + CapsuleToast.show( + l10n.translate('robot_power_v2.toast_channel_short').replaceAll('%s', action), + showCheck: false, + ); } /// 次操作:语音播报走公共弹窗,其余走联动通道 @@ -212,7 +244,12 @@ class _RobotAppPowerPageV2State extends State { return; } if (!_powered) { - CapsuleToast.show('请先远程开机,开机后方可执行语音播报', showCheck: false); + CapsuleToast.show( + AppLocalizations.of( + context, + ).translate('robot_power_v2.toast_need_power_voice'), + showCheck: false, + ); return; } _openVoiceBroadcast(); @@ -228,17 +265,24 @@ class _RobotAppPowerPageV2State extends State { showVoiceBroadcastSheet(context, robot: widget.robot); } - void _openMoreUnlock() => - CapsuleToast.show('更多解锁功能将随版本逐步开放', showCheck: false); + void _openMoreUnlock() => CapsuleToast.show( + AppLocalizations.of(context).translate('robot_power_v2.toast_more_unlock'), + showCheck: false, + ); - void _openPowerHelp() => - CapsuleToast.show('远程开机后才可执行割刀、底盘、急停、灯光、鸣笛等联动操作', showCheck: false); + void _openPowerHelp() => CapsuleToast.show( + AppLocalizations.of(context).translate('robot_power_v2.toast_power_help'), + showCheck: false, + ); - void _openSafetyHelp() => - CapsuleToast.show('远程上电与动作控制需设备端安全鉴权,确认周边无人后再操作', showCheck: false); + void _openSafetyHelp() => CapsuleToast.show( + AppLocalizations.of(context).translate('robot_power_v2.toast_safety_help'), + showCheck: false, + ); @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); final tileWidth = (MediaQuery.of(context).size.width - 32 - 20) / 3; return Scaffold( backgroundColor: const Color(0xFFEDF6FD), @@ -263,28 +307,28 @@ class _RobotAppPowerPageV2State extends State { RobotSummaryCardV2(robot: widget.robot), const SizedBox(height: 16), _buildSectionTitle( - '远程开关机', + l10n.translate('robot_power_v2.sec_remote_power'), trailing: GestureDetector( onTap: _openPowerHelp, behavior: HitTestBehavior.opaque, - child: const Row( + child: Row( mainAxisSize: MainAxisSize.min, children: [ - Icon( + const Icon( Icons.help_outline_rounded, size: 14, color: _blue, ), - SizedBox(width: 3), + const SizedBox(width: 3), Text( - '开关机说明', - style: TextStyle( + l10n.translate('robot_power_v2.power_help'), + style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: _blue, ), ), - Icon( + const Icon( Icons.chevron_right_rounded, size: 15, color: _blue, @@ -297,9 +341,13 @@ class _RobotAppPowerPageV2State extends State { _buildRemotePowerCard(), const SizedBox(height: 16), _buildSectionTitle( - '联动操作', + l10n.translate('robot_power_v2.sec_linkage'), trailing: Text( - _powered ? '已解锁,可直接操作' : '开机后再操作', + l10n.translate( + _powered + ? 'robot_power_v2.unlocked' + : 'robot_power_v2.locked_hint', + ), style: const TextStyle(fontSize: 11.5, color: _hint), ), ), @@ -321,10 +369,10 @@ class _RobotAppPowerPageV2State extends State { ), const SizedBox(height: 16), _buildSectionTitle( - '开机后解锁的控制', - trailing: const Text( - '远程开机成功后自动解锁', - style: TextStyle(fontSize: 11.5, color: _hint), + l10n.translate('robot_power_v2.sec_unlock'), + trailing: Text( + l10n.translate('robot_power_v2.unlock_hint'), + style: const TextStyle(fontSize: 11.5, color: _hint), ), ), const SizedBox(height: 10), @@ -373,12 +421,12 @@ class _RobotAppPowerPageV2State extends State { ), ), const SizedBox(width: 10), - const Expanded( + Expanded( child: Text( - 'APP开关机与联动控制', + AppLocalizations.of(context).translate('robot_power_v2.title'), maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle( + style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w800, color: _navy, @@ -402,7 +450,11 @@ class _RobotAppPowerPageV2State extends State { ), const SizedBox(width: 4), Text( - _powered ? '已开机' : '未开机', + AppLocalizations.of(context).translate( + _powered + ? 'robot_power_v2.state_on' + : 'robot_power_v2.state_off_short', + ), style: TextStyle( fontSize: 11, fontWeight: FontWeight.w700, @@ -484,13 +536,17 @@ class _RobotAppPowerPageV2State extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '当前状态', - style: TextStyle(fontSize: 12, color: _gray), + Text( + AppLocalizations.of(context).translate('robot_power_v2.cur_state'), + style: const TextStyle(fontSize: 12, color: _gray), ), const SizedBox(height: 4), Text( - _powered ? '已开机' : '已关机', + AppLocalizations.of(context).translate( + _powered + ? 'robot_power_v2.state_on' + : 'robot_power_v2.state_off', + ), style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w800, @@ -499,9 +555,11 @@ class _RobotAppPowerPageV2State extends State { ), const SizedBox(height: 6), Text( - _powered - ? '联动操作已解锁,可执行割刀、底盘、灯光、鸣笛等操作。' - : '远程开机后,方可执行割刀、底盘、急停、灯光、鸣笛等联动操作。', + AppLocalizations.of(context).translate( + _powered + ? 'robot_power_v2.powered_desc' + : 'robot_power_v2.unpowered_desc', + ), style: const TextStyle( fontSize: 11, height: 1.5, @@ -516,10 +574,18 @@ class _RobotAppPowerPageV2State extends State { width: 104, child: Column( children: [ - _buildPowerButton(label: '远程开机', filled: true, onTap: _powerOn), + _buildPowerButton( + label: AppLocalizations.of( + context, + ).translate('robot_power_v2.btn_power_on'), + filled: true, + onTap: _powerOn, + ), const SizedBox(height: 8), _buildPowerButton( - label: '远程关机', + label: AppLocalizations.of( + context, + ).translate('robot_power_v2.btn_power_off'), filled: false, onTap: _powerOff, ), @@ -580,6 +646,7 @@ class _RobotAppPowerPageV2State extends State { // ===== 联动磁贴:图标 + 名称 + 锁提示 + 分段按钮 ===== Widget _buildLinkageTile(_LinkageItem item) { + final l10n = AppLocalizations.of(context); final locked = !_powered; final accent = locked ? _hint : (item.danger ? _danger : _blue); return Container( @@ -606,7 +673,7 @@ class _RobotAppPowerPageV2State extends State { ), const SizedBox(height: 8), Text( - item.label, + l10n.translate(item.label), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -624,7 +691,11 @@ class _RobotAppPowerPageV2State extends State { const SizedBox(width: 2), ], Text( - locked ? '设备未开机' : '可操作', + l10n.translate( + locked + ? 'robot_power_v2.tile_device_off' + : 'robot_power_v2.tile_operable', + ), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 9.5, color: _hint), @@ -653,6 +724,7 @@ class _RobotAppPowerPageV2State extends State { required VoidCallback onPrimary, VoidCallback? onSecondary, }) { + final l10n = AppLocalizations.of(context); final textColor = locked ? _lockedText : accent; return Container( height: 28, @@ -669,7 +741,7 @@ class _RobotAppPowerPageV2State extends State { behavior: HitTestBehavior.opaque, child: Center( child: Text( - item.action, + l10n.translate(item.action), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -694,7 +766,7 @@ class _RobotAppPowerPageV2State extends State { behavior: HitTestBehavior.opaque, child: Center( child: Text( - item.secondary!, + l10n.translate(item.secondary!), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -738,7 +810,7 @@ class _RobotAppPowerPageV2State extends State { ), const SizedBox(height: 8), Text( - '一键寻车', + AppLocalizations.of(context).translate('robot_power_v2.find_tile'), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -756,7 +828,11 @@ class _RobotAppPowerPageV2State extends State { const SizedBox(width: 2), ], Text( - locked ? '声光需开机' : '可操作', + AppLocalizations.of(context).translate( + locked + ? 'robot_power_v2.find_locked' + : 'robot_power_v2.tile_operable', + ), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 9.5, color: _hint), @@ -775,11 +851,11 @@ class _RobotAppPowerPageV2State extends State { borderRadius: BorderRadius.circular(9), border: Border.all(color: _segBorder), ), - child: const Text( - '开始寻车', + child: Text( + AppLocalizations.of(context).translate('robot_power_v2.find_start'), maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle( + style: const TextStyle( fontSize: 10.5, fontWeight: FontWeight.w700, color: _blue, @@ -794,11 +870,12 @@ class _RobotAppPowerPageV2State extends State { // ===== 开机后解锁的控制:开关机 → 联动功能链 ===== Widget _buildUnlockCard() { + final l10n = AppLocalizations.of(context); const items = <(IconData, String)>[ - (Icons.content_cut, '割刀启停'), - (Icons.unfold_more_rounded, '底盘升降'), - (Icons.lightbulb_outline, '灯光'), - (Icons.campaign_outlined, '鸣笛/语音'), + (Icons.content_cut, 'robot_power_v2.lk_blade'), + (Icons.unfold_more_rounded, 'robot_power_v2.lk_chassis'), + (Icons.lightbulb_outline, 'robot_power_v2.lk_light'), + (Icons.campaign_outlined, 'robot_power_v2.lk_horn'), ]; return Container( padding: const EdgeInsets.fromLTRB(12, 12, 12, 12), @@ -827,7 +904,7 @@ class _RobotAppPowerPageV2State extends State { ), const SizedBox(height: 6), Text( - '远程开机', + l10n.translate('robot_power_v2.btn_power_on'), style: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, @@ -836,7 +913,11 @@ class _RobotAppPowerPageV2State extends State { ), const SizedBox(height: 2), Text( - _powered ? '已开机' : '当前状态', + l10n.translate( + _powered + ? 'robot_power_v2.state_on' + : 'robot_power_v2.cur_state', + ), style: const TextStyle(fontSize: 8.5, color: _hint), ), ], @@ -871,7 +952,7 @@ class _RobotAppPowerPageV2State extends State { ), const SizedBox(height: 5), Text( - item.$2, + l10n.translate(item.$2), style: TextStyle( fontSize: 9, fontWeight: FontWeight.w600, @@ -883,9 +964,9 @@ class _RobotAppPowerPageV2State extends State { GestureDetector( onTap: _openMoreUnlock, behavior: HitTestBehavior.opaque, - child: const Column( + child: Column( children: [ - SizedBox( + const SizedBox( width: 30, height: 30, child: Icon( @@ -894,8 +975,11 @@ class _RobotAppPowerPageV2State extends State { color: _hint, ), ), - SizedBox(height: 5), - Text('更多', style: TextStyle(fontSize: 9, color: _hint)), + const SizedBox(height: 5), + Text( + l10n.translate('robot_power_v2.more'), + style: const TextStyle(fontSize: 9, color: _hint), + ), ], ), ), @@ -923,10 +1007,10 @@ class _RobotAppPowerPageV2State extends State { children: [ const Icon(Icons.verified_user_rounded, size: 16, color: _blue), const SizedBox(width: 8), - const Expanded( + Expanded( child: Text( - '涉及远程上电与动作控制,需通过安全鉴权,确保设备与人员安全。', - style: TextStyle( + AppLocalizations.of(context).translate('robot_power_v2.footer'), + style: const TextStyle( fontSize: 11.5, height: 1.5, color: Color(0xFF5C6B84), diff --git a/lib/features/v2/device_list/presentation/pages/robot_autonomous_work_page_v2.dart b/lib/features/v2/device_list/presentation/pages/robot_autonomous_work_page_v2.dart index 7732c8b0..5b1b1ef5 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_autonomous_work_page_v2.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_autonomous_work_page_v2.dart @@ -1,14 +1,21 @@ import 'dart:async'; +// import 'package:maibu_satabot_v2/features/device_control_mode/domain/entities/device_control_mode.dart'; +// import 'package:maibu_satabot_v2/features/device_control_mode/presentation/widgets/control_mode_entry_gate.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:latlong2/latlong.dart'; import 'package:maibu_satabot_v2/components/capsule_toast.dart'; import 'package:maibu_satabot_v2/core/di/injection.dart'; +import 'package:maibu_satabot_v2/core/logging/work_trace_diagnostics.dart'; import 'package:maibu_satabot_v2/features/autonomous_work/domain/entities/work_session_models.dart'; import 'package:maibu_satabot_v2/features/autonomous_work/domain/repositories/autonomous_work_gateway.dart'; +import 'package:maibu_satabot_v2/features/devices/domain/entities/running_status_entity.dart'; import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart'; +import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_state.dart'; import 'package:maibu_satabot_v2/features/pv_control/presentation/widgets/pv_work_map.dart'; +import 'package:maibu_satabot_v2/features/pv_control/presentation/models/work_trace_recorder.dart'; import '../../data/models/robot_data_model.dart'; import '../widgets/autonomous/work_confirm_dialog.dart'; @@ -35,17 +42,36 @@ import 'route_editor_page_v2.dart'; /// /// 这样换环境(A/B 两套后端)只需新增一个 gateway 实现 + 改装配层,本页零改动。 +/// 进入本页时要自动展开的功能面板(用于区分入口语义): +/// - 任务启动入口:不传,停在准备态; +/// - 任务管理入口:传 [taskQueue],进页直接展开「多区域连续任务队列」。 +enum RobotWorkEntryPanel { taskQueue, routeLibrary, record, settings } + class RobotAutonomousWorkPageV2 extends StatefulWidget { - const RobotAutonomousWorkPageV2({super.key, required this.robot}); + const RobotAutonomousWorkPageV2({ + super.key, + required this.robot, + this.entryPanel, + this.initialRoute, + }); final RobotDataModel robot; + /// 入口初始面板:null = 准备态(任务启动入口行为不变) + final RobotWorkEntryPanel? entryPanel; + + /// 从「路线库」(作业 tab → 路线库页)选中带入的路线: + /// 进场即选中该路线并画线,等价抽屉面板里点「确认路线」。 + final WorkRouteOption? initialRoute; + @override State createState() => _RobotAutonomousWorkPageV2State(); } class _RobotAutonomousWorkPageV2State extends State { + final _traceDiagnostics = WorkTraceDiagnostics(); + /// 与原生 MainActivity 的窗口通道(挖孔区域占用控制) static const MethodChannel _windowChannel = MethodChannel('satabot/window'); @@ -65,25 +91,71 @@ class _RobotAutonomousWorkPageV2State extends State { StreamSubscription? _taskSub; + String? _lastFinishedTaskId; + Timer? _finishTraceTimer; + + /// 清轨迹信号:每次 +1 就把 PvWorkMap 内部的绿色行走轨迹清空 + /// (等价路径规划页的 traceManager.reset()) + int _clearTraceSignal = 0; + /// 正在拉取的 routeId:防止状态流连续推送时重复查询同一路线 String? _restoringRouteId; + int _routeLoadGeneration = 0; bool _infoExpanded = true; String get _deviceId => widget.robot.name; bool get _isOnline => widget.robot.isOnline; - String get _routeName => - _route?.name ?? _session?.routeName ?? '未选择作业路线'; - List get _previewRoute => - _route?.previewPoints ?? const []; + String get _routeName => _route?.name ?? _session?.routeName ?? '未选择作业路线'; + List get _previewRoute => _route?.previewPoints ?? const []; /// 覆盖模式的作业区域边界框(自定义模式为空,与路径规划一致) - List get _previewOuter => - _route?.previewOuter ?? const []; + List get _previewOuter => _route?.previewOuter ?? const []; /// 覆盖模式:地图上画灰色区域框;自定义模式画绿色边界线 bool get _previewCoverage => _route?.coverageMode ?? true; + // ===== 实时设备数据(与「远程遥控」同源:全局 DeviceStatusBloc,MQTT 500ms 节流)===== + // 顶部 RTK / 行驶速度、信息卡的 定位 / 割刀状态 / 行驶速度 均取这里, + // 数据口径与「设备数据监控」弹窗一致(qual / linearSpeed / knifeCuttingSpeed)。 + DeviceStatusBloc get _statusBloc => sl(); + + /// 从 DeviceStatus 状态里取最新机器状态快照(未收到数据为 null) + static RunningStatusEntity? _statusOf(DeviceStatusState s) => + s is DeviceStatusUpdated ? s.status : null; + + /// 定位质量枚举 → 文案(与设备数据监控 _getLocationQualityText 同口径) + static String _qualText(int? q) => switch (q) { + null => '未知', + 0 => '无定位', + 1 => '单点定位', + 2 => '差分定位', + 3 => '惯性导航', + 4 => 'RTK 固定解', + 5 => 'RTK 浮点解', + 6 => '单点(无航向)', + 7 => '差分(无航向)', + 8 => 'RTK 固定(无航向)', + 9 => 'RTK 浮点(无航向)', + _ => '未知', + }; + + /// 是否 RTK 固定解(4=固定,8=固定无航向)——绿色高亮依据 + static bool _isRtkFixed(int? q) => q == 4 || q == 8; + + /// 行驶速度(m/s):仅当已收到 host 实时数据时取线性速度,否则 null(显示 '--') + static double? _speedOf(RunningStatusEntity? s) => + (s != null && s.hostDataReceived) ? s.linearSpeed : null; + + /// 割刀转速(rpm):来自机器状态 knifeCuttingSpeed;无数据为 null + static double? _knifeRpmOf(RunningStatusEntity? s) => + s == null ? null : (double.tryParse(s.knifeCuttingSpeed) ?? 0); + + /// 实时电量(%):与远程遥控同口径,仅当已收到 host 实时数据时取 status.battery; + /// 未收到时返回 null,由 UI 回退设备列表的静态电量 + static double? _batteryOf(RunningStatusEntity? s) => + (s != null && s.hostDataReceived) ? double.tryParse(s.battery) : null; + @override void initState() { super.initState(); @@ -95,11 +167,34 @@ class _RobotAutonomousWorkPageV2State extends State { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; sl().startMqttRealtimeListening(_deviceId); - _bootstrap(); + // 先还原本设备活跃任务,再应用「路线库」带入的路线(两者有先后依赖) + unawaited(_bootstrapAndApplyInitialRoute()); + // 自主作业暂不弹模式确认,直接恢复对应入口面板。 + _openEntryPanelIfNeeded(); }); _taskSub = _gateway.watchTask(deviceId: _deviceId).listen(_applySession); } + /// 入口自动展开的面板:任务管理 → 任务队列;其余入口按参数展开 + void _openEntryPanelIfNeeded() { + final panel = widget.entryPanel; + if (panel == null || !mounted) return; + switch (panel) { + case RobotWorkEntryPanel.taskQueue: + _openTaskQueue(); + break; + case RobotWorkEntryPanel.routeLibrary: + _openRouteLibrary(); + break; + case RobotWorkEntryPanel.record: + _openRecordPanel(); + break; + case RobotWorkEntryPanel.settings: + _openSettingPanel(); + break; + } + } + /// 进入横屏(与 PV 遥控页一致):沉浸式 + 占用挖孔区域铺满全屏 void _enterLandscape() { SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); @@ -128,6 +223,7 @@ class _RobotAutonomousWorkPageV2State extends State { @override void dispose() { _taskSub?.cancel(); + _finishTraceTimer?.cancel(); _restoreOrientation(); super.dispose(); } @@ -142,6 +238,43 @@ class _RobotAutonomousWorkPageV2State extends State { if (mounted) _enterLandscape(); } + void _onWorkFinished(WorkTaskSnapshot task) { + if (_lastFinishedTaskId == task.taskId) { + _traceDiagnostics.log('结束页面去重', { + 'deviceId': _deviceId, + 'taskId': task.taskId, + 'status': task.rawStatus, + }); + return; + } + _traceDiagnostics.log('结束页面确认', { + 'deviceId': _deviceId, + 'taskId': task.taskId, + 'status': task.rawStatus, + 'points': task.completedPoints.length, + 'note': '页面收到结束快照,准备显示完成提示;确认来源见结束状态落地日志', + }); + _lastFinishedTaskId = task.taskId; + _finishTraceTimer?.cancel(); + CapsuleToast.show( + task.rawStatus == 'FINISH' ? '作业已完成' : '作业已结束', + showCheck: true, + ); + _finishTraceTimer = Timer(const Duration(seconds: 1), () { + if (mounted && + _session?.taskId == task.taskId && + _phase == WorkRunState.finished) { + setState(() => _clearTraceSignal++); + _traceDiagnostics.log('结束显示清理', { + 'deviceId': _deviceId, + 'taskId': task.taskId, + 'pagePhase': _phase.name, + 'clearTraceSignal': _clearTraceSignal, + }); + } + }); + } + // ===== 状态映射:唯一真相是 gateway 给的 WorkTaskSnapshot ===== /// 把任务快照落到界面态。 /// - queued:本页刚点过「开始」就显示执行中(后端 NEW→EXECUTING 有延迟) @@ -149,6 +282,18 @@ class _RobotAutonomousWorkPageV2State extends State { /// - null:任务被清空,但若是「已结束」保持结束态,不弹回准备 void _applySession(WorkTaskSnapshot? session) { if (!mounted) return; + if (session != null && session.deviceId != _deviceId) { + _traceDiagnostics.log('页面过滤', { + 'expectedDeviceId': _deviceId, + 'receivedDeviceId': session.deviceId, + 'taskId': session.taskId, + 'reason': '任务快照设备不匹配', + }); + return; + } + if (session != null && session.state == WorkRunState.finished) { + _onWorkFinished(session); + } setState(() { _session = session; final state = session?.state; @@ -159,7 +304,9 @@ class _RobotAutonomousWorkPageV2State extends State { } switch (state) { case WorkRunState.queued: - _phase = _startedByUser ? WorkRunState.running : WorkRunState.queued; + _phase = _startedByUser || session?.executionStarted == true + ? WorkRunState.running + : WorkRunState.queued; case WorkRunState.running: // 后端已确认执行:乐观标记可以撤掉了 _startedByUser = false; @@ -176,6 +323,36 @@ class _RobotAutonomousWorkPageV2State extends State { _phase = state; } }); + if (session != null && session.state == WorkRunState.finished) { + _traceDiagnostics.log('结束页面落地', { + 'deviceId': _deviceId, + 'taskId': session.taskId, + 'status': session.rawStatus, + 'pagePhase': _phase.name, + 'receivedPoints': session.completedPoints.length, + }); + } + _traceDiagnostics.log( + '页面接收', + { + 'deviceId': _deviceId, + 'taskId': session?.taskId, + 'status': session?.rawStatus, + 'taskState': session?.state.name, + 'executionStarted': session?.executionStarted, + 'canRecordTrace': session?.canRecordTrace ?? false, + 'pagePhase': _phase.name, + 'receivedPoints': session?.completedPoints.length ?? 0, + 'mapCompletedPoints': + _phase == WorkRunState.running || _phase == WorkRunState.paused + ? session?.completedPoints.length ?? 0 + : 0, + 'recordLocationTrace': false, + 'traceMode': '首点前定位累计,首点后到达点+实时车位;实际画线见地图画线日志', + }, + sampleKey: + 'page/$_deviceId/${session?.taskId}/${session?.state.name}/${_phase.name}/${(session?.completedPoints.length ?? 0) < 2 ? session?.completedPoints.length ?? 0 : "2+"}', + ); // 换了一条任务:把它的路线加载出来画到地图上 // (对应路径规划页点选列表项后立即画线的行为) final routeId = session?.routeId; @@ -190,22 +367,48 @@ class _RobotAutonomousWorkPageV2State extends State { Future _bootstrap() async { final result = await _gateway.loadTasks(deviceId: _deviceId); if (!mounted) return; - final tasks = result.data ?? const []; - if (tasks.isEmpty) return; - _applySession(tasks.first); + if (!result.isOk) { + CapsuleToast.show(result.error ?? '任务恢复失败', showCheck: false); + } + } + + /// 先完成进场任务还原,再应用「路线库」带入的路线: + /// 设备上有执行中的任务时以任务为准(与在抽屉里换路线的拦截口径一致), + /// 否则选中带入路线并画线;列表没带几何时走 resolveRouteGeometry 补全。 + Future _bootstrapAndApplyInitialRoute() async { + await _bootstrap(); + final route = widget.initialRoute; + if (!mounted || route == null) return; + if (_session != null && _phase == WorkRunState.running) { + CapsuleToast.show('作业执行中,已保留当前任务路线', showCheck: false); + return; + } + if (await _selectRoute(route) && mounted) { + if (_phase == WorkRunState.finished || _phase == WorkRunState.idle) { + _finishTraceTimer?.cancel(); + setState(() { + _session = null; + _phase = WorkRunState.idle; + _startedByUser = false; + _clearTraceSignal++; + }); + } + CapsuleToast.show('已选择路线:${route.name}'); + } } /// 选中一条路线:先把已知几何画上(有就立即显示),没有再去查详情补全。 /// 对应路径规划「点列表项 → 清掉旧线 → 解析 path/outer → 画线 → 定位 21 级」。 /// 返回 false 表示这条路线确实没有可画的数据。 Future _selectRoute(WorkRouteOption route) async { + final generation = ++_routeLoadGeneration; setState(() => _route = route); if (route.hasGeometry) return true; final result = await _gateway.resolveRouteGeometry( deviceId: _deviceId, route: route, ); - if (!mounted) return false; + if (!mounted || generation != _routeLoadGeneration) return false; final hydrated = result.data; if (hydrated != null && hydrated.hasGeometry) { setState(() => _route = hydrated); @@ -220,13 +423,18 @@ class _RobotAutonomousWorkPageV2State extends State { /// 按 routeId 从路线库取回完整几何(轨迹 + 边界框) Future _restoreRoute(String routeId) async { + final taskId = _session?.taskId; _restoringRouteId = routeId; try { final scope = await _gateway.resolveScope(deviceId: _deviceId); final siteId = scope.siteId; if (siteId == null) return; final result = await _gateway.loadRoutes(siteId: siteId); - if (!mounted) return; + if (!mounted || + _session?.taskId != taskId || + _session?.routeId != routeId) { + return; + } for (final route in result.data ?? const []) { if (route.id == routeId) { await _selectRoute(route); @@ -265,15 +473,12 @@ class _RobotAutonomousWorkPageV2State extends State { final ok = await showWorkConfirmDialog( context, title: '开始作业 · 二次确认', - desc: '路线:${route.name}' + desc: + '路线:${route.name}' '${route.metaText != null ? ' · ${route.metaText}' : ''}' '${route.hasGeometry ? '' : '(无路径数据,可能下发失败)'}' '。确认后开始执行。', - checks: const [ - '设备已上电、控制权符合要求', - '定位有效,路线及电子围栏匹配', - '作业区域安全,割刀与急停互锁有效', - ], + checks: const ['设备已上电、控制权符合要求', '定位有效,路线及电子围栏匹配', '作业区域安全,割刀与急停互锁有效'], confirmLabel: '确认开始作业', ); if (ok != true || !mounted) return; @@ -284,6 +489,10 @@ class _RobotAutonomousWorkPageV2State extends State { if (!mounted) return; WorkTaskSnapshot? reuse; + if (!pooled.isOk) { + CapsuleToast.show(pooled.error ?? '任务查询失败', showCheck: false); + return; + } final candidates = pooled.data ?? const []; if (candidates.isNotEmpty) { if (_gateway.capabilities.multiActiveTaskResolve && @@ -296,9 +505,7 @@ class _RobotAutonomousWorkPageV2State extends State { } // ② 已有任务跑的是别的路线:不静默覆盖,交给用户先处理 - if (reuse != null && - reuse.routeId != null && - reuse.routeId != route.id) { + if (reuse != null && reuse.routeId != null && reuse.routeId != route.id) { CapsuleToast.show( '设备上已有其它路线的任务(${reuse.routeName ?? reuse.routeId}),请先在「任务队列」处理', showCheck: false, @@ -320,7 +527,7 @@ class _RobotAutonomousWorkPageV2State extends State { return; } - _startedByUser = true; + _startedByUser = result.data?.state == WorkRunState.queued; _applySession(result.data); CapsuleToast.show('作业已下发:${route.name}'); }); @@ -336,11 +543,7 @@ class _RobotAutonomousWorkPageV2State extends State { context, title: '暂停作业 · 确认', desc: '暂停后保留当前任务ID、路线及作业断点;机器人需要先完成安全停止。', - checks: const [ - '行驶速度降至安全状态', - '割刀按任务安全暂停策略停转', - '保存断点位置与已覆盖区域', - ], + checks: const ['行驶速度降至安全状态', '割刀按任务安全暂停策略停转', '保存断点位置与已覆盖区域'], confirmLabel: '确认暂停', ); if (ok != true || !mounted) return; @@ -367,11 +570,7 @@ class _RobotAutonomousWorkPageV2State extends State { context, title: '停止作业 · 二次确认', desc: '本次任务将被终止,不会自动续作。已完成区域、轨迹与停止时间会保存到作业记录。', - checks: const [ - '先安全停止底盘与割刀', - '记录人工停止原因与实际完成区域', - '已终止任务不能用“继续”恢复', - ], + checks: const ['先安全停止底盘与割刀', '记录人工停止原因与实际完成区域', '已终止任务不能用“继续”恢复'], confirmLabel: '确认停止作业', danger: true, ); @@ -402,26 +601,19 @@ class _RobotAutonomousWorkPageV2State extends State { context, title: '继续 / 断点续作 · 确认', desc: '继续原任务「$_routeName」,沿用原路线与已保存的覆盖区域。', - checks: const [ - '暂停或故障原因已消除', - '任务ID、地图、路线与断点匹配', - '定位质量、控制权限、割刀与底盘状态合格', - ], + checks: const ['暂停或故障原因已消除', '任务ID、地图、路线与断点匹配', '定位质量、控制权限、割刀与底盘状态合格'], confirmLabel: '确认继续作业', ); if (ok != true || !mounted) return; await _runBusy(() async { - final result = await _gateway.resumeWork( - deviceId: _deviceId, - task: task, - ); + final result = await _gateway.resumeWork(deviceId: _deviceId, task: task); if (!mounted) return; if (!result.isOk) { CapsuleToast.show(result.error ?? '继续失败', showCheck: false); return; } - _startedByUser = true; + _startedByUser = result.data?.state == WorkRunState.queued; _applySession(result.data); CapsuleToast.show('已下发继续指令'); }); @@ -517,9 +709,17 @@ class _RobotAutonomousWorkPageV2State extends State { return; } // 进「地图与区域编辑」:本页已是横屏,直接推入(自定义淡入 + 极轻缩放转场) - await Navigator.of( - context, - ).push(RouteEditorPageV2.route(deviceSn: _deviceId)); + // exitToLandscape:本页是横屏锁定页,编辑器退出时必须把方向交还横屏, + // 否则它会恢复系统方向,本页被转成竖屏、布局全部错位。 + await Navigator.of(context).push( + RouteEditorPageV2.route(deviceSn: _deviceId, exitToLandscape: true), + ); + // 兜底:无论以何种方式返回,都重新锁定横屏 + if (mounted) { + _enterLandscape(); + _statusBloc.startMqttRealtimeListening(_deviceId); + unawaited(_bootstrap()); + } return; } @@ -530,6 +730,15 @@ class _RobotAutonomousWorkPageV2State extends State { return; } if (await _selectRoute(route) && mounted) { + if (_phase == WorkRunState.finished || _phase == WorkRunState.idle) { + _finishTraceTimer?.cancel(); + setState(() { + _session = null; + _phase = WorkRunState.idle; + _startedByUser = false; + _clearTraceSignal++; + }); + } CapsuleToast.show('已选择路线:${route.name}'); } } @@ -543,7 +752,7 @@ class _RobotAutonomousWorkPageV2State extends State { } void _openRecordPanel() { - showWorkRecordPanel(context); + showWorkRecordPanel(context, deviceId: _deviceId); } void _openSettingPanel() { @@ -671,6 +880,18 @@ class _RobotAutonomousWorkPageV2State extends State { // ===== 页面骨架 ===== @override Widget build(BuildContext context) { + // 自主作业的控制模式弹窗暂时停用,保留代码供后续恢复。 + // return ControlModeEntryGate( + // key: ValueKey('autonomous-mode/$_deviceId'), + // deviceId: _deviceId, + // target: ControlModeTarget.autonomous, + // onReady: _openEntryPanelIfNeeded, + // child: _buildPage(context), + // ); + return _buildPage(context); + } + + Widget _buildPage(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFF26331F), body: Stack( @@ -683,7 +904,19 @@ class _RobotAutonomousWorkPageV2State extends State { previewRoute: _previewRoute, previewOuter: _previewOuter, previewCoverage: _previewCoverage, - previewSignature: _route?.id ?? '', + previewSignature: '${_route?.id ?? ''}/${_session?.taskId ?? ''}', + expectedTaskId: _session?.taskId, + traceDiagnosticsEnabled: true, + recordLocationTrace: false, + taskTrace: WorkTraceInput( + taskId: _session?.taskId, + completedPoints: _session?.completedPoints ?? const [], + recording: _session?.canRecordTrace ?? false, + visible: + _phase == WorkRunState.running || + _phase == WorkRunState.paused, + ), + clearTraceSignal: _clearTraceSignal, ), ), // 不用 SafeArea:本页横屏全屏,需铺满到挖孔/手势条区域 @@ -695,6 +928,13 @@ class _RobotAutonomousWorkPageV2State extends State { constraints.biggest, cutout: MediaQuery.viewPaddingOf(context), ); + // 兜底自愈:本页只支持横屏。宽 < 高说明方向被解锁后系统转成了竖屏, + // 按竖屏尺寸算出的顶栏 / 栏位会全部错位,这里把方向拉回横屏(下一帧生效)。 + if (constraints.maxWidth < constraints.maxHeight) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _enterLandscape(); + }); + } return Stack( children: [ _buildTopBar(m), @@ -721,10 +961,7 @@ class _RobotAutonomousWorkPageV2State extends State { top: 0, height: m.topBarH, child: Container( - padding: EdgeInsets.only( - left: m.topPad, - right: m.topPad, - ), + padding: EdgeInsets.only(left: m.topPad, right: m.topPad), decoration: const BoxDecoration( color: Colors.white, border: Border(bottom: BorderSide(color: Color(0xFFE3EAF3))), @@ -777,16 +1014,24 @@ class _RobotAutonomousWorkPageV2State extends State { ), ), SizedBox(width: m.topGap), - _rtkPill(m), - if (!m.topTight) ...[ - SizedBox(width: m.topGap), - _signalPill(m), - ], + BlocBuilder( + bloc: _statusBloc, + builder: (context, ds) => _rtkPill(m, _statusOf(ds)?.qual), + ), + if (!m.topTight) ...[SizedBox(width: m.topGap), _signalPill(m)], SizedBox(width: m.topGap), - _batteryPill(m), + BlocBuilder( + bloc: _statusBloc, + builder: (context, ds) => + _batteryPill(m, _batteryOf(_statusOf(ds))), + ), if (!m.topTight) ...[ SizedBox(width: m.topGap), - _speedPill(m), + BlocBuilder( + bloc: _statusBloc, + builder: (context, ds) => + _speedPill(m, _speedOf(_statusOf(ds))), + ), ], SizedBox(width: m.topGap), _statusDetailButton(m), @@ -905,23 +1150,26 @@ class _RobotAutonomousWorkPageV2State extends State { ); } - Widget _rtkPill(_WorkLayout m) { + Widget _rtkPill(_WorkLayout m, int? qual) { final k = m.topK; + final fixed = _isRtkFixed(qual); + final fg = qual == null ? kWtGray2 : (fixed ? kWtGreen : kWtWarn); + final bg = qual == null ? kWtSegBg : (fixed ? kWtGreenBg : kWtWarnBg); return Container( height: (32 * k).clamp(22.0, 32.0), padding: EdgeInsets.symmetric(horizontal: (13 * k).clamp(8.0, 13.0)), alignment: Alignment.center, decoration: BoxDecoration( - color: kWtGreenBg, + color: bg, borderRadius: BorderRadius.circular(999), ), child: Text( - 'RTK 固定解', + _qualText(qual), style: TextStyle( fontSize: (12.5 * k).clamp(9.5, 12.5), height: 1.1, fontWeight: FontWeight.w700, - color: kWtGreen, + color: fg, ), ), ); @@ -951,20 +1199,23 @@ class _RobotAutonomousWorkPageV2State extends State { ); } - Widget _batteryPill(_WorkLayout m) { + Widget _batteryPill(_WorkLayout m, double? pct) { final k = m.topK; - final pct = widget.robot.battery.round(); + // 只展实时电量(host/realTime/message);未收到数据时显示“未知”, + // 不回退设备列表的静态电量(列表值常为默认 100%,会误导)。 + final fg = pct == null ? kWtMuted : (pct > 20 ? kWtGreen : kWtDanger); + final text = pct == null ? '未知' : '${pct.round()}%'; return Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.battery_5_bar_rounded, size: (16 * k).clamp(11.0, 16.0), - color: kWtGreen, + color: fg, ), SizedBox(width: (4 * k).clamp(2.0, 4.0)), Text( - '$pct%', + text, style: TextStyle( fontSize: m.topStatusFont, height: 1.1, @@ -976,10 +1227,12 @@ class _RobotAutonomousWorkPageV2State extends State { ); } - Widget _speedPill(_WorkLayout m) { - final speed = _phase.isRunning ? '0.6 m/s' : '0.0 m/s'; + Widget _speedPill(_WorkLayout m, double? speed) { + final text = speed == null + ? '-- m/s' + : '${speed.abs().toStringAsFixed(1)} m/s'; return Text( - speed, + text, style: TextStyle( fontSize: m.topStatusFont, height: 1.1, @@ -1271,33 +1524,56 @@ class _RobotAutonomousWorkPageV2State extends State { constraints: BoxConstraints(maxHeight: m.infoRowsMaxH), child: SingleChildScrollView( padding: EdgeInsets.fromLTRB(px, m.tiny ? 3 : 6, px, 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _kvRow( - m, - '设备运行', - _runText(), - color: _phase.isPaused - ? kWtWarn - : (_phase.isRunning ? kWtGreen : kWtNavy), - ), - _kvRow(m, '当前任务', _taskText()), - _kvRow(m, '电量', '${widget.robot.battery.round()}%'), - _kvRow(m, '定位', 'RTK 固定解', color: kWtGreen), - _kvRow( - m, - '割刀状态', - _phase.isRunning ? '运行中' : '已停止', - color: _phase.isRunning ? kWtGreen : kWtNavy, - ), - _kvRow( - m, - '行驶速度', - _phase.isRunning ? '0.6 m/s' : '0.0 m/s', - ), - _kvRow(m, '当前故障', '0 项', color: kWtGreen), - ], + child: BlocBuilder( + bloc: _statusBloc, + builder: (context, ds) { + final st = _statusOf(ds); + final fixed = _isRtkFixed(st?.qual); + final knife = _knifeRpmOf(st); + final speed = _speedOf(st); + final battery = _batteryOf(st); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _kvRow( + m, + '设备运行', + _runText(), + color: _phase.isPaused + ? kWtWarn + : (_phase.isRunning ? kWtGreen : kWtNavy), + ), + _kvRow(m, '当前任务', _taskText()), + _kvRow( + m, + '电量', + battery == null ? '未知' : '${battery.round()}%', + ), + _kvRow( + m, + '定位', + _qualText(st?.qual), + color: fixed ? kWtGreen : kWtNavy, + ), + _kvRow( + m, + '割刀状态', + knife == null ? '未知' : (knife > 0 ? '运行中' : '已停止'), + color: (knife != null && knife > 0) + ? kWtGreen + : kWtNavy, + ), + _kvRow( + m, + '行驶速度', + speed == null + ? '-- m/s' + : '${speed.abs().toStringAsFixed(1)} m/s', + ), + _kvRow(m, '当前故障', '0 项', color: kWtGreen), + ], + ); + }, ), ), ), @@ -1692,7 +1968,7 @@ class _RobotAutonomousWorkPageV2State extends State { if (task == null) { switch (_phase) { case WorkRunState.finished: - return '本次作业已结束 · 作业记录已保存'; + return '本次作业已结束'; case WorkRunState.running: return '任务执行中'; default: @@ -1722,7 +1998,11 @@ class _RobotAutonomousWorkPageV2State extends State { case WorkRunState.failed: return ('异常中断', kWtWarnBg, kWtWarn); case WorkRunState.finished: - return ('已结束 · 已归档', kWtGreenBg, kWtGreen); + return ( + _session?.rawStatus == 'FINISH' ? '已完成' : '已结束', + kWtGreenBg, + kWtGreen, + ); } } @@ -1733,12 +2013,7 @@ class _RobotAutonomousWorkPageV2State extends State { switch (_phase) { case WorkRunState.idle: return [ - ( - '放弃准备', - null, - 2, - () => Navigator.of(context).maybePop(), - ), + ('放弃准备', null, 2, () => Navigator.of(context).maybePop()), if (caps.routeLibrary) ('预览 / 选择路线', null, 2, _openRouteLibrary), ('开始作业', Icons.play_arrow_rounded, 0, _startWork), ]; diff --git a/lib/features/v2/device_list/presentation/pages/robot_fault_center_page_v2.dart b/lib/features/v2/device_list/presentation/pages/robot_fault_center_page_v2.dart index 01028a1c..52da990d 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_fault_center_page_v2.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_fault_center_page_v2.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; import 'package:maibu_satabot_v2/components/capsule_toast.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import '../../data/models/robot_data_model.dart'; import '../widgets/robot_summary_card_v2.dart'; import 'robot_sensor_page_v2.dart'; /// 一条故障码说明(诊断支持 → 故障码说明) +/// +/// name / desc / impact / advice 均为语言包 key,渲染时再取词 class _FaultCode { final String code; final String name; @@ -26,6 +29,8 @@ class _FaultCode { } /// 一条故障事件(已恢复 / 当前) +/// +/// name / time 为语言包 key class _FaultEvent { final String code; final String name; @@ -74,19 +79,19 @@ class _RobotFaultCenterPageV2State extends State { static const List<_FaultCode> _faultCodes = [ _FaultCode( code: 'C201', - name: '4G信号短时中断', + name: 'robot_fault_v2.fc_c201_name', icon: Icons.wifi_off_rounded, - desc: '设备与云端之间的 4G 链路在短时间内断开,重连成功后自动恢复。', - impact: '影响:远程指令下发与状态上报延迟,作业本身不受影响。', - advice: '排查:确认作业区域 4G 覆盖;检查天线连接;频繁出现时更换运营商卡。', + desc: 'robot_fault_v2.fc_c201_desc', + impact: 'robot_fault_v2.fc_c201_impact', + advice: 'robot_fault_v2.fc_c201_advice', ), _FaultCode( code: 'U108', - name: '超声波短时通信异常', + name: 'robot_fault_v2.fc_u108_name', icon: Icons.settings_input_antenna_rounded, - desc: '超声波探头的串口通信出现短暂丢包,链路恢复后自动解除。', - impact: '影响:避障距离刷新滞后,期间低速作业仍可继续。', - advice: '排查:检查探头线束与接插件是否松动;清洁探头表面;必要时更换探头。', + desc: 'robot_fault_v2.fc_u108_desc', + impact: 'robot_fault_v2.fc_u108_impact', + advice: 'robot_fault_v2.fc_u108_advice', ), ]; @@ -94,14 +99,14 @@ class _RobotFaultCenterPageV2State extends State { static const List<_FaultEvent> _recoveredEvents = [ _FaultEvent( code: 'C201', - name: '4G信号短时中断', - time: '10月19日 18:23', + name: 'robot_fault_v2.fc_c201_name', + time: 'robot_fault_v2.ev_time_1', icon: Icons.wifi_off_rounded, ), _FaultEvent( code: 'U108', - name: '超声波短时通信异常', - time: '10月17日 15:42', + name: 'robot_fault_v2.fc_u108_name', + time: 'robot_fault_v2.ev_time_2', icon: Icons.settings_input_antenna_rounded, ), ]; @@ -125,7 +130,10 @@ class _RobotFaultCenterPageV2State extends State { return '${two(now.hour)}:${two(now.minute)}:${two(now.second)}'; } - void _todo() => CapsuleToast.show('功能暂未开发', showCheck: false); + void _todo() => CapsuleToast.show( + AppLocalizations.of(context).translate('robot_fault_v2.todo'), + showCheck: false, + ); void _openSensorPage() { Navigator.push( @@ -135,7 +143,8 @@ class _RobotFaultCenterPageV2State extends State { } /// 已恢复事件 / 故障码说明共用的弹窗 - void _showFaultDetail(_FaultCode code, {String? recoveredAt}) { + void _showFaultDetail(_FaultCode code, {String? recoveredAtKey}) { + final l10n = AppLocalizations.of(context); showModalBottomSheet( context: context, backgroundColor: Colors.transparent, @@ -168,24 +177,26 @@ class _RobotFaultCenterPageV2State extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '${code.code} · ${code.name}', + '${code.code} · ${l10n.translate(code.name)}', style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w800, color: _navy, ), ), - if (recoveredAt != null) ...[ + if (recoveredAtKey != null) ...[ const SizedBox(height: 3), Text( - '$recoveredAt · 已恢复', + l10n + .translate('robot_fault_v2.recovered_suffix') + .replaceAll('%s', l10n.translate(recoveredAtKey)), style: const TextStyle(fontSize: 12, color: _gray), ), ], ], ), ), - if (recoveredAt != null) + if (recoveredAtKey != null) Container( padding: const EdgeInsets.symmetric( horizontal: 9, @@ -195,9 +206,9 @@ class _RobotFaultCenterPageV2State extends State { color: _greenBg, borderRadius: BorderRadius.circular(10), ), - child: const Text( - '已恢复', - style: TextStyle( + child: Text( + l10n.translate('robot_fault_v2.badge_recovered'), + style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: _green, @@ -207,9 +218,15 @@ class _RobotFaultCenterPageV2State extends State { ], ), const SizedBox(height: 14), - _buildSheetLine('问题说明', code.desc), + _buildSheetLine( + l10n.translate('robot_fault_v2.sheet_problem'), + l10n.translate(code.desc), + ), const SizedBox(height: 10), - _buildSheetLine('影响与建议', '${code.impact}\n${code.advice}'), + _buildSheetLine( + l10n.translate('robot_fault_v2.sheet_impact'), + '${l10n.translate(code.impact)}\n${l10n.translate(code.advice)}', + ), const SizedBox(height: 14), GestureDetector( onTap: () => Navigator.of(sheetCtx).pop(), @@ -221,9 +238,9 @@ class _RobotFaultCenterPageV2State extends State { color: _softBlue, borderRadius: BorderRadius.circular(12), ), - child: const Text( - '知道了', - style: TextStyle( + child: Text( + l10n.translate('robot_fault_v2.sheet_ok'), + style: const TextStyle( fontSize: 14.5, fontWeight: FontWeight.w700, color: _blue, @@ -260,6 +277,7 @@ class _RobotFaultCenterPageV2State extends State { } void _openFaultCodeList() { + final l10n = AppLocalizations.of(context); showModalBottomSheet( context: context, backgroundColor: Colors.transparent, @@ -275,18 +293,18 @@ class _RobotFaultCenterPageV2State extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '故障码说明', - style: TextStyle( + Text( + l10n.translate('robot_fault_v2.sheet_codes_title'), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w800, color: _navy, ), ), const SizedBox(height: 4), - const Text( - '点击任意故障码查看问题说明、影响与排查建议', - style: TextStyle(fontSize: 11.5, color: _hint), + Text( + l10n.translate('robot_fault_v2.sheet_codes_hint'), + style: const TextStyle(fontSize: 11.5, color: _hint), ), const SizedBox(height: 12), for (var i = 0; i < _faultCodes.length; i++) @@ -321,7 +339,7 @@ class _RobotFaultCenterPageV2State extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '${_faultCodes[i].code} · ${_faultCodes[i].name}', + '${_faultCodes[i].code} · ${l10n.translate(_faultCodes[i].name)}', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( @@ -332,7 +350,7 @@ class _RobotFaultCenterPageV2State extends State { ), const SizedBox(height: 3), Text( - _faultCodes[i].desc, + l10n.translate(_faultCodes[i].desc), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( @@ -365,6 +383,7 @@ class _RobotFaultCenterPageV2State extends State { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); return Scaffold( backgroundColor: const Color(0xFFEDF6FD), body: Container( @@ -387,19 +406,25 @@ class _RobotFaultCenterPageV2State extends State { children: [ RobotSummaryCardV2(robot: widget.robot), const SizedBox(height: 18), - _buildSectionTitle('当前故障与预警'), + _buildSectionTitle( + l10n.translate('robot_fault_v2.sec_current'), + ), const SizedBox(height: 10), _buildCurrentFaultCard(), const SizedBox(height: 18), - _buildSectionTitle('故障筛选'), + _buildSectionTitle( + l10n.translate('robot_fault_v2.sec_filter'), + ), const SizedBox(height: 10), _buildFilterBar(), const SizedBox(height: 18), - _buildSectionTitle(_listTitle), + _buildSectionTitle(l10n.translate(_listTitleKey)), const SizedBox(height: 10), _buildEventList(), const SizedBox(height: 18), - _buildSectionTitle('诊断支持'), + _buildSectionTitle( + l10n.translate('robot_fault_v2.sec_support'), + ), const SizedBox(height: 10), _buildSupportCard(), const SizedBox(height: 14), @@ -446,12 +471,12 @@ class _RobotFaultCenterPageV2State extends State { ), ), const SizedBox(width: 12), - const Expanded( + Expanded( child: Text( - '故障诊断中心', + AppLocalizations.of(context).translate('robot_fault_v2.title'), maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle( + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: _navy, @@ -514,12 +539,21 @@ class _RobotFaultCenterPageV2State extends State { // ===== 当前故障卡 ===== Widget _buildCurrentFaultCard() { + final l10n = AppLocalizations.of(context); // 设计稿规则:离线或遥测过期时不得显示「无故障」,只能标注状态未知 - final title = _online ? '当前无未恢复故障' : '当前故障状态未知'; - final subtitle = _online - ? '最新有效上报 · $_checkedAt' - : '设备离线,遥测已中断 · 检查于 $_checkedAt'; - final pillText = _online ? '正常' : '未知'; + final title = l10n.translate( + _online ? 'robot_fault_v2.cur_none' : 'robot_fault_v2.cur_unknown', + ); + final subtitle = l10n + .translate( + _online + ? 'robot_fault_v2.cur_sub_online' + : 'robot_fault_v2.cur_sub_offline', + ) + .replaceAll('%s', _checkedAt); + final pillText = l10n.translate( + _online ? 'robot_fault_v2.st_normal' : 'robot_fault_v2.st_unknown', + ); final pillColor = _online ? _green : _gray; final pillBg = _online ? _greenBg : _softGray; @@ -585,7 +619,14 @@ class _RobotFaultCenterPageV2State extends State { // ===== 故障筛选 ===== Widget _buildFilterBar() { - final labels = ['当前(0)', '已恢复(${_recoveredEvents.length})', '全部记录']; + final l10n = AppLocalizations.of(context); + final labels = [ + l10n.translate('robot_fault_v2.filter_current'), + l10n + .translate('robot_fault_v2.filter_recovered') + .replaceAll('%s', '${_recoveredEvents.length}'), + l10n.translate('robot_fault_v2.filter_all'), + ]; return Container( height: 46, padding: const EdgeInsets.all(4), @@ -635,15 +676,15 @@ class _RobotFaultCenterPageV2State extends State { ); } - String get _listTitle { + String get _listTitleKey { switch (_filter) { case 1: - return '已恢复事件'; + return 'robot_fault_v2.list_recovered'; case 2: - return '全部记录'; + return 'robot_fault_v2.list_all'; case 0: default: - return '最近已恢复事件'; + return 'robot_fault_v2.list_recent_recovered'; } } @@ -664,7 +705,11 @@ class _RobotFaultCenterPageV2State extends State { padding: const EdgeInsets.symmetric(vertical: 30), child: Center( child: Text( - _showEmptyCurrent ? '设备离线,暂无可用记录' : '暂无记录', + AppLocalizations.of(context).translate( + _showEmptyCurrent + ? 'robot_fault_v2.empty_offline' + : 'robot_fault_v2.empty', + ), style: const TextStyle(fontSize: 13, color: _gray), ), ), @@ -691,12 +736,13 @@ class _RobotFaultCenterPageV2State extends State { } Widget _buildEventRow(_FaultEvent event) { + final l10n = AppLocalizations.of(context); final code = _faultCodes.firstWhere( (e) => e.code == event.code, orElse: () => _faultCodes.first, ); return GestureDetector( - onTap: () => _showFaultDetail(code, recoveredAt: event.time), + onTap: () => _showFaultDetail(code, recoveredAtKey: event.time), behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.fromLTRB(12, 12, 10, 12), @@ -717,7 +763,7 @@ class _RobotFaultCenterPageV2State extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '${event.code} · ${event.name}', + '${event.code} · ${l10n.translate(event.name)}', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( @@ -728,7 +774,9 @@ class _RobotFaultCenterPageV2State extends State { ), const SizedBox(height: 4), Text( - '${event.time} · 已恢复', + l10n + .translate('robot_fault_v2.recovered_suffix') + .replaceAll('%s', l10n.translate(event.time)), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: _gray), @@ -743,9 +791,9 @@ class _RobotFaultCenterPageV2State extends State { color: _greenBg, borderRadius: BorderRadius.circular(10), ), - child: const Text( - '已恢复', - style: TextStyle( + child: Text( + l10n.translate('robot_fault_v2.badge_recovered'), + style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: _green, @@ -762,6 +810,7 @@ class _RobotFaultCenterPageV2State extends State { // ===== 诊断支持 ===== Widget _buildSupportCard() { + final l10n = AppLocalizations.of(context); return Container( decoration: BoxDecoration( color: Colors.white, @@ -772,8 +821,8 @@ class _RobotFaultCenterPageV2State extends State { children: [ _buildSupportRow( icon: Icons.description_outlined, - title: '查看故障码说明', - subtitle: '问题说明、影响和排查建议', + title: l10n.translate('robot_fault_v2.support_codes'), + subtitle: l10n.translate('robot_fault_v2.support_codes_sub'), onTap: _openFaultCodeList, ), const Divider( @@ -785,8 +834,8 @@ class _RobotFaultCenterPageV2State extends State { ), _buildSupportRow( icon: Icons.show_chart_rounded, - title: '传感器与通信实时状态', - subtitle: '确认当前链路和硬件反馈', + title: l10n.translate('robot_fault_v2.support_sensor'), + subtitle: l10n.translate('robot_fault_v2.support_sensor_sub'), onTap: _openSensorPage, ), ], @@ -858,10 +907,9 @@ class _RobotFaultCenterPageV2State extends State { borderRadius: BorderRadius.circular(12), border: Border.all(color: const Color(0xFFD6E9FC)), ), - child: const Text( - '离线或遥测过期时不得显示「无故障」,应标注「当前故障状态未知」。' - '已恢复事件为示例数据,接入故障记录接口后自动替换。', - style: TextStyle(fontSize: 11.5, height: 1.5, color: _hint), + child: Text( + AppLocalizations.of(context).translate('robot_fault_v2.note'), + style: const TextStyle(fontSize: 11.5, height: 1.5, color: _hint), ), ); } diff --git a/lib/features/v2/device_list/presentation/pages/robot_firmware_page_v2.dart b/lib/features/v2/device_list/presentation/pages/robot_firmware_page_v2.dart index 4b7be535..b5263958 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_firmware_page_v2.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_firmware_page_v2.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:maibu_satabot_v2/components/capsule_toast.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import '../../data/models/robot_data_model.dart'; import '../widgets/robot_summary_card_v2.dart'; -/// 一条历史升级记录 +/// 一条历史升级记录(module / time / result 均为语言包 key) class _UpgradeRecord { final String module; final String time; @@ -47,24 +48,24 @@ class _RobotFirmwarePageV2State extends State { static const Color _greenBg = Color(0xFFE5F8EC); static const Color _trackBg = Color(0xFFF1F3F6); - /// 检查状态:待检查 / 检查中 / 已是最新(示例态,无真实 OTA 服务) - String _checkState = '待检查'; + /// 是否正在检查更新(无真实 OTA 服务,仅示例态) + bool _checking = false; static const List<_UpgradeRecord> _records = [ _UpgradeRecord( - module: 'MCU v1.0.3', + module: 'robot_firmware_v2.mod_mcu', time: '2026-10-18 10:36', - result: '成功', + result: 'robot_firmware_v2.rec_success', ), _UpgradeRecord( - module: '通信模块 v1.1.2', + module: 'robot_firmware_v2.mod_comm', time: '2026-09-26 15:12', - result: '成功', + result: 'robot_firmware_v2.rec_success', ), _UpgradeRecord( - module: '图传模块 v1.0.8', + module: 'robot_firmware_v2.mod_video', time: '2026-08-30 09:05', - result: '成功', + result: 'robot_firmware_v2.rec_success', ), ]; @@ -72,23 +73,40 @@ class _RobotFirmwarePageV2State extends State { /// 检查更新:先做前置条件校验,再明确告知 OTA 通道未接入 Future _checkUpdate() async { + final l10n = AppLocalizations.of(context); if (!_online) { - CapsuleToast.show('设备离线,无法检查更新', showCheck: false); + CapsuleToast.show( + l10n.translate('robot_firmware_v2.toast_offline'), + showCheck: false, + ); return; } - if (_checkState == '检查中…') return; - setState(() => _checkState = '检查中…'); + if (_checking) return; + setState(() => _checking = true); await Future.delayed(const Duration(milliseconds: 600)); if (!mounted) return; - setState(() => _checkState = '待检查'); - CapsuleToast.show('OTA 升级服务未接入,暂不返回版本检查结果', showCheck: false); + setState(() => _checking = false); + CapsuleToast.show( + AppLocalizations.of( + context, + ).translate('robot_firmware_v2.toast_ota_todo'), + showCheck: false, + ); } - void _openRecord(_UpgradeRecord record) => - CapsuleToast.show('${record.module} 升级详情暂未开放', showCheck: false); + void _openRecord(_UpgradeRecord record) { + final l10n = AppLocalizations.of(context); + CapsuleToast.show( + l10n + .translate('robot_firmware_v2.record_open_tip') + .replaceAll('%s', l10n.translate(record.module)), + showCheck: false, + ); + } @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); return Scaffold( backgroundColor: const Color(0xFFEDF6FD), body: Container( @@ -111,19 +129,27 @@ class _RobotFirmwarePageV2State extends State { children: [ RobotSummaryCardV2(robot: widget.robot), const SizedBox(height: 18), - _buildSectionTitle('当前版本'), + _buildSectionTitle( + l10n.translate('robot_firmware_v2.sec_version'), + ), const SizedBox(height: 10), _buildVersionCard(), const SizedBox(height: 18), - _buildSectionTitle('升级检查'), + _buildSectionTitle( + l10n.translate('robot_firmware_v2.sec_check'), + ), const SizedBox(height: 10), _buildCheckCard(), const SizedBox(height: 18), - _buildSectionTitle('升级前必须确认'), + _buildSectionTitle( + l10n.translate('robot_firmware_v2.sec_prereq'), + ), const SizedBox(height: 10), _buildPrerequisiteCard(), const SizedBox(height: 18), - _buildSectionTitle('历史升级记录'), + _buildSectionTitle( + l10n.translate('robot_firmware_v2.sec_history'), + ), const SizedBox(height: 10), _buildHistoryCard(), const SizedBox(height: 14), @@ -170,12 +196,14 @@ class _RobotFirmwarePageV2State extends State { ), ), const SizedBox(width: 12), - const Expanded( + Expanded( child: Text( - '软件与固件升级', + AppLocalizations.of( + context, + ).translate('robot_firmware_v2.title'), maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle( + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: _navy, @@ -184,8 +212,12 @@ class _RobotFirmwarePageV2State extends State { ), const SizedBox(width: 8), GestureDetector( - onTap: () => - CapsuleToast.show('升级需设备在线、供电稳定且处于安全停机状态', showCheck: false), + onTap: () => CapsuleToast.show( + AppLocalizations.of( + context, + ).translate('robot_firmware_v2.info_tip'), + showCheck: false, + ), behavior: HitTestBehavior.opaque, child: Container( width: 36, @@ -239,11 +271,12 @@ class _RobotFirmwarePageV2State extends State { // ===== 当前版本:四行只读 KV ===== Widget _buildVersionCard() { - const rows = <(String, String)>[ - ('下位机 MCU', 'v1.0.3'), - ('通信模块', 'v1.1.2'), - ('图传模块', 'v1.0.8'), - ('设备配置版本', 'CFG-20261018'), + final l10n = AppLocalizations.of(context); + final rows = <(String, String)>[ + (l10n.translate('robot_firmware_v2.row_mcu'), 'v1.0.3'), + (l10n.translate('robot_firmware_v2.row_comm'), 'v1.1.2'), + (l10n.translate('robot_firmware_v2.row_video'), 'v1.0.8'), + (l10n.translate('robot_firmware_v2.row_cfg'), 'CFG-20261018'), ]; return Container( padding: const EdgeInsets.symmetric(horizontal: 14), @@ -288,7 +321,8 @@ class _RobotFirmwarePageV2State extends State { // ===== 升级检查:标题行 + 状态药丸 + 通栏按钮 ===== Widget _buildCheckCard() { - final checking = _checkState == '检查中…'; + final l10n = AppLocalizations.of(context); + final checking = _checking; return Container( padding: const EdgeInsets.fromLTRB(14, 14, 14, 14), decoration: BoxDecoration( @@ -315,24 +349,24 @@ class _RobotFirmwarePageV2State extends State { ), ), const SizedBox(width: 12), - const Expanded( + Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '系统更新检查', - style: TextStyle( + l10n.translate('robot_firmware_v2.check_title'), + style: const TextStyle( fontSize: 14.5, fontWeight: FontWeight.w800, color: _navy, ), ), - SizedBox(height: 3), + const SizedBox(height: 3), Text( - '需确认设备在线、供电稳定且安全停机', + l10n.translate('robot_firmware_v2.check_sub'), maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 11.5, color: _gray), + style: const TextStyle(fontSize: 11.5, color: _gray), ), ], ), @@ -348,7 +382,11 @@ class _RobotFirmwarePageV2State extends State { borderRadius: BorderRadius.circular(10), ), child: Text( - _checkState, + l10n.translate( + checking + ? 'robot_firmware_v2.check_checking' + : 'robot_firmware_v2.check_idle', + ), style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w700, @@ -377,7 +415,11 @@ class _RobotFirmwarePageV2State extends State { ], ), child: Text( - checking ? '检查中…' : '检查可用更新', + l10n.translate( + checking + ? 'robot_firmware_v2.check_checking' + : 'robot_firmware_v2.check_btn', + ), style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w700, @@ -393,11 +435,12 @@ class _RobotFirmwarePageV2State extends State { // ===== 升级前必须确认:勾选清单 ===== Widget _buildPrerequisiteCard() { - const items = [ - '机器人处于安全停机/待机状态', - '割刀与底盘执行机构已停止', - '电源充足且通信稳定', - '当前账号具有升级权限', + final l10n = AppLocalizations.of(context); + const itemKeys = [ + 'robot_firmware_v2.prereq_1', + 'robot_firmware_v2.prereq_2', + 'robot_firmware_v2.prereq_3', + 'robot_firmware_v2.prereq_4', ]; return Container( padding: const EdgeInsets.fromLTRB(14, 6, 14, 6), @@ -408,7 +451,7 @@ class _RobotFirmwarePageV2State extends State { ), child: Column( children: [ - for (final text in items) + for (final key in itemKeys) Padding( padding: const EdgeInsets.symmetric(vertical: 11), child: Row( @@ -417,7 +460,7 @@ class _RobotFirmwarePageV2State extends State { const SizedBox(width: 9), Expanded( child: Text( - text, + l10n.translate(key), style: const TextStyle(fontSize: 13, color: _navy), ), ), @@ -431,6 +474,7 @@ class _RobotFirmwarePageV2State extends State { // ===== 历史升级记录 ===== Widget _buildHistoryCard() { + final l10n = AppLocalizations.of(context); return Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( @@ -474,7 +518,7 @@ class _RobotFirmwarePageV2State extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - _records[i].module, + l10n.translate(_records[i].module), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( @@ -485,7 +529,7 @@ class _RobotFirmwarePageV2State extends State { ), const SizedBox(height: 3), Text( - '${_records[i].time} · ${_records[i].result}', + '${_records[i].time} · ${l10n.translate(_records[i].result)}', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( @@ -506,9 +550,9 @@ class _RobotFirmwarePageV2State extends State { color: _greenBg, borderRadius: BorderRadius.circular(10), ), - child: const Text( - '已完成', - style: TextStyle( + child: Text( + l10n.translate('robot_firmware_v2.rec_done'), + style: const TextStyle( fontSize: 10.5, fontWeight: FontWeight.w600, color: _green, @@ -545,8 +589,7 @@ class _RobotFirmwarePageV2State extends State { const SizedBox(width: 7), Expanded( child: Text( - '版本号与历史记录为示例口径;设备端版本接口与 OTA 通道接入后自动替换为真实数据,' - '升级动作会先做二次确认再下发。', + AppLocalizations.of(context).translate('robot_firmware_v2.footer'), style: TextStyle( fontSize: 11.5, height: 1.55, diff --git a/lib/features/v2/device_list/presentation/pages/robot_full_info_page_v2.dart b/lib/features/v2/device_list/presentation/pages/robot_full_info_page_v2.dart index 33e6391e..52635f3c 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_full_info_page_v2.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_full_info_page_v2.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:maibu_satabot_v2/components/capsule_toast.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import '../../data/models/robot_data_model.dart'; import '../widgets/robot_summary_card_v2.dart'; @@ -42,11 +43,14 @@ class RobotFullInfoPageV2 extends StatelessWidget { } /// SN 脱敏:保留前 4 位与后 2 位 - String get _maskedSn { + String _maskedSnLabel(AppLocalizations l10n) { final sn = robot.id; if (sn.isEmpty) return '--'; if (sn.length <= 6) return sn; - return '${sn.substring(0, 4)} · · · · ${sn.substring(sn.length - 2)}(脱敏)'; + return l10n + .translate('robot_full_info_v2.sn_masked') + .replaceFirst('%s', sn.substring(0, 4)) + .replaceFirst('%s', sn.substring(sn.length - 2)); } /// 电量:0 视为未上报 @@ -56,17 +60,27 @@ class RobotFullInfoPageV2 extends StatelessWidget { return '${b.round()}%'; } - /// 运行模式:任务字段为“无/空”时按待机展示 + /// 运行模式:任务字段为“无/空”时按待机展示(返回空串表示待机) String get _modeText { final task = robot.task.trim(); - if (task.isEmpty || task == '无' || task == 'null') return '待机'; + // '无' / 'null' 为接口约定原值,不翻译 + if (task.isEmpty || task == '无' || task == 'null') return ''; return task; } - void _todo() => CapsuleToast.show('功能暂未开发', showCheck: false); + String _modeLabel(AppLocalizations l10n) { + final mode = _modeText; + return mode.isEmpty ? l10n.translate('robot_full_info_v2.mode_idle') : mode; + } + + void _todo(BuildContext context) => CapsuleToast.show( + AppLocalizations.of(context).translate('robot_full_info_v2.todo'), + showCheck: false, + ); @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); return Scaffold( backgroundColor: const Color(0xFFEDF6FD), body: Container( @@ -89,18 +103,23 @@ class RobotFullInfoPageV2 extends StatelessWidget { children: [ RobotSummaryCardV2(robot: robot), const SizedBox(height: 18), - _buildSectionTitle('机器人基础资料'), + _buildSectionTitle( + l10n.translate('robot_full_info_v2.sec_profile'), + ), const SizedBox(height: 10), - _buildProfileCard(), + _buildProfileCard(l10n), const SizedBox(height: 18), - _buildSectionTitle('运行与控制信息'), + _buildSectionTitle( + l10n.translate('robot_full_info_v2.sec_runtime'), + ), const SizedBox(height: 10), - _buildRuntimeCard(), + _buildRuntimeCard(l10n), const SizedBox(height: 14), - const Center( + Center( child: Text( - '动力类型、最近上报、控制会话、故障数为示例;接入后自动替换', - style: TextStyle(fontSize: 10.5, color: _hint), + l10n.translate('robot_full_info_v2.footer'), + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 10.5, color: _hint), ), ), ], @@ -145,17 +164,22 @@ class RobotFullInfoPageV2 extends StatelessWidget { ), ), const SizedBox(width: 12), - const Text( - '机器人完整信息', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: _navy, + // 英文标题较长:允许收缩省略,避免顶栏 Row 横向溢出 + Flexible( + child: Text( + AppLocalizations.of(context).translate('robot_full_info_v2.title'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: _navy, + ), ), ), const Spacer(), GestureDetector( - onTap: _todo, + onTap: () => _todo(context), behavior: HitTestBehavior.opaque, child: Container( width: 36, @@ -214,12 +238,16 @@ class RobotFullInfoPageV2 extends StatelessWidget { ), ), const SizedBox(width: 8), - Text( - title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w800, - color: _navy, + Flexible( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: _navy, + ), ), ), ], @@ -227,7 +255,7 @@ class RobotFullInfoPageV2 extends StatelessWidget { } // ===== 基础资料卡 ===== - Widget _buildProfileCard() { + Widget _buildProfileCard(AppLocalizations l10n) { return Container( decoration: BoxDecoration( color: Colors.white, @@ -255,7 +283,9 @@ class RobotFullInfoPageV2 extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '$_model · STD标准版', + l10n + .translate('robot_full_info_v2.model_suffix') + .replaceFirst('%s', _model), maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( @@ -266,15 +296,21 @@ class RobotFullInfoPageV2 extends StatelessWidget { ), ), const SizedBox(height: 5), - const Text( - '独立图传 · 超声波声光 · 基础GNSS', + Text( + l10n.translate('robot_full_info_v2.tagline'), maxLines: 2, overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 10.5, color: _gray), + style: const TextStyle(fontSize: 10.5, color: _gray), ), const SizedBox(height: 7), _buildPill( - _online ? '在线 · $_modeText' : '离线 · $_modeText', + l10n + .translate( + _online + ? 'robot_full_info_v2.pill_online' + : 'robot_full_info_v2.pill_offline', + ) + .replaceFirst('%s', _modeLabel(l10n)), _online ? _green : const Color(0xFF6B7280), _online ? _greenBg : const Color(0xFFF0F2F5), ), @@ -285,19 +321,42 @@ class RobotFullInfoPageV2 extends StatelessWidget { ), ), const Divider(height: 1, color: _divider), - _buildInfoRow('设备名称', _displayName), - _buildInfoRow('设备SN', _maskedSn), - _buildInfoRow('绑定状态', robot.hasHost ? '已授权' : '未绑定'), - _buildInfoRow('所属类型', '履带式割草机器人'), - _buildInfoRow('动力类型', '示例:纯电'), - _buildInfoRow('最近上报', '示例:2026-10-20 09:41', isLast: true), + _buildInfoRow( + l10n.translate('robot_full_info_v2.row_name'), + _displayName, + ), + _buildInfoRow( + l10n.translate('robot_full_info_v2.row_sn'), + _maskedSnLabel(l10n), + ), + _buildInfoRow( + l10n.translate('robot_full_info_v2.row_bind'), + l10n.translate( + robot.hasHost + ? 'robot_full_info_v2.val_bound' + : 'robot_full_info_v2.val_unbound', + ), + ), + _buildInfoRow( + l10n.translate('robot_full_info_v2.row_type'), + l10n.translate('robot_full_info_v2.val_type'), + ), + _buildInfoRow( + l10n.translate('robot_full_info_v2.row_power'), + l10n.translate('robot_full_info_v2.val_power'), + ), + _buildInfoRow( + l10n.translate('robot_full_info_v2.row_report'), + l10n.translate('robot_full_info_v2.val_report'), + isLast: true, + ), ], ), ); } // ===== 运行与控制卡 ===== - Widget _buildRuntimeCard() { + Widget _buildRuntimeCard(AppLocalizations l10n) { return Container( decoration: BoxDecoration( color: Colors.white, @@ -314,23 +373,23 @@ class RobotFullInfoPageV2 extends StatelessWidget { child: _buildStatBlock( Icons.battery_std_rounded, _batteryText, - '能源状态', + l10n.translate('robot_full_info_v2.stat_energy'), ), ), const SizedBox(width: 8), Expanded( child: _buildStatBlock( Icons.monitor_heart_outlined, - _modeText, - '运行模式', + _modeLabel(l10n), + l10n.translate('robot_full_info_v2.stat_mode'), ), ), const SizedBox(width: 8), Expanded( child: _buildStatBlock( Icons.sports_esports_outlined, - '无', - '控制会话', + l10n.translate('robot_full_info_v2.val_none'), + l10n.translate('robot_full_info_v2.stat_session'), ), ), ], @@ -338,11 +397,19 @@ class RobotFullInfoPageV2 extends StatelessWidget { ), const Divider(height: 1, color: _divider), _buildInfoRow( - 'GNSS定位', - _online ? '定位有效' : '无定位', + l10n.translate('robot_full_info_v2.row_gnss'), + l10n.translate( + _online + ? 'robot_full_info_v2.val_gnss_ok' + : 'robot_full_info_v2.val_gnss_none', + ), valueColor: _online ? _green : _gray, ), - _buildInfoRow('当前故障', '0项', isLast: true), + _buildInfoRow( + l10n.translate('robot_full_info_v2.row_fault'), + l10n.translate('robot_full_info_v2.val_fault_count'), + isLast: true, + ), ], ), ); diff --git a/lib/features/v2/device_list/presentation/pages/robot_light_sound_page_v2.dart b/lib/features/v2/device_list/presentation/pages/robot_light_sound_page_v2.dart index 312332e7..e072254e 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_light_sound_page_v2.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_light_sound_page_v2.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:maibu_satabot_v2/components/capsule_toast.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import '../../data/models/robot_data_model.dart'; import '../widgets/robot_summary_card_v2.dart'; @@ -52,12 +53,18 @@ class _RobotLightSoundPageV2State extends State { bool get _online => widget.robot.isOnline; /// 统一的未接入提示:状态仍按操作切换,方便验收界面态 - void _notifyChannel(String action) { + /// [actionKey] 为操作名语言包 key + void _notifyChannel(String actionKey) { + final l10n = AppLocalizations.of(context); + final action = l10n.translate(actionKey); if (!_online) { - CapsuleToast.show('设备离线,$action指令未下发', showCheck: false); + CapsuleToast.show( + l10n.translate('robot_light_v2.offline_tip').replaceAll('%s', action), + showCheck: false, + ); } else { CapsuleToast.show( - '$action指令通道未接入(设备端未开放灯光位),当前状态仅作界面预览', + l10n.translate('robot_light_v2.channel_tip').replaceAll('%s', action), showCheck: false, ); } @@ -65,6 +72,7 @@ class _RobotLightSoundPageV2State extends State { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); return Scaffold( backgroundColor: const Color(0xFFEDF6FD), body: Container( @@ -88,7 +96,7 @@ class _RobotLightSoundPageV2State extends State { RobotSummaryCardV2(robot: widget.robot), const SizedBox(height: 16), _buildSectionTitle( - '灯光控制', + l10n.translate('robot_light_v2.sec_light'), trailing: _buildStatePill( on: _headlight || _warningLight || _markerLight, ), @@ -96,11 +104,15 @@ class _RobotLightSoundPageV2State extends State { const SizedBox(height: 10), _buildLightCard(), const SizedBox(height: 16), - _buildSectionTitle('声光提示'), + _buildSectionTitle( + l10n.translate('robot_light_v2.sec_sound'), + ), const SizedBox(height: 10), _buildSoundCard(), const SizedBox(height: 16), - _buildSectionTitle('自动策略'), + _buildSectionTitle( + l10n.translate('robot_light_v2.sec_auto'), + ), const SizedBox(height: 10), _buildAutoCard(), const SizedBox(height: 16), @@ -146,13 +158,13 @@ class _RobotLightSoundPageV2State extends State { ), ), ), - const Expanded( + Expanded( child: Text( - '灯光与声光', + AppLocalizations.of(context).translate('robot_light_v2.title'), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle( + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w800, color: _navy, @@ -160,8 +172,10 @@ class _RobotLightSoundPageV2State extends State { ), ), GestureDetector( - onTap: () => - CapsuleToast.show('灯光与鸣笛需设备端开放控制通道,当前仅作界面预览', showCheck: false), + onTap: () => CapsuleToast.show( + AppLocalizations.of(context).translate('robot_light_v2.info_tip'), + showCheck: false, + ), behavior: HitTestBehavior.opaque, child: Container( width: 36, @@ -198,15 +212,19 @@ class _RobotLightSoundPageV2State extends State { ), ), const SizedBox(width: 8), - Text( - title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w800, - color: _navy, + Flexible( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: _navy, + ), ), ), - const Spacer(), + const SizedBox(width: 8), ?trailing, ], ); @@ -220,7 +238,9 @@ class _RobotLightSoundPageV2State extends State { borderRadius: BorderRadius.circular(10), ), child: Text( - on ? '灯光已开' : '灯光全关', + AppLocalizations.of(context).translate( + on ? 'robot_light_v2.pill_on' : 'robot_light_v2.pill_off', + ), style: TextStyle( fontSize: 10.5, fontWeight: FontWeight.w700, @@ -248,38 +268,51 @@ class _RobotLightSoundPageV2State extends State { // ===== 灯光控制 ===== Widget _buildLightCard() { + final l10n = AppLocalizations.of(context); return _buildCard( children: [ _buildSwitchRow( icon: Icons.lightbulb_outline, - title: '前照灯', - subtitle: '作业与夜间照明,照亮行进方向', + title: l10n.translate('robot_light_v2.headlight'), + subtitle: l10n.translate('robot_light_v2.headlight_sub'), value: _headlight, onChanged: (v) { setState(() => _headlight = v); - _notifyChannel(v ? '开启前照灯' : '关闭前照灯'); + _notifyChannel( + v + ? 'robot_light_v2.act_on_headlight' + : 'robot_light_v2.act_off_headlight', + ); }, ), const Divider(height: 1, thickness: 0.6, color: _divider), _buildSwitchRow( icon: Icons.warning_amber_rounded, - title: '警示灯', - subtitle: '黄色频闪,作业时提醒周边人员', + title: l10n.translate('robot_light_v2.warning_light'), + subtitle: l10n.translate('robot_light_v2.warning_light_sub'), value: _warningLight, onChanged: (v) { setState(() => _warningLight = v); - _notifyChannel(v ? '开启警示灯' : '关闭警示灯'); + _notifyChannel( + v + ? 'robot_light_v2.act_on_warning' + : 'robot_light_v2.act_off_warning', + ); }, ), const Divider(height: 1, thickness: 0.6, color: _divider), _buildSwitchRow( icon: Icons.light_mode_outlined, - title: '示廓灯', - subtitle: '车体轮廓提示,夜间让他人识别车身', + title: l10n.translate('robot_light_v2.marker_light'), + subtitle: l10n.translate('robot_light_v2.marker_light_sub'), value: _markerLight, onChanged: (v) { setState(() => _markerLight = v); - _notifyChannel(v ? '开启示廓灯' : '关闭示廓灯'); + _notifyChannel( + v + ? 'robot_light_v2.act_on_marker' + : 'robot_light_v2.act_off_marker', + ); }, isLast: true, ), @@ -289,39 +322,60 @@ class _RobotLightSoundPageV2State extends State { // ===== 声光提示 ===== Widget _buildSoundCard() { + final l10n = AppLocalizations.of(context); + final volumeLabels = [ + l10n.translate('robot_light_v2.vol_low'), + l10n.translate('robot_light_v2.vol_mid'), + l10n.translate('robot_light_v2.vol_high'), + ]; + final hornLabels = [ + l10n.translate('robot_light_v2.dur_3'), + l10n.translate('robot_light_v2.dur_5'), + l10n.translate('robot_light_v2.dur_10'), + ]; return _buildCard( children: [ const SizedBox(height: 12), - _buildRowTitle(icon: Icons.volume_up_outlined, title: '报警音量'), + _buildRowTitle( + icon: Icons.volume_up_outlined, + title: l10n.translate('robot_light_v2.volume'), + ), const SizedBox(height: 10), _buildSegBar( - labels: const ['低', '中', '高'], + labels: volumeLabels, index: _volumeIdx, onChanged: (i) { setState(() => _volumeIdx = i); CapsuleToast.show( - '音量档位已设为「${['低', '中', '高'][i]}」(本地偏好,未下发设备)', + l10n + .translate('robot_light_v2.volume_set') + .replaceAll('%s', volumeLabels[i]), showCheck: false, ); }, ), const SizedBox(height: 16), - _buildRowTitle(icon: Icons.schedule_outlined, title: '鸣笛时长'), + _buildRowTitle( + icon: Icons.schedule_outlined, + title: l10n.translate('robot_light_v2.horn_dur'), + ), const SizedBox(height: 10), _buildSegBar( - labels: const ['3 秒', '5 秒', '10 秒'], + labels: hornLabels, index: _hornIdx, onChanged: (i) { setState(() => _hornIdx = i); CapsuleToast.show( - '鸣笛时长已设为「${['3 秒', '5 秒', '10 秒'][i]}」(本地偏好,未下发设备)', + l10n + .translate('robot_light_v2.horn_set') + .replaceAll('%s', hornLabels[i]), showCheck: false, ); }, ), const SizedBox(height: 16), GestureDetector( - onTap: () => _notifyChannel('鸣笛'), + onTap: () => _notifyChannel('robot_light_v2.act_horn'), behavior: HitTestBehavior.opaque, child: Container( height: 44, @@ -337,7 +391,9 @@ class _RobotLightSoundPageV2State extends State { const Icon(Icons.campaign_outlined, size: 18, color: _blue), const SizedBox(width: 6), Text( - '试鸣笛 ${['3 秒', '5 秒', '10 秒'][_hornIdx]}', + l10n + .translate('robot_light_v2.test_horn') + .replaceAll('%s', hornLabels[_hornIdx]), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w700, @@ -355,17 +411,22 @@ class _RobotLightSoundPageV2State extends State { // ===== 自动策略 ===== Widget _buildAutoCard() { + final l10n = AppLocalizations.of(context); return _buildCard( children: [ _buildSwitchRow( icon: Icons.nightlight_outlined, - title: '夜间自动开灯', - subtitle: '光线不足时自动开启前照灯与示廓灯', + title: l10n.translate('robot_light_v2.auto_night'), + subtitle: l10n.translate('robot_light_v2.auto_night_sub'), value: _nightAuto, onChanged: (v) { setState(() => _nightAuto = v); CapsuleToast.show( - '夜间自动开灯${v ? '已启用' : '已关闭'}(本地策略,未下发设备)', + l10n.translate( + v + ? 'robot_light_v2.auto_night_on' + : 'robot_light_v2.auto_night_off', + ), showCheck: false, ); }, @@ -373,13 +434,17 @@ class _RobotLightSoundPageV2State extends State { const Divider(height: 1, thickness: 0.6, color: _divider), _buildSwitchRow( icon: Icons.agriculture_outlined, - title: '作业时开启警示灯', - subtitle: '任务开始自动点亮警示灯,结束后关闭', + title: l10n.translate('robot_light_v2.auto_work'), + subtitle: l10n.translate('robot_light_v2.auto_work_sub'), value: _workWarning, onChanged: (v) { setState(() => _workWarning = v); CapsuleToast.show( - '作业时开启警示灯${v ? '已启用' : '已关闭'}(本地策略,未下发设备)', + l10n.translate( + v + ? 'robot_light_v2.auto_work_on' + : 'robot_light_v2.auto_work_off', + ), showCheck: false, ); }, @@ -387,13 +452,17 @@ class _RobotLightSoundPageV2State extends State { const Divider(height: 1, thickness: 0.6, color: _divider), _buildSwitchRow( icon: Icons.battery_alert_outlined, - title: '低电量声光提醒', - subtitle: '电量低于 20% 时鸣笛并闪烁警示灯', + title: l10n.translate('robot_light_v2.auto_battery'), + subtitle: l10n.translate('robot_light_v2.auto_battery_sub'), value: _lowBatteryAlert, onChanged: (v) { setState(() => _lowBatteryAlert = v); CapsuleToast.show( - '低电量声光提醒${v ? '已启用' : '已关闭'}(本地策略,未下发设备)', + l10n.translate( + v + ? 'robot_light_v2.auto_battery_on' + : 'robot_light_v2.auto_battery_off', + ), showCheck: false, ); }, @@ -547,8 +616,7 @@ class _RobotLightSoundPageV2State extends State { const SizedBox(width: 8), Expanded( child: Text( - '遥控协议暂未开放灯光与鸣笛指令位:本页开关只反映界面状态,' - '音量、鸣笛时长与自动策略为本地偏好。真机控制需设备端开放对应通道。', + AppLocalizations.of(context).translate('robot_light_v2.footer'), style: TextStyle( fontSize: 11.5, height: 1.55, diff --git a/lib/features/v2/device_list/presentation/pages/robot_list_page.dart b/lib/features/v2/device_list/presentation/pages/robot_list_page.dart index 50134239..42442cc4 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_list_page.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_list_page.dart @@ -649,6 +649,7 @@ class _RobotListViewState extends State { id: robot.id, alias: robot.alias, // 传递别名 type: robot.type, + modelText: robot.modelLabelOf(AppLocalizations.of(context)), status: robot.status, battery: robot.battery, task: robot.task, @@ -665,33 +666,35 @@ class _RobotListViewState extends State { '🔴🔴🔴 [选中机器人] hasHost: ${robot.hasHost}', ); - // 🔥 hasHost 字段判定:false → 蓝牙连接流程,true 走原逻辑 - if (!robot.hasHost) { - // hasHost == false 或接口未返回该字段 → 蓝牙连接流程 - debugPrint('⚠️ [RobotListPage] hasHost=false,进入蓝牙连接流程'); - debugPrint('📱 [RobotListPage] 机器人名称: ${robot.name}'); - - // 从设备名中提取时间戳(例如:MC700PRO-CN-JS-1751525588120-0000000A-9527) - final timestamp = _extractTimestamp(robot.name); - if (timestamp == null || timestamp.isEmpty) { - debugPrint('❌ [RobotListPage] 无法从设备名中提取时间戳: ${robot.name}'); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('设备名称异常,暂时无法连接'), - backgroundColor: context.appColors.warning, - ), - ); - return; - } - - debugPrint('✅ [RobotListPage] 提取到时间戳: $timestamp'); - - // 调用蓝牙连接流程 - _connectToDeviceViaBluetooth(context, robot.name, timestamp); - return; - } + // ===== 【暂时停用】hasHost 分层判定 ===== + // 需求:暂时不管有无宿主,点卡片一律直接进设备详情页。 + // 恢复方式:取消下面整段注释即可(原逻辑:hasHost=false → 蓝牙连接流程) + // if (!robot.hasHost) { + // // hasHost == false 或接口未返回该字段 → 蓝牙连接流程 + // debugPrint('⚠️ [RobotListPage] hasHost=false,进入蓝牙连接流程'); + // debugPrint('📱 [RobotListPage] 机器人名称: ${robot.name}'); + // + // // 从设备名中提取时间戳(例如:MC700PRO-CN-JS-1751525588120-0000000A-9527) + // final timestamp = _extractTimestamp(robot.name); + // if (timestamp == null || timestamp.isEmpty) { + // debugPrint('❌ [RobotListPage] 无法从设备名中提取时间戳: ${robot.name}'); + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar( + // content: Text('设备名称异常,暂时无法连接'), + // backgroundColor: context.appColors.warning, + // ), + // ); + // return; + // } + // + // debugPrint('✅ [RobotListPage] 提取到时间戳: $timestamp'); + // + // // 调用蓝牙连接流程 + // _connectToDeviceViaBluetooth(context, robot.name, timestamp); + // return; + // } - // hasHost == true → 走原来的逻辑:前置(setTargetDevice)不变, + // 原 hasHost == true 分支(现在所有设备都走这里):前置(setTargetDevice)不变, // 跳转目标换成新设备详情页(V2 概览设计稿); // 页内铃铛/远程遥控再回旧 RobotControlPage,方便新旧对照。 debugPrint('✅ [RobotListPage] hasHost=true,走正常控制流程'); diff --git a/lib/features/v2/device_list/presentation/pages/robot_location_page_v2.dart b/lib/features/v2/device_list/presentation/pages/robot_location_page_v2.dart index 037f5e1b..75682c3f 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_location_page_v2.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_location_page_v2.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:maibu_satabot_v2/components/capsule_toast.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:maibu_satabot_v2/features/devices/domain/usecases/get_device_location_usecase.dart'; import 'package:maibu_satabot_v2/features/v2/site/presentation/cubit/site_cubit.dart'; @@ -92,10 +93,14 @@ class _RobotLocationPageV2State extends State { return '${v.abs().toStringAsFixed(5)}° ${v >= 0 ? 'N' : 'S'}'; } - void _todo() => CapsuleToast.show('功能暂未开发', showCheck: false); + void _todo() => CapsuleToast.show( + AppLocalizations.of(context).translate('robot_location_v2.todo'), + showCheck: false, + ); @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); return Scaffold( backgroundColor: const Color(0xFFEDF6FD), body: Container( @@ -119,26 +124,36 @@ class _RobotLocationPageV2State extends State { RobotSummaryCardV2(robot: widget.robot), const SizedBox(height: 18), _buildSectionHeader( - '当前位置与地图', - trailing: '定位信息', + l10n.translate('robot_location_v2.sec_current'), + trailing: l10n.translate('robot_location_v2.trailing_loc'), onTrailingTap: _todo, ), const SizedBox(height: 10), _buildMapCard(), const SizedBox(height: 18), - _buildSectionTitle('位置数据'), + _buildSectionTitle( + l10n.translate('robot_location_v2.sec_data'), + ), const SizedBox(height: 10), _buildLocationDataCard(), const SizedBox(height: 14), Center( child: Text( _loading - ? '正在获取设备定位…' + ? l10n.translate('robot_location_v2.note_loading') : (_hasLocation - ? '坐标来自设备实际 GNSS 上报' + ? l10n.translate( + 'robot_location_v2.note_from_device', + ) : (_error != null - ? '定位获取失败:$_error' - : '设备暂未上报有效定位')), + ? l10n + .translate( + 'robot_location_v2.note_error', + ) + .replaceFirst('%s', _error!) + : l10n.translate( + 'robot_location_v2.note_no_fix', + ))), textAlign: TextAlign.center, style: const TextStyle(fontSize: 10.5, color: _hint), ), @@ -185,12 +200,17 @@ class _RobotLocationPageV2State extends State { ), ), const SizedBox(width: 12), - const Text( - '位置与地图详情', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: _navy, + // 英文标题较长:允许收缩省略,避免顶栏 Row 横向溢出 + Flexible( + child: Text( + AppLocalizations.of(context).translate('robot_location_v2.title'), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: _navy, + ), ), ), const Spacer(), @@ -254,12 +274,16 @@ class _RobotLocationPageV2State extends State { ), ), const SizedBox(width: 8), - Text( - title, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w800, - color: _navy, + Flexible( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: _navy, + ), ), ), ], @@ -300,6 +324,7 @@ class _RobotLocationPageV2State extends State { // ===== 地图卡 ===== Widget _buildMapCard() { + final l10n = AppLocalizations.of(context); return Container( decoration: BoxDecoration( color: Colors.white, @@ -350,27 +375,31 @@ class _RobotLocationPageV2State extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - '设备位置', - style: TextStyle( + Text( + l10n.translate('robot_location_v2.map_title'), + style: const TextStyle( fontSize: 13.5, fontWeight: FontWeight.w800, color: _navy, ), ), const SizedBox(height: 3), - const Text( - '地图为示意,定位点来自设备实际GNSS数据', + Text( + l10n.translate('robot_location_v2.map_sub'), maxLines: 2, overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 10.5, color: _gray), + style: const TextStyle(fontSize: 10.5, color: _gray), ), ], ), ), const SizedBox(width: 8), _buildPill( - _hasLocation ? 'GNSS有效' : '无定位', + l10n.translate( + _hasLocation + ? 'robot_location_v2.pill_fix' + : 'robot_location_v2.pill_no_fix', + ), _hasLocation ? _green : const Color(0xFF6B7280), _hasLocation ? _greenBg : const Color(0xFFF0F2F5), ), @@ -412,6 +441,7 @@ class _RobotLocationPageV2State extends State { // ===== 位置数据卡 ===== Widget _buildLocationDataCard() { + final l10n = AppLocalizations.of(context); return Container( decoration: BoxDecoration( color: Colors.white, @@ -420,15 +450,26 @@ class _RobotLocationPageV2State extends State { ), child: Column( children: [ - _buildInfoRow('经度', _lngText), - _buildInfoRow('纬度', _latText), - _buildInfoRow('定位方式', '内置 GNSS'), + _buildInfoRow(l10n.translate('robot_location_v2.row_lng'), _lngText), + _buildInfoRow(l10n.translate('robot_location_v2.row_lat'), _latText), _buildInfoRow( - '定位状态', - _hasLocation ? '有效 · 米级' : '未上报', + l10n.translate('robot_location_v2.row_mode'), + l10n.translate('robot_location_v2.val_mode'), + ), + _buildInfoRow( + l10n.translate('robot_location_v2.row_fix'), + l10n.translate( + _hasLocation + ? 'robot_location_v2.val_fix_ok' + : 'robot_location_v2.val_fix_none', + ), valueColor: _hasLocation ? _green : _gray, ), - _buildInfoRow('卫星信息', '仅显示设备真实上报', isLast: true), + _buildInfoRow( + l10n.translate('robot_location_v2.row_sat'), + l10n.translate('robot_location_v2.val_sat'), + isLast: true, + ), ], ), ); diff --git a/lib/features/v2/device_list/presentation/pages/robot_network_location_page_v2.dart b/lib/features/v2/device_list/presentation/pages/robot_network_location_page_v2.dart index 216a8503..df650a5c 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_network_location_page_v2.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_network_location_page_v2.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:maibu_satabot_v2/components/capsule_toast.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:maibu_satabot_v2/features/devices/domain/usecases/get_device_location_usecase.dart'; import '../../data/models/robot_data_model.dart'; @@ -105,7 +106,10 @@ class _RobotNetworkLocationPageV2State return '${v.abs().toStringAsFixed(5)}° ${v >= 0 ? 'E' : 'W'}'; } - void _todo() => CapsuleToast.show('配置通道未接入', showCheck: false); + void _todo() => CapsuleToast.show( + AppLocalizations.of(context).translate('robot_network_v2.todo'), + showCheck: false, + ); void _openMap() { Navigator.push( @@ -188,12 +192,12 @@ class _RobotNetworkLocationPageV2State ), ), const SizedBox(width: 12), - const Expanded( + Expanded( child: Text( - '网络与定位设置', + AppLocalizations.of(context).translate('robot_network_v2.title'), maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle( + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: _navy, @@ -203,7 +207,7 @@ class _RobotNetworkLocationPageV2State const SizedBox(width: 8), GestureDetector( onTap: () => CapsuleToast.show( - '网络配置以设备端实际能力为准,APP 侧仅展示与只读', + AppLocalizations.of(context).translate('robot_network_v2.info_tip'), showCheck: false, ), behavior: HitTestBehavior.opaque, @@ -235,7 +239,11 @@ class _RobotNetworkLocationPageV2State // ===== 双档切换:网络通信 / GNSS定位 ===== Widget _buildTabBar() { - const labels = ['网络通信', 'GNSS定位']; + final l10n = AppLocalizations.of(context); + final labels = [ + l10n.translate('robot_network_v2.tab_network'), + l10n.translate('robot_network_v2.tab_gnss'), + ]; return Container( height: 46, padding: const EdgeInsets.all(4), @@ -285,58 +293,141 @@ class _RobotNetworkLocationPageV2State // ===== 网络通信 ===== List _buildNetworkTab() { + final l10n = AppLocalizations.of(context); return [ - _buildSectionTitle('当前网络'), + _buildSectionTitle(l10n.translate('robot_network_v2.sec_net_current')), const SizedBox(height: 10), _buildInfoCard([ - ('连接模式', _online ? '4G蜂窝网络' : '未连接'), - ('网络状态', _online ? '在线 · 信号良好' : '离线'), - ('云平台连接', _online ? '正常' : '断开'), - ('最近设备心跳', _online ? '刚刚' : '无心跳'), - ('独立图传视频', '未打开监看会话'), + ( + l10n.translate('robot_network_v2.row_conn_mode'), + l10n.translate( + _online + ? 'robot_network_v2.val_cellular' + : 'robot_network_v2.val_disconnected', + ), + ), + ( + l10n.translate('robot_network_v2.row_net_state'), + l10n.translate( + _online + ? 'robot_network_v2.val_online_good' + : 'robot_network_v2.val_offline', + ), + ), + ( + l10n.translate('robot_network_v2.row_cloud'), + l10n.translate( + _online + ? 'robot_network_v2.val_normal' + : 'robot_network_v2.val_broken', + ), + ), + ( + l10n.translate('robot_network_v2.row_heartbeat'), + l10n.translate( + _online + ? 'robot_network_v2.val_just_now' + : 'robot_network_v2.val_no_heartbeat', + ), + ), + ( + l10n.translate('robot_network_v2.row_video_session'), + l10n.translate('robot_network_v2.val_no_session'), + ), ]), const SizedBox(height: 18), - _buildSectionTitle('网络配置'), + _buildSectionTitle(l10n.translate('robot_network_v2.sec_net_cfg')), const SizedBox(height: 10), _buildEntryCard([ - (Icons.cell_tower_rounded, '4G模块信息', '运营商、信号强度与模块状态', _todo), - (Icons.wifi_rounded, 'Wi-Fi配置', '按设备实际Wi-Fi硬件开放', _todo), - (Icons.cloud_outlined, '云平台接入', '服务器与心跳只读/受限配置', _todo), + ( + Icons.cell_tower_rounded, + l10n.translate('robot_network_v2.entry_4g'), + l10n.translate('robot_network_v2.entry_4g_sub'), + _todo, + ), + ( + Icons.wifi_rounded, + l10n.translate('robot_network_v2.entry_wifi'), + l10n.translate('robot_network_v2.entry_wifi_sub'), + _todo, + ), + ( + Icons.cloud_outlined, + l10n.translate('robot_network_v2.entry_cloud'), + l10n.translate('robot_network_v2.entry_cloud_sub'), + _todo, + ), ]), const SizedBox(height: 14), - _buildNote('连接模式与云平台状态取自设备在线态;运营商、信号强度需设备端上报后才会显示真实值。'), + _buildNote(l10n.translate('robot_network_v2.note_net')), ]; } // ===== GNSS 定位 ===== List _buildGnssTab() { + final l10n = AppLocalizations.of(context); return [ - _buildSectionTitle('定位状态'), + _buildSectionTitle(l10n.translate('robot_network_v2.sec_gnss_state')), const SizedBox(height: 10), _buildInfoCard([ - ('定位模块', '内置 GNSS'), - ('当前定位', _loading ? '获取中…' : (_hasLocation ? '有效 · 米级' : '无定位')), - ('卫星信息', '仅显示设备真实上报'), - ('最近定位时间', _hasLocation ? _loadedAt : '--'), + ( + l10n.translate('robot_network_v2.row_fix_module'), + l10n.translate('robot_network_v2.val_builtin_gnss'), + ), + ( + l10n.translate('robot_network_v2.row_cur_fix'), + l10n.translate( + _loading + ? 'robot_network_v2.fix_loading' + : (_hasLocation + ? 'robot_network_v2.fix_valid' + : 'robot_network_v2.fix_none'), + ), + ), + ( + l10n.translate('robot_network_v2.row_sat'), + l10n.translate('robot_network_v2.val_sat_only'), + ), + ( + l10n.translate('robot_network_v2.row_fix_time'), + _hasLocation ? _loadedAt : '--', + ), ]), const SizedBox(height: 18), - _buildSectionTitle('坐标'), + _buildSectionTitle(l10n.translate('robot_network_v2.sec_coord')), const SizedBox(height: 10), - _buildInfoCard([('纬度', _latText), ('经度', _lngText)]), + _buildInfoCard([ + (l10n.translate('robot_network_v2.row_lat'), _latText), + (l10n.translate('robot_network_v2.row_lng'), _lngText), + ]), const SizedBox(height: 18), - _buildSectionTitle('定位配置与地图'), + _buildSectionTitle(l10n.translate('robot_network_v2.sec_gnss_cfg')), const SizedBox(height: 10), _buildEntryCard([ - (Icons.map_outlined, '在地图中查看定位', '定位点、轨迹与最后更新时间', _openMap), - (Icons.explore_outlined, '定位参数', '坐标系、上报频率(设备端配置)', _todo), + ( + Icons.map_outlined, + l10n.translate('robot_network_v2.entry_map'), + l10n.translate('robot_network_v2.entry_map_sub'), + _openMap, + ), + ( + Icons.explore_outlined, + l10n.translate('robot_network_v2.entry_gnss_param'), + l10n.translate('robot_network_v2.entry_gnss_param_sub'), + _todo, + ), ]), const SizedBox(height: 14), _buildNote( _loading - ? '正在获取设备定位…' + ? l10n.translate('robot_network_v2.note_loading') : (_hasLocation - ? '坐标来自设备实际 GNSS 上报,已同步到「位置与地图详情」' - : (_error != null ? '定位获取失败:$_error' : '设备暂未上报有效定位')), + ? l10n.translate('robot_network_v2.note_from_device') + : (_error != null + ? l10n + .translate('robot_network_v2.note_error') + .replaceAll('%s', _error!) + : l10n.translate('robot_network_v2.note_no_fix'))), ), ]; } diff --git a/lib/features/v2/device_list/presentation/pages/robot_ownership_page_v2.dart b/lib/features/v2/device_list/presentation/pages/robot_ownership_page_v2.dart index 82fd9bde..9ac9923b 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_ownership_page_v2.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_ownership_page_v2.dart @@ -1,25 +1,45 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:maibu_satabot_v2/components/capsule_toast.dart'; +import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart'; +import 'package:maibu_satabot_v2/core/di/injection.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import '../../data/models/robot_data_model.dart'; +import '../../domain/entities/bind_device_entities.dart'; +import '../../domain/usecases/bind_device_usecases.dart'; +import '../../domain/usecases/update_device_alias_usecase.dart'; import '../widgets/device_confirm_dialog.dart'; +import '../widgets/rename_device_dialog.dart'; import '../widgets/robot_summary_card_v2.dart'; +import 'device_ownership_action_page_v2.dart'; /// 设备归属与安全(设计稿:设备卡片 → 更多 → 设备归属与安全) /// /// 二级 push 页面,无底部导航。 /// -/// 数据口径: -/// - 设备名称、型号、在线态、SN 均来自 [RobotDataModel],SN 支持明文/脱敏切换; -/// - 所有者、归属类型、首次认领、管理员列表后端暂无对应接口,按设计稿展示为示例值, -/// 页脚与行内统一注明,不伪装成真实归属关系; -/// - 高风险操作(移交管理员、解绑与所有权转让)一律先过 [showDeviceConfirmDialog], -/// 未接入下发通道时只给明确提示,不执行不可逆动作。 +/// 页面按「先看信息、再看自己能干什么、最后动手」的顺序分三段: +/// 1. 设备基础信息:设备卡片 + 归属快照(后端未接入的字段统一标「未同步」); +/// 2. 我的操作权限:把 roleKey 翻译成可操作层级,避免用户在操作页才被拦住; +/// 3. 归属操作:转交设备(绑定接口)与解绑设备(解绑接口), +/// 越权项置灰并说明原因,而不是静默消失。 class RobotOwnershipPageV2 extends StatefulWidget { final RobotDataModel robot; - const RobotOwnershipPageV2({super.key, required this.robot}); + /// 设备列表当前选中场站:getSiteList 按场站查设备,设备一定属于它, + /// 所以设备自身未下发 siteName 时用它兜底,不显示「未知」。 + final int? siteId; + final String? siteName; + final int? orgId; + + const RobotOwnershipPageV2({ + super.key, + required this.robot, + this.siteId, + this.siteName, + this.orgId, + }); @override State createState() => _RobotOwnershipPageV2State(); @@ -33,53 +53,222 @@ class _RobotOwnershipPageV2State extends State { static const Color _danger = Color(0xFFF0483E); static const Color _cardBorder = Color(0xFFE5EDF7); static const Color _softBlue = Color(0xFFEAF3FE); + static const Color _softGray = Color(0xFFF2F4F8); static const Color _divider = Color(0xFFF0F4FA); static const Color _green = Color(0xFF2FBF6B); /// SN 是否显示明文 bool _showSn = false; + /// 本页改名后的名称(接口只写 deviceAlias,不动 deviceName) + String? _aliasOverride; + + /// 展示名:本页刚改的 > 后端 deviceAlias > deviceName + String get _displayName { + final current = _aliasOverride?.trim(); + if (current != null && current.isNotEmpty) return current; + return RobotSummaryCardV2.displayNameOf(widget.robot); + } + + /// 组织列表:只用于把 orgId 翻成组织名 + List _orgs = const []; + bool get _online => widget.robot.isOnline; + @override + void initState() { + super.initState(); + _loadOrgs(); + } + + Future _loadOrgs() async { + final result = await sl()(); + result.fold( + (failure) => debugPrint( + '>>> [Ownership] 组织列表加载失败: ${failure.message}', + ), + (orgs) { + if (!mounted) return; + setState(() => _orgs = orgs); + }, + ); + } + + // ── 真实归属链路:组织 → 场站 → 负责人(拿不到就标「未知」,不编造)── + int? get _orgId => widget.robot.orgId ?? widget.orgId; + + String? get _orgName { + final direct = widget.robot.orgName?.trim(); + if (direct != null && direct.isNotEmpty) return direct; + final id = _orgId; + if (id == null || id <= 0) return null; + for (final org in _orgs) { + if (org.id == id) return org.name; + } + return null; + } + + String? get _siteName { + final direct = widget.robot.siteName?.trim(); + if (direct != null && direct.isNotEmpty) return direct; + final fallback = widget.siteName?.trim(); + return (fallback != null && fallback.isNotEmpty) ? fallback : null; + } + + String? get _ownerName { + final direct = widget.robot.userName?.trim(); + return (direct != null && direct.isNotEmpty) ? direct : null; + } + String get _snText => _showSn ? widget.robot.name : RobotSummaryCardV2.maskedSnOf(widget.robot); void _toggleSn() => setState(() => _showSn = !_showSn); - void _openSharedMembers() => - CapsuleToast.show('成员共享管理通道未接入', showCheck: false); + /// 改名:复用设备列表那套底部弹窗,只下发 deviceAlias + /// + /// 设备标识用设备编码(deviceName,MC700- 开头),不是数字主键 id; + /// 成功后只更新本页展示名,不重新拉归属(归属关系没变)。 + Future _renameDevice() async { + final l10n = AppLocalizations.of(context); + final oldName = _displayName; + final newName = await showRenameDeviceDialog( + context, + currentName: oldName, + modelText: widget.robot.modelLabelOf(l10n), + snKey: widget.robot.name.isNotEmpty ? widget.robot.name : widget.robot.id, + ); + // 取消 / 名称未变:不调接口 + if (newName == null || newName == oldName || !mounted) return; - /// 移交管理员:变更角色不改变所有权 - Future _transferAdmin() async { + final targetDeviceId = widget.robot.name.isNotEmpty + ? widget.robot.name + : widget.robot.id; + debugPrint( + '>>> [Ownership] 改名下发 deviceId=$targetDeviceId alias=$newName', + ); + + final result = await sl().execute( + deviceId: targetDeviceId, + deviceAlias: newName, + ); + if (!mounted) return; + + result.fold( + (failure) => CapsuleToast.show( + failure.message.isEmpty + ? l10n.translate('device_list_v2.rename_failed') + : failure.message, + showCheck: false, + ), + (updated) { + if (!updated) { + CapsuleToast.show( + l10n.translate('device_list_v2.rename_failed'), + showCheck: false, + ); + return; + } + setState(() => _aliasOverride = newName); + CapsuleToast.show( + l10n + .translate('device_list_v2.rename_success') + .replaceAll('%s', newName), + ); + }, + ); + } + + // ── 角色与可操作层级(口径与归属操作页一致)── + String _roleKey = 'user'; + + bool get _canOrg => _roleKey == 'admin'; + + bool get _canSite => _roleKey == 'admin' || _roleKey == 'manager'; + + bool get _canUser => + _roleKey == 'admin' || _roleKey == 'manager' || _roleKey == 'siteManager'; + + /// 普通用户 / 个人角色只能动自己,所以负责人这一级是「仅本人」 + bool get _selfOnly => !_canUser; + + /// 能触达的最深一级:admin 到组织、manager 到场站、其余到负责人 + UnbindScope get _maxScope { + if (_canOrg) return UnbindScope.org; + if (_canSite) return UnbindScope.site; + return UnbindScope.user; + } + + // 共享管理(成员管理)暂不开放,入口已从基础信息卡注释,方法一并注释 + // void _openSharedMembers() => CapsuleToast.show( + // AppLocalizations.of(context).translate('device_ownership_v2.toast_member_todo'), + // showCheck: false, + // ); + + void _notifyLocked() => CapsuleToast.show( + AppLocalizations.of(context).translate('device_ownership_v2.action_locked'), + showCheck: false, + ); + + /// 转交设备:本质就是绑定接口 + /// + /// 「转交」只是顺着组织 → 场站 → 负责人的层级把设备重新挂到某一级, + /// 下发走 /iot/device/bind,与扫码绑定同一条链路; + /// 普通用户只能转给自己,普通用户以上的角色才能转给其他人。 + Future _transferDevice() async { + final l10n = AppLocalizations.of(context); final ok = await showDeviceConfirmDialog( context, icon: Icons.workspace_premium_rounded, - title: '移交管理员权限', - message: '移交后你将成为普通成员,设备所有权不变。确认继续?', - note: '建议先确认接手成员已在成员列表中。', - confirmText: '继续移交', + title: l10n.translate('device_ownership_v2.transfer_device_title'), + message: l10n.translate('device_ownership_v2.transfer_device_msg'), + note: l10n.translate('device_ownership_v2.transfer_device_note'), + confirmText: l10n.translate('device_ownership_v2.transfer_device_ok'), ); if (!ok || !mounted) return; - CapsuleToast.show('管理员移交通道未接入', showCheck: false); + await _openActionPage(OwnershipAction.transfer); } - /// 解绑与所有权转让:不可逆,红色确认 - Future _transferOwnership() async { + /// 解绑设备:确认 → 扫码核验 → 选解除归属层级 → 下发解绑 + /// + /// 解绑不可逆,所以保留红色二次确认,再接扫码与层级选择; + /// 成功后向上一级回传 true,由设备列表重拉统计与机器人数据。 + Future _unbindDevice() async { + final l10n = AppLocalizations.of(context); final ok = await showDeviceConfirmDialog( context, - icon: Icons.swap_horiz_rounded, - title: '设备解绑与所有权转让', - message: '解绑后你与本设备的关联将立即失效,需重新绑定才能继续使用。确认继续?', - note: '需核验所有者权限和身份,SN 不能代替身份核验。', - confirmText: '确认操作', + icon: Icons.link_off_rounded, + title: l10n.translate('device_ownership_v2.unbind_device_title'), + message: l10n.translate('device_ownership_v2.unbind_device_msg'), + note: l10n.translate('device_ownership_v2.unbind_device_note'), + confirmText: l10n.translate('device_ownership_v2.unbind_device_ok'), danger: true, ); if (!ok || !mounted) return; - CapsuleToast.show('所有权转让通道未接入,需所有者身份核验', showCheck: false); + await _openActionPage(OwnershipAction.unbind); + } + + /// 两个动作共用跳转:不再经过扫码页,直接进归属操作页 + /// (入口已由归属页本身承担设备确认,多一道扫码只会让用户点「跳过」) + Future _openActionPage(OwnershipAction action) async { + final done = await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => + DeviceOwnershipActionPageV2(robot: widget.robot, action: action), + ), + ); + if (!mounted) return; + + // 动作已完成,归属关系变了,回到设备列表让上层重拉 + if (done == true) { + Navigator.of(context).pop(true); + } } @override Widget build(BuildContext context) { + _roleKey = context.watch().state.user?.roleKey ?? 'user'; + return Scaffold( backgroundColor: const Color(0xFFEDF6FD), body: Container( @@ -102,11 +291,11 @@ class _RobotOwnershipPageV2State extends State { children: [ RobotSummaryCardV2(robot: widget.robot), const SizedBox(height: 14), - _buildOwnershipCard(), + _buildBasicCard(), const SizedBox(height: 14), - _buildAdminCard(), + _buildPermissionCard(), const SizedBox(height: 14), - _buildRiskyCard(), + _buildActionCard(), const SizedBox(height: 14), _buildFooterNote(), ], @@ -127,7 +316,8 @@ class _RobotOwnershipPageV2State extends State { child: Row( children: [ GestureDetector( - onTap: () => Navigator.of(context).pop(), + // 改过名 / 做过归属操作都回传 true,由列表页重拉设备卡片 + onTap: () => Navigator.of(context).pop(_aliasOverride != null), behavior: HitTestBehavior.opaque, child: const SizedBox( width: 36, @@ -139,13 +329,13 @@ class _RobotOwnershipPageV2State extends State { ), ), ), - const Expanded( + Expanded( child: Text( - '设备归属与安全', + AppLocalizations.of(context).translate('device_ownership_v2.title'), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle( + style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w800, color: _navy, @@ -158,7 +348,6 @@ class _RobotOwnershipPageV2State extends State { ); } - // ===== 卡内标题:18 w800 ===== Widget _buildCardTitle(String title, {Color color = _navy}) { return Text( title, @@ -166,118 +355,313 @@ class _RobotOwnershipPageV2State extends State { ); } - // ===== 设备归属信息 ===== - Widget _buildOwnershipCard() { + Widget _buildCard({ + required List children, + Color color = Colors.white, + Color borderColor = _cardBorder, + }) { return Container( padding: const EdgeInsets.fromLTRB(14, 16, 14, 4), decoration: BoxDecoration( - color: Colors.white, + color: color, borderRadius: BorderRadius.circular(16), - border: Border.all(color: _cardBorder), + border: Border.all(color: borderColor), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, + ), + ); + } + + // ===== ① 设备基础信息 ===== + Widget _buildBasicCard() { + final l10n = AppLocalizations.of(context); + final unknown = l10n.translate('device_ownership_v2.unknown'); + final orgName = _orgName; + final siteName = _siteName; + final ownerName = _ownerName; + + return _buildCard( + children: [ + Row( + children: [ + Expanded( + child: _buildCardTitle( + l10n.translate('device_ownership_v2.card_basic'), + ), + ), + _buildStatusPill( + l10n.translate( + _online + ? 'device_ownership_v2.bound_active' + : 'device_ownership_v2.bound_offline', + ), + _online ? _green : _gray, + ), + ], + ), + const SizedBox(height: 8), + _buildOwnershipPath( + orgName: orgName, + siteName: siteName, + ownerName: ownerName, + ), + // 设备名称可现场改:整行都能点,不只铅笔图标 + _buildKvRow( + l10n.translate('device_ownership_v2.basic_name'), + _displayName, + onTap: _renameDevice, + trailing: const Padding( + padding: EdgeInsets.only(left: 8, top: 4, bottom: 4), + child: Icon(Icons.edit_outlined, size: 18, color: _blue), + ), + ), + const Divider(height: 1, thickness: 0.6, color: _divider), + _buildKvRow( + l10n.translate('device_ownership_v2.basic_org'), + orgName ?? unknown, + muted: orgName == null, + ), + const Divider(height: 1, thickness: 0.6, color: _divider), + _buildKvRow( + l10n.translate('device_ownership_v2.basic_site'), + siteName ?? unknown, + muted: siteName == null, + ), + const Divider(height: 1, thickness: 0.6, color: _divider), + _buildKvRow( + l10n.translate('device_ownership_v2.basic_owner'), + ownerName ?? unknown, + muted: ownerName == null, + ), + const Divider(height: 1, thickness: 0.6, color: _divider), + _buildKvRow( + l10n.translate('device_ownership_v2.sn'), + _snText, + trailing: GestureDetector( + onTap: _toggleSn, + behavior: HitTestBehavior.opaque, + child: Padding( + padding: const EdgeInsets.only(left: 6), + child: Icon( + _showSn + ? Icons.visibility_rounded + : Icons.visibility_off_outlined, + size: 18, + color: _gray, + ), + ), + ), + isLast: true, + ), + // 共享管理(成员管理)暂不开放,先注释入口 + // const Divider(height: 1, thickness: 0.6, color: _divider), + // _buildKvRow( + // l10n.translate('device_ownership_v2.member_enter'), + // '', + // trailing: GestureDetector( + // onTap: _openSharedMembers, + // behavior: HitTestBehavior.opaque, + // child: const Padding( + // padding: EdgeInsets.only(left: 6), + // child: Icon( + // Icons.chevron_right_rounded, + // size: 20, + // color: _blue, + // ), + // ), + // ), + // isLast: true, + // ), + ], + ); + } + + /// 归属链路:组织 / 场站 / 负责人 一条线看完,缺哪级就标未知 + Widget _buildOwnershipPath({ + required String? orgName, + required String? siteName, + required String? ownerName, + }) { + final l10n = AppLocalizations.of(context); + final unknown = l10n.translate('device_ownership_v2.unknown'); + final text = [orgName ?? unknown, siteName ?? unknown, ownerName ?? unknown] + .join(' / '); + + return Container( + margin: const EdgeInsets.only(bottom: 2), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 11), + decoration: BoxDecoration( + color: _softBlue, + borderRadius: BorderRadius.circular(10), + ), + child: Row( children: [ - _buildCardTitle('设备归属信息'), - const SizedBox(height: 6), - _buildKvRow('设备所有者', '待归属接口上报', muted: true), - const Divider(height: 1, thickness: 0.6, color: _divider), - _buildKvRow('归属类型', '企业组织', muted: true), - const Divider(height: 1, thickness: 0.6, color: _divider), - _buildKvRow('当前绑定', _online ? '已激活 · 归属有效' : '已绑定 · 设备离线'), - const Divider(height: 1, thickness: 0.6, color: _divider), + const Icon(Icons.account_tree_rounded, size: 15, color: _blue), + const SizedBox(width: 8), + Expanded( + child: Text( + text, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 12.5, + height: 1.45, + fontWeight: FontWeight.w700, + color: _navy, + ), + ), + ), + ], + ), + ); + } + + // ===== ② 我的操作权限 ===== + Widget _buildPermissionCard() { + final l10n = AppLocalizations.of(context); + + return _buildCard( + children: [ + Row( + children: [ + Expanded( + child: _buildCardTitle( + l10n.translate('device_ownership_v2.card_permission'), + ), + ), + _buildStatusPill( + l10n + .translate('device_ownership_v2.role_current') + .replaceFirst('%s', l10n.translate(_roleLabelKey(_roleKey))), + _blue, + soft: true, + ), + ], + ), + const SizedBox(height: 6), + Text( + l10n.translate('device_ownership_v2.perm_desc'), + style: const TextStyle( + fontSize: 11.5, + height: 1.55, + color: _gray, + ), + ), + const SizedBox(height: 12), + // 三段权限指示器:亮 = 可操作,灰 = 无权限,浅蓝 = 仅本人 + Row( + children: [ + _buildLevelChip( + l10n.translate('device_ownership_v2.perm_level_org'), + _canOrg ? _LevelState.full : _LevelState.none, + ), + const SizedBox(width: 8), + _buildLevelChip( + l10n.translate('device_ownership_v2.perm_level_site'), + _canSite ? _LevelState.full : _LevelState.none, + ), + const SizedBox(width: 8), + _buildLevelChip( + l10n.translate('device_ownership_v2.perm_level_user'), + _selfOnly ? _LevelState.self : _LevelState.full, + ), + ], + ), + const SizedBox(height: 4), + const Divider(height: 1, thickness: 0.6, color: _divider), + // 转交入口只给普通用户以上的角色,这里也同步不展示,避免前后矛盾 + if (_canUser) ...[ _buildKvRow( - '设备SN', - _snText, - trailing: GestureDetector( - onTap: _toggleSn, - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.only(left: 6), - child: Icon( - _showSn - ? Icons.visibility_rounded - : Icons.visibility_off_outlined, - size: 18, - color: _gray, + l10n.translate('device_ownership_v2.perm_transfer_to'), + _scopePathText(_maxScope), + ), + const Divider(height: 1, thickness: 0.6, color: _divider), + ], + _buildKvRow( + l10n.translate('device_ownership_v2.perm_unbind_to'), + _scopePathText(_maxScope), + isLast: true, + ), + ], + color: Colors.white, + borderColor: _cardBorder, + ); + } + + /// 层级胶囊:full 蓝底白字,self 浅蓝底蓝字,none 灰底灰字 + Widget _buildLevelChip(String label, _LevelState state) { + final l10n = AppLocalizations.of(context); + final (Color bg, Color fg, IconData icon) = switch (state) { + _LevelState.full => (_blue, Colors.white, Icons.check_rounded), + _LevelState.self => (_softBlue, _blue, Icons.person_rounded), + _LevelState.none => (_softGray, _hint, Icons.lock_outline_rounded), + }; + final text = state == _LevelState.self + ? '${l10n.translate('device_ownership_v2.perm_level_user')}' + ' · ${l10n.translate('device_ownership_v2.perm_self_short')}' + : label; + + return Expanded( + child: Container( + height: 42, + alignment: Alignment.center, + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(11), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 14, color: fg), + const SizedBox(width: 5), + Flexible( + child: Text( + text, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12.5, + fontWeight: FontWeight.w700, + color: fg, ), ), ), - ), - const Divider(height: 1, thickness: 0.6, color: _divider), - _buildKvRow('首次认领', '待归属接口上报', muted: true, isLast: true), - ], + ], + ), ), ); } - // ===== 管理员与授权 ===== - Widget _buildAdminCard() { - return Container( - padding: const EdgeInsets.fromLTRB(14, 16, 14, 4), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: _cardBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildCardTitle('管理员与授权'), - const SizedBox(height: 6), - _buildKvRow('当前管理员', '待归属接口上报', muted: true), - const Divider(height: 1, thickness: 0.6, color: _divider), - GestureDetector( - onTap: _openSharedMembers, - behavior: HitTestBehavior.opaque, - child: _buildKvRow( - '成员权限管理', - '进入共享管理', - valueColor: _blue, - isLast: true, - trailing: const Icon( - Icons.chevron_right_rounded, - size: 20, - color: _hint, - ), - ), - ), - ], - ), - ); - } + // ===== ③ 归属操作 ===== + Widget _buildActionCard() { + final l10n = AppLocalizations.of(context); - // ===== 高风险归属操作 ===== - Widget _buildRiskyCard() { - return Container( - padding: const EdgeInsets.fromLTRB(14, 16, 14, 4), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: _cardBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildCardTitle('高风险归属操作', color: _danger), - const SizedBox(height: 4), + return _buildCard( + children: [ + _buildCardTitle(l10n.translate('device_ownership_v2.card_actions')), + const SizedBox(height: 4), + // 转交只对「普通用户以上」的角色开放:普通用户/个人角色不能把设备 + // 转给他人,也就不展示这个入口,避免进去后才发现无法选人 + if (_canUser) ...[ _buildActionRow( icon: Icons.workspace_premium_rounded, - title: '移交管理员权限', - subtitle: '变更管理角色,不改变所有权', - onTap: _transferAdmin, + title: l10n.translate('device_ownership_v2.risky_transfer'), + subtitle: l10n.translate('device_ownership_v2.risky_transfer_sub'), + onTap: _transferDevice, ), const Divider(height: 1, thickness: 0.6, color: _divider), - _buildActionRow( - icon: Icons.swap_horiz_rounded, - title: '设备解绑与所有权转让', - subtitle: '需核验所有者权限和身份', - danger: true, - isLast: true, - onTap: _transferOwnership, - ), ], - ), + _buildActionRow( + icon: Icons.link_off_rounded, + title: l10n.translate('device_ownership_v2.risky_unbind'), + subtitle: l10n.translate('device_ownership_v2.risky_unbind_sub'), + danger: true, + isLast: true, + onTap: _unbindDevice, + ), + ], ); } @@ -286,52 +670,143 @@ class _RobotOwnershipPageV2State extends State { required String title, required String subtitle, required VoidCallback onTap, + bool enabled = true, bool danger = false, bool isLast = false, }) { - final color = danger ? _danger : _blue; + final l10n = AppLocalizations.of(context); + final accent = enabled ? (danger ? _danger : _blue) : _hint; + return GestureDetector( - onTap: onTap, + onTap: enabled ? onTap : _notifyLocked, behavior: HitTestBehavior.opaque, - child: Padding( - padding: EdgeInsets.only(top: 14, bottom: isLast ? 14 : 12), - child: Row( - children: [ - SizedBox(width: 30, child: Icon(icon, size: 22, color: color)), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w800, - color: color, - ), - ), - const SizedBox(height: 4), - Text( - subtitle, - style: const TextStyle(fontSize: 12, color: _gray), - ), - ], + child: Opacity( + opacity: enabled ? 1 : 0.75, + child: Padding( + padding: EdgeInsets.only(top: 14, bottom: isLast ? 14 : 12), + child: Row( + children: [ + Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: enabled + ? (danger ? const Color(0xFFFDEDEC) : _softBlue) + : _softGray, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, size: 19, color: accent), ), - ), - const SizedBox(width: 8), - const Icon( - Icons.chevron_right_rounded, - size: 20, - color: Color(0xFFC9CDD4), - ), - ], + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + title, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w800, + color: accent, + ), + ), + if (!enabled) ...[ + const SizedBox(width: 6), + _buildStatusPill( + l10n.translate('device_ownership_v2.tag_locked'), + _hint, + soft: true, + ), + ], + ], + ), + const SizedBox(height: 4), + Text( + subtitle, + style: const TextStyle(fontSize: 12, color: _gray), + ), + ], + ), + ), + const SizedBox(width: 8), + Icon( + enabled + ? Icons.chevron_right_rounded + : Icons.lock_outline_rounded, + size: enabled ? 20 : 16, + color: enabled ? const Color(0xFFC9CDD4) : _hint, + ), + ], + ), ), ), ); } - // ===== 通用 KV 行 ===== + // ===== 通用小组件 ===== + /// 右上角状态胶囊:实心或浅底 + Widget _buildStatusPill( + String text, + Color color, { + bool soft = false, + }) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: soft ? color.withValues(alpha: 0.1) : color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + text, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: color, + ), + ), + ); + } + + String _roleLabelKey(String roleKey) { + switch (roleKey) { + case 'admin': + return 'device_ownership_action_v2.role_admin'; + case 'manager': + return 'device_ownership_action_v2.role_manager'; + case 'siteManager': + return 'device_ownership_action_v2.role_site_manager'; + case 'personal': + return 'device_ownership_action_v2.role_personal'; + default: + // user 在归属操作里与 personal 同口径:只能是自己 + return 'device_ownership_action_v2.role_user'; + } + } + + /// 可操作层级文案:组织 / 场站 / 负责人 逐级列出 + String _scopePathText(UnbindScope max) { + final l10n = AppLocalizations.of(context); + final names = []; + for (final scope in UnbindScope.values.reversed) { + if (scope.value > max.value) continue; + names.add(l10n.translate(_scopeLabelKey(scope))); + } + return names.join(' / '); + } + + String _scopeLabelKey(UnbindScope scope) { + switch (scope) { + case UnbindScope.org: + return 'device_ownership_v2.perm_level_org'; + case UnbindScope.site: + return 'device_ownership_v2.perm_level_site'; + case UnbindScope.user: + return 'device_ownership_v2.perm_level_user'; + } + } + Widget _buildKvRow( String label, String value, { @@ -339,8 +814,9 @@ class _RobotOwnershipPageV2State extends State { Color? valueColor, Widget? trailing, bool isLast = false, + VoidCallback? onTap, }) { - return Padding( + final row = Padding( padding: EdgeInsets.only(top: 14, bottom: isLast ? 14 : 12), child: Row( children: [ @@ -363,6 +839,14 @@ class _RobotOwnershipPageV2State extends State { ], ), ); + + // 整行都可点:只让小图标可点的话,手指很难点中 + if (onTap == null) return row; + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: row, + ); } Widget _buildFooterNote() { @@ -381,11 +865,10 @@ class _RobotOwnershipPageV2State extends State { color: _online ? _green : _blue, ), const SizedBox(width: 8), - const Expanded( + Expanded( child: Text( - '管理员身份不等于设备所有者;SN 仅用于识别,不能凭 SN 完成解绑。' - '归属信息需设备归属接口接入后显示真实值。', - style: TextStyle( + AppLocalizations.of(context).translate('device_ownership_v2.footer'), + style: const TextStyle( fontSize: 11.5, height: 1.55, color: Color(0xFF5C6B84), @@ -397,3 +880,6 @@ class _RobotOwnershipPageV2State extends State { ); } } + +/// 权限层级的三态 +enum _LevelState { full, self, none } diff --git a/lib/features/v2/device_list/presentation/pages/robot_run_record_page_v2.dart b/lib/features/v2/device_list/presentation/pages/robot_run_record_page_v2.dart index 4d8d3292..c64c0cab 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_run_record_page_v2.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_run_record_page_v2.dart @@ -3,11 +3,12 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:maibu_satabot_v2/components/capsule_toast.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import '../../data/models/robot_data_model.dart'; import '../widgets/robot_summary_card_v2.dart'; -/// 时间范围档位:标题 + 柱状图数据 + 横轴标签(示例数据) +/// 时间范围档位:标题 key + 柱状图数据 + 横轴标签 key(示例数据) class _RangeData { final String label; final String chartTitle; @@ -22,7 +23,7 @@ class _RangeData { }); } -/// 一条历史事件 +/// 一条历史事件(title / time / tag 均为语言包 key) class _RunEvent { final String title; final String time; @@ -61,45 +62,86 @@ class _RobotRunRecordPageV2State extends State { /// 三个可切换档位(数据为示例,仅用于展示图表形态) static const List<_RangeData> _ranges = [ _RangeData( - label: '近7天', - chartTitle: '运行趋势(最近7天)', + label: 'robot_run_v2.range_7', + chartTitle: 'robot_run_v2.chart_7', bars: [3.2, 4.6, 2.8, 5.1, 4.0, 4.4, 5.0], - axis: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], + axis: [ + 'robot_run_v2.axis_mon', + 'robot_run_v2.axis_tue', + 'robot_run_v2.axis_wed', + 'robot_run_v2.axis_thu', + 'robot_run_v2.axis_fri', + 'robot_run_v2.axis_sat', + 'robot_run_v2.axis_sun', + ], ), _RangeData( - label: '近30天', - chartTitle: '运行趋势(最近30天)', + label: 'robot_run_v2.range_30', + chartTitle: 'robot_run_v2.chart_30', bars: [18.4, 22.6, 20.1, 25.4, 21.9], - axis: ['第1周', '第2周', '第3周', '第4周', '第5周'], + axis: [ + 'robot_run_v2.axis_week_1', + 'robot_run_v2.axis_week_2', + 'robot_run_v2.axis_week_3', + 'robot_run_v2.axis_week_4', + 'robot_run_v2.axis_week_5', + ], ), _RangeData( - label: '近90天', - chartTitle: '运行趋势(最近90天)', + label: 'robot_run_v2.range_90', + chartTitle: 'robot_run_v2.chart_90', bars: [62.5, 74.2, 81.6], - axis: ['7月', '8月', '9月'], + axis: [ + 'robot_run_v2.axis_month_7', + 'robot_run_v2.axis_month_8', + 'robot_run_v2.axis_month_9', + ], ), ]; /// 历史记录与事件(示例数据:后端暂无运行事件接口) static const List<_RunEvent> _events = [ - _RunEvent(title: '机器人开机', time: '今日 08:41', tag: '运行状态'), - _RunEvent(title: 'APP遥控会话释放', time: '昨日 17:06', tag: '控制操作'), - _RunEvent(title: '超声波联动停刀触发', time: '10月18日 15:42', tag: '已解除'), - _RunEvent(title: '云端连接恢复', time: '10月18日 15:40', tag: '网络'), + _RunEvent( + title: 'robot_run_v2.ev_1_title', + time: 'robot_run_v2.ev_1_time', + tag: 'robot_run_v2.ev_1_tag', + ), + _RunEvent( + title: 'robot_run_v2.ev_2_title', + time: 'robot_run_v2.ev_2_time', + tag: 'robot_run_v2.ev_2_tag', + ), + _RunEvent( + title: 'robot_run_v2.ev_3_title', + time: 'robot_run_v2.ev_3_time', + tag: 'robot_run_v2.ev_3_tag', + ), + _RunEvent( + title: 'robot_run_v2.ev_4_title', + time: 'robot_run_v2.ev_4_time', + tag: 'robot_run_v2.ev_4_tag', + ), ]; int _rangeIndex = 0; _RangeData get _range => _ranges[_rangeIndex]; - void _todo() => CapsuleToast.show('功能暂未开发', showCheck: false); + void _todo() => CapsuleToast.show( + AppLocalizations.of(context).translate('robot_run_v2.todo'), + showCheck: false, + ); void _onCustomRange() { - CapsuleToast.show('自定义时间范围暂未开发', showCheck: false); + CapsuleToast.show( + AppLocalizations.of(context).translate('robot_run_v2.custom_todo'), + showCheck: false, + ); } @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); return Scaffold( backgroundColor: const Color(0xFFEDF6FD), body: Container( @@ -124,23 +166,30 @@ class _RobotRunRecordPageV2State extends State { const SizedBox(height: 14), _buildRangeBar(), const SizedBox(height: 18), - _buildSectionTitle('运行数据概况'), + _buildSectionTitle( + l10n.translate('robot_run_v2.sec_overview'), + ), const SizedBox(height: 10), _buildOverviewCard(), const SizedBox(height: 18), - _buildSectionTitle(_range.chartTitle), + _buildSectionTitle(l10n.translate(_range.chartTitle)), const SizedBox(height: 10), _buildChartCard(), const SizedBox(height: 18), - _buildSectionTitle('历史记录与事件'), + _buildSectionTitle( + l10n.translate('robot_run_v2.sec_events'), + ), const SizedBox(height: 10), _buildEventTimeline(), const SizedBox(height: 14), - const Center( + Center( child: Text( - '运行统计与历史事件为示例数据,接入统计接口后自动替换', + l10n.translate('robot_run_v2.footer'), textAlign: TextAlign.center, - style: TextStyle(fontSize: 10.5, color: _hint), + style: const TextStyle( + fontSize: 10.5, + color: _hint, + ), ), ), ], @@ -185,12 +234,12 @@ class _RobotRunRecordPageV2State extends State { ), ), const SizedBox(width: 12), - const Expanded( + Expanded( child: Text( - '运行记录与健康分析', + AppLocalizations.of(context).translate('robot_run_v2.title'), maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle( + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: _navy, @@ -257,7 +306,11 @@ class _RobotRunRecordPageV2State extends State { // ===== 时间范围:近7天 / 近30天 / 近90天 / 自定义 ===== Widget _buildRangeBar() { - final labels = [..._ranges.map((e) => e.label), '自定义']; + final l10n = AppLocalizations.of(context); + final labels = [ + ..._ranges.map((e) => l10n.translate(e.label)), + l10n.translate('robot_run_v2.range_custom'), + ]; return Container( height: 46, padding: const EdgeInsets.all(4), @@ -312,6 +365,7 @@ class _RobotRunRecordPageV2State extends State { // ===== 运行数据概况:三块浅蓝统计 ===== Widget _buildOverviewCard() { + final l10n = AppLocalizations.of(context); return Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( @@ -324,24 +378,24 @@ class _RobotRunRecordPageV2State extends State { Expanded( child: _buildStatBlock( icon: Icons.access_time_rounded, - value: '128.5h', - label: '累计运行', + value: l10n.translate('robot_run_v2.stat_runtime_value'), + label: l10n.translate('robot_run_v2.stat_runtime'), ), ), const SizedBox(width: 8), Expanded( child: _buildStatBlock( icon: Icons.power_settings_new_rounded, - value: '86次', - label: '启动次数', + value: l10n.translate('robot_run_v2.stat_starts_value'), + label: l10n.translate('robot_run_v2.stat_starts'), ), ), const SizedBox(width: 8), Expanded( child: _buildStatBlock( icon: Icons.show_chart_rounded, - value: '12.8km', - label: '示例里程', + value: l10n.translate('robot_run_v2.stat_mileage_value'), + label: l10n.translate('robot_run_v2.stat_mileage'), ), ), ], @@ -401,9 +455,11 @@ class _RobotRunRecordPageV2State extends State { Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ - const Text( - '每日运行时长', - style: TextStyle( + Text( + AppLocalizations.of( + context, + ).translate('robot_run_v2.chart_daily'), + style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w800, color: _navy, @@ -411,7 +467,7 @@ class _RobotRunRecordPageV2State extends State { ), const Spacer(), Text( - '单位:小时', + AppLocalizations.of(context).translate('robot_run_v2.chart_unit'), style: TextStyle( fontSize: 11, color: _hint.withValues(alpha: 0.95), @@ -478,7 +534,7 @@ class _RobotRunRecordPageV2State extends State { for (var i = 0; i < axis.length; i++) Expanded( child: Text( - axis[i], + AppLocalizations.of(context).translate(axis[i]), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -527,6 +583,7 @@ class _RobotRunRecordPageV2State extends State { } Widget _buildEventRow(_RunEvent event, {required bool isLast}) { + final l10n = AppLocalizations.of(context); return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -557,7 +614,7 @@ class _RobotRunRecordPageV2State extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - event.title, + l10n.translate(event.title), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( @@ -568,7 +625,7 @@ class _RobotRunRecordPageV2State extends State { ), const SizedBox(height: 5), Text( - '${event.time} · ${event.tag}', + '${l10n.translate(event.time)} · ${l10n.translate(event.tag)}', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: _gray), diff --git a/lib/features/v2/device_list/presentation/pages/robot_sensor_page_v2.dart b/lib/features/v2/device_list/presentation/pages/robot_sensor_page_v2.dart index 35d6e0cf..fa263f1d 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_sensor_page_v2.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_sensor_page_v2.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'package:maibu_satabot_v2/components/capsule_toast.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import 'package:maibu_satabot_v2/features/devices/domain/usecases/get_device_location_usecase.dart'; import '../../data/models/robot_data_model.dart'; @@ -37,16 +38,21 @@ class _RobotSensorPageV2State extends State { static const Color _divider = Color(0xFFF0F4FA); static const Color _trackBg = Color(0xFFDFEBF9); - static const List _tabs = ['超声波', '定位', '通信', '机构']; + static const List _tabKeys = [ + 'robot_sensor_v2.tab_ultrasonic', + 'robot_sensor_v2.tab_position', + 'robot_sensor_v2.tab_comm', + 'robot_sensor_v2.tab_mechanism', + ]; - /// 超声波 6 路测距(示例值,与设计稿一致) + /// 超声波 6 路测距(演示值,与设计稿一致):(方位 key, 距离) static const List<(String, String)> _ultrasonicRows = [ - ('前左', '1.5m'), - ('前中', '1.8m'), - ('前右', '2.0m'), - ('左侧', '1.3m'), - ('右侧', '1.7m'), - ('后部', '2.1m'), + ('robot_sensor_v2.dir_front_left', '1.5m'), + ('robot_sensor_v2.dir_front_mid', '1.8m'), + ('robot_sensor_v2.dir_front_right', '2.0m'), + ('robot_sensor_v2.dir_left', '1.3m'), + ('robot_sensor_v2.dir_right', '1.7m'), + ('robot_sensor_v2.dir_rear', '2.1m'), ]; int _tabIndex = 0; @@ -96,7 +102,10 @@ class _RobotSensorPageV2State extends State { return '${v.abs().toStringAsFixed(5)}° ${v >= 0 ? 'N' : 'S'}'; } - void _todo() => CapsuleToast.show('功能暂未开发', showCheck: false); + void _todo() => CapsuleToast.show( + AppLocalizations.of(context).translate('robot_sensor_v2.todo'), + showCheck: false, + ); @override Widget build(BuildContext context) { @@ -167,12 +176,12 @@ class _RobotSensorPageV2State extends State { ), ), const SizedBox(width: 12), - const Expanded( + Expanded( child: Text( - '传感器与通信详情', + AppLocalizations.of(context).translate('robot_sensor_v2.title'), maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle( + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: _navy, @@ -218,7 +227,7 @@ class _RobotSensorPageV2State extends State { borderRadius: BorderRadius.circular(14), ), child: Row( - children: List.generate(_tabs.length, (index) { + children: List.generate(_tabKeys.length, (index) { final selected = _tabIndex == index; return Expanded( child: GestureDetector( @@ -242,7 +251,7 @@ class _RobotSensorPageV2State extends State { ), alignment: Alignment.center, child: Text( - _tabs[index], + AppLocalizations.of(context).translate(_tabKeys[index]), style: TextStyle( fontSize: 12.5, fontWeight: selected ? FontWeight.w700 : FontWeight.w500, @@ -298,10 +307,11 @@ class _RobotSensorPageV2State extends State { // ===== 超声波 ===== Widget _buildUltrasonicTab() { + final l10n = AppLocalizations.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildSectionTitle('超声波实时状态'), + _buildSectionTitle(l10n.translate('robot_sensor_v2.sec_ultrasonic')), const SizedBox(height: 10), Container( decoration: BoxDecoration( @@ -316,9 +326,9 @@ class _RobotSensorPageV2State extends State { padding: const EdgeInsets.fromLTRB(14, 13, 14, 0), child: Row( children: [ - const Text( - '超声波障碍检测', - style: TextStyle( + Text( + l10n.translate('robot_sensor_v2.us_card_title'), + style: const TextStyle( fontSize: 13.5, fontWeight: FontWeight.w800, color: _navy, @@ -334,9 +344,9 @@ class _RobotSensorPageV2State extends State { color: _greenBg, borderRadius: BorderRadius.circular(10), ), - child: const Text( - '6/6正常', - style: TextStyle( + child: Text( + l10n.translate('robot_sensor_v2.us_ok_count'), + style: const TextStyle( fontSize: 10.5, fontWeight: FontWeight.w600, color: _green, @@ -353,7 +363,7 @@ class _RobotSensorPageV2State extends State { Padding( padding: const EdgeInsets.fromLTRB(14, 10, 14, 13), child: Text( - '以下为演示距离;实际传感器位置、数量按设备SN加载。', + l10n.translate('robot_sensor_v2.us_note'), style: TextStyle( fontSize: 10.5, color: _gray.withValues(alpha: 0.95), @@ -364,11 +374,19 @@ class _RobotSensorPageV2State extends State { ), ), const SizedBox(height: 18), - _buildSectionTitle('传感器测距'), + _buildSectionTitle(l10n.translate('robot_sensor_v2.sec_distance')), const SizedBox(height: 10), _buildRowsCard( _ultrasonicRows - .map((row) => (row.$1, '${row.$2} · 正常', _green)) + .map( + (row) => ( + l10n.translate(row.$1), + l10n + .translate('robot_sensor_v2.dist_normal') + .replaceAll('%s', row.$2), + _green, + ), + ) .toList(), ), ], @@ -396,11 +414,21 @@ class _RobotSensorPageV2State extends State { child: CustomPaint(painter: _SonarRingPainter(color: _blue)), ), // 前方距离标签 - Positioned(top: 26, child: _buildDistanceTag('前:1.8m')), + Positioned( + top: 26, + child: _buildDistanceTag( + AppLocalizations.of(context).translate('robot_sensor_v2.sonar_front'), + ), + ), // 中央机器人 _buildRobotBlock(), // 后方距离标签 - Positioned(bottom: 26, child: _buildDistanceTag('后:2.1m')), + Positioned( + bottom: 26, + child: _buildDistanceTag( + AppLocalizations.of(context).translate('robot_sensor_v2.sonar_rear'), + ), + ), ], ), ), @@ -493,22 +521,41 @@ class _RobotSensorPageV2State extends State { // ===== 定位 ===== Widget _buildPositionTab() { + final l10n = AppLocalizations.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildSectionTitle('定位与卫星'), + _buildSectionTitle(l10n.translate('robot_sensor_v2.sec_position')), const SizedBox(height: 10), _buildRowsCard([ - ('经度', _lngText, _navy), - ('纬度', _latText, _navy), - ('定位方式', '内置 GNSS', _navy), + (l10n.translate('robot_sensor_v2.row_lng'), _lngText, _navy), + (l10n.translate('robot_sensor_v2.row_lat'), _latText, _navy), ( - '定位状态', - _locLoading ? '获取中…' : (_hasLocation ? '有效 · 米级' : '未上报'), + l10n.translate('robot_sensor_v2.row_fix'), + l10n.translate('robot_sensor_v2.row_fix_value'), + _navy, + ), + ( + l10n.translate('robot_sensor_v2.row_fix_state'), + l10n.translate( + _locLoading + ? 'robot_sensor_v2.fix_loading' + : (_hasLocation + ? 'robot_sensor_v2.fix_valid' + : 'robot_sensor_v2.fix_none'), + ), _hasLocation ? _green : _gray, ), - ('卫星数', '示例:18 颗', _navy), - ('RTK 差分', '示例:未启用', _navy), + ( + l10n.translate('robot_sensor_v2.row_sats'), + l10n.translate('robot_sensor_v2.row_sats_value'), + _navy, + ), + ( + l10n.translate('robot_sensor_v2.row_rtk'), + l10n.translate('robot_sensor_v2.row_rtk_value'), + _navy, + ), ]), const SizedBox(height: 10), _buildSampleNote(), @@ -518,22 +565,47 @@ class _RobotSensorPageV2State extends State { // ===== 通信 ===== Widget _buildCommTab() { + final l10n = AppLocalizations.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildSectionTitle('通信链路'), + _buildSectionTitle(l10n.translate('robot_sensor_v2.sec_comm')), const SizedBox(height: 10), _buildRowsCard([ ( - '设备在线', - widget.robot.isOnline ? '在线' : '离线', + l10n.translate('robot_sensor_v2.row_online'), + l10n.translate( + widget.robot.isOnline + ? 'robot_sensor_v2.online' + : 'robot_sensor_v2.offline', + ), widget.robot.isOnline ? _green : _gray, ), - ('通信方式', '示例:MQTT 长连接', _navy), - ('网络类型', '示例:4G 全网通', _navy), - ('信号强度', '示例:-68 dBm', _navy), - ('链路延迟', '示例:120 ms', _navy), - ('最后上报', '示例:2026-10-20 09:41', _navy), + ( + l10n.translate('robot_sensor_v2.row_comm_mode'), + l10n.translate('robot_sensor_v2.row_comm_mode_value'), + _navy, + ), + ( + l10n.translate('robot_sensor_v2.row_net_type'), + l10n.translate('robot_sensor_v2.row_net_type_value'), + _navy, + ), + ( + l10n.translate('robot_sensor_v2.row_signal'), + l10n.translate('robot_sensor_v2.row_signal_value'), + _navy, + ), + ( + l10n.translate('robot_sensor_v2.row_latency'), + l10n.translate('robot_sensor_v2.row_latency_value'), + _navy, + ), + ( + l10n.translate('robot_sensor_v2.row_last_report'), + l10n.translate('robot_sensor_v2.row_last_report_value'), + _navy, + ), ]), const SizedBox(height: 10), _buildSampleNote(), @@ -543,18 +615,43 @@ class _RobotSensorPageV2State extends State { // ===== 机构 ===== Widget _buildMechanismTab() { + final l10n = AppLocalizations.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildSectionTitle('作业机构'), + _buildSectionTitle(l10n.translate('robot_sensor_v2.sec_mechanism')), const SizedBox(height: 10), _buildRowsCard([ - ('割盘升降', '示例:停止', _navy), - ('刀盘转速', '示例:0 r/min', _navy), - ('底盘倾角', '示例:正常', _green), - ('障碍物检测', '示例:未触发', _green), - ('行走驱动', '示例:正常', _green), - ('急停开关', '示例:未按下', _green), + ( + l10n.translate('robot_sensor_v2.row_blade_lift'), + l10n.translate('robot_sensor_v2.row_blade_lift_value'), + _navy, + ), + ( + l10n.translate('robot_sensor_v2.row_blade_rpm'), + l10n.translate('robot_sensor_v2.row_blade_rpm_value'), + _navy, + ), + ( + l10n.translate('robot_sensor_v2.row_tilt'), + l10n.translate('robot_sensor_v2.row_tilt_value'), + _green, + ), + ( + l10n.translate('robot_sensor_v2.row_obstacle'), + l10n.translate('robot_sensor_v2.row_obstacle_value'), + _green, + ), + ( + l10n.translate('robot_sensor_v2.row_drive'), + l10n.translate('robot_sensor_v2.row_drive_value'), + _green, + ), + ( + l10n.translate('robot_sensor_v2.row_estop'), + l10n.translate('robot_sensor_v2.row_estop_value'), + _green, + ), ]), const SizedBox(height: 10), _buildSampleNote(), @@ -563,11 +660,11 @@ class _RobotSensorPageV2State extends State { } Widget _buildSampleNote() { - return const Center( + return Center( child: Text( - '标注「示例」的字段后端暂未上报,接入后自动替换', + AppLocalizations.of(context).translate('robot_sensor_v2.sample_note'), textAlign: TextAlign.center, - style: TextStyle(fontSize: 10.5, color: _hint), + style: const TextStyle(fontSize: 10.5, color: _hint), ), ); } diff --git a/lib/features/v2/device_list/presentation/pages/robot_voice_preset_page_v2.dart b/lib/features/v2/device_list/presentation/pages/robot_voice_preset_page_v2.dart index 8dccca21..2c134709 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_voice_preset_page_v2.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_voice_preset_page_v2.dart @@ -1,11 +1,31 @@ import 'package:flutter/material.dart'; import 'package:maibu_satabot_v2/components/capsule_toast.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import '../../data/models/robot_data_model.dart'; import '../../data/voice_preset_store.dart'; import '../widgets/robot_summary_card_v2.dart'; +/// 出厂默认语音的本地化版本:首次落盘时写入当前语言,之后以本地为准 +List localizedVoiceDefaults(AppLocalizations l10n) => [ + VoicePreset( + id: 'default_safety', + name: l10n.translate('robot_voice_v2.def_safety_name'), + text: l10n.translate('robot_voice_v2.def_safety_text'), + ), + VoicePreset( + id: 'default_start', + name: l10n.translate('robot_voice_v2.def_start_name'), + text: l10n.translate('robot_voice_v2.def_start_text'), + ), + VoicePreset( + id: 'default_polite', + name: l10n.translate('robot_voice_v2.def_polite_name'), + text: l10n.translate('robot_voice_v2.def_polite_text'), + ), +]; + /// 预设语音管理(设计稿:全部功能中心 → 语音喊话) /// /// 二级 push 页面,无底部导航。 @@ -40,14 +60,17 @@ class _RobotVoicePresetPageV2State extends State { List _presets = []; bool _loading = true; + bool _loadStarted = false; /// null 表示正在新增;否则为被编辑语音的 id String? _editingId; @override - void initState() { - super.initState(); - _load(); + void didChangeDependencies() { + super.didChangeDependencies(); + if (_loadStarted) return; + _loadStarted = true; + _load(AppLocalizations.of(context)); } @override @@ -59,8 +82,11 @@ class _RobotVoicePresetPageV2State extends State { super.dispose(); } - Future _load() async { - final presets = await VoicePresetStore.loadPresets(widget.robot.name); + Future _load(AppLocalizations l10n) async { + final presets = await VoicePresetStore.loadPresets( + widget.robot.name, + fallback: localizedVoiceDefaults(l10n), + ); if (!mounted) return; setState(() { _presets = presets; @@ -68,7 +94,10 @@ class _RobotVoicePresetPageV2State extends State { }); } - void _todo() => CapsuleToast.show('功能暂未开发', showCheck: false); + void _todo() => CapsuleToast.show( + AppLocalizations.of(context).translate('robot_voice_v2.todo'), + showCheck: false, + ); /// 点列表项右侧铅笔:把这条填进下方编辑卡 void _editPreset(VoicePreset preset) { @@ -102,15 +131,22 @@ class _RobotVoicePresetPageV2State extends State { } Future _savePreset() async { + final l10n = AppLocalizations.of(context); final name = _nameCtrl.text.trim(); final text = _textCtrl.text.trim(); if (name.isEmpty) { - CapsuleToast.show('请填写语音名称', showCheck: false); + CapsuleToast.show( + l10n.translate('robot_voice_v2.toast_name_required'), + showCheck: false, + ); _nameFocus.requestFocus(); return; } if (text.isEmpty) { - CapsuleToast.show('请填写语音文字', showCheck: false); + CapsuleToast.show( + l10n.translate('robot_voice_v2.toast_text_required'), + showCheck: false, + ); return; } @@ -132,20 +168,29 @@ class _RobotVoicePresetPageV2State extends State { await VoicePresetStore.savePresets(widget.robot.name, presets); if (!mounted) return; setState(() => _presets = presets); - CapsuleToast.show('预设语音已保存'); + CapsuleToast.show( + AppLocalizations.of(context).translate('robot_voice_v2.toast_saved'), + ); } void _playPreset(VoicePreset preset) { debugPrint('🔊 [VoicePreset] 试听: ${preset.name} / ${preset.text}'); - CapsuleToast.show('试听需设备端语音合成,暂未接入', showCheck: false); + CapsuleToast.show( + AppLocalizations.of(context).translate('robot_voice_v2.toast_preview'), + showCheck: false, + ); } void _realtimeTalk() { - CapsuleToast.show('实时喊话将在第二阶段开放', showCheck: false); + CapsuleToast.show( + AppLocalizations.of(context).translate('robot_voice_v2.toast_realtime'), + showCheck: false, + ); } @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); return Scaffold( backgroundColor: const Color(0xFFEDF6FD), body: Container( @@ -169,23 +214,29 @@ class _RobotVoicePresetPageV2State extends State { children: [ RobotSummaryCardV2(robot: widget.robot), const SizedBox(height: 18), - _buildSectionHeader('预设语音列表'), + _buildSectionHeader( + l10n.translate('robot_voice_v2.sec_list'), + ), const SizedBox(height: 10), _buildPresetList(), const SizedBox(height: 18), - _buildSectionTitle('编辑语音内容'), + _buildSectionTitle( + l10n.translate('robot_voice_v2.sec_edit'), + ), const SizedBox(height: 10), _buildEditorCard(), const SizedBox(height: 18), - _buildSectionTitle('实时喊话(第二阶段)'), + _buildSectionTitle( + l10n.translate('robot_voice_v2.sec_realtime'), + ), const SizedBox(height: 10), _buildRealtimeCard(), const SizedBox(height: 14), - const Center( + Center( child: Text( - '预设语音保存在本机;语音合成为设备端能力,需设备支持后才会播报', + l10n.translate('robot_voice_v2.footer'), textAlign: TextAlign.center, - style: TextStyle(fontSize: 10.5, color: _hint), + style: const TextStyle(fontSize: 10.5, color: _hint), ), ), ], @@ -230,12 +281,12 @@ class _RobotVoicePresetPageV2State extends State { ), ), const SizedBox(width: 12), - const Expanded( + Expanded( child: Text( - '预设语音管理', + AppLocalizations.of(context).translate('robot_voice_v2.title'), maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle( + style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: _navy, @@ -243,7 +294,7 @@ class _RobotVoicePresetPageV2State extends State { ), ), GestureDetector( - onTap: _todo, + onTap: () => _todo(), behavior: HitTestBehavior.opaque, child: Container( width: 36, @@ -303,11 +354,11 @@ class _RobotVoicePresetPageV2State extends State { GestureDetector( onTap: _startCreate, behavior: HitTestBehavior.opaque, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 2, vertical: 4), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 4), child: Text( - '+ 新增', - style: TextStyle( + AppLocalizations.of(context).translate('robot_voice_v2.add'), + style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: _blue, @@ -449,6 +500,7 @@ class _RobotVoicePresetPageV2State extends State { // ===== 编辑语音内容 ===== Widget _buildEditorCard() { + final l10n = AppLocalizations.of(context); return Container( key: _editorKey, padding: const EdgeInsets.fromLTRB(14, 14, 14, 14), @@ -460,38 +512,47 @@ class _RobotVoicePresetPageV2State extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Text('语音名称', style: TextStyle(fontSize: 12.5, color: _gray)), + Text( + l10n.translate('robot_voice_v2.name_label'), + style: const TextStyle(fontSize: 12.5, color: _gray), + ), const SizedBox(height: 8), _buildField( controller: _nameCtrl, focusNode: _nameFocus, - hint: '如:安全警示', + hint: l10n.translate('robot_voice_v2.name_hint'), ), const SizedBox(height: 14), - const Text( - '语音文字(用户可编辑)', - style: TextStyle(fontSize: 12.5, color: _gray), + Text( + l10n.translate('robot_voice_v2.text_label'), + style: const TextStyle(fontSize: 12.5, color: _gray), ), const SizedBox(height: 8), _buildField( controller: _textCtrl, - hint: '输入要播报的文字内容', + hint: l10n.translate('robot_voice_v2.text_hint'), minLines: 4, maxLines: 6, ), const SizedBox(height: 12), Row( children: [ - const Text( - '语言:中文简体', - style: TextStyle(fontSize: 12.5, color: _gray), - ), - const Spacer(), Text( - '语音合成/同步需设备支持', - style: TextStyle( - fontSize: 11.5, - color: _hint.withValues(alpha: 0.95), + l10n.translate('robot_voice_v2.language'), + style: const TextStyle(fontSize: 12.5, color: _gray), + ), + const SizedBox(width: 12), + // 英文文案较长:可收缩换行,避免 Row 横向溢出 + Expanded( + child: Text( + l10n.translate('robot_voice_v2.sync_note'), + textAlign: TextAlign.right, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 11.5, + color: _hint.withValues(alpha: 0.95), + ), ), ), ], @@ -514,9 +575,9 @@ class _RobotVoicePresetPageV2State extends State { ], ), alignment: Alignment.center, - child: const Text( - '保存预设语音', - style: TextStyle( + child: Text( + l10n.translate('robot_voice_v2.save'), + style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w700, color: Colors.white, @@ -574,6 +635,7 @@ class _RobotVoicePresetPageV2State extends State { // ===== 实时喊话(第二阶段)===== Widget _buildRealtimeCard() { + final l10n = AppLocalizations.of(context); return GestureDetector( onTap: _realtimeTalk, behavior: HitTestBehavior.opaque, @@ -596,26 +658,30 @@ class _RobotVoicePresetPageV2State extends State { child: const Icon(Icons.mic_none_rounded, size: 21, color: _gray), ), const SizedBox(width: 12), - const Expanded( + Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '通过APP麦克风实时喊话', + l10n.translate('robot_voice_v2.realtime_title'), maxLines: 1, overflow: TextOverflow.ellipsis, - style: TextStyle( + style: const TextStyle( fontSize: 14.5, fontWeight: FontWeight.w800, color: _navy, ), ), - SizedBox(height: 4), + const SizedBox(height: 4), Text( - '通过APP麦克风采集语音,实时下发设备播放', + l10n.translate('robot_voice_v2.realtime_sub'), maxLines: 2, overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 12, height: 1.35, color: _gray), + style: const TextStyle( + fontSize: 12, + height: 1.35, + color: _gray, + ), ), ], ), @@ -627,9 +693,9 @@ class _RobotVoicePresetPageV2State extends State { color: _softGray, borderRadius: BorderRadius.circular(10), ), - child: const Text( - '待开放', - style: TextStyle( + child: Text( + l10n.translate('robot_voice_v2.badge_todo'), + style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: _gray, diff --git a/lib/features/v2/device_list/presentation/widgets/autonomous/work_record_panels.dart b/lib/features/v2/device_list/presentation/widgets/autonomous/work_record_panels.dart index 8d6fa64f..f998c297 100644 --- a/lib/features/v2/device_list/presentation/widgets/autonomous/work_record_panels.dart +++ b/lib/features/v2/device_list/presentation/widgets/autonomous/work_record_panels.dart @@ -1,111 +1,89 @@ import 'package:flutter/material.dart'; -import 'package:maibu_satabot_v2/components/capsule_toast.dart'; - +import 'package:maibu_satabot_v2/core/di/injection.dart'; +import 'package:maibu_satabot_v2/features/autonomous_work/domain/entities/work_session_models.dart'; +import 'package:maibu_satabot_v2/features/autonomous_work/domain/repositories/autonomous_work_gateway.dart'; import 'work_ui.dart'; -/// 自主作业记录(设计稿:右侧抽屉「自主作业记录」+「单次作业记录」)。 -/// -/// 记录数据为设计稿演示口径(作业记录接口未接入,页面内已标注「示例」); -/// 面板内部在「列表 ↔ 单次详情」间切换,对应设计稿的 ‹返回记录列表。 -Future showWorkRecordPanel(BuildContext context, {String? initialRoute}) { +/// 展示后端任务池和本机实际收到的完成记录,不使用设计稿示例数据。 +Future showWorkRecordPanel( + BuildContext context, { + required String deviceId, +}) { return showWorkRightPanel( context, width: 470, - builder: (close) => - _WorkRecordPanel(onClose: close, initialRoute: initialRoute), + builder: (close) => _WorkRecordPanel(deviceId: deviceId, onClose: close), ); } class _WorkRecordPanel extends StatefulWidget { - const _WorkRecordPanel({required this.onClose, this.initialRoute}); - + const _WorkRecordPanel({required this.deviceId, required this.onClose}); + final String deviceId; final VoidCallback onClose; - final String? initialRoute; - @override State<_WorkRecordPanel> createState() => _WorkRecordPanelState(); } class _WorkRecordPanelState extends State<_WorkRecordPanel> { - /// (路线, 状态, 时间范围, 明细, 状态样式) - static const List<(String, String, String, String, int)> - _records = <(String, String, String, String, int)>[ - ('A区 · 路线01', '已完成', '10月20日 09:10–10:02', '完成2.50亩 · 52分钟 · 进度100%', 0), - ('B区 · 路线01', '人工停止', '10月19日 14:20–14:57', '完成1.34亩 · 37分钟 · 进度42%', 1), - ('A区 · 路线02', '异常中断', '10月19日 09:02–09:44', '完成1.20亩 · 42分钟 · 进度51%', 2), - ]; - - int _tab = 0; - int? _detailIndex; + late Future>> _records = _load(); + Future>> _load() async { + final gateway = sl(); + if (gateway is WorkTaskHistorySource) { + return (gateway as WorkTaskHistorySource).loadTaskHistory( + deviceId: widget.deviceId, + ); + } + return const WorkResult.fail('当前环境尚未提供真实作业记录'); + } @override Widget build(BuildContext context) { - if (_detailIndex != null) { - return _buildDetail(_detailIndex!); - } - return _buildList(); - } - - // ===== 列表 ===== - Widget _buildList() { return WorkPanelScaffold( title: '自主作业记录', - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + desc: '显示任务池中的结束任务及本机收到的完成通知;未提供的面积、耗时不作估算。', + body: FutureBuilder>>( + future: _records, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } + final result = snapshot.data!; + if (!result.isOk) return Text(result.error ?? '记录加载失败'); + final records = result.data ?? const []; + if (records.isEmpty) return const Text('该设备暂无可查询的作业记录'); + return Column( children: [ - _tabChip('任务历史', 0), - const SizedBox(width: 8), - _tabChip('效率分析', 1), + for (final task in records) + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: WorkCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + task.routeName ?? '任务 #${task.taskId}', + style: const TextStyle(fontWeight: FontWeight.w700), + ), + const SizedBox(height: 6), + Text(task.rawStatus == 'FINISH' ? '已完成' : '已终止'), + Text('任务 ID:${task.taskId}'), + if (task.createdAt != null) + Text('下发时间:${task.createdAt}'), + Text('已接收到达点:${task.completedPoints.length} 个'), + ], + ), + ), + ), ], - ), - const SizedBox(height: 14), - if (_tab == 0) ...[ - const Row( - children: [ - WorkStatCard(value: '3', label: '历史任务(示例)'), - SizedBox(width: 10), - WorkStatCard(value: '5.04亩', label: '累计完成面积'), - SizedBox(width: 10), - WorkStatCard(value: '1', label: '人工停止'), - ], - ), - const SizedBox(height: 12), - const Text( - '可按日期、区域、路线和结果筛选;手动停止的任务也必须保留已完成轨迹。', - style: TextStyle(fontSize: 11.5, height: 1.6, color: kWtGray2), - ), - const SizedBox(height: 12), - for (var i = 0; i < _records.length; i++) ...[ - _recordRow(i), - const SizedBox(height: 8), - ], - ] else ...[ - const Row( - children: [ - WorkStatCard(value: '2.4亩/时', label: '平均作业效率'), - SizedBox(width: 10), - WorkStatCard(value: '86%', label: '平均完成率'), - SizedBox(width: 10), - WorkStatCard(value: '12%', label: '任务中断率'), - ], - ), - const SizedBox(height: 12), - const WorkNoteBar( - text: '效率分析为演示口径,接入作业统计接口后替换。', - background: kWtSoftBlue, - foreground: kWtBlue, - ), - ], - ], + ); + }, ), footer: Row( children: [ Expanded( child: WorkButton( - label: '筛选 / 导出', - onTap: () => CapsuleToast.show('记录筛选与导出暂未开发', showCheck: false), + label: '刷新记录', + onTap: () => setState(() => _records = _load()), ), ), const SizedBox(width: 12), @@ -120,274 +98,4 @@ class _WorkRecordPanelState extends State<_WorkRecordPanel> { ), ); } - - Widget _tabChip(String label, int index) { - final selected = _tab == index; - return InkWell( - onTap: () => setState(() => _tab = index), - borderRadius: BorderRadius.circular(10), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9), - decoration: BoxDecoration( - color: selected ? kWtBlue : Colors.white, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: selected ? Colors.transparent : kWtBorder), - ), - child: Text( - label, - style: TextStyle( - fontSize: 12.5, - fontWeight: FontWeight.w700, - color: selected ? Colors.white : kWtGray2, - ), - ), - ), - ); - } - - Widget _recordRow(int index) { - final record = _records[index]; - final (bg, fg) = _statusStyle(record.$5); - return InkWell( - onTap: () => setState(() => _detailIndex = index), - borderRadius: BorderRadius.circular(14), - child: WorkCard( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - record.$1, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w800, - color: kWtNavy, - ), - ), - ), - WorkChip(text: record.$2, background: bg, foreground: fg), - ], - ), - const SizedBox(height: 6), - Text( - record.$3, - style: const TextStyle(fontSize: 11.5, color: kWtGray), - ), - const SizedBox(height: 4), - Row( - children: [ - Expanded( - child: Text( - record.$4, - style: const TextStyle(fontSize: 11.5, color: kWtGray2), - ), - ), - const Text( - '查看详情 ›', - style: TextStyle( - fontSize: 11.5, - fontWeight: FontWeight.w700, - color: kWtBlue, - ), - ), - ], - ), - ], - ), - ), - ); - } - - (Color, Color) _statusStyle(int kind) { - switch (kind) { - case 0: - return (kWtGreenBg, kWtGreen); - case 1: - return (kWtWarnBg, kWtWarn); - default: - return (const Color(0xFFFDEAEA), kWtDanger); - } - } - - // ===== 单次详情(以「B区 · 路线01 / 人工停止」为例,与设计稿一致)===== - Widget _buildDetail(int index) { - final record = _records[index]; - final (bg, fg) = _statusStyle(record.$5); - return WorkPanelScaffold( - title: '单次作业记录', - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - InkWell( - onTap: () => setState(() => _detailIndex = null), - borderRadius: BorderRadius.circular(10), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: kWtSoftBlue, - borderRadius: BorderRadius.circular(10), - ), - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.arrow_back_ios_new_rounded, - size: 12, - color: kWtBlue, - ), - SizedBox(width: 6), - Text( - '返回记录列表', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w700, - color: kWtBlue, - ), - ), - ], - ), - ), - ), - const SizedBox(height: 14), - Text( - record.$1, - style: const TextStyle( - fontSize: 15.5, - fontWeight: FontWeight.w800, - color: kWtNavy, - ), - ), - const SizedBox(height: 8), - Row( - children: [ - const WorkChip( - text: 'RW-1019-02', - background: kWtSoftBlue, - foreground: kWtBlue, - ), - const SizedBox(width: 8), - WorkChip(text: record.$2, background: bg, foreground: fg), - ], - ), - const SizedBox(height: 14), - const Row( - children: [ - WorkStatCard(value: '1.34亩', label: '实际完成面积'), - SizedBox(width: 10), - WorkStatCard(value: '37分钟', label: '执行时长'), - SizedBox(width: 10), - WorkStatCard(value: '42%', label: '最终进度'), - ], - ), - const SizedBox(height: 16), - const WorkSectionTitle('任务信息'), - const WorkCard( - padding: EdgeInsets.symmetric(horizontal: 14, vertical: 6), - child: Column( - children: [ - WorkLabelRow(label: '路线版本', value: 'v1.3'), - WorkLabelRow(label: '任务时间', value: '10月19日 14:20–14:57'), - WorkLabelRow(label: '结束状态', value: '人工停止'), - WorkLabelRow(label: '结果说明', value: '用户主动停止,已完成区域保留'), - WorkLabelRow(label: '轨迹保存', value: '已保留已完成部分(示例)'), - ], - ), - ), - const SizedBox(height: 16), - const WorkSectionTitle('执行事件'), - const _EventItem(title: '任务开始', subtitle: '定位与安全条件通过', last: false), - const _EventItem(title: '执行路径', subtitle: '割刀随任务启停', last: true), - ], - ), - footer: Row( - children: [ - Expanded( - child: WorkButton( - label: '查看地图轨迹', - onTap: () => CapsuleToast.show('轨迹回放暂未开发', showCheck: false), - ), - ), - const SizedBox(width: 12), - Expanded( - child: WorkButton( - label: '导出报告', - primary: true, - onTap: () => CapsuleToast.show('报告导出暂未开发', showCheck: false), - ), - ), - ], - ), - ); - } -} - -class _EventItem extends StatelessWidget { - const _EventItem({ - required this.title, - required this.subtitle, - required this.last, - }); - - final String title; - final String subtitle; - final bool last; - - @override - Widget build(BuildContext context) { - return IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - children: [ - Container( - width: 10, - height: 10, - margin: const EdgeInsets.only(top: 3), - decoration: const BoxDecoration( - color: kWtBlue, - shape: BoxShape.circle, - ), - ), - if (!last) - Expanded( - child: Container( - width: 2, - margin: const EdgeInsets.symmetric(vertical: 3), - color: kWtBorder, - ), - ), - ], - ), - const SizedBox(width: 10), - Expanded( - child: Padding( - padding: EdgeInsets.only(bottom: last ? 0 : 15), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 12.5, - fontWeight: FontWeight.w700, - color: kWtNavy, - ), - ), - const SizedBox(height: 2), - Text( - subtitle, - style: const TextStyle(fontSize: 11.5, color: kWtGray), - ), - ], - ), - ), - ), - ], - ), - ); - } } diff --git a/lib/features/v2/device_list/presentation/widgets/autonomous/work_route_panels.dart b/lib/features/v2/device_list/presentation/widgets/autonomous/work_route_panels.dart index d9a6840f..7307553a 100644 --- a/lib/features/v2/device_list/presentation/widgets/autonomous/work_route_panels.dart +++ b/lib/features/v2/device_list/presentation/widgets/autonomous/work_route_panels.dart @@ -12,11 +12,9 @@ import 'work_ui.dart'; /// 用结构体而不是字符串哨兵,是为了后续新增结果类型不破坏调用方。 class WorkRoutePanelResult { const WorkRoutePanelResult.select(WorkRouteOption this.route) - : editRequested = false; + : editRequested = false; - const WorkRoutePanelResult.edit() - : route = null, - editRequested = true; + const WorkRoutePanelResult.edit() : route = null, editRequested = true; final WorkRouteOption? route; final bool editRequested; @@ -91,9 +89,7 @@ class _RouteLibraryPanelState extends State<_RouteLibraryPanel> { setState(() { _loading = false; _routes = result.data ?? const []; - _error = _routes.isEmpty - ? (result.error ?? '该场站下暂无作业路线') - : null; + _error = _routes.isEmpty ? (result.error ?? '该场站下暂无作业路线') : null; if (_selectedId != null && !_routes.any((r) => r.id == _selectedId)) { _selectedId = null; } @@ -306,7 +302,14 @@ class _TaskQueuePanelState extends State<_TaskQueuePanel> { @override void initState() { super.initState(); - _sub = _gateway.watchTask(deviceId: widget.deviceId).listen(_onSnapshot); + final gateway = _gateway; + _sub = + (gateway is WorkTaskQueueSource + ? (gateway as WorkTaskQueueSource).watchTaskChanges( + deviceId: widget.deviceId, + ) + : gateway.watchTask(deviceId: widget.deviceId)) + .listen(_onSnapshot); _load(); } @@ -335,14 +338,11 @@ class _TaskQueuePanelState extends State<_TaskQueuePanel> { if (!mounted) return; setState(() { if (snapshot == null) { - _tasks = _tasks - .where( - (t) => - t.state != WorkRunState.queued && - t.state != WorkRunState.running && - t.state != WorkRunState.paused, - ) - .toList(); + // 当前任务为空不代表其它排队任务也被取消;列表以 loadTasks 为准。 + return; + } + if (snapshot.state == WorkRunState.finished) { + _tasks = _tasks.where((t) => t.taskId != snapshot.taskId).toList(); return; } final index = _tasks.indexWhere((t) => t.taskId == snapshot.taskId); diff --git a/lib/features/v2/device_list/presentation/widgets/robot_control_panel.dart b/lib/features/v2/device_list/presentation/widgets/robot_control_panel.dart index ed67e757..681b051f 100644 --- a/lib/features/v2/device_list/presentation/widgets/robot_control_panel.dart +++ b/lib/features/v2/device_list/presentation/widgets/robot_control_panel.dart @@ -14,12 +14,22 @@ import '../../../../pv_control/presentation/pages/pv_control_page.dart'; /// 保留可扩展:将来要按账号属性动态决定「默认进哪个模式 / 展示哪几个模式」, /// 只需在此枚举增删值即可,不影响前置信息获取与原极简页逻辑。 enum _RobotControlMode { - minimal('极简模式', '传统远程遥控界面', Icons.videogame_asset_outlined), - full('全功能模式', '横屏大屏可视化控制', Icons.grid_view_rounded); + minimal( + 'remote_control.mode_minimal', + 'remote_control.mode_minimal_sub', + Icons.videogame_asset_outlined, + ), + full( + 'remote_control.mode_full', + 'remote_control.mode_full_sub', + Icons.grid_view_rounded, + ); - const _RobotControlMode(this.title, this.subtitle, this.icon); - final String title; - final String subtitle; + const _RobotControlMode(this.titleKey, this.subtitleKey, this.icon); + + /// 标题 / 副标题的语言包 key(在弹窗 builder 内按当前语言翻译) + final String titleKey; + final String subtitleKey; final IconData icon; } @@ -257,10 +267,11 @@ class RobotControlPanel extends StatelessWidget { /// 模式选择弹窗:极简 / 全功能两个选项,选中即返回对应模式, /// 点外部或返回手势取消则返回 null。选项由枚举驱动,仅本文件使用。 Future<_RobotControlMode?> _showControlModePicker(BuildContext context) { + final l10n = AppLocalizations.of(context); return showDialog<_RobotControlMode>( context: context, builder: (dialogContext) => AlertDialog( - title: const Text('选择控制模式'), + title: Text(l10n.translate('remote_control.mode_picker_title')), contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), content: SizedBox( width: 320, @@ -271,14 +282,14 @@ class RobotControlPanel extends StatelessWidget { ListTile( leading: Icon(m.icon, color: context.appColors.primary), title: Text( - m.title, + l10n.translate(m.titleKey), style: TextStyle( fontWeight: FontWeight.bold, color: context.appColors.textPrimary, ), ), subtitle: Text( - m.subtitle, + l10n.translate(m.subtitleKey), style: TextStyle(color: context.appColors.textTertiary), ), onTap: () => Navigator.of(dialogContext).pop(m), diff --git a/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart b/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart index 2e0bc2a3..ea5d6738 100644 --- a/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart +++ b/lib/features/v2/device_list/presentation/widgets/robot_item_card.dart @@ -11,6 +11,10 @@ class RobotItemCard extends StatelessWidget { final String status; final double battery; final String task; + + /// 型号 · 版本(如 MC700 · PRO 旗舰版),由列表页从 productModel 解析后传入 + final String? modelText; + final VoidCallback onTap; const RobotItemCard({ @@ -22,6 +26,7 @@ class RobotItemCard extends StatelessWidget { required this.status, required this.battery, required this.task, + this.modelText, required this.onTap, }); @@ -93,6 +98,19 @@ class RobotItemCard extends StatelessWidget { maxLines: 2, softWrap: true, ), + if (modelText != null && modelText!.isNotEmpty) ...[ + const SizedBox(height: 2), + Text( + modelText!, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: context.appColors.textSecondary, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ], ), ), diff --git a/lib/features/v2/device_list/presentation/widgets/robot_summary_card_v2.dart b/lib/features/v2/device_list/presentation/widgets/robot_summary_card_v2.dart index b86b792d..c3bb3838 100644 --- a/lib/features/v2/device_list/presentation/widgets/robot_summary_card_v2.dart +++ b/lib/features/v2/device_list/presentation/widgets/robot_summary_card_v2.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import '../../data/models/robot_data_model.dart'; @@ -104,7 +105,9 @@ class RobotSummaryCardV2 extends StatelessWidget { ), const SizedBox(height: 6), Text( - '${modelOf(robot)} · STD 标准版', + // 型号 · 版本:deviceType 排在 PRO / PLUS / STD 之前, + // 版本由 productModel 解析,不再写死 STD + robot.modelLabelOf(AppLocalizations.of(context)), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11.5, color: _gray), diff --git a/lib/features/v2/device_list/presentation/widgets/robot_task_info.dart b/lib/features/v2/device_list/presentation/widgets/robot_task_info.dart index 9b02343b..6256c269 100644 --- a/lib/features/v2/device_list/presentation/widgets/robot_task_info.dart +++ b/lib/features/v2/device_list/presentation/widgets/robot_task_info.dart @@ -39,7 +39,10 @@ class RobotTaskInfo extends StatelessWidget { children: [ Expanded( child: Text( - robot['task'] ?? '逆变器区巡检', + robot['task'] ?? + AppLocalizations.of( + context, + ).translate('device_list_v2.task_default'), style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, diff --git a/lib/features/v2/device_list/presentation/widgets/voice_broadcast_sheet.dart b/lib/features/v2/device_list/presentation/widgets/voice_broadcast_sheet.dart index 448c400b..f46129a8 100644 --- a/lib/features/v2/device_list/presentation/widgets/voice_broadcast_sheet.dart +++ b/lib/features/v2/device_list/presentation/widgets/voice_broadcast_sheet.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:maibu_satabot_v2/components/capsule_toast.dart'; +import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; import '../../data/models/robot_data_model.dart'; import '../../data/voice_preset_store.dart'; @@ -55,15 +56,21 @@ class _VoiceBroadcastSheetState extends State<_VoiceBroadcastSheet> { List _presets = const []; bool _loading = true; + bool _loadStarted = false; @override - void initState() { - super.initState(); - _load(); + void didChangeDependencies() { + super.didChangeDependencies(); + if (_loadStarted) return; + _loadStarted = true; + _load(AppLocalizations.of(context)); } - Future _load() async { - final presets = await VoicePresetStore.loadPresets(widget.robot.name); + Future _load(AppLocalizations l10n) async { + final presets = await VoicePresetStore.loadPresets( + widget.robot.name, + fallback: localizedVoiceDefaults(l10n), + ); if (!mounted) return; setState(() { _presets = presets; @@ -77,7 +84,10 @@ class _VoiceBroadcastSheetState extends State<_VoiceBroadcastSheet> { widget.onPlay!(preset); return; } - CapsuleToast.show('试听需设备端语音合成,暂未接入', showCheck: false); + CapsuleToast.show( + AppLocalizations.of(context).translate('robot_voice_v2.toast_preview'), + showCheck: false, + ); } Future _manage() async { @@ -93,11 +103,12 @@ class _VoiceBroadcastSheetState extends State<_VoiceBroadcastSheet> { ), ); if (!mounted) return; - _load(); + _load(AppLocalizations.of(context)); } @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); return SafeArea( top: false, child: Container( @@ -113,10 +124,10 @@ class _VoiceBroadcastSheetState extends State<_VoiceBroadcastSheet> { children: [ Row( children: [ - const Expanded( + Expanded( child: Text( - '语音喊话', - style: TextStyle( + l10n.translate('robot_voice_v2.sheet_title'), + style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w800, color: _navy, @@ -149,14 +160,14 @@ class _VoiceBroadcastSheetState extends State<_VoiceBroadcastSheet> { borderRadius: BorderRadius.circular(12), border: Border.all(color: _blue, width: 1.2), ), - child: const Row( + child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.edit_outlined, size: 17, color: _blue), - SizedBox(width: 7), + const Icon(Icons.edit_outlined, size: 17, color: _blue), + const SizedBox(width: 7), Text( - '管理/编辑预设语音', - style: TextStyle( + l10n.translate('robot_voice_v2.sheet_manage'), + style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w700, color: _blue, @@ -177,10 +188,9 @@ class _VoiceBroadcastSheetState extends State<_VoiceBroadcastSheet> { color: _noteBg, borderRadius: BorderRadius.circular(10), ), - child: const Text( - '后续开放按住麦克风实时喊话;预设语音永久保留。' - '该模式不自动开麦,也不要求进入视频监看。', - style: TextStyle( + child: Text( + l10n.translate('robot_voice_v2.sheet_note'), + style: const TextStyle( fontSize: 11.5, height: 1.55, color: _noteText, @@ -195,7 +205,11 @@ class _VoiceBroadcastSheetState extends State<_VoiceBroadcastSheet> { } Widget _buildTabBar() { - const labels = ['预设语音 · 首期', '实时喊话 · 后续']; + final l10n = AppLocalizations.of(context); + final labels = [ + l10n.translate('robot_voice_v2.tab_preset'), + l10n.translate('robot_voice_v2.tab_realtime'), + ]; return Container( height: 46, padding: const EdgeInsets.all(4), @@ -210,7 +224,10 @@ class _VoiceBroadcastSheetState extends State<_VoiceBroadcastSheet> { child: GestureDetector( onTap: () { if (index == 1) { - CapsuleToast.show('实时喊话将在后续开放', showCheck: false); + CapsuleToast.show( + l10n.translate('robot_voice_v2.toast_realtime_later'), + showCheck: false, + ); } setState(() => _tab = index); }, @@ -358,6 +375,7 @@ class _VoiceBroadcastSheetState extends State<_VoiceBroadcastSheet> { } Widget _buildRealtimePanel() { + final l10n = AppLocalizations.of(context); return Container( padding: const EdgeInsets.fromLTRB(14, 16, 14, 16), decoration: BoxDecoration( @@ -376,19 +394,19 @@ class _VoiceBroadcastSheetState extends State<_VoiceBroadcastSheet> { child: const Icon(Icons.mic_none_rounded, size: 28, color: _gray), ), const SizedBox(height: 12), - const Text( - '按住麦克风实时喊话', - style: TextStyle( + Text( + l10n.translate('robot_voice_v2.hold_title'), + style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w800, color: _navy, ), ), const SizedBox(height: 6), - const Text( - '后续开放:长按说话,松开立即下发设备播放', + Text( + l10n.translate('robot_voice_v2.hold_sub'), textAlign: TextAlign.center, - style: TextStyle(fontSize: 12, height: 1.5, color: _gray), + style: const TextStyle(fontSize: 12, height: 1.5, color: _gray), ), const SizedBox(height: 12), Container( @@ -397,9 +415,9 @@ class _VoiceBroadcastSheetState extends State<_VoiceBroadcastSheet> { color: const Color(0xFFF4F6F9), borderRadius: BorderRadius.circular(10), ), - child: const Text( - '待开放', - style: TextStyle( + child: Text( + l10n.translate('robot_voice_v2.badge_todo'), + style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: _gray,