1064 lines
43 KiB
Dart
1064 lines
43 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';
|
|
|
|
/// 应用级控制器,提供 MQTT 配置变更事件总线。
|
|
final appC = Get.find<AppControl>();
|
|
|
|
/// 终端一级菜单控制器,提供可配置页面的实时排序。
|
|
final terminalMenuC = Get.find<TerminalMenuControl>();
|
|
|
|
/// 当前设备系统设置;初始化完成前为空,修改深层字段后需主动 refresh。
|
|
final sysSetting = Rxn<SysSetting>();
|
|
|
|
/// 当前登录用户的本地设置;初始化完成前为空。
|
|
final userSetting = Rxn<UserSetting>();
|
|
|
|
/// 是否使用人工输入代替扫码、读卡和人脸等真实硬件输入。
|
|
final manualHardwareInput = false.obs;
|
|
|
|
/// Android Kiosk 当前能力与运行状态,应用恢复前台时刷新。
|
|
final kioskStatus = AppRuntimeTool.kioskStatus.obs;
|
|
|
|
/// Root 手动刷新是否正在执行,用于禁用重复点击并展示局部进度。
|
|
final rootRefreshing = false.obs;
|
|
|
|
/// Root 一键配置 Device Owner 是否正在执行,命令期间禁止重复提交。
|
|
final kioskProvisioning = false.obs;
|
|
|
|
/// Kiosk 开关是否正在同步原生 Lock Task,防止响应返回前被连续点击形成状态反转。
|
|
final kioskSwitching = false.obs;
|
|
|
|
/// 最近一次 Root 配置 Kiosk 的失败处理建议;成功或重新加载设置后清空。
|
|
final kioskProvisionMessage = ''.obs;
|
|
|
|
/// 收银支付方式的界面排序,初始顺序来自枚举并由本地持久化覆盖。
|
|
final payTypeOrder = <OrderPayDisplayType>[
|
|
...OrderPayDisplayType.values,
|
|
].obs;
|
|
|
|
/// 允许在收银弹框内重新识别人员的支付方式集合;初始化采用业务默认值,配置加载后以持久化结果为准。
|
|
final cashierReidentifiablePayTypes =
|
|
AppSettingTool.defaultCashierReidentifiablePayTypes.toSet().obs;
|
|
|
|
/// 页面设置当前编辑的终端一级页面,默认展示收银页面配置。
|
|
final pageSettingType = TerminalMenuItemType.cashier.obs;
|
|
|
|
/// 收银页右下角快捷操作显隐独立观察,切换时不重建支付方式排序区域。
|
|
final cashierShowHoldAction = true.obs;
|
|
|
|
/// 收银页右下角“更多”快捷操作是否展示。
|
|
final cashierShowMoreAction = true.obs;
|
|
|
|
/// 收银页是否允许主动修改餐段;关闭后餐段严格按餐段配置时间自动切换。
|
|
final cashierAllowManualMealPeriodChange = true.obs;
|
|
|
|
/// 预约查询识别方式的展示顺序,持久化到现有设置 JSON。
|
|
final reservationQueryMethodOrder = <ReservationRecognitionMethod>[
|
|
...ReservationRecognitionMethod.values,
|
|
].obs;
|
|
|
|
/// 预约查询默认识别方式,必须始终包含在当前识别方式顺序中。
|
|
final reservationQueryDefaultMethod = ReservationRecognitionMethod.card.obs;
|
|
|
|
/// 查询菜单是否在进入页面和切换方式后自动启动当前识别设备。
|
|
final reservationQueryAutoRecognitionEnabled = false.obs;
|
|
|
|
/// 自动识别正常等待超时后是否继续开启下一轮;设备故障和主动取消不自动重试。
|
|
final reservationQueryContinuousRecognitionEnabled = true.obs;
|
|
|
|
/// 查询菜单是否要求服务端脱敏用户姓名。
|
|
final reservationQueryNameMaskEnabled = true.obs;
|
|
|
|
/// 查询菜单是否启用服务端二次核销校验。
|
|
final reservationQueryVerificationEnabled = false.obs;
|
|
|
|
/// 查询菜单是否在识别成功后直接打开今天预选餐段的核销弹框。
|
|
final reservationQueryQuickVerificationEnabled = false.obs;
|
|
|
|
/// 查询菜单是否允许主动修改餐段;关闭后餐段严格按餐段配置时间自动切换。
|
|
final reservationQueryAllowManualMealPeriodChange = true.obs;
|
|
|
|
/// 查询菜单核销允许的日期范围,默认仅限当天。
|
|
final reservationQueryVerificationDateScope =
|
|
ReservationVerificationDateScope.today.obs;
|
|
|
|
/// 早餐、中餐和晚餐的本地时间范围,设置加载后由 sysJson 中的持久化值覆盖。
|
|
final productMealPeriodSchedule = ProductMealPeriodSchedule.defaults.obs;
|
|
|
|
/// 餐段配置正在保存时禁用重复提交并展示按钮状态。
|
|
final productMealPeriodSaving = false.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;
|
|
|
|
/// 旧内联编辑器当前展开的任务类型;保留给兼容组件读取,为空表示全部收起。
|
|
final expandedScheduledTask = Rxn<ScheduledTaskType>();
|
|
|
|
/// 各任务尚未保存的 Cron 输入草稿,以任务类型隔离编辑状态。
|
|
final scheduledTaskCronDrafts = <ScheduledTaskType, String>{}.obs;
|
|
|
|
/// 各任务 Cron 草稿的本地校验错误,保存成功或继续输入后移除。
|
|
final scheduledTaskCronErrors = <ScheduledTaskType, String>{}.obs;
|
|
|
|
/// 正在持久化配置的任务集合,用于禁止同任务重复提交并局部禁用控件。
|
|
final savingScheduledTasks = <ScheduledTaskType>{}.obs;
|
|
|
|
/// MQTT 配置更新事件订阅,在控制器关闭时释放。
|
|
StreamSubscription? _mqUpdateListen;
|
|
|
|
/// 是否正在串行写入支付方式偏好,防止拖动操作并发覆盖。
|
|
bool _payPersisting = false;
|
|
|
|
/// 支付方式保存期间是否又产生了新状态,需要在当前写入后继续保存。
|
|
bool _payPendingPersist = false;
|
|
|
|
/// 是否正在串行写入收银页面快捷操作偏好。
|
|
bool _cashierPagePersisting = false;
|
|
|
|
/// 收银页面偏好保存期间是否又产生了待写入状态。
|
|
bool _cashierPagePendingPersist = false;
|
|
|
|
/// 是否正在串行写入预约查询页面偏好。
|
|
bool _reservationQueryPersisting = false;
|
|
|
|
/// 预约查询偏好保存期间是否又产生了待写入状态。
|
|
bool _reservationQueryPendingPersist = false;
|
|
|
|
/// 页面设置跟随主屏左侧菜单的实时顺序,但不继承隐藏状态;设置页自身不递归配置。
|
|
List<TerminalMenuItemType> get configurablePageTypes =>
|
|
terminalMenuC.menuOrder
|
|
.toList()
|
|
.where((type) => type != TerminalMenuItemType.settings)
|
|
.toList(growable: false);
|
|
|
|
/// 支付配置已迁入“页面设置 > 收银”,旧枚举和页面仅保留兼容,避免破坏既有代码契约。
|
|
/// Release 真机隐藏调试入口,模拟器即使使用 Release 也保留页面检查能力。
|
|
List<SettingMenuType> get visibleMenuTypes => SettingMenuType.values
|
|
.where(
|
|
(type) =>
|
|
type != SettingMenuType.paySetting &&
|
|
(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();
|
|
await SyncCheckTool.instance.startAll();
|
|
scheduledTaskCronDrafts.assignAll({
|
|
for (final entry in SyncCheckTool.instance.configs.entries)
|
|
entry.key: entry.value.cron,
|
|
});
|
|
_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;
|
|
}
|
|
}
|
|
|
|
/// 释放应用生命周期观察和 MQTT 配置事件订阅。
|
|
@override
|
|
void onClose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
_mqUpdateListen?.cancel();
|
|
super.onClose();
|
|
}
|
|
|
|
/// 应用恢复前台时重新读取 Kiosk 状态,反映外部系统设置变化。
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
if (state == AppLifecycleState.resumed) refreshKioskStatus();
|
|
}
|
|
|
|
/// 从现有持久化服务恢复系统、用户、支付和页面偏好,并更新设备绑定状态。
|
|
Future<void> getSetting() async {
|
|
await refreshKioskStatus();
|
|
final setting = await AppSettingTool.getSysSetting();
|
|
final storedPayTypeOrderFuture = AppSettingTool.getPayTypeOrder();
|
|
final reidentifiablePayTypesFuture =
|
|
AppSettingTool.getCashierReidentifiablePayTypes();
|
|
final cashierPagePreferencesFuture =
|
|
AppSettingTool.getCashierPagePreferences();
|
|
final reservationQueryPreferencesFuture =
|
|
AppSettingTool.getReservationQueryPagePreferences();
|
|
final productMealPeriodScheduleFuture =
|
|
AppSettingTool.getProductMealPeriodSchedule();
|
|
final storedPayTypeOrder = await storedPayTypeOrderFuture;
|
|
final reidentifiablePayTypes = await reidentifiablePayTypesFuture;
|
|
final cashierPagePreferences = await cashierPagePreferencesFuture;
|
|
final reservationQueryPreferences = await reservationQueryPreferencesFuture;
|
|
final restoredProductMealPeriodSchedule =
|
|
await productMealPeriodScheduleFuture;
|
|
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);
|
|
cashierReidentifiablePayTypes.assignAll(reidentifiablePayTypes);
|
|
cashierShowHoldAction.value = cashierPagePreferences.showHoldAction;
|
|
cashierShowMoreAction.value = cashierPagePreferences.showMoreAction;
|
|
cashierAllowManualMealPeriodChange.value =
|
|
cashierPagePreferences.allowManualMealPeriodChange;
|
|
reservationQueryMethodOrder.assignAll(
|
|
reservationQueryPreferences.methodOrder,
|
|
);
|
|
reservationQueryDefaultMethod.value =
|
|
reservationQueryPreferences.defaultMethod;
|
|
reservationQueryAutoRecognitionEnabled.value =
|
|
reservationQueryPreferences.autoRecognitionEnabled;
|
|
reservationQueryContinuousRecognitionEnabled.value =
|
|
reservationQueryPreferences.continuousRecognitionEnabled;
|
|
reservationQueryNameMaskEnabled.value =
|
|
reservationQueryPreferences.nameMaskEnabled;
|
|
reservationQueryVerificationEnabled.value =
|
|
reservationQueryPreferences.verificationEnabled;
|
|
reservationQueryQuickVerificationEnabled.value =
|
|
reservationQueryPreferences.quickVerificationEnabled;
|
|
reservationQueryAllowManualMealPeriodChange.value =
|
|
reservationQueryPreferences.allowManualMealPeriodChange;
|
|
reservationQueryVerificationDateScope.value =
|
|
reservationQueryPreferences.verificationDateScope;
|
|
productMealPeriodSchedule.value = restoredProductMealPeriodSchedule;
|
|
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);
|
|
}
|
|
|
|
/// 切换页面设置当前编辑对象;不可配置的一级菜单请求会被忽略。
|
|
void selectPageSettingType(TerminalMenuItemType type) {
|
|
if (!configurablePageTypes.contains(type)) return;
|
|
pageSettingType.value = type;
|
|
}
|
|
|
|
/// 校验并保存餐段时间;空档允许保留,相邻或交叉范围由强类型模型统一处理。
|
|
Future<bool> saveProductMealPeriods(List<ProductMealPeriod> periods) async {
|
|
if (productMealPeriodSaving.value) return false;
|
|
final error = ProductMealPeriodSchedule.validate(periods);
|
|
if (error != null) {
|
|
Toast.error(msg: error);
|
|
return false;
|
|
}
|
|
productMealPeriodSaving.value = true;
|
|
try {
|
|
final schedule = ProductMealPeriodSchedule.checked(periods);
|
|
await AppSettingTool.setProductMealPeriodSchedule(schedule);
|
|
productMealPeriodSchedule.value = schedule;
|
|
Toast.success(msg: '餐段配置已保存');
|
|
return true;
|
|
} catch (e, stack) {
|
|
logger.e('|$_tag|保存餐段配置失败error:|$e|stack:|$stack|');
|
|
Toast.error(msg: '餐段配置保存失败,请重试');
|
|
return false;
|
|
} finally {
|
|
productMealPeriodSaving.value = false;
|
|
}
|
|
}
|
|
|
|
/// 调整预约查询识别方式顺序,并合并保存到最新页面偏好。
|
|
void reorderReservationQueryMethod(int oldIndex, int newIndex) {
|
|
if (oldIndex == newIndex ||
|
|
oldIndex < 0 ||
|
|
oldIndex >= reservationQueryMethodOrder.length) {
|
|
return;
|
|
}
|
|
if (newIndex > oldIndex) newIndex -= 1;
|
|
if (newIndex < 0 || newIndex >= reservationQueryMethodOrder.length) return;
|
|
final updated = reservationQueryMethodOrder.toList();
|
|
final method = updated.removeAt(oldIndex);
|
|
updated.insert(newIndex, method);
|
|
reservationQueryMethodOrder.assignAll(updated);
|
|
_scheduleReservationQueryPreferencesPersist();
|
|
}
|
|
|
|
/// 设置预约查询默认识别方式,仅接受当前列表内的方式。
|
|
void setReservationQueryDefaultMethod(
|
|
ReservationRecognitionMethod method,
|
|
) {
|
|
if (!reservationQueryMethodOrder.contains(method) ||
|
|
reservationQueryDefaultMethod.value == method) {
|
|
return;
|
|
}
|
|
reservationQueryDefaultMethod.value = method;
|
|
_scheduleReservationQueryPreferencesPersist();
|
|
}
|
|
|
|
/// 切换查询菜单自动识别模式并安排持久化,当前已打开的查询页不被强制重启。
|
|
void setReservationQueryAutoRecognitionEnabled(bool enabled) {
|
|
if (reservationQueryAutoRecognitionEnabled.value == enabled) return;
|
|
reservationQueryAutoRecognitionEnabled.value = enabled;
|
|
_scheduleReservationQueryPreferencesPersist();
|
|
}
|
|
|
|
/// 切换查询菜单持续自动识别并安排持久化,关闭后保留硬件原有单轮超时行为。
|
|
void setReservationQueryContinuousRecognitionEnabled(bool enabled) {
|
|
if (reservationQueryContinuousRecognitionEnabled.value == enabled) return;
|
|
reservationQueryContinuousRecognitionEnabled.value = enabled;
|
|
_scheduleReservationQueryPreferencesPersist();
|
|
}
|
|
|
|
/// 切换查询菜单服务端二次核销校验并安排持久化。
|
|
void setReservationQueryVerificationEnabled(bool enabled) {
|
|
if (reservationQueryVerificationEnabled.value == enabled) return;
|
|
reservationQueryVerificationEnabled.value = enabled;
|
|
// 快速核销从属于核销入口,关闭父开关时同步清理隐藏配置,避免再次进入查询页后误触发。
|
|
if (!enabled) reservationQueryQuickVerificationEnabled.value = false;
|
|
_scheduleReservationQueryPreferencesPersist();
|
|
}
|
|
|
|
/// 切换查询菜单快速核销并安排持久化,仅在核销入口已开启时接受开启操作。
|
|
void setReservationQueryQuickVerificationEnabled(bool enabled) {
|
|
final normalized = reservationQueryVerificationEnabled.value && enabled;
|
|
if (reservationQueryQuickVerificationEnabled.value == normalized) return;
|
|
reservationQueryQuickVerificationEnabled.value = normalized;
|
|
_scheduleReservationQueryPreferencesPersist();
|
|
}
|
|
|
|
/// 切换查询结果姓名脱敏并安排持久化。
|
|
void setReservationQueryNameMaskEnabled(bool enabled) {
|
|
if (reservationQueryNameMaskEnabled.value == enabled) return;
|
|
reservationQueryNameMaskEnabled.value = enabled;
|
|
_scheduleReservationQueryPreferencesPersist();
|
|
}
|
|
|
|
/// 更新预约核销日期范围并安排持久化。
|
|
void setReservationQueryVerificationDateScope(
|
|
ReservationVerificationDateScope scope,
|
|
) {
|
|
if (reservationQueryVerificationDateScope.value == scope) return;
|
|
reservationQueryVerificationDateScope.value = scope;
|
|
_scheduleReservationQueryPreferencesPersist();
|
|
}
|
|
|
|
/// 切换查询菜单是否允许主动修改餐段并安排持久化。
|
|
void setReservationQueryAllowManualMealPeriodChange(bool allowed) {
|
|
if (reservationQueryAllowManualMealPeriodChange.value == allowed) return;
|
|
reservationQueryAllowManualMealPeriodChange.value = allowed;
|
|
_scheduleReservationQueryPreferencesPersist();
|
|
}
|
|
|
|
/// 将预约查询页面偏好恢复为产品默认值并安排持久化。
|
|
void resetReservationQueryPreferences() {
|
|
reservationQueryMethodOrder.assignAll(
|
|
ReservationRecognitionMethod.values,
|
|
);
|
|
reservationQueryDefaultMethod.value = ReservationRecognitionMethod.card;
|
|
reservationQueryAutoRecognitionEnabled.value = false;
|
|
reservationQueryContinuousRecognitionEnabled.value = true;
|
|
reservationQueryNameMaskEnabled.value = true;
|
|
reservationQueryVerificationEnabled.value = false;
|
|
reservationQueryQuickVerificationEnabled.value = false;
|
|
reservationQueryAllowManualMealPeriodChange.value = true;
|
|
reservationQueryVerificationDateScope.value =
|
|
ReservationVerificationDateScope.today;
|
|
_scheduleReservationQueryPreferencesPersist();
|
|
}
|
|
|
|
/// 连续上下移动或切换默认项时串行保存,最终以界面最新状态为准。
|
|
void _scheduleReservationQueryPreferencesPersist() {
|
|
_reservationQueryPendingPersist = true;
|
|
if (!_reservationQueryPersisting) {
|
|
unawaited(_flushReservationQueryPreferences());
|
|
}
|
|
}
|
|
|
|
/// 串行写入预约查询页面偏好,保存期间的新变更会继续下一轮写入。
|
|
Future<void> _flushReservationQueryPreferences() async {
|
|
_reservationQueryPersisting = true;
|
|
while (_reservationQueryPendingPersist) {
|
|
_reservationQueryPendingPersist = false;
|
|
try {
|
|
await AppSettingTool.setReservationQueryPagePreferences(
|
|
order: reservationQueryMethodOrder.toList(),
|
|
defaultMethod: reservationQueryDefaultMethod.value,
|
|
autoRecognitionEnabled: reservationQueryAutoRecognitionEnabled.value,
|
|
continuousRecognitionEnabled:
|
|
reservationQueryContinuousRecognitionEnabled.value,
|
|
nameMaskEnabled: reservationQueryNameMaskEnabled.value,
|
|
verificationEnabled: reservationQueryVerificationEnabled.value,
|
|
quickVerificationEnabled:
|
|
reservationQueryQuickVerificationEnabled.value,
|
|
allowManualMealPeriodChange:
|
|
reservationQueryAllowManualMealPeriodChange.value,
|
|
verificationDateScope: reservationQueryVerificationDateScope.value,
|
|
);
|
|
} catch (e, stack) {
|
|
logger.e('|$_tag|保存查询菜单页面配置失败error:|$e|stack:|$stack|');
|
|
Toast.error(msg: '查询菜单设置保存失败,请重试');
|
|
}
|
|
}
|
|
_reservationQueryPersisting = false;
|
|
}
|
|
|
|
/// 从 Android 原生能力重新读取当前 Kiosk 状态。
|
|
Future<void> refreshKioskStatus() async {
|
|
kioskStatus.value = await AppRuntimeTool.refreshKioskStatus();
|
|
}
|
|
|
|
/// 立即执行真实 Root 命令并局部持久化结果;失败状态也作为有效检查结果保存。
|
|
Future<void> refreshRootAccess({bool showFeedback = true}) async {
|
|
if (rootRefreshing.value) return;
|
|
rootRefreshing.value = true;
|
|
try {
|
|
final result = await AppRuntimeTool.checkRootAccess();
|
|
await AppSettingTool.setRootAccessResult(result);
|
|
final setting = sysSetting.value;
|
|
if (setting != null) {
|
|
setting.rootAccessStatus = result.status;
|
|
setting.rootAccessCheckedAt = result.checkedAt;
|
|
setting.rootAccessDiagnostic = result.diagnostic;
|
|
sysSetting.refresh();
|
|
}
|
|
if (showFeedback) {
|
|
if (result.available) {
|
|
Toast.success(msg: 'Root 权限可用');
|
|
} else {
|
|
Toast.error(msg: result.diagnostic);
|
|
}
|
|
}
|
|
} catch (e, stack) {
|
|
logger.e('|$_tag|刷新Root状态失败error:|$e|stack:|$stack|');
|
|
if (showFeedback) Toast.error(msg: 'Root 状态检查失败,请稍后重试');
|
|
} finally {
|
|
rootRefreshing.value = false;
|
|
}
|
|
}
|
|
|
|
/// 经用户二次确认后通过 Root 配置 Device Owner;不删除账户、不移除其他 Owner 或修改系统策略文件。
|
|
Future<void> enableKioskWithRoot() async {
|
|
final setting = sysSetting.value;
|
|
if (setting == null || kioskProvisioning.value) return;
|
|
if (setting.rootAccessStatus != RootAccessStatus.available) {
|
|
kioskProvisionMessage.value = 'Root 不可用,请先刷新 Root 状态并完成授权';
|
|
return;
|
|
}
|
|
final confirmed = await appMessageDialog(
|
|
Get.context!,
|
|
'将使用 Root 把当前应用配置为 Android Device Owner,并立即进入 Kiosk。该操作不会删除账户或移除其他 Device Owner,系统拒绝时只会返回原因。是否继续?',
|
|
);
|
|
if (!confirmed) return;
|
|
|
|
kioskProvisioning.value = true;
|
|
kioskProvisionMessage.value = '';
|
|
EasyLoading.show(
|
|
status: '正在启用专用设备模式...',
|
|
maskType: EasyLoadingMaskType.black,
|
|
);
|
|
try {
|
|
final result = await AppRuntimeTool.enableKioskWithRoot();
|
|
kioskStatus.value = result.status;
|
|
if (result.success) {
|
|
kioskProvisionMessage.value = '';
|
|
Toast.success(msg: 'Kiosk 已启用并自动进入');
|
|
} else {
|
|
kioskProvisionMessage.value = result.message;
|
|
Toast.error(msg: result.message);
|
|
}
|
|
} catch (e, stack) {
|
|
logger.e('|$_tag|Root启用Kiosk失败error:|$e|stack:|$stack|');
|
|
kioskProvisionMessage.value = '启用失败,请稍后重试并检查系统 Device Owner 状态';
|
|
Toast.error(msg: kioskProvisionMessage.value);
|
|
} finally {
|
|
EasyLoading.dismiss();
|
|
kioskProvisioning.value = false;
|
|
}
|
|
}
|
|
|
|
/// 保存全局字体倍率并立即应用到运行中主题。
|
|
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 togglePayTypeReidentification(OrderPayDisplayType type) {
|
|
if (cashierReidentifiablePayTypes.contains(type)) {
|
|
cashierReidentifiablePayTypes.remove(type);
|
|
} else {
|
|
cashierReidentifiablePayTypes.add(type);
|
|
}
|
|
_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);
|
|
cashierReidentifiablePayTypes.assignAll(
|
|
AppSettingTool.defaultCashierReidentifiablePayTypes,
|
|
);
|
|
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 setCashierShowHoldAction(bool visible) {
|
|
if (cashierShowHoldAction.value == visible) return;
|
|
cashierShowHoldAction.value = visible;
|
|
_scheduleCashierPagePreferencesPersist();
|
|
}
|
|
|
|
/// 切换收银页“更多”快捷操作显隐并安排持久化。
|
|
void setCashierShowMoreAction(bool visible) {
|
|
if (cashierShowMoreAction.value == visible) return;
|
|
cashierShowMoreAction.value = visible;
|
|
_scheduleCashierPagePreferencesPersist();
|
|
}
|
|
|
|
/// 切换收银页是否允许主动修改餐段并安排持久化。
|
|
void setCashierAllowManualMealPeriodChange(bool allowed) {
|
|
if (cashierAllowManualMealPeriodChange.value == allowed) return;
|
|
cashierAllowManualMealPeriodChange.value = allowed;
|
|
_scheduleCashierPagePreferencesPersist();
|
|
}
|
|
|
|
/// 将收银页面快捷操作恢复为默认显示状态。
|
|
void resetCashierPagePreferences() {
|
|
cashierShowHoldAction.value = true;
|
|
cashierShowMoreAction.value = true;
|
|
cashierAllowManualMealPeriodChange.value = true;
|
|
_scheduleCashierPagePreferencesPersist();
|
|
}
|
|
|
|
/// 连续切换快捷操作显隐时串行保存,最终落盘界面最后一次选择。
|
|
void _scheduleCashierPagePreferencesPersist() {
|
|
_cashierPagePendingPersist = true;
|
|
if (!_cashierPagePersisting) {
|
|
unawaited(_flushCashierPagePreferences());
|
|
}
|
|
}
|
|
|
|
/// 串行写入收银页面偏好,保存期间的新变更会继续下一轮写入。
|
|
Future<void> _flushCashierPagePreferences() async {
|
|
_cashierPagePersisting = true;
|
|
while (_cashierPagePendingPersist) {
|
|
_cashierPagePendingPersist = false;
|
|
try {
|
|
await AppSettingTool.setCashierPagePreferences(
|
|
showHoldAction: cashierShowHoldAction.value,
|
|
showMoreAction: cashierShowMoreAction.value,
|
|
allowManualMealPeriodChange: cashierAllowManualMealPeriodChange.value,
|
|
);
|
|
} catch (e, stack) {
|
|
logger.e('|$_tag|保存收银页面配置失败error:|$e|stack:|$stack|');
|
|
Toast.error(msg: '收银页面设置保存失败,请重试');
|
|
}
|
|
}
|
|
_cashierPagePersisting = false;
|
|
}
|
|
|
|
/// 支付方式连续拖动或切换时串行保存,始终以最后一次界面状态为准。
|
|
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,
|
|
reidentifiableTypes: cashierReidentifiablePayTypes,
|
|
);
|
|
} 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();
|
|
sysSetting.value = await AppSettingTool.getSysSetting();
|
|
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 = '';
|
|
}
|
|
}
|
|
|
|
/// 通过旧同步入口关闭并重新初始化 MQTT 长连接。
|
|
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);
|
|
}
|
|
|
|
/// 展开或收起旧内联 Cron 编辑器,并用当前已保存规则重置草稿和错误。
|
|
void toggleScheduledTaskEditor(ScheduledTaskType type) {
|
|
expandedScheduledTask.value =
|
|
expandedScheduledTask.value == type ? null : type;
|
|
scheduledTaskCronDrafts[type] =
|
|
SyncCheckTool.instance.configs[type]?.cron ?? '';
|
|
scheduledTaskCronErrors.remove(type);
|
|
}
|
|
|
|
/// 更新指定任务的 Cron 草稿,输入变化后清除该任务旧校验错误。
|
|
void setScheduledTaskCronDraft(ScheduledTaskType type, String value) {
|
|
scheduledTaskCronDrafts[type] = value;
|
|
if (scheduledTaskCronErrors.containsKey(type)) {
|
|
scheduledTaskCronErrors.remove(type);
|
|
}
|
|
}
|
|
|
|
/// 切换任务自动调度开关;关闭运行保障任务前要求操作人员确认。
|
|
Future<void> setScheduledTaskEnabled(
|
|
ScheduledTaskType type,
|
|
bool enabled,
|
|
) async {
|
|
final config = SyncCheckTool.instance.configs[type];
|
|
if (config == null || config.enabled == enabled) return;
|
|
if (!enabled && _isRuntimeGuardTask(type)) {
|
|
final confirmed = await appMessageDialog(
|
|
Get.context!,
|
|
'关闭后终端将不再自动执行该运行保障任务,是否继续?',
|
|
);
|
|
if (!confirmed) return;
|
|
}
|
|
await _saveScheduledTaskConfig(config.copyWith(enabled: enabled));
|
|
}
|
|
|
|
/// 切换任务是否仅在完整空闲窗口自动执行,并持久化到结构化任务表。
|
|
Future<void> setScheduledTaskIdleOnly(
|
|
ScheduledTaskType type,
|
|
bool idleOnly,
|
|
) async {
|
|
final config = SyncCheckTool.instance.configs[type];
|
|
if (config == null || config.idleOnly == idleOnly) return;
|
|
await _saveScheduledTaskConfig(config.copyWith(idleOnly: idleOnly));
|
|
}
|
|
|
|
/// Cron 弹窗返回经过本地解析校验的表达式,Controller 仍在持久化边界再次校验,
|
|
/// 避免其他调用入口绕过校验写入无法注册的任务规则。
|
|
Future<void> setScheduledTaskCron(
|
|
ScheduledTaskType type,
|
|
String expression,
|
|
) async {
|
|
final config = SyncCheckTool.instance.configs[type];
|
|
if (config == null) return;
|
|
final cron = expression.trim();
|
|
if (!SyncCheckTool.isValidCron(cron)) {
|
|
Toast.error(msg: '请输入有效的 6 段 Cron 表达式');
|
|
return;
|
|
}
|
|
if (cron == config.cron) return;
|
|
await _saveScheduledTaskConfig(config.copyWith(cron: cron));
|
|
}
|
|
|
|
/// 校验并保存旧内联编辑器中的 Cron 草稿,非法值只更新对应错误状态。
|
|
Future<void> saveScheduledTaskCron(ScheduledTaskType type) async {
|
|
final config = SyncCheckTool.instance.configs[type];
|
|
if (config == null) return;
|
|
final cron = (scheduledTaskCronDrafts[type] ?? '').trim();
|
|
if (!SyncCheckTool.isValidCron(cron)) {
|
|
scheduledTaskCronErrors[type] = '请输入有效的 6 段 Cron 表达式';
|
|
return;
|
|
}
|
|
await _saveScheduledTaskConfig(config.copyWith(cron: cron));
|
|
}
|
|
|
|
/// 串联调度重建和 SQLite 保存,负责重复提交保护、草稿回退及用户反馈。
|
|
Future<void> _saveScheduledTaskConfig(ScheduledTaskConfig config) async {
|
|
if (savingScheduledTasks.contains(config.type)) return;
|
|
final previous = SyncCheckTool.instance.configs[config.type];
|
|
savingScheduledTasks.add(config.type);
|
|
try {
|
|
await SyncCheckTool.instance.updateConfig(config);
|
|
scheduledTaskCronDrafts[config.type] = config.cron;
|
|
scheduledTaskCronErrors.remove(config.type);
|
|
Toast.success(msg: '任务配置已保存');
|
|
} catch (e, stack) {
|
|
if (previous != null) {
|
|
scheduledTaskCronDrafts[config.type] = previous.cron;
|
|
}
|
|
logger
|
|
.e('|$_tag|保存定时任务失败type:${config.type.name}|error:$e|stack:$stack|');
|
|
Toast.error(msg: '任务配置保存失败,请重试');
|
|
} finally {
|
|
savingScheduledTasks.remove(config.type);
|
|
}
|
|
}
|
|
|
|
/// 手动触发指定任务,并根据立即执行、排队或合并结果向操作人员反馈。
|
|
Future<void> runScheduledTaskNow(ScheduledTaskType type) async {
|
|
final scheduler = SyncCheckTool.instance;
|
|
if (scheduler.runningTasks.contains(type) ||
|
|
scheduler.pendingTasks.contains(type)) {
|
|
await scheduler.trigger(type);
|
|
Toast.success(msg: '已合并到待执行任务');
|
|
return;
|
|
}
|
|
await scheduler.trigger(type);
|
|
if (scheduler.pendingTasks.contains(type)) {
|
|
Toast.success(msg: '任务已排队,将在安全窗口执行');
|
|
return;
|
|
}
|
|
final record = scheduler.records[type];
|
|
if (record?.success == true) {
|
|
Toast.success(msg: record?.message ?? '任务执行完成');
|
|
} else {
|
|
Toast.error(msg: record?.message ?? '任务执行失败');
|
|
}
|
|
}
|
|
|
|
/// 返回关闭自动调度前需要二次确认的 MQTT、打印、广播和设备状态保障任务。
|
|
bool _isRuntimeGuardTask(ScheduledTaskType type) => const {
|
|
ScheduledTaskType.mqReconnect,
|
|
ScheduledTaskType.printCheck,
|
|
ScheduledTaskType.broadcastCheck,
|
|
ScheduledTaskType.deviceStatus,
|
|
}.contains(type);
|
|
|
|
/// 执行旧同步动作的统一网络校验、遮罩、成功反馈和异常记录流程。
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// 手动备份本地 SQLite 数据库并刷新最近备份时间。
|
|
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 ? '已设置为主控手动输入模式' : '已恢复真实硬件模式');
|
|
}
|
|
|
|
/// 立即启停当前 Kiosk,并同步保存以后进入 App 时是否自动恢复 Lock Task。
|
|
Future<void> setKioskAutoEnterEnabled(bool enabled) async {
|
|
if (!kioskStatus.value.available || kioskSwitching.value) return;
|
|
kioskSwitching.value = true;
|
|
try {
|
|
final status = await AppRuntimeTool.setKioskAutoEnterEnabled(enabled);
|
|
kioskStatus.value = status;
|
|
final applied =
|
|
status.autoEnterEnabled == enabled && status.active == enabled;
|
|
if (!applied) {
|
|
Toast.error(msg: enabled ? 'Kiosk 未能立即开启,请检查设备策略' : 'Kiosk 未能立即关闭,请重试');
|
|
return;
|
|
}
|
|
Toast.success(msg: enabled ? 'Kiosk 已立即开启' : 'Kiosk 已立即关闭');
|
|
} finally {
|
|
kioskSwitching.value = false;
|
|
}
|
|
}
|
|
|
|
/// 管理密码验证由按钮组件负责,此处只处理原生退出结果与失败反馈。
|
|
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();
|
|
}
|
|
}
|
|
}
|