Files
biz_xfj_cloud/lib/pages/setting/controller.dart
T
dzc a0ad3a06ff feat(kiosk): add android kiosk terminal mode support
完整实现收银终端的Android专用Kiosk模式支持,包括:
- 添加原生设备管理与Kiosk模式核心逻辑
- 新增Flutter设置页的Kiosk相关配置与控制UI
- 更新README添加部署命令
- 补充Android权限与组件声明
2026-07-24 09:53:51 +08:00

309 lines
10 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 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;
@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) {
cuurentMenuType.value = type;
}
Future<void> 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<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();
}
Future<void> togglePayType(OrderPayDisplayType type) async {
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);
}
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<void> 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<void> syncConfig() async {
await _runSync('config', '设备信息', () async {
final setting = await SyncConfigTool.autoSyncSysInfo();
if (setting != null) sysSetting.value = setting;
});
}
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,
) 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<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();
}
}
}