741 lines
26 KiB
Dart
741 lines
26 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:common_utils/common_utils.dart';
|
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
|
import 'package:forui/forui.dart';
|
|
import 'package:get/get.dart';
|
|
|
|
import 'package:pj_supermarket_cloud/common/index.dart';
|
|
|
|
class SettingController extends GetxController {
|
|
static const String _tag = "设置 Controller";
|
|
|
|
final appC = Get.find<AppControl>();
|
|
|
|
Rx<SysSetting?> sysSetting = Rx(null);
|
|
|
|
Rx<UserSetting?> userSetting = Rx(null);
|
|
|
|
/// 是否使用人工输入代替扫码、读卡和人脸等真实硬件输入。
|
|
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;
|
|
|
|
// 全局文字大小
|
|
var globalFontSizeC = FRadioSelectGroupController<String>(
|
|
value: SettingGlobalFontSize.xl3.code,
|
|
);
|
|
|
|
/// 支付设置的界面排序,初始顺序来自枚举并由本地持久化覆盖。
|
|
final payTypeOrder = <OrderPayDisplayType>[
|
|
...OrderPayDisplayType.values,
|
|
].obs;
|
|
|
|
/// 允许在收银弹框内重新识别人员的支付方式集合;初始化采用业务默认值,配置加载后以持久化结果为准。
|
|
final cashierReidentifiablePayTypes =
|
|
AppSettingTool.defaultCashierReidentifiablePayTypes.toSet().obs;
|
|
|
|
/// 支付偏好串行保存中;保存期间的新变更会继续下一轮写入。
|
|
bool _payPersisting = false;
|
|
|
|
/// 支付偏好存在待写入变更;连续拖动或切换开关时合并为一次落盘。
|
|
bool _payPendingPersist = false;
|
|
|
|
/// 商品餐段时间配置;餐段校验弹框和自动切换按该时间范围判定。
|
|
final productMealPeriodSchedule = ProductMealPeriodSchedule.defaults.obs;
|
|
|
|
/// 餐段配置正在保存时禁用重复提交并展示按钮状态。
|
|
final productMealPeriodSaving = false.obs;
|
|
|
|
_initData() {
|
|
update(["setting"]);
|
|
}
|
|
|
|
RxList<SettingMenu> menuList = <SettingMenu>[
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.bookCopy,
|
|
title: "基本信息",
|
|
type: SettingMenuType.baseInfo,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.databaseBackup,
|
|
title: "资源备份",
|
|
type: SettingMenuType.backup,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.refreshCw,
|
|
title: "定时任务",
|
|
type: SettingMenuType.syncSetting,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.japaneseYen,
|
|
title: "支付设置",
|
|
type: SettingMenuType.paySetting,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.users,
|
|
title: "离线购买人员",
|
|
type: SettingMenuType.offlineUserSetting,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.utensils,
|
|
title: "餐段时间配置",
|
|
type: SettingMenuType.mealPeriodSetting,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.scanFace,
|
|
title: "人脸管理",
|
|
type: SettingMenuType.faceSetting,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.settings2,
|
|
title: "人脸设置",
|
|
type: SettingMenuType.facePluginSetting,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.shoppingCart,
|
|
title: "商品管理",
|
|
type: SettingMenuType.productSetting,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.logs,
|
|
title: "日志预览",
|
|
type: SettingMenuType.logPreview,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.rocket,
|
|
title: "版本管理",
|
|
type: SettingMenuType.versionSetting,
|
|
),
|
|
].obs;
|
|
|
|
Rx<SettingMenuType> cuurentMenuType = SettingMenuType.baseInfo.obs;
|
|
|
|
/// 各任务尚未保存的 Cron 输入草稿,以任务类型隔离编辑状态。
|
|
final scheduledTaskCronDrafts = <ScheduledTaskType, String>{}.obs;
|
|
|
|
/// 各任务 Cron 草稿的本地校验错误,保存成功或继续输入后移除。
|
|
final scheduledTaskCronErrors = <ScheduledTaskType, String>{}.obs;
|
|
|
|
/// 正在持久化配置的任务集合,用于禁止同任务重复提交并局部禁用控件。
|
|
final savingScheduledTasks = <ScheduledTaskType>{}.obs;
|
|
|
|
late StreamSubscription mqUpdateListen;
|
|
|
|
@override
|
|
void onInit() async {
|
|
super.onInit();
|
|
await getSetting();
|
|
await _initScheduledTasks();
|
|
globalFontSizeC.addValueListener((v) {
|
|
final fontSizeType = SettingGlobalFontSize.values.fromCode(
|
|
v.first,
|
|
);
|
|
if (fontSizeType == null) return;
|
|
final appTheme = Get.find<AppThemeControl>();
|
|
appTheme.setSizeScalar(fontSizeType);
|
|
});
|
|
mqUpdateListen = appC.mqUpdateEventBus.on<MqtActionEnum>().listen(
|
|
(event) async {
|
|
if (event == MqtActionEnum.CONFIG) {
|
|
sysSetting.value = _orderedSysSetting(
|
|
await AppSettingTool.getSysSetting(),
|
|
);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
void onReady() {
|
|
super.onReady();
|
|
_initData();
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
super.onClose();
|
|
globalFontSizeC.dispose();
|
|
mqUpdateListen.cancel();
|
|
// 隐藏键盘
|
|
// FocusManager.instance.primaryFocus?.unfocus();
|
|
// SystemChannels.textInput.invokeMethod('TextInput.hide');
|
|
}
|
|
|
|
void doMenu(SettingMenu v) {
|
|
if (v.type == cuurentMenuType.value) {
|
|
return;
|
|
}
|
|
cuurentMenuType.value = v.type;
|
|
menuList.refresh();
|
|
}
|
|
|
|
/// 按菜单类型直接切换(供人脸设置页跳转人脸管理等场景使用)。
|
|
void openMenu(SettingMenuType type) {
|
|
cuurentMenuType.value = type;
|
|
menuList.refresh();
|
|
}
|
|
|
|
/// 启动统一调度器并用已保存的 Cron 规则初始化草稿。
|
|
Future<void> _initScheduledTasks() async {
|
|
try {
|
|
await SyncCheckTool.instance.startAll();
|
|
scheduledTaskCronDrafts.assignAll({
|
|
for (final entry in SyncCheckTool.instance.configs.entries)
|
|
entry.key: entry.value.cron,
|
|
});
|
|
} catch (e, stack) {
|
|
logger.e("|$_tag|定时任务初始化失败error:|$e|stack:|$stack|");
|
|
}
|
|
}
|
|
|
|
/// 更新指定任务的 Cron 草稿,输入变化后清除该任务旧校验错误。
|
|
void setScheduledTaskCronDraft(ScheduledTaskType type, String value) {
|
|
scheduledTaskCronDrafts[type] = value;
|
|
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));
|
|
}
|
|
|
|
/// 串联调度重建和 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);
|
|
|
|
// 同步设备信息(人脸设置页等入口使用)
|
|
void syncConfig() async {
|
|
final isNetwork = AppNetworkTool.isAutoNetworkMsg();
|
|
if (!isNetwork) return;
|
|
EasyLoading.show(
|
|
status: '同步中...',
|
|
maskType: EasyLoadingMaskType.black,
|
|
);
|
|
try {
|
|
final result = await SyncConfigTool.autoSyncSysInfo();
|
|
// result.setting 是同步后最新快照,直接刷新 Rx 才能让基本信息页显示新配置。
|
|
if (result != null) {
|
|
sysSetting.value = _orderedSysSetting(result.setting);
|
|
}
|
|
Toast.success(msg: "同步设备信息成功");
|
|
EasyLoading.dismiss();
|
|
} catch (e, stack) {
|
|
Toast.error(msg: "同步设备信息错误");
|
|
logger.e("|$_tag|同步设备信息error:|$e|stack:|$stack|");
|
|
EasyLoading.dismiss();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 备份sql
|
|
void backupsSql() async {
|
|
EasyLoading.show(
|
|
status: '备份中...',
|
|
maskType: EasyLoadingMaskType.black,
|
|
);
|
|
try {
|
|
final bool = await AppFileTool.backupSqlFile();
|
|
if (!bool) throw Error();
|
|
EasyLoading.dismiss();
|
|
Toast.success(msg: "备份中sql成功");
|
|
EasyLoading.dismiss();
|
|
} catch (e) {
|
|
Toast.error(msg: "备份中sql失败");
|
|
logger.e("|$_tag|备份中sqlerror:|$e|");
|
|
EasyLoading.dismiss();
|
|
}
|
|
}
|
|
|
|
// 备份日志
|
|
void backupsLog() async {
|
|
Toast.success(msg: "当前日志为实时备份");
|
|
}
|
|
|
|
// sql恢复
|
|
Future<void> recoverSql() async {
|
|
EasyLoading.show(
|
|
status: '恢复中...',
|
|
maskType: EasyLoadingMaskType.black,
|
|
);
|
|
try {
|
|
await AppFileTool.useLastBackDatabase();
|
|
EasyLoading.dismiss();
|
|
Toast.success(msg: "sql恢复成功");
|
|
EasyLoading.dismiss();
|
|
} catch (e) {
|
|
Toast.error(msg: "sql恢复失败");
|
|
logger.e("|$_tag|sql恢复失败error:|$e|");
|
|
EasyLoading.dismiss();
|
|
}
|
|
}
|
|
|
|
Future<void> getSetting() async {
|
|
final setting = await AppSettingTool.getSysSetting();
|
|
final storedPayTypeOrder = await AppSettingTool.getPayTypeOrder();
|
|
final reidentifiablePayTypes =
|
|
await AppSettingTool.getCashierReidentifiablePayTypes();
|
|
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);
|
|
sysSetting.value = _orderedSysSetting(setting);
|
|
productMealPeriodSchedule.value =
|
|
await AppSettingTool.getProductMealPeriodSchedule();
|
|
manualHardwareInput.value =
|
|
AppRuntimeTool.isEmulator || sysSetting.value!.manualHardwareInput;
|
|
AppRuntimeTool.applyManualHardwareInput(
|
|
sysSetting.value!.manualHardwareInput,
|
|
);
|
|
if (sysSetting.value!.globalFontSize != null) {
|
|
globalFontSizeC.value = {sysSetting.value!.globalFontSize!.code};
|
|
}
|
|
}
|
|
|
|
/// 将设置中的启用支付方式按本地排序重排;null 保持 null 由收银页按默认处理。
|
|
SysSetting _orderedSysSetting(SysSetting setting) {
|
|
if (setting.usePayDisplayType != null) {
|
|
setting.usePayDisplayType = payTypeOrder
|
|
.where(setting.usePayDisplayType!.contains)
|
|
.toList();
|
|
}
|
|
return setting;
|
|
}
|
|
|
|
/// 启用或停用支付方式,始终至少保留一种并修正失效的默认方式。
|
|
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 toggleDefaultPayType(OrderPayDisplayType type, bool value) {
|
|
if (value) {
|
|
setDefaultPayType(type);
|
|
return;
|
|
}
|
|
if (sysSetting.value?.defaultPayDisplayType == type) {
|
|
Toast.error(msg: '默认支付方式不能取消,请选择其他支付方式');
|
|
}
|
|
}
|
|
|
|
/// 支付方式连续拖动或切换时串行保存,始终以最后一次界面状态为准。
|
|
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<bool> checkIsBind() async {
|
|
final sysSetting = await AppSettingTool.getSysSetting();
|
|
final id = sysSetting.serviceMerchantId;
|
|
if (ObjectUtil.isEmpty(id)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// 校验并保存餐段时间;空档允许保留,相邻或交叉范围由强类型模型统一处理。
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// 从 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> 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 自动进入,并同步原生 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> 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();
|
|
}
|
|
}
|