This commit is contained in:
dzc committed 2026-09-09 17:45:45 +08:00
1 parent 60d9380a65
commit fe6d988fa3
46 files changed
+5738 -1013

No files matched your search

@@ -0,0 +1,70 @@
import 'dart:async';
import 'package:get/get.dart';
/// 管理设置与退出共用的临时解锁会话。
///
/// 会话只保存在当前应用进程中,并使用绝对过期时间校验,避免切页、卡顿或应用进入后台
/// 导致简单减秒计时失真。退出登录时会主动锁定,不把管理权限带到下一次登录。
class TerminalAdminSessionControl extends GetxController {
static const Duration sessionDuration = Duration(minutes: 5);
final remainingSeconds = 0.obs;
DateTime? _expiresAt;
Timer? _timer;
bool get isUnlocked {
final expiresAt = _expiresAt;
return expiresAt != null && DateTime.now().isBefore(expiresAt);
}
String get remainingLabel {
final seconds = remainingSeconds.value.clamp(
0,
sessionDuration.inSeconds,
);
final minutesPart = (seconds ~/ 60).toString().padLeft(2, '0');
final secondsPart = (seconds % 60).toString().padLeft(2, '0');
return '$minutesPart:$secondsPart';
}
void unlock() {
_expiresAt = DateTime.now().add(sessionDuration);
_syncRemainingTime();
_timer?.cancel();
_timer = Timer.periodic(
const Duration(seconds: 1),
(_) => _syncRemainingTime(),
);
}
void lock() {
_expiresAt = null;
_timer?.cancel();
_timer = null;
if (remainingSeconds.value != 0) remainingSeconds.value = 0;
}
void _syncRemainingTime() {
final expiresAt = _expiresAt;
if (expiresAt == null) {
lock();
return;
}
final milliseconds = expiresAt.difference(DateTime.now()).inMilliseconds;
if (milliseconds <= 0) {
lock();
return;
}
// 向上取整可让刚完成验证的第一帧稳定显示 05:00。
final seconds = (milliseconds / 1000).ceil();
if (remainingSeconds.value != seconds) remainingSeconds.value = seconds;
}
@override
void onClose() {
_timer?.cancel();
super.onClose();
}
}
@@ -0,0 +1,98 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:pj_supermarket_cloud/common/index.dart';
/// 读取终端顶部隐藏设备集合的方法签名,生产环境从本机 `sysJson` 恢复。
typedef HiddenTerminalDeviceStatusesLoader = Future<Set<TerminalDeviceType>>
Function();
/// 保存终端顶部隐藏设备集合的方法签名,每次接收一份不可变含义的完整快照。
typedef HiddenTerminalDeviceStatusesSaver = Future<void> Function(
Set<TerminalDeviceType> hiddenDevices,
);
/// 管理终端顶部设备状态的实时显隐,并串行持久化本机界面偏好。
class TerminalDeviceStatusControl extends GetxController {
/// 创建应用级逐项显隐控制器;可注入读写方法以隔离 Widget 测试与真实数据库。
TerminalDeviceStatusControl({
HiddenTerminalDeviceStatusesLoader? loadHiddenDevices,
HiddenTerminalDeviceStatusesSaver? saveHiddenDevices,
}) : _loadHiddenDevices =
loadHiddenDevices ?? AppSettingTool.getHiddenTerminalDeviceStatuses,
_saveHiddenDevices =
saveHiddenDevices ?? AppSettingTool.setHiddenTerminalDeviceStatuses;
/// 顶部需要隐藏的设备集合;默认空集合保证首次启动和旧总开关升级后五项全部显示。
final hiddenHeaderDevices = <TerminalDeviceType>{}.obs;
/// 从本机设置读取初始隐藏集合,测试可注入内存实现。
final HiddenTerminalDeviceStatusesLoader _loadHiddenDevices;
/// 将最新隐藏集合写入本机设置,测试可注入内存实现。
final HiddenTerminalDeviceStatusesSaver _saveHiddenDevices;
/// 保存期间是否又发生显隐切换,用于在当前写入后继续保存最终状态。
bool _pendingPersist = false;
/// 当前串行保存任务;为空表示没有设置写入正在执行。
Future<void>? _persistFuture;
/// 是否至少有一个设备需要显示;调用方应在 `Obx` 内读取以响应逐项开关变化。
bool get hasVisibleHeaderDevices =>
hiddenHeaderDevices.length < TerminalDeviceType.values.length;
/// 判断指定设备是否显示在顶部;不在隐藏集合中的新增设备默认显示。
bool isHeaderDeviceVisible(TerminalDeviceType type) =>
!hiddenHeaderDevices.contains(type);
/// 启动时恢复本机隐藏集合;读取失败时清空集合,避免升级后丢失设备告警入口。
Future<void> initAsync() async {
try {
hiddenHeaderDevices.assignAll(await _loadHiddenDevices());
} catch (error, stack) {
hiddenHeaderDevices.clear();
logger.e('|终端设备状态|恢复顶部显隐失败error:|$error|stack:|$stack|');
}
}
/// 立即切换指定设备并串行保存完整集合;连续操作时返回覆盖全部待写入状态的同一任务。
Future<void> setHeaderDeviceVisible(
TerminalDeviceType type,
bool visible,
) {
if (isHeaderDeviceVisible(type) != visible) {
if (visible) {
hiddenHeaderDevices.remove(type);
} else {
hiddenHeaderDevices.add(type);
}
_pendingPersist = true;
}
final running = _persistFuture;
if (running != null) return running;
if (!_pendingPersist) return Future<void>.value();
late final Future<void> future;
future = _flushPersist().whenComplete(() {
if (identical(_persistFuture, future)) _persistFuture = null;
});
_persistFuture = future;
return future;
}
/// 逐次写入保存期间产生的最新快照,保证较慢的旧写入不会覆盖最终选择。
Future<void> _flushPersist() async {
while (_pendingPersist) {
_pendingPersist = false;
final hiddenDevices = Set<TerminalDeviceType>.from(hiddenHeaderDevices);
try {
await _saveHiddenDevices(hiddenDevices);
} catch (error, stack) {
logger.e('|终端设备状态|保存顶部显隐失败error:|$error|stack:|$stack|');
Toast.error(msg: '设备状态显示设置保存失败,请重试');
}
}
}
}
@@ -0,0 +1,43 @@
import 'package:get/get.dart';
/// 执行一次终端唤醒身份校验,返回是否允许解除睡眠遮罩。
typedef TerminalWakeVerifier = Future<bool> Function();
/// 管理主屏睡眠遮罩及唤醒验证的应用级状态。
///
/// 睡眠仅阻止主屏查看和触摸,不切换路由、不销毁页面 Controller,也不暂停串口、
/// 人脸、网络请求或副屏通信。状态仅保存在当前进程,设备重新启动后不自动进入睡眠。
class TerminalSleepControl extends GetxController {
/// 创建一份默认处于唤醒状态的主屏睡眠控制器。
TerminalSleepControl();
/// 主屏是否正在展示全屏睡眠遮罩。
final isSleeping = false.obs;
/// 是否已有唤醒密码校验正在进行,防止快速点击叠加多个密码弹窗。
bool _wakeVerificationInProgress = false;
/// 立即进入睡眠状态;重复调用保持幂等且不影响当前业务页面生命周期。
void enterSleep() {
if (!isSleeping.value) isSleeping.value = true;
}
/// 通过调用方提供的密码校验请求唤醒主屏。
///
/// 校验取消、失败或重复提交时继续保持睡眠;校验过程抛出的异常会向上返回,
/// 由界面层统一记录并提示,同时保证下一次唤醒仍可重新发起。
Future<bool> requestWake(TerminalWakeVerifier verifier) async {
if (!isSleeping.value) return true;
if (_wakeVerificationInProgress) return false;
_wakeVerificationInProgress = true;
try {
final verified = await verifier();
if (!verified) return false;
isSleeping.value = false;
return true;
} finally {
_wakeVerificationInProgress = false;
}
}
}