11
This commit is contained in:
1 parent
c4806fbaf2
commit
d1cb2273d6
17 files changed
+450
-238
No files matched your search
@@ -7,7 +7,7 @@ flutter pub run build_runner build --delete-conflicting-outputs~
|
||||
## flutter打包
|
||||
|
||||
```
|
||||
flutter build apk --build-name 2.0.3 --build-number 203
|
||||
flutter build apk --build-name 2.1.2 --build-number 212
|
||||
--build-name为版本号(展示)
|
||||
--build-number为版本号(内部比较)
|
||||
```
|
||||
|
||||
@@ -178,6 +178,38 @@ object KioskModeManager {
|
||||
return provisionResult(false, code, message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 由 Device Owner 应用自身解除设备所有者身份,使本应用恢复为普通应用、可正常卸载。
|
||||
*
|
||||
* 仅在用户主动触发后执行:先关闭自动进入并退出当前 Lock Task,再清除 Owner 身份。
|
||||
* 清除成功后 Kiosk、开机自启能力随之失效,需重新 Root 配置才能恢复。
|
||||
*/
|
||||
fun clearDeviceOwner(activity: Activity): Map<String, Any> {
|
||||
val policyManager = policyManager(activity)
|
||||
if (!policyManager.isDeviceOwnerApp(activity.packageName)) {
|
||||
return provisionResult(false, "NOT_OWNER", "当前应用不是 Device Owner,无需解除")
|
||||
}
|
||||
val autoEnterWasEnabled = isAutoEnterEnabled(activity)
|
||||
preferences(activity).edit().putBoolean(AUTO_ENTER_ENABLED_KEY, false).commit()
|
||||
exitCurrentKiosk(activity)
|
||||
return try {
|
||||
@Suppress("DEPRECATION")
|
||||
policyManager.clearDeviceOwnerApp(activity.packageName)
|
||||
if (policyManager.isDeviceOwnerApp(activity.packageName)) {
|
||||
// 厂商固件可能静默拒绝清除;回滚自动进入配置并返回明确错误。
|
||||
preferences(activity).edit()
|
||||
.putBoolean(AUTO_ENTER_ENABLED_KEY, autoEnterWasEnabled).commit()
|
||||
provisionResult(false, "OEM_REFUSED", "系统拒绝解除 Device Owner,请恢复出厂设置处理")
|
||||
} else {
|
||||
provisionResult(true, "OK", "Device Owner 已解除")
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
preferences(activity).edit()
|
||||
.putBoolean(AUTO_ENTER_ENABLED_KEY, autoEnterWasEnabled).commit()
|
||||
provisionResult(false, "CLEAR_FAILED", error.message ?: "解除 Device Owner 失败")
|
||||
}
|
||||
}
|
||||
|
||||
/** 窗口重新获得焦点时恢复 Kiosk 系统栏策略,防止临时系统界面留下可退出入口。 */
|
||||
fun reapplySystemBars(activity: Activity) {
|
||||
if (isKioskActive(activity)) hideSystemBars(activity)
|
||||
|
||||
@@ -146,6 +146,11 @@ class MainActivity : FlutterActivity() {
|
||||
}
|
||||
}
|
||||
"exitKioskToHome" -> result.success(KioskModeManager.exitToHome(this))
|
||||
"clearKioskDeviceOwner" -> result.success(
|
||||
// 清除必须先在 UI 线程退出 Lock Task,操作本身是快速本地调用。
|
||||
KioskModeManager.clearDeviceOwner(this) +
|
||||
("status" to KioskModeManager.status(this))
|
||||
)
|
||||
"validateApk" -> {
|
||||
val path = call.argument<String>("path")
|
||||
val expectedVersionCode = call.argument<Number>("expectedVersionCode")?.toLong()
|
||||
|
||||
@@ -197,6 +197,7 @@ enum ProductHourType implements BaseEnum {
|
||||
enum WalletType implements BaseEnum {
|
||||
EAT("1", "饭卡钱包"),
|
||||
PAY("2", "消费钱包"),
|
||||
TOPUP("3", "充值钱包"),
|
||||
;
|
||||
|
||||
@override
|
||||
|
||||
@@ -71,6 +71,25 @@ class FaceSyncSummary {
|
||||
}
|
||||
}
|
||||
|
||||
/// 人脸同步过程上报的最新进度快照,供手动同步界面展示进度条。
|
||||
class FaceSyncProgress {
|
||||
/// 创建进度快照;[total] 在清单分页获取阶段为空,比对处理阶段为已知总数。
|
||||
const FaceSyncProgress({required this.processed, this.total});
|
||||
|
||||
/// 已获取的清单条数或已处理的记录数,含义随阶段变化但始终单调递增。
|
||||
final int processed;
|
||||
|
||||
/// 本次需要处理的总记录数;清单仍在分页获取时尚不确定,为空。
|
||||
final int? total;
|
||||
|
||||
/// 已处理记录占比;总数未知或非法时返回空,由调用方展示不定态样式。
|
||||
double? get ratio {
|
||||
final value = total;
|
||||
if (value == null || value <= 0) return null;
|
||||
return (processed / value).clamp(0.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// V3 快照不完整、游标倒退或样本标识冲突时抛出的整次同步异常。
|
||||
class FaceSyncSnapshotException implements Exception {
|
||||
/// 使用不包含图片地址和人员信息的安全摘要创建快照异常。
|
||||
|
||||
@@ -391,8 +391,20 @@ class FaceRecognitionSessionService extends GetxService {
|
||||
}) async {
|
||||
final sessionId = _activeSessionId;
|
||||
final scene = _activeScene;
|
||||
if (sessionId == null || scene == null || !AppSubScreenTool.isAvailable) {
|
||||
return;
|
||||
if (sessionId == null || scene == null) return;
|
||||
if (!AppSubScreenTool.isAvailable) {
|
||||
// isAvailable 启动默认 false,全工程只有每分钟的设备状态定时任务会把它
|
||||
// 置 true;收银页进页直接调用插件 doubleScreenShow 并不经过本工具。若在
|
||||
// 首次定时探测前发起人脸支付,本会话全部副屏消息会被这里静默丢弃,
|
||||
// 副屏便一直停留在购物车列表。发送前按需补一次轻量探测自愈该盲区。
|
||||
await AppSubScreenTool.refreshAvailability(reportStatus: false);
|
||||
if (!AppSubScreenTool.isAvailable) {
|
||||
logger.w(
|
||||
'|人脸识别|副屏不可用,跳过副屏状态通知|sessionId:$sessionId|'
|
||||
'action:${action.code}|',
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_lastViceAction == action && _lastViceMessage == message) return;
|
||||
final revision = ++_revision;
|
||||
|
||||
@@ -462,6 +462,25 @@ abstract class AppRuntimeTool {
|
||||
}
|
||||
}
|
||||
|
||||
/// 用户确认后解除当前应用的 Device Owner 身份;解除后恢复为普通应用、可正常卸载。
|
||||
static Future<AppKioskProvisionResult> clearKioskDeviceOwner() async {
|
||||
try {
|
||||
final data = await _channel.invokeMapMethod<dynamic, dynamic>(
|
||||
'clearKioskDeviceOwner',
|
||||
);
|
||||
final cleared = AppKioskProvisionResult.fromMap(data);
|
||||
_kioskStatus = cleared.status;
|
||||
return cleared;
|
||||
} on PlatformException catch (error) {
|
||||
return AppKioskProvisionResult(
|
||||
success: false,
|
||||
code: error.code,
|
||||
message: error.message ?? '解除 Device Owner 失败,请稍后重试',
|
||||
status: _kioskStatus,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 执行带原生 5 秒超时的 `su -c id`,只有退出码为零且输出确认 uid=0 才返回可用。
|
||||
static Future<RootAccessCheckResult> checkRootAccess() async {
|
||||
try {
|
||||
|
||||
@@ -34,6 +34,9 @@ class SyncCheckTool {
|
||||
/// 最近一次终端日志上传统计,供设置页展示扫描、跳过、成功和失败数量。
|
||||
final terminalLogSummary = const TerminalLogUploadSummary().obs;
|
||||
|
||||
/// 人脸同步任务执行中的最新进度快照,任务结束后清空;仅驱动界面展示。
|
||||
final departmentFaceSyncProgress = Rxn<FaceSyncProgress>();
|
||||
|
||||
/// 当前进程内 Cron 调度器;任务全部停止后置空并关闭其内部计时资源。
|
||||
Cron? _cron;
|
||||
|
||||
@@ -728,6 +731,9 @@ class SyncCheckTool {
|
||||
}
|
||||
} finally {
|
||||
if (heavy) SyncIdleTool.instance.endHeavyTask();
|
||||
if (type == ScheduledTaskType.departmentFaceSync) {
|
||||
departmentFaceSyncProgress.value = null;
|
||||
}
|
||||
runningTasks.remove(type);
|
||||
final nextRequest = _pendingRequests[type];
|
||||
if (nextRequest != null) {
|
||||
@@ -835,6 +841,12 @@ class SyncCheckTool {
|
||||
final summary = await SyncFaceTool.sync(
|
||||
mode: mode,
|
||||
userIds: request.faceUserIds,
|
||||
onProgress: (processed, total) {
|
||||
departmentFaceSyncProgress.value = FaceSyncProgress(
|
||||
processed: processed,
|
||||
total: total,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (summary.changed > 0 &&
|
||||
request.triggerType == ScheduledTaskTriggerType.mqtt &&
|
||||
|
||||
@@ -25,9 +25,9 @@ class Constants {
|
||||
static const pwdEncKey = 'pigxpigxpigxpigx';
|
||||
|
||||
/// 服务 url 线上
|
||||
// static const apiUrl = 'https://www.pjzhhq.cn/api';
|
||||
static const apiUrl = 'https://www.pjzhhq.cn/api';
|
||||
|
||||
static const apiUrl = 'https://dzc-test-1.fy.takin.cc';
|
||||
// static const apiUrl = 'https://dzc-test-1.fy.takin.cc';
|
||||
|
||||
/// 静态资源 url
|
||||
static const ossUrl = apiUrl;
|
||||
|
||||
@@ -5,6 +5,10 @@ import 'package:forui/forui.dart';
|
||||
import 'package:pj_supermarket_cloud/common/index.dart';
|
||||
|
||||
/// 商品卡片
|
||||
///
|
||||
/// 图片用 Expanded 弹性占满剩余高度,下方文字按自然高度排列,
|
||||
/// 全局字号(typography.scale)变化时由图片伸缩吸收,不会再溢出。
|
||||
/// 要求父级提供有界高度(网格 mainAxisExtent / 显式 .height)。
|
||||
Widget orderCaneenSpuCard(
|
||||
BuildContext context, {
|
||||
String? image = "",
|
||||
@@ -12,24 +16,30 @@ Widget orderCaneenSpuCard(
|
||||
num price = 0.0,
|
||||
}) {
|
||||
final theme = FTheme.of(context);
|
||||
return FCard(
|
||||
style: theme.cardStyle.copyWith(
|
||||
contentStyle: theme.cardStyle.contentStyle.copyWith(
|
||||
padding: EdgeInsets.all(0),
|
||||
return FCard.raw(
|
||||
style: theme.cardStyle,
|
||||
child: <Widget>[
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: theme.style.borderRadius,
|
||||
child: appImg(image, width: double.infinity, height: double.infinity),
|
||||
),
|
||||
),
|
||||
),
|
||||
image: appImg(image, width: double.infinity, height: 150.h),
|
||||
subtitle: <Widget>[
|
||||
// 与 FCard 内置图文间距一致(常量 10,不缩放)
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.typography.base.copyWith(height: 1),
|
||||
),
|
||||
SizedBox(height: 5.h),
|
||||
appPrice(context, price),
|
||||
// 底部余量,边框完整包住价格
|
||||
SizedBox(height: 8.h),
|
||||
]
|
||||
.toColumn(crossAxisAlignment: CrossAxisAlignment.center)
|
||||
.width(double.infinity),
|
||||
).width(double.infinity);
|
||||
);
|
||||
}
|
||||
@@ -2902,6 +2902,12 @@ class _PersonWalletSection extends StatelessWidget {
|
||||
return wallets.first;
|
||||
}
|
||||
|
||||
/// 按后端钱包类型枚举编码解析展示名;食堂内嵌钱包没有类型编码,按饭卡展示。
|
||||
String _walletTypeName(String? walletTypeUniqueCode) {
|
||||
if (walletTypeUniqueCode == null) return WalletType.EAT.title;
|
||||
return WalletType.values.fromCode(walletTypeUniqueCode)?.title ?? '未知钱包';
|
||||
}
|
||||
|
||||
/// 构建钱包摘要和账户列表,冻结及余额不足都使用文字与颜色双重提示。
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -2928,6 +2934,7 @@ class _PersonWalletSection extends StatelessWidget {
|
||||
? <Widget>[
|
||||
_PersonWalletRow(
|
||||
key: const ValueKey('recognized-user-embedded-wallet'),
|
||||
typeName: _walletTypeName(null),
|
||||
balance: user.walletBalance,
|
||||
cardNumber: user.walletCardNumber,
|
||||
frozen: user.walletIsFreeze,
|
||||
@@ -2939,6 +2946,7 @@ class _PersonWalletSection extends StatelessWidget {
|
||||
.map(
|
||||
(wallet) => _PersonWalletRow(
|
||||
key: ValueKey('recognized-user-wallet-${wallet.id}'),
|
||||
typeName: _walletTypeName(wallet.walletTypeUniqueCode),
|
||||
balance: wallet.balance,
|
||||
cardNumber: wallet.cardNumber,
|
||||
frozen: wallet.isFreeze,
|
||||
@@ -2992,7 +3000,8 @@ class _PersonWalletSection extends StatelessWidget {
|
||||
].toRow(crossAxisAlignment: CrossAxisAlignment.end),
|
||||
SizedBox(height: 8.h),
|
||||
Text(
|
||||
'饭卡 ${_maskCardNumber(cardNumber)}',
|
||||
'${_walletTypeName(activeWallet?.walletTypeUniqueCode)}'
|
||||
' ${_maskCardNumber(cardNumber)}',
|
||||
style: theme.typography.sm.copyWith(color: XfjDesignTokens.muted),
|
||||
),
|
||||
SizedBox(height: 12.h),
|
||||
@@ -3031,6 +3040,7 @@ class _PersonWalletSection extends StatelessWidget {
|
||||
class _PersonWalletRow extends StatelessWidget {
|
||||
/// 创建一个只读或可切换的钱包账户行。
|
||||
const _PersonWalletRow({
|
||||
required this.typeName,
|
||||
required this.balance,
|
||||
required this.cardNumber,
|
||||
required this.frozen,
|
||||
@@ -3039,6 +3049,9 @@ class _PersonWalletRow extends StatelessWidget {
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// 钱包类型名称,如饭卡钱包、消费钱包、充值钱包。
|
||||
final String typeName;
|
||||
|
||||
/// 钱包余额,单位为分;为空表示接口未返回。
|
||||
final int? balance;
|
||||
|
||||
@@ -3076,13 +3089,20 @@ class _PersonWalletRow extends StatelessWidget {
|
||||
),
|
||||
SizedBox(width: 9.w),
|
||||
<Widget>[
|
||||
Text(
|
||||
_maskCardNumber(cardNumber),
|
||||
style: theme.typography.sm.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontWeight: FontWeight.w700,
|
||||
<Widget>[
|
||||
Text(
|
||||
typeName,
|
||||
style: theme.typography.sm.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Text(
|
||||
_maskCardNumber(cardNumber),
|
||||
style: theme.typography.xs.copyWith(color: XfjDesignTokens.muted),
|
||||
),
|
||||
].toRow(crossAxisAlignment: CrossAxisAlignment.center),
|
||||
Text(
|
||||
stateText,
|
||||
style: theme.typography.xs.copyWith(color: stateColor),
|
||||
|
||||
@@ -89,38 +89,35 @@ class CanteenProductPage extends GetView<CanteenProductController> {
|
||||
}
|
||||
|
||||
Widget _buildSortProduct() {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext _, BoxConstraints constraints) {
|
||||
const spacing = 10;
|
||||
const runSpacing = 10;
|
||||
double maxHeight = constraints.maxHeight;
|
||||
double maxWidth = constraints.maxWidth;
|
||||
double itemWidth = (maxWidth - spacing * 6) / 7;
|
||||
double itemHeight = (maxHeight - (runSpacing + 30) * 3) / 3;
|
||||
return Obx(
|
||||
() => GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 7,
|
||||
mainAxisSpacing: 10.w,
|
||||
crossAxisSpacing: 10.w,
|
||||
childAspectRatio: 0.8,
|
||||
),
|
||||
itemCount: controller.canteenSpuList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final product = controller.canteenSpuList[index];
|
||||
return orderCaneenSpuCard(
|
||||
context,
|
||||
image: product.image,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
)
|
||||
.width(itemWidth)
|
||||
.height(itemHeight)
|
||||
.onTap(() => controller.doProduct(product));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
final theme = FTheme.of(Get.context!);
|
||||
// 卡片高度 = 图片 150.h + 图文间距 10 + 两行名称(base) + 间距 5.h
|
||||
// + 价格行(xl) + 底部余量 8.h。字号受全局字号设置(typography.scale)
|
||||
// 影响,必须运行时从主题读取,不能用固定常数。
|
||||
final cardHeight = 150.h +
|
||||
10 +
|
||||
(theme.typography.base.fontSize ?? 16) * 2 +
|
||||
5.h +
|
||||
(theme.typography.xl.fontSize ?? 20) +
|
||||
8.h;
|
||||
return Obx(
|
||||
() => GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 7,
|
||||
mainAxisSpacing: 10.w,
|
||||
crossAxisSpacing: 10.w,
|
||||
mainAxisExtent: cardHeight,
|
||||
),
|
||||
itemCount: controller.canteenSpuList.length,
|
||||
itemBuilder: (context, index) {
|
||||
final product = controller.canteenSpuList[index];
|
||||
return orderCaneenSpuCard(
|
||||
context,
|
||||
image: product.image,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
).onTap(() => controller.doProduct(product));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ class SettingController extends GetxController {
|
||||
/// Root 一键配置 Device Owner 是否正在执行,命令期间禁止重复提交。
|
||||
final kioskProvisioning = false.obs;
|
||||
|
||||
/// 解除 Device Owner 是否正在执行,防止连点重复触发清除。
|
||||
final kioskClearing = false.obs;
|
||||
|
||||
/// Kiosk 开关是否正在同步原生 Lock Task,防止连续点击形成状态反转。
|
||||
final kioskSwitching = false.obs;
|
||||
|
||||
@@ -653,6 +656,40 @@ class SettingController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 经用户二次确认后解除 Device Owner;解除后本应用恢复为普通应用、可正常卸载。
|
||||
Future<void> clearKioskDeviceOwner() async {
|
||||
if (kioskClearing.value || !kioskStatus.value.deviceOwner) return;
|
||||
final confirmed = await appMessageDialog(
|
||||
Get.context!,
|
||||
'解除后 Kiosk 锁定与开机自启立即失效,本应用恢复为普通应用,随后即可正常卸载。是否继续?',
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
kioskClearing.value = true;
|
||||
EasyLoading.show(
|
||||
status: '正在解除设备所有者...',
|
||||
maskType: EasyLoadingMaskType.black,
|
||||
);
|
||||
try {
|
||||
final result = await AppRuntimeTool.clearKioskDeviceOwner();
|
||||
kioskStatus.value = result.status;
|
||||
if (result.success) {
|
||||
kioskProvisionMessage.value = '';
|
||||
Toast.success(msg: '已解除设备所有者,现在可以正常卸载本应用');
|
||||
} else {
|
||||
kioskProvisionMessage.value = result.message;
|
||||
Toast.error(msg: result.message);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
logger.e('|$_tag|解除DeviceOwner失败error:$e|stack:$stack|');
|
||||
kioskProvisionMessage.value = '解除失败,请稍后重试';
|
||||
Toast.error(msg: kioskProvisionMessage.value);
|
||||
} finally {
|
||||
EasyLoading.dismiss();
|
||||
kioskClearing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换主控手动硬件输入;虚拟机强制开启,开启前需操作员确认。
|
||||
Future<void> setManualHardwareInput(bool enabled) async {
|
||||
if (AppRuntimeTool.isEmulator && !enabled) {
|
||||
|
||||
@@ -160,6 +160,7 @@ class FaceManageController extends GetxController {
|
||||
///
|
||||
/// 队列会先避开支付、人脸会话和业务弹框;只有任务已经实际开始时才显示遮罩,
|
||||
/// 避免遮罩本身被空闲检测识别为业务占用而导致任务无法启动。
|
||||
/// 遮罩会跟随调度器上报的同步进度在转圈与百分比进度条之间切换。
|
||||
Future<void> asyncFace({bool isAll = false}) async {
|
||||
if (operationInProgress) return;
|
||||
final isNetwork = AppNetworkTool.isAutoNetworkMsg();
|
||||
@@ -194,11 +195,26 @@ class FaceManageController extends GetxController {
|
||||
);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
var overlayVisible = false;
|
||||
Worker? progressWorker;
|
||||
if (scheduler.runningTasks.contains(ScheduledTaskType.departmentFaceSync)) {
|
||||
overlayVisible = true;
|
||||
EasyLoading.show(
|
||||
status: '同步中...',
|
||||
maskType: EasyLoadingMaskType.black,
|
||||
_showFaceSyncProgress(null);
|
||||
var lastPaintAt = DateTime.now();
|
||||
progressWorker = ever<FaceSyncProgress?>(
|
||||
scheduler.departmentFaceSyncProgress,
|
||||
(progress) {
|
||||
if (progress == null) return;
|
||||
final finished =
|
||||
progress.total != null && progress.processed >= progress.total!;
|
||||
// 跳过等记录处理极快,逐条重绘 loading 会掉帧,按 100 毫秒节流收敛。
|
||||
if (!finished &&
|
||||
DateTime.now().difference(lastPaintAt) <
|
||||
const Duration(milliseconds: 100)) {
|
||||
return;
|
||||
}
|
||||
lastPaintAt = DateTime.now();
|
||||
_showFaceSyncProgress(progress);
|
||||
},
|
||||
);
|
||||
}
|
||||
try {
|
||||
@@ -220,12 +236,32 @@ class FaceManageController extends GetxController {
|
||||
Toast.error(msg: '同步人脸失败,请稍后重试');
|
||||
logger.e('|人脸管理|同步失败error:$e|stack:$stack|');
|
||||
} finally {
|
||||
progressWorker?.dispose();
|
||||
currentPage.value = 1;
|
||||
isSyncing.value = false;
|
||||
if (overlayVisible) EasyLoading.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/// 按最新进度刷新同步遮罩:清单分页阶段用转圈,比对阶段切为百分比进度条。
|
||||
void _showFaceSyncProgress(FaceSyncProgress? progress) {
|
||||
final ratio = progress?.ratio;
|
||||
if (ratio == null || progress == null) {
|
||||
EasyLoading.show(
|
||||
status: progress == null
|
||||
? '同步中...'
|
||||
: '正在获取人脸清单 ${progress.processed} 条',
|
||||
maskType: EasyLoadingMaskType.black,
|
||||
);
|
||||
return;
|
||||
}
|
||||
EasyLoading.showProgress(
|
||||
ratio,
|
||||
status: '人脸同步中 ${progress.processed}/${progress.total}',
|
||||
maskType: EasyLoadingMaskType.black,
|
||||
);
|
||||
}
|
||||
|
||||
/// 重新读取 Android 人脸配置并按新阈值立即重查低质量分类。
|
||||
Future<void> refreshQualityThreshold() async {
|
||||
try {
|
||||
|
||||
@@ -275,6 +275,18 @@ Widget settingBaseInfo(BuildContext context) {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (controller.kioskStatus.value.deviceOwner)
|
||||
SettingInfoRow(
|
||||
icon: FAssets.icons.shieldAlert,
|
||||
label: '解除设备所有者',
|
||||
subtitle: '解除后 Kiosk 锁定与开机自启立即失效,本应用恢复为普通应用,可正常卸载',
|
||||
trailing: SettingActionButton(
|
||||
label: controller.kioskClearing.value ? '正在解除' : '解除并允许卸载',
|
||||
danger: true,
|
||||
enabled: !controller.kioskClearing.value,
|
||||
onTap: controller.clearKioskDeviceOwner,
|
||||
),
|
||||
),
|
||||
SettingInfoRow(
|
||||
icon: FAssets.icons.keyboard,
|
||||
label: '设置为主控',
|
||||
|
||||
+182
-182
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -1,7 +1,7 @@
|
||||
name: pj_supermarket_cloud
|
||||
description: "智慧后勤-测试"
|
||||
publish_to: "none"
|
||||
version: 2.0.3+203
|
||||
version: 2.1.2+212
|
||||
|
||||
environment:
|
||||
sdk: ^3.6.0
|
||||
|
||||
Reference in new issue
Block a user