feat(i18n): 完成全量 UI 文本国际化,替换所有硬编码中文为 AppLocalizations 调用

- core/localization: 新增约 60 个翻译键(含参数化方法),中英双语覆盖
- shared/widgets: CommonDialog 默认参数国际化
- features/home: 完成页操作步骤指引、状态栏串口连接状态、程序列表状态标签
- features/programs: 表头状态列、表单验证提示、导入/模板操作反馈、删除确认(参数化)
- features/program_detail: 步骤列表/表单标题、删除确认、速度档位显示(参数化)
- features/device: run_state_provider 错误消息改为错误码
- features/settings: 升级页、密码面板、语言面板、U盘导入面板、串口配置面板全部替换

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Developer
2026-06-12 15:09:47 +08:00
parent 5d65744618
commit 3d849bd468
23 changed files with 688 additions and 127 deletions

View File

@@ -103,7 +103,7 @@ class RunStateNotifier extends StateNotifier<RunState> {
if (steps.isEmpty) {
state = state.copyWith(
status: RunStatus.error,
errorMessage: '程序步骤为空',
errorMessage: 'PROGRAM_STEPS_EMPTY',
);
return;
}

View File

@@ -89,7 +89,7 @@ class CompletePage extends ConsumerWidget {
// 标题
Text(
l10n?.runComplete ?? '程序运行完成',
l10n?.programRunComplete ?? '程序运行完成',
style: TextStyle(
color: AppTheme.textPrimary,
fontSize: 24,
@@ -121,7 +121,7 @@ class CompletePage extends ConsumerWidget {
const SizedBox(height: 20),
// 操作示意图
_buildOperationGuide(),
_buildOperationGuide(context),
const SizedBox(height: 24),
@@ -167,7 +167,8 @@ class CompletePage extends ConsumerWidget {
}
/// 操作指引示意图
Widget _buildOperationGuide() {
Widget _buildOperationGuide(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
@@ -178,7 +179,7 @@ class CompletePage extends ConsumerWidget {
child: Column(
children: [
Text(
'操作步骤',
l10n?.operationSteps ?? '操作步骤',
style: TextStyle(
color: AppTheme.textPrimary,
fontSize: 14,
@@ -189,10 +190,10 @@ class CompletePage extends ConsumerWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildStepItem(1, '取出样本', Icons.science),
_buildStepItem(2, '滴入检测卡', Icons.water_drop),
_buildStepItem(3, '等待反应', Icons.timer),
_buildStepItem(4, '查看结果', Icons.visibility),
_buildStepItem(1, l10n?.extractSample ?? '取出样本', Icons.science),
_buildStepItem(2, l10n?.dropToTestCard ?? '滴入检测卡', Icons.water_drop),
_buildStepItem(3, l10n?.waitForReaction ?? '等待反应', Icons.timer),
_buildStepItem(4, l10n?.viewResults ?? '查看结果', Icons.visibility),
],
),
],

View File

@@ -102,6 +102,7 @@ class _ProgramCard extends StatelessWidget {
final dateFormat = DateFormat('yyyy-MM-dd HH:mm');
final createdAt = _parseDate(program.createdAt);
final l10n = AppLocalizations.of(context);
return Material(
color: Colors.transparent,
child: InkWell(
@@ -168,7 +169,9 @@ class _ProgramCard extends StatelessWidget {
borderRadius: BorderRadius.circular(4),
),
child: Text(
program.status == 1 ? '启用' : '停用',
program.status == 1
? (l10n?.enabled ?? '启用')
: (l10n?.disabled ?? '停用'),
style: TextStyle(
color: program.status == 1
? AppTheme.statusRunning

View File

@@ -263,13 +263,14 @@ class _SerialConnectionIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final connected = state == SerialConnectionState.connected;
final connecting = state == SerialConnectionState.connecting;
final text = connected
? '已连接'
? (l10n?.serialConnected ?? '已连接')
: connecting
? '连接中'
: '未连接';
? (l10n?.serialConnecting ?? '连接中')
: (l10n?.serialDisconnected ?? '未连接');
final color = connected
? AppTheme.statusRunning
: connecting

View File

@@ -107,7 +107,7 @@ class _ProgramDetailPageState extends ConsumerState<ProgramDetailPage> {
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('步骤已更新'),
content: Text(l10n?.stepUpdated ?? '步骤已更新'),
backgroundColor: AppTheme.successColor,
),
);
@@ -125,7 +125,7 @@ class _ProgramDetailPageState extends ConsumerState<ProgramDetailPage> {
),
const SizedBox(height: 16),
Text(
'请选择或添加步骤',
l10n?.selectStepFirst ?? '请选择或添加步骤',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 16,
@@ -164,7 +164,7 @@ class _ProgramDetailPageState extends ConsumerState<ProgramDetailPage> {
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
tooltip: l10n?.backToHome ?? '返回',
tooltip: l10n?.back ?? '返回',
onPressed: () => context.go('/?tab=1'),
),
const SizedBox(width: 8),
@@ -184,7 +184,7 @@ class _ProgramDetailPageState extends ConsumerState<ProgramDetailPage> {
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已保存'),
content: Text(l10n?.saved ?? '已保存'),
backgroundColor: AppTheme.successColor,
),
);

View File

@@ -72,7 +72,7 @@ class _StepFormState extends State<StepForm> {
children: [
// 标题
Text(
widget.isNew ? '添加步骤' : '编辑步骤',
widget.isNew ? (l10n?.addStep ?? '添加步骤') : (l10n?.editStep ?? '编辑步骤'),
style: TextStyle(
color: AppTheme.textPrimary,
fontSize: 18,
@@ -86,12 +86,12 @@ class _StepFormState extends State<StepForm> {
controller: _nameController,
decoration: InputDecoration(
labelText: l10n?.stepName ?? '步骤名称',
hintText: '例如: 混合、吸磁、吹气',
hintText: l10n?.hintStepName ?? '例如: 混合、吸磁、吹气',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入步骤名称';
return l10n?.enterStepName ?? '请输入步骤名称';
}
return null;
},
@@ -180,7 +180,7 @@ class _StepFormState extends State<StepForm> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${l10n?.speed ?? '速度'}: $_speed',
'${l10n?.speed ?? '速度'}: ${l10n?.speedLevelValue(_speed) ?? '$_speed'}',
style: TextStyle(color: AppTheme.textPrimary),
),
Slider(

View File

@@ -53,7 +53,7 @@ class _StepListState extends State<StepList> {
Icon(Icons.list, color: AppTheme.primaryColor, size: 20),
const SizedBox(width: 12),
Text(
'步骤列表',
l10n?.stepList ?? '步骤列表',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
@@ -61,7 +61,7 @@ class _StepListState extends State<StepList> {
),
const Spacer(),
Text(
'${widget.steps.length}',
l10n?.stepsCountLabel(widget.steps.length) ?? '${widget.steps.length}',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 12,
@@ -114,7 +114,7 @@ class _StepListState extends State<StepList> {
children: [
Icon(Icons.add_circle_outline, size: 48, color: AppTheme.idleColor),
const SizedBox(height: 12),
Text('暂无步骤', style: TextStyle(color: AppTheme.textSecondary)),
Text(l10n?.noSteps ?? '暂无步骤', style: TextStyle(color: AppTheme.textSecondary)),
],
),
)
@@ -149,7 +149,7 @@ class _StepListState extends State<StepList> {
children: [
// 添加按钮
CommonButton(
text: '添加',
text: l10n?.add ?? '添加',
icon: Icons.add,
type: ButtonType.primary,
onPressed: widget.onAddStep,
@@ -158,7 +158,7 @@ class _StepListState extends State<StepList> {
// 删除按钮
if (_selectedIds.isNotEmpty)
CommonButton(
text: '删除',
text: l10n?.delete ?? '删除',
icon: Icons.delete,
type: ButtonType.danger,
onPressed: () => _showDeleteConfirmDialog(context),
@@ -241,8 +241,8 @@ class _StepListState extends State<StepList> {
title: Text(l10n?.confirm ?? '确认'),
content: Text(
_selectedIds.length == 1
? '确定要删除此步骤吗?'
: '确定要删除选中的 ${_selectedIds.length} 个步骤吗?',
? l10n?.deleteStepConfirmSingle ?? '确定要删除此步骤吗?'
: l10n?.deleteStepConfirmMultiple(_selectedIds.length) ?? '确定要删除选中的 ${_selectedIds.length} 个步骤吗?',
),
actions: [
TextButton(

View File

@@ -259,7 +259,7 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
SizedBox(
width: 80,
child: Text(
'状态',
l10n?.status ?? '状态',
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
@@ -358,7 +358,7 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
borderRadius: BorderRadius.circular(4),
),
child: Text(
program.status == 1 ? '启用' : '停用',
program.status == 1 ? (l10n?.enabled ?? '启用') : (l10n?.disabled ?? '停用'),
style: TextStyle(
color: program.status == 1
? AppTheme.successColor
@@ -413,6 +413,7 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
/// 导入程序
Future<void> _importPrograms(BuildContext context, WidgetRef ref) async {
final l10n = AppLocalizations.of(context);
try {
// 选择文件
final result = await FilePicker.platform.pickFiles(
@@ -428,7 +429,7 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
final file = result.files.first;
if (file.path == null) {
if (!context.mounted) return;
ToastService.showError(context, '无法读取文件');
ToastService.showError(context, l10n?.cannotReadFile ?? '无法读取文件');
return;
}
@@ -441,25 +442,26 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
if (!context.mounted) return;
if (importedCount > 0) {
ToastService.showSuccess(context, '成功处理 $importedCount 个程序');
ToastService.showSuccess(context, l10n?.processedPrograms(importedCount) ?? '成功处理 $importedCount 个程序');
} else {
ToastService.showWarning(context, 'Excel 中无有效程序数据');
ToastService.showWarning(context, l10n?.noValidProgramData ?? 'Excel 中无有效程序数据');
}
} catch (e) {
if (!context.mounted) return;
ToastService.showError(context, '导入失败: ${e.toString()}');
ToastService.showError(context, '${l10n?.importFailed ?? '导入失败'}: ${e.toString()}');
}
}
/// 下载 Excel 模板
Future<void> _downloadTemplate(BuildContext context) async {
final l10n = AppLocalizations.of(context);
try {
final path = await ExcelTemplateService.instance.generateTemplate();
if (!context.mounted) return;
ToastService.showSuccess(context, '模板已保存: $path');
ToastService.showSuccess(context, '${l10n?.templateSaved ?? '模板已保存'}: $path');
} catch (e) {
if (!context.mounted) return;
ToastService.showError(context, '生成模板失败: ${e.toString()}');
ToastService.showError(context, '${l10n?.generateTemplateFailed ?? '生成模板失败'}: ${e.toString()}');
}
}
@@ -484,8 +486,8 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
title: Text(l10n?.confirm ?? '确认'),
content: Text(
ids.length == 1
? '确定要删除此程序吗?'
: '确定要删除选中的 ${ids.length} 个程序吗?',
? (l10n?.deleteConfirmSingle ?? '确定要删除此程序吗?')
: (l10n?.deleteConfirmMultiple(ids.length) ?? '确定要删除选中的 ${ids.length} 个程序吗?'),
),
actions: [
TextButton(

View File

@@ -71,14 +71,14 @@ class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
controller: _codeController,
decoration: InputDecoration(
labelText: l10n?.programCode ?? '编号',
hintText: '例如: P001',
hintText: l10n?.hintCode ?? '例如: P001',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入编号';
return l10n?.enterCode ?? '请输入编号';
}
return null;
},
@@ -90,14 +90,14 @@ class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
controller: _nameController,
decoration: InputDecoration(
labelText: l10n?.programName ?? '名称',
hintText: '请输入程序名称',
hintText: l10n?.hintProgramName ?? '请输入程序名称',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入名称';
return l10n?.enterName ?? '请输入名称';
}
return null;
},
@@ -140,7 +140,7 @@ class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
Row(
children: [
Text(
'状态',
l10n?.status ?? '状态',
style: TextStyle(color: AppTheme.textPrimary),
),
const Spacer(),
@@ -154,7 +154,7 @@ class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
activeColor: AppTheme.successColor,
),
Text(
_isEnabled ? '启用' : '停用',
_isEnabled ? (l10n?.enabled ?? '启用') : (l10n?.disabled ?? '停用'),
style: TextStyle(
color: _isEnabled ? AppTheme.successColor : AppTheme.idleColor,
),
@@ -222,7 +222,7 @@ class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('保存失败,请检查编号是否重复'),
content: Text(l10n?.saveFailed ?? '保存失败,请检查编号是否重复'),
backgroundColor: AppTheme.errorColor,
),
);

View File

@@ -116,11 +116,12 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
}
Widget _buildUpgradeContent() {
final l10n = AppLocalizations.of(context);
return ListView(
padding: EdgeInsets.zero,
children: [
Text(
'软件升级',
l10n?.upgrade ?? '软件升级',
style: TextStyle(
color: AppTheme.textPrimary,
fontSize: 18,
@@ -139,7 +140,7 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
Icon(Icons.info_outline, color: AppTheme.primaryColor),
const SizedBox(width: 12),
Text(
'当前版本: $_currentVersion',
'${l10n?.currentVersion ?? '当前版本'}: $_currentVersion',
style: TextStyle(color: AppTheme.textPrimary),
),
],
@@ -147,13 +148,13 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
),
const SizedBox(height: 24),
CommonButton(
text: '检查更新',
text: l10n?.checkUpdate ?? '检查更新',
icon: Icons.refresh,
type: ButtonType.primary,
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已是最新版本'),
content: Text(l10n?.latestVersion ?? '已是最新版本'),
backgroundColor: AppTheme.successColor,
),
);

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/localization/locale_provider.dart';
import '../../../core/theme/app_theme.dart';
@@ -13,6 +14,7 @@ class LanguagePanel extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final locale = ref.watch(localeProvider);
final currentLang = locale.languageCode;
@@ -20,7 +22,7 @@ class LanguagePanel extends ConsumerWidget {
padding: EdgeInsets.zero,
children: [
_SectionCard(
title: '语言设置',
title: l10n?.languageSettings ?? '语言设置',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -46,7 +48,7 @@ class LanguagePanel extends ConsumerWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
'切换语言后立即生效',
l10n?.switchLanguageEffect ?? '切换语言后立即生效',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 12),
),
),

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/services/toast_service.dart';
import '../services/settings_service.dart';
@@ -33,20 +34,21 @@ class _PasswordPanelState extends ConsumerState<PasswordPanel> {
Future<void> _submit() async {
if (_submitting) return;
final l10n = AppLocalizations.of(context);
final oldPwd = _oldCtrl.text.trim();
final newPwd = _newCtrl.text.trim();
final confirmPwd = _confirmCtrl.text.trim();
if (oldPwd.isEmpty || newPwd.isEmpty || confirmPwd.isEmpty) {
setState(() => _error = '请填写所有字段');
setState(() => _error = l10n?.fillAllFields ?? '请填写所有字段');
return;
}
if (newPwd.length < 6) {
setState(() => _error = '新密码至少6位字符');
setState(() => _error = l10n?.newPwdMinLength ?? '新密码至少6位字符');
return;
}
if (newPwd != confirmPwd) {
setState(() => _error = '两次输入的新密码不一致');
setState(() => _error = l10n?.passwordMismatch ?? '两次输入的新密码不一致');
return;
}
@@ -60,7 +62,7 @@ class _PasswordPanelState extends ConsumerState<PasswordPanel> {
if (!mounted) return;
setState(() {
_submitting = false;
_error = '原密码错误';
_error = l10n?.oldPasswordError ?? '原密码错误';
});
return;
}
@@ -73,14 +75,15 @@ class _PasswordPanelState extends ConsumerState<PasswordPanel> {
_oldCtrl.clear();
_newCtrl.clear();
_confirmCtrl.clear();
ToastService.showSuccess(context, '密码已修改');
ToastService.showSuccess(context, l10n?.passwordChanged ?? '密码已修改');
} else {
ToastService.showError(context, '密码修改失败');
ToastService.showError(context, l10n?.passwordChangeFailed ?? '密码修改失败');
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return ListView(
padding: EdgeInsets.zero,
children: [
@@ -95,7 +98,7 @@ class _PasswordPanelState extends ConsumerState<PasswordPanel> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'密码修改',
l10n?.password ?? '密码修改',
style: TextStyle(
color: AppTheme.textPrimary,
fontSize: 16,
@@ -105,20 +108,20 @@ class _PasswordPanelState extends ConsumerState<PasswordPanel> {
const Divider(),
_field(
controller: _oldCtrl,
label: '原密码',
hint: '请输入当前密码',
label: l10n?.oldPassword ?? '原密码',
hint: l10n?.enterCurrentPassword ?? '请输入当前密码',
),
const SizedBox(height: 16),
_field(
controller: _newCtrl,
label: '新密码',
hint: '至少6位字符',
label: l10n?.newPassword ?? '新密码',
hint: l10n?.passwordMinLength ?? '至少6位字符',
),
const SizedBox(height: 16),
_field(
controller: _confirmCtrl,
label: '确认新密码',
hint: '再次输入新密码',
label: l10n?.confirmNewPassword ?? '确认新密码',
hint: l10n?.enterNewPassword ?? '再次输入新密码',
),
if (_error != null) ...[
const SizedBox(height: 12),
@@ -140,7 +143,7 @@ class _PasswordPanelState extends ConsumerState<PasswordPanel> {
_confirmCtrl.clear();
setState(() => _error = null);
},
child: const Text('重置'),
child: Text(l10n?.reset ?? '重置'),
),
const SizedBox(width: 12),
ElevatedButton.icon(
@@ -155,7 +158,7 @@ class _PasswordPanelState extends ConsumerState<PasswordPanel> {
),
)
: const Icon(Icons.check, size: 18),
label: const Text('确认'),
label: Text(l10n?.confirm ?? '确认'),
),
],
),
@@ -166,7 +169,7 @@ class _PasswordPanelState extends ConsumerState<PasswordPanel> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
'默认密码为 123456',
l10n?.defaultPassword ?? '默认密码为 123456',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 12),
),
),

View File

@@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:usb_serial/usb_serial.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../device/models/serial_config.dart';
import '../../device/providers/serial_provider.dart';
@@ -48,14 +49,14 @@ class _SerialConfigPanelState extends ConsumerState<SerialConfigPanel> {
} catch (e) {
if (!mounted) return;
setState(() => _loadingDevices = false);
_showSnack('扫描设备失败: $e', AppTheme.errorColor);
_showSnack('${AppLocalizations.of(context)?.scanFailed ?? '扫描设备失败'}: $e', AppTheme.errorColor);
}
}
Future<void> _connect() async {
final device = _selectedDevice;
if (device == null) {
_showSnack('请先选择串口设备', AppTheme.warningColor);
_showSnack(AppLocalizations.of(context)?.selectSerialFirst ?? '请先选择串口设备', AppTheme.warningColor);
return;
}
setState(() => _operating = true);
@@ -65,7 +66,7 @@ class _SerialConfigPanelState extends ConsumerState<SerialConfigPanel> {
if (!mounted) return;
setState(() => _operating = false);
_showSnack(
ok ? '连接成功' : '连接失败: ${service.lastError ?? "未知错误"}',
ok ? (AppLocalizations.of(context)?.connectSuccess ?? '连接成功') : ('${AppLocalizations.of(context)?.connectFailed ?? '连接失败'}: ${service.lastError ?? (AppLocalizations.of(context)?.unknownError ?? '未知错误')}'),
ok ? AppTheme.successColor : AppTheme.errorColor,
);
}
@@ -76,13 +77,13 @@ class _SerialConfigPanelState extends ConsumerState<SerialConfigPanel> {
await service.disconnect();
if (!mounted) return;
setState(() => _operating = false);
_showSnack('已断开串口', AppTheme.infoColor);
_showSnack(AppLocalizations.of(context)?.disconnected ?? '已断开串口', AppTheme.infoColor);
}
Future<void> _testConnection() async {
final service = ref.read(serialPortServiceProvider);
if (!service.isConnected) {
_showSnack('请先连接串口', AppTheme.warningColor);
_showSnack(AppLocalizations.of(context)?.connectFirst ?? '请先连接串口', AppTheme.warningColor);
return;
}
final msgService = ref.read(deviceMessageServiceProvider);
@@ -94,7 +95,7 @@ class _SerialConfigPanelState extends ConsumerState<SerialConfigPanel> {
));
if (!mounted) return;
_showSnack(
ok ? '已发送 device_info 查询' : '发送失败',
ok ? (AppLocalizations.of(context)?.sendTestFrame ?? '已发送测试查询') : (AppLocalizations.of(context)?.connectFailed ?? '发送失败'),
ok ? AppTheme.successColor : AppTheme.errorColor,
);
}
@@ -107,25 +108,26 @@ class _SerialConfigPanelState extends ConsumerState<SerialConfigPanel> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final config = ref.watch(serialConfigProvider);
final state = ref.watch(serialPortServiceProvider).state;
return ListView(
padding: const EdgeInsets.all(0),
children: [
_buildStatusCard(state),
_buildStatusCard(state, l10n),
const SizedBox(height: 16),
_buildDeviceCard(),
_buildDeviceCard(l10n),
const SizedBox(height: 16),
_buildParamCard(config),
_buildParamCard(config, l10n),
const SizedBox(height: 16),
_buildActionRow(),
_buildActionRow(l10n),
],
);
}
// -- 状态卡 ----------------------------------------------------------
Widget _buildStatusCard(SerialConnectionState state) {
Widget _buildStatusCard(SerialConnectionState state, AppLocalizations? l10n) {
final color = switch (state) {
SerialConnectionState.connected => AppTheme.successColor,
SerialConnectionState.connecting => AppTheme.warningColor,
@@ -133,10 +135,10 @@ class _SerialConfigPanelState extends ConsumerState<SerialConfigPanel> {
SerialConnectionState.disconnected => AppTheme.idleColor,
};
final text = switch (state) {
SerialConnectionState.connected => '已连接',
SerialConnectionState.connecting => '连接中...',
SerialConnectionState.error => '错误',
SerialConnectionState.disconnected => '未连接',
SerialConnectionState.connected => l10n?.serialConnected ?? '已连接',
SerialConnectionState.connecting => l10n?.serialConnecting ?? '连接中...',
SerialConnectionState.error => l10n?.serialError ?? '错误',
SerialConnectionState.disconnected => l10n?.serialDisconnected ?? '未连接',
};
return Container(
padding: const EdgeInsets.all(16),
@@ -153,7 +155,7 @@ class _SerialConfigPanelState extends ConsumerState<SerialConfigPanel> {
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 12),
Text('串口状态: $text',
Text('${l10n?.serialStatus ?? '串口状态'}: $text',
style: TextStyle(
color: AppTheme.textPrimary,
fontSize: 16,
@@ -166,13 +168,13 @@ class _SerialConfigPanelState extends ConsumerState<SerialConfigPanel> {
// -- 设备列表 --------------------------------------------------------
Widget _buildDeviceCard() {
Widget _buildDeviceCard(AppLocalizations? l10n) {
return _SectionCard(
title: '可用串口设备',
title: l10n?.availableSerialDevices ?? '可用串口设备',
trailing: TextButton.icon(
onPressed: _loadingDevices ? null : _refreshDevices,
icon: const Icon(Icons.refresh, size: 18),
label: const Text('刷新'),
label: Text(l10n?.refresh ?? '刷新'),
),
child: _loadingDevices
? const Padding(
@@ -190,16 +192,17 @@ class _SerialConfigPanelState extends ConsumerState<SerialConfigPanel> {
}
Widget _buildEmptyDevice() {
final l10n = AppLocalizations.of(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
child: Column(
children: [
Icon(Icons.usb_off, size: 40, color: AppTheme.idleColor),
const SizedBox(height: 8),
Text('未检测到 USB 串口设备',
Text(l10n?.noSerialDevice ?? '未检测到 USB 串口设备',
style: TextStyle(color: AppTheme.textSecondary)),
const SizedBox(height: 4),
Text('请确认下位机已上电并通过 USB 接入设备',
Text(l10n?.serialDeviceHint ?? '请确认下位机已上电并通过 USB 接入设备',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 12)),
],
),
@@ -255,45 +258,45 @@ class _SerialConfigPanelState extends ConsumerState<SerialConfigPanel> {
// -- 参数配置 --------------------------------------------------------
Widget _buildParamCard(SerialConfig config) {
Widget _buildParamCard(SerialConfig config, AppLocalizations? l10n) {
return _SectionCard(
title: '串口参数',
title: l10n?.serialParams ?? '串口参数',
child: Column(
children: [
_baudRateRow(config),
_baudRateRow(config, l10n),
_dropdownRow<int>(
label: '数据位',
label: l10n?.dataBits ?? '数据位',
value: config.dataBits,
options: const [5, 6, 7, 8],
display: (v) => '$v',
onChanged: (v) => _updateConfig((c) => c.copyWith(dataBits: v)),
),
_dropdownRow<int>(
label: '停止位',
label: l10n?.stopBits ?? '停止位',
value: config.stopBits,
options: const [1, 2],
display: (v) => '$v',
onChanged: (v) => _updateConfig((c) => c.copyWith(stopBits: v)),
),
_dropdownRow<SerialParity>(
label: '校验位',
label: l10n?.parity ?? '校验位',
value: config.parity,
options: SerialParity.values,
display: (v) => switch (v) {
SerialParity.none => '',
SerialParity.odd => '',
SerialParity.even => '',
SerialParity.mark => '标记',
SerialParity.space => '',
SerialParity.none => l10n?.parityNone ?? '',
SerialParity.odd => l10n?.parityOdd ?? '',
SerialParity.even => l10n?.parityEven ?? '',
SerialParity.mark => l10n?.parityMark ?? '标记',
SerialParity.space => l10n?.paritySpace ?? '',
},
onChanged: (v) => _updateConfig((c) => c.copyWith(parity: v)),
),
_dropdownRow<SerialFlowControl>(
label: '流控',
label: l10n?.flowControl ?? '流控',
value: config.flowControl,
options: SerialFlowControl.values,
display: (v) => switch (v) {
SerialFlowControl.none => '',
SerialFlowControl.none => l10n?.parityNone ?? '',
SerialFlowControl.rtsCts => 'RTS/CTS',
SerialFlowControl.xonXoff => 'XON/XOFF',
SerialFlowControl.dtrDsr => 'DTR/DSR',
@@ -304,7 +307,7 @@ class _SerialConfigPanelState extends ConsumerState<SerialConfigPanel> {
Align(
alignment: Alignment.centerRight,
child: Text(
'参数修改后自动保存',
l10n?.autoSaveParams ?? '参数修改后自动保存',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 12),
),
),
@@ -313,7 +316,7 @@ class _SerialConfigPanelState extends ConsumerState<SerialConfigPanel> {
);
}
Widget _baudRateRow(SerialConfig config) {
Widget _baudRateRow(SerialConfig config, AppLocalizations? l10n) {
final ctrl = TextEditingController(text: config.baudRate.toString());
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
@@ -322,7 +325,7 @@ class _SerialConfigPanelState extends ConsumerState<SerialConfigPanel> {
SizedBox(
width: 80,
child:
Text('波特率', style: TextStyle(color: AppTheme.textPrimary))),
Text(l10n?.baudRate ?? '波特率', style: TextStyle(color: AppTheme.textPrimary))),
const SizedBox(width: 12),
SizedBox(
width: 140,
@@ -425,7 +428,7 @@ class _SerialConfigPanelState extends ConsumerState<SerialConfigPanel> {
// -- 操作按钮 --------------------------------------------------------
Widget _buildActionRow() {
Widget _buildActionRow(AppLocalizations? l10n) {
return Wrap(
spacing: 12,
runSpacing: 12,
@@ -433,19 +436,19 @@ class _SerialConfigPanelState extends ConsumerState<SerialConfigPanel> {
ElevatedButton.icon(
onPressed: _operating ? null : _connect,
icon: const Icon(Icons.link),
label: const Text('连接'),
label: Text(l10n?.connect ?? '连接'),
),
ElevatedButton.icon(
onPressed:
_operating ? null : _disconnect,
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.errorColor),
icon: const Icon(Icons.link_off),
label: const Text('断开'),
label: Text(l10n?.disconnect ?? '断开'),
),
OutlinedButton.icon(
onPressed: _operating ? null : _testConnection,
icon: const Icon(Icons.send),
label: const Text('发送测试帧'),
label: Text(l10n?.sendTestFrame ?? '发送测试帧'),
),
],
);

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/services/toast_service.dart';
import '../services/usb_detection_service.dart';
@@ -21,18 +22,20 @@ class _UsbImportPanelState extends ConsumerState<UsbImportPanel> {
Future<void> _detect() async {
if (_detecting) return;
setState(() => _detecting = true);
final l10n = AppLocalizations.of(context);
final connected = await UsbDetectionService.instance.detectUsb();
if (!mounted) return;
setState(() => _detecting = false);
if (connected) {
ToastService.showInfo(context, '正在检测U盘...');
ToastService.showInfo(context, l10n?.detectingUsb ?? '正在检测U盘...');
} else {
ToastService.showWarning(context, '未检测到U盘');
ToastService.showWarning(context, l10n?.usbNotDetected ?? '未检测到U盘');
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final state = ref.watch(_usbStateProvider);
final connected = state.maybeWhen(
data: (s) => s.isConnected,
@@ -67,7 +70,7 @@ class _UsbImportPanelState extends ConsumerState<UsbImportPanel> {
const SizedBox(width: 12),
Expanded(
child: Text(
connected ? 'U盘已连接' : '未检测到U盘',
connected ? (l10n?.usbConnected ?? 'U盘已连接') : (l10n?.usbNotDetected ?? '未检测到U盘'),
style: TextStyle(
color: AppTheme.textPrimary,
fontSize: 16,
@@ -80,8 +83,8 @@ class _UsbImportPanelState extends ConsumerState<UsbImportPanel> {
const SizedBox(height: 12),
Text(
connected
? '挂载路径: ${path ?? "未知"}'
: '请插入U盘后点击"重新检测"',
? '${l10n?.mountPath ?? '挂载路径'}: ${path ?? "?"}'
: (l10n?.insertUsb ?? '请插入U盘后重试'),
style: TextStyle(
color: AppTheme.textSecondary, fontSize: 13),
),
@@ -98,13 +101,13 @@ class _UsbImportPanelState extends ConsumerState<UsbImportPanel> {
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh, size: 18),
label: const Text('重新检测'),
label: Text(l10n?.reDetect ?? '重新检测'),
),
const SizedBox(width: 12),
ElevatedButton.icon(
onPressed: connected ? () => _import(path) : null,
icon: const Icon(Icons.download, size: 18),
label: const Text('导入程序'),
label: Text(l10n?.importProgram ?? '导入程序'),
),
],
),
@@ -123,7 +126,7 @@ class _UsbImportPanelState extends ConsumerState<UsbImportPanel> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'使用说明',
l10n?.usageInstructions ?? '使用说明',
style: TextStyle(
color: AppTheme.textPrimary,
fontSize: 14,
@@ -131,9 +134,9 @@ class _UsbImportPanelState extends ConsumerState<UsbImportPanel> {
),
),
const SizedBox(height: 8),
_bullet('将程序文件 (.json) 放入 U盘根目录的 programs 文件夹'),
_bullet('插入 U盘后点击"重新检测"'),
_bullet('检测成功后点击"导入程序"加载程序列表'),
_bullet(l10n?.usbUsageStep1 ?? '将程序文件 (.json) 放入 U盘根目录的 programs 文件夹'),
_bullet(l10n?.usbUsageStep2 ?? '插入 U盘后点击"重新检测"'),
_bullet(l10n?.usbUsageStep3 ?? '检测成功后点击"导入程序"加载程序列表'),
],
),
),
@@ -142,11 +145,12 @@ class _UsbImportPanelState extends ConsumerState<UsbImportPanel> {
}
void _import(String? path) {
final l10n = AppLocalizations.of(context);
if (path == null) {
ToastService.showWarning(context, 'U盘路径无效');
ToastService.showWarning(context, l10n?.usbPathInvalid ?? 'U盘路径无效');
return;
}
ToastService.showInfo(context, '正在导入程序...');
ToastService.showInfo(context, l10n?.importingPrograms ?? '正在导入程序...');
// TODO: 接入 program_import_service 实现真正的导入流程
}