机器列表和远程喊话等ui
This commit is contained in:
@@ -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<String, dynamic> json, List<String> 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<String, dynamic> 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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,12 @@ class VoicePresetStore {
|
||||
VoicePreset(id: 'default_polite', name: '礼貌提示', text: '请注意避让,感谢配合。'),
|
||||
];
|
||||
|
||||
static Future<List<VoicePreset>> loadPresets(String deviceName) async {
|
||||
/// 首次进入时落盘一份出厂默认;[fallback] 允许调用方传入已本地化的默认值
|
||||
static Future<List<VoicePreset>> loadPresets(
|
||||
String deviceName, {
|
||||
List<VoicePreset>? fallback,
|
||||
}) async {
|
||||
final defaults = fallback ?? VoicePresetStore.defaults;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_keyOf(deviceName));
|
||||
if (raw == null || raw.isEmpty) {
|
||||
|
||||
@@ -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<Either<Failure, bool>> execute({required String deviceCode}) async {
|
||||
final code = deviceCode.trim();
|
||||
if (code.isEmpty) {
|
||||
return Left(Failure(''));
|
||||
}
|
||||
return await repository.searchDevice(deviceCode: code);
|
||||
}
|
||||
}
|
||||
@@ -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<Either<Failure, bool>> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<RobotListEvent, RobotListState> {
|
||||
}
|
||||
|
||||
final List<dynamic> 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<RobotListEvent, RobotListState> {
|
||||
}
|
||||
|
||||
final List<dynamic> 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<RobotListEvent, RobotListState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔍 专项日志:只核查 getSiteList 返回体里是否带 productModel 字段
|
||||
/// 整体打印 row 时内容过长会被截断,这里只摘取关键字段 + 型号相关候选字段
|
||||
void _logProductModel(
|
||||
List<dynamic> 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<String, dynamic>.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<String> _findKeyPaths(dynamic node, String key, [String path = '']) {
|
||||
final hits = <String>[];
|
||||
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<RobotListState> emit,
|
||||
|
||||
@@ -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<RobotAppPowerPageV2> {
|
||||
|
||||
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<void> _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<void> _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<RobotAppPowerPageV2> {
|
||||
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<RobotAppPowerPageV2> {
|
||||
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<RobotAppPowerPageV2> {
|
||||
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<RobotAppPowerPageV2> {
|
||||
_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<RobotAppPowerPageV2> {
|
||||
),
|
||||
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<RobotAppPowerPageV2> {
|
||||
),
|
||||
),
|
||||
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<RobotAppPowerPageV2> {
|
||||
),
|
||||
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<RobotAppPowerPageV2> {
|
||||
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<RobotAppPowerPageV2> {
|
||||
),
|
||||
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<RobotAppPowerPageV2> {
|
||||
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<RobotAppPowerPageV2> {
|
||||
|
||||
// ===== 联动磁贴:图标 + 名称 + 锁提示 + 分段按钮 =====
|
||||
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<RobotAppPowerPageV2> {
|
||||
),
|
||||
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<RobotAppPowerPageV2> {
|
||||
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<RobotAppPowerPageV2> {
|
||||
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<RobotAppPowerPageV2> {
|
||||
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<RobotAppPowerPageV2> {
|
||||
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<RobotAppPowerPageV2> {
|
||||
),
|
||||
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<RobotAppPowerPageV2> {
|
||||
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<RobotAppPowerPageV2> {
|
||||
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<RobotAppPowerPageV2> {
|
||||
|
||||
// ===== 开机后解锁的控制:开关机 → 联动功能链 =====
|
||||
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<RobotAppPowerPageV2> {
|
||||
),
|
||||
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<RobotAppPowerPageV2> {
|
||||
),
|
||||
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<RobotAppPowerPageV2> {
|
||||
),
|
||||
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<RobotAppPowerPageV2> {
|
||||
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<RobotAppPowerPageV2> {
|
||||
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<RobotAppPowerPageV2> {
|
||||
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),
|
||||
|
||||
@@ -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<RobotAutonomousWorkPageV2> createState() =>
|
||||
_RobotAutonomousWorkPageV2State();
|
||||
}
|
||||
|
||||
class _RobotAutonomousWorkPageV2State extends State<RobotAutonomousWorkPageV2> {
|
||||
final _traceDiagnostics = WorkTraceDiagnostics();
|
||||
|
||||
/// 与原生 MainActivity 的窗口通道(挖孔区域占用控制)
|
||||
static const MethodChannel _windowChannel = MethodChannel('satabot/window');
|
||||
|
||||
@@ -65,25 +91,71 @@ class _RobotAutonomousWorkPageV2State extends State<RobotAutonomousWorkPageV2> {
|
||||
|
||||
StreamSubscription<WorkTaskSnapshot?>? _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<LatLng> get _previewRoute =>
|
||||
_route?.previewPoints ?? const <LatLng>[];
|
||||
String get _routeName => _route?.name ?? _session?.routeName ?? '未选择作业路线';
|
||||
List<LatLng> get _previewRoute => _route?.previewPoints ?? const <LatLng>[];
|
||||
|
||||
/// 覆盖模式的作业区域边界框(自定义模式为空,与路径规划一致)
|
||||
List<LatLng> get _previewOuter =>
|
||||
_route?.previewOuter ?? const <LatLng>[];
|
||||
List<LatLng> get _previewOuter => _route?.previewOuter ?? const <LatLng>[];
|
||||
|
||||
/// 覆盖模式:地图上画灰色区域框;自定义模式画绿色边界线
|
||||
bool get _previewCoverage => _route?.coverageMode ?? true;
|
||||
|
||||
// ===== 实时设备数据(与「远程遥控」同源:全局 DeviceStatusBloc,MQTT 500ms 节流)=====
|
||||
// 顶部 RTK / 行驶速度、信息卡的 定位 / 割刀状态 / 行驶速度 均取这里,
|
||||
// 数据口径与「设备数据监控」弹窗一致(qual / linearSpeed / knifeCuttingSpeed)。
|
||||
DeviceStatusBloc get _statusBloc => sl<DeviceStatusBloc>();
|
||||
|
||||
/// 从 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<RobotAutonomousWorkPageV2> {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
sl<DeviceStatusBloc>().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<RobotAutonomousWorkPageV2> {
|
||||
@override
|
||||
void dispose() {
|
||||
_taskSub?.cancel();
|
||||
_finishTraceTimer?.cancel();
|
||||
_restoreOrientation();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -142,6 +238,43 @@ class _RobotAutonomousWorkPageV2State extends State<RobotAutonomousWorkPageV2> {
|
||||
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<RobotAutonomousWorkPageV2> {
|
||||
/// - 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<RobotAutonomousWorkPageV2> {
|
||||
}
|
||||
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<RobotAutonomousWorkPageV2> {
|
||||
_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<RobotAutonomousWorkPageV2> {
|
||||
Future<void> _bootstrap() async {
|
||||
final result = await _gateway.loadTasks(deviceId: _deviceId);
|
||||
if (!mounted) return;
|
||||
final tasks = result.data ?? const <WorkTaskSnapshot>[];
|
||||
if (tasks.isEmpty) return;
|
||||
_applySession(tasks.first);
|
||||
if (!result.isOk) {
|
||||
CapsuleToast.show(result.error ?? '任务恢复失败', showCheck: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 先完成进场任务还原,再应用「路线库」带入的路线:
|
||||
/// 设备上有执行中的任务时以任务为准(与在抽屉里换路线的拦截口径一致),
|
||||
/// 否则选中带入路线并画线;列表没带几何时走 resolveRouteGeometry 补全。
|
||||
Future<void> _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<bool> _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<RobotAutonomousWorkPageV2> {
|
||||
|
||||
/// 按 routeId 从路线库取回完整几何(轨迹 + 边界框)
|
||||
Future<void> _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 <WorkRouteOption>[]) {
|
||||
if (route.id == routeId) {
|
||||
await _selectRoute(route);
|
||||
@@ -265,15 +473,12 @@ class _RobotAutonomousWorkPageV2State extends State<RobotAutonomousWorkPageV2> {
|
||||
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<RobotAutonomousWorkPageV2> {
|
||||
if (!mounted) return;
|
||||
|
||||
WorkTaskSnapshot? reuse;
|
||||
if (!pooled.isOk) {
|
||||
CapsuleToast.show(pooled.error ?? '任务查询失败', showCheck: false);
|
||||
return;
|
||||
}
|
||||
final candidates = pooled.data ?? const <WorkTaskSnapshot>[];
|
||||
if (candidates.isNotEmpty) {
|
||||
if (_gateway.capabilities.multiActiveTaskResolve &&
|
||||
@@ -296,9 +505,7 @@ class _RobotAutonomousWorkPageV2State extends State<RobotAutonomousWorkPageV2> {
|
||||
}
|
||||
|
||||
// ② 已有任务跑的是别的路线:不静默覆盖,交给用户先处理
|
||||
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<RobotAutonomousWorkPageV2> {
|
||||
return;
|
||||
}
|
||||
|
||||
_startedByUser = true;
|
||||
_startedByUser = result.data?.state == WorkRunState.queued;
|
||||
_applySession(result.data);
|
||||
CapsuleToast.show('作业已下发:${route.name}');
|
||||
});
|
||||
@@ -336,11 +543,7 @@ class _RobotAutonomousWorkPageV2State extends State<RobotAutonomousWorkPageV2> {
|
||||
context,
|
||||
title: '暂停作业 · 确认',
|
||||
desc: '暂停后保留当前任务ID、路线及作业断点;机器人需要先完成安全停止。',
|
||||
checks: const [
|
||||
'行驶速度降至安全状态',
|
||||
'割刀按任务安全暂停策略停转',
|
||||
'保存断点位置与已覆盖区域',
|
||||
],
|
||||
checks: const ['行驶速度降至安全状态', '割刀按任务安全暂停策略停转', '保存断点位置与已覆盖区域'],
|
||||
confirmLabel: '确认暂停',
|
||||
);
|
||||
if (ok != true || !mounted) return;
|
||||
@@ -367,11 +570,7 @@ class _RobotAutonomousWorkPageV2State extends State<RobotAutonomousWorkPageV2> {
|
||||
context,
|
||||
title: '停止作业 · 二次确认',
|
||||
desc: '本次任务将被终止,不会自动续作。已完成区域、轨迹与停止时间会保存到作业记录。',
|
||||
checks: const [
|
||||
'先安全停止底盘与割刀',
|
||||
'记录人工停止原因与实际完成区域',
|
||||
'已终止任务不能用“继续”恢复',
|
||||
],
|
||||
checks: const ['先安全停止底盘与割刀', '记录人工停止原因与实际完成区域', '已终止任务不能用“继续”恢复'],
|
||||
confirmLabel: '确认停止作业',
|
||||
danger: true,
|
||||
);
|
||||
@@ -402,26 +601,19 @@ class _RobotAutonomousWorkPageV2State extends State<RobotAutonomousWorkPageV2> {
|
||||
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<RobotAutonomousWorkPageV2> {
|
||||
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<RobotAutonomousWorkPageV2> {
|
||||
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<RobotAutonomousWorkPageV2> {
|
||||
}
|
||||
|
||||
void _openRecordPanel() {
|
||||
showWorkRecordPanel(context);
|
||||
showWorkRecordPanel(context, deviceId: _deviceId);
|
||||
}
|
||||
|
||||
void _openSettingPanel() {
|
||||
@@ -671,6 +880,18 @@ class _RobotAutonomousWorkPageV2State extends State<RobotAutonomousWorkPageV2> {
|
||||
// ===== 页面骨架 =====
|
||||
@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<RobotAutonomousWorkPageV2> {
|
||||
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 <LatLng>[],
|
||||
recording: _session?.canRecordTrace ?? false,
|
||||
visible:
|
||||
_phase == WorkRunState.running ||
|
||||
_phase == WorkRunState.paused,
|
||||
),
|
||||
clearTraceSignal: _clearTraceSignal,
|
||||
),
|
||||
),
|
||||
// 不用 SafeArea:本页横屏全屏,需铺满到挖孔/手势条区域
|
||||
@@ -695,6 +928,13 @@ class _RobotAutonomousWorkPageV2State extends State<RobotAutonomousWorkPageV2> {
|
||||
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<RobotAutonomousWorkPageV2> {
|
||||
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<RobotAutonomousWorkPageV2> {
|
||||
),
|
||||
),
|
||||
SizedBox(width: m.topGap),
|
||||
_rtkPill(m),
|
||||
if (!m.topTight) ...[
|
||||
SizedBox(width: m.topGap),
|
||||
_signalPill(m),
|
||||
],
|
||||
BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
|
||||
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<DeviceStatusBloc, DeviceStatusState>(
|
||||
bloc: _statusBloc,
|
||||
builder: (context, ds) =>
|
||||
_batteryPill(m, _batteryOf(_statusOf(ds))),
|
||||
),
|
||||
if (!m.topTight) ...[
|
||||
SizedBox(width: m.topGap),
|
||||
_speedPill(m),
|
||||
BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
|
||||
bloc: _statusBloc,
|
||||
builder: (context, ds) =>
|
||||
_speedPill(m, _speedOf(_statusOf(ds))),
|
||||
),
|
||||
],
|
||||
SizedBox(width: m.topGap),
|
||||
_statusDetailButton(m),
|
||||
@@ -905,23 +1150,26 @@ class _RobotAutonomousWorkPageV2State extends State<RobotAutonomousWorkPageV2> {
|
||||
);
|
||||
}
|
||||
|
||||
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<RobotAutonomousWorkPageV2> {
|
||||
);
|
||||
}
|
||||
|
||||
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<RobotAutonomousWorkPageV2> {
|
||||
);
|
||||
}
|
||||
|
||||
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<RobotAutonomousWorkPageV2> {
|
||||
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<DeviceStatusBloc, DeviceStatusState>(
|
||||
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<RobotAutonomousWorkPageV2> {
|
||||
if (task == null) {
|
||||
switch (_phase) {
|
||||
case WorkRunState.finished:
|
||||
return '本次作业已结束 · 作业记录已保存';
|
||||
return '本次作业已结束';
|
||||
case WorkRunState.running:
|
||||
return '任务执行中';
|
||||
default:
|
||||
@@ -1722,7 +1998,11 @@ class _RobotAutonomousWorkPageV2State extends State<RobotAutonomousWorkPageV2> {
|
||||
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<RobotAutonomousWorkPageV2> {
|
||||
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),
|
||||
];
|
||||
|
||||
@@ -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<RobotFaultCenterPageV2> {
|
||||
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<RobotFaultCenterPageV2> {
|
||||
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<RobotFaultCenterPageV2> {
|
||||
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<RobotFaultCenterPageV2> {
|
||||
}
|
||||
|
||||
/// 已恢复事件 / 故障码说明共用的弹窗
|
||||
void _showFaultDetail(_FaultCode code, {String? recoveredAt}) {
|
||||
void _showFaultDetail(_FaultCode code, {String? recoveredAtKey}) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
@@ -168,24 +177,26 @@ class _RobotFaultCenterPageV2State extends State<RobotFaultCenterPageV2> {
|
||||
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<RobotFaultCenterPageV2> {
|
||||
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<RobotFaultCenterPageV2> {
|
||||
],
|
||||
),
|
||||
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<RobotFaultCenterPageV2> {
|
||||
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<RobotFaultCenterPageV2> {
|
||||
}
|
||||
|
||||
void _openFaultCodeList() {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
@@ -275,18 +293,18 @@ class _RobotFaultCenterPageV2State extends State<RobotFaultCenterPageV2> {
|
||||
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<RobotFaultCenterPageV2> {
|
||||
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<RobotFaultCenterPageV2> {
|
||||
),
|
||||
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<RobotFaultCenterPageV2> {
|
||||
|
||||
@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<RobotFaultCenterPageV2> {
|
||||
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<RobotFaultCenterPageV2> {
|
||||
),
|
||||
),
|
||||
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<RobotFaultCenterPageV2> {
|
||||
|
||||
// ===== 当前故障卡 =====
|
||||
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<RobotFaultCenterPageV2> {
|
||||
|
||||
// ===== 故障筛选 =====
|
||||
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<RobotFaultCenterPageV2> {
|
||||
);
|
||||
}
|
||||
|
||||
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<RobotFaultCenterPageV2> {
|
||||
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<RobotFaultCenterPageV2> {
|
||||
}
|
||||
|
||||
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<RobotFaultCenterPageV2> {
|
||||
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<RobotFaultCenterPageV2> {
|
||||
),
|
||||
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<RobotFaultCenterPageV2> {
|
||||
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<RobotFaultCenterPageV2> {
|
||||
|
||||
// ===== 诊断支持 =====
|
||||
Widget _buildSupportCard() {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
@@ -772,8 +821,8 @@ class _RobotFaultCenterPageV2State extends State<RobotFaultCenterPageV2> {
|
||||
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<RobotFaultCenterPageV2> {
|
||||
),
|
||||
_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<RobotFaultCenterPageV2> {
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<RobotFirmwarePageV2> {
|
||||
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<RobotFirmwarePageV2> {
|
||||
|
||||
/// 检查更新:先做前置条件校验,再明确告知 OTA 通道未接入
|
||||
Future<void> _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<void>.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<RobotFirmwarePageV2> {
|
||||
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<RobotFirmwarePageV2> {
|
||||
),
|
||||
),
|
||||
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<RobotFirmwarePageV2> {
|
||||
),
|
||||
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<RobotFirmwarePageV2> {
|
||||
|
||||
// ===== 当前版本:四行只读 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<RobotFirmwarePageV2> {
|
||||
|
||||
// ===== 升级检查:标题行 + 状态药丸 + 通栏按钮 =====
|
||||
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<RobotFirmwarePageV2> {
|
||||
),
|
||||
),
|
||||
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<RobotFirmwarePageV2> {
|
||||
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<RobotFirmwarePageV2> {
|
||||
],
|
||||
),
|
||||
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<RobotFirmwarePageV2> {
|
||||
|
||||
// ===== 升级前必须确认:勾选清单 =====
|
||||
Widget _buildPrerequisiteCard() {
|
||||
const items = <String>[
|
||||
'机器人处于安全停机/待机状态',
|
||||
'割刀与底盘执行机构已停止',
|
||||
'电源充足且通信稳定',
|
||||
'当前账号具有升级权限',
|
||||
final l10n = AppLocalizations.of(context);
|
||||
const itemKeys = <String>[
|
||||
'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<RobotFirmwarePageV2> {
|
||||
),
|
||||
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<RobotFirmwarePageV2> {
|
||||
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<RobotFirmwarePageV2> {
|
||||
|
||||
// ===== 历史升级记录 =====
|
||||
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<RobotFirmwarePageV2> {
|
||||
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<RobotFirmwarePageV2> {
|
||||
),
|
||||
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<RobotFirmwarePageV2> {
|
||||
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<RobotFirmwarePageV2> {
|
||||
const SizedBox(width: 7),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'版本号与历史记录为示例口径;设备端版本接口与 OTA 通道接入后自动替换为真实数据,'
|
||||
'升级动作会先做二次确认再下发。',
|
||||
AppLocalizations.of(context).translate('robot_firmware_v2.footer'),
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
height: 1.55,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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<RobotLightSoundPageV2> {
|
||||
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<RobotLightSoundPageV2> {
|
||||
|
||||
@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<RobotLightSoundPageV2> {
|
||||
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<RobotLightSoundPageV2> {
|
||||
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<RobotLightSoundPageV2> {
|
||||
),
|
||||
),
|
||||
),
|
||||
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<RobotLightSoundPageV2> {
|
||||
),
|
||||
),
|
||||
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<RobotLightSoundPageV2> {
|
||||
),
|
||||
),
|
||||
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<RobotLightSoundPageV2> {
|
||||
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<RobotLightSoundPageV2> {
|
||||
|
||||
// ===== 灯光控制 =====
|
||||
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<RobotLightSoundPageV2> {
|
||||
|
||||
// ===== 声光提示 =====
|
||||
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<RobotLightSoundPageV2> {
|
||||
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<RobotLightSoundPageV2> {
|
||||
|
||||
// ===== 自动策略 =====
|
||||
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<RobotLightSoundPageV2> {
|
||||
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<RobotLightSoundPageV2> {
|
||||
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<RobotLightSoundPageV2> {
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'遥控协议暂未开放灯光与鸣笛指令位:本页开关只反映界面状态,'
|
||||
'音量、鸣笛时长与自动策略为本地偏好。真机控制需设备端开放对应通道。',
|
||||
AppLocalizations.of(context).translate('robot_light_v2.footer'),
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
height: 1.55,
|
||||
|
||||
@@ -649,6 +649,7 @@ class _RobotListViewState extends State<RobotListView> {
|
||||
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<RobotListView> {
|
||||
'🔴🔴🔴 [选中机器人] 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,走正常控制流程');
|
||||
|
||||
@@ -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<RobotLocationPageV2> {
|
||||
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<RobotLocationPageV2> {
|
||||
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<RobotLocationPageV2> {
|
||||
),
|
||||
),
|
||||
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<RobotLocationPageV2> {
|
||||
),
|
||||
),
|
||||
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<RobotLocationPageV2> {
|
||||
|
||||
// ===== 地图卡 =====
|
||||
Widget _buildMapCard() {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
@@ -350,27 +375,31 @@ class _RobotLocationPageV2State extends State<RobotLocationPageV2> {
|
||||
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<RobotLocationPageV2> {
|
||||
|
||||
// ===== 位置数据卡 =====
|
||||
Widget _buildLocationDataCard() {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
@@ -420,15 +450,26 @@ class _RobotLocationPageV2State extends State<RobotLocationPageV2> {
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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<Widget> _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<Widget> _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'))),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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<RobotOwnershipPageV2> createState() => _RobotOwnershipPageV2State();
|
||||
@@ -33,53 +53,222 @@ class _RobotOwnershipPageV2State extends State<RobotOwnershipPageV2> {
|
||||
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<OrgEntity> _orgs = const [];
|
||||
|
||||
bool get _online => widget.robot.isOnline;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadOrgs();
|
||||
}
|
||||
|
||||
Future<void> _loadOrgs() async {
|
||||
final result = await sl<GetOrgListUseCase>()();
|
||||
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<void> _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<void> _transferAdmin() async {
|
||||
final targetDeviceId = widget.robot.name.isNotEmpty
|
||||
? widget.robot.name
|
||||
: widget.robot.id;
|
||||
debugPrint(
|
||||
'>>> [Ownership] 改名下发 deviceId=$targetDeviceId alias=$newName',
|
||||
);
|
||||
|
||||
final result = await sl<UpdateDeviceAliasUseCase>().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<void> _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<void> _transferOwnership() async {
|
||||
/// 解绑设备:确认 → 扫码核验 → 选解除归属层级 → 下发解绑
|
||||
///
|
||||
/// 解绑不可逆,所以保留红色二次确认,再接扫码与层级选择;
|
||||
/// 成功后向上一级回传 true,由设备列表重拉统计与机器人数据。
|
||||
Future<void> _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<void> _openActionPage(OwnershipAction action) async {
|
||||
final done = await Navigator.of(context).push<bool>(
|
||||
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<AppUserCubit>().state.user?.roleKey ?? 'user';
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFEDF6FD),
|
||||
body: Container(
|
||||
@@ -102,11 +291,11 @@ class _RobotOwnershipPageV2State extends State<RobotOwnershipPageV2> {
|
||||
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<RobotOwnershipPageV2> {
|
||||
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<RobotOwnershipPageV2> {
|
||||
),
|
||||
),
|
||||
),
|
||||
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<RobotOwnershipPageV2> {
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 卡内标题:18 w800 =====
|
||||
Widget _buildCardTitle(String title, {Color color = _navy}) {
|
||||
return Text(
|
||||
title,
|
||||
@@ -166,118 +355,313 @@ class _RobotOwnershipPageV2State extends State<RobotOwnershipPageV2> {
|
||||
);
|
||||
}
|
||||
|
||||
// ===== 设备归属信息 =====
|
||||
Widget _buildOwnershipCard() {
|
||||
Widget _buildCard({
|
||||
required List<Widget> 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<RobotOwnershipPageV2> {
|
||||
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 = <String>[];
|
||||
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<RobotOwnershipPageV2> {
|
||||
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<RobotOwnershipPageV2> {
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// 整行都可点:只让小图标可点的话,手指很难点中
|
||||
if (onTap == null) return row;
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: row,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooterNote() {
|
||||
@@ -381,11 +865,10 @@ class _RobotOwnershipPageV2State extends State<RobotOwnershipPageV2> {
|
||||
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<RobotOwnershipPageV2> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 权限层级的三态
|
||||
enum _LevelState { full, self, none }
|
||||
|
||||
@@ -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<RobotRunRecordPageV2> {
|
||||
/// 三个可切换档位(数据为示例,仅用于展示图表形态)
|
||||
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<RobotRunRecordPageV2> {
|
||||
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<RobotRunRecordPageV2> {
|
||||
),
|
||||
),
|
||||
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<RobotRunRecordPageV2> {
|
||||
|
||||
// ===== 时间范围:近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<RobotRunRecordPageV2> {
|
||||
|
||||
// ===== 运行数据概况:三块浅蓝统计 =====
|
||||
Widget _buildOverviewCard() {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
@@ -324,24 +378,24 @@ class _RobotRunRecordPageV2State extends State<RobotRunRecordPageV2> {
|
||||
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<RobotRunRecordPageV2> {
|
||||
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<RobotRunRecordPageV2> {
|
||||
),
|
||||
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<RobotRunRecordPageV2> {
|
||||
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<RobotRunRecordPageV2> {
|
||||
}
|
||||
|
||||
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<RobotRunRecordPageV2> {
|
||||
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<RobotRunRecordPageV2> {
|
||||
),
|
||||
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),
|
||||
|
||||
@@ -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<RobotSensorPageV2> {
|
||||
static const Color _divider = Color(0xFFF0F4FA);
|
||||
static const Color _trackBg = Color(0xFFDFEBF9);
|
||||
|
||||
static const List<String> _tabs = ['超声波', '定位', '通信', '机构'];
|
||||
static const List<String> _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<RobotSensorPageV2> {
|
||||
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<RobotSensorPageV2> {
|
||||
),
|
||||
),
|
||||
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<RobotSensorPageV2> {
|
||||
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<RobotSensorPageV2> {
|
||||
),
|
||||
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<RobotSensorPageV2> {
|
||||
|
||||
// ===== 超声波 =====
|
||||
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<RobotSensorPageV2> {
|
||||
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<RobotSensorPageV2> {
|
||||
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<RobotSensorPageV2> {
|
||||
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<RobotSensorPageV2> {
|
||||
),
|
||||
),
|
||||
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<RobotSensorPageV2> {
|
||||
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<RobotSensorPageV2> {
|
||||
|
||||
// ===== 定位 =====
|
||||
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<RobotSensorPageV2> {
|
||||
|
||||
// ===== 通信 =====
|
||||
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<RobotSensorPageV2> {
|
||||
|
||||
// ===== 机构 =====
|
||||
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<RobotSensorPageV2> {
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<VoicePreset> 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<RobotVoicePresetPageV2> {
|
||||
|
||||
List<VoicePreset> _presets = <VoicePreset>[];
|
||||
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<RobotVoicePresetPageV2> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final presets = await VoicePresetStore.loadPresets(widget.robot.name);
|
||||
Future<void> _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<RobotVoicePresetPageV2> {
|
||||
});
|
||||
}
|
||||
|
||||
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<RobotVoicePresetPageV2> {
|
||||
}
|
||||
|
||||
Future<void> _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<RobotVoicePresetPageV2> {
|
||||
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<RobotVoicePresetPageV2> {
|
||||
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<RobotVoicePresetPageV2> {
|
||||
),
|
||||
),
|
||||
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<RobotVoicePresetPageV2> {
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: _todo,
|
||||
onTap: () => _todo(),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
width: 36,
|
||||
@@ -303,11 +354,11 @@ class _RobotVoicePresetPageV2State extends State<RobotVoicePresetPageV2> {
|
||||
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<RobotVoicePresetPageV2> {
|
||||
|
||||
// ===== 编辑语音内容 =====
|
||||
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<RobotVoicePresetPageV2> {
|
||||
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<RobotVoicePresetPageV2> {
|
||||
],
|
||||
),
|
||||
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<RobotVoicePresetPageV2> {
|
||||
|
||||
// ===== 实时喊话(第二阶段)=====
|
||||
Widget _buildRealtimeCard() {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return GestureDetector(
|
||||
onTap: _realtimeTalk,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
@@ -596,26 +658,30 @@ class _RobotVoicePresetPageV2State extends State<RobotVoicePresetPageV2> {
|
||||
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<RobotVoicePresetPageV2> {
|
||||
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,
|
||||
|
||||
@@ -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<void> showWorkRecordPanel(BuildContext context, {String? initialRoute}) {
|
||||
/// 展示后端任务池和本机实际收到的完成记录,不使用设计稿示例数据。
|
||||
Future<void> 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<WorkResult<List<WorkTaskSnapshot>>> _records = _load();
|
||||
Future<WorkResult<List<WorkTaskSnapshot>>> _load() async {
|
||||
final gateway = sl<AutonomousWorkGateway>();
|
||||
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<WorkResult<List<WorkTaskSnapshot>>>(
|
||||
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 <WorkTaskSnapshot>[];
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <WorkRouteOption>[];
|
||||
_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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<VoicePreset> _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<void> _load() async {
|
||||
final presets = await VoicePresetStore.loadPresets(widget.robot.name);
|
||||
Future<void> _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<void> _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,
|
||||
|
||||
Reference in New Issue
Block a user