feat:收银页面副屏改大
This commit is contained in:
1 parent
72486eb7ac
commit
aff30bf45d
4 files changed
+210
-95
No files matched your search
@@ -74,6 +74,7 @@ class CustomerDisplayProduct {
|
||||
required this.quantityLabel,
|
||||
required this.unitPrice,
|
||||
required this.totalPrice,
|
||||
this.imageSource,
|
||||
this.isCombo = false,
|
||||
this.comboItems = const [],
|
||||
});
|
||||
@@ -90,6 +91,9 @@ class CustomerDisplayProduct {
|
||||
/// 当前购物车条目的结算小计,单位为分。
|
||||
final int totalPrice;
|
||||
|
||||
/// 商品图片来源,兼容完整网络地址和服务端返回的 OSS 相对地址;为空时展示名称首字占位。
|
||||
final String? imageSource;
|
||||
|
||||
/// 是否为食堂套餐;旧版副屏消息缺失该字段时为 false。
|
||||
final bool isCombo;
|
||||
|
||||
@@ -104,6 +108,7 @@ class CustomerDisplayProduct {
|
||||
other.quantityLabel == quantityLabel &&
|
||||
other.unitPrice == unitPrice &&
|
||||
other.totalPrice == totalPrice &&
|
||||
other.imageSource == imageSource &&
|
||||
other.isCombo == isCombo &&
|
||||
listEquals(other.comboItems, comboItems);
|
||||
}
|
||||
@@ -115,6 +120,7 @@ class CustomerDisplayProduct {
|
||||
quantityLabel,
|
||||
unitPrice,
|
||||
totalPrice,
|
||||
imageSource,
|
||||
isCombo,
|
||||
Object.hashAll(comboItems),
|
||||
);
|
||||
|
||||
@@ -168,13 +168,16 @@ class CustomerDisplayCoordinator extends ChangeNotifier {
|
||||
final unitPrice = _nonNegativeInt(data['productPrice']);
|
||||
final totalPrice = _nonNegativeInt(data['totalPrice']);
|
||||
final nature = data['nature'];
|
||||
final rawImageSource = data['productPic'];
|
||||
if (name is! String ||
|
||||
name.trim().isEmpty ||
|
||||
unitPrice == null ||
|
||||
totalPrice == null ||
|
||||
nature is! String) {
|
||||
nature is! String ||
|
||||
(rawImageSource != null && rawImageSource is! String)) {
|
||||
return null;
|
||||
}
|
||||
final trimmedImageSource = (rawImageSource as String?)?.trim();
|
||||
|
||||
String quantityLabel;
|
||||
if (nature == 'WEIGHT') {
|
||||
@@ -207,6 +210,8 @@ class CustomerDisplayCoordinator extends ChangeNotifier {
|
||||
quantityLabel: quantityLabel,
|
||||
unitPrice: unitPrice,
|
||||
totalPrice: totalPrice,
|
||||
imageSource:
|
||||
trimmedImageSource?.isEmpty == true ? null : trimmedImageSource,
|
||||
// 套餐组成非空时兼容主屏仅透传明细但缺失标识的过渡消息。
|
||||
isCombo: rawIsCombo == true || comboItems.isNotEmpty,
|
||||
comboItems: List.unmodifiable(comboItems),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import 'package:pj_supermarket_cloud/common/style/xfj_design_tokens.dart';
|
||||
import 'package:pj_supermarket_cloud/common/values/constants.dart';
|
||||
import 'package:pj_supermarket_cloud/sub_screen/common/models/customer_display_models.dart';
|
||||
import 'package:pj_supermarket_cloud/sub_screen/common/utils/customer_display_coordinator.dart';
|
||||
import 'package:pj_supermarket_cloud/sub_screen/page/customer_welcome.dart';
|
||||
@@ -143,8 +144,8 @@ class _Header extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 76 * scale,
|
||||
padding: EdgeInsets.symmetric(horizontal: 22 * scale),
|
||||
height: 82 * scale,
|
||||
padding: EdgeInsets.symmetric(horizontal: 18 * scale),
|
||||
decoration: _surfaceDecoration(radius: 16 * scale),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -160,33 +161,21 @@ class _Header extends StatelessWidget {
|
||||
'Z',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24 * scale,
|
||||
fontSize: _fontSize(28, scale),
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 14 * scale),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
state.businessTitle,
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontSize: 22 * scale,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'顾客显示屏',
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.muted,
|
||||
fontSize: 14 * scale,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(width: 10 * scale),
|
||||
Text(
|
||||
state.businessTitle,
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontSize: _fontSize(28, scale),
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (state.maskedCustomerName != null) ...[
|
||||
@@ -230,8 +219,8 @@ class _HeaderPill extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(minHeight: 44 * scale),
|
||||
padding: EdgeInsets.symmetric(horizontal: 14 * scale),
|
||||
constraints: BoxConstraints(minHeight: 42 * scale),
|
||||
padding: EdgeInsets.symmetric(horizontal: 10 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.08),
|
||||
border: Border.all(color: color.withValues(alpha: 0.22)),
|
||||
@@ -239,13 +228,14 @@ class _HeaderPill extends StatelessWidget {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 20 * scale),
|
||||
SizedBox(width: 8 * scale),
|
||||
Icon(icon, color: color, size: 22 * scale),
|
||||
SizedBox(width: 5 * scale),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 15 * scale,
|
||||
fontSize: _fontSize(18, scale),
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
@@ -255,12 +245,18 @@ class _HeaderPill extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 副屏商品核对面板,集中展示商品明细、同步状态和顾客提示。
|
||||
class _ProductPanel extends StatelessWidget {
|
||||
/// 创建读取当前顾客屏状态的商品核对面板。
|
||||
const _ProductPanel({required this.state, required this.scale});
|
||||
|
||||
/// 副屏协调器产出的当前结算展示状态。
|
||||
final CustomerDisplayState state;
|
||||
|
||||
/// 依据副屏分辨率计算的统一缩放比例。
|
||||
final double scale;
|
||||
|
||||
/// 构建适合远距离核对的商品区域,并在商品较多时保留纵向滚动能力。
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -269,10 +265,10 @@ class _ProductPanel extends StatelessWidget {
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
22 * scale,
|
||||
18 * scale,
|
||||
22 * scale,
|
||||
14 * scale,
|
||||
12 * scale,
|
||||
14 * scale,
|
||||
10 * scale,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -284,18 +280,18 @@ class _ProductPanel extends StatelessWidget {
|
||||
state.products.isEmpty ? '欢迎光临' : '请核对商品信息',
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontSize: 24 * scale,
|
||||
fontSize: _fontSize(34, scale),
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4 * scale),
|
||||
SizedBox(height: 2 * scale),
|
||||
Text(
|
||||
state.products.isEmpty
|
||||
? '商品加入购物车后将在这里实时展示'
|
||||
: '本次消费共 ${state.products.length} 项商品',
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.muted,
|
||||
fontSize: 15 * scale,
|
||||
fontSize: _fontSize(22, scale),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -326,8 +322,8 @@ class _ProductPanel extends StatelessWidget {
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 22 * scale,
|
||||
vertical: 12 * scale,
|
||||
horizontal: 14 * scale,
|
||||
vertical: 8 * scale,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFAFAFB),
|
||||
@@ -337,7 +333,7 @@ class _ProductPanel extends StatelessWidget {
|
||||
'商品和金额由收银员实时更新,如有疑问请及时告知收银员',
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.muted,
|
||||
fontSize: 14 * scale,
|
||||
fontSize: _fontSize(18, scale),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -377,7 +373,7 @@ class _EmptyProducts extends StatelessWidget {
|
||||
'等待添加商品',
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontSize: 24 * scale,
|
||||
fontSize: _fontSize(30, scale),
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
@@ -386,7 +382,7 @@ class _EmptyProducts extends StatelessWidget {
|
||||
'请将商品交给收银员进行结算',
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.muted,
|
||||
fontSize: 16 * scale,
|
||||
fontSize: _fontSize(20, scale),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -395,16 +391,20 @@ class _EmptyProducts extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 副屏商品列表表头,与商品行共享相同的列宽比例。
|
||||
class _ProductHeader extends StatelessWidget {
|
||||
/// 创建当前分辨率下的商品列表表头。
|
||||
const _ProductHeader({required this.scale});
|
||||
|
||||
/// 依据副屏分辨率计算的统一缩放比例。
|
||||
final double scale;
|
||||
|
||||
/// 构建商品、数量或重量、单价和小计四列表头。
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 48 * scale,
|
||||
padding: EdgeInsets.symmetric(horizontal: 22 * scale),
|
||||
height: 42 * scale,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10 * scale),
|
||||
color: const Color(0xFFFAFAFB),
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -418,6 +418,7 @@ class _ProductHeader extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 副屏商品列表中的单条商品记录,可选展示套餐组成。
|
||||
class _ProductRow extends StatelessWidget {
|
||||
/// 创建一条可同时展示主商品和套餐组成的副屏商品行。
|
||||
const _ProductRow({required this.product, required this.scale});
|
||||
@@ -432,11 +433,13 @@ class _ProductRow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: 74 * scale),
|
||||
// 单品行按当前副屏可视区容纳 10 条设置高度,字号放大后通过收紧内边距避免溢出。
|
||||
constraints: BoxConstraints(minHeight: 72 * scale),
|
||||
child: Padding(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 22 * scale, vertical: 10 * scale),
|
||||
EdgeInsets.symmetric(horizontal: 10 * scale, vertical: 3 * scale),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
@@ -445,19 +448,21 @@ class _ProductRow extends StatelessWidget {
|
||||
flex: 5,
|
||||
child: Row(
|
||||
children: [
|
||||
_ProductImage(product: product, scale: scale),
|
||||
SizedBox(width: 6 * scale),
|
||||
_ProductTypeBadge(
|
||||
isCombo: product.isCombo,
|
||||
scale: scale,
|
||||
),
|
||||
SizedBox(width: 9 * scale),
|
||||
SizedBox(width: 5 * scale),
|
||||
Expanded(
|
||||
child: Text(
|
||||
product.name,
|
||||
maxLines: 2,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontSize: 18 * scale,
|
||||
fontSize: _fontSize(28, scale),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
@@ -476,7 +481,7 @@ class _ProductRow extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
if (product.isCombo && product.comboItems.isNotEmpty) ...[
|
||||
SizedBox(height: 10 * scale),
|
||||
SizedBox(height: 6 * scale),
|
||||
_ComboContents(product: product, scale: scale),
|
||||
],
|
||||
],
|
||||
@@ -486,6 +491,66 @@ class _ProductRow extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 商品行左侧的固定尺寸图片,网络加载与失败时均保持行高稳定。
|
||||
class _ProductImage extends StatelessWidget {
|
||||
/// 创建使用当前商品图片来源的顾客屏缩略图。
|
||||
const _ProductImage({required this.product, required this.scale});
|
||||
|
||||
/// 当前商品,用于读取图片来源、占位首字和无障碍说明。
|
||||
final CustomerDisplayProduct product;
|
||||
|
||||
/// 依据副屏分辨率计算的几何缩放比例。
|
||||
final double scale;
|
||||
|
||||
/// 构建真实商品图片;副屏 Engine 不依赖磁盘缓存插件,使用 Flutter 原生网络图片缓存。
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final imageSource = product.imageSource;
|
||||
final imageSize = 60 * scale;
|
||||
final placeholder = _buildPlaceholder();
|
||||
return Semantics(
|
||||
image: true,
|
||||
label: '${product.name}商品图片',
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(7 * scale),
|
||||
child: SizedBox(
|
||||
key: ValueKey('customer-product-image-${product.name}'),
|
||||
width: imageSize,
|
||||
height: imageSize,
|
||||
child: imageSource == null
|
||||
? placeholder
|
||||
: Image.network(
|
||||
_productImageUrl(imageSource),
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, loadingProgress) =>
|
||||
loadingProgress == null ? child : placeholder,
|
||||
errorBuilder: (context, error, stackTrace) => placeholder,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 为无图片、加载中和加载失败场景生成一致的名称首字占位。
|
||||
Widget _buildPlaceholder() {
|
||||
final trimmedName = product.name.trim();
|
||||
final shortLabel = trimmedName.isEmpty ? '商' : trimmedName.substring(0, 1);
|
||||
return ColoredBox(
|
||||
color: XfjDesignTokens.control,
|
||||
child: Center(
|
||||
child: Text(
|
||||
shortLabel,
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.muted,
|
||||
fontSize: _fontSize(22, scale),
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 副屏主商品类型标识,帮助顾客快速区分单品和套餐。
|
||||
class _ProductTypeBadge extends StatelessWidget {
|
||||
/// 创建商品类型标识。
|
||||
@@ -511,14 +576,14 @@ class _ProductTypeBadge extends StatelessWidget {
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 7 * scale,
|
||||
vertical: 3 * scale,
|
||||
horizontal: 5 * scale,
|
||||
vertical: 2 * scale,
|
||||
),
|
||||
child: Text(
|
||||
isCombo ? '套餐' : '单品',
|
||||
style: TextStyle(
|
||||
color: foreground,
|
||||
fontSize: 13 * scale,
|
||||
fontSize: _fontSize(18, scale),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
@@ -548,10 +613,10 @@ class _ComboContents extends StatelessWidget {
|
||||
return Container(
|
||||
key: ValueKey('combo-details-${product.name}'),
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
14 * scale,
|
||||
10 * scale,
|
||||
14 * scale,
|
||||
11 * scale,
|
||||
6 * scale,
|
||||
10 * scale,
|
||||
7 * scale,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
color: XfjDesignTokens.primarySoft,
|
||||
@@ -566,10 +631,10 @@ class _ComboContents extends StatelessWidget {
|
||||
children: [
|
||||
Icon(
|
||||
Icons.layers_outlined,
|
||||
size: 17 * scale,
|
||||
size: 18 * scale,
|
||||
color: XfjDesignTokens.primary,
|
||||
),
|
||||
SizedBox(width: 7 * scale),
|
||||
SizedBox(width: 5 * scale),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'套餐内含 · 每套 ${product.comboItems.length} 样 / $totalQuantity 份',
|
||||
@@ -577,17 +642,17 @@ class _ComboContents extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.primary,
|
||||
fontSize: 14 * scale,
|
||||
fontSize: _fontSize(20, scale),
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12 * scale),
|
||||
SizedBox(width: 8 * scale),
|
||||
Text(
|
||||
'明细不单独计价',
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.muted,
|
||||
fontSize: 12 * scale,
|
||||
fontSize: _fontSize(18, scale),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
@@ -595,15 +660,15 @@ class _ComboContents extends StatelessWidget {
|
||||
),
|
||||
for (final item in product.comboItems)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 8 * scale),
|
||||
padding: EdgeInsets.only(top: 5 * scale),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.subdirectory_arrow_right_rounded,
|
||||
size: 16 * scale,
|
||||
size: 17 * scale,
|
||||
color: XfjDesignTokens.muted,
|
||||
),
|
||||
SizedBox(width: 8 * scale),
|
||||
SizedBox(width: 5 * scale),
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.name,
|
||||
@@ -611,17 +676,17 @@ class _ComboContents extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontSize: 15 * scale,
|
||||
fontSize: _fontSize(21, scale),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12 * scale),
|
||||
SizedBox(width: 8 * scale),
|
||||
Text(
|
||||
'×${item.quantity}',
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontSize: 15 * scale,
|
||||
fontSize: _fontSize(21, scale),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
@@ -635,6 +700,7 @@ class _ComboContents extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 按商品表格的列宽比例渲染表头文本。
|
||||
Widget _tableText(
|
||||
String value,
|
||||
int flex,
|
||||
@@ -648,13 +714,14 @@ Widget _tableText(
|
||||
textAlign: align,
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.muted,
|
||||
fontSize: 14 * scale,
|
||||
fontSize: _fontSize(20, scale),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 按商品表格的列宽比例渲染数量或金额,并保持数字宽度稳定。
|
||||
Widget _valueCell(
|
||||
String value,
|
||||
int flex,
|
||||
@@ -668,7 +735,7 @@ Widget _valueCell(
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: emphasized ? XfjDesignTokens.ink : XfjDesignTokens.muted,
|
||||
fontSize: 17 * scale,
|
||||
fontSize: _fontSize(26, scale),
|
||||
fontWeight: emphasized ? FontWeight.w800 : FontWeight.w600,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
@@ -685,7 +752,7 @@ class _SettlementPanel extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(20 * scale),
|
||||
padding: EdgeInsets.all(16 * scale),
|
||||
decoration: _surfaceDecoration(radius: 16 * scale),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
@@ -694,7 +761,7 @@ class _SettlementPanel extends StatelessWidget {
|
||||
'实际扣款',
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.muted,
|
||||
fontSize: 15 * scale,
|
||||
fontSize: _fontSize(20, scale),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
@@ -704,7 +771,7 @@ class _SettlementPanel extends StatelessWidget {
|
||||
key: const ValueKey('customer-payable-price'),
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.primary,
|
||||
fontSize: 54 * scale,
|
||||
fontSize: _fontSize(64, scale),
|
||||
height: 1.05,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
@@ -751,14 +818,15 @@ class _PriceLine extends StatelessWidget {
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.muted,
|
||||
fontSize: 16 * scale,
|
||||
fontSize: _fontSize(22, scale),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_money(price),
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontSize: 18 * scale,
|
||||
fontSize: _fontSize(26, scale),
|
||||
fontWeight: FontWeight.w800,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
@@ -784,6 +852,8 @@ class _PaymentStatePanel extends StatelessWidget {
|
||||
constraints.maxWidth * 0.54,
|
||||
constraints.maxHeight * 0.6,
|
||||
);
|
||||
// 主屏调试预览可能只有 800×600;仅在极矮区域收拢辅助说明,金额、倒计时和主提示仍保持大字号。
|
||||
final compactPaymentHint = constraints.maxHeight < 180;
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
@@ -808,30 +878,36 @@ class _PaymentStatePanel extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
'请扫码支付',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontSize: 26 * scale,
|
||||
fontSize: _fontSize(34, scale),
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10 * scale),
|
||||
SizedBox(height: 6 * scale),
|
||||
Text(
|
||||
_countdown(state.countdownSeconds),
|
||||
key: const ValueKey('customer-payment-countdown'),
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.danger,
|
||||
fontSize: 34 * scale,
|
||||
fontSize: _fontSize(44, scale),
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8 * scale),
|
||||
SizedBox(height: 5 * scale),
|
||||
Text(
|
||||
'支付完成后请勿重复扫码',
|
||||
maxLines: compactPaymentHint ? 1 : 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.muted,
|
||||
fontSize: 14 * scale,
|
||||
height: 1.4,
|
||||
fontSize: _fontSize(20, scale),
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -855,34 +931,37 @@ class _PaymentStatePanel extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 76 * scale,
|
||||
height: 76 * scale,
|
||||
width: 68 * scale,
|
||||
height: 68 * scale,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: visual.color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(visual.icon, color: Colors.white, size: 40 * scale),
|
||||
child: Icon(visual.icon, color: Colors.white, size: 36 * scale),
|
||||
),
|
||||
SizedBox(height: 16 * scale),
|
||||
SizedBox(height: 10 * scale),
|
||||
Text(
|
||||
visual.title,
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontSize: 28 * scale,
|
||||
fontSize: _fontSize(36, scale),
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 7 * scale),
|
||||
SizedBox(height: 5 * scale),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20 * scale),
|
||||
child: Text(
|
||||
visual.description,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: XfjDesignTokens.muted,
|
||||
fontSize: 15 * scale,
|
||||
height: 1.45,
|
||||
fontSize: _fontSize(22, scale),
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -946,7 +1025,7 @@ class _Step extends StatelessWidget {
|
||||
'$index',
|
||||
style: TextStyle(
|
||||
color: active ? Colors.white : XfjDesignTokens.muted,
|
||||
fontSize: 14 * scale,
|
||||
fontSize: _fontSize(18, scale),
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
@@ -956,7 +1035,7 @@ class _Step extends StatelessWidget {
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: active ? XfjDesignTokens.ink : XfjDesignTokens.muted,
|
||||
fontSize: 13 * scale,
|
||||
fontSize: _fontSize(18, scale),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
@@ -966,31 +1045,36 @@ class _Step extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// 商品面板顶部的结算阶段标签,用图标和文字同时表达当前状态。
|
||||
class _StatusBadge extends StatelessWidget {
|
||||
/// 创建与当前顾客屏场景对应的状态标签。
|
||||
const _StatusBadge({required this.scenario, required this.scale});
|
||||
|
||||
/// 当前结算场景,决定标签的文案、图标和语义颜色。
|
||||
final CustomerDisplayScenario scenario;
|
||||
|
||||
/// 依据副屏分辨率计算的统一缩放比例。
|
||||
final double scale;
|
||||
|
||||
/// 构建紧凑但可远距离识别的商品核对状态标签。
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final visual = _scenarioVisual(scenario);
|
||||
return Container(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 12 * scale, vertical: 7 * scale),
|
||||
padding: EdgeInsets.symmetric(horizontal: 8 * scale, vertical: 4 * scale),
|
||||
decoration: BoxDecoration(
|
||||
color: visual.color.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(18 * scale),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(visual.icon, color: visual.color, size: 17 * scale),
|
||||
SizedBox(width: 6 * scale),
|
||||
Icon(visual.icon, color: visual.color, size: 20 * scale),
|
||||
SizedBox(width: 4 * scale),
|
||||
Text(
|
||||
scenario.title,
|
||||
style: TextStyle(
|
||||
color: visual.color,
|
||||
fontSize: 14 * scale,
|
||||
fontSize: _fontSize(20, scale),
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
@@ -1066,6 +1150,20 @@ BoxDecoration _surfaceDecoration({required double radius}) {
|
||||
|
||||
String _money(int cents) => '¥ ${(cents / 100).toStringAsFixed(2)}';
|
||||
|
||||
/// 将文字缩放与布局缩放分离,小尺寸副屏仍保留远距离可读的最低字号。
|
||||
double _fontSize(double designSize, double scale) {
|
||||
return designSize * math.max(scale, 0.9);
|
||||
}
|
||||
|
||||
/// 将主屏传来的完整地址或 OSS 相对路径转换为副屏可加载的网络地址。
|
||||
String _productImageUrl(String source) {
|
||||
if (RegExp(r'^https?://', caseSensitive: false).hasMatch(source)) {
|
||||
return source;
|
||||
}
|
||||
final separator = source.startsWith('/') || source.startsWith('?') ? '' : '/';
|
||||
return '${Constants.ossUrl}$separator$source';
|
||||
}
|
||||
|
||||
String _countdown(int seconds) {
|
||||
final minutes = seconds ~/ 60;
|
||||
final remainingSeconds = seconds % 60;
|
||||
|
||||
@@ -11,6 +11,11 @@ void main() {
|
||||
expect(coordinator.state.scenario, CustomerDisplayScenario.review);
|
||||
expect(coordinator.state.products, hasLength(2));
|
||||
expect(coordinator.state.products.last.quantityLabel, '0.36 kg');
|
||||
expect(
|
||||
coordinator.state.products.first.imageSource,
|
||||
'/admin/sys-file/oss/file?fileName=milk.png',
|
||||
);
|
||||
expect(coordinator.state.products.last.imageSource, isNull);
|
||||
expect(coordinator.state.products.first.isCombo, isFalse);
|
||||
expect(coordinator.state.products.first.comboItems, isEmpty);
|
||||
expect(coordinator.state.totalPrice, 4670);
|
||||
@@ -272,6 +277,7 @@ Map<String, dynamic> _productEvent() {
|
||||
'shopList': [
|
||||
{
|
||||
'productName': '纯牛奶 250ml',
|
||||
'productPic': '/admin/sys-file/oss/file?fileName=milk.png',
|
||||
'productPrice': 480,
|
||||
'nature': 'DEFAULT',
|
||||
'productNumber': 1,
|
||||
|
||||
Reference in new issue
Block a user