Files
biz_xfj_cloud/lib/pages/root/load/view.dart
T
2026-08-31 15:09:38 +08:00

604 lines
19 KiB
Dart

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<LoadController> {
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<void> 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 = <SvgAsset>[
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 智慧消费终端 · 正在启动本地服务';
}