机器列表和远程喊话等ui

This commit is contained in:
2026-11-06 08:47:36 +08:00
parent e6165b180b
commit 84eec7da96
25 changed files with 2662 additions and 1214 deletions

View File

@@ -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),
),
],
),
),
),
],
),
);
}
}

View File

@@ -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);

View File

@@ -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),

View File

@@ -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,
),
],
],
),
),

View File

@@ -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),

View File

@@ -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,

View File

@@ -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,