243 lines
7.5 KiB
Dart
243 lines
7.5 KiB
Dart
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:forui/forui.dart';
|
|
import 'package:get/get.dart';
|
|
|
|
import 'package:pj_supermarket_cloud/common/style/xfj_design_tokens.dart';
|
|
import 'package:pj_supermarket_cloud/common/models/plugs/face_recognition_session_models.dart';
|
|
import 'package:pj_supermarket_cloud/common/services/face_recognition_session_service.dart';
|
|
import 'package:pj_supermarket_cloud/common/utils/app/app_device_status_tool.dart';
|
|
|
|
/// 顶栏统一订阅设备状态快照,避免各业务页面分别维护硬件在线标记。
|
|
class TerminalDeviceStatus extends StatelessWidget {
|
|
const TerminalDeviceStatus({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Obx(() {
|
|
final status = AppDeviceStatusTool.status.value;
|
|
final faceService = Get.isRegistered<FaceRecognitionSessionService>()
|
|
? Get.find<FaceRecognitionSessionService>()
|
|
: null;
|
|
final cameraOpen = faceService?.cameraOpen.value ?? false;
|
|
final cameraStatus =
|
|
faceService?.cameraStatus.value ?? FaceRecognitionSessionStatus.cold;
|
|
return Row(
|
|
children: [
|
|
TerminalStatusPill(
|
|
icon: FAssets.icons.printer,
|
|
label: '打印机',
|
|
active: status.printerAvailable,
|
|
showBorder: false,
|
|
),
|
|
TerminalStatusPill(
|
|
icon: FAssets.icons.scale,
|
|
label: '电子秤',
|
|
active: status.steelyardAvailable,
|
|
showBorder: false,
|
|
),
|
|
TerminalStatusPill(
|
|
icon: FAssets.icons.idCard,
|
|
label: '读卡器',
|
|
active: status.cardReaderAvailable,
|
|
showBorder: false,
|
|
),
|
|
TerminalStatusPill(
|
|
icon: FAssets.icons.monitor,
|
|
label: '副屏',
|
|
active: status.subScreenAvailable,
|
|
showBorder: false,
|
|
),
|
|
FaceCameraStatusCard(
|
|
cameraOpen: cameraOpen,
|
|
status: cameraStatus,
|
|
),
|
|
],
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
/// 摄像头既有识别中状态,也有连续客流所需的热保持状态,因此在同一行保留具体状态文字。
|
|
class FaceCameraStatusCard extends StatelessWidget {
|
|
const FaceCameraStatusCard({
|
|
super.key,
|
|
required this.cameraOpen,
|
|
required this.status,
|
|
});
|
|
|
|
final bool cameraOpen;
|
|
final FaceRecognitionSessionStatus status;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = FTheme.of(context);
|
|
final sizeScale = XfjDesignTokens.buttonSizeScale;
|
|
final itemPadding = 12.w * sizeScale;
|
|
final visual = _FaceCameraStatusVisual.resolve(
|
|
cameraOpen: cameraOpen,
|
|
status: status,
|
|
);
|
|
return Tooltip(
|
|
message: visual.description,
|
|
child: SizedBox(
|
|
key: ValueKey('terminal-camera-${visual.label}'),
|
|
height: 36.h,
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: itemPadding),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
FIcon(
|
|
cameraOpen ? FAssets.icons.camera : FAssets.icons.cameraOff,
|
|
size: 19.w,
|
|
color: XfjDesignTokens.ink,
|
|
),
|
|
SizedBox(width: 8.w),
|
|
Text(
|
|
'摄像头',
|
|
style: theme.typography.sm.copyWith(
|
|
color: XfjDesignTokens.ink,
|
|
),
|
|
),
|
|
SizedBox(width: 8.w),
|
|
Container(
|
|
width: 8.w,
|
|
height: 8.w,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: visual.accent,
|
|
),
|
|
),
|
|
SizedBox(width: 8.w),
|
|
Text(
|
|
visual.label,
|
|
style: theme.typography.xs.copyWith(
|
|
color: visual.accent,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _FaceCameraStatusVisual {
|
|
const _FaceCameraStatusVisual({
|
|
required this.label,
|
|
required this.description,
|
|
required this.accent,
|
|
});
|
|
|
|
final String label;
|
|
final String description;
|
|
final Color accent;
|
|
|
|
static _FaceCameraStatusVisual resolve({
|
|
required bool cameraOpen,
|
|
required FaceRecognitionSessionStatus status,
|
|
}) {
|
|
if (!cameraOpen) {
|
|
final failed = status == FaceRecognitionSessionStatus.failed;
|
|
return _FaceCameraStatusVisual(
|
|
label: failed ? '异常关闭' : '已关闭',
|
|
description: failed ? '摄像头或识别引擎异常,当前已释放资源' : '摄像头当前未开启',
|
|
accent: failed ? XfjDesignTokens.danger : XfjDesignTokens.muted,
|
|
);
|
|
}
|
|
if (status == FaceRecognitionSessionStatus.initializing) {
|
|
return const _FaceCameraStatusVisual(
|
|
label: '启动中',
|
|
description: '摄像头已打开,正在初始化虹软识别引擎',
|
|
accent: XfjDesignTokens.primary,
|
|
);
|
|
}
|
|
if (status == FaceRecognitionSessionStatus.detecting) {
|
|
return const _FaceCameraStatusVisual(
|
|
label: '识别中',
|
|
description: '摄像头已打开,正在实时识别人脸',
|
|
accent: XfjDesignTokens.success,
|
|
);
|
|
}
|
|
if (status == FaceRecognitionSessionStatus.failed) {
|
|
return const _FaceCameraStatusVisual(
|
|
label: '等待重试',
|
|
description: '摄像头仍保持开启,可直接重试当前人脸会话',
|
|
accent: XfjDesignTokens.warning,
|
|
);
|
|
}
|
|
if (status == FaceRecognitionSessionStatus.success) {
|
|
return const _FaceCameraStatusVisual(
|
|
label: '识别成功',
|
|
description: '摄像头已打开,正在处理本次识别结果',
|
|
accent: XfjDesignTokens.success,
|
|
);
|
|
}
|
|
return const _FaceCameraStatusVisual(
|
|
label: '热保持',
|
|
description: '摄像头已打开,连续 20 分钟无新的人脸会话后自动关闭',
|
|
accent: Color(0xFF0F766E),
|
|
);
|
|
}
|
|
}
|
|
|
|
class TerminalStatusPill extends StatelessWidget {
|
|
const TerminalStatusPill({
|
|
super.key,
|
|
required this.label,
|
|
this.icon,
|
|
this.active = true,
|
|
this.showBorder = true,
|
|
});
|
|
|
|
final String label;
|
|
final SvgAsset? icon;
|
|
final bool active;
|
|
final bool showBorder;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = FTheme.of(context);
|
|
final sizeScale = XfjDesignTokens.buttonSizeScale;
|
|
return Container(
|
|
height: 36.h,
|
|
padding: EdgeInsets.symmetric(horizontal: 12.w * sizeScale),
|
|
decoration: BoxDecoration(
|
|
color: XfjDesignTokens.surface,
|
|
border: showBorder ? Border.all(color: XfjDesignTokens.line) : null,
|
|
borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (icon != null) ...[
|
|
FIcon(icon!, size: 19.w, color: XfjDesignTokens.ink),
|
|
SizedBox(width: 8.w),
|
|
],
|
|
Container(
|
|
key: ValueKey(
|
|
'terminal-status-$label-${active ? 'online' : 'offline'}',
|
|
),
|
|
width: 8.w,
|
|
height: 8.w,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: active ? XfjDesignTokens.success : XfjDesignTokens.warning,
|
|
),
|
|
),
|
|
SizedBox(width: 8.w),
|
|
Text(
|
|
label,
|
|
style: theme.typography.sm.copyWith(
|
|
color: XfjDesignTokens.ink,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|