import 'package:ducafe_ui_core/ducafe_ui_core.dart'; import 'package:flutter/material.dart'; import 'package:forui/forui.dart'; import 'package:get/get.dart'; import 'package:pj_supermarket_cloud/common/index.dart'; import 'controller.dart'; import 'widgets/startup_step_card.dart'; class LoadPage extends GetView { const LoadPage({super.key}); @override Widget build(BuildContext context) { final theme = FTheme.of(context); return ColoredBox( color: XfjDesignTokens.canvas, child: SafeArea( child: Padding( padding: EdgeInsets.fromLTRB(28.w, 24.h, 28.w, 16.h), child: Column( children: [ _buildHeader(theme), SizedBox(height: 16.h), Center( child: TerminalSurface( width: 1260.w, height: 820.h, clipBehavior: Clip.hardEdge, child: Row( children: [ _StartupEnvironmentPanel(controller: controller), _StartupProgressPanel(controller: controller).expanded(), ], ), ), ).expanded(), SizedBox(height: 10.h), _StartupFooter(controller: controller), ], ), ), ), ); } Widget _buildHeader(FThemeData theme) { return SizedBox( height: 58.h, child: Row( children: [ Padding( padding: EdgeInsets.fromLTRB(4.w, 4.h, 8.w, 4.h), child: Text( 'ZAX', style: TextStyle( color: const Color(0xFF0865E8), fontSize: 38.w, height: 1.1, fontWeight: FontWeight.w900, letterSpacing: 0.4, ), ), ), SizedBox(width: 20.w), Container(width: 1, height: 34.h, color: XfjDesignTokens.line), SizedBox(width: 20.w), Text( '智慧消费终端', style: theme.typography.xl.copyWith( color: XfjDesignTokens.ink, fontWeight: FontWeight.w700, ), ), const Spacer(), Obx( () => _HeaderStatus( label: controller.appControl.startupStatusLabel.value, status: _overallStatus(controller), ), ), ], ), ); } } class _StartupEnvironmentPanel extends StatelessWidget { const _StartupEnvironmentPanel({required this.controller}); final LoadController controller; @override Widget build(BuildContext context) { final theme = FTheme.of(context); return Container( width: 460.w, padding: EdgeInsets.all(34.w), decoration: const BoxDecoration( color: XfjDesignTokens.primarySoft, border: Border(right: BorderSide(color: XfjDesignTokens.line)), ), child: Obx(() { final status = _environmentStatus(controller); final color = _statusColor(status); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _EnvironmentPill( label: _environmentPillLabel(status), status: status, ), const Spacer(), Center(child: _EnvironmentIndicator(status: status)), const Spacer(), Container( width: double.infinity, padding: EdgeInsets.all(22.w), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.82), border: Border.all( color: color.withValues(alpha: 0.22), ), borderRadius: BorderRadius.circular( XfjDesignTokens.cardRadius, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ FIcon( _environmentIcon(controller, status), size: 24.w, color: color, ), SizedBox(width: 12.w), Text( controller .appControl.startupEnvironmentDescription.value, style: theme.typography.lg.copyWith( color: color, fontWeight: FontWeight.w700, ), ).expanded(), ], ), SizedBox(height: 12.h), Text( controller.appControl.startupEnvironmentDetail.value, style: theme.typography.base.copyWith( color: XfjDesignTokens.muted, height: 1.55, ), ), ], ), ), ], ); }), ); } } class _StartupProgressPanel extends StatelessWidget { const _StartupProgressPanel({required this.controller}); final LoadController controller; @override Widget build(BuildContext context) { final theme = FTheme.of(context); return Padding( padding: EdgeInsets.symmetric(horizontal: 40.w, vertical: 28.h), child: Obx(() { final failureMessage = controller.appControl.startupFailureMessage.value; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( failureMessage.isEmpty ? '正在准备收银工作台' : '终端启动未完成', style: theme.typography.xl3.copyWith( color: XfjDesignTokens.ink, fontWeight: FontWeight.w800, ), ), SizedBox(height: 8.h), Text( failureMessage.isEmpty ? '系统正在按实际执行结果准备本地数据与设备服务,完成后将自动进入登录页面。' : failureMessage, style: theme.typography.base.copyWith( color: XfjDesignTokens.muted, height: 1.5, ), ), ], ).expanded(), SizedBox(width: 24.w), _ProgressCount( current: controller.completedCount, total: controller.startupSteps.length, failed: failureMessage.isNotEmpty, ), ], ), SizedBox(height: 18.h), ClipRRect( borderRadius: BorderRadius.circular(999), child: LinearProgressIndicator( value: controller.progress, minHeight: 8.h, color: failureMessage.isEmpty ? XfjDesignTokens.primary : XfjDesignTokens.danger, backgroundColor: const Color(0xFFEDEFF3), ), ), SizedBox(height: 18.h), ListView.separated( primary: false, padding: EdgeInsets.zero, physics: const ClampingScrollPhysics(), itemCount: controller.startupSteps.length, separatorBuilder: (_, __) => SizedBox(height: 8.h), itemBuilder: (_, index) { final item = controller.startupSteps[index]; return StartupStepCard( icon: _stepIconAt(index), state: item, ); }, ).expanded(), SizedBox(height: 12.h), _StartupNotice( failed: failureMessage.isNotEmpty, onRetry: controller.retry, ), ], ); }), ); } } class _StartupNotice extends StatelessWidget { const _StartupNotice({required this.failed, required this.onRetry}); final bool failed; final Future Function() onRetry; @override Widget build(BuildContext context) { final theme = FTheme.of(context); return Container( constraints: BoxConstraints(minHeight: 56.h), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 4.h), decoration: BoxDecoration( color: failed ? const Color(0xFFFFF3F2) : const Color(0xFFFAFBFC), border: Border.all( color: failed ? XfjDesignTokens.danger.withValues(alpha: 0.28) : XfjDesignTokens.line, ), borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius), ), child: Row( children: [ FIcon( failed ? FAssets.icons.circleAlert : FAssets.icons.shieldCheck, size: 20.w, color: failed ? XfjDesignTokens.danger : XfjDesignTokens.success, ), SizedBox(width: 10.w), Text( failed ? '可重试初始化;如果仍失败,请检查本地数据库、权限和设备配置。' : '启动过程按真实服务状态自动完成,请勿关闭应用或断开设备电源。', style: theme.typography.sm.copyWith( color: XfjDesignTokens.muted, ), ).expanded(), SizedBox(width: 12.w), if (failed) FButton( style: FButtonStyle.outline, label: const Text('重新检查'), onPress: () => onRetry(), ).height(48.h) else Text( '版本 1.1.1', style: theme.typography.sm.copyWith( color: XfjDesignTokens.muted, fontWeight: FontWeight.w600, ), ), ], ), ); } } class _EnvironmentIndicator extends StatelessWidget { const _EnvironmentIndicator({required this.status}); final AppStartupStepStatus status; @override Widget build(BuildContext context) { final color = _statusColor(status); return SizedBox( width: 270.w, height: 270.w, child: Stack( alignment: Alignment.center, children: [ Container( width: 250.w, height: 250.w, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withValues(alpha: 0.44), border: Border.all(color: color.withValues(alpha: 0.08)), ), ), SizedBox( width: 224.w, height: 224.w, child: CircularProgressIndicator( value: status == AppStartupStepStatus.failure ? 1 : null, strokeWidth: 4.w, color: color, backgroundColor: color.withValues(alpha: 0.12), ), ), Container( width: 154.w, height: 154.w, decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, border: Border.all(color: color.withValues(alpha: 0.16)), boxShadow: [ BoxShadow( color: color.withValues(alpha: 0.12), blurRadius: 36, offset: const Offset(0, 14), ), ], ), child: FIcon( status == AppStartupStepStatus.failure ? FAssets.icons.circleAlert : FAssets.icons.monitor, size: 72.w, color: color, ), ), ], ), ); } } class _EnvironmentPill extends StatelessWidget { const _EnvironmentPill({required this.label, required this.status}); final String label; final AppStartupStepStatus status; @override Widget build(BuildContext context) { final theme = FTheme.of(context); final color = _statusColor(status); return Container( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.88), border: Border.all(color: color.withValues(alpha: 0.25)), borderRadius: BorderRadius.circular(999), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ FIcon( status == AppStartupStepStatus.failure ? FAssets.icons.circleAlert : status == AppStartupStepStatus.running ? FAssets.icons.refreshCw : FAssets.icons.check, size: 19.w, color: color, ), SizedBox(width: 8.w), Text( label, style: theme.typography.sm.copyWith( color: color, fontWeight: FontWeight.w700, ), ), ], ), ); } } class _HeaderStatus extends StatelessWidget { const _HeaderStatus({required this.label, required this.status}); final String label; final AppStartupStepStatus status; @override Widget build(BuildContext context) { final theme = FTheme.of(context); final color = _statusColor(status); return Container( height: 40.h, padding: EdgeInsets.symmetric(horizontal: 16.w), decoration: BoxDecoration( color: color.withValues(alpha: 0.08), border: Border.all(color: color.withValues(alpha: 0.24)), borderRadius: BorderRadius.circular(999), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 8.w, height: 8.w, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), SizedBox(width: 8.w), Text( label, style: theme.typography.sm.copyWith( color: color, fontWeight: FontWeight.w700, ), ), ], ), ); } } class _ProgressCount extends StatelessWidget { const _ProgressCount({ required this.current, required this.total, required this.failed, }); final int current; final int total; final bool failed; @override Widget build(BuildContext context) { final theme = FTheme.of(context); return Container( constraints: BoxConstraints(minWidth: 72.w, minHeight: 54.h), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h), decoration: BoxDecoration( color: failed ? const Color(0xFFFFF3F2) : XfjDesignTokens.control, borderRadius: BorderRadius.circular(999), ), child: Center( child: Text( '$current / $total', style: theme.typography.sm.copyWith( color: failed ? XfjDesignTokens.danger : XfjDesignTokens.muted, fontWeight: FontWeight.w700, fontFeatures: const [FontFeature.tabularFigures()], ), ), ), ); } } class _StartupFooter extends StatelessWidget { const _StartupFooter({required this.controller}); final LoadController controller; @override Widget build(BuildContext context) { final theme = FTheme.of(context); return Obx(() { final status = _overallStatus(controller); final color = _statusColor(status); return SizedBox( height: 32.h, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 8.w, height: 8.w, decoration: BoxDecoration(color: color, shape: BoxShape.circle), ), SizedBox(width: 8.w), Text( _footerLabel(controller), style: theme.typography.sm.copyWith( color: XfjDesignTokens.muted, ), ), ], ), ); }); } } /// Load 页启动步骤图标映射,顺序与 [AppControl.startupSteps] 保持一致。 final _stepIcons = [ FAssets.icons.hardDrive, FAssets.icons.database, FAssets.icons.printer, FAssets.icons.scanFace, ]; /// 按启动步骤索引取得图标;后续增加步骤但遗漏映射时使用通用设置图标避免页面越界崩溃。 SvgAsset _stepIconAt(int index) { if (index >= 0 && index < _stepIcons.length) return _stepIcons[index]; return FAssets.icons.settings; } AppStartupStepStatus _overallStatus(LoadController controller) { if (controller.appControl.startupFailureMessage.value.isNotEmpty) { return AppStartupStepStatus.failure; } if (controller.appControl.startupFinished.value) { return controller.startupSteps .any((item) => item.status == AppStartupStepStatus.warning) ? AppStartupStepStatus.warning : AppStartupStepStatus.success; } return AppStartupStepStatus.running; } Color _statusColor(AppStartupStepStatus status) { return switch (status) { AppStartupStepStatus.running => XfjDesignTokens.primary, AppStartupStepStatus.success => XfjDesignTokens.success, AppStartupStepStatus.warning => XfjDesignTokens.warning, AppStartupStepStatus.failure => XfjDesignTokens.danger, AppStartupStepStatus.waiting => XfjDesignTokens.muted, }; } AppStartupStepStatus _environmentStatus(LoadController controller) { return controller.startupSteps.isEmpty ? AppStartupStepStatus.waiting : controller.startupSteps.first.status; } String _environmentPillLabel(AppStartupStepStatus status) { return switch (status) { AppStartupStepStatus.running => '正在检测运行环境', AppStartupStepStatus.success => '运行环境检查完成', AppStartupStepStatus.warning => '运行环境受限可用', AppStartupStepStatus.failure => '运行环境检查失败', AppStartupStepStatus.waiting => '等待环境检查', }; } SvgAsset _environmentIcon( LoadController controller, AppStartupStepStatus status, ) { if (status == AppStartupStepStatus.failure) { return FAssets.icons.circleAlert; } if (AppRuntimeTool.isEmulator || AppRuntimeTool.useManualHardwareInput) { return FAssets.icons.keyboard; } return FAssets.icons.monitor; } String _footerLabel(LoadController controller) { if (controller.appControl.startupFailureMessage.value.isNotEmpty) { return 'ZAX 智慧消费终端 · 本地服务启动失败'; } if (controller.appControl.startupFinished.value) { return 'ZAX 智慧消费终端 · 本地服务运行中'; } return 'ZAX 智慧消费终端 · 正在启动本地服务'; }