Files
biz_xfj_cloud/lib/common/widgets/card/card_common.dart
T
2026-08-06 20:55:09 +08:00

219 lines
7.9 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/index.dart';
/// 展示读写卡弹框,并由弹框自身所属的 Navigator 负责关闭路由。
///
/// [doSubmit] 只处理串口和页面状态清理,不得再弹出调用页面所在的外层路由。
Future<void> cardDialog({
required String title,
required Rx<bool> loading,
required VoidCallback doSubmit,
required Rx<ServiceOpenCard?> detail,
Rx<SerialMifareScanProgress?>? scanProgress,
VoidCallback? onScanMifareBlocks,
}) async {
await showAppDialog<void>(
context: Get.context!,
barrierDismissible: false,
canPop: false,
builder: (BuildContext dialogContext) {
final width = dialogContext.width;
final height = dialogContext.height;
final theme = FTheme.of(dialogContext);
final textStyle = theme.typography.xl;
final titleTheme = theme.tileGroupStyle.tileStyle.copyWith(
border: Border.all(
color: Colors.transparent,
),
);
return AppDialog(
title: Text(title, style: theme.typography.lg),
width: width * 0.58,
maxWidth: width * 0.58,
height: height * 0.68,
showCloseButton: false,
content: <Widget>[
Obx(
() => Visibility(
visible: scanProgress?.value == null,
replacement: _mifareScanPanel(
dialogContext,
scanProgress?.value,
),
child: Visibility(
visible: detail.value == null,
replacement: <Widget>[
FTile(
style: titleTheme,
prefixIcon: FIcon(FAssets.icons.user),
title: Text('姓名', style: textStyle),
details: Text(
"${detail.value?.username}",
style: textStyle,
),
),
FTile(
style: titleTheme,
prefixIcon: FIcon(FAssets.icons.badgeDollarSign),
title: Text('卡号', style: textStyle),
details: Text(
"${detail.value?.cardNumber}",
style: textStyle,
),
),
].toColumn(
mainAxisAlignment: MainAxisAlignment.center,
),
child: appEmpty(
dialogContext,
loading: loading.value,
loadingText: "扫描中",
emptyText: "请让用户放置饭卡",
emptyIcon: FAssets.icons.idCard,
),
),
),
).expanded(),
].toColumn(),
actions: [
<Widget>[
if (onScanMifareBlocks != null)
Obx(
() {
final progress = scanProgress?.value;
final scanning = progress != null && !progress.completed;
return FButton(
style: FButtonStyle.outline,
label: Text(
progress == null
? "扫描 M1 数据块"
: scanning
? "扫描中"
: "重新扫描",
style: theme.typography.xl,
),
// 普通读卡等待期间允许切换到诊断扫描,Controller 会先安全结束旧串口操作。
onPress: scanning ? null : onScanMifareBlocks,
);
},
).flexible(flex: 1),
FButton(
label: Text(
"确定",
style: theme.typography.xl.copyWith(
color: theme.colorScheme.secondary,
),
),
onPress: () {
// 弹框可能挂在根 Navigator 上,必须使用 builder 提供的上下文关闭自身,
// 否则设置页的嵌套 Navigator 会被误弹出并失去最后一条路由。
if (!dialogContext.mounted) return;
Navigator.of(dialogContext).pop();
doSubmit();
},
).flexible(flex: 1),
].toRowSpace(space: 12.w),
],
);
},
);
}
/// M1 扫描只展示普通数据块;第 0 块和密钥控制块在协议层已经过滤。
Widget _mifareScanPanel(
BuildContext context,
SerialMifareScanProgress? progress,
) {
final theme = FTheme.of(context);
if (progress == null) return const SizedBox.shrink();
final status = progress.completed
? progress.stopReason ??
(progress.items.isEmpty
? '扫描完成,未发现非空数据块'
: '扫描完成,发现 ${progress.items.length} 个非空数据块')
: progress.currentBlockAddress == null
? '等待识别 M1 卡'
: '正在扫描块 ${progress.currentBlockAddress}';
final capacity = progress.detectedS50 ? '按 S50 容量结束(推断)' : 'M1';
return <Widget>[
<Widget>[
Text(
'$capacity 只读扫描',
style: theme.typography.lg.copyWith(fontWeight: FontWeight.w700),
).expanded(),
Text(
'${progress.processedBlocks}/${progress.totalBlocks}',
style: theme.typography.base.copyWith(
color: XfjDesignTokens.primary,
fontWeight: FontWeight.w700,
),
),
].toRow(),
const SizedBox(height: 10),
ClipRRect(
borderRadius: BorderRadius.circular(99),
child: LinearProgressIndicator(
minHeight: 8,
value: progress.fraction,
backgroundColor: XfjDesignTokens.control,
valueColor: const AlwaysStoppedAnimation<Color>(
XfjDesignTokens.primary,
),
),
),
const SizedBox(height: 8),
Text(
'$status;可读 ${progress.readableBlocks},失败 ${progress.failedBlocks}',
style: theme.typography.sm.copyWith(color: XfjDesignTokens.muted),
),
const SizedBox(height: 4),
Text(
'使用当前读卡设置密钥,只读普通数据块;跳过块 0 和全部密钥控制块。',
style: theme.typography.sm.copyWith(color: XfjDesignTokens.muted),
),
const SizedBox(height: 12),
if (progress.items.isEmpty)
Text(
progress.completed ? '当前配置密钥下没有发现可识别的数据' : '扫描到非空块后会显示在这里',
style: theme.typography.base.copyWith(color: XfjDesignTokens.muted),
).center().expanded()
else
ListView.separated(
itemCount: progress.items.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final item = progress.items[index];
final title = item.text ?? '二进制数据';
return ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
title: Text(
'块 ${item.blockAddress} $title',
style: theme.typography.base.copyWith(
color: item.isCardNumberCandidate
? XfjDesignTokens.success
: XfjDesignTokens.ink,
fontWeight: item.isCardNumberCandidate
? FontWeight.w700
: FontWeight.w500,
),
),
subtitle: Text(
item.isCardNumberCandidate ? '疑似卡号 · ${item.hex}' : item.hex,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.typography.sm.copyWith(
color: XfjDesignTokens.muted,
),
),
);
},
).expanded(),
].toColumn(crossAxisAlignment: CrossAxisAlignment.stretch);
}