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 payTypeOrder = [ ...OrderPayDisplayType.values, ].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; bool _payPersisting = false; bool _payPendingPersist = false; /// Release 真机隐藏调试入口,模拟器即使使用 Release 也保留页面检查能力。 List get visibleMenuTypes => SettingMenuType.values .where( (type) => type != SettingMenuType.subScreenRoutes || AppRuntimeTool.isEmulator || AppSysTool.isDev(), ) .toList(growable: false); @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) { if (!visibleMenuTypes.contains(type)) return; cuurentMenuType.value = type; } Future getSetting() async { await refreshKioskStatus(); final setting = await AppSettingTool.getSysSetting(); final storedPayTypeOrder = await AppSettingTool.getPayTypeOrder(); final byCode = { for (final item in OrderPayDisplayType.values) item.code: item, }; final restoredPayTypeOrder = []; for (final code in storedPayTypeOrder) { final item = byCode[code]; if (item != null && !restoredPayTypeOrder.contains(item)) { restoredPayTypeOrder.add(item); } } for (final item in OrderPayDisplayType.values) { if (!restoredPayTypeOrder.contains(item)) { restoredPayTypeOrder.add(item); } } payTypeOrder.assignAll(restoredPayTypeOrder); if (setting.usePayDisplayType != null) { setting.usePayDisplayType = restoredPayTypeOrder .where(setting.usePayDisplayType!.contains) .toList(); } 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(); } void togglePayType(OrderPayDisplayType type) { 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); } final orderedEnabled = payTypeOrder.where(enabled.contains).toList(); // 默认支付方式必须属于启用集合,否则收银页会出现默认值不可选择的矛盾状态。 if (!orderedEnabled.contains(setting.defaultPayDisplayType)) { setting.defaultPayDisplayType = orderedEnabled.first; } setting.usePayDisplayType = orderedEnabled; sysSetting.refresh(); _schedulePayPreferencesPersist(); } void reorderPayType(int oldIndex, int newIndex) { if (oldIndex == newIndex || oldIndex < 0 || oldIndex >= payTypeOrder.length) { return; } if (newIndex > oldIndex) newIndex -= 1; if (newIndex < 0 || newIndex >= payTypeOrder.length) return; final updated = payTypeOrder.toList(); final item = updated.removeAt(oldIndex); updated.insert(newIndex, item); payTypeOrder.assignAll(updated); final setting = sysSetting.value; if (setting != null) { final visible = setting.usePayDisplayType ?? OrderPayDisplayType.values; setting.usePayDisplayType = updated.where(visible.contains).toList(); sysSetting.refresh(); } _schedulePayPreferencesPersist(); } void resetPayTypes() { payTypeOrder.assignAll(OrderPayDisplayType.values); final setting = sysSetting.value; if (setting != null) { setting.usePayDisplayType = OrderPayDisplayType.values.toList(); if (!setting.usePayDisplayType!.contains(setting.defaultPayDisplayType)) { setting.defaultPayDisplayType = setting.usePayDisplayType!.first; } sysSetting.refresh(); } _schedulePayPreferencesPersist(); } void setDefaultPayType(OrderPayDisplayType type) { 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; sysSetting.refresh(); _schedulePayPreferencesPersist(); } /// 支付方式连续拖动或切换时串行保存,始终以最后一次界面状态为准。 void _schedulePayPreferencesPersist() { _payPendingPersist = true; if (!_payPersisting) unawaited(_flushPayPreferences()); } Future _flushPayPreferences() async { _payPersisting = true; while (_payPendingPersist) { _payPendingPersist = false; final setting = sysSetting.value; if (setting == null) continue; try { await AppSettingTool.setPayTypePreferences( sysInfo: setting, order: payTypeOrder, ); } catch (e, stack) { logger.e('|$_tag|保存支付管理配置失败error:|$e|stack:|$stack|'); Toast.error(msg: '支付管理保存失败,请重试'); } } _payPersisting = false; } Future syncConfig() async { await _runSync('config', '设备信息', () async { final setting = await SyncConfigTool.autoSyncSysInfo(); if (setting != null) sysSetting.value = setting; }); } Future syncDeviceStatus() async { if (AppDeviceStatusTool.isRefreshing) { AppDeviceStatusTool.cancelRefresh(); if (syncingKey.value == 'deviceStatus') syncingKey.value = ''; Toast.success(msg: '已取消设备状态检查'); return; } if (syncingKey.isNotEmpty) return; syncingKey.value = 'deviceStatus'; syncFailedKeys.remove('deviceStatus'); try { await AppDeviceStatusTool.refreshAll(); Toast.success(msg: '设备状态检查完成'); } on TerminalDeviceCheckCancelled { // 用户主动取消不属于检查失败,保留上一次已经确认的设备状态。 } catch (e, stack) { syncFailedKeys.add('deviceStatus'); Toast.error(msg: '同步设备状态失败,请稍后重试'); logger.e('|$_tag|同步设备状态失败error:|$e|stack:|$stack|'); } finally { if (syncingKey.value == 'deviceStatus') syncingKey.value = ''; } } 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, { bool requireNetwork = true, bool refreshSetting = true, }) async { if (syncingKey.isNotEmpty) return; if (requireNetwork && !AppNetworkTool.isAutoNetworkMsg()) return; syncingKey.value = key; syncFailedKeys.remove(key); EasyLoading.show(status: '同步中...', maskType: EasyLoadingMaskType.black); try { await action(); if (refreshSetting) 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(); } } }