From 88ff8988fbe50eef7a576fa996516aecdeedb915 Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Fri, 4 Sep 2026 18:57:29 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E6=A0=B8=E9=94=80bug=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/api/hard_api.dart | 16 +-- lib/common/utils/app/app_network_tool.dart | 58 +++++++-- .../verification_order_detail_drawer.dart | 8 +- .../widgets/voucher_reprint_dialog.dart | 111 ++++++++++-------- pubspec.lock | 74 ++++++------ test/common/app_network_tool_test.dart | 44 +++++++ ...verification_order_detail_drawer_test.dart | 40 ++++++- test/common/voucher_reprint_dialog_test.dart | 100 ++++++++++++++++ 8 files changed, 339 insertions(+), 112 deletions(-) create mode 100644 test/common/app_network_tool_test.dart create mode 100644 test/common/voucher_reprint_dialog_test.dart diff --git a/lib/common/api/hard_api.dart b/lib/common/api/hard_api.dart index fd79ded..7ce60c3 100644 --- a/lib/common/api/hard_api.dart +++ b/lib/common/api/hard_api.dart @@ -161,15 +161,15 @@ abstract class HardApi { required List 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; } diff --git a/lib/common/utils/app/app_network_tool.dart b/lib/common/utils/app/app_network_tool.dart index f69adc8..48be74e 100644 --- a/lib/common/utils/app/app_network_tool.dart +++ b/lib/common/utils/app/app_network_tool.dart @@ -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>? 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 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 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); } } diff --git a/lib/pages/terminal/verification_records/widgets/verification_order_detail_drawer.dart b/lib/pages/terminal/verification_records/widgets/verification_order_detail_drawer.dart index 8dfb189..10d3038 100644 --- a/lib/pages/terminal/verification_records/widgets/verification_order_detail_drawer.dart +++ b/lib/pages/terminal/verification_records/widgets/verification_order_detail_drawer.dart @@ -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 页面相同的中文名称。 diff --git a/lib/pages/terminal/verification_records/widgets/voucher_reprint_dialog.dart b/lib/pages/terminal/verification_records/widgets/voucher_reprint_dialog.dart index d60851d..660b7fb 100644 --- a/lib/pages/terminal/verification_records/widgets/voucher_reprint_dialog.dart +++ b/lib/pages/terminal/verification_records/widgets/voucher_reprint_dialog.dart @@ -4,8 +4,15 @@ import 'package:forui/forui.dart'; import 'package:pj_supermarket_cloud/common/index.dart'; -/// 人工兑换券重打原因允许提交的最大字符数,与服务端错误摘要边界保持明显余量。 -const int voucherReprintReasonMaxLength = 200; +/// 终端操作员可直接选择的兑换券重打原因,避免触屏设备输入文字。 +const List voucherReprintReasons = [ + '首次打印未正常出纸', + '纸张破损', + '打印字迹不清', + '打印内容不完整', + '卡纸或打印机故障', + '原兑换券遗失', +]; /// 收集人工重新打印兑换券原因,取消、遮罩和系统返回均不会提交打印任务。 Future showVoucherReprintDialog(BuildContext context) { @@ -17,28 +24,25 @@ Future showVoucherReprintDialog(BuildContext context) { ); } -/// 兑换券重打原因弹框,由自身状态持有输入控制器直到退场动画结束。 +/// 兑换券重打原因弹框,由自身状态持有单选控制器直到退场动画结束。 class _VoucherReprintDialog extends StatefulWidget { - /// 创建不可绕过必填校验的兑换券重打原因弹框。 + /// 创建必须明确选择原因的兑换券重打弹框。 const _VoucherReprintDialog(); - /// 创建输入状态并接管文本控制器生命周期。 + /// 创建原因选择状态。 @override State<_VoucherReprintDialog> createState() => _VoucherReprintDialogState(); } -/// 兑换券重打原因弹框的输入、校验与关闭状态。 +/// 兑换券重打原因弹框的选择与关闭状态。 class _VoucherReprintDialogState extends State<_VoucherReprintDialog> { - /// 当前原因表单的校验键。 - final _formKey = GlobalKey(); - - /// 操作员输入的人工重打原因。 - final _reasonController = TextEditingController(); + /// 操作员当前选择的人工重打原因。 + final _reasonController = FRadioSelectGroupController(); /// 是否已经触发关闭,避免快速连续点击重复弹出路由。 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( + 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), + ), ], ); } diff --git a/pubspec.lock b/pubspec.lock index ac04131..0d9cebb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/test/common/app_network_tool_test.dart b/test/common/app_network_tool_test.dart new file mode 100644 index 0000000..6e99647 --- /dev/null +++ b/test/common/app_network_tool_test.dart @@ -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); + }); +} diff --git a/test/common/verification_order_detail_drawer_test.dart b/test/common/verification_order_detail_drawer_test.dart index 4a9d2a3..b1483e9 100644 --- a/test/common/verification_order_detail_drawer_test.dart +++ b/test/common/verification_order_detail_drawer_test.dart @@ -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( + 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( + find.byType(CachedNetworkImage), + ); + expect( + evidenceImage.imageUrl, + '${Constants.ossUrl.replaceFirst(RegExp(r'/+$'), '')}' + '/admin/sys-file/oss/file?fileName=face-reference.png', + ); expect(tester.takeException(), isNull); }); } diff --git a/test/common/voucher_reprint_dialog_test.dart b/test/common/voucher_reprint_dialog_test.dart new file mode 100644 index 0000000..c0e9847 --- /dev/null +++ b/test/common/voucher_reprint_dialog_test.dart @@ -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( + find.widgetWithText(FButton, '确认重打'), + ); + expect(confirmBeforeSelection.onPress, isNull); + + const reason = '打印字迹不清'; + await tester.tap(find.text(reason)); + await tester.pump(); + + final confirmAfterSelection = tester.widget( + 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)), + ), + ), + ); +}