chore(project): 初始化项目基础配置文件

- 添加 CodeGraph、Android 和通用 gitignore 配置
- 创建项目元数据文件跟踪 Flutter 项目属性
- 添加 Codex AI 指导文档 AGENTS.md 说明项目架构
- 配置代码分析选项 analysis_options.yaml
- 设置 Android 应用清单权限和 Kiosk 模式配置
- 实现中英文国际化支持 AppLocalizations
- 配置 GoRouter 应用路由导航
- 创建明亮工业控制风格的主题配置 AppTheme
This commit is contained in:
Developer
2026-06-04 11:19:44 +08:00
commit 5d28bf631b
85 changed files with 21423 additions and 0 deletions

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

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

View 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;
}
}
}

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

View 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 ?? '确认'),
),
],
),
);
}
}

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