fix:核销bug修改

This commit is contained in:
dzc committed 2026-09-04 18:57:29 +08:00
1 parent d517f45f38
commit 88ff8988fb
8 files changed
+339 -112

No files matched your search

+8 -8
View File
@@ -161,15 +161,15 @@ abstract class HardApi {
required List<int> orderIds,
}) async {
final setting = await AppSettingTool.getSysSetting();
final hardId = setting.serviceId;
if (hardId == null || hardId.isEmpty) {
throw StateError('当前业务终端尚未绑定');
}
final res = await HttpManage().dio.post(
'/hard/hard-api/ad/reservation-menu/cancel-batch',
data: {'orderIds': orderIds},
options: dio.Options(
headers: {
if (setting.serviceId != null) 'DEVICE-ID': setting.serviceId,
},
),
);
'/hard/hard-api/ad/reservation-menu/cancel-batch',
data: {'orderIds': orderIds},
options: dio.Options(headers: {'DEVICE-ID': hardId}),
);
return res.data == true;
}
+46 -12
View File
@@ -13,17 +13,25 @@ import 'package:pj_supermarket_cloud/common/index.dart';
abstract class AppNetworkTool {
static const _tag = "网络工具类";
/// 文件中心对外读取 OSS 对象的统一网关路径。
static const _ossFilePath = '/admin/sys-file/oss/file';
static StreamSubscription<List<ConnectivityResult>>? subscription;
static bool isNetworkOssImage(String imagePath) {
return imagePath.contains("?fileName=");
final uri = Uri.tryParse(imagePath.trim());
return uri?.queryParameters.containsKey('fileName') == true;
}
// 是否是网络图片
static bool isNetworkImage(String imagePath) {
if (isNetworkOssImage(imagePath)) return true;
final networkImagePattern = RegExp(r'^(http|https)://');
return networkImagePattern.hasMatch(imagePath);
final value = imagePath.trim();
if (value.startsWith('//')) return true;
final uri = Uri.tryParse(value);
return uri != null &&
(uri.scheme == 'http' || uri.scheme == 'https') &&
uri.host.isNotEmpty;
}
// 手动判断有没有要网络 todo 不建议使用
@@ -118,10 +126,7 @@ abstract class AppNetworkTool {
/// 从网络中获取图片
static Future<imglib.Image?> loadImageFromNetwork(String url) async {
try {
String networkImage = url;
if (AppNetworkTool.isNetworkOssImage(networkImage)) {
networkImage = "${Constants.ossUrl}$networkImage";
}
final networkImage = serverImageUrl(url);
final response = await http.get(Uri.parse(networkImage));
if (response.statusCode == 200) {
final Uint8List imageData = response.bodyBytes;
@@ -140,10 +145,7 @@ abstract class AppNetworkTool {
// 从网络中下载图片
static Future<Uint8List?> downloadImage(String url) async {
String networkImage = url;
if (AppNetworkTool.isNetworkOssImage(networkImage)) {
networkImage = "${Constants.ossUrl}$networkImage";
}
final networkImage = serverImageUrl(url);
try {
final client = http.Client();
try {
@@ -166,7 +168,39 @@ abstract class AppNetworkTool {
}
}
/// 将服务端图片字段统一转换为图片组件可直接加载的绝对地址。
///
/// 完整 HTTP(S) 地址保持不变;文件中心相对地址拼到 [Constants.ossUrl]
/// 配置的部署前缀后;只有 OSS 对象名时,通过标准文件读取接口访问。
static String serverImageUrl(String src) {
final value = src.trim();
if (value.isEmpty) return value;
final sourceUri = Uri.tryParse(value);
if (sourceUri != null &&
(sourceUri.scheme == 'http' || sourceUri.scheme == 'https') &&
sourceUri.host.isNotEmpty) {
return value;
}
final baseUri = Uri.parse(Constants.ossUrl);
final origin = baseUri.origin;
final base = Constants.ossUrl.replaceFirst(RegExp(r'/+$'), '');
if (value.startsWith('//')) return '${baseUri.scheme}:$value';
if (value.startsWith('/api/')) return '$origin$value';
if (value.startsWith('api/')) return '$origin/$value';
if (value.startsWith('/admin/')) return '$base$value';
if (value.startsWith('admin/')) return '$base/$value';
final endpoint = Uri.parse('$base$_ossFilePath');
if (value.startsWith('?')) {
return endpoint.replace(query: value.substring(1)).toString();
}
return endpoint.replace(queryParameters: {'fileName': value}).toString();
}
/// 兼容既有调用名称,所有 OSS 图片地址统一委托给全局解析方法。
static String ossImage(String src) {
return "${Constants.ossUrl}$src";
return serverImageUrl(src);
}
}
@@ -600,7 +600,7 @@ class VerificationOrderDetailDrawer extends StatelessWidget {
EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: constraints.maxWidth >= 860.w ? 2 : 1,
mainAxisExtent: 224.h,
mainAxisExtent: 248.h,
crossAxisSpacing: 10.w,
mainAxisSpacing: 10.h,
),
@@ -944,13 +944,11 @@ class VerificationOrderDetailDrawer extends StatelessWidget {
);
}
/// 根据服务端地址类型补全 OSS 前缀,完整 HTTP 地址保持原样。
/// 使用全局服务端图片规则补齐文件中心地址。
String? _networkImageUrl(String? source) {
final value = source?.trim();
if (value == null || value.isEmpty) return null;
return AppNetworkTool.isNetworkImage(value)
? value
: AppNetworkTool.ossImage(value);
return AppNetworkTool.serverImageUrl(value);
}
/// 将打印类型编码转换为 PC 页面相同的中文名称。
@@ -4,8 +4,15 @@ import 'package:forui/forui.dart';
import 'package:pj_supermarket_cloud/common/index.dart';
/// 人工兑换券重打原因允许提交的最大字符数,与服务端错误摘要边界保持明显余量。
const int voucherReprintReasonMaxLength = 200;
/// 终端操作员可直接选择的兑换券重打原因,避免触屏设备输入文字。
const List<String> voucherReprintReasons = [
'首次打印未正常出纸',
'纸张破损',
'打印字迹不清',
'打印内容不完整',
'卡纸或打印机故障',
'原兑换券遗失',
];
/// 收集人工重新打印兑换券原因,取消、遮罩和系统返回均不会提交打印任务。
Future<String?> showVoucherReprintDialog(BuildContext context) {
@@ -17,28 +24,25 @@ Future<String?> showVoucherReprintDialog(BuildContext context) {
);
}
/// 兑换券重打原因弹框,由自身状态持有输入控制器直到退场动画结束。
/// 兑换券重打原因弹框,由自身状态持有单选控制器直到退场动画结束。
class _VoucherReprintDialog extends StatefulWidget {
/// 创建不可绕过必填校验的兑换券重打原因弹框。
/// 创建必须明确选择原因的兑换券重打弹框。
const _VoucherReprintDialog();
/// 创建输入状态并接管文本控制器生命周期。
/// 创建原因选择状态。
@override
State<_VoucherReprintDialog> createState() => _VoucherReprintDialogState();
}
/// 兑换券重打原因弹框的输入、校验与关闭状态。
/// 兑换券重打原因弹框的选择与关闭状态。
class _VoucherReprintDialogState extends State<_VoucherReprintDialog> {
/// 当前原因表单的校验键。
final _formKey = GlobalKey<FormState>();
/// 操作员输入的人工重打原因。
final _reasonController = TextEditingController();
/// 操作员当前选择的人工重打原因。
final _reasonController = FRadioSelectGroupController<String>();
/// 是否已经触发关闭,避免快速连续点击重复弹出路由。
bool _closing = false;
/// 释放输入控制器,避免弹框关闭后继续持有编辑资源。
/// 释放单选控制器,避免弹框关闭后继续持有监听资源。
@override
void dispose() {
_reasonController.dispose();
@@ -48,25 +52,26 @@ class _VoucherReprintDialogState extends State<_VoucherReprintDialog> {
/// 取消本次操作并返回空结果。
void _cancel() => _close();
/// 校验非空及长度后返回去除首尾空白的人工原因。
/// 返回操作员明确选中的原因;未选择时确认按钮不可用。
void _submit() {
if (_closing || _formKey.currentState?.validate() != true) return;
_close(_reasonController.text.trim());
final reasons = _reasonController.value;
if (_closing || reasons.isEmpty) return;
_close(reasons.first);
}
/// 只执行一次路由关闭,并在关闭前收起软键盘。
/// 只执行一次路由关闭。
void _close([String? result]) {
if (_closing) return;
_closing = true;
FocusManager.instance.primaryFocus?.unfocus();
Navigator.of(context).pop(result);
}
/// 构建带风险说明、必填输入和标准触控按钮的 Forui 弹框。
/// 构建带风险说明、大触控单选项和标准操作按钮的 Forui 弹框。
@override
Widget build(BuildContext context) {
final theme = FTheme.of(context);
final viewport = MediaQuery.sizeOf(context);
final actionWidth = 180.w.clamp(120.0, 180.0).toDouble();
return AppDialog(
title: Text('重新打印兑换券', style: theme.typography.xl2),
subtitle: Text(
@@ -82,42 +87,54 @@ class _VoucherReprintDialogState extends State<_VoucherReprintDialog> {
maxWidth: viewport.width * 0.52,
onClose: _cancel,
semanticLabel: '重新打印兑换券原因弹框',
content: Form(
key: _formKey,
child: FTextField(
controller: _reasonController,
autofocus: true,
label: Text('重打原因', style: theme.typography.base),
description: Text(
'原因会随打印记录上传,便于后续追溯。',
style: theme.typography.sm,
),
hint: '例如:纸张破损、字迹不清',
minLines: 2,
maxLines: 3,
validator: (value) {
final reason = value?.trim() ?? '';
if (reason.isEmpty) return '请输入重打原因';
if (reason.length > voucherReprintReasonMaxLength) {
return '重打原因不能超过$voucherReprintReasonMaxLength个字符';
}
return null;
},
content: FSelectGroup<String>(
controller: _reasonController,
label: Text('重打原因(必选)', style: theme.typography.base),
description: Text(
'请选择最符合现场情况的一项,原因会随打印记录上传。',
style: theme.typography.sm,
),
style: theme.selectGroupStyle.copyWith(
labelLayoutStyle: theme.selectGroupStyle.labelLayoutStyle.copyWith(
childPadding: EdgeInsets.symmetric(horizontal: 8.w),
),
radioStyle: theme.selectGroupStyle.radioStyle.copyWith(
padding: EdgeInsets.symmetric(vertical: 4.h),
),
),
items: voucherReprintReasons
.map(
(reason) => FSelectGroupItem.radio(
key: ValueKey('voucher-reprint-reason-$reason'),
value: reason,
semanticLabel: '重打原因:$reason',
label: SizedBox(
height: 44,
child: Align(
alignment: Alignment.centerLeft,
child: Text(reason, style: theme.typography.base),
),
),
),
)
.toList(growable: false),
).padding(vertical: 20.h),
actions: [
FButton(
style: FButtonStyle.outline,
label: Text('取消', style: theme.typography.base),
onPress: _cancel,
).width(180.w),
FButton(
label: Text(
'确认重打',
style: theme.typography.base.copyWith(color: Colors.white),
),
onPress: _submit,
).width(180.w),
).width(actionWidth),
ListenableBuilder(
listenable: _reasonController,
builder: (_, __) => FButton(
label: Text(
'确认重打',
style: theme.typography.base.copyWith(color: Colors.white),
),
onPress: _reasonController.value.isEmpty ? null : _submit,
).width(actionWidth),
),
],
);
}
+37 -37
View File
@@ -50,10 +50,10 @@ packages:
dependency: transitive
description:
name: async
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.12.0"
version: "2.11.0"
background_downloader:
dependency: "direct main"
description:
@@ -82,10 +82,10 @@ packages:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
version: "2.1.1"
build:
dependency: transitive
description:
@@ -178,10 +178,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
version: "1.3.0"
charcode:
dependency: transitive
description:
@@ -202,10 +202,10 @@ packages:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.2"
version: "1.1.1"
code_builder:
dependency: transitive
description:
@@ -218,10 +218,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.19.1"
version: "1.19.0"
common_utils:
dependency: "direct main"
description:
@@ -393,10 +393,10 @@ packages:
dependency: transitive
description:
name: fake_async
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.2"
version: "1.3.1"
ffi:
dependency: transitive
description:
@@ -709,18 +709,18 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.0.8"
version: "10.0.7"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.9"
version: "3.0.8"
leak_tracker_testing:
dependency: transitive
description:
@@ -781,10 +781,10 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.17"
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
@@ -797,10 +797,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.16.0"
version: "1.15.0"
mime:
dependency: transitive
description:
@@ -901,10 +901,10 @@ packages:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.1"
version: "1.9.0"
path_parsing:
dependency: transitive
description:
@@ -1186,10 +1186,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.1"
version: "1.10.0"
sprintf:
dependency: transitive
description:
@@ -1266,18 +1266,18 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.12.1"
version: "1.12.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
version: "2.1.2"
stream_transform:
dependency: transitive
description:
@@ -1290,10 +1290,10 @@ packages:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.1"
version: "1.3.0"
strings:
dependency: transitive
description:
@@ -1338,18 +1338,18 @@ packages:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.2"
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.4"
version: "0.7.3"
timing:
dependency: transitive
description:
@@ -1434,10 +1434,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.flutter-io.cn"
source: hosted
version: "14.3.1"
version: "14.3.0"
watcher:
dependency: transitive
description:
@@ -1503,5 +1503,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.7.0-0 <4.0.0"
dart: ">=3.6.0 <4.0.0"
flutter: ">=3.27.0"
+44
View File
@@ -0,0 +1,44 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:pj_supermarket_cloud/common/index.dart';
void main() {
final ossBase = Constants.ossUrl.replaceFirst(RegExp(r'/+$'), '');
test('服务端图片完整地址保持不变', () {
const source = 'https://cdn.example.com/evidence/face.png';
expect(AppNetworkTool.serverImageUrl(source), source);
});
test('文件中心相对地址补齐网关前缀', () {
const source = '/admin/sys-file/oss/file?fileName=evidence.png';
expect(
AppNetworkTool.serverImageUrl(source),
'$ossBase/admin/sys-file/oss/file?fileName=evidence.png',
);
});
test('已经携带 API 部署前缀的相对地址不会重复拼接', () {
const source = '/api/admin/sys-file/oss/file?fileName=evidence.png';
expect(
AppNetworkTool.serverImageUrl(source),
'${Uri.parse(Constants.ossUrl).origin}$source',
);
});
test('纯 OSS 文件名通过标准图片接口访问并正确编码', () {
const source = 'receipt/voucher 1.png';
final resolved = Uri.parse(AppNetworkTool.serverImageUrl(source));
expect(resolved.origin, Uri.parse(Constants.ossUrl).origin);
expect(
resolved.path,
'${Uri.parse(Constants.ossUrl).path.replaceFirst(RegExp(r'/+$'), '')}'
'/admin/sys-file/oss/file',
);
expect(resolved.queryParameters['fileName'], source);
});
}
@@ -1,3 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -233,6 +234,7 @@ void main() {
retryVersion: 0,
attemptCount: 1,
status: '2',
pic: 'voucher.png',
printedAt: DateTime(2026, 8, 31, 16, 41, 12),
createTime: DateTime(2026, 8, 31, 16, 41, 9),
),
@@ -240,6 +242,17 @@ void main() {
printErrorMessage: '',
evidenceStatus: OrderRecordDetailLoadStatus.success,
evidenceImages: [
ServiceOrderEvidenceImage(
id: '199',
evidenceType: OrderEvidenceType.faceRecognition.code,
evidenceRole: OrderEvidenceRole.faceReference.code,
sequenceNo: 0,
status: 'AVAILABLE',
imageUrl:
'/admin/sys-file/oss/file?fileName=face-reference.png',
hardId: '10',
capturedAt: DateTime(2026, 8, 31, 16, 40, 56),
),
ServiceOrderEvidenceImage(
id: '200',
evidenceType: OrderEvidenceType.faceRecognition.code,
@@ -268,19 +281,40 @@ void main() {
await tester.pumpAndSettle();
await tester.tap(find.text('打印与核销凭证(1)'));
await tester.pumpAndSettle();
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(find.text('打印与核销凭证'), findsOneWidget);
expect(find.text('兑换券'), findsOneWidget);
expect(find.text('重新打印兑换券'), findsOneWidget);
final printImage = tester.widget<CachedNetworkImage>(
find.byType(CachedNetworkImage),
);
final printImageUri = Uri.parse(printImage.imageUrl);
expect(
printImageUri.path,
'${Uri.parse(Constants.ossUrl).path.replaceFirst(RegExp(r'/+$'), '')}'
'/admin/sys-file/oss/file',
);
expect(printImageUri.queryParameters['fileName'], 'voucher.png');
await tester.tap(find.text('重新打印兑换券'));
expect(requestedReprint, isTrue);
await tester.tap(find.text('识别凭证(1)'));
await tester.pumpAndSettle();
await tester.tap(find.text('识别凭证(2)'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
expect(find.text('识别凭证'), findsOneWidget);
expect(find.text('人脸参考图'), findsOneWidget);
expect(find.text('现场识别图'), findsOneWidget);
expect(find.textContaining('92.34%'), findsOneWidget);
expect(find.textContaining('终端现场图片已过保留期'), findsOneWidget);
final evidenceImage = tester.widget<CachedNetworkImage>(
find.byType(CachedNetworkImage),
);
expect(
evidenceImage.imageUrl,
'${Constants.ossUrl.replaceFirst(RegExp(r'/+$'), '')}'
'/admin/sys-file/oss/file?fileName=face-reference.png',
);
expect(tester.takeException(), isNull);
});
}
@@ -0,0 +1,100 @@
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:forui/forui.dart';
import 'package:pj_supermarket_cloud/pages/terminal/verification_records/widgets/voucher_reprint_dialog.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
testWidgets('兑换券重打使用固定原因单选且不会显示文字输入框', (tester) async {
await tester.binding.setSurfaceSize(const Size(1280, 720));
addTearDown(() => tester.binding.setSurfaceSize(null));
String? selectedReason;
await tester.pumpWidget(
_testApp(
Builder(
builder: (context) => FButton(
label: const Text('打开重打'),
onPress: () async {
selectedReason = await showVoucherReprintDialog(context);
},
).width(160),
),
),
);
await tester.tap(find.text('打开重打'));
await tester.pumpAndSettle();
expect(find.byType(FTextField), findsNothing);
expect(find.byType(EditableText), findsNothing);
for (final reason in voucherReprintReasons) {
expect(find.text(reason), findsOneWidget);
}
expect(
tester.getSize(find.byType(FRadio).first).height,
greaterThanOrEqualTo(44),
);
final confirmBeforeSelection = tester.widget<FButton>(
find.widgetWithText(FButton, '确认重打'),
);
expect(confirmBeforeSelection.onPress, isNull);
const reason = '打印字迹不清';
await tester.tap(find.text(reason));
await tester.pump();
final confirmAfterSelection = tester.widget<FButton>(
find.widgetWithText(FButton, '确认重打'),
);
expect(confirmAfterSelection.onPress, isNotNull);
await tester.tap(find.text('确认重打'));
await tester.pumpAndSettle();
expect(selectedReason, reason);
expect(find.text('重新打印兑换券'), findsNothing);
expect(tester.takeException(), isNull);
});
testWidgets('取消兑换券重打不会返回原因', (tester) async {
await tester.binding.setSurfaceSize(const Size(1280, 720));
addTearDown(() => tester.binding.setSurfaceSize(null));
String? selectedReason = '未关闭';
await tester.pumpWidget(
_testApp(
Builder(
builder: (context) => FButton(
label: const Text('打开重打'),
onPress: () async {
selectedReason = await showVoucherReprintDialog(context);
},
).width(160),
),
),
);
await tester.tap(find.text('打开重打'));
await tester.pumpAndSettle();
await tester.tap(find.text('取消'));
await tester.pumpAndSettle();
expect(selectedReason, isNull);
expect(tester.takeException(), isNull);
});
}
Widget _testApp(Widget child) {
return ScreenUtilInit(
designSize: const Size(1920, 1080),
builder: (_, __) => MaterialApp(
home: FTheme(
data: FThemes.zinc.light,
child: Scaffold(body: Center(child: child)),
),
),
);
}