feat:键盘颜色渐变

feat:键盘长按删除
feat:查询菜单姓名加粗颜色为主题颜色
This commit is contained in:
dzc committed 2026-10-08 11:46:33 +08:00
1 parent 56531aad9d
commit fe88259cf2
11 files changed
+425 -49

No files matched your search

+1
View File
@@ -50,4 +50,5 @@ app.*.map.json
.video_agent/
.mimosa/
.v2c/
.zcode/
.zcodeignore
+2 -2
View File
@@ -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';
+141 -32
View File
@@ -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,
);
}),
@@ -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);
+71 -2
View File
@@ -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();
});
}