feat:键盘颜色渐变
feat:键盘长按删除 feat:查询菜单姓名加粗颜色为主题颜色
This commit is contained in:
1 parent
56531aad9d
commit
fe88259cf2
11 files changed
+425
-49
No files matched your search
@@ -50,4 +50,5 @@ app.*.map.json
|
||||
.video_agent/
|
||||
.mimosa/
|
||||
.v2c/
|
||||
.zcode/
|
||||
.zcodeignore
|
||||
@@ -15,8 +15,8 @@ class Constants {
|
||||
static const pwdEncKey = 'pigxpigxpigxpigx';
|
||||
|
||||
/// 服务 url 线上
|
||||
// static const apiUrl = 'https://dzc-test-1.fy.takin.cc';
|
||||
static const apiUrl = 'https://www.zianxin.com/api';
|
||||
static const apiUrl = 'https://dzc-test-1.fy.takin.cc';
|
||||
// static const apiUrl = 'https://www.zianxin.com/api';
|
||||
|
||||
// static const apiUrl = 'https://dzc-test-1.fy.takin.cc';
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:forui/forui.dart';
|
||||
@@ -249,10 +251,13 @@ class _AppSoftKeyboardPanelState extends State<AppSoftKeyboardPanel> {
|
||||
),
|
||||
),
|
||||
),
|
||||
key(
|
||||
'退格删除一位',
|
||||
// 退格是全键盘唯一的长按键:点按删一位,长按连发持续删除。
|
||||
_Key(
|
||||
semanticLabel: '退格删除,长按连续删除',
|
||||
onTap: _backspace,
|
||||
content: FIcon(
|
||||
onLongPressRepeat: _backspace,
|
||||
width: keyWidth,
|
||||
child: FIcon(
|
||||
FAssets.icons.delete,
|
||||
color: XfjDesignTokens.muted,
|
||||
size: 22.sp,
|
||||
@@ -351,15 +356,17 @@ class _AppSoftKeyboardPanelState extends State<AppSoftKeyboardPanel> {
|
||||
content: Text('空格', style: actionStyle),
|
||||
onTap: () => _insert(' '),
|
||||
),
|
||||
actionKey(
|
||||
'退格删除一位',
|
||||
// 退格是全键盘唯一的长按键:点按删一位,长按连发持续删除。
|
||||
_Key(
|
||||
semanticLabel: '退格删除,长按连续删除',
|
||||
onTap: _backspace,
|
||||
onLongPressRepeat: _backspace,
|
||||
width: avail * 2 / 9.5,
|
||||
content: FIcon(
|
||||
child: FIcon(
|
||||
FAssets.icons.delete,
|
||||
color: XfjDesignTokens.muted,
|
||||
size: 22.sp,
|
||||
),
|
||||
onTap: _backspace,
|
||||
),
|
||||
if (widget.onCloseRequested != null)
|
||||
actionKey(
|
||||
@@ -433,9 +440,11 @@ class _AppSoftKeyboardPanelState extends State<AppSoftKeyboardPanel> {
|
||||
width: avail * 2 / 5,
|
||||
child: Text('空格', style: actionStyle),
|
||||
),
|
||||
// 退格是全键盘唯一的长按键:点按删一位,长按连发持续删除。
|
||||
_Key(
|
||||
semanticLabel: '退格删除一位',
|
||||
semanticLabel: '退格删除,长按连续删除',
|
||||
onTap: _backspace,
|
||||
onLongPressRepeat: _backspace,
|
||||
width: avail * 1.5 / 5,
|
||||
child: FIcon(
|
||||
FAssets.icons.delete,
|
||||
@@ -626,9 +635,10 @@ class _SoftKeyboardDialogState extends State<_SoftKeyboardDialog> {
|
||||
/// 单个键盘按键;固定宽度布局(弹层处于无界高度约束,禁用 Expanded),
|
||||
/// 主操作键使用主题色底白字。
|
||||
///
|
||||
/// 按压反馈走“整键缩放”而非涟漪:Listener 直接监听指针按下/抬起,只触发
|
||||
/// 本键的一次 Transform 动画,不参与手势竞技场、不引起布局重排,是触摸
|
||||
/// 反馈里开销最低的实现;透明 Material 上涟漪本就不可见。
|
||||
/// 按压反馈走“整键缩放 + 主题色渐变”而非涟漪:Listener 直接监听指针按下/
|
||||
/// 抬起,只触发本键的一次 Transform 与 decoration 隐式动画,不参与手势
|
||||
/// 竞技场、不引起布局重排,是触摸反馈里开销最低的实现;透明 Material 上
|
||||
/// 涟漪本就不可见。
|
||||
class _Key extends StatefulWidget {
|
||||
const _Key({
|
||||
required this.semanticLabel,
|
||||
@@ -637,6 +647,7 @@ class _Key extends StatefulWidget {
|
||||
required this.child,
|
||||
this.emphasized = false,
|
||||
this.height,
|
||||
this.onLongPressRepeat,
|
||||
});
|
||||
|
||||
final String semanticLabel;
|
||||
@@ -654,6 +665,10 @@ class _Key extends StatefulWidget {
|
||||
/// 按键高度;默认 62。
|
||||
final double? height;
|
||||
|
||||
/// 长按连发回调;非空时按住超过系统长按阈值后先触发一次,随后周期连发。
|
||||
/// 整个键盘目前只有退格键使用(长按持续删除)。
|
||||
final VoidCallback? onLongPressRepeat;
|
||||
|
||||
@override
|
||||
State<_Key> createState() => _KeyState();
|
||||
}
|
||||
@@ -662,49 +677,143 @@ class _KeyState extends State<_Key> {
|
||||
/// 按压目标缩放:模拟实体键帽下沉,幅度保证一眼可辨。
|
||||
static const double _pressedScale = 0.92;
|
||||
|
||||
/// 当前是否有指针按在本键上;驱动按压缩放。
|
||||
/// 缩放与渐变共用同一时长曲线,让按压表现为一次同步手势。
|
||||
static const Duration _pressDuration = Duration(milliseconds: 90);
|
||||
|
||||
/// 长按连发周期:10 次/秒,短按删一位、长按可控地连续清空。
|
||||
static const Duration _longPressRepeatInterval = Duration(milliseconds: 100);
|
||||
|
||||
/// 普通键按压渐变:品牌浅蓝自上而下过渡到主题蓝浅调。
|
||||
static final LinearGradient _normalPressedGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
XfjDesignTokens.primarySoft,
|
||||
Color.alphaBlend(
|
||||
XfjDesignTokens.primary.withValues(alpha: 0.18),
|
||||
XfjDesignTokens.surface,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
/// 主操作键按压渐变:主题蓝自上而下加深一档,替代抬起的纯色底。
|
||||
static final LinearGradient _emphasizedPressedGradient = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
XfjDesignTokens.primary,
|
||||
Color.alphaBlend(
|
||||
Colors.black.withValues(alpha: 0.14),
|
||||
XfjDesignTokens.primary,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
/// 当前是否有指针按在本键上;驱动按压缩放与主题色渐变。
|
||||
bool _pressed = false;
|
||||
|
||||
/// 长按连发定时器;只在按住期间存在。
|
||||
Timer? _repeatTimer;
|
||||
|
||||
void _setPressed(bool value) {
|
||||
if (!mounted || _pressed == value) return;
|
||||
setState(() => _pressed = value);
|
||||
}
|
||||
|
||||
/// 长按阈值到达:先删一次,再以固定周期连发,直到指针抬起或取消。
|
||||
void _startLongPressRepeat() {
|
||||
widget.onLongPressRepeat?.call();
|
||||
_repeatTimer?.cancel();
|
||||
_repeatTimer = Timer.periodic(_longPressRepeatInterval, (_) {
|
||||
if (!mounted) return;
|
||||
widget.onLongPressRepeat?.call();
|
||||
});
|
||||
}
|
||||
|
||||
void _stopLongPressRepeat() {
|
||||
_repeatTimer?.cancel();
|
||||
_repeatTimer = null;
|
||||
}
|
||||
|
||||
/// 指针结束的统一收尾:松开按压视觉并停掉连发。
|
||||
void _pointerEnd() {
|
||||
_setPressed(false);
|
||||
_stopLongPressRepeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopLongPressRepeat();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final keyHeight = widget.height ?? 62.h;
|
||||
final radius = BorderRadius.circular(XfjDesignTokens.controlRadius);
|
||||
final BoxDecoration decoration;
|
||||
if (_pressed) {
|
||||
decoration = BoxDecoration(
|
||||
gradient: widget.emphasized
|
||||
? _emphasizedPressedGradient
|
||||
: _normalPressedGradient,
|
||||
border: Border.all(
|
||||
color: widget.emphasized
|
||||
? XfjDesignTokens.primary
|
||||
: XfjDesignTokens.primary.withValues(alpha: 0.45),
|
||||
),
|
||||
borderRadius: radius,
|
||||
);
|
||||
} else {
|
||||
decoration = BoxDecoration(
|
||||
color: widget.emphasized
|
||||
? XfjDesignTokens.primary
|
||||
: XfjDesignTokens.surface,
|
||||
border: Border.all(
|
||||
color: widget.emphasized
|
||||
? XfjDesignTokens.primary
|
||||
: XfjDesignTokens.line,
|
||||
),
|
||||
borderRadius: radius,
|
||||
);
|
||||
}
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: widget.semanticLabel,
|
||||
child: Listener(
|
||||
onPointerDown: (_) => _setPressed(true),
|
||||
onPointerUp: (_) => _setPressed(false),
|
||||
onPointerCancel: (_) => _setPressed(false),
|
||||
onPointerUp: (_) => _pointerEnd(),
|
||||
onPointerCancel: (_) => _pointerEnd(),
|
||||
child: AnimatedScale(
|
||||
scale: _pressed ? _pressedScale : 1,
|
||||
duration: const Duration(milliseconds: 90),
|
||||
duration: _pressDuration,
|
||||
curve: Curves.easeOut,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: widget.onTap,
|
||||
borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius),
|
||||
child: (widget.child ?? const SizedBox.shrink())
|
||||
.center()
|
||||
.tight(width: widget.width, height: keyHeight)
|
||||
.decorated(
|
||||
color: widget.emphasized
|
||||
? XfjDesignTokens.primary
|
||||
: XfjDesignTokens.surface,
|
||||
border: Border.all(
|
||||
color: widget.emphasized
|
||||
? XfjDesignTokens.primary
|
||||
: XfjDesignTokens.line,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(
|
||||
XfjDesignTokens.controlRadius,
|
||||
),
|
||||
),
|
||||
onLongPress: widget.onLongPressRepeat == null
|
||||
? null
|
||||
: _startLongPressRepeat,
|
||||
borderRadius: radius,
|
||||
// 渐变走 AnimatedContainer 隐式动画:只重绘本键 decoration,
|
||||
// 不触发布局,与外层缩放共同构成零重排的按压反馈。
|
||||
// 尺寸放在外层 SizedBox、容器内部用恒定 infinity 撑满:
|
||||
// 面板切换会按位置复用 _Key 元素,键宽变化若参与隐式动画,
|
||||
// 行布局会在过渡帧溢出。
|
||||
child: SizedBox(
|
||||
width: widget.width,
|
||||
height: keyHeight,
|
||||
child: AnimatedContainer(
|
||||
duration: _pressDuration,
|
||||
curve: Curves.easeOut,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
alignment: Alignment.center,
|
||||
decoration: decoration,
|
||||
child: widget.child ?? const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -501,7 +501,7 @@ class _ReservationOrderDialogState extends State<_ReservationOrderDialog> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 用餐人主卡是摘要区唯一着色卡片:白圈大头像 + 大号姓名 + 身份确认角标,内容整体居中。
|
||||
/// 用餐人主卡是摘要区唯一着色卡片:白圈大头像 + 主题色大号姓名,内容整体居中。
|
||||
Widget _dinerHero(FThemeData theme, String? deptName) {
|
||||
final name = widget.result.maskedName;
|
||||
final hasDept = deptName?.isNotEmpty == true;
|
||||
@@ -528,18 +528,13 @@ class _ReservationOrderDialogState extends State<_ReservationOrderDialog> {
|
||||
name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.typography.xl2.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
style: theme.typography.xl3.copyWith(
|
||||
color: XfjDesignTokens.primary,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
FIcon(
|
||||
FAssets.icons.circleCheck,
|
||||
color: XfjDesignTokens.success,
|
||||
size: 26.sp,
|
||||
),
|
||||
]
|
||||
.toRowSpace(
|
||||
space: 14.w,
|
||||
|
||||
@@ -77,6 +77,9 @@ class VerificationRecordsController extends GetxController {
|
||||
/// 是否正在提交兑换券重打,期间禁用重复点击。
|
||||
final isReprintingVoucher = false.obs;
|
||||
|
||||
/// 是否正在提交兑换券失败重试,期间禁用重复点击。
|
||||
final isRetryingVoucher = false.obs;
|
||||
|
||||
/// 当前订单最新一条兑换券任务;列表由服务端按创建时间倒序返回。
|
||||
ServicePrintRecord? get latestVoucherRecord {
|
||||
for (final record in selectedPrintRecords) {
|
||||
@@ -88,6 +91,9 @@ class VerificationRecordsController extends GetxController {
|
||||
/// 只有最新兑换券已经完成打印时才能创建下一打印序号。
|
||||
bool get canReprintVoucher => latestVoucherRecord?.status == '2';
|
||||
|
||||
/// 最新兑换券任务失败时允许人工重试,复用同一任务而不新增打印序号。
|
||||
bool get canRetryVoucher => latestVoucherRecord?.status == '3';
|
||||
|
||||
List<ServiceVerificationRecord> get visibleRecords {
|
||||
final keyword = keywordController.text.trim().toLowerCase();
|
||||
if (keyword.isEmpty) return records;
|
||||
@@ -301,6 +307,40 @@ class VerificationRecordsController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 手动重试失败的兑换券打印任务,服务端复用同一任务重新下发,打印序号不变。
|
||||
Future<void> retryFailedVoucher() async {
|
||||
final record = selectedRecord.value;
|
||||
final voucher = latestVoucherRecord;
|
||||
if (record == null || voucher == null || isRetryingVoucher.value) return;
|
||||
if (voucher.status != '3') {
|
||||
Toast.error(msg: '最新兑换券不是失败状态,暂不能重试');
|
||||
return;
|
||||
}
|
||||
if (voucher.failureStage == 'BINDING') {
|
||||
// 服务端未绑定打印机时重试必然原样失败,直接提示避免无效往返。
|
||||
Toast.error(msg: '来源终端未绑定打印机,请先完成打印机绑定');
|
||||
return;
|
||||
}
|
||||
isRetryingVoucher.value = true;
|
||||
try {
|
||||
final accepted = await HardApi.createPrinterTask(
|
||||
orderId: '${record.orderId}',
|
||||
printType: '2',
|
||||
);
|
||||
if (!accepted) throw StateError('服务端未接受重试任务');
|
||||
await _loadSelectedPrintRecords(record);
|
||||
// MQTT 是实时通道,HTTP 补拉用于覆盖消息延迟或重连窗口,二者继续由任务 ID 幂等去重。
|
||||
await SyncPrintTool.pullPendingTasks();
|
||||
Toast.success(msg: '打印重试任务已提交');
|
||||
} catch (error, stack) {
|
||||
logger.e(
|
||||
'|核销记录|兑换券重试失败|orderId:${record.orderId}|task:${voucher.id}|error:$error|stack:$stack|');
|
||||
Toast.error(msg: '打印重试提交失败,请检查网络后重试');
|
||||
} finally {
|
||||
isRetryingVoucher.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 关闭完整详情抽屉并取消其未完成请求的 UI 更新。
|
||||
void closeOrderDetail() {
|
||||
_selectedDetailRequestId++;
|
||||
@@ -318,6 +358,7 @@ class VerificationRecordsController extends GetxController {
|
||||
selectedEvidenceError.value = '';
|
||||
selectedEvidenceStatus.value = OrderRecordDetailLoadStatus.success;
|
||||
isReprintingVoucher.value = false;
|
||||
isRetryingVoucher.value = false;
|
||||
}
|
||||
|
||||
/// 按当前订单和 DEVICE-ID 查询服务端打印记录,失败不影响订单与识别凭证展示。
|
||||
|
||||
@@ -67,10 +67,13 @@ class VerificationRecordsPage extends GetView<VerificationRecordsController> {
|
||||
evidenceErrorMessage: controller.selectedEvidenceError.value,
|
||||
canReprintVoucher: controller.canReprintVoucher,
|
||||
isReprintingVoucher: controller.isReprintingVoucher.value,
|
||||
canRetryVoucher: controller.canRetryVoucher,
|
||||
isRetryingVoucher: controller.isRetryingVoucher.value,
|
||||
onRetry: controller.retryOrderDetail,
|
||||
onRefreshPrintRecords: controller.refreshPrintRecords,
|
||||
onRefreshEvidence: controller.refreshEvidence,
|
||||
onReprintVoucher: () => _requestVoucherReprint(context),
|
||||
onRetryVoucher: controller.retryFailedVoucher,
|
||||
onClose: controller.closeOrderDetail,
|
||||
);
|
||||
}),
|
||||
|
||||
+22
-4
@@ -24,10 +24,13 @@ class VerificationOrderDetailDrawer extends StatelessWidget {
|
||||
required this.evidenceErrorMessage,
|
||||
required this.canReprintVoucher,
|
||||
required this.isReprintingVoucher,
|
||||
required this.canRetryVoucher,
|
||||
required this.isRetryingVoucher,
|
||||
required this.onRetry,
|
||||
required this.onRefreshPrintRecords,
|
||||
required this.onRefreshEvidence,
|
||||
required this.onReprintVoucher,
|
||||
required this.onRetryVoucher,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
@@ -70,6 +73,12 @@ class VerificationOrderDetailDrawer extends StatelessWidget {
|
||||
/// 是否正在向服务端提交兑换券重打任务。
|
||||
final bool isReprintingVoucher;
|
||||
|
||||
/// 最新兑换券任务处于失败状态,允许人工重试同一任务。
|
||||
final bool canRetryVoucher;
|
||||
|
||||
/// 是否正在向服务端提交兑换券失败重试。
|
||||
final bool isRetryingVoucher;
|
||||
|
||||
/// 重新请求订单和购物车数据。
|
||||
final VoidCallback onRetry;
|
||||
|
||||
@@ -82,6 +91,9 @@ class VerificationOrderDetailDrawer extends StatelessWidget {
|
||||
/// 打开重打原因弹框并提交最新已打印兑换券。
|
||||
final VoidCallback onReprintVoucher;
|
||||
|
||||
/// 重试失败的最新兑换券任务,服务端复用同一任务且不新增打印序号。
|
||||
final VoidCallback onRetryVoucher;
|
||||
|
||||
/// 关闭右侧详情抽屉。
|
||||
final VoidCallback onClose;
|
||||
|
||||
@@ -362,6 +374,10 @@ class VerificationOrderDetailDrawer extends StatelessWidget {
|
||||
|
||||
/// 构建打印凭证独立视图,加载、失败和空数据均不影响订单概览。
|
||||
Widget _printRecords(BuildContext context, FThemeData theme) {
|
||||
// 失败态优先重试:按钮语义随最新兑换券任务状态切换,其余状态维持重打按钮。
|
||||
final voucherBusy = isRetryingVoucher || isReprintingVoucher;
|
||||
final voucherActionEnabled = !voucherBusy && (canRetryVoucher || canReprintVoucher);
|
||||
final voucherAction = canRetryVoucher ? onRetryVoucher : onReprintVoucher;
|
||||
return ColoredBox(
|
||||
color: XfjDesignTokens.canvas,
|
||||
child: Column(
|
||||
@@ -381,12 +397,14 @@ class VerificationOrderDetailDrawer extends StatelessWidget {
|
||||
: onRefreshPrintRecords,
|
||||
),
|
||||
TerminalToolbarButton(
|
||||
label: isReprintingVoucher ? '提交中...' : '重新打印兑换券',
|
||||
label: voucherBusy
|
||||
? '提交中...'
|
||||
: canRetryVoucher
|
||||
? '重试失败打印'
|
||||
: '重新打印兑换券',
|
||||
icon: FAssets.icons.printer,
|
||||
minWidth: 180.w,
|
||||
onTap: canReprintVoucher && !isReprintingVoucher
|
||||
? onReprintVoucher
|
||||
: null,
|
||||
onTap: voucherActionEnabled ? voucherAction : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -177,10 +177,13 @@ void main() {
|
||||
evidenceErrorMessage: '',
|
||||
canReprintVoucher: false,
|
||||
isReprintingVoucher: false,
|
||||
canRetryVoucher: false,
|
||||
isRetryingVoucher: false,
|
||||
onRetry: () {},
|
||||
onRefreshPrintRecords: () {},
|
||||
onRefreshEvidence: () {},
|
||||
onReprintVoucher: () {},
|
||||
onRetryVoucher: () {},
|
||||
onClose: () {},
|
||||
),
|
||||
],
|
||||
@@ -268,10 +271,13 @@ void main() {
|
||||
evidenceErrorMessage: '',
|
||||
canReprintVoucher: true,
|
||||
isReprintingVoucher: false,
|
||||
canRetryVoucher: false,
|
||||
isRetryingVoucher: false,
|
||||
onRetry: () {},
|
||||
onRefreshPrintRecords: () {},
|
||||
onRefreshEvidence: () {},
|
||||
onReprintVoucher: () => requestedReprint = true,
|
||||
onRetryVoucher: () {},
|
||||
onClose: () {},
|
||||
),
|
||||
],
|
||||
@@ -317,6 +323,131 @@ void main() {
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('最新兑换券失败时打印按钮切换为重试失败打印', (tester) async {
|
||||
tester.view.physicalSize = const Size(1920, 1080);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
var requestedRetry = false;
|
||||
var requestedReprint = false;
|
||||
|
||||
await tester.pumpWidget(
|
||||
_terminalApp(
|
||||
Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
const ColoredBox(color: XfjDesignTokens.canvas),
|
||||
VerificationOrderDetailDrawer(
|
||||
record: _record(),
|
||||
status: OrderRecordDetailLoadStatus.success,
|
||||
order: _order(),
|
||||
items: _items(),
|
||||
errorMessage: '',
|
||||
printStatus: OrderRecordDetailLoadStatus.success,
|
||||
printRecords: [
|
||||
ServicePrintRecord(
|
||||
id: '100',
|
||||
orderId: '2094302933091917826',
|
||||
hardId: '10',
|
||||
hardName: '一号食堂终端',
|
||||
printerHardId: '20',
|
||||
printerHardName: '一号打印机',
|
||||
printType: '2',
|
||||
printNo: 1,
|
||||
retryVersion: 0,
|
||||
attemptCount: 0,
|
||||
status: '3',
|
||||
failureStage: 'MQTT_DISPATCH',
|
||||
lastError: '【503】 during【POST】 to [http://pjcloud-mqtt/send-print]',
|
||||
createTime: DateTime(2026, 8, 31, 16, 41, 9),
|
||||
),
|
||||
],
|
||||
printErrorMessage: '',
|
||||
evidenceStatus: OrderRecordDetailLoadStatus.success,
|
||||
evidenceImages: const [],
|
||||
evidenceErrorMessage: '',
|
||||
canReprintVoucher: false,
|
||||
isReprintingVoucher: false,
|
||||
canRetryVoucher: true,
|
||||
isRetryingVoucher: false,
|
||||
onRetry: () {},
|
||||
onRefreshPrintRecords: () {},
|
||||
onRefreshEvidence: () {},
|
||||
onReprintVoucher: () => requestedReprint = true,
|
||||
onRetryVoucher: () => requestedRetry = true,
|
||||
onClose: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('打印与核销凭证(1)'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
expect(find.text('打印失败'), findsOneWidget);
|
||||
expect(find.text('失败阶段:MQTT 下发'), findsOneWidget);
|
||||
expect(find.text('重试失败打印'), findsOneWidget);
|
||||
expect(find.text('重新打印兑换券'), findsNothing);
|
||||
await tester.tap(find.text('重试失败打印'));
|
||||
await tester.pump();
|
||||
expect(requestedRetry, isTrue);
|
||||
expect(requestedReprint, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('兑换券重试提交期间按钮禁用并显示提交中', (tester) async {
|
||||
tester.view.physicalSize = const Size(1920, 1080);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
var requestedRetry = false;
|
||||
|
||||
await tester.pumpWidget(
|
||||
_terminalApp(
|
||||
Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
const ColoredBox(color: XfjDesignTokens.canvas),
|
||||
VerificationOrderDetailDrawer(
|
||||
record: _record(),
|
||||
status: OrderRecordDetailLoadStatus.success,
|
||||
order: _order(),
|
||||
items: _items(),
|
||||
errorMessage: '',
|
||||
printStatus: OrderRecordDetailLoadStatus.success,
|
||||
printRecords: const [],
|
||||
printErrorMessage: '',
|
||||
evidenceStatus: OrderRecordDetailLoadStatus.success,
|
||||
evidenceImages: const [],
|
||||
evidenceErrorMessage: '',
|
||||
canReprintVoucher: false,
|
||||
isReprintingVoucher: false,
|
||||
canRetryVoucher: true,
|
||||
isRetryingVoucher: true,
|
||||
onRetry: () {},
|
||||
onRefreshPrintRecords: () {},
|
||||
onRefreshEvidence: () {},
|
||||
onReprintVoucher: () {},
|
||||
onRetryVoucher: () => requestedRetry = true,
|
||||
onClose: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('打印与核销凭证(0)'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
expect(find.text('提交中...'), findsOneWidget);
|
||||
expect(find.text('重试失败打印'), findsNothing);
|
||||
await tester.tap(find.text('提交中...'));
|
||||
await tester.pump();
|
||||
expect(requestedRetry, isFalse);
|
||||
});
|
||||
}
|
||||
|
||||
/// 跳过远程加载,让完整页面测试只验证根布局和既有静态数据渲染。
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 97 KiB |
@@ -188,7 +188,16 @@ void main() {
|
||||
expect(find.text('确认核销'), findsNWidgets(2));
|
||||
expect(find.text('可核销'), findsOneWidget);
|
||||
expect(find.text('单品'), findsNWidgets(2));
|
||||
expect(find.text('张*明 · 三年级二班'), findsOneWidget);
|
||||
expect(find.text('张*明'), findsOneWidget);
|
||||
// 部门信息自“核销突出姓名”改版后不再单独成文,仅在用餐人主卡的无障碍标签中保留。
|
||||
final semanticsHandle = tester.ensureSemantics();
|
||||
// 开启语义后需要一帧让已渲染的树生成语义节点;子节点文案会并入主卡标签,用包含匹配。
|
||||
await tester.pump();
|
||||
expect(
|
||||
find.bySemanticsLabel(RegExp('用餐人 张\\*明,三年级二班')),
|
||||
findsOneWidget,
|
||||
);
|
||||
semanticsHandle.dispose();
|
||||
expect(find.text('第一食堂'), findsOneWidget);
|
||||
expect(find.text('2026-07-27 周一'), findsOneWidget);
|
||||
expect(find.text('小青菜'), findsOneWidget);
|
||||
|
||||
@@ -300,18 +300,87 @@ void main() {
|
||||
.ancestor(of: find.text('5'), matching: find.byType(AnimatedScale))
|
||||
.first;
|
||||
expect(tester.widget<AnimatedScale>(keyScale).scale, 1.0);
|
||||
// 抬起态为纯色底,无渐变。
|
||||
final keyContainer = find
|
||||
.ancestor(of: find.text('5'), matching: find.byType(AnimatedContainer))
|
||||
.first;
|
||||
var keyDecoration =
|
||||
tester.widget<AnimatedContainer>(keyContainer).decoration
|
||||
as BoxDecoration;
|
||||
expect(keyDecoration.gradient, isNull);
|
||||
|
||||
// 指针按下未抬起:键帽立即进入按压缩放态。
|
||||
// 指针按下未抬起:键帽立即进入按压缩放态并铺上主题色渐变。
|
||||
final gesture = await tester.startGesture(
|
||||
tester.getCenter(find.text('5')),
|
||||
);
|
||||
await tester.pump();
|
||||
expect(tester.widget<AnimatedScale>(keyScale).scale, closeTo(0.92, 0.001));
|
||||
keyDecoration = tester.widget<AnimatedContainer>(keyContainer).decoration
|
||||
as BoxDecoration;
|
||||
expect(keyDecoration.gradient, isA<LinearGradient>());
|
||||
|
||||
// 抬起后键帽复原并完成一次输入。
|
||||
// 抬起后键帽复原、渐变退回纯色并完成一次输入。
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.widget<AnimatedScale>(keyScale).scale, 1.0);
|
||||
keyDecoration = tester.widget<AnimatedContainer>(keyContainer).decoration
|
||||
as BoxDecoration;
|
||||
expect(keyDecoration.gradient, isNull);
|
||||
expect(controller.text, '5');
|
||||
});
|
||||
|
||||
testWidgets('长按退格键持续删除,抬起即停', (tester) async {
|
||||
final semanticsHandle = tester.ensureSemantics();
|
||||
|
||||
final controller = TextEditingController(text: '12345678');
|
||||
tester.view.physicalSize = const Size(1920, 1080);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
addTearDown(tester.view.reset);
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrapHost(
|
||||
(context) => Scaffold(
|
||||
body: Center(
|
||||
child: FButton(
|
||||
onPress: () => AppSoftKeyboard.show(
|
||||
context,
|
||||
controller: controller,
|
||||
mode: AppSoftKeyboardMode.digits,
|
||||
),
|
||||
label: const Text('打开键盘'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('打开键盘'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final deleteFinder = find.bySemanticsLabel('退格删除,长按连续删除');
|
||||
final gesture = await tester.startGesture(
|
||||
tester.getCenter(deleteFinder),
|
||||
);
|
||||
|
||||
// 长按阈值(500ms)到达时触发第一次删除,连发尚未开始。
|
||||
await tester.pump(const Duration(milliseconds: 550));
|
||||
expect(controller.text, '1234567');
|
||||
|
||||
// 连发周期 100ms:推进 340ms 触发 600/700/800 三个周期。
|
||||
await tester.pump(const Duration(milliseconds: 340));
|
||||
expect(controller.text, '1234');
|
||||
|
||||
// 抬起后连发立即停止,文本不再变化。
|
||||
await gesture.up();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(controller.text, '1234');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 单击退格仍然只删除一位。
|
||||
await tester.tap(deleteFinder);
|
||||
await tester.pump();
|
||||
expect(controller.text, '123');
|
||||
|
||||
semanticsHandle.dispose();
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user