feat(home): 更新完成页面UI并优化串口连接状态管理
- 更新设备屏幕尺寸配置从1920*1080调整为1024x600 - 添加完成页面的AppBar导航和返回功能 - 重构CompletePage布局,使用SafeArea和ConstrainedBox适配不同屏幕 - 添加国际化支持的完成按钮文本 - 优化完成页面视觉元素,包括图标大小和间距调整 - 实现串口连接状态的响应式管理,解决UI状态同步问题 - 优化串口运行器的状态更新逻辑,实现乐观更新机制 - 调整完成页面按钮布局,提供完成和重新运行选项
This commit is contained in:
@@ -163,4 +163,4 @@ AppLocalizations 支持中文(zh)和英文(en),使用 `AppLocalizations.of(con
|
||||
## 其他说明
|
||||
|
||||
1. 需求文档:[已确认-污水毒品快检一体机_功能需求文档.md](docs/%E5%B7%B2%E7%A1%AE%E8%AE%A4-%E6%B1%A1%E6%B0%B4%E6%AF%92%E5%93%81%E5%BF%AB%E6%A3%80%E4%B8%80%E4%BD%93%E6%9C%BA_%E5%8A%9F%E8%83%BD%E9%9C%80%E6%B1%82%E6%96%87%E6%A1%A3.md)
|
||||
2. 运行设备屏幕尺寸为1920*1080(横屏),UI设计必须支持此尺寸
|
||||
2. 运行设备屏幕尺寸为1024x600(横屏),屏幕密度为200,UI设计必须支持此尺寸
|
||||
@@ -93,6 +93,7 @@ class AppLocalizations {
|
||||
// 完成提示
|
||||
String get runComplete => _localizedValues[locale.languageCode]?['runComplete'] ?? '运行完成';
|
||||
String get sampleDropGuide => _localizedValues[locale.languageCode]?['sampleDropGuide'] ?? '请将样本滴入检测卡';
|
||||
String get complete => _localizedValues[locale.languageCode]?['complete'] ?? '完成';
|
||||
|
||||
// 补充缺失的翻译
|
||||
String get lightOn => _localizedValues[locale.languageCode]?['lightOn'] ?? '亮';
|
||||
@@ -198,6 +199,7 @@ class AppLocalizations {
|
||||
'noData': '暂无数据',
|
||||
'runComplete': '运行完成',
|
||||
'sampleDropGuide': '请将样本滴入检测卡',
|
||||
'complete': '完成',
|
||||
'lightOn': '亮',
|
||||
'lightOff': '暗',
|
||||
'enabled': '启用',
|
||||
@@ -300,6 +302,7 @@ class AppLocalizations {
|
||||
'noData': 'No Data',
|
||||
'runComplete': 'Complete',
|
||||
'sampleDropGuide': 'Drop sample to test card',
|
||||
'complete': 'Done',
|
||||
'lightOn': 'On',
|
||||
'lightOff': 'Off',
|
||||
'enabled': 'Enabled',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/database/database_service.dart';
|
||||
@@ -16,6 +18,34 @@ final serialPortServiceProvider = Provider<SerialPortService>((ref) {
|
||||
return service;
|
||||
});
|
||||
|
||||
/// 串口连接状态(响应式)
|
||||
///
|
||||
/// 直接 `ref.watch(serialPortServiceProvider).state` 不会触发 UI 重建,
|
||||
/// 因为 [SerialPortService] 内部 `_state` 的变化不会冒泡到 Provider 层。
|
||||
/// 这里把状态抽出为独立的 StateNotifierProvider,让状态栏等 UI 能即时
|
||||
/// 反映连接/断开事件,避免出现"标题栏显示已连接、实际下发失败"的错觉。
|
||||
class SerialConnectionStateNotifier
|
||||
extends StateNotifier<SerialConnectionState> {
|
||||
SerialConnectionStateNotifier(this._service) : super(_service.state) {
|
||||
_sub = _service.connectionStateChanges.listen((s) => state = s);
|
||||
}
|
||||
|
||||
final SerialPortService _service;
|
||||
late final StreamSubscription<SerialConnectionState> _sub;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
final serialConnectionStateProvider = StateNotifierProvider<
|
||||
SerialConnectionStateNotifier, SerialConnectionState>((ref) {
|
||||
final service = ref.watch(serialPortServiceProvider);
|
||||
return SerialConnectionStateNotifier(service);
|
||||
});
|
||||
|
||||
/// 启动自动连接服务
|
||||
///
|
||||
/// 通过 [main] 中的 ProviderContainer 在 runApp 之前触发一次,
|
||||
|
||||
@@ -66,6 +66,9 @@ class JsonSerialRunner implements Runner {
|
||||
final messageId = _msg.nextId();
|
||||
_pendingCreateTaskId = messageId;
|
||||
final msg = payload.toMessage(messageId, needAck: true);
|
||||
// 乐观更新:与 RunStateNotifier 的运行态保持一致,
|
||||
// 避免在 create_task 应答到达前的窗口里,pause/stop 被状态守卫静默丢弃。
|
||||
status = RunnerStatus.running;
|
||||
DeviceLog.info('Runner.start: program="${program.name}" steps=${steps.length} '
|
||||
'temperature=${program.temperature} airflow=${program.airflowTime}');
|
||||
_msg.send(msg).then((ok) {
|
||||
@@ -132,9 +135,9 @@ class JsonSerialRunner implements Runner {
|
||||
if (ack.ack != _pendingCreateTaskId) return;
|
||||
_pendingCreateTaskId = null;
|
||||
DeviceLog.info('Runner received create_task ack: id=${ack.messageId}');
|
||||
// ack 即视为下位机已接受任务,进入 running 状态
|
||||
if (status == RunnerStatus.idle || status == RunnerStatus.error) {
|
||||
status = RunnerStatus.running;
|
||||
// 状态已由 start() 乐观置为 running,此处仅启动本地兜底倒计时。
|
||||
// 若发送失败,.then() 已将 status 置为 error,不应再启动倒计时。
|
||||
if (status == RunnerStatus.running) {
|
||||
_startLocalTicker();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,12 @@ import '../../../shared/widgets/common_button.dart';
|
||||
import '../../device/providers/run_state_provider.dart';
|
||||
|
||||
/// 运行完成提示页面
|
||||
///
|
||||
/// 页面提供两种返回设备控制页的方式:
|
||||
/// 1. 顶部 AppBar 的返回箭头
|
||||
/// 2. 卡片底部的「完成」主按钮
|
||||
///
|
||||
/// 内部使用 [SingleChildScrollView] 包裹,避免在父级高度受限时按钮被裁切。
|
||||
class CompletePage extends ConsumerWidget {
|
||||
const CompletePage({super.key});
|
||||
|
||||
@@ -16,115 +22,143 @@ class CompletePage extends ConsumerWidget {
|
||||
final runState = ref.watch(runStateProvider);
|
||||
final runNotifier = ref.read(runStateProvider.notifier);
|
||||
|
||||
/// 完成并返回设备控制页
|
||||
void finishAndGoHome() {
|
||||
runNotifier.reset();
|
||||
context.go('/');
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
color: AppTheme.backgroundColor,
|
||||
backgroundColor: AppTheme.backgroundColor,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppTheme.backgroundColor,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
color: AppTheme.textPrimary,
|
||||
tooltip: l10n?.complete ?? '完成',
|
||||
onPressed: finishAndGoHome,
|
||||
),
|
||||
title: Text(
|
||||
l10n?.runComplete ?? '运行完成',
|
||||
style: TextStyle(
|
||||
color: AppTheme.textPrimary,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 600,
|
||||
padding: const EdgeInsets.all(40),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.15),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 成功图标
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.successColor.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check_circle,
|
||||
size: 60,
|
||||
color: AppTheme.successColor,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 标题
|
||||
Text(
|
||||
l10n?.runComplete ?? '程序运行完成',
|
||||
style: TextStyle(
|
||||
color: AppTheme.textPrimary,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 提示信息
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.warningColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
l10n?.sampleDropGuide ?? '请将样本滴入检测卡',
|
||||
style: TextStyle(
|
||||
color: AppTheme.warningColor,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 操作示意图
|
||||
_buildOperationGuide(),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// 按钮区域
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 返回首页按钮
|
||||
CommonButton(
|
||||
text: l10n?.backToHome ?? '返回首页',
|
||||
icon: Icons.home,
|
||||
type: ButtonType.primary,
|
||||
onPressed: () {
|
||||
runNotifier.reset();
|
||||
context.go('/');
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
|
||||
// 重新运行按钮
|
||||
CommonButton(
|
||||
text: l10n?.runAgain ?? '重新运行',
|
||||
icon: Icons.refresh,
|
||||
type: ButtonType.secondary,
|
||||
onPressed: () {
|
||||
final program = runState.currentProgram;
|
||||
if (program != null) {
|
||||
runNotifier.reset();
|
||||
runNotifier.start(program);
|
||||
context.go('/');
|
||||
}
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 600),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(32),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.15),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 成功图标
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.successColor.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.check_circle,
|
||||
size: 48,
|
||||
color: AppTheme.successColor,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 标题
|
||||
Text(
|
||||
l10n?.runComplete ?? '程序运行完成',
|
||||
style: TextStyle(
|
||||
color: AppTheme.textPrimary,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 提示信息
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.warningColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
l10n?.sampleDropGuide ?? '请将样本滴入检测卡',
|
||||
style: TextStyle(
|
||||
color: AppTheme.warningColor,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 操作示意图
|
||||
_buildOperationGuide(),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 按钮区域:完成(主按钮)+ 重新运行(次按钮)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 完成按钮 - 主操作,返回设备控制页
|
||||
CommonButton(
|
||||
text: l10n?.complete ?? '完成',
|
||||
icon: Icons.check_circle,
|
||||
type: ButtonType.primary,
|
||||
onPressed: finishAndGoHome,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// 重新运行按钮
|
||||
CommonButton(
|
||||
text: l10n?.runAgain ?? '重新运行',
|
||||
icon: Icons.refresh,
|
||||
type: ButtonType.secondary,
|
||||
onPressed: () {
|
||||
final program = runState.currentProgram;
|
||||
if (program != null) {
|
||||
runNotifier.reset();
|
||||
runNotifier.start(program);
|
||||
context.go('/');
|
||||
} else {
|
||||
finishAndGoHome();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -135,7 +169,7 @@ class CompletePage extends ConsumerWidget {
|
||||
/// 操作指引示意图
|
||||
Widget _buildOperationGuide() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -151,7 +185,7 @@ class CompletePage extends ConsumerWidget {
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
@@ -171,15 +205,15 @@ class CompletePage extends ConsumerWidget {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: AppTheme.primaryColor, size: 24),
|
||||
child: Icon(icon, color: AppTheme.primaryColor, size: 22),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'$number',
|
||||
style: TextStyle(
|
||||
@@ -188,7 +222,7 @@ class CompletePage extends ConsumerWidget {
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
|
||||
@@ -78,7 +78,7 @@ class _StatusBarState extends ConsumerState<StatusBar> {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final deviceInfo = ref.watch(deviceInfoProvider);
|
||||
final serialState = ref.watch(serialPortServiceProvider).state;
|
||||
final serialState = ref.watch(serialConnectionStateProvider);
|
||||
|
||||
return Container(
|
||||
height: 56,
|
||||
|
||||
Reference in New Issue
Block a user