import 'dart:async'; import 'package:common_utils/common_utils.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 '../routes/navigation.dart'; import 'setting_components.dart'; import 'setting_cron_rule_dialog.dart'; /// 同步设置只读取统一调度器状态;开关、规则和立即执行均由 SettingController 持久化或触发。 Widget settingSync(BuildContext context) { final controller = Get.find(); final scheduler = SyncCheckTool.instance; return Obx(() { final configs = [ for (final type in ScheduledTaskType.values) if (scheduler.configs[type] != null) scheduler.configs[type]!, ]; if (configs.isEmpty) return const SettingEmpty(message: '正在加载同步任务'); return [ SettingGroup( title: '自动任务', child: [ for (var index = 0; index < configs.length; index++) _ScheduledTaskRow( config: configs[index], record: scheduler.records[configs[index].type], running: scheduler.runningTasks.contains(configs[index].type), pending: scheduler.pendingTasks.contains(configs[index].type), saving: controller.savingScheduledTasks.contains(configs[index].type), showDivider: index != configs.length - 1, controller: controller, ), ].toColumn(), ), _terminalLogSummary(context, scheduler.terminalLogSummary.value), const SettingNotice( text: '定时任务仅在应用进程存活时执行。自动空闲任务要求前台连续 60 秒无操作;手动执行和后台拉取只等待支付、人脸、遮罩和业务弹框结束。', ), ] .toColumnSpace( crossAxisAlignment: CrossAxisAlignment.stretch, space: XfjDesignTokens.cardGap.h, ) .scrollable(); }); } /// 单个定时任务的紧凑配置行,任务标题区域同时作为执行日志入口。 class _ScheduledTaskRow extends StatelessWidget { /// 创建由调度器快照和设置控制器共同驱动的任务行。 const _ScheduledTaskRow({ required this.config, required this.record, required this.running, required this.pending, required this.saving, required this.showDivider, required this.controller, }); /// 当前任务的自动开关、Cron 和空闲策略配置。 final ScheduledTaskConfig config; /// 当前任务最近一次执行结果;从未执行或尚未加载时为空。 final ScheduledTaskRecord? record; /// 当前任务是否正在实际执行业务动作。 final bool running; /// 当前任务是否正在等待安全窗口或等待同类任务结束。 final bool pending; /// 当前任务配置是否正在写入 SQLite,用于暂时禁用编辑控件。 final bool saving; /// 是否在本行底部展示与下一任务之间的分隔线。 final bool showDivider; /// 当前设置页 Binding 注册的控制器,负责保存配置和立即执行。 final SettingController controller; /// 构建日志入口、Cron 规则、空闲开关、自动开关和立即执行操作。 @override Widget build(BuildContext context) { final theme = FTheme.of(context); final metadata = _taskMetadata(config.type); final status = _taskStatus(record, running: running, pending: pending); final rowTextStyle = theme.typography.sm; // 任务详情、规则、开关和立即执行使用独立热区,避免点击日志时误切换任务配置。 final taskSummary = Tooltip( message: '查看${metadata.title}执行日志', child: InkWell( onTap: () => unawaited( SettingNavigation.openTaskLogs(config.type), ), borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius), child: [ _taskIcon(metadata.icon, running: running), [ Text( metadata.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: rowTextStyle.copyWith( color: XfjDesignTokens.ink, fontWeight: FontWeight.w800, ), ), SizedBox(height: 4.h), Text( '${status.text} · ${_formatRecordTime(record?.lastRunAt)}', maxLines: 1, overflow: TextOverflow.ellipsis, style: rowTextStyle.copyWith(color: status.color), ), ].toColumn(crossAxisAlignment: CrossAxisAlignment.start).expanded(), ].toRowSpace(space: 10.w).padding(vertical: 4.h), ), ); final taskActions = [ SizedBox(width: 8.w), _TaskLogButton( taskTitle: metadata.title, onTap: () => unawaited(SettingNavigation.openTaskLogs(config.type)), ), SizedBox(width: 10.w), _CronRuleButton( expression: config.cron, enabled: !saving, onTap: () => _selectCron(context), ), SizedBox(width: 10.w), _TaskSwitch( label: config.idleOnly ? '仅空闲' : '直接执行', semanticLabel: '${metadata.title}仅空闲执行', cellWidth: 144.w, value: config.idleOnly, enabled: !saving, onChange: (value) => controller.setScheduledTaskIdleOnly(config.type, value), ), SizedBox(width: 10.w), _TaskSwitch( label: '自动', semanticLabel: '${metadata.title}自动执行', cellWidth: 112.w, value: config.enabled, enabled: !saving, onChange: (value) => controller.setScheduledTaskEnabled(config.type, value), ), const Spacer(), SizedBox(width: 10.w), FButton( style: FButtonStyle.outline, onPress: saving ? null : () => unawaited( controller.runScheduledTaskNow(config.type), ), prefix: running ? SizedBox.square( dimension: 18.w, child: const CircularProgressIndicator(strokeWidth: 2), ) : FIcon(FAssets.icons.refreshCw, size: 18.sp), label: Text( running ? '执行中' : pending ? '再执行一次' : '立即执行', style: rowTextStyle, ), ).constrained(minWidth: 138.w, minHeight: 48.h), ].toRow(); return [ taskSummary.expanded(flex: 5), taskActions.expanded(flex: 5), ] .toRow() .padding(horizontal: 18.w, vertical: 10.h) .constrained(minHeight: 72.h) .decorated( border: showDivider ? const Border( bottom: BorderSide(color: XfjDesignTokens.line), ) : null, ); } /// 打开规则弹窗,并在用户确认了不同表达式时交由 Controller 二次校验和保存。 Future _selectCron(BuildContext context) async { final expression = await showScheduledCronRuleDialog( context, currentExpression: config.cron, ); if (expression == null || expression == config.cron) return; await controller.setScheduledTaskCron(config.type, expression); } } /// 任务行中独立的结构化日志入口,图标热区不受任务摘要宽度影响。 class _TaskLogButton extends StatelessWidget { /// 创建固定 48dp 热区的日志页面入口。 const _TaskLogButton({required this.taskTitle, required this.onTap}); /// 当前任务中文名称,用于 Tooltip 和语义说明。 final String taskTitle; /// 点击后打开对应任务日志路由的回调。 final VoidCallback onTap; /// 使用卷轴文本图标构建更明确的日志操作入口。 @override Widget build(BuildContext context) => Tooltip( message: '查看$taskTitle执行日志', child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius), child: FIcon( FAssets.icons.scrollText, color: XfjDesignTokens.primary, size: 24.sp, ).center().constrained(width: 48.w, height: 48.h).decorated( color: XfjDesignTokens.primarySoft, borderRadius: BorderRadius.circular( XfjDesignTokens.controlRadius, ), ), ), ); } /// 在任务行中垂直、水平居中展示当前规则摘要的 Cron 编辑按钮。 class _CronRuleButton extends StatelessWidget { /// 创建一个固定尺寸的 Cron 规则按钮。 const _CronRuleButton({ required this.expression, required this.enabled, required this.onTap, }); /// 当前任务已保存的六段 Cron 表达式。 final String expression; /// 是否允许打开规则弹窗;配置保存期间为假。 final bool enabled; /// 打开 Cron 规则弹窗的回调。 final VoidCallback onTap; /// 构建 48dp 点击热区内的紧凑 Cron 标签,保持任务行列稳定。 @override Widget build(BuildContext context) { final theme = FTheme.of(context); return Tooltip( message: '设置 Cron 定时规则', child: InkWell( onTap: enabled ? onTap : null, borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius), child: [ FIcon( FAssets.icons.clock, color: enabled ? XfjDesignTokens.primary : XfjDesignTokens.muted, size: 16.sp, ), Text( _cronLabel(expression), maxLines: 1, softWrap: false, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: theme.typography.sm.copyWith( color: enabled ? XfjDesignTokens.primary : XfjDesignTokens.muted, fontWeight: FontWeight.w700, fontFeatures: const [FontFeature.tabularFigures()], ), ).expanded(), ] .toRowSpace(space: 6.w) .padding(horizontal: 9.w) .constrained(width: 158.w, height: 36.h) .decorated( color: XfjDesignTokens.primarySoft, border: Border.all( color: enabled ? XfjDesignTokens.primary : XfjDesignTokens.line, ), borderRadius: BorderRadius.circular( XfjDesignTokens.controlRadius, ), ) .center() .constrained(width: 164.w, height: 48.h), ), ); } } /// 任务行中复用的带短标签二元开关。 class _TaskSwitch extends StatelessWidget { /// 创建一个由父任务行管理值与保存行为的开关。 const _TaskSwitch({ required this.label, required this.semanticLabel, required this.cellWidth, required this.value, required this.enabled, required this.onChange, }); /// 开关左侧的短展示名称。 final String label; /// 读屏使用的完整任务操作说明。 final String semanticLabel; /// 本列固定宽度,分别为执行策略和自动开关保留不换行空间。 final double cellWidth; /// 当前开关值。 final bool value; /// 配置保存期间是否允许切换。 final bool enabled; /// 值变化后交由设置控制器持久化的回调。 final ValueChanged onChange; /// 构建固定尺寸标签与 Forui 开关,保证各任务行列对齐。 @override Widget build(BuildContext context) { final theme = FTheme.of(context); // 当前开关没有使用 Forui 的内置标签,移除其默认水平留白,避免固定表格列产生溢出; // 保留垂直留白,使开关本身仍具有 48dp 的稳定触控高度。 final switchStyle = theme.switchStyle.copyWith( labelLayoutStyle: theme.switchStyle.labelLayoutStyle.copyWith( childPadding: EdgeInsets.symmetric(vertical: 4.5.h), ), ); return [ Text( label, maxLines: 1, softWrap: false, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, style: theme.typography.sm.copyWith( color: value ? XfjDesignTokens.ink : XfjDesignTokens.muted, fontWeight: FontWeight.w700, ), ).expanded(), SizedBox(width: 6.w), FSwitch( style: switchStyle, semanticLabel: semanticLabel, value: value, enabled: enabled, onChange: onChange, ), ].toRow().constrained(width: cellWidth, minHeight: 48.h); } } /// 构建任务行固定尺寸图标;运行中用加载指示器替代业务图标。 Widget _taskIcon(SvgAsset icon, {required bool running}) => Container( width: 44.w, height: 44.w, alignment: Alignment.center, decoration: BoxDecoration( color: XfjDesignTokens.primarySoft, borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius), ), child: running ? SizedBox.square( dimension: 22.w, child: const CircularProgressIndicator(strokeWidth: 2), ) : FIcon(icon, color: XfjDesignTokens.primary, size: 23.sp), ); /// 构建最近一次日志上传的文件统计和成功时间概况。 Widget _terminalLogSummary( BuildContext context, TerminalLogUploadSummary summary, ) { final theme = FTheme.of(context); final lastSuccess = summary.lastSuccessAt == null ? '暂无成功记录' : DateUtil.formatDate( summary.lastSuccessAt!, format: DateFormats.full, ); return SettingGroup( title: '日志上传概况', child: [ _summaryValue(theme, '扫描', summary.scanned, XfjDesignTokens.ink), _summaryValue(theme, '跳过', summary.skipped, XfjDesignTokens.muted), _summaryValue(theme, '成功', summary.succeeded, XfjDesignTokens.success), _summaryValue(theme, '失败', summary.failed, XfjDesignTokens.danger), Text( '最近成功:$lastSuccess', maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.typography.sm.copyWith( color: XfjDesignTokens.muted, fontFeatures: const [FontFeature.tabularFigures()], ), ).expanded(), ] .toRowSpace(space: 22.w) .padding(horizontal: 20.w, vertical: 14.h) .constrained(minHeight: 64.h), ); } /// 构建单个日志上传统计值,颜色由成功、失败等业务语义传入。 Widget _summaryValue( FThemeData theme, String label, int value, Color color, ) => Text( '$label $value', style: theme.typography.base.copyWith( color: color, fontWeight: FontWeight.w800, fontFeatures: const [FontFeature.tabularFigures()], ), ); /// 将任务类型转换为设置页使用的中文名称和图标。 ({String title, SvgAsset icon}) _taskMetadata(ScheduledTaskType type) => switch (type) { ScheduledTaskType.mqReconnect => ( title: 'MQTT 重连', icon: FAssets.icons.wifi ), ScheduledTaskType.printCheck => ( title: '打印任务检查', icon: FAssets.icons.printer ), ScheduledTaskType.broadcastCheck => ( title: '取餐广播检查', icon: FAssets.icons.receiptText ), ScheduledTaskType.deviceStatus => ( title: '设备状态检查', icon: FAssets.icons.hardDrive ), ScheduledTaskType.autoUpdate => ( title: 'APK 自动更新', icon: FAssets.icons.packageCheck ), ScheduledTaskType.configSync => ( title: '设备配置同步', icon: FAssets.icons.settings ), ScheduledTaskType.departmentFaceSync => ( title: '本部门人脸同步', icon: FAssets.icons.scanFace ), ScheduledTaskType.classifySync => ( title: '分类同步', icon: FAssets.icons.packageOpen ), ScheduledTaskType.productSync => ( title: '商品同步', icon: FAssets.icons.package ), ScheduledTaskType.offlineOrderSync => ( title: '离线订单同步', icon: FAssets.icons.cloudDownload ), ScheduledTaskType.orderEvidenceUpload => ( title: '订单证据补传', icon: FAssets.icons.image ), ScheduledTaskType.terminalLogUpload => ( title: '终端日志上传', icon: FAssets.icons.logs ), ScheduledTaskType.expiredLogCleanup => ( title: '过期日志清理', icon: FAssets.icons.trash2 ), }; /// 根据运行、等待和最近结果生成任务行状态文案与语义颜色。 ({String text, Color color}) _taskStatus( ScheduledTaskRecord? record, { required bool running, required bool pending, }) { if (running) return (text: '正在执行', color: XfjDesignTokens.primary); if (pending) return (text: '等待安全窗口', color: XfjDesignTokens.warning); if (record == null) return (text: '尚未执行', color: XfjDesignTokens.muted); return record.success ? (text: record.message, color: XfjDesignTokens.success) : (text: record.message, color: XfjDesignTokens.danger); } /// 优先返回常用预设名称,自定义表达式无法匹配预设时展示原值。 String _cronLabel(String cron) { for (final preset in SyncCheckTool.cronPresets) { if (preset.expression == cron) return preset.label; } return cron; } /// 将最近执行时间格式化为任务行摘要,从未执行时返回明确空状态。 String _formatRecordTime(DateTime? time) => time == null ? '最近执行:暂无' : '最近执行:${DateUtil.formatDate(time, format: DateFormats.full)}';