feat:核销突出姓名
This commit is contained in:
1 parent
cad67b53b7
commit
22c4d4b9b9
3 files changed
+264
-37
No files matched your search
@@ -361,18 +361,10 @@ class _ReservationOrderDialogState extends State<_ReservationOrderDialog> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建用餐人、食堂、日期和餐段四项查询摘要。
|
||||
/// 构建摘要区:用餐人以蓝底主卡占据左侧大权重位,食堂、日期和餐段平分剩余宽度。
|
||||
Widget _summary(FThemeData theme) {
|
||||
final deptName = widget.result.deptName?.trim();
|
||||
final information = <({SvgAsset icon, String label, String value})>[
|
||||
(
|
||||
icon: FAssets.icons.user,
|
||||
label: '用餐人',
|
||||
value: [
|
||||
widget.result.maskedName,
|
||||
if (deptName?.isNotEmpty == true) deptName!,
|
||||
].join(' · '),
|
||||
),
|
||||
final metas = <({SvgAsset icon, String label, String value})>[
|
||||
(
|
||||
icon: FAssets.icons.store,
|
||||
label: '食堂',
|
||||
@@ -389,47 +381,124 @@ class _ReservationOrderDialogState extends State<_ReservationOrderDialog> {
|
||||
value: '${widget.meal.title} · ${widget.meal.orderIds.length} 笔预约',
|
||||
),
|
||||
];
|
||||
return information
|
||||
.map((item) => _summaryItem(theme, item))
|
||||
.toList()
|
||||
.toRowSpace(space: 10.w)
|
||||
.padding(horizontal: 24.w, vertical: 14.h)
|
||||
.decorated(color: XfjDesignTokens.canvas);
|
||||
return IntrinsicHeight(
|
||||
// 摘要行处于无界高度的弹框列内,IntrinsicHeight 让 stretch 按最高卡片对齐,
|
||||
// 避免横轴 Row 直接把无穷高度约束传给子卡片。
|
||||
child: <Widget>[
|
||||
Expanded(flex: 17, child: _dinerHero(theme, deptName)),
|
||||
...metas.expand(
|
||||
(meta) => [
|
||||
SizedBox(width: XfjDesignTokens.cardGap * 1.5),
|
||||
Expanded(flex: 10, child: _summaryMetaCard(theme, meta)),
|
||||
],
|
||||
),
|
||||
]
|
||||
.toRow(crossAxisAlignment: CrossAxisAlignment.stretch)
|
||||
.padding(horizontal: 24.w, vertical: 12.h)
|
||||
.decorated(color: XfjDesignTokens.canvas),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建单项摘要并对过长内容执行省略,避免挤压相邻信息。
|
||||
Widget _summaryItem(
|
||||
/// 用餐人主卡是摘要区唯一着色卡片:白圈大头像 + 大号姓名 + 身份确认角标,内容整体居中。
|
||||
Widget _dinerHero(FThemeData theme, String? deptName) {
|
||||
final name = widget.result.maskedName;
|
||||
final hasDept = deptName?.isNotEmpty == true;
|
||||
return Semantics(
|
||||
container: true,
|
||||
label: '用餐人 $name${hasDept ? ',$deptName' : ''}',
|
||||
child: <Widget>[
|
||||
Container(
|
||||
width: 52.w,
|
||||
height: 52.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: const BoxDecoration(
|
||||
color: XfjDesignTokens.surface,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: FIcon(
|
||||
FAssets.icons.user,
|
||||
color: XfjDesignTokens.primary,
|
||||
size: 28.sp,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.typography.xl2.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontWeight: FontWeight.w800,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
FIcon(
|
||||
FAssets.icons.circleCheck,
|
||||
color: XfjDesignTokens.success,
|
||||
size: 26.sp,
|
||||
),
|
||||
]
|
||||
.toRowSpace(
|
||||
space: 14.w,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
)
|
||||
.padding(horizontal: 16.w, vertical: 12.h)
|
||||
.decorated(
|
||||
color: XfjDesignTokens.primarySoft,
|
||||
border: Border.all(
|
||||
color: XfjDesignTokens.primary.withValues(alpha: 0.2),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 单条次要元信息沿用“图标 + 标签 + 值”的白底小卡,内容整体居中对齐。
|
||||
Widget _summaryMetaCard(
|
||||
FThemeData theme,
|
||||
({SvgAsset icon, String label, String value}) item,
|
||||
) {
|
||||
return <Widget>[
|
||||
FIcon(item.icon, color: XfjDesignTokens.primary, size: 18.sp)
|
||||
FIcon(item.icon, color: XfjDesignTokens.primary, size: 22.sp)
|
||||
.center()
|
||||
.tightSize(34.w),
|
||||
<Widget>[
|
||||
Text(
|
||||
item.label,
|
||||
style: theme.typography.xs.copyWith(color: XfjDesignTokens.muted),
|
||||
),
|
||||
Text(
|
||||
item.value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.typography.sm.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontWeight: FontWeight.w700,
|
||||
.tightSize(44.w)
|
||||
.decorated(
|
||||
color: XfjDesignTokens.primarySoft,
|
||||
borderRadius: BorderRadius.circular(10.w),
|
||||
),
|
||||
Flexible(
|
||||
child: <Widget>[
|
||||
Text(
|
||||
item.label,
|
||||
style: theme.typography.xs.copyWith(color: XfjDesignTokens.muted),
|
||||
),
|
||||
SizedBox(height: 2.h),
|
||||
Text(
|
||||
item.value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.typography.sm.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
].toColumn(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
),
|
||||
].toColumn(crossAxisAlignment: CrossAxisAlignment.start).expanded(),
|
||||
),
|
||||
]
|
||||
.toRowSpace(space: 8.w)
|
||||
.padding(horizontal: 10.w, vertical: 8.h)
|
||||
.toRowSpace(
|
||||
space: 12.w,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
)
|
||||
.padding(horizontal: 10.w, vertical: 10.h)
|
||||
.decorated(
|
||||
color: XfjDesignTokens.surface,
|
||||
border: Border.all(color: XfjDesignTokens.line),
|
||||
borderRadius: BorderRadius.circular(XfjDesignTokens.controlRadius),
|
||||
)
|
||||
.expanded();
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建包含“全部”和逐笔预约的左侧导航,核销模式额外提供复选框。
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
@@ -0,0 +1,158 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show FontLoader;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:forui/forui.dart';
|
||||
|
||||
import 'package:pj_supermarket_cloud/common/index.dart';
|
||||
import 'package:pj_supermarket_cloud/pages/terminal/menu_query/widgets/reservation_verification_dialog.dart';
|
||||
|
||||
/// 加载系统内置的中文字体,保证 Golden 预览图渲染真实文案而不是测试占位方块。
|
||||
Future<void> _loadCjkFont() async {
|
||||
final file = File('/System/Library/Fonts/Supplemental/Arial Unicode.ttf');
|
||||
final bytes = await file.readAsBytes();
|
||||
final loader = FontLoader('PreviewCJK')
|
||||
..addFont(Future.value(ByteData.view(bytes.buffer)));
|
||||
await loader.load();
|
||||
}
|
||||
|
||||
/// 以终端设计分辨率渲染确认核销弹框,输出顶部摘要区的视觉预览 Golden 图。
|
||||
void main() {
|
||||
// 字体加载是真实异步引擎回调,必须放在 testWidgets 的 fake-async 区之外,否则永远挂起。
|
||||
setUpAll(_loadCjkFont);
|
||||
|
||||
testWidgets('确认核销弹框摘要区预览', (tester) async {
|
||||
final sw = Stopwatch()..start();
|
||||
addTearDown(tester.view.reset);
|
||||
tester.view.physicalSize = const Size(1920, 1080);
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
|
||||
const result = ReservationMenuDisplayData(
|
||||
maskedName: '邓樟晨',
|
||||
deptName: '青鸟同文学校',
|
||||
merchantName: '青鸟同文',
|
||||
totalDayCount: 1,
|
||||
totalMealCount: 1,
|
||||
days: [
|
||||
ReservationMenuDayData(
|
||||
date: '2026-09-22',
|
||||
meals: [
|
||||
ReservationMenuMealData(
|
||||
hourType: 'BREAKFAST',
|
||||
title: '早餐',
|
||||
items: ['鲜肉包', '豆浆', '学生营养早餐套餐'],
|
||||
orderIds: [900912345678],
|
||||
orders: [
|
||||
ReservationMenuOrderData(
|
||||
orderId: 900912345678,
|
||||
items: [
|
||||
ReservationMenuOrderItemData(
|
||||
productId: 101,
|
||||
productName: '鲜肉包',
|
||||
productCount: 2,
|
||||
),
|
||||
ReservationMenuOrderItemData(
|
||||
productId: 102,
|
||||
productName: '学生营养早餐套餐',
|
||||
productCount: 1,
|
||||
spuType: 2,
|
||||
comboItems: [
|
||||
ReservationMenuComboItemData(
|
||||
childSpuId: 201,
|
||||
name: '小米粥',
|
||||
quantity: 1,
|
||||
price: 200,
|
||||
),
|
||||
ReservationMenuComboItemData(
|
||||
childSpuId: 202,
|
||||
name: '茶叶蛋',
|
||||
quantity: 1,
|
||||
price: 150,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
debugPrint('[preview] pumping base app');
|
||||
await tester.pumpWidget(
|
||||
ScreenUtilInit(
|
||||
designSize: const Size(1920, 1080),
|
||||
minTextAdapt: true,
|
||||
builder: (_, __) {
|
||||
return FTheme(
|
||||
// 与 app.dart 根主题同口径:Forui 预置浅色底 + Xfj 语义令牌覆盖。
|
||||
// typography 用默认构造去掉 forui 强制的 Inter 字体,
|
||||
// 让中文文本继承 MaterialApp 的 PreviewCJK 字体(forui 的 Inter 无 CJK 字形)。
|
||||
data: FThemeData.inherit(
|
||||
colorScheme: FThemes.zinc.light.colorScheme.copyWith(
|
||||
background: XfjDesignTokens.surface,
|
||||
foreground: XfjDesignTokens.ink,
|
||||
primary: XfjDesignTokens.primary,
|
||||
primaryForeground: Colors.white,
|
||||
secondary: XfjDesignTokens.surface,
|
||||
secondaryForeground: XfjDesignTokens.ink,
|
||||
muted: XfjDesignTokens.canvas,
|
||||
mutedForeground: XfjDesignTokens.muted,
|
||||
destructive: XfjDesignTokens.danger,
|
||||
destructiveForeground: Colors.white,
|
||||
error: XfjDesignTokens.danger,
|
||||
errorForeground: Colors.white,
|
||||
border: XfjDesignTokens.line,
|
||||
),
|
||||
typography: const FTypography(),
|
||||
),
|
||||
child: MaterialApp(
|
||||
theme: ThemeData(fontFamily: 'PreviewCJK'),
|
||||
localizationsDelegates: FLocalizations.localizationsDelegates,
|
||||
supportedLocales: FLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
backgroundColor: XfjDesignTokens.canvas,
|
||||
body: Builder(
|
||||
builder: (context) => Center(
|
||||
child: FButton.raw(
|
||||
onPress: () {
|
||||
reservationOrderDialog(
|
||||
context,
|
||||
result: result,
|
||||
day: result.days.first,
|
||||
meal: result.days.first.meals.first,
|
||||
verificationEnabled: true,
|
||||
);
|
||||
},
|
||||
child: const Text('打开弹框'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
debugPrint('[preview] base app pumped ${sw.elapsedMilliseconds}ms');
|
||||
|
||||
await tester.tap(find.text('打开弹框'));
|
||||
debugPrint('[preview] tapped ${sw.elapsedMilliseconds}ms');
|
||||
// 固定步进推进弹框 180ms 进场动画,避免潜在无限动画让 pumpAndSettle 挂起。
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
debugPrint('[preview] dialog settled ${sw.elapsedMilliseconds}ms');
|
||||
|
||||
await expectLater(
|
||||
find.byType(MaterialApp),
|
||||
matchesGoldenFile('goldens/reservation_verification_dialog_preview.png'),
|
||||
);
|
||||
debugPrint('[preview] golden captured ${sw.elapsedMilliseconds}ms');
|
||||
}, timeout: const Timeout(Duration(minutes: 2)));
|
||||
}
|
||||
Reference in new issue
Block a user