feat(device): 添加USB设备通信支持和程序参数优化
- 在AndroidManifest.xml中添加USB Host权限和设备过滤器配置 - 新增设备控制国际化词条包括速度档位、吹气时间等 - 重构数据库结构将速度相关字段统一为档位数值存储 - 添加通用KV存储方法用于settings表数据读写 - 优化首页导航实现tab间跳转和状态保持功能 - 更新程序详情页面布局和参数表单界面 - 移除模拟运行器相关测试代码 - 添加USB串口通信依赖包usb_serial
This commit is contained in:
126
lib/features/device/models/serial_config.dart
Normal file
126
lib/features/device/models/serial_config.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'dart:convert';
|
||||
|
||||
/// 串口奇偶校验
|
||||
enum SerialParity { none, odd, even, mark, space }
|
||||
|
||||
/// 串口流控
|
||||
enum SerialFlowControl { none, rtsCts, xonXoff, dtrDsr }
|
||||
|
||||
/// 串口配置
|
||||
///
|
||||
/// 持久化到 settings 表的 `serial_config` key 中;
|
||||
/// 打开串口时根据此配置构造底层 `UsbConfig`。
|
||||
class SerialConfig {
|
||||
/// 设备 Vendor ID(十六进制)
|
||||
final int vendorId;
|
||||
|
||||
/// 设备 Product ID(十六进制),0 表示不指定
|
||||
final int productId;
|
||||
|
||||
/// 波特率
|
||||
final int baudRate;
|
||||
|
||||
/// 数据位 (5/6/7/8)
|
||||
final int dataBits;
|
||||
|
||||
/// 停止位 (1/2)
|
||||
final int stopBits;
|
||||
|
||||
/// 校验位
|
||||
final SerialParity parity;
|
||||
|
||||
/// 流控
|
||||
final SerialFlowControl flowControl;
|
||||
|
||||
/// 读超时(毫秒)
|
||||
final int readTimeoutMs;
|
||||
|
||||
/// 写超时(毫秒)
|
||||
final int writeTimeoutMs;
|
||||
|
||||
const SerialConfig({
|
||||
this.vendorId = 0x1A86,
|
||||
this.productId = 0x7523,
|
||||
this.baudRate = 9600,
|
||||
this.dataBits = 8,
|
||||
this.stopBits = 1,
|
||||
this.parity = SerialParity.none,
|
||||
this.flowControl = SerialFlowControl.none,
|
||||
this.readTimeoutMs = 2000,
|
||||
this.writeTimeoutMs = 2000,
|
||||
});
|
||||
|
||||
/// 默认配置
|
||||
static const SerialConfig defaults = SerialConfig();
|
||||
|
||||
/// 常用波特率
|
||||
static const List<int> commonBaudRates = [
|
||||
1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800,
|
||||
];
|
||||
|
||||
SerialConfig copyWith({
|
||||
int? vendorId,
|
||||
int? productId,
|
||||
int? baudRate,
|
||||
int? dataBits,
|
||||
int? stopBits,
|
||||
SerialParity? parity,
|
||||
SerialFlowControl? flowControl,
|
||||
int? readTimeoutMs,
|
||||
int? writeTimeoutMs,
|
||||
}) {
|
||||
return SerialConfig(
|
||||
vendorId: vendorId ?? this.vendorId,
|
||||
productId: productId ?? this.productId,
|
||||
baudRate: baudRate ?? this.baudRate,
|
||||
dataBits: dataBits ?? this.dataBits,
|
||||
stopBits: stopBits ?? this.stopBits,
|
||||
parity: parity ?? this.parity,
|
||||
flowControl: flowControl ?? this.flowControl,
|
||||
readTimeoutMs: readTimeoutMs ?? this.readTimeoutMs,
|
||||
writeTimeoutMs: writeTimeoutMs ?? this.writeTimeoutMs,
|
||||
);
|
||||
}
|
||||
|
||||
/// 编码为 JSON 字符串(用于持久化)
|
||||
String toJsonString() => jsonEncode({
|
||||
'vendorId': vendorId,
|
||||
'productId': productId,
|
||||
'baudRate': baudRate,
|
||||
'dataBits': dataBits,
|
||||
'stopBits': stopBits,
|
||||
'parity': parity.name,
|
||||
'flowControl': flowControl.name,
|
||||
'readTimeoutMs': readTimeoutMs,
|
||||
'writeTimeoutMs': writeTimeoutMs,
|
||||
});
|
||||
|
||||
/// 从 JSON 字符串解码;解析失败时返回默认值
|
||||
factory SerialConfig.fromJsonString(String? raw) {
|
||||
if (raw == null || raw.isEmpty) return defaults;
|
||||
try {
|
||||
final map = jsonDecode(raw) as Map<String, dynamic>;
|
||||
return SerialConfig(
|
||||
vendorId: (map['vendorId'] as num?)?.toInt() ?? defaults.vendorId,
|
||||
productId: (map['productId'] as num?)?.toInt() ?? defaults.productId,
|
||||
baudRate: (map['baudRate'] as num?)?.toInt() ?? defaults.baudRate,
|
||||
dataBits: (map['dataBits'] as num?)?.toInt() ?? defaults.dataBits,
|
||||
stopBits: (map['stopBits'] as num?)?.toInt() ?? defaults.stopBits,
|
||||
parity: SerialParity.values.firstWhere(
|
||||
(e) => e.name == map['parity'],
|
||||
orElse: () => defaults.parity,
|
||||
),
|
||||
flowControl: SerialFlowControl.values.firstWhere(
|
||||
(e) => e.name == map['flowControl'],
|
||||
orElse: () => defaults.flowControl,
|
||||
),
|
||||
readTimeoutMs:
|
||||
(map['readTimeoutMs'] as num?)?.toInt() ?? defaults.readTimeoutMs,
|
||||
writeTimeoutMs:
|
||||
(map['writeTimeoutMs'] as num?)?.toInt() ?? defaults.writeTimeoutMs,
|
||||
);
|
||||
} catch (_) {
|
||||
return defaults;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../programs/models/program.dart';
|
||||
import '../../programs/models/step.dart';
|
||||
import '../../programs/services/program_service.dart';
|
||||
import '../services/mock_runner.dart';
|
||||
import '../services/runner_interface.dart';
|
||||
import 'serial_provider.dart';
|
||||
|
||||
/// 运行状态枚举
|
||||
enum RunStatus {
|
||||
idle, // 待机
|
||||
running, // 运行中
|
||||
paused, // 已暂停
|
||||
completed,// 已完成
|
||||
error, // 错误
|
||||
idle, // 待机
|
||||
running, // 运行中
|
||||
paused, // 已暂停
|
||||
completed, // 已完成
|
||||
error, // 错误
|
||||
}
|
||||
|
||||
/// 运行状态
|
||||
@@ -23,6 +24,7 @@ class RunState {
|
||||
final int remainingSeconds;
|
||||
final double progress;
|
||||
final String? currentWell;
|
||||
final String? errorMessage;
|
||||
|
||||
const RunState({
|
||||
this.status = RunStatus.idle,
|
||||
@@ -32,6 +34,7 @@ class RunState {
|
||||
this.remainingSeconds = 0,
|
||||
this.progress = 0,
|
||||
this.currentWell,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
RunState copyWith({
|
||||
@@ -42,17 +45,21 @@ class RunState {
|
||||
int? remainingSeconds,
|
||||
double? progress,
|
||||
String? currentWell,
|
||||
String? errorMessage,
|
||||
bool clearProgram = false,
|
||||
bool clearWell = false,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return RunState(
|
||||
status: status ?? this.status,
|
||||
currentProgram: clearProgram ? null : (currentProgram ?? this.currentProgram),
|
||||
currentProgram:
|
||||
clearProgram ? null : (currentProgram ?? this.currentProgram),
|
||||
steps: steps ?? this.steps,
|
||||
currentStepIndex: currentStepIndex ?? this.currentStepIndex,
|
||||
remainingSeconds: remainingSeconds ?? this.remainingSeconds,
|
||||
progress: progress ?? this.progress,
|
||||
currentWell: clearWell ? null : (currentWell ?? this.currentWell),
|
||||
errorMessage: clearError ? null : (errorMessage ?? this.errorMessage),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,8 +75,8 @@ class RunState {
|
||||
final minutes = (remainingSeconds % 3600) ~/ 60;
|
||||
final seconds = remainingSeconds % 60;
|
||||
return '${hours.toString().padLeft(2, '0')}:'
|
||||
'${minutes.toString().padLeft(2, '0')}:'
|
||||
'${seconds.toString().padLeft(2, '0')}';
|
||||
'${minutes.toString().padLeft(2, '0')}:'
|
||||
'${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// 格式化进度百分比
|
||||
@@ -80,18 +87,24 @@ class RunState {
|
||||
|
||||
/// 运行状态 Notifier
|
||||
class RunStateNotifier extends StateNotifier<RunState> {
|
||||
final MockRunner _runner;
|
||||
final Runner _runner;
|
||||
final ProgramService _programService;
|
||||
|
||||
RunStateNotifier(this._runner, this._programService) : super(const RunState());
|
||||
|
||||
/// 开始运行程序
|
||||
Future<void> start(Program program) async {
|
||||
// 获取程序步骤(这里使用模拟数据,实际应从数据库读取)
|
||||
final steps = await _loadSteps(program.id!);
|
||||
if (state.status == RunStatus.running ||
|
||||
state.status == RunStatus.paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
final steps = await _programService.getStepsByProgramId(program.id!);
|
||||
if (steps.isEmpty) {
|
||||
state = state.copyWith(status: RunStatus.error);
|
||||
state = state.copyWith(
|
||||
status: RunStatus.error,
|
||||
errorMessage: '程序步骤为空',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -101,26 +114,36 @@ class RunStateNotifier extends StateNotifier<RunState> {
|
||||
steps: steps,
|
||||
currentStepIndex: 0,
|
||||
progress: 0,
|
||||
currentWell: steps.first.position,
|
||||
clearError: true,
|
||||
);
|
||||
|
||||
_runner.start(
|
||||
program,
|
||||
steps,
|
||||
(stepIndex, remaining, progress, well) {
|
||||
state = state.copyWith(
|
||||
currentStepIndex: stepIndex,
|
||||
remainingSeconds: remaining,
|
||||
progress: progress,
|
||||
currentWell: well,
|
||||
);
|
||||
},
|
||||
() {
|
||||
state = state.copyWith(
|
||||
status: RunStatus.completed,
|
||||
progress: 1,
|
||||
clearWell: true,
|
||||
);
|
||||
},
|
||||
RunnerCallbacks(
|
||||
onProgress: (stepIndex, remaining, progress, well) {
|
||||
state = state.copyWith(
|
||||
currentStepIndex: stepIndex,
|
||||
remainingSeconds: remaining,
|
||||
progress: progress,
|
||||
currentWell: well,
|
||||
);
|
||||
},
|
||||
onComplete: () {
|
||||
state = state.copyWith(
|
||||
status: RunStatus.completed,
|
||||
progress: 1,
|
||||
currentWell: steps.last.position,
|
||||
);
|
||||
},
|
||||
onError: (msg) {
|
||||
state = state.copyWith(
|
||||
status: RunStatus.error,
|
||||
errorMessage: msg,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -142,26 +165,17 @@ class RunStateNotifier extends StateNotifier<RunState> {
|
||||
|
||||
/// 停止运行
|
||||
void stop() {
|
||||
_runner.stop();
|
||||
if (state.status == RunStatus.running ||
|
||||
state.status == RunStatus.paused) {
|
||||
_runner.stop();
|
||||
}
|
||||
state = const RunState(status: RunStatus.idle);
|
||||
}
|
||||
|
||||
/// 重置状态
|
||||
void reset() {
|
||||
stop();
|
||||
}
|
||||
|
||||
/// 加载程序步骤(从数据库读取)
|
||||
Future<List<Step>> _loadSteps(int programId) async {
|
||||
return await _programService.getStepsByProgramId(programId);
|
||||
}
|
||||
void reset() => stop();
|
||||
}
|
||||
|
||||
/// MockRunner Provider
|
||||
final mockRunnerProvider = Provider<MockRunner>((ref) {
|
||||
return MockRunner();
|
||||
});
|
||||
|
||||
/// ProgramService Provider
|
||||
final programServiceProvider = Provider<ProgramService>((ref) {
|
||||
return ProgramService.instance;
|
||||
@@ -170,7 +184,7 @@ final programServiceProvider = Provider<ProgramService>((ref) {
|
||||
/// 运行状态 Provider
|
||||
final runStateProvider =
|
||||
StateNotifierProvider<RunStateNotifier, RunState>((ref) {
|
||||
final runner = ref.watch(mockRunnerProvider);
|
||||
final runner = ref.watch(runnerProvider);
|
||||
final programService = ref.watch(programServiceProvider);
|
||||
return RunStateNotifier(runner, programService);
|
||||
});
|
||||
@@ -185,4 +199,4 @@ final isRunningProvider = Provider<bool>((ref) {
|
||||
final isPausedProvider = Provider<bool>((ref) {
|
||||
final status = ref.watch(runStateProvider).status;
|
||||
return status == RunStatus.paused;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
import 'dart:async';
|
||||
import '../../programs/models/step.dart';
|
||||
import '../../programs/models/program.dart';
|
||||
|
||||
/// 模拟运行器回调
|
||||
typedef RunProgressCallback = void Function(
|
||||
int currentStepIndex,
|
||||
int remainingSeconds,
|
||||
double progress,
|
||||
String currentWell,
|
||||
);
|
||||
|
||||
typedef RunCompleteCallback = void Function();
|
||||
|
||||
/// 模拟运行器
|
||||
/// 用于在没有实际硬件连接时模拟程序执行过程
|
||||
class MockRunner {
|
||||
Timer? _timer;
|
||||
Program? _currentProgram;
|
||||
List<Step> _steps = [];
|
||||
int _currentStepIndex = 0;
|
||||
int _remainingSeconds = 0;
|
||||
bool _isPaused = false;
|
||||
RunProgressCallback? _onProgress;
|
||||
RunCompleteCallback? _onComplete;
|
||||
|
||||
/// 是否正在运行
|
||||
bool get isRunning => _timer != null && !_isPaused;
|
||||
|
||||
/// 是否已暂停
|
||||
bool get isPaused => _isPaused;
|
||||
|
||||
/// 当前程序
|
||||
Program? get currentProgram => _currentProgram;
|
||||
|
||||
/// 开始运行程序
|
||||
void start(
|
||||
Program program,
|
||||
List<Step> steps,
|
||||
RunProgressCallback onProgress,
|
||||
RunCompleteCallback onComplete,
|
||||
) {
|
||||
_currentProgram = program;
|
||||
_steps = steps;
|
||||
_onProgress = onProgress;
|
||||
_onComplete = onComplete;
|
||||
_currentStepIndex = 0;
|
||||
_isPaused = false;
|
||||
|
||||
if (steps.isEmpty) {
|
||||
onComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
// 开始执行第一个步骤
|
||||
_startStep(steps[0]);
|
||||
}
|
||||
|
||||
/// 暂停运行
|
||||
void pause() {
|
||||
if (_timer != null && !_isPaused) {
|
||||
_isPaused = true;
|
||||
_timer!.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 继续运行
|
||||
void resume() {
|
||||
if (_isPaused && _currentProgram != null) {
|
||||
_isPaused = false;
|
||||
_resumeStep();
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止运行
|
||||
void stop() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
_currentProgram = null;
|
||||
_steps = [];
|
||||
_currentStepIndex = 0;
|
||||
_remainingSeconds = 0;
|
||||
_isPaused = false;
|
||||
}
|
||||
|
||||
/// 开始执行步骤
|
||||
void _startStep(Step step) {
|
||||
// 计算步骤总时间(混合时间 + 吸磁时间 + 吹气时间)
|
||||
_remainingSeconds = step.mixTime + step.magnetTime + step.blowTime;
|
||||
|
||||
// 如果步骤时间为0,设置最小演示时间(5秒)
|
||||
if (_remainingSeconds == 0) {
|
||||
_remainingSeconds = 5;
|
||||
}
|
||||
|
||||
// 启动定时器,每秒更新
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
_remainingSeconds--;
|
||||
|
||||
// 计算总进度
|
||||
final totalSeconds = _calculateTotalSeconds();
|
||||
final elapsedSeconds = _calculateElapsedSeconds();
|
||||
final progress = totalSeconds > 0 ? elapsedSeconds / totalSeconds : 0.0;
|
||||
|
||||
// 回调进度更新
|
||||
_onProgress?.call(
|
||||
_currentStepIndex,
|
||||
_remainingSeconds,
|
||||
progress,
|
||||
step.position,
|
||||
);
|
||||
|
||||
// 步骤完成
|
||||
if (_remainingSeconds <= 0) {
|
||||
timer.cancel();
|
||||
_timer = null;
|
||||
_nextStep();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 继续执行步骤(从暂停恢复)
|
||||
void _resumeStep() {
|
||||
if (_currentStepIndex >= _steps.length) return;
|
||||
|
||||
final step = _steps[_currentStepIndex];
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
_remainingSeconds--;
|
||||
|
||||
final totalSeconds = _calculateTotalSeconds();
|
||||
final elapsedSeconds = _calculateElapsedSeconds();
|
||||
final progress = totalSeconds > 0 ? elapsedSeconds / totalSeconds : 0.0;
|
||||
|
||||
_onProgress?.call(
|
||||
_currentStepIndex,
|
||||
_remainingSeconds,
|
||||
progress,
|
||||
step.position,
|
||||
);
|
||||
|
||||
if (_remainingSeconds <= 0) {
|
||||
timer.cancel();
|
||||
_timer = null;
|
||||
_nextStep();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 执行下一个步骤
|
||||
void _nextStep() {
|
||||
_currentStepIndex++;
|
||||
|
||||
if (_currentStepIndex >= _steps.length) {
|
||||
// 所有步骤完成
|
||||
_onComplete?.call();
|
||||
stop();
|
||||
} else {
|
||||
// 执行下一个步骤
|
||||
_startStep(_steps[_currentStepIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算总执行时间
|
||||
int _calculateTotalSeconds() {
|
||||
int total = 0;
|
||||
for (final step in _steps) {
|
||||
int stepTime = step.mixTime + step.magnetTime + step.blowTime;
|
||||
if (stepTime == 0) stepTime = 5;
|
||||
total += stepTime;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/// 计算已执行时间
|
||||
int _calculateElapsedSeconds() {
|
||||
int elapsed = 0;
|
||||
for (int i = 0; i < _currentStepIndex; i++) {
|
||||
int stepTime = _steps[i].mixTime + _steps[i].magnetTime + _steps[i].blowTime;
|
||||
if (stepTime == 0) stepTime = 5;
|
||||
elapsed += stepTime;
|
||||
}
|
||||
// 加上当前步骤已执行的时间
|
||||
final currentStep = _steps[_currentStepIndex];
|
||||
int currentStepTime = currentStep.mixTime + currentStep.magnetTime + currentStep.blowTime;
|
||||
if (currentStepTime == 0) currentStepTime = 5;
|
||||
elapsed += currentStepTime - _remainingSeconds;
|
||||
return elapsed;
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import '../../programs/models/program.dart';
|
||||
import '../../programs/models/step.dart';
|
||||
import 'runner_interface.dart';
|
||||
|
||||
/// 模拟运行器(用于开发测试)
|
||||
/// 模拟硬件运行过程
|
||||
class MockRunner implements Runner {
|
||||
@override
|
||||
RunnerStatus status = RunnerStatus.idle;
|
||||
|
||||
bool _isRunning = false;
|
||||
int _currentStep = 0;
|
||||
int _remainingSeconds = 0;
|
||||
RunnerCallbacks? _callbacks;
|
||||
List<Step> _steps = [];
|
||||
|
||||
@override
|
||||
void start(Program program, List<Step> steps, RunnerCallbacks callbacks) {
|
||||
if (steps.isEmpty) {
|
||||
callbacks.onError?.call('No steps to run');
|
||||
status = RunnerStatus.error;
|
||||
return;
|
||||
}
|
||||
|
||||
_steps = steps;
|
||||
_callbacks = callbacks;
|
||||
_currentStep = 0;
|
||||
_isRunning = true;
|
||||
status = RunnerStatus.running;
|
||||
|
||||
// 开始模拟运行
|
||||
_runSimulation();
|
||||
}
|
||||
|
||||
void _runSimulation() {
|
||||
if (!_isRunning || _currentStep >= _steps.length) {
|
||||
_completeRun();
|
||||
return;
|
||||
}
|
||||
|
||||
final step = _steps[_currentStep];
|
||||
// 计算步骤时间(混合时间 + 吸磁时间 + 吹气时间 + 5秒最小)
|
||||
final stepTime = (step.mixTime ?? 0) + (step.magnetTime ?? 0) + (step.blowTime ?? 0) + 5;
|
||||
_remainingSeconds = stepTime.clamp(5, 300);
|
||||
|
||||
// 模拟倒计时
|
||||
_simulateStepProgress(stepTime);
|
||||
}
|
||||
|
||||
void _simulateStepProgress(int totalSeconds) {
|
||||
// 简化模拟:每秒更新进度
|
||||
int elapsed = 0;
|
||||
while (_isRunning && elapsed < totalSeconds) {
|
||||
elapsed++;
|
||||
final remaining = totalSeconds - elapsed;
|
||||
final progress = elapsed / totalSeconds;
|
||||
|
||||
_callbacks?.onProgress?.call(
|
||||
_currentStep,
|
||||
remaining,
|
||||
(_currentStep + progress) / _steps.length,
|
||||
_steps[_currentStep].position,
|
||||
);
|
||||
|
||||
// 实际实现需要使用 Timer
|
||||
// await Future.delayed(Duration(seconds: 1));
|
||||
}
|
||||
|
||||
if (_isRunning) {
|
||||
_currentStep++;
|
||||
_runSimulation();
|
||||
}
|
||||
}
|
||||
|
||||
void _completeRun() {
|
||||
status = RunnerStatus.completed;
|
||||
_isRunning = false;
|
||||
_callbacks?.onComplete?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void pause() {
|
||||
if (status == RunnerStatus.running) {
|
||||
_isRunning = false;
|
||||
status = RunnerStatus.paused;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void resume() {
|
||||
if (status == RunnerStatus.paused) {
|
||||
_isRunning = true;
|
||||
status = RunnerStatus.running;
|
||||
// 继续运行
|
||||
_runSimulation();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void stop() {
|
||||
_isRunning = false;
|
||||
status = RunnerStatus.idle;
|
||||
_currentStep = 0;
|
||||
_remainingSeconds = 0;
|
||||
}
|
||||
|
||||
@override
|
||||
RunnerStatus getStatus() => status;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../core/database/database_service.dart';
|
||||
import '../../../core/localization/app_localizations.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../device/providers/run_state_provider.dart';
|
||||
import '../../programs/pages/programs_page.dart';
|
||||
@@ -12,9 +13,13 @@ import '../widgets/running_control_panel.dart';
|
||||
import '../widgets/run_status_monitor.dart';
|
||||
|
||||
/// 首页 - 设备控制面板 (暗色工业风格)
|
||||
/// 布局:状态栏 + 导航标签栏 + 内容区(设备控制/程序管理/系统设置)
|
||||
/// 布局:状态栏(含标签导航) + 内容区(设备控制/程序管理/系统设置)
|
||||
///
|
||||
/// [initialTab] 用于从子页面(如程序详情页)跳转回首页时指定要显示的 tab
|
||||
class HomePage extends ConsumerStatefulWidget {
|
||||
const HomePage({super.key});
|
||||
final int initialTab;
|
||||
|
||||
const HomePage({super.key, this.initialTab = 0});
|
||||
|
||||
@override
|
||||
ConsumerState<HomePage> createState() => _HomePageState();
|
||||
@@ -22,17 +27,19 @@ class HomePage extends ConsumerStatefulWidget {
|
||||
|
||||
class _HomePageState extends ConsumerState<HomePage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
int _currentIndex = 0;
|
||||
late int _currentIndex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentIndex = widget.initialTab.clamp(0, 2);
|
||||
DatabaseService.instance.initTestData();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final runState = ref.watch(runStateProvider);
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
// 监听运行完成状态,自动跳转
|
||||
ref.listen<RunState>(runStateProvider, (prev, next) {
|
||||
@@ -44,19 +51,25 @@ class _HomePageState extends ConsumerState<HomePage>
|
||||
}
|
||||
});
|
||||
|
||||
final tabs = <StatusBarTab>[
|
||||
StatusBarTab(icon: Icons.dashboard, label: l10n?.deviceControl ?? '设备控制'),
|
||||
StatusBarTab(icon: Icons.list_alt, label: l10n?.programs ?? '程序管理'),
|
||||
StatusBarTab(icon: Icons.settings, label: l10n?.settings ?? '系统设置'),
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
color: AppTheme.bgDeep,
|
||||
child: Column(
|
||||
children: [
|
||||
// 状态栏
|
||||
// 状态栏(内嵌标签导航)
|
||||
StatusBar(
|
||||
isRunning: runState.status == RunStatus.running,
|
||||
tabs: tabs,
|
||||
currentTabIndex: _currentIndex,
|
||||
onTabChanged: (index) => setState(() => _currentIndex = index),
|
||||
),
|
||||
|
||||
// 导航标签栏
|
||||
_buildTabBar(),
|
||||
|
||||
// 内容区
|
||||
Expanded(
|
||||
child: IndexedStack(
|
||||
@@ -74,69 +87,10 @@ class _HomePageState extends ConsumerState<HomePage>
|
||||
);
|
||||
}
|
||||
|
||||
/// 导航标签栏
|
||||
Widget _buildTabBar() {
|
||||
const tabs = [
|
||||
(icon: Icons.dashboard, label: '设备控制'),
|
||||
(icon: Icons.list_alt, label: '程序管理'),
|
||||
(icon: Icons.settings, label: '系统设置'),
|
||||
];
|
||||
|
||||
return Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: List.generate(tabs.length, (index) {
|
||||
final tab = tabs[index];
|
||||
final isSelected = _currentIndex == index;
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _currentIndex = index),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.only(right: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppTheme.accentPrimary : AppTheme.cardBg,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? AppTheme.accentPrimary
|
||||
: AppTheme.borderSubtle,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
tab.icon,
|
||||
size: 18,
|
||||
color: isSelected ? Colors.white : AppTheme.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
tab.label,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.white : AppTheme.textSecondary,
|
||||
fontSize: 14,
|
||||
fontWeight: isSelected
|
||||
? FontWeight.w600
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 设备控制页面内容
|
||||
Widget _buildDeviceControlPage(RunState runState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
children: [
|
||||
// 左侧:程序列表(运行时锁定)
|
||||
|
||||
@@ -194,7 +194,7 @@ class RunStatusMonitor extends ConsumerWidget {
|
||||
if (step.mixTime > 0)
|
||||
_buildParamRow(
|
||||
l10n?.speed ?? '转速',
|
||||
'${step.mixSpeed}',
|
||||
'${step.speed} 档',
|
||||
),
|
||||
if (step.magnetTime > 0)
|
||||
_buildParamRow(
|
||||
|
||||
@@ -4,10 +4,11 @@ import '../../../core/localization/app_localizations.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../shared/widgets/common_button.dart';
|
||||
import '../../device/providers/run_state_provider.dart';
|
||||
import '../../programs/models/program.dart';
|
||||
import '../../programs/providers/programs_provider.dart';
|
||||
|
||||
/// 运行控制面板 - 暗色工业风格
|
||||
/// 显示当前程序信息、瓷套棒状态和运行控制按钮
|
||||
/// 显示当前程序信息和运行控制按钮
|
||||
class RunningControlPanel extends ConsumerWidget {
|
||||
const RunningControlPanel({super.key});
|
||||
|
||||
@@ -96,40 +97,6 @@ class RunningControlPanel extends ConsumerWidget {
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 瓷套棒确认提示
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.cardBg,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: AppTheme.borderSubtle, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppTheme.statusStopped,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n?.ceramicNotInstalled ?? '瓷套棒: 未安装 — 禁止启动',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 控制按钮
|
||||
Row(
|
||||
children: [
|
||||
@@ -144,7 +111,7 @@ class RunningControlPanel extends ConsumerWidget {
|
||||
type: ButtonType.primary,
|
||||
enabled: selectedProgram != null,
|
||||
onPressed: selectedProgram != null
|
||||
? () => runNotifier.start(selectedProgram)
|
||||
? () => _confirmAndStart(context, runNotifier, selectedProgram, l10n)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
@@ -321,6 +288,49 @@ class RunningControlPanel extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示瓷套棒放置确认对话框,确认后启动程序
|
||||
void _confirmAndStart(
|
||||
BuildContext context,
|
||||
RunStateNotifier runNotifier,
|
||||
Program program,
|
||||
AppLocalizations? l10n,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: AppTheme.cardBg,
|
||||
title: Text(
|
||||
l10n?.ceramicSleeveConfirm ?? '运行前请确认已安装瓷套棒',
|
||||
style: const TextStyle(color: AppTheme.textHeading),
|
||||
),
|
||||
content: Text(
|
||||
l10n?.ceramicSleeveConfirmMessage ?? '请确认已放置瓷套棒后再启动程序。',
|
||||
style: const TextStyle(color: AppTheme.textPrimary),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(
|
||||
l10n?.cancel ?? '取消',
|
||||
style: const TextStyle(color: AppTheme.textSecondary),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.accentPrimary,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
runNotifier.start(program);
|
||||
},
|
||||
child: Text(l10n?.confirm ?? '确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示停止确认对话框
|
||||
void _showStopConfirm(
|
||||
BuildContext context,
|
||||
|
||||
@@ -6,16 +6,32 @@ import '../../../core/theme/app_theme.dart';
|
||||
import '../../../shared/widgets/status_indicator.dart';
|
||||
import '../../device/providers/device_info_provider.dart';
|
||||
|
||||
/// 状态栏标签项数据
|
||||
class StatusBarTab {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
const StatusBarTab({required this.icon, required this.label});
|
||||
}
|
||||
|
||||
/// 状态栏组件 - 明亮工业风格
|
||||
/// 显示设备名称、实时时钟、系统状态、照明控制
|
||||
/// 显示: 设备名称 | 导航标签 | 灯按钮 | 状态指示器 | 实时时钟
|
||||
///
|
||||
/// [tabs] 不为空时,会在设备名右侧渲染胶囊样式的导航标签按钮组,
|
||||
/// 由父组件通过 [currentTabIndex] 和 [onTabChanged] 控制切换。
|
||||
class StatusBar extends ConsumerStatefulWidget {
|
||||
final bool isRunning;
|
||||
final VoidCallback? onLightToggle;
|
||||
final List<StatusBarTab> tabs;
|
||||
final int currentTabIndex;
|
||||
final ValueChanged<int>? onTabChanged;
|
||||
|
||||
const StatusBar({
|
||||
super.key,
|
||||
this.isRunning = false,
|
||||
this.onLightToggle,
|
||||
this.tabs = const [],
|
||||
this.currentTabIndex = 0,
|
||||
this.onTabChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -39,6 +55,7 @@ class _StatusBarState extends ConsumerState<StatusBar> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 更新当前时间显示
|
||||
void _updateTime() {
|
||||
final now = DateTime.now();
|
||||
_currentTime =
|
||||
@@ -49,6 +66,7 @@ class _StatusBarState extends ConsumerState<StatusBar> {
|
||||
|
||||
String _twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||
|
||||
/// 切换照明开关
|
||||
Future<void> _onLightTap() async {
|
||||
widget.onLightToggle?.call();
|
||||
await ref.read(deviceInfoProvider.notifier).toggleLight();
|
||||
@@ -77,7 +95,7 @@ class _StatusBarState extends ConsumerState<StatusBar> {
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.precision_manufacturing, color: Colors.white, size: 22),
|
||||
const Icon(Icons.precision_manufacturing, color: Colors.white, size: 22),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
l10n?.deviceName ?? '污水毒品前处理一体机',
|
||||
@@ -89,6 +107,10 @@ class _StatusBarState extends ConsumerState<StatusBar> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.tabs.isNotEmpty) ...[
|
||||
const SizedBox(width: 32),
|
||||
_buildNavTabs(),
|
||||
],
|
||||
const Spacer(),
|
||||
_LightToggleButton(
|
||||
isOn: deviceInfo.lightingOn,
|
||||
@@ -117,6 +139,92 @@ class _StatusBarState extends ConsumerState<StatusBar> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建标题栏内嵌的导航标签按钮组(胶囊样式)
|
||||
Widget _buildNavTabs() {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(widget.tabs.length, (index) {
|
||||
final tab = widget.tabs[index];
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: index == widget.tabs.length - 1 ? 0 : 6),
|
||||
child: _NavTabButton(
|
||||
icon: tab.icon,
|
||||
label: tab.label,
|
||||
selected: index == widget.currentTabIndex,
|
||||
onTap: () => widget.onTabChanged?.call(index),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 标题栏内嵌的导航标签按钮(胶囊样式)
|
||||
/// 选中时使用半透明白色背景突出,未选中时仅显示文字
|
||||
class _NavTabButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final bool selected;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const _NavTabButton({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.selected,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
height: 36,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? Colors.white.withValues(alpha: 0.22)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(
|
||||
color: selected
|
||||
? Colors.white.withValues(alpha: 0.35)
|
||||
: Colors.transparent,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: selected
|
||||
? Colors.white
|
||||
: Colors.white.withValues(alpha: 0.78),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: selected
|
||||
? Colors.white
|
||||
: Colors.white.withValues(alpha: 0.85),
|
||||
fontSize: 14,
|
||||
fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LightToggleButton extends StatelessWidget {
|
||||
|
||||
@@ -7,10 +7,12 @@ import '../../../shared/widgets/common_button.dart';
|
||||
import '../providers/steps_provider.dart';
|
||||
import '../widgets/step_list.dart';
|
||||
import '../widgets/step_form.dart';
|
||||
import '../../home/widgets/status_bar.dart';
|
||||
import '../../device/providers/run_state_provider.dart';
|
||||
import '../../programs/providers/programs_provider.dart';
|
||||
|
||||
/// 程序详情页面
|
||||
/// 左侧步骤列表 + 右侧参数表单
|
||||
/// 布局:顶部状态栏(含导航tab) + 子工具栏(返回/程序名/保存) + 左侧步骤列表 + 右侧参数表单
|
||||
class ProgramDetailPage extends ConsumerStatefulWidget {
|
||||
final String programId;
|
||||
|
||||
@@ -35,61 +37,31 @@ class _ProgramDetailPageState extends ConsumerState<ProgramDetailPage> {
|
||||
final programsState = ref.watch(programsProvider);
|
||||
final program = programsState.programs.where((p) => p.id == _programIdInt).firstOrNull;
|
||||
final stepsState = ref.watch(stepsProvider(_programIdInt));
|
||||
final runState = ref.watch(runStateProvider);
|
||||
|
||||
// 详情页从程序管理进入,高亮"程序管理"tab(index=1)
|
||||
final tabs = <StatusBarTab>[
|
||||
StatusBarTab(icon: Icons.dashboard, label: l10n?.deviceControl ?? '设备控制'),
|
||||
StatusBarTab(icon: Icons.list_alt, label: l10n?.programs ?? '程序管理'),
|
||||
StatusBarTab(icon: Icons.settings, label: l10n?.settings ?? '系统设置'),
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
color: AppTheme.backgroundColor,
|
||||
child: Column(
|
||||
children: [
|
||||
// 顶部导航栏
|
||||
Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 返回按钮
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.go('/programs'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// 程序名称
|
||||
Text(
|
||||
program?.name ?? (l10n?.detail ?? '程序详情'),
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// 保存按钮
|
||||
CommonButton(
|
||||
text: l10n?.save ?? '保存',
|
||||
icon: Icons.save,
|
||||
type: ButtonType.primary,
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('已保存'),
|
||||
backgroundColor: AppTheme.successColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
// 顶部状态栏(含导航tab),tab点击跳回首页对应 tab
|
||||
StatusBar(
|
||||
isRunning: runState.status == RunStatus.running,
|
||||
tabs: tabs,
|
||||
currentTabIndex: 1,
|
||||
onTabChanged: (index) => context.go('/?tab=$index'),
|
||||
),
|
||||
|
||||
// 子工具栏:返回按钮 + 程序名 + 保存按钮
|
||||
_buildSubToolbar(context, l10n, program?.name),
|
||||
|
||||
// 主内容区域
|
||||
Expanded(
|
||||
child: stepsState.isLoading
|
||||
@@ -172,6 +144,57 @@ class _ProgramDetailPageState extends ConsumerState<ProgramDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 子工具栏:返回按钮 + 程序名 + 保存按钮
|
||||
/// 位于状态栏下方,提供详情页特有的操作入口
|
||||
Widget _buildSubToolbar(BuildContext context, AppLocalizations? l10n, String? programName) {
|
||||
return Container(
|
||||
height: 56,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.06),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
tooltip: l10n?.backToHome ?? '返回',
|
||||
onPressed: () => context.go('/?tab=1'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
programName ?? (l10n?.detail ?? '程序详情'),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textHeading,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
CommonButton(
|
||||
text: l10n?.save ?? '保存',
|
||||
icon: Icons.save,
|
||||
type: ButtonType.primary,
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('已保存'),
|
||||
backgroundColor: AppTheme.successColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示添加步骤对话框
|
||||
void _showAddStepDialog(BuildContext context, WidgetRef ref) {
|
||||
showDialog(
|
||||
|
||||
@@ -34,9 +34,7 @@ class _StepFormState extends State<StepForm> {
|
||||
late TextEditingController _blowTimeController;
|
||||
|
||||
String _position = 'A1';
|
||||
String _mixSpeed = '中速';
|
||||
String _blowSpeed = '中速';
|
||||
int _needleSpeed = 5;
|
||||
int _speed = 5;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -48,9 +46,7 @@ class _StepFormState extends State<StepForm> {
|
||||
_blowTimeController = TextEditingController(text: '${widget.step?.blowTime ?? 0}');
|
||||
|
||||
_position = widget.step?.position ?? 'A1';
|
||||
_mixSpeed = widget.step?.mixSpeed ?? '中速';
|
||||
_blowSpeed = widget.step?.blowSpeed ?? '中速';
|
||||
_needleSpeed = widget.step?.needleSpeed ?? 5;
|
||||
_speed = widget.step?.speed ?? 5;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -179,53 +175,22 @@ class _StepFormState extends State<StepForm> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 速度选择
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _mixSpeed,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n?.mixSpeed ?? '混合速度',
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
items: Constants.speedOptions.map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) setState(() => _mixSpeed = value);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _blowSpeed,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n?.blowSpeed ?? '吹气速度',
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
items: Constants.speedOptions.map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) setState(() => _blowSpeed = value);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 下针速度滑块
|
||||
// 速度滑块
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${l10n?.needleSpeed ?? '下针速度'}: $_needleSpeed 档', style: TextStyle(color: AppTheme.textPrimary)),
|
||||
Text(
|
||||
'${l10n?.speed ?? '速度'}: $_speed 档',
|
||||
style: TextStyle(color: AppTheme.textPrimary),
|
||||
),
|
||||
Slider(
|
||||
value: _needleSpeed.toDouble(),
|
||||
value: _speed.toDouble(),
|
||||
min: 1,
|
||||
max: 10,
|
||||
divisions: 9,
|
||||
activeColor: AppTheme.primaryColor,
|
||||
onChanged: (value) {
|
||||
setState(() => _needleSpeed = value.round());
|
||||
setState(() => _speed = value.round());
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -259,10 +224,8 @@ class _StepFormState extends State<StepForm> {
|
||||
mixTime: int.tryParse(_mixTimeController.text) ?? 0,
|
||||
magnetTime: int.tryParse(_magnetTimeController.text) ?? 0,
|
||||
volume: int.tryParse(_volumeController.text) ?? 0,
|
||||
mixSpeed: _mixSpeed,
|
||||
blowSpeed: _blowSpeed,
|
||||
blowTime: int.tryParse(_blowTimeController.text) ?? 0,
|
||||
needleSpeed: _needleSpeed,
|
||||
speed: _speed,
|
||||
);
|
||||
|
||||
widget.onSave(step);
|
||||
|
||||
@@ -5,6 +5,8 @@ class Program {
|
||||
final String name;
|
||||
final String createdAt;
|
||||
final int status; // 1: 启用, 0: 停用
|
||||
final int temperature;
|
||||
final int airflowTime;
|
||||
|
||||
Program({
|
||||
this.id,
|
||||
@@ -12,6 +14,8 @@ class Program {
|
||||
required this.name,
|
||||
required this.createdAt,
|
||||
this.status = 1,
|
||||
this.temperature = 50,
|
||||
this.airflowTime = 60,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
@@ -21,6 +25,8 @@ class Program {
|
||||
'name': name,
|
||||
'created_at': createdAt,
|
||||
'status': status,
|
||||
'temperature': temperature,
|
||||
'airflow_time': airflowTime,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,6 +37,8 @@ class Program {
|
||||
name: map['name'] as String,
|
||||
createdAt: map['created_at'] as String,
|
||||
status: map['status'] as int? ?? 1,
|
||||
temperature: map['temperature'] as int? ?? 50,
|
||||
airflowTime: map['airflow_time'] as int? ?? 60,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,6 +48,8 @@ class Program {
|
||||
String? name,
|
||||
String? createdAt,
|
||||
int? status,
|
||||
int? temperature,
|
||||
int? airflowTime,
|
||||
}) {
|
||||
return Program(
|
||||
id: id ?? this.id,
|
||||
@@ -47,6 +57,8 @@ class Program {
|
||||
name: name ?? this.name,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
status: status ?? this.status,
|
||||
temperature: temperature ?? this.temperature,
|
||||
airflowTime: airflowTime ?? this.airflowTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,8 @@ class Step {
|
||||
final int mixTime;
|
||||
final int magnetTime;
|
||||
final int volume;
|
||||
final String mixSpeed;
|
||||
final String blowSpeed;
|
||||
final int blowTime;
|
||||
final int needleSpeed;
|
||||
final int speed;
|
||||
|
||||
Step({
|
||||
this.id,
|
||||
@@ -22,10 +20,8 @@ class Step {
|
||||
this.mixTime = 0,
|
||||
this.magnetTime = 0,
|
||||
this.volume = 0,
|
||||
this.mixSpeed = '中速',
|
||||
this.blowSpeed = '中速',
|
||||
this.blowTime = 0,
|
||||
this.needleSpeed = 5,
|
||||
this.speed = 5,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
@@ -38,10 +34,8 @@ class Step {
|
||||
'mix_time': mixTime,
|
||||
'magnet_time': magnetTime,
|
||||
'volume': volume,
|
||||
'mix_speed': mixSpeed,
|
||||
'blow_speed': blowSpeed,
|
||||
'blow_time': blowTime,
|
||||
'needle_speed': needleSpeed,
|
||||
'speed': speed,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,10 +49,8 @@ class Step {
|
||||
mixTime: map['mix_time'] as int? ?? 0,
|
||||
magnetTime: map['magnet_time'] as int? ?? 0,
|
||||
volume: map['volume'] as int? ?? 0,
|
||||
mixSpeed: map['mix_speed'] as String? ?? '中速',
|
||||
blowSpeed: map['blow_speed'] as String? ?? '中速',
|
||||
blowTime: map['blow_time'] as int? ?? 0,
|
||||
needleSpeed: map['needle_speed'] as int? ?? 5,
|
||||
speed: map['speed'] as int? ?? 5,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,10 +63,8 @@ class Step {
|
||||
int? mixTime,
|
||||
int? magnetTime,
|
||||
int? volume,
|
||||
String? mixSpeed,
|
||||
String? blowSpeed,
|
||||
int? blowTime,
|
||||
int? needleSpeed,
|
||||
int? speed,
|
||||
}) {
|
||||
return Step(
|
||||
id: id ?? this.id,
|
||||
@@ -85,10 +75,8 @@ class Step {
|
||||
mixTime: mixTime ?? this.mixTime,
|
||||
magnetTime: magnetTime ?? this.magnetTime,
|
||||
volume: volume ?? this.volume,
|
||||
mixSpeed: mixSpeed ?? this.mixSpeed,
|
||||
blowSpeed: blowSpeed ?? this.blowSpeed,
|
||||
blowTime: blowTime ?? this.blowTime,
|
||||
needleSpeed: needleSpeed ?? this.needleSpeed,
|
||||
speed: speed ?? this.speed,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,12 +48,6 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 返回按钮
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.go('/'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
l10n?.programs ?? '程序管理',
|
||||
style: const TextStyle(
|
||||
|
||||
@@ -48,6 +48,8 @@ class ProgramImportService {
|
||||
name: programData['name'] as String,
|
||||
createdAt: programData['createdAt'] ?? DateTime.now().toString().split('.')[0],
|
||||
status: programData['status'] ?? 1,
|
||||
temperature: programData['temperature'] as int? ?? 50,
|
||||
airflowTime: programData['airflowTime'] as int? ?? 60,
|
||||
);
|
||||
|
||||
final programId = await _programService.addProgram(program);
|
||||
@@ -65,10 +67,8 @@ class ProgramImportService {
|
||||
mixTime: stepData['mixTime'] as int? ?? 0,
|
||||
magnetTime: stepData['magnetTime'] as int? ?? 0,
|
||||
volume: stepData['volume'] as int? ?? 0,
|
||||
mixSpeed: stepData['mixSpeed'] as String? ?? '中速',
|
||||
blowSpeed: stepData['blowSpeed'] as String? ?? '中速',
|
||||
blowTime: stepData['blowTime'] as int? ?? 0,
|
||||
needleSpeed: stepData['needleSpeed'] as int? ?? 5,
|
||||
speed: stepData['speed'] as int? ?? 5,
|
||||
);
|
||||
await _programService.addStep(step);
|
||||
}
|
||||
@@ -107,16 +107,16 @@ class ProgramImportService {
|
||||
'name': program.name,
|
||||
'createdAt': program.createdAt,
|
||||
'status': program.status,
|
||||
'temperature': program.temperature,
|
||||
'airflowTime': program.airflowTime,
|
||||
'steps': steps.map((s) => {
|
||||
'position': s.position,
|
||||
'name': s.name,
|
||||
'mixTime': s.mixTime,
|
||||
'magnetTime': s.magnetTime,
|
||||
'volume': s.volume,
|
||||
'mixSpeed': s.mixSpeed,
|
||||
'blowSpeed': s.blowSpeed,
|
||||
'blowTime': s.blowTime,
|
||||
'needleSpeed': s.needleSpeed,
|
||||
'speed': s.speed,
|
||||
}).toList(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,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/utils/constants.dart';
|
||||
import '../../../shared/widgets/common_button.dart';
|
||||
import '../models/program.dart';
|
||||
import '../providers/programs_provider.dart';
|
||||
@@ -21,6 +22,8 @@ class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late TextEditingController _codeController;
|
||||
late TextEditingController _nameController;
|
||||
late TextEditingController _temperatureController;
|
||||
late TextEditingController _airflowTimeController;
|
||||
bool _isEnabled = true;
|
||||
bool _isSaving = false;
|
||||
|
||||
@@ -29,6 +32,10 @@ class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
|
||||
super.initState();
|
||||
_codeController = TextEditingController(text: widget.program?.code ?? '');
|
||||
_nameController = TextEditingController(text: widget.program?.name ?? '');
|
||||
_temperatureController =
|
||||
TextEditingController(text: '${widget.program?.temperature ?? 50}');
|
||||
_airflowTimeController =
|
||||
TextEditingController(text: '${widget.program?.airflowTime ?? 60}');
|
||||
_isEnabled = widget.program?.status == 1;
|
||||
}
|
||||
|
||||
@@ -36,6 +43,8 @@ class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
|
||||
void dispose() {
|
||||
_codeController.dispose();
|
||||
_nameController.dispose();
|
||||
_temperatureController.dispose();
|
||||
_airflowTimeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -95,6 +104,38 @@ class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 温度和吹气时间
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _temperatureController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '${l10n?.temperature ?? '温度'} (°C)',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _airflowTimeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '${l10n?.airflowTime ?? '吹气时间'} (${Constants.timeUnitSeconds})',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 状态开关
|
||||
Row(
|
||||
children: [
|
||||
@@ -161,6 +202,8 @@ class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
|
||||
name: _nameController.text.trim(),
|
||||
createdAt: widget.program?.createdAt ?? now,
|
||||
status: _isEnabled ? 1 : 0,
|
||||
temperature: int.tryParse(_temperatureController.text) ?? 50,
|
||||
airflowTime: int.tryParse(_airflowTimeController.text) ?? 60,
|
||||
);
|
||||
|
||||
bool success;
|
||||
|
||||
Reference in New Issue
Block a user