219 lines
7.9 KiB
Dart
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);
|
|
}
|