chore(project): 初始化项目基础配置文件
- 添加 CodeGraph、Android 和通用 gitignore 配置 - 创建项目元数据文件跟踪 Flutter 项目属性 - 添加 Codex AI 指导文档 AGENTS.md 说明项目架构 - 配置代码分析选项 analysis_options.yaml - 设置 Android 应用清单权限和 Kiosk 模式配置 - 实现中英文国际化支持 AppLocalizations - 配置 GoRouter 应用路由导航 - 创建明亮工业控制风格的主题配置 AppTheme
This commit is contained in:
73
lib/features/device/models/device_state.dart
Normal file
73
lib/features/device/models/device_state.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
/// 设备状态模型
|
||||
enum DeviceStatus { idle, running, paused, error }
|
||||
|
||||
/// 设备状态数据
|
||||
class DeviceState {
|
||||
final DeviceStatus status;
|
||||
final String? currentProgram;
|
||||
final String? currentPosition;
|
||||
final int? currentStepNo;
|
||||
final String? currentStepName;
|
||||
final int? remainingSeconds;
|
||||
final double? progress;
|
||||
final bool lightingOn;
|
||||
|
||||
DeviceState({
|
||||
this.status = DeviceStatus.idle,
|
||||
this.currentProgram,
|
||||
this.currentPosition,
|
||||
this.currentStepNo,
|
||||
this.currentStepName,
|
||||
this.remainingSeconds,
|
||||
this.progress,
|
||||
this.lightingOn = false,
|
||||
});
|
||||
|
||||
bool get isRunning => status == DeviceStatus.running;
|
||||
bool get isPaused => status == DeviceStatus.paused;
|
||||
bool get isIdle => status == DeviceStatus.idle;
|
||||
bool get hasError => status == DeviceStatus.error;
|
||||
|
||||
String statusText() {
|
||||
switch (status) {
|
||||
case DeviceStatus.running:
|
||||
return '运行中';
|
||||
case DeviceStatus.paused:
|
||||
return '已暂停';
|
||||
case DeviceStatus.error:
|
||||
return '错误';
|
||||
case DeviceStatus.idle:
|
||||
return '未运行';
|
||||
}
|
||||
}
|
||||
|
||||
String formatRemainingTime() {
|
||||
if (remainingSeconds == null) return '--:--:--';
|
||||
final hours = remainingSeconds! ~/ 3600;
|
||||
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')}';
|
||||
}
|
||||
|
||||
DeviceState copyWith({
|
||||
DeviceStatus? status,
|
||||
String? currentProgram,
|
||||
String? currentPosition,
|
||||
int? currentStepNo,
|
||||
String? currentStepName,
|
||||
int? remainingSeconds,
|
||||
double? progress,
|
||||
bool? lightingOn,
|
||||
}) {
|
||||
return DeviceState(
|
||||
status: status ?? this.status,
|
||||
currentProgram: currentProgram ?? this.currentProgram,
|
||||
currentPosition: currentPosition ?? this.currentPosition,
|
||||
currentStepNo: currentStepNo ?? this.currentStepNo,
|
||||
currentStepName: currentStepName ?? this.currentStepName,
|
||||
remainingSeconds: remainingSeconds ?? this.remainingSeconds,
|
||||
progress: progress ?? this.progress,
|
||||
lightingOn: lightingOn ?? this.lightingOn,
|
||||
);
|
||||
}
|
||||
}
|
||||
188
lib/features/device/providers/run_state_provider.dart
Normal file
188
lib/features/device/providers/run_state_provider.dart
Normal file
@@ -0,0 +1,188 @@
|
||||
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';
|
||||
|
||||
/// 运行状态枚举
|
||||
enum RunStatus {
|
||||
idle, // 待机
|
||||
running, // 运行中
|
||||
paused, // 已暂停
|
||||
completed,// 已完成
|
||||
error, // 错误
|
||||
}
|
||||
|
||||
/// 运行状态
|
||||
class RunState {
|
||||
final RunStatus status;
|
||||
final Program? currentProgram;
|
||||
final List<Step> steps;
|
||||
final int currentStepIndex;
|
||||
final int remainingSeconds;
|
||||
final double progress;
|
||||
final String? currentWell;
|
||||
|
||||
const RunState({
|
||||
this.status = RunStatus.idle,
|
||||
this.currentProgram,
|
||||
this.steps = const [],
|
||||
this.currentStepIndex = 0,
|
||||
this.remainingSeconds = 0,
|
||||
this.progress = 0,
|
||||
this.currentWell,
|
||||
});
|
||||
|
||||
RunState copyWith({
|
||||
RunStatus? status,
|
||||
Program? currentProgram,
|
||||
List<Step>? steps,
|
||||
int? currentStepIndex,
|
||||
int? remainingSeconds,
|
||||
double? progress,
|
||||
String? currentWell,
|
||||
bool clearProgram = false,
|
||||
bool clearWell = false,
|
||||
}) {
|
||||
return RunState(
|
||||
status: status ?? this.status,
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取当前步骤
|
||||
Step? get currentStep {
|
||||
if (steps.isEmpty || currentStepIndex >= steps.length) return null;
|
||||
return steps[currentStepIndex];
|
||||
}
|
||||
|
||||
/// 格式化剩余时间 (HH:MM:SS)
|
||||
String get formattedRemainingTime {
|
||||
final hours = remainingSeconds ~/ 3600;
|
||||
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')}';
|
||||
}
|
||||
|
||||
/// 格式化进度百分比
|
||||
String get formattedProgress {
|
||||
return '${(progress * 100).toStringAsFixed(0)}%';
|
||||
}
|
||||
}
|
||||
|
||||
/// 运行状态 Notifier
|
||||
class RunStateNotifier extends StateNotifier<RunState> {
|
||||
final MockRunner _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 (steps.isEmpty) {
|
||||
state = state.copyWith(status: RunStatus.error);
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
status: RunStatus.running,
|
||||
currentProgram: program,
|
||||
steps: steps,
|
||||
currentStepIndex: 0,
|
||||
progress: 0,
|
||||
);
|
||||
|
||||
_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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 暂停运行
|
||||
void pause() {
|
||||
if (state.status == RunStatus.running) {
|
||||
_runner.pause();
|
||||
state = state.copyWith(status: RunStatus.paused);
|
||||
}
|
||||
}
|
||||
|
||||
/// 继续运行
|
||||
void resume() {
|
||||
if (state.status == RunStatus.paused) {
|
||||
_runner.resume();
|
||||
state = state.copyWith(status: RunStatus.running);
|
||||
}
|
||||
}
|
||||
|
||||
/// 停止运行
|
||||
void stop() {
|
||||
_runner.stop();
|
||||
state = const RunState(status: RunStatus.idle);
|
||||
}
|
||||
|
||||
/// 重置状态
|
||||
void reset() {
|
||||
stop();
|
||||
}
|
||||
|
||||
/// 加载程序步骤(从数据库读取)
|
||||
Future<List<Step>> _loadSteps(int programId) async {
|
||||
return await _programService.getStepsByProgramId(programId);
|
||||
}
|
||||
}
|
||||
|
||||
/// MockRunner Provider
|
||||
final mockRunnerProvider = Provider<MockRunner>((ref) {
|
||||
return MockRunner();
|
||||
});
|
||||
|
||||
/// ProgramService Provider
|
||||
final programServiceProvider = Provider<ProgramService>((ref) {
|
||||
return ProgramService.instance;
|
||||
});
|
||||
|
||||
/// 运行状态 Provider
|
||||
final runStateProvider =
|
||||
StateNotifierProvider<RunStateNotifier, RunState>((ref) {
|
||||
final runner = ref.watch(mockRunnerProvider);
|
||||
final programService = ref.watch(programServiceProvider);
|
||||
return RunStateNotifier(runner, programService);
|
||||
});
|
||||
|
||||
/// 是否正在运行 Provider
|
||||
final isRunningProvider = Provider<bool>((ref) {
|
||||
final status = ref.watch(runStateProvider).status;
|
||||
return status == RunStatus.running;
|
||||
});
|
||||
|
||||
/// 是否已暂停 Provider
|
||||
final isPausedProvider = Provider<bool>((ref) {
|
||||
final status = ref.watch(runStateProvider).status;
|
||||
return status == RunStatus.paused;
|
||||
});
|
||||
190
lib/features/device/services/mock_runner.dart
Normal file
190
lib/features/device/services/mock_runner.dart
Normal file
@@ -0,0 +1,190 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
114
lib/features/device/services/mock_runner_impl.dart
Normal file
114
lib/features/device/services/mock_runner_impl.dart
Normal file
@@ -0,0 +1,114 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
54
lib/features/device/services/runner_interface.dart
Normal file
54
lib/features/device/services/runner_interface.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import '../../programs/models/program.dart';
|
||||
import '../../programs/models/step.dart';
|
||||
|
||||
/// 运行器状态
|
||||
enum RunnerStatus {
|
||||
idle,
|
||||
running,
|
||||
paused,
|
||||
completed,
|
||||
error,
|
||||
}
|
||||
|
||||
/// 运行器回调
|
||||
class RunnerCallbacks {
|
||||
/// 步骤进度回调: (stepIndex, remainingSeconds, progress, currentWell)
|
||||
final void Function(int stepIndex, int remainingSeconds, double progress, String well)? onProgress;
|
||||
|
||||
/// 运行完成回调
|
||||
final void Function()? onComplete;
|
||||
|
||||
/// 错误回调
|
||||
final void Function(String error)? onError;
|
||||
|
||||
const RunnerCallbacks({
|
||||
this.onProgress,
|
||||
this.onComplete,
|
||||
this.onError,
|
||||
});
|
||||
}
|
||||
|
||||
/// 运行器抽象接口
|
||||
/// 定义硬件运行控制的标准接口
|
||||
abstract class Runner {
|
||||
/// 当前状态
|
||||
RunnerStatus status = RunnerStatus.idle;
|
||||
|
||||
/// 启动程序运行
|
||||
void start(Program program, List<Step> steps, RunnerCallbacks callbacks);
|
||||
|
||||
/// 暂停运行
|
||||
void pause();
|
||||
|
||||
/// 继续运行
|
||||
void resume();
|
||||
|
||||
/// 停止运行
|
||||
void stop();
|
||||
|
||||
/// 获取当前状态
|
||||
RunnerStatus getStatus();
|
||||
|
||||
/// 释放资源
|
||||
void dispose();
|
||||
}
|
||||
91
lib/features/device/services/serial_runner.dart
Normal file
91
lib/features/device/services/serial_runner.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
import '../../programs/models/program.dart';
|
||||
import '../../programs/models/step.dart';
|
||||
import 'runner_interface.dart';
|
||||
|
||||
/// 串口运行器(真实硬件实现)
|
||||
/// 实现与设备的串口通信
|
||||
class SerialRunner implements Runner {
|
||||
@override
|
||||
RunnerStatus status = RunnerStatus.idle;
|
||||
|
||||
/// 串口配置
|
||||
final String portName;
|
||||
final int baudRate;
|
||||
final int dataBits;
|
||||
final int stopBits;
|
||||
|
||||
SerialRunner({
|
||||
this.portName = '/dev/ttyUSB0',
|
||||
this.baudRate = 9600,
|
||||
this.dataBits = 8,
|
||||
this.stopBits = 1,
|
||||
});
|
||||
|
||||
@override
|
||||
void start(Program program, List<Step> steps, RunnerCallbacks callbacks) {
|
||||
// TODO: 实现串口通信启动逻辑
|
||||
// 1. 打开串口连接
|
||||
// 2. 发送程序配置
|
||||
// 3. 按步骤发送控制指令
|
||||
// 4. 接收设备反馈并更新状态
|
||||
|
||||
status = RunnerStatus.running;
|
||||
|
||||
// 示例:发送启动指令
|
||||
// _sendCommand('START', program.code);
|
||||
|
||||
// 示例:监听设备状态
|
||||
// _listenToDevice(callbacks);
|
||||
}
|
||||
|
||||
@override
|
||||
void pause() {
|
||||
if (status == RunnerStatus.running) {
|
||||
// _sendCommand('PAUSE');
|
||||
status = RunnerStatus.paused;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void resume() {
|
||||
if (status == RunnerStatus.paused) {
|
||||
// _sendCommand('RESUME');
|
||||
status = RunnerStatus.running;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void stop() {
|
||||
// _sendCommand('STOP');
|
||||
// _closeConnection();
|
||||
status = RunnerStatus.idle;
|
||||
}
|
||||
|
||||
@override
|
||||
RunnerStatus getStatus() => status;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
stop();
|
||||
}
|
||||
|
||||
/// 发送控制指令(待硬件协议确定后实现)
|
||||
Future<void> _sendCommand(String command, [String? data]) async {
|
||||
// TODO: 根据硬件通信协议实现
|
||||
// 示例协议格式: [CMD:data] 或 二进制协议
|
||||
}
|
||||
|
||||
/// 监听设备反馈(待硬件协议确定后实现)
|
||||
void _listenToDevice(RunnerCallbacks callbacks) {
|
||||
// TODO: 解析设备返回的状态数据
|
||||
// 状态格式示例: [STEP:1,TIME:60,POS:A1]
|
||||
}
|
||||
|
||||
/// 执行单个步骤
|
||||
Future<void> _executeStep(Step step) async {
|
||||
// TODO: 根据步骤参数生成控制指令
|
||||
// 混合: MIX(position, time, speed)
|
||||
// 吸磁: MAGNET(position, time)
|
||||
// 吹气: BLOW(position, speed, time)
|
||||
}
|
||||
}
|
||||
202
lib/features/home/pages/complete_page.dart
Normal file
202
lib/features/home/pages/complete_page.dart
Normal file
@@ -0,0 +1,202 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
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';
|
||||
|
||||
/// 运行完成提示页面
|
||||
class CompletePage extends ConsumerWidget {
|
||||
const CompletePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final runState = ref.watch(runStateProvider);
|
||||
final runNotifier = ref.read(runStateProvider.notifier);
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
color: AppTheme.backgroundColor,
|
||||
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('/');
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 操作指引示意图
|
||||
Widget _buildOperationGuide() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppTheme.idleColor.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'操作步骤',
|
||||
style: TextStyle(
|
||||
color: AppTheme.textPrimary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildStepItem(1, '取出样本', Icons.science),
|
||||
_buildStepItem(2, '滴入检测卡', Icons.water_drop),
|
||||
_buildStepItem(3, '等待反应', Icons.timer),
|
||||
_buildStepItem(4, '查看结果', Icons.visibility),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 步骤项
|
||||
Widget _buildStepItem(int number, String text, IconData icon) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, color: AppTheme.primaryColor, size: 24),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'$number',
|
||||
style: TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
176
lib/features/home/pages/home_page.dart
Normal file
176
lib/features/home/pages/home_page.dart
Normal file
@@ -0,0 +1,176 @@
|
||||
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/theme/app_theme.dart';
|
||||
import '../../device/providers/run_state_provider.dart';
|
||||
import '../../programs/pages/programs_page.dart';
|
||||
import '../../settings/pages/settings_page.dart';
|
||||
import '../widgets/status_bar.dart';
|
||||
import '../widgets/program_list.dart';
|
||||
import '../widgets/running_control_panel.dart';
|
||||
import '../widgets/run_status_monitor.dart';
|
||||
|
||||
/// 首页 - 设备控制面板 (暗色工业风格)
|
||||
/// 布局:状态栏 + 导航标签栏 + 内容区(设备控制/程序管理/系统设置)
|
||||
class HomePage extends ConsumerStatefulWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends ConsumerState<HomePage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
bool _lightOn = false;
|
||||
final bool _ceramicSleeveInstalled = false; // TODO: 后续对接硬件传感器后改为可变状态
|
||||
int _currentIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
DatabaseService.instance.initTestData();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final runState = ref.watch(runStateProvider);
|
||||
|
||||
// 监听运行完成状态,自动跳转
|
||||
ref.listen<RunState>(runStateProvider, (prev, next) {
|
||||
if (prev?.status != RunStatus.completed && next.status == RunStatus.completed) {
|
||||
// 仅首页才自动跳转
|
||||
if (_currentIndex == 0) {
|
||||
context.push('/complete');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
color: AppTheme.bgDeep,
|
||||
child: Column(
|
||||
children: [
|
||||
// 状态栏
|
||||
StatusBar(
|
||||
isRunning: runState.status == RunStatus.running,
|
||||
lightOn: _lightOn,
|
||||
onLightToggle: () {
|
||||
setState(() {
|
||||
_lightOn = !_lightOn;
|
||||
});
|
||||
},
|
||||
ceramicSleeveInstalled: _ceramicSleeveInstalled,
|
||||
),
|
||||
|
||||
// 导航标签栏
|
||||
_buildTabBar(),
|
||||
|
||||
// 内容区
|
||||
Expanded(
|
||||
child: IndexedStack(
|
||||
index: _currentIndex,
|
||||
children: [
|
||||
_buildDeviceControlPage(runState),
|
||||
const ProgramsPage(),
|
||||
const SettingsPage(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 导航标签栏
|
||||
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),
|
||||
child: Row(
|
||||
children: [
|
||||
// 左侧:程序列表(运行时锁定)
|
||||
Opacity(
|
||||
opacity: runState.status == RunStatus.idle ? 1.0 : 0.6,
|
||||
child: IgnorePointer(
|
||||
ignoring: runState.status != RunStatus.idle,
|
||||
child: const ProgramList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
// 右侧:运行控制区域
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
const Expanded(child: RunningControlPanel()),
|
||||
if (runState.status != RunStatus.idle) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Expanded(child: RunStatusMonitor()),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
222
lib/features/home/widgets/program_list.dart
Normal file
222
lib/features/home/widgets/program_list.dart
Normal file
@@ -0,0 +1,222 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../core/localization/app_localizations.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../programs/models/program.dart';
|
||||
import '../../programs/providers/programs_provider.dart';
|
||||
|
||||
/// 程序列表组件 - 暗色工业风格
|
||||
/// 显示程序卡片列表,支持选择操作
|
||||
class ProgramList extends ConsumerWidget {
|
||||
const ProgramList({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final programsState = ref.watch(programsProvider);
|
||||
final programsNotifier = ref.read(programsProvider.notifier);
|
||||
|
||||
return Container(
|
||||
width: 380,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.cardBg,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppTheme.borderSubtle, width: 1),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 标题
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.list_alt, color: AppTheme.textHeading, size: 18),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
l10n?.availablePrograms ?? '可用程序',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textHeading,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 程序列表
|
||||
Expanded(
|
||||
child: programsState.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: programsState.programs.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
l10n?.noData ?? '暂无数据',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
itemCount: programsState.programs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final program = programsState.programs[index];
|
||||
final isSelected =
|
||||
programsState.selectedProgramId == program.id;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: _ProgramCard(
|
||||
program: program,
|
||||
isSelected: isSelected,
|
||||
onTap: () {
|
||||
programsNotifier.selectProgram(program.id);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 单个程序卡片 - 暗色工业风格
|
||||
class _ProgramCard extends StatelessWidget {
|
||||
final Program program;
|
||||
final bool isSelected;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const _ProgramCard({
|
||||
required this.program,
|
||||
this.isSelected = false,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dateFormat = DateFormat('yyyy-MM-dd HH:mm');
|
||||
final createdAt = _parseDate(program.createdAt);
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppTheme.cardSelectedBg : AppTheme.cardBg,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppTheme.accentPrimary : AppTheme.borderSubtle,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 选择指示器
|
||||
Container(
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isSelected
|
||||
? AppTheme.accentPrimary
|
||||
: Colors.transparent,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? AppTheme.accentPrimary
|
||||
: AppTheme.statusStopped,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: isSelected
|
||||
? const Icon(Icons.check, color: Colors.white, size: 12)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 程序信息
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
program.code,
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: program.status == 1
|
||||
? AppTheme.statusRunning.withValues(alpha: 0.15)
|
||||
: AppTheme.statusStopped.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
program.status == 1 ? '启用' : '停用',
|
||||
style: TextStyle(
|
||||
color: program.status == 1
|
||||
? AppTheme.statusRunning
|
||||
: AppTheme.statusStopped,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
program.name,
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textHeading,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
createdAt != null
|
||||
? dateFormat.format(createdAt)
|
||||
: program.createdAt,
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textTertiary,
|
||||
fontSize: 11,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
DateTime? _parseDate(String dateStr) {
|
||||
try {
|
||||
return DateTime.parse(dateStr);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
242
lib/features/home/widgets/run_status_monitor.dart
Normal file
242
lib/features/home/widgets/run_status_monitor.dart
Normal file
@@ -0,0 +1,242 @@
|
||||
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 '../../device/providers/run_state_provider.dart';
|
||||
|
||||
/// 运行状态监控面板 - 暗色工业风格
|
||||
/// 显示当前孔位、步骤、倒计时、进度条、参数详情
|
||||
class RunStatusMonitor extends ConsumerWidget {
|
||||
const RunStatusMonitor({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final runState = ref.watch(runStateProvider);
|
||||
|
||||
if (runState.status == RunStatus.idle) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.cardBg,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppTheme.borderSubtle, width: 1),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题 + 程序名
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
l10n?.runningMonitor ?? '运行状态监控',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textHeading,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
runState.currentProgram?.name ?? '',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.accentPrimary,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// 进度信息横排 (孔位 / 步骤 / 剩余时间)
|
||||
Row(
|
||||
children: [
|
||||
// 当前孔位
|
||||
_buildInfoBlock(
|
||||
label: l10n?.currentHole ?? '当前孔位',
|
||||
value: runState.currentWell ?? '--',
|
||||
valueColor: AppTheme.textHeading,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
// 当前步骤
|
||||
_buildInfoBlock(
|
||||
label: l10n?.currentStep ?? '当前步骤',
|
||||
value: '${l10n?.stepNo ?? '步骤'} ${runState.currentStepIndex + 1}',
|
||||
subValue: runState.currentStep?.name ?? '--',
|
||||
valueColor: AppTheme.accentInfo,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
// 剩余时间
|
||||
_buildInfoBlock(
|
||||
label: l10n?.remainingTime ?? '剩余时间',
|
||||
value: runState.formattedRemainingTime,
|
||||
valueColor: AppTheme.textHeading,
|
||||
valueSize: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// 总进度条
|
||||
_buildProgressBar(l10n, runState),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// 步骤参数
|
||||
if (runState.currentStep != null)
|
||||
_buildStepParams(l10n, runState.currentStep!),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 信息块
|
||||
Widget _buildInfoBlock({
|
||||
required String label,
|
||||
required String value,
|
||||
String? subValue,
|
||||
Color valueColor = AppTheme.textHeading,
|
||||
double valueSize = 16,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textTertiary,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: valueColor,
|
||||
fontSize: valueSize,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
if (subValue != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subValue,
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 进度条
|
||||
Widget _buildProgressBar(AppLocalizations? l10n, RunState runState) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
l10n?.progress ?? '总进度',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textTertiary,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
runState.formattedProgress,
|
||||
style: const TextStyle(
|
||||
color: AppTheme.accentPrimary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: runState.progress,
|
||||
minHeight: 8,
|
||||
backgroundColor: const Color(0xFF1E293B),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.accentPrimary),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 步骤参数详情
|
||||
Widget _buildStepParams(AppLocalizations? l10n, dynamic step) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n?.stepParams ?? '步骤参数',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textTertiary,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
if (step.mixTime > 0)
|
||||
_buildParamRow(
|
||||
l10n?.speed ?? '转速',
|
||||
'${step.mixSpeed}',
|
||||
),
|
||||
if (step.magnetTime > 0)
|
||||
_buildParamRow(
|
||||
l10n?.temperature ?? '温度',
|
||||
'65.0 °C',
|
||||
),
|
||||
_buildParamRow(
|
||||
l10n?.duration ?? '持续时间',
|
||||
step.mixTime > 0 ? '${step.mixTime} min' : '--',
|
||||
),
|
||||
_buildParamRow(
|
||||
l10n?.sampleVolume ?? '样品体积',
|
||||
'10.0 mL',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 参数行
|
||||
Widget _buildParamRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textTertiary,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textPrimary,
|
||||
fontSize: 11,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
365
lib/features/home/widgets/running_control_panel.dart
Normal file
365
lib/features/home/widgets/running_control_panel.dart
Normal file
@@ -0,0 +1,365 @@
|
||||
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/widgets/common_button.dart';
|
||||
import '../../device/providers/run_state_provider.dart';
|
||||
import '../../programs/providers/programs_provider.dart';
|
||||
|
||||
/// 运行控制面板 - 暗色工业风格
|
||||
/// 显示当前程序信息、瓷套棒状态和运行控制按钮
|
||||
class RunningControlPanel extends ConsumerWidget {
|
||||
const RunningControlPanel({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final runState = ref.watch(runStateProvider);
|
||||
final programsState = ref.watch(programsProvider);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.cardBg,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppTheme.borderSubtle, width: 1),
|
||||
),
|
||||
child: runState.status == RunStatus.idle
|
||||
? _buildIdleState(context, ref, l10n, programsState.selectedProgram)
|
||||
: _buildRunningState(context, ref, l10n, runState),
|
||||
);
|
||||
}
|
||||
|
||||
/// 待机状态布局
|
||||
Widget _buildIdleState(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
AppLocalizations? l10n,
|
||||
dynamic selectedProgram,
|
||||
) {
|
||||
final runNotifier = ref.read(runStateProvider.notifier);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 当前选中程序显示
|
||||
if (selectedProgram != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.cardSelectedBg,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: AppTheme.accentPrimary, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'${l10n?.selectedProgramLabel ?? '当前选中'}:',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textTertiary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${selectedProgram.code} ${selectedProgram.name}',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.accentPrimary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.cardBg,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: AppTheme.borderSubtle, width: 1),
|
||||
),
|
||||
child: Text(
|
||||
l10n?.pleaseSelectProgram ?? '请选择要运行的程序',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textTertiary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
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: [
|
||||
// 开始运行按钮
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: CommonButton(
|
||||
text: l10n?.startRun ?? '开始运行',
|
||||
icon: Icons.play_arrow,
|
||||
type: ButtonType.primary,
|
||||
enabled: selectedProgram != null,
|
||||
onPressed: selectedProgram != null
|
||||
? () => runNotifier.start(selectedProgram)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 暂停/继续按钮(待机态禁用)
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: CommonButton(
|
||||
text: l10n?.pause ?? '暂停',
|
||||
icon: Icons.pause,
|
||||
type: ButtonType.secondary,
|
||||
enabled: false,
|
||||
onPressed: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 停止按钮(待机态禁用)
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: CommonButton(
|
||||
text: l10n?.stop ?? '停止',
|
||||
icon: Icons.stop,
|
||||
type: ButtonType.danger,
|
||||
enabled: false,
|
||||
onPressed: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 运行状态布局
|
||||
Widget _buildRunningState(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
AppLocalizations? l10n,
|
||||
RunState runState,
|
||||
) {
|
||||
final runNotifier = ref.read(runStateProvider.notifier);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 当前程序名称
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.cardSelectedBg,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: AppTheme.accentPrimary, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'${l10n?.selectedProgramLabel ?? '当前选中'}:',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.textTertiary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
runState.currentProgram?.name ?? '',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.accentPrimary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 控制按钮
|
||||
Row(
|
||||
children: [
|
||||
// 开始/继续按钮
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: CommonButton(
|
||||
text: runState.status == RunStatus.paused
|
||||
? (l10n?.continue_ ?? '继续')
|
||||
: (l10n?.run ?? '运行'),
|
||||
icon: runState.status == RunStatus.paused
|
||||
? Icons.play_arrow
|
||||
: Icons.play_arrow,
|
||||
type: ButtonType.primary,
|
||||
onPressed: () => runNotifier.resume(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 暂停按钮
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: CommonButton(
|
||||
text: l10n?.pause ?? '暂停',
|
||||
icon: Icons.pause,
|
||||
type: ButtonType.warning,
|
||||
onPressed: runState.status == RunStatus.paused
|
||||
? null
|
||||
: () => runNotifier.pause(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 停止按钮
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: CommonButton(
|
||||
text: l10n?.stop ?? '停止',
|
||||
icon: Icons.stop,
|
||||
type: ButtonType.danger,
|
||||
onPressed: () => _showStopConfirm(context, runNotifier, l10n),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 状态指示
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: runState.status == RunStatus.paused
|
||||
? AppTheme.accentWarning
|
||||
: AppTheme.statusRunning,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
runState.status == RunStatus.paused
|
||||
? (l10n?.paused ?? '已暂停')
|
||||
: (l10n?.running ?? '运行中'),
|
||||
style: TextStyle(
|
||||
color: runState.status == RunStatus.paused
|
||||
? AppTheme.accentWarning
|
||||
: AppTheme.statusRunning,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示停止确认对话框
|
||||
void _showStopConfirm(
|
||||
BuildContext context,
|
||||
RunStateNotifier runNotifier,
|
||||
AppLocalizations? l10n,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: AppTheme.cardBg,
|
||||
title: Text(
|
||||
l10n?.confirm ?? '确认',
|
||||
style: const TextStyle(color: AppTheme.textHeading),
|
||||
),
|
||||
content: Text(
|
||||
l10n?.stopConfirm ?? '确定要停止当前运行的程序吗?',
|
||||
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.accentCritical,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
runNotifier.stop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(l10n?.confirm ?? '确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
181
lib/features/home/widgets/status_bar.dart
Normal file
181
lib/features/home/widgets/status_bar.dart
Normal file
@@ -0,0 +1,181 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/localization/app_localizations.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../shared/widgets/status_indicator.dart';
|
||||
|
||||
/// 状态栏组件 - 明亮工业风格
|
||||
/// 显示设备名称、实时时钟、系统状态、照明控制、瓷套棒状态
|
||||
class StatusBar extends StatefulWidget {
|
||||
final bool isRunning;
|
||||
final bool lightOn;
|
||||
final VoidCallback? onLightToggle;
|
||||
final bool ceramicSleeveInstalled;
|
||||
|
||||
const StatusBar({
|
||||
super.key,
|
||||
this.isRunning = false,
|
||||
this.lightOn = false,
|
||||
this.onLightToggle,
|
||||
this.ceramicSleeveInstalled = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatusBar> createState() => _StatusBarState();
|
||||
}
|
||||
|
||||
class _StatusBarState extends State<StatusBar> {
|
||||
String _currentTime = '';
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateTime();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (_) => _updateTime());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateTime() {
|
||||
final now = DateTime.now();
|
||||
_currentTime =
|
||||
'${now.year}-${_twoDigits(now.month)}-${_twoDigits(now.day)} '
|
||||
'${_twoDigits(now.hour)}:${_twoDigits(now.minute)}:${_twoDigits(now.second)}';
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
String _twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return Container(
|
||||
height: 56,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.08),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.precision_manufacturing, color: Colors.white, size: 22),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
l10n?.deviceName ?? '污水毒品前处理一体机',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
_LightToggleButton(isOn: widget.lightOn, onTap: widget.onLightToggle),
|
||||
const SizedBox(width: 16),
|
||||
_CeramicSleeveStatus(installed: widget.ceramicSleeveInstalled),
|
||||
const SizedBox(width: 20),
|
||||
StatusIndicator(
|
||||
text: widget.isRunning
|
||||
? (l10n?.running ?? '运行中')
|
||||
: (l10n?.idle ?? '未运行'),
|
||||
status: widget.isRunning
|
||||
? DeviceStatusType.running
|
||||
: DeviceStatusType.idle,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Text(
|
||||
_currentTime,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CeramicSleeveStatus extends StatelessWidget {
|
||||
final bool installed;
|
||||
const _CeramicSleeveStatus({required this.installed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: installed ? Colors.greenAccent : Colors.redAccent,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
installed ? '瓷套棒: 已安装' : '瓷套棒: 未安装',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LightToggleButton extends StatelessWidget {
|
||||
final bool isOn;
|
||||
final VoidCallback? onTap;
|
||||
const _LightToggleButton({this.isOn = false, this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: isOn
|
||||
? Colors.white.withValues(alpha: 0.25)
|
||||
: Colors.white.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
isOn ? Icons.lightbulb : Icons.lightbulb_outline_rounded,
|
||||
color: isOn ? Colors.yellowAccent : Colors.white.withValues(alpha: 0.8),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
199
lib/features/program_detail/pages/program_detail_page.dart
Normal file
199
lib/features/program_detail/pages/program_detail_page.dart
Normal file
@@ -0,0 +1,199 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../core/localization/app_localizations.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../shared/widgets/common_button.dart';
|
||||
import '../providers/steps_provider.dart';
|
||||
import '../widgets/step_list.dart';
|
||||
import '../widgets/step_form.dart';
|
||||
import '../../programs/providers/programs_provider.dart';
|
||||
|
||||
/// 程序详情页面
|
||||
/// 左侧步骤列表 + 右侧参数表单
|
||||
class ProgramDetailPage extends ConsumerStatefulWidget {
|
||||
final String programId;
|
||||
|
||||
const ProgramDetailPage({super.key, required this.programId});
|
||||
|
||||
@override
|
||||
ConsumerState<ProgramDetailPage> createState() => _ProgramDetailPageState();
|
||||
}
|
||||
|
||||
class _ProgramDetailPageState extends ConsumerState<ProgramDetailPage> {
|
||||
late int _programIdInt;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_programIdInt = int.tryParse(widget.programId) ?? 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final programsState = ref.watch(programsProvider);
|
||||
final program = programsState.programs.where((p) => p.id == _programIdInt).firstOrNull;
|
||||
final stepsState = ref.watch(stepsProvider(_programIdInt));
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 主内容区域
|
||||
Expanded(
|
||||
child: stepsState.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Row(
|
||||
children: [
|
||||
// 左侧:步骤列表
|
||||
SizedBox(
|
||||
width: 400,
|
||||
child: StepList(
|
||||
programId: _programIdInt,
|
||||
steps: stepsState.steps,
|
||||
selectedStepId: stepsState.selectedStepId,
|
||||
onStepSelected: (stepId) {
|
||||
ref.read(stepsProvider(_programIdInt).notifier).selectStep(stepId);
|
||||
},
|
||||
onAddStep: () => _showAddStepDialog(context, ref),
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
ref.read(stepsProvider(_programIdInt).notifier).reorderSteps(oldIndex, newIndex);
|
||||
},
|
||||
onDeleteSteps: (stepIds) {
|
||||
ref.read(stepsProvider(_programIdInt).notifier).deleteSteps(stepIds);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// 分隔线
|
||||
Container(
|
||||
width: 1,
|
||||
color: AppTheme.idleColor.withValues(alpha: 0.3),
|
||||
),
|
||||
|
||||
// 右侧:步骤参数表单
|
||||
Expanded(
|
||||
child: stepsState.selectedStep != null
|
||||
? StepForm(
|
||||
programId: _programIdInt,
|
||||
step: stepsState.selectedStep!,
|
||||
onSave: (step) async {
|
||||
final success = await ref
|
||||
.read(stepsProvider(_programIdInt).notifier)
|
||||
.updateStep(step);
|
||||
if (success) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('步骤已更新'),
|
||||
backgroundColor: AppTheme.successColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.edit_note,
|
||||
size: 64,
|
||||
color: AppTheme.idleColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'请选择或添加步骤',
|
||||
style: TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示添加步骤对话框
|
||||
void _showAddStepDialog(BuildContext context, WidgetRef ref) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => Dialog(
|
||||
child: Container(
|
||||
width: 600,
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: StepForm(
|
||||
programId: _programIdInt,
|
||||
isNew: true,
|
||||
onSave: (step) async {
|
||||
final success = await ref
|
||||
.read(stepsProvider(_programIdInt).notifier)
|
||||
.addStep(step);
|
||||
if (success) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
161
lib/features/program_detail/providers/steps_provider.dart
Normal file
161
lib/features/program_detail/providers/steps_provider.dart
Normal file
@@ -0,0 +1,161 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../programs/models/step.dart';
|
||||
import '../../programs/services/program_service.dart';
|
||||
|
||||
/// 步骤状态
|
||||
class StepsState {
|
||||
final List<Step> steps;
|
||||
final int? selectedStepId;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
const StepsState({
|
||||
this.steps = const [],
|
||||
this.selectedStepId,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
StepsState copyWith({
|
||||
List<Step>? steps,
|
||||
int? selectedStepId,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
bool clearSelection = false,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return StepsState(
|
||||
steps: steps ?? this.steps,
|
||||
selectedStepId: clearSelection ? null : (selectedStepId ?? this.selectedStepId),
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取选中的步骤
|
||||
Step? get selectedStep {
|
||||
if (selectedStepId == null) return null;
|
||||
return steps.where((s) => s.id == selectedStepId).firstOrNull;
|
||||
}
|
||||
}
|
||||
|
||||
/// 步骤 Notifier
|
||||
class StepsNotifier extends StateNotifier<StepsState> {
|
||||
final ProgramService _service;
|
||||
final int programId;
|
||||
|
||||
StepsNotifier(this._service, this.programId) : super(const StepsState()) {
|
||||
loadSteps();
|
||||
}
|
||||
|
||||
/// 加载步骤
|
||||
Future<void> loadSteps() async {
|
||||
state = state.copyWith(isLoading: true, clearError: true);
|
||||
try {
|
||||
final steps = await _service.getStepsByProgramId(programId);
|
||||
state = state.copyWith(steps: steps, isLoading: false);
|
||||
} catch (e) {
|
||||
state = state.copyWith(isLoading: false, error: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// 选择步骤
|
||||
void selectStep(int? stepId) {
|
||||
state = state.copyWith(selectedStepId: stepId);
|
||||
}
|
||||
|
||||
/// 清除选择
|
||||
void clearSelection() {
|
||||
state = state.copyWith(clearSelection: true);
|
||||
}
|
||||
|
||||
/// 添加步骤
|
||||
Future<bool> addStep(Step step) async {
|
||||
try {
|
||||
await _service.addStep(step);
|
||||
await loadSteps();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新步骤
|
||||
Future<bool> updateStep(Step step) async {
|
||||
if (step.id == null) return false;
|
||||
try {
|
||||
await _service.updateStep(step);
|
||||
await loadSteps();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除步骤
|
||||
Future<bool> deleteStep(int stepId) async {
|
||||
try {
|
||||
await _service.deleteStep(stepId);
|
||||
if (state.selectedStepId == stepId) {
|
||||
state = state.copyWith(clearSelection: true);
|
||||
}
|
||||
await loadSteps();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 批量删除步骤
|
||||
Future<bool> deleteSteps(List<int> stepIds) async {
|
||||
try {
|
||||
await _service.deleteSteps(stepIds);
|
||||
if (stepIds.contains(state.selectedStepId)) {
|
||||
state = state.copyWith(clearSelection: true);
|
||||
}
|
||||
await loadSteps();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 重新排序步骤
|
||||
Future<void> reorderSteps(int oldIndex, int newIndex) async {
|
||||
final steps = List<Step>.from(state.steps);
|
||||
final step = steps.removeAt(oldIndex);
|
||||
steps.insert(newIndex, step);
|
||||
|
||||
// 更新 step_no
|
||||
for (int i = 0; i < steps.length; i++) {
|
||||
steps[i] = steps[i].copyWith(stepNo: i + 1);
|
||||
}
|
||||
|
||||
state = state.copyWith(steps: steps);
|
||||
|
||||
// 持久化排序
|
||||
await _service.reorderSteps(programId, steps.map((s) => s.id!).toList());
|
||||
}
|
||||
}
|
||||
|
||||
/// 程序服务 Provider
|
||||
final programServiceProvider = Provider<ProgramService>((ref) {
|
||||
return ProgramService.instance;
|
||||
});
|
||||
|
||||
/// 步骤 Provider(按程序ID)
|
||||
final stepsProvider = StateNotifierProvider.family<StepsNotifier, StepsState, int>(
|
||||
(ref, programId) {
|
||||
final service = ref.watch(programServiceProvider);
|
||||
return StepsNotifier(service, programId);
|
||||
},
|
||||
);
|
||||
|
||||
/// 选中的步骤 Provider
|
||||
final selectedStepProvider = Provider.family<Step?, int>((ref, programId) {
|
||||
return ref.watch(stepsProvider(programId)).selectedStep;
|
||||
});
|
||||
270
lib/features/program_detail/widgets/step_form.dart
Normal file
270
lib/features/program_detail/widgets/step_form.dart
Normal file
@@ -0,0 +1,270 @@
|
||||
import 'package:flutter/material.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 '../../programs/models/step.dart' as models;
|
||||
|
||||
/// 步骤参数表单
|
||||
class StepForm extends StatefulWidget {
|
||||
final int programId;
|
||||
final models.Step? step;
|
||||
final bool isNew;
|
||||
final void Function(models.Step) onSave;
|
||||
|
||||
const StepForm({
|
||||
super.key,
|
||||
required this.programId,
|
||||
this.step,
|
||||
this.isNew = false,
|
||||
required this.onSave,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StepForm> createState() => _StepFormState();
|
||||
}
|
||||
|
||||
class _StepFormState extends State<StepForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
late TextEditingController _nameController;
|
||||
late TextEditingController _mixTimeController;
|
||||
late TextEditingController _magnetTimeController;
|
||||
late TextEditingController _volumeController;
|
||||
late TextEditingController _blowTimeController;
|
||||
|
||||
String _position = 'A1';
|
||||
String _mixSpeed = '中速';
|
||||
String _blowSpeed = '中速';
|
||||
int _needleSpeed = 5;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nameController = TextEditingController(text: widget.step?.name ?? '');
|
||||
_mixTimeController = TextEditingController(text: '${widget.step?.mixTime ?? 0}');
|
||||
_magnetTimeController = TextEditingController(text: '${widget.step?.magnetTime ?? 0}');
|
||||
_volumeController = TextEditingController(text: '${widget.step?.volume ?? 0}');
|
||||
_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;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_mixTimeController.dispose();
|
||||
_magnetTimeController.dispose();
|
||||
_volumeController.dispose();
|
||||
_blowTimeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 标题
|
||||
Text(
|
||||
widget.isNew ? '添加步骤' : '编辑步骤',
|
||||
style: TextStyle(
|
||||
color: AppTheme.textPrimary,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 步骤名称
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n?.stepName ?? '步骤名称',
|
||||
hintText: '例如: 混合、吸磁、吹气',
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入步骤名称';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 孔位选择
|
||||
Row(
|
||||
children: [
|
||||
Text(l10n?.position ?? '孔位', style: TextStyle(color: AppTheme.textPrimary)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: _position,
|
||||
decoration: InputDecoration(
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
items: Constants.positions.map((p) => DropdownMenuItem(value: p, child: Text(p))).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) setState(() => _position = value);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 时间参数行
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _mixTimeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '${l10n?.mixTime ?? '混合时间'} (${Constants.timeUnitSeconds})',
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _magnetTimeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '${l10n?.magnetTime ?? '吸磁时间'} (${Constants.timeUnitSeconds})',
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 容积和吹气时间
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _volumeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '${l10n?.volume ?? '容积'} (${Constants.volumeUnit})',
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _blowTimeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '${l10n?.blowTime ?? '吹气时间'} (${Constants.timeUnitMinutes})',
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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)),
|
||||
Slider(
|
||||
value: _needleSpeed.toDouble(),
|
||||
min: 1,
|
||||
max: 10,
|
||||
divisions: 9,
|
||||
activeColor: AppTheme.primaryColor,
|
||||
onChanged: (value) {
|
||||
setState(() => _needleSpeed = value.round());
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 保存按钮
|
||||
CommonButton(
|
||||
text: l10n?.save ?? '保存',
|
||||
icon: Icons.save,
|
||||
type: ButtonType.primary,
|
||||
onPressed: _saveStep,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 保存步骤
|
||||
void _saveStep() {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
final step = models.Step(
|
||||
id: widget.step?.id,
|
||||
programId: widget.programId,
|
||||
stepNo: widget.step?.stepNo ?? 1,
|
||||
position: _position,
|
||||
name: _nameController.text.trim(),
|
||||
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,
|
||||
);
|
||||
|
||||
widget.onSave(step);
|
||||
}
|
||||
}
|
||||
272
lib/features/program_detail/widgets/step_list.dart
Normal file
272
lib/features/program_detail/widgets/step_list.dart
Normal file
@@ -0,0 +1,272 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/localization/app_localizations.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../shared/widgets/common_button.dart';
|
||||
import '../../programs/models/step.dart' as models;
|
||||
|
||||
/// 步骤列表组件
|
||||
class StepList extends StatefulWidget {
|
||||
final int programId;
|
||||
final List<models.Step> steps;
|
||||
final int? selectedStepId;
|
||||
final void Function(int?) onStepSelected;
|
||||
final void Function() onAddStep;
|
||||
final void Function(int oldIndex, int newIndex)? onReorder;
|
||||
final void Function(List<int> stepIds)? onDeleteSteps;
|
||||
|
||||
const StepList({
|
||||
super.key,
|
||||
required this.programId,
|
||||
required this.steps,
|
||||
this.selectedStepId,
|
||||
required this.onStepSelected,
|
||||
required this.onAddStep,
|
||||
this.onReorder,
|
||||
this.onDeleteSteps,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StepList> createState() => _StepListState();
|
||||
}
|
||||
|
||||
class _StepListState extends State<StepList> {
|
||||
final Set<int> _selectedIds = {};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final allSelected = _selectedIds.length == widget.steps.length && widget.steps.isNotEmpty;
|
||||
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
children: [
|
||||
// 标题
|
||||
Container(
|
||||
height: 50,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.list, color: AppTheme.primaryColor, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'步骤列表',
|
||||
style: TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${widget.steps.length} 步',
|
||||
style: TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 表头
|
||||
Container(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: AppTheme.idleColor.withValues(alpha: 0.2)),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 40,
|
||||
child: Checkbox(
|
||||
value: allSelected,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value == true) {
|
||||
_selectedIds.clear();
|
||||
_selectedIds.addAll(widget.steps.map((s) => s.id!));
|
||||
} else {
|
||||
_selectedIds.clear();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(width: 40, child: Text('#', style: TextStyle(fontSize: 12))),
|
||||
Expanded(child: Text(l10n?.stepName ?? '名称', style: TextStyle(fontSize: 12))),
|
||||
SizedBox(width: 60, child: Text(l10n?.position ?? '孔位', style: TextStyle(fontSize: 12))),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 步骤列表(可拖拽排序)
|
||||
Expanded(
|
||||
child: widget.steps.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.add_circle_outline, size: 48, color: AppTheme.idleColor),
|
||||
const SizedBox(height: 12),
|
||||
Text('暂无步骤', style: TextStyle(color: AppTheme.textSecondary)),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ReorderableListView.builder(
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemCount: widget.steps.length,
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (widget.onReorder != null) {
|
||||
// 调整 newIndex(ReorderableListView 的特殊行为)
|
||||
if (newIndex > oldIndex) newIndex -= 1;
|
||||
widget.onReorder!(oldIndex, newIndex);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final step = widget.steps[index];
|
||||
final isSelected = widget.selectedStepId == step.id || _selectedIds.contains(step.id);
|
||||
return _buildStepItem(step, isSelected, index);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// 底部操作栏
|
||||
Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: AppTheme.idleColor.withValues(alpha: 0.2)),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 添加按钮
|
||||
CommonButton(
|
||||
text: '添加',
|
||||
icon: Icons.add,
|
||||
type: ButtonType.primary,
|
||||
onPressed: widget.onAddStep,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 删除按钮
|
||||
if (_selectedIds.isNotEmpty)
|
||||
CommonButton(
|
||||
text: '删除',
|
||||
icon: Icons.delete,
|
||||
type: ButtonType.danger,
|
||||
onPressed: () => _showDeleteConfirmDialog(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 步骤项
|
||||
Widget _buildStepItem(models.Step step, bool isSelected, int index) {
|
||||
return Container(
|
||||
key: ValueKey(step.id),
|
||||
margin: const EdgeInsets.symmetric(vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppTheme.primaryLight.withValues(alpha: 0.3) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: isSelected ? Border.all(color: AppTheme.primaryColor, width: 2) : null,
|
||||
),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: Checkbox(
|
||||
value: _selectedIds.contains(step.id),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value == true) {
|
||||
_selectedIds.add(step.id!);
|
||||
} else {
|
||||
_selectedIds.remove(step.id!);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 30,
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'${step.stepNo}',
|
||||
style: TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(step.name)),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
step.position,
|
||||
style: TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Icon(Icons.drag_handle, color: AppTheme.idleColor),
|
||||
onTap: () => widget.onStepSelected(step.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示删除确认对话框
|
||||
void _showDeleteConfirmDialog(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(l10n?.confirm ?? '确认'),
|
||||
content: Text(
|
||||
_selectedIds.length == 1
|
||||
? '确定要删除此步骤吗?'
|
||||
: '确定要删除选中的 ${_selectedIds.length} 个步骤吗?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: Text(l10n?.cancel ?? '取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
if (widget.onDeleteSteps != null) {
|
||||
widget.onDeleteSteps!(_selectedIds.toList());
|
||||
}
|
||||
setState(() {
|
||||
_selectedIds.clear();
|
||||
});
|
||||
},
|
||||
child: Text(l10n?.confirm ?? '确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
52
lib/features/programs/models/program.dart
Normal file
52
lib/features/programs/models/program.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
/// 程序模型
|
||||
class Program {
|
||||
final int? id;
|
||||
final String code;
|
||||
final String name;
|
||||
final String createdAt;
|
||||
final int status; // 1: 启用, 0: 停用
|
||||
|
||||
Program({
|
||||
this.id,
|
||||
required this.code,
|
||||
required this.name,
|
||||
required this.createdAt,
|
||||
this.status = 1,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'code': code,
|
||||
'name': name,
|
||||
'created_at': createdAt,
|
||||
'status': status,
|
||||
};
|
||||
}
|
||||
|
||||
factory Program.fromMap(Map<String, dynamic> map) {
|
||||
return Program(
|
||||
id: map['id'] as int?,
|
||||
code: map['code'] as String,
|
||||
name: map['name'] as String,
|
||||
createdAt: map['created_at'] as String,
|
||||
status: map['status'] as int? ?? 1,
|
||||
);
|
||||
}
|
||||
|
||||
Program copyWith({
|
||||
int? id,
|
||||
String? code,
|
||||
String? name,
|
||||
String? createdAt,
|
||||
int? status,
|
||||
}) {
|
||||
return Program(
|
||||
id: id ?? this.id,
|
||||
code: code ?? this.code,
|
||||
name: name ?? this.name,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
status: status ?? this.status,
|
||||
);
|
||||
}
|
||||
}
|
||||
94
lib/features/programs/models/step.dart
Normal file
94
lib/features/programs/models/step.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
/// 步骤模型
|
||||
class Step {
|
||||
final int? id;
|
||||
final int programId;
|
||||
final int stepNo;
|
||||
final String position;
|
||||
final String name;
|
||||
final int mixTime;
|
||||
final int magnetTime;
|
||||
final int volume;
|
||||
final String mixSpeed;
|
||||
final String blowSpeed;
|
||||
final int blowTime;
|
||||
final int needleSpeed;
|
||||
|
||||
Step({
|
||||
this.id,
|
||||
required this.programId,
|
||||
required this.stepNo,
|
||||
required this.position,
|
||||
required this.name,
|
||||
this.mixTime = 0,
|
||||
this.magnetTime = 0,
|
||||
this.volume = 0,
|
||||
this.mixSpeed = '中速',
|
||||
this.blowSpeed = '中速',
|
||||
this.blowTime = 0,
|
||||
this.needleSpeed = 5,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'program_id': programId,
|
||||
'step_no': stepNo,
|
||||
'position': position,
|
||||
'name': name,
|
||||
'mix_time': mixTime,
|
||||
'magnet_time': magnetTime,
|
||||
'volume': volume,
|
||||
'mix_speed': mixSpeed,
|
||||
'blow_speed': blowSpeed,
|
||||
'blow_time': blowTime,
|
||||
'needle_speed': needleSpeed,
|
||||
};
|
||||
}
|
||||
|
||||
factory Step.fromMap(Map<String, dynamic> map) {
|
||||
return Step(
|
||||
id: map['id'] as int?,
|
||||
programId: map['program_id'] as int,
|
||||
stepNo: map['step_no'] as int,
|
||||
position: map['position'] as String,
|
||||
name: map['name'] as String,
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
Step copyWith({
|
||||
int? id,
|
||||
int? programId,
|
||||
int? stepNo,
|
||||
String? position,
|
||||
String? name,
|
||||
int? mixTime,
|
||||
int? magnetTime,
|
||||
int? volume,
|
||||
String? mixSpeed,
|
||||
String? blowSpeed,
|
||||
int? blowTime,
|
||||
int? needleSpeed,
|
||||
}) {
|
||||
return Step(
|
||||
id: id ?? this.id,
|
||||
programId: programId ?? this.programId,
|
||||
stepNo: stepNo ?? this.stepNo,
|
||||
position: position ?? this.position,
|
||||
name: name ?? this.name,
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
509
lib/features/programs/pages/programs_page.dart
Normal file
509
lib/features/programs/pages/programs_page.dart
Normal file
@@ -0,0 +1,509 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'dart:io';
|
||||
import '../../../core/localization/app_localizations.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../shared/widgets/common_button.dart';
|
||||
import '../models/program.dart';
|
||||
import '../providers/programs_provider.dart';
|
||||
import '../widgets/program_form_dialog.dart';
|
||||
import '../services/program_import_service.dart';
|
||||
|
||||
/// 程序管理页面
|
||||
class ProgramsPage extends ConsumerStatefulWidget {
|
||||
const ProgramsPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ProgramsPage> createState() => _ProgramsPageState();
|
||||
}
|
||||
|
||||
class _ProgramsPageState extends ConsumerState<ProgramsPage> {
|
||||
final Set<int> _selectedIds = {};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final programsState = ref.watch(programsProvider);
|
||||
|
||||
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('/'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
l10n?.programs ?? '程序管理',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// 新增按钮
|
||||
CommonButton(
|
||||
text: l10n?.addProgram ?? '新增',
|
||||
icon: Icons.add,
|
||||
type: ButtonType.primary,
|
||||
onPressed: () => _showAddDialog(context, ref),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 导入按钮
|
||||
CommonButton(
|
||||
text: l10n?.importProgram ?? '导入',
|
||||
icon: Icons.file_upload,
|
||||
type: ButtonType.secondary,
|
||||
onPressed: () => _importPrograms(context, ref),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 程序列表表格
|
||||
Expanded(
|
||||
child: programsState.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: programsState.programs.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.folder_open,
|
||||
size: 64,
|
||||
color: AppTheme.idleColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n?.noData ?? '暂无数据',
|
||||
style: TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(2, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 表头
|
||||
_buildTableHeader(l10n, programsState.programs),
|
||||
// 表格内容
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: programsState.programs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final program = programsState.programs[index];
|
||||
final isSelected = _selectedIds.contains(program.id);
|
||||
return _buildTableRow(
|
||||
context,
|
||||
ref,
|
||||
l10n,
|
||||
program,
|
||||
isSelected,
|
||||
index == programsState.programs.length - 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 底部操作栏
|
||||
if (programsState.programs.isNotEmpty)
|
||||
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: [
|
||||
Text(
|
||||
'${l10n?.selected ?? '已选择'}: ${_selectedIds.length}',
|
||||
style: TextStyle(color: AppTheme.textSecondary),
|
||||
),
|
||||
const Spacer(),
|
||||
if (_selectedIds.isNotEmpty)
|
||||
CommonButton(
|
||||
text: l10n?.deleteProgram ?? '删除',
|
||||
icon: Icons.delete,
|
||||
type: ButtonType.danger,
|
||||
onPressed: () => _showDeleteConfirmDialog(
|
||||
context,
|
||||
ref,
|
||||
l10n,
|
||||
_selectedIds.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 表头
|
||||
Widget _buildTableHeader(AppLocalizations? l10n, List<Program> programs) {
|
||||
final allSelected = _selectedIds.length == programs.length && programs.isNotEmpty;
|
||||
|
||||
return Container(
|
||||
height: 50,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 复选框
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: Checkbox(
|
||||
value: allSelected,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value == true) {
|
||||
_selectedIds.clear();
|
||||
_selectedIds.addAll(programs.map((p) => p.id!));
|
||||
} else {
|
||||
_selectedIds.clear();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
// 编号
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
l10n?.programCode ?? '编号',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 名称
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
l10n?.programName ?? '名称',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 创建时间
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n?.createTime ?? '创建时间',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 状态
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Text(
|
||||
'状态',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 操作
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: Text(
|
||||
l10n?.detail ?? '操作',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 表格行
|
||||
Widget _buildTableRow(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
AppLocalizations? l10n,
|
||||
Program program,
|
||||
bool isSelected,
|
||||
bool isLast,
|
||||
) {
|
||||
return Container(
|
||||
height: 50,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppTheme.primaryLight.withValues(alpha: 0.2) : null,
|
||||
border: isLast
|
||||
? null
|
||||
: Border(
|
||||
bottom: BorderSide(
|
||||
color: AppTheme.idleColor.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 复选框
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value == true) {
|
||||
_selectedIds.add(program.id!);
|
||||
} else {
|
||||
_selectedIds.remove(program.id!);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
// 编号
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
program.code,
|
||||
style: TextStyle(color: AppTheme.textPrimary),
|
||||
),
|
||||
),
|
||||
// 名称
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
program.name,
|
||||
style: TextStyle(color: AppTheme.textPrimary),
|
||||
),
|
||||
),
|
||||
// 创建时间
|
||||
Expanded(
|
||||
child: Text(
|
||||
program.createdAt,
|
||||
style: TextStyle(color: AppTheme.textSecondary),
|
||||
),
|
||||
),
|
||||
// 状态
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: program.status == 1
|
||||
? AppTheme.successColor.withValues(alpha: 0.1)
|
||||
: AppTheme.idleColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
program.status == 1 ? '启用' : '停用',
|
||||
style: TextStyle(
|
||||
color: program.status == 1
|
||||
? AppTheme.successColor
|
||||
: AppTheme.idleColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 操作按钮
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, size: 20),
|
||||
color: AppTheme.primaryColor,
|
||||
onPressed: () => _showEditDialog(context, ref, program),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, size: 20),
|
||||
color: AppTheme.errorColor,
|
||||
onPressed: () => _showDeleteConfirmDialog(
|
||||
context,
|
||||
ref,
|
||||
l10n,
|
||||
[program.id!],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.visibility, size: 20),
|
||||
color: AppTheme.textSecondary,
|
||||
onPressed: () => context.go('/programs/${program.id}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示新增对话框
|
||||
void _showAddDialog(BuildContext context, WidgetRef ref) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => const ProgramFormDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 导入程序
|
||||
Future<void> _importPrograms(BuildContext context, WidgetRef ref) async {
|
||||
try {
|
||||
// 选择文件
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
allowMultiple: false,
|
||||
);
|
||||
|
||||
if (result == null || result.files.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final file = result.files.first;
|
||||
if (file.path == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('无法读取文件'),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
final jsonContent = await File(file.path!).readAsString();
|
||||
|
||||
// 导入程序
|
||||
final importedCount = await ProgramImportService.instance.importFromJson(jsonContent);
|
||||
|
||||
// 刷新程序列表
|
||||
ref.read(programsProvider.notifier).loadPrograms();
|
||||
|
||||
// 显示结果
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('成功导入 $importedCount 个程序'),
|
||||
backgroundColor: importedCount > 0 ? AppTheme.successColor : AppTheme.warningColor,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('导入失败: ${e.toString()}'),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示编辑对话框
|
||||
void _showEditDialog(BuildContext context, WidgetRef ref, Program program) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ProgramFormDialog(program: program),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示删除确认对话框
|
||||
void _showDeleteConfirmDialog(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
AppLocalizations? l10n,
|
||||
List<int> ids,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(l10n?.confirm ?? '确认'),
|
||||
content: Text(
|
||||
ids.length == 1
|
||||
? '确定要删除此程序吗?'
|
||||
: '确定要删除选中的 ${ids.length} 个程序吗?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(l10n?.cancel ?? '取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () async {
|
||||
final notifier = ref.read(programsProvider.notifier);
|
||||
await notifier.deletePrograms(ids);
|
||||
setState(() {
|
||||
_selectedIds.removeAll(ids);
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(l10n?.confirm ?? '确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
192
lib/features/programs/providers/programs_provider.dart
Normal file
192
lib/features/programs/providers/programs_provider.dart
Normal file
@@ -0,0 +1,192 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/database/database_service.dart';
|
||||
import '../models/program.dart';
|
||||
|
||||
/// 程序列表状态
|
||||
class ProgramsState {
|
||||
final List<Program> programs;
|
||||
final int? selectedProgramId;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
const ProgramsState({
|
||||
this.programs = const [],
|
||||
this.selectedProgramId,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
ProgramsState copyWith({
|
||||
List<Program>? programs,
|
||||
int? selectedProgramId,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
bool clearSelection = false,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return ProgramsState(
|
||||
programs: programs ?? this.programs,
|
||||
selectedProgramId: clearSelection ? null : (selectedProgramId ?? this.selectedProgramId),
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取选中的程序
|
||||
Program? get selectedProgram {
|
||||
if (selectedProgramId == null) return null;
|
||||
return programs.where((p) => p.id == selectedProgramId).firstOrNull;
|
||||
}
|
||||
}
|
||||
|
||||
/// 程序列表 Notifier
|
||||
class ProgramsNotifier extends StateNotifier<ProgramsState> {
|
||||
final DatabaseService _db;
|
||||
|
||||
ProgramsNotifier(this._db) : super(const ProgramsState()) {
|
||||
loadPrograms();
|
||||
}
|
||||
|
||||
/// 加载所有程序
|
||||
Future<void> loadPrograms() async {
|
||||
state = state.copyWith(isLoading: true, clearError: true);
|
||||
|
||||
try {
|
||||
final db = await _db.database;
|
||||
final maps = await db.query('programs', orderBy: 'created_at DESC');
|
||||
final programs = maps.map((m) => Program.fromMap(m)).toList();
|
||||
|
||||
state = state.copyWith(programs: programs, isLoading: false);
|
||||
} catch (e) {
|
||||
state = state.copyWith(isLoading: false, error: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// 选择程序
|
||||
void selectProgram(int? programId) {
|
||||
state = state.copyWith(selectedProgramId: programId);
|
||||
}
|
||||
|
||||
/// 清除选择
|
||||
void clearSelection() {
|
||||
state = state.copyWith(clearSelection: true);
|
||||
}
|
||||
|
||||
/// 新增程序
|
||||
Future<bool> addProgram(Program program) async {
|
||||
try {
|
||||
final db = await _db.database;
|
||||
await db.insert('programs', program.toMap());
|
||||
await loadPrograms();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新程序
|
||||
Future<bool> updateProgram(Program program) async {
|
||||
if (program.id == null) return false;
|
||||
|
||||
try {
|
||||
final db = await _db.database;
|
||||
await db.update(
|
||||
'programs',
|
||||
program.toMap(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [program.id],
|
||||
);
|
||||
await loadPrograms();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除程序
|
||||
Future<bool> deleteProgram(int programId) async {
|
||||
try {
|
||||
final db = await _db.database;
|
||||
await db.delete('programs', where: 'id = ?', whereArgs: [programId]);
|
||||
|
||||
// 如果删除的是选中的程序,清除选择
|
||||
if (state.selectedProgramId == programId) {
|
||||
state = state.copyWith(clearSelection: true);
|
||||
}
|
||||
|
||||
await loadPrograms();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 批量删除程序
|
||||
Future<bool> deletePrograms(List<int> programIds) async {
|
||||
try {
|
||||
final db = await _db.database;
|
||||
await db.delete(
|
||||
'programs',
|
||||
where: 'id IN (${programIds.map((_) => '?').join(',')})',
|
||||
whereArgs: programIds,
|
||||
);
|
||||
|
||||
// 如果删除的是选中的程序,清除选择
|
||||
if (programIds.contains(state.selectedProgramId)) {
|
||||
state = state.copyWith(clearSelection: true);
|
||||
}
|
||||
|
||||
await loadPrograms();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换程序状态
|
||||
Future<bool> toggleStatus(int programId) async {
|
||||
try {
|
||||
final db = await _db.database;
|
||||
final program = state.programs.where((p) => p.id == programId).firstOrNull;
|
||||
if (program == null) return false;
|
||||
|
||||
await db.update(
|
||||
'programs',
|
||||
{'status': program.status == 1 ? 0 : 1},
|
||||
where: 'id = ?',
|
||||
whereArgs: [programId],
|
||||
);
|
||||
await loadPrograms();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 数据库服务 Provider
|
||||
final databaseServiceProvider = Provider<DatabaseService>((ref) {
|
||||
return DatabaseService.instance;
|
||||
});
|
||||
|
||||
/// 程序列表 Provider
|
||||
final programsProvider =
|
||||
StateNotifierProvider<ProgramsNotifier, ProgramsState>((ref) {
|
||||
final db = ref.watch(databaseServiceProvider);
|
||||
return ProgramsNotifier(db);
|
||||
});
|
||||
|
||||
/// 选中的程序 Provider
|
||||
final selectedProgramProvider = Provider<Program?>((ref) {
|
||||
return ref.watch(programsProvider).selectedProgram;
|
||||
});
|
||||
|
||||
/// 启用的程序列表 Provider
|
||||
final enabledProgramsProvider = Provider<List<Program>>((ref) {
|
||||
return ref.watch(programsProvider).programs.where((p) => p.status == 1).toList();
|
||||
});
|
||||
126
lib/features/programs/services/program_import_service.dart
Normal file
126
lib/features/programs/services/program_import_service.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'dart:convert';
|
||||
import '../../programs/models/program.dart';
|
||||
import '../../programs/models/step.dart';
|
||||
import '../../programs/services/program_service.dart';
|
||||
|
||||
/// 程序导入服务
|
||||
class ProgramImportService {
|
||||
static final ProgramImportService instance = ProgramImportService._internal();
|
||||
final ProgramService _programService = ProgramService.instance;
|
||||
|
||||
ProgramImportService._internal();
|
||||
|
||||
/// 从 JSON 字符串导入程序
|
||||
/// 返回导入的程序数量
|
||||
Future<int> importFromJson(String jsonContent) async {
|
||||
final data = jsonDecode(jsonContent);
|
||||
|
||||
// 支持单个程序或程序数组
|
||||
final List<dynamic> programsData;
|
||||
if (data is List) {
|
||||
programsData = data;
|
||||
} else if (data is Map && data.containsKey('programs')) {
|
||||
programsData = data['programs'] as List;
|
||||
} else {
|
||||
programsData = [data];
|
||||
}
|
||||
|
||||
int importedCount = 0;
|
||||
|
||||
for (final programData in programsData) {
|
||||
try {
|
||||
// 验证必填字段
|
||||
if (!_validateProgramData(programData)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查编号是否已存在
|
||||
final existingPrograms = await _programService.getAllPrograms();
|
||||
final code = programData['code'] as String;
|
||||
if (existingPrograms.any((p) => p.code == code)) {
|
||||
// 编号已存在,跳过或使用新编号
|
||||
continue;
|
||||
}
|
||||
|
||||
// 创建程序
|
||||
final program = Program(
|
||||
code: code,
|
||||
name: programData['name'] as String,
|
||||
createdAt: programData['createdAt'] ?? DateTime.now().toString().split('.')[0],
|
||||
status: programData['status'] ?? 1,
|
||||
);
|
||||
|
||||
final programId = await _programService.addProgram(program);
|
||||
|
||||
// 导入步骤
|
||||
final stepsData = programData['steps'] as List?;
|
||||
if (stepsData != null) {
|
||||
for (int i = 0; i < stepsData.length; i++) {
|
||||
final stepData = stepsData[i];
|
||||
final step = Step(
|
||||
programId: programId,
|
||||
stepNo: i + 1,
|
||||
position: stepData['position'] as String? ?? 'A1',
|
||||
name: stepData['name'] as String? ?? '步骤${i + 1}',
|
||||
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,
|
||||
);
|
||||
await _programService.addStep(step);
|
||||
}
|
||||
}
|
||||
|
||||
importedCount++;
|
||||
} catch (e) {
|
||||
// 忽略单个程序导入错误
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return importedCount;
|
||||
}
|
||||
|
||||
/// 验证程序数据
|
||||
bool _validateProgramData(Map<String, dynamic> data) {
|
||||
return data.containsKey('code') &&
|
||||
data.containsKey('name') &&
|
||||
data['code'] is String &&
|
||||
data['name'] is String;
|
||||
}
|
||||
|
||||
/// 导出程序为 JSON
|
||||
Future<String> exportToJson(List<int> programIds) async {
|
||||
final programs = [];
|
||||
|
||||
for (final id in programIds) {
|
||||
final program = await _programService.getProgramById(id);
|
||||
if (program == null) continue;
|
||||
|
||||
final steps = await _programService.getStepsByProgramId(id);
|
||||
|
||||
programs.add({
|
||||
'code': program.code,
|
||||
'name': program.name,
|
||||
'createdAt': program.createdAt,
|
||||
'status': program.status,
|
||||
'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,
|
||||
}).toList(),
|
||||
});
|
||||
}
|
||||
|
||||
return jsonEncode({'programs': programs});
|
||||
}
|
||||
}
|
||||
156
lib/features/programs/services/program_service.dart
Normal file
156
lib/features/programs/services/program_service.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import '../../../core/database/database_service.dart';
|
||||
import '../models/program.dart';
|
||||
import '../models/step.dart';
|
||||
|
||||
/// 程序服务
|
||||
/// 封装程序和步骤的数据库操作
|
||||
class ProgramService {
|
||||
static final ProgramService instance = ProgramService._internal();
|
||||
final DatabaseService _db = DatabaseService.instance;
|
||||
|
||||
ProgramService._internal();
|
||||
|
||||
/// 获取所有程序
|
||||
Future<List<Program>> getAllPrograms() async {
|
||||
final database = await _db.database;
|
||||
final maps = await database.query('programs', orderBy: 'created_at DESC');
|
||||
return maps.map((m) => Program.fromMap(m)).toList();
|
||||
}
|
||||
|
||||
/// 根据ID获取程序
|
||||
Future<Program?> getProgramById(int id) async {
|
||||
final database = await _db.database;
|
||||
final maps = await database.query(
|
||||
'programs',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
if (maps.isEmpty) return null;
|
||||
return Program.fromMap(maps.first);
|
||||
}
|
||||
|
||||
/// 新增程序
|
||||
Future<int> addProgram(Program program) async {
|
||||
final database = await _db.database;
|
||||
return await database.insert('programs', program.toMap());
|
||||
}
|
||||
|
||||
/// 更新程序
|
||||
Future<bool> updateProgram(Program program) async {
|
||||
if (program.id == null) return false;
|
||||
final database = await _db.database;
|
||||
final count = await database.update(
|
||||
'programs',
|
||||
program.toMap(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [program.id],
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 删除程序(含步骤)
|
||||
Future<bool> deleteProgram(int id) async {
|
||||
final database = await _db.database;
|
||||
// 先删除关联的步骤
|
||||
await database.delete('steps', where: 'program_id = ?', whereArgs: [id]);
|
||||
// 再删除程序
|
||||
final count = await database.delete('programs', where: 'id = ?', whereArgs: [id]);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 批量删除程序
|
||||
Future<bool> deletePrograms(List<int> ids) async {
|
||||
if (ids.isEmpty) return true;
|
||||
final database = await _db.database;
|
||||
// 先删除关联的步骤
|
||||
await database.delete(
|
||||
'steps',
|
||||
where: 'program_id IN (${ids.map((_) => '?').join(',')})',
|
||||
whereArgs: ids,
|
||||
);
|
||||
// 再删除程序
|
||||
final count = await database.delete(
|
||||
'programs',
|
||||
where: 'id IN (${ids.map((_) => '?').join(',')})',
|
||||
whereArgs: ids,
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 切换程序状态
|
||||
Future<bool> toggleProgramStatus(int id) async {
|
||||
final database = await _db.database;
|
||||
final program = await getProgramById(id);
|
||||
if (program == null) return false;
|
||||
final count = await database.update(
|
||||
'programs',
|
||||
{'status': program.status == 1 ? 0 : 1},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 获取程序的步骤列表
|
||||
Future<List<Step>> getStepsByProgramId(int programId) async {
|
||||
final database = await _db.database;
|
||||
final maps = await database.query(
|
||||
'steps',
|
||||
where: 'program_id = ?',
|
||||
whereArgs: [programId],
|
||||
orderBy: 'step_no ASC',
|
||||
);
|
||||
return maps.map((m) => Step.fromMap(m)).toList();
|
||||
}
|
||||
|
||||
/// 新增步骤
|
||||
Future<int> addStep(Step step) async {
|
||||
final database = await _db.database;
|
||||
return await database.insert('steps', step.toMap());
|
||||
}
|
||||
|
||||
/// 更新步骤
|
||||
Future<bool> updateStep(Step step) async {
|
||||
if (step.id == null) return false;
|
||||
final database = await _db.database;
|
||||
final count = await database.update(
|
||||
'steps',
|
||||
step.toMap(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [step.id],
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 删除步骤
|
||||
Future<bool> deleteStep(int id) async {
|
||||
final database = await _db.database;
|
||||
final count = await database.delete('steps', where: 'id = ?', whereArgs: [id]);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 批量删除步骤
|
||||
Future<bool> deleteSteps(List<int> ids) async {
|
||||
if (ids.isEmpty) return true;
|
||||
final database = await _db.database;
|
||||
final count = await database.delete(
|
||||
'steps',
|
||||
where: 'id IN (${ids.map((_) => '?').join(',')})',
|
||||
whereArgs: ids,
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 更新步骤排序
|
||||
Future<void> reorderSteps(int programId, List<int> stepIds) async {
|
||||
final database = await _db.database;
|
||||
for (int i = 0; i < stepIds.length; i++) {
|
||||
await database.update(
|
||||
'steps',
|
||||
{'step_no': i + 1},
|
||||
where: 'id = ? AND program_id = ?',
|
||||
whereArgs: [stepIds[i], programId],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
188
lib/features/programs/widgets/program_form_dialog.dart
Normal file
188
lib/features/programs/widgets/program_form_dialog.dart
Normal file
@@ -0,0 +1,188 @@
|
||||
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/widgets/common_button.dart';
|
||||
import '../models/program.dart';
|
||||
import '../providers/programs_provider.dart';
|
||||
|
||||
/// 程序表单弹窗
|
||||
/// 用于新增和编辑程序
|
||||
class ProgramFormDialog extends ConsumerStatefulWidget {
|
||||
final Program? program;
|
||||
|
||||
const ProgramFormDialog({super.key, this.program});
|
||||
|
||||
@override
|
||||
ConsumerState<ProgramFormDialog> createState() => _ProgramFormDialogState();
|
||||
}
|
||||
|
||||
class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late TextEditingController _codeController;
|
||||
late TextEditingController _nameController;
|
||||
bool _isEnabled = true;
|
||||
bool _isSaving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_codeController = TextEditingController(text: widget.program?.code ?? '');
|
||||
_nameController = TextEditingController(text: widget.program?.name ?? '');
|
||||
_isEnabled = widget.program?.status == 1;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_codeController.dispose();
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final isEditing = widget.program != null;
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
isEditing
|
||||
? (l10n?.editProgram ?? '编辑程序')
|
||||
: (l10n?.addProgram ?? '新增程序'),
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 400,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 编号输入
|
||||
TextFormField(
|
||||
controller: _codeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n?.programCode ?? '编号',
|
||||
hintText: '例如: P001',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入编号';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 名称输入
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n?.programName ?? '名称',
|
||||
hintText: '请输入程序名称',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入名称';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 状态开关
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'状态',
|
||||
style: TextStyle(color: AppTheme.textPrimary),
|
||||
),
|
||||
const Spacer(),
|
||||
Switch(
|
||||
value: _isEnabled,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isEnabled = value;
|
||||
});
|
||||
},
|
||||
activeColor: AppTheme.successColor,
|
||||
),
|
||||
Text(
|
||||
_isEnabled ? '启用' : '停用',
|
||||
style: TextStyle(
|
||||
color: _isEnabled ? AppTheme.successColor : AppTheme.idleColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isSaving ? null : () => Navigator.of(context).pop(),
|
||||
child: Text(l10n?.cancel ?? '取消'),
|
||||
),
|
||||
CommonButton(
|
||||
text: l10n?.save ?? '保存',
|
||||
icon: Icons.save,
|
||||
type: ButtonType.primary,
|
||||
isLoading: _isSaving,
|
||||
onPressed: _isSaving ? null : () => _saveProgram(context, ref, l10n),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 保存程序
|
||||
Future<void> _saveProgram(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
AppLocalizations? l10n,
|
||||
) async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() {
|
||||
_isSaving = true;
|
||||
});
|
||||
|
||||
final notifier = ref.read(programsProvider.notifier);
|
||||
final now = DateTime.now().toString().substring(0, 10);
|
||||
|
||||
final program = Program(
|
||||
id: widget.program?.id,
|
||||
code: _codeController.text.trim(),
|
||||
name: _nameController.text.trim(),
|
||||
createdAt: widget.program?.createdAt ?? now,
|
||||
status: _isEnabled ? 1 : 0,
|
||||
);
|
||||
|
||||
bool success;
|
||||
if (widget.program != null) {
|
||||
success = await notifier.updateProgram(program);
|
||||
} else {
|
||||
success = await notifier.addProgram(program);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isSaving = false;
|
||||
});
|
||||
|
||||
if (success) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('保存失败,请检查编号是否重复'),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
382
lib/features/settings/pages/settings_page.dart
Normal file
382
lib/features/settings/pages/settings_page.dart
Normal file
@@ -0,0 +1,382 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../core/localization/app_localizations.dart';
|
||||
import '../../../core/localization/locale_provider.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../shared/widgets/common_button.dart';
|
||||
import '../services/settings_service.dart';
|
||||
|
||||
/// 系统设置页面
|
||||
class SettingsPage extends ConsumerStatefulWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
class _SettingsPageState extends ConsumerState<SettingsPage> {
|
||||
String _currentVersion = 'V1.0.0';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
// locale 用于语言切换,通过 ref.watch 保持监听
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
color: AppTheme.backgroundColor,
|
||||
child: Row(
|
||||
children: [
|
||||
// 左侧导航菜单
|
||||
SizedBox(
|
||||
width: 280,
|
||||
child: Container(
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
children: [
|
||||
// 返回按钮
|
||||
Container(
|
||||
height: 50,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
color: AppTheme.textPrimary,
|
||||
onPressed: () => context.go('/'),
|
||||
),
|
||||
Text(
|
||||
'返回首页',
|
||||
style: TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 设置标题
|
||||
Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.settings, color: AppTheme.primaryColor, size: 24),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
l10n?.settings ?? '系统设置',
|
||||
style: TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 软件升级
|
||||
_buildMenuItem(
|
||||
icon: Icons.system_update,
|
||||
title: l10n?.upgrade ?? '软件升级',
|
||||
onTap: () {},
|
||||
),
|
||||
// 语言设置
|
||||
_buildMenuItem(
|
||||
icon: Icons.language,
|
||||
title: l10n?.language ?? '语言设置',
|
||||
onTap: () => _showLanguageDialog(),
|
||||
),
|
||||
// 安全设置
|
||||
_buildMenuItem(
|
||||
icon: Icons.lock,
|
||||
title: l10n?.password ?? '密码修改',
|
||||
onTap: () => _showPasswordDialog(),
|
||||
),
|
||||
// U盘导入
|
||||
_buildMenuItem(
|
||||
icon: Icons.usb,
|
||||
title: l10n?.usbImport ?? 'U盘导入',
|
||||
onTap: () => _showUsbImportDialog(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 右侧内容区域
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n?.upgrade ?? '软件升级',
|
||||
style: TextStyle(
|
||||
color: AppTheme.textPrimary,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 版本信息
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.backgroundColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: AppTheme.primaryColor),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'当前版本: $_currentVersion',
|
||||
style: TextStyle(color: AppTheme.textPrimary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 检查更新按钮
|
||||
CommonButton(
|
||||
text: '检查更新',
|
||||
icon: Icons.refresh,
|
||||
type: ButtonType.primary,
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('已是最新版本'),
|
||||
backgroundColor: AppTheme.successColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 导航菜单项
|
||||
Widget _buildMenuItem({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: AppTheme.textSecondary),
|
||||
title: Text(title, style: TextStyle(color: AppTheme.textPrimary)),
|
||||
trailing: Icon(Icons.chevron_right, color: AppTheme.idleColor),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示语言选择对话框
|
||||
void _showLanguageDialog() {
|
||||
final locale = ref.read(localeProvider);
|
||||
final currentLang = locale.languageCode;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text('语言设置'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RadioListTile<String>(
|
||||
title: Text('简体中文'),
|
||||
value: 'zh',
|
||||
groupValue: currentLang,
|
||||
onChanged: (value) {
|
||||
ref.read(localeProvider.notifier).setChinese();
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
),
|
||||
RadioListTile<String>(
|
||||
title: Text('English'),
|
||||
value: 'en',
|
||||
groupValue: currentLang,
|
||||
onChanged: (value) {
|
||||
ref.read(localeProvider.notifier).setEnglish();
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示密码修改对话框
|
||||
void _showPasswordDialog() {
|
||||
final oldPasswordController = TextEditingController();
|
||||
final newPasswordController = TextEditingController();
|
||||
final confirmPasswordController = TextEditingController();
|
||||
String? errorMessage;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: Text('密码修改'),
|
||||
content: SizedBox(
|
||||
width: 300,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: oldPasswordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '原密码',
|
||||
errorText: null,
|
||||
),
|
||||
obscureText: true,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: newPasswordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '新密码',
|
||||
helperText: '至少6位字符',
|
||||
),
|
||||
obscureText: true,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: confirmPasswordController,
|
||||
decoration: InputDecoration(labelText: '确认新密码'),
|
||||
obscureText: true,
|
||||
),
|
||||
if (errorMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Text(
|
||||
errorMessage!,
|
||||
style: TextStyle(color: AppTheme.errorColor, fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
child: Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
// 验证逻辑
|
||||
final oldPassword = oldPasswordController.text.trim();
|
||||
final newPassword = newPasswordController.text.trim();
|
||||
final confirmPassword = confirmPasswordController.text.trim();
|
||||
|
||||
// 检查空值
|
||||
if (oldPassword.isEmpty || newPassword.isEmpty || confirmPassword.isEmpty) {
|
||||
setState(() => errorMessage = '请填写所有字段');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查新密码长度
|
||||
if (newPassword.length < 6) {
|
||||
setState(() => errorMessage = '新密码至少6位字符');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查新密码一致性
|
||||
if (newPassword != confirmPassword) {
|
||||
setState(() => errorMessage = '两次输入的新密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证原密码
|
||||
final isValid = await SettingsService.instance.verifyPassword(oldPassword);
|
||||
if (!isValid) {
|
||||
setState(() => errorMessage = '原密码错误');
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存新密码
|
||||
final success = await SettingsService.instance.setPassword(newPassword);
|
||||
Navigator.of(ctx).pop();
|
||||
|
||||
if (success) {
|
||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('密码已修改'),
|
||||
backgroundColor: AppTheme.successColor,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('密码修改失败'),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text('确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示U盘导入对话框
|
||||
void _showUsbImportDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('U盘导入'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.usb, size: 48, color: AppTheme.warningColor),
|
||||
const SizedBox(height: 16),
|
||||
Text('未检测到U盘'),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'请插入U盘后重试',
|
||||
style: TextStyle(color: AppTheme.textSecondary, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('关闭'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('正在检测U盘...'),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text('重新检测'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
63
lib/features/settings/services/settings_service.dart
Normal file
63
lib/features/settings/services/settings_service.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import '../../../core/database/database_service.dart';
|
||||
|
||||
/// 设置服务
|
||||
/// 管理系统设置(密码、语言偏好等)
|
||||
class SettingsService {
|
||||
static final SettingsService instance = SettingsService._internal();
|
||||
final DatabaseService _db = DatabaseService.instance;
|
||||
|
||||
SettingsService._internal();
|
||||
|
||||
/// 获取密码
|
||||
Future<String> getPassword() async {
|
||||
final database = await _db.database;
|
||||
final results = await database.query(
|
||||
'settings',
|
||||
where: 'key = ?',
|
||||
whereArgs: ['password'],
|
||||
);
|
||||
if (results.isEmpty) return '123456'; // 默认密码
|
||||
return results.first['value'] as String;
|
||||
}
|
||||
|
||||
/// 设置密码
|
||||
Future<bool> setPassword(String newPassword) async {
|
||||
final database = await _db.database;
|
||||
final count = await database.update(
|
||||
'settings',
|
||||
{'value': newPassword},
|
||||
where: 'key = ?',
|
||||
whereArgs: ['password'],
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 验证密码
|
||||
Future<bool> verifyPassword(String password) async {
|
||||
final storedPassword = await getPassword();
|
||||
return password == storedPassword;
|
||||
}
|
||||
|
||||
/// 获取设置值
|
||||
Future<String?> getSetting(String key) async {
|
||||
final database = await _db.database;
|
||||
final results = await database.query(
|
||||
'settings',
|
||||
where: 'key = ?',
|
||||
whereArgs: [key],
|
||||
);
|
||||
if (results.isEmpty) return null;
|
||||
return results.first['value'] as String;
|
||||
}
|
||||
|
||||
/// 设置值
|
||||
Future<bool> setSetting(String key, String value) async {
|
||||
final database = await _db.database;
|
||||
// 使用 insert 或 replace
|
||||
await database.execute(
|
||||
'INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)',
|
||||
[key, value],
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
99
lib/features/settings/services/usb_detection_service.dart
Normal file
99
lib/features/settings/services/usb_detection_service.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'dart:async';
|
||||
|
||||
/// USB 检测服务
|
||||
/// 监听 U盘插入/拔出事件
|
||||
class UsbDetectionService {
|
||||
static final UsbDetectionService instance = UsbDetectionService._internal();
|
||||
|
||||
UsbDetectionService._internal();
|
||||
|
||||
/// USB 状态
|
||||
bool _isUsbConnected = false;
|
||||
String? _usbPath;
|
||||
|
||||
/// 状态流
|
||||
final StreamController<UsbState> _stateController = StreamController<UsbState>.broadcast();
|
||||
|
||||
/// 监听 USB 状态变化
|
||||
Stream<UsbState> get stateStream => _stateController.stream;
|
||||
|
||||
/// 当前 USB 是否连接
|
||||
bool get isConnected => _isUsbConnected;
|
||||
|
||||
/// USB 路径
|
||||
String? get usbPath => _usbPath;
|
||||
|
||||
/// 开始监听 USB 事件
|
||||
void startMonitoring() {
|
||||
// TODO: 实现平台特定的 USB 监听
|
||||
// Android: 使用 BroadcastReceiver 监听 ACTION_MEDIA_MOUNTED
|
||||
// Linux: 监听 /dev/disk/by-path/ 或使用 udev
|
||||
// Windows: 监听 WM_DEVICECHANGE
|
||||
|
||||
// 模拟实现:定时检测
|
||||
_startPolling();
|
||||
}
|
||||
|
||||
/// 停止监听
|
||||
void stopMonitoring() {
|
||||
// _stopPolling();
|
||||
}
|
||||
|
||||
/// 模拟轮询检测(待平台实现)
|
||||
void _startPolling() {
|
||||
// TODO: 根据平台实现真实的 USB 检测
|
||||
// 定时检测 /mnt/usb 或 /media/*/ 目录
|
||||
}
|
||||
|
||||
/// 手动检测 USB
|
||||
Future<bool> detectUsb() async {
|
||||
// TODO: 实现平台特定的 USB 检测
|
||||
// Android: 检查 getExternalFilesDir 或 mount points
|
||||
// Linux: 检查 /mnt, /media 目录
|
||||
// Windows: 检查 D:, E: 等驱动器
|
||||
|
||||
// 返回检测结果
|
||||
return _isUsbConnected;
|
||||
}
|
||||
|
||||
/// 获取 USB 上的程序文件列表
|
||||
Future<List<String>> listProgramFiles() async {
|
||||
if (!_isUsbConnected || _usbPath == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// TODO: 扫描 USB 目录中的 .json 程序文件
|
||||
// 示例路径: $_usbPath/programs/*.json
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/// 模拟 USB 连接(用于测试)
|
||||
void simulateConnection(String path) {
|
||||
_isUsbConnected = true;
|
||||
_usbPath = path;
|
||||
_stateController.add(UsbState.connected(path));
|
||||
}
|
||||
|
||||
/// 模拟 USB 断开(用于测试)
|
||||
void simulateDisconnection() {
|
||||
_isUsbConnected = false;
|
||||
_usbPath = null;
|
||||
_stateController.add(UsbState.disconnected());
|
||||
}
|
||||
|
||||
/// 释放资源
|
||||
void dispose() {
|
||||
stopMonitoring();
|
||||
_stateController.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// USB 状态
|
||||
class UsbState {
|
||||
final bool isConnected;
|
||||
final String? path;
|
||||
|
||||
const UsbState.connected(String path) : isConnected = true, path = path;
|
||||
const UsbState.disconnected() : isConnected = false, path = null;
|
||||
}
|
||||
Reference in New Issue
Block a user