Files
biz_xfj_cloud/lib/common/widgets/app/terminal_device_status.dart
T
2026-07-24 20:41:17 +08:00

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,
),
),
],
),
);
}
}