机器列表和远程喊话等ui
This commit is contained in:
@@ -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