433 lines
15 KiB
Dart
433 lines
15 KiB
Dart
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<AppControl>();
|
|
final sysSetting = Rxn<SysSetting>();
|
|
final userSetting = Rxn<UserSetting>();
|
|
final manualHardwareInput = false.obs;
|
|
final kioskStatus = AppRuntimeTool.kioskStatus.obs;
|
|
final payTypeOrder = <OrderPayDisplayType>[
|
|
...OrderPayDisplayType.values,
|
|
].obs;
|
|
final cuurentMenuType = SettingMenuType.baseInfo.obs;
|
|
final isLoading = true.obs;
|
|
final isBound = false.obs;
|
|
final syncingKey = ''.obs;
|
|
final syncFailedKeys = <String>{}.obs;
|
|
final sqlBackupTime = Rxn<DateTime>();
|
|
final logBackupTime = Rxn<DateTime>();
|
|
final lastBackupMessage = ''.obs;
|
|
|
|
StreamSubscription? _mqUpdateListen;
|
|
bool _payPersisting = false;
|
|
bool _payPendingPersist = false;
|
|
|
|
/// Release 真机隐藏调试入口,模拟器即使使用 Release 也保留页面检查能力。
|
|
List<SettingMenuType> 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<void> _initialize() async {
|
|
try {
|
|
await getSetting();
|
|
await refreshBackupTimes();
|
|
_mqUpdateListen = appC.mqUpdateEventBus.on<MqtActionEnum>().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<void> 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 = <OrderPayDisplayType>[];
|
|
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<void> refreshKioskStatus() async {
|
|
kioskStatus.value = await AppRuntimeTool.refreshKioskStatus();
|
|
}
|
|
|
|
Future<void> setGlobalFontSize(SettingGlobalFontSize fontSize) async {
|
|
final setting = sysSetting.value;
|
|
if (setting == null || setting.globalFontSize == fontSize) return;
|
|
setting.globalFontSize = fontSize;
|
|
await AppSettingTool.setSysSetting(setting);
|
|
Get.find<AppThemeControl>().setSizeScalar(fontSize);
|
|
sysSetting.refresh();
|
|
}
|
|
|
|
Future<void> setButtonRadius(SettingButtonRadius value) async {
|
|
final setting = sysSetting.value;
|
|
if (setting == null || setting.buttonRadius == value) return;
|
|
setting.buttonRadius = value;
|
|
await AppSettingTool.setSysSetting(setting);
|
|
Get.find<AppThemeControl>().setButtonRadius(value);
|
|
sysSetting.refresh();
|
|
}
|
|
|
|
Future<void> setCardRadius(SettingCardRadius value) async {
|
|
final setting = sysSetting.value;
|
|
if (setting == null || setting.cardRadius == value) return;
|
|
setting.cardRadius = value;
|
|
await AppSettingTool.setSysSetting(setting);
|
|
Get.find<AppThemeControl>().setCardRadius(value);
|
|
sysSetting.refresh();
|
|
}
|
|
|
|
Future<void> setButtonSize(SettingButtonSize value) async {
|
|
final setting = sysSetting.value;
|
|
if (setting == null || setting.buttonSize == value) return;
|
|
setting.buttonSize = value;
|
|
await AppSettingTool.setSysSetting(setting);
|
|
Get.find<AppThemeControl>().setButtonSize(value);
|
|
sysSetting.refresh();
|
|
}
|
|
|
|
void togglePayType(OrderPayDisplayType type) {
|
|
final setting = sysSetting.value;
|
|
if (setting == null) return;
|
|
final enabled = List<OrderPayDisplayType>.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<void> _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<void> syncConfig() async {
|
|
await _runSync('config', '设备信息', () async {
|
|
final setting = await SyncConfigTool.autoSyncSysInfo();
|
|
if (setting != null) sysSetting.value = setting;
|
|
});
|
|
}
|
|
|
|
Future<void> 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<void> syncMq() async {
|
|
await _runSync('mq', '长链接', () async {
|
|
await MqManager.closeMqtt();
|
|
await AppMqTool.mqRefresh();
|
|
});
|
|
}
|
|
|
|
Future<void> syncClassify() async {
|
|
await _runSync('classify', '分类', SyncProductTool.syncClassify);
|
|
}
|
|
|
|
Future<void> syncProduct() async {
|
|
await _runSync('product', '商品', SyncProductTool.syncProduct);
|
|
}
|
|
|
|
Future<void> syncOfflineOrder() async {
|
|
await _runSync('offlineOrder', '离线订单', SyncOrderTool.syncOfflineOrder);
|
|
}
|
|
|
|
Future<void> _runSync(
|
|
String key,
|
|
String label,
|
|
Future<void> 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<void> backupsSql() async {
|
|
await _runBackup('SQL', AppFileTool.backupSqlFilePath);
|
|
}
|
|
|
|
Future<void> backupsLog() async {
|
|
await _runBackup('日志', AppFileTool.backupLogFile);
|
|
}
|
|
|
|
Future<void> _runBackup(
|
|
String label,
|
|
Future<String?> 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<void> 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<void> 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<void> 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<void> 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();
|
|
}
|
|
}
|
|
}
|