import 'dart:async'; import 'package:common_utils/common_utils.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; import 'package:pj_supermarket_cloud/common/index.dart'; class SettingController extends GetxController with WidgetsBindingObserver { static const String _tag = '设置 Controller'; final appC = Get.find(); final sysSetting = Rxn(); final userSetting = Rxn(); final manualHardwareInput = false.obs; final kioskStatus = AppRuntimeTool.kioskStatus.obs; final cuurentMenuType = SettingMenuType.baseInfo.obs; final isLoading = true.obs; final isBound = false.obs; final syncingKey = ''.obs; final syncFailedKeys = {}.obs; final sqlBackupTime = Rxn(); final logBackupTime = Rxn(); final lastBackupMessage = ''.obs; StreamSubscription? _mqUpdateListen; @override void onInit() { super.onInit(); WidgetsBinding.instance.addObserver(this); _initialize(); } Future _initialize() async { try { await getSetting(); await refreshBackupTimes(); _mqUpdateListen = appC.mqUpdateEventBus.on().listen( (event) async { if (event == MqtActionEnum.CONFIG) await getSetting(); }, ); } catch (e, stack) { logger.e('|$_tag|初始化失败error:|$e|stack:|$stack|'); Toast.error(msg: '设置加载失败,请稍后重试'); } finally { isLoading.value = false; } } @override void onClose() { WidgetsBinding.instance.removeObserver(this); _mqUpdateListen?.cancel(); super.onClose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) refreshKioskStatus(); } void doMenu(SettingMenuType type) { cuurentMenuType.value = type; } Future getSetting() async { await refreshKioskStatus(); final setting = await AppSettingTool.getSysSetting(); sysSetting.value = setting; userSetting.value = await AppSettingTool.getUserSetting(); isBound.value = ObjectUtil.isNotEmpty(setting.serviceMerchantId); manualHardwareInput.value = AppRuntimeTool.isEmulator || setting.manualHardwareInput; AppRuntimeTool.applyManualHardwareInput(setting.manualHardwareInput); } Future refreshKioskStatus() async { kioskStatus.value = await AppRuntimeTool.refreshKioskStatus(); } Future setGlobalFontSize(SettingGlobalFontSize fontSize) async { final setting = sysSetting.value; if (setting == null || setting.globalFontSize == fontSize) return; setting.globalFontSize = fontSize; await AppSettingTool.setSysSetting(setting); Get.find().setSizeScalar(fontSize); sysSetting.refresh(); } Future setButtonRadius(SettingButtonRadius value) async { final setting = sysSetting.value; if (setting == null || setting.buttonRadius == value) return; setting.buttonRadius = value; await AppSettingTool.setSysSetting(setting); Get.find().setButtonRadius(value); sysSetting.refresh(); } Future setCardRadius(SettingCardRadius value) async { final setting = sysSetting.value; if (setting == null || setting.cardRadius == value) return; setting.cardRadius = value; await AppSettingTool.setSysSetting(setting); Get.find().setCardRadius(value); sysSetting.refresh(); } Future setButtonSize(SettingButtonSize value) async { final setting = sysSetting.value; if (setting == null || setting.buttonSize == value) return; setting.buttonSize = value; await AppSettingTool.setSysSetting(setting); Get.find().setButtonSize(value); sysSetting.refresh(); } Future togglePayType(OrderPayDisplayType type) async { final setting = sysSetting.value; if (setting == null) return; final enabled = List.from( setting.usePayDisplayType ?? OrderPayDisplayType.values, ); if (enabled.contains(type)) { if (enabled.length == 1) { Toast.error(msg: '至少保留一种支付方式'); return; } enabled.remove(type); } else { enabled.add(type); } enabled.sort((a, b) => a.order.compareTo(b.order)); // 默认支付方式必须属于启用集合,否则收银页会出现默认值不可选择的矛盾状态。 if (!enabled.contains(setting.defaultPayDisplayType)) { setting.defaultPayDisplayType = enabled.first; } setting.usePayDisplayType = enabled; await AppSettingTool.setSysSetting(setting); sysSetting.refresh(); } Future setDefaultPayType(OrderPayDisplayType type) async { final setting = sysSetting.value; if (setting == null) return; final enabled = setting.usePayDisplayType ?? OrderPayDisplayType.values; if (!enabled.contains(type)) { Toast.error(msg: '请先启用该支付方式'); return; } setting.defaultPayDisplayType = type; await AppSettingTool.setSysSetting(setting); sysSetting.refresh(); } Future syncConfig() async { await _runSync('config', '设备信息', () async { final setting = await SyncConfigTool.autoSyncSysInfo(); if (setting != null) sysSetting.value = setting; }); } Future syncMq() async { await _runSync('mq', '长链接', () async { await MqManager.closeMqtt(); await AppMqTool.mqRefresh(); }); } Future syncClassify() async { await _runSync('classify', '分类', SyncProductTool.syncClassify); } Future syncProduct() async { await _runSync('product', '商品', SyncProductTool.syncProduct); } Future syncOfflineOrder() async { await _runSync('offlineOrder', '离线订单', SyncOrderTool.syncOfflineOrder); } Future _runSync( String key, String label, Future Function() action, ) async { if (syncingKey.isNotEmpty) return; if (!AppNetworkTool.isAutoNetworkMsg()) return; syncingKey.value = key; syncFailedKeys.remove(key); EasyLoading.show(status: '同步中...', maskType: EasyLoadingMaskType.black); try { await action(); await getSetting(); Toast.success(msg: '同步$label成功'); } catch (e, stack) { syncFailedKeys.add(key); Toast.error(msg: '同步$label失败,请稍后重试'); logger.e('|$_tag|同步$label失败error:|$e|stack:|$stack|'); } finally { syncingKey.value = ''; EasyLoading.dismiss(); } } Future backupsSql() async { await _runBackup('SQL', AppFileTool.backupSqlFilePath); } Future backupsLog() async { await _runBackup('日志', AppFileTool.backupLogFile); } Future _runBackup( String label, Future Function() action, ) async { EasyLoading.show(status: '备份中...', maskType: EasyLoadingMaskType.black); try { final path = await action(); if (ObjectUtil.isEmpty(path)) throw Exception('未生成备份文件'); lastBackupMessage.value = '$label 备份已生成:$path'; await refreshBackupTimes(); Toast.success(msg: '$label 备份成功'); } catch (e, stack) { Toast.error(msg: '$label 备份失败,请检查存储权限和文件状态'); logger.e('|$_tag|$label备份失败error:|$e|stack:|$stack|'); } finally { EasyLoading.dismiss(); } } Future refreshBackupTimes() async { final setting = sysSetting.value; if (setting == null) return; final basePath = await AppFileTool.getExternalPath( backupExternalPath: false, ); sqlBackupTime.value = await AppFileTool.getLatestModifiedTime( '$basePath/backup/database', ); logBackupTime.value = await AppFileTool.getLatestModifiedTime( '$basePath/backup/logger/manual', ); } Future setManualHardwareInput(bool enabled) async { if (AppRuntimeTool.isEmulator && !enabled) { Toast.error(msg: '虚拟机必须使用手动硬件输入模式'); manualHardwareInput.value = true; return; } if (enabled && !manualHardwareInput.value) { final confirmed = await appMessageDialog( Get.context!, '设置为主控后,扫码、饭卡读写和人脸识别将改为人工输入,是否继续?', ); if (!confirmed) return; } final setting = sysSetting.value; if (setting == null) return; setting.manualHardwareInput = enabled; await AppSettingTool.setSysSetting(setting); AppRuntimeTool.applyManualHardwareInput(enabled); manualHardwareInput.value = enabled; sysSetting.refresh(); Toast.success(msg: enabled ? '已设置为主控手动输入模式' : '已恢复真实硬件模式'); } /// 该配置只控制进入 App 时是否自动锁定;关闭后仍需通过管理密码退出当前 Kiosk。 Future setKioskAutoEnterEnabled(bool enabled) async { if (!kioskStatus.value.available) return; final status = await AppRuntimeTool.setKioskAutoEnterEnabled(enabled); kioskStatus.value = status; final applied = status.autoEnterEnabled == enabled && (!enabled || status.active || status.lockTaskPermitted); if (!applied) { Toast.error(msg: 'Kiosk 自动开启配置失败,请检查设备策略'); return; } Toast.success( msg: enabled ? '每次进入 App 将自动开启 Kiosk' : '已关闭进入 App 自动开启 Kiosk'); } /// 管理密码验证由按钮组件负责,此处只处理原生退出结果与失败反馈。 Future exitKioskToAndroidHome() async { if (!kioskStatus.value.canExitToAndroidHome) return; EasyLoading.show( status: '正在退出专用设备模式...', maskType: EasyLoadingMaskType.black, ); try { final success = await AppRuntimeTool.exitKioskToAndroidHome(); kioskStatus.value = await AppRuntimeTool.refreshKioskStatus(); if (!success) { Toast.error(msg: '退出 Android 桌面失败,请检查设备策略'); } } catch (e, stack) { logger.e('|$_tag|退出Kiosk失败error:|$e|stack:|$stack|'); Toast.error(msg: '退出 Android 桌面失败,请稍后重试'); } finally { EasyLoading.dismiss(); } } }