893 lines
26 KiB
Dart
893 lines
26 KiB
Dart
import 'dart:math' as math;
|
|
|
|
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/sub_screen/common/models/customer_display_models.dart';
|
|
import 'package:pj_supermarket_cloud/sub_screen/common/utils/customer_display_coordinator.dart';
|
|
|
|
/// 真实副屏和模拟器预览共用的顾客结算屏。
|
|
class CustomerDisplayPage extends StatelessWidget {
|
|
const CustomerDisplayPage({
|
|
super.key,
|
|
required this.coordinator,
|
|
this.previewMode = false,
|
|
this.onExit,
|
|
});
|
|
|
|
final CustomerDisplayCoordinator coordinator;
|
|
final bool previewMode;
|
|
final VoidCallback? onExit;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: XfjDesignTokens.canvas,
|
|
body: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: AnimatedBuilder(
|
|
animation: coordinator,
|
|
builder: (context, _) => _CustomerDisplayView(
|
|
state: coordinator.state,
|
|
),
|
|
),
|
|
),
|
|
if (previewMode)
|
|
Positioned(
|
|
top: 16,
|
|
right: 16,
|
|
child: SafeArea(
|
|
child: Semantics(
|
|
button: true,
|
|
label: '退出顾客屏预览',
|
|
child: Material(
|
|
color: const Color(0xD9111827),
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: InkWell(
|
|
onTap: onExit,
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: const SizedBox(
|
|
width: 118,
|
|
height: 48,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.close_fullscreen_rounded,
|
|
color: Colors.white, size: 20),
|
|
SizedBox(width: 8),
|
|
Text(
|
|
'退出预览',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CustomerDisplayView extends StatelessWidget {
|
|
const _CustomerDisplayView({required this.state});
|
|
|
|
final CustomerDisplayState state;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final scale = math
|
|
.min(constraints.maxWidth / 1920, constraints.maxHeight / 1080)
|
|
.clamp(0.55, 1.12)
|
|
.toDouble();
|
|
final padding = 24.0 * scale;
|
|
final gap = 16.0 * scale;
|
|
return ColoredBox(
|
|
key: ValueKey('customer-scenario-${state.scenario.name}'),
|
|
color: XfjDesignTokens.canvas,
|
|
child: SafeArea(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(padding),
|
|
child: Column(
|
|
children: [
|
|
_Header(state: state, scale: scale),
|
|
SizedBox(height: gap),
|
|
Expanded(
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Expanded(
|
|
flex: 12,
|
|
child: _ProductPanel(state: state, scale: scale),
|
|
),
|
|
SizedBox(width: gap),
|
|
Expanded(
|
|
flex: 8,
|
|
child: _SettlementPanel(state: state, scale: scale),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Header extends StatelessWidget {
|
|
const _Header({required this.state, required this.scale});
|
|
|
|
final CustomerDisplayState state;
|
|
final double scale;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
height: 76 * scale,
|
|
padding: EdgeInsets.symmetric(horizontal: 22 * scale),
|
|
decoration: _surfaceDecoration(radius: 16 * scale),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 46 * scale,
|
|
height: 46 * scale,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: XfjDesignTokens.primary,
|
|
borderRadius: BorderRadius.circular(13 * scale),
|
|
),
|
|
child: Text(
|
|
'Z',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 24 * scale,
|
|
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const Spacer(),
|
|
if (state.maskedCustomerName != null) ...[
|
|
_HeaderPill(
|
|
icon: Icons.person_outline_rounded,
|
|
label: '${state.maskedCustomerName},您好',
|
|
color: XfjDesignTokens.primary,
|
|
scale: scale,
|
|
),
|
|
SizedBox(width: 10 * scale),
|
|
],
|
|
_HeaderPill(
|
|
icon: state.connected
|
|
? Icons.desktop_windows_outlined
|
|
: Icons.desktop_access_disabled_outlined,
|
|
label: state.connected ? '设备已连接' : '设备未连接',
|
|
color: state.connected
|
|
? XfjDesignTokens.success
|
|
: XfjDesignTokens.warning,
|
|
scale: scale,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _HeaderPill extends StatelessWidget {
|
|
const _HeaderPill({
|
|
required this.icon,
|
|
required this.label,
|
|
required this.color,
|
|
required this.scale,
|
|
});
|
|
|
|
final IconData icon;
|
|
final String label;
|
|
final Color color;
|
|
final double scale;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
constraints: BoxConstraints(minHeight: 44 * scale),
|
|
padding: EdgeInsets.symmetric(horizontal: 14 * scale),
|
|
decoration: BoxDecoration(
|
|
color: color.withValues(alpha: 0.08),
|
|
border: Border.all(color: color.withValues(alpha: 0.22)),
|
|
borderRadius: BorderRadius.circular(22 * scale),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, color: color, size: 20 * scale),
|
|
SizedBox(width: 8 * scale),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: color,
|
|
fontSize: 15 * scale,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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(
|
|
decoration: _surfaceDecoration(radius: 16 * scale),
|
|
child: Column(
|
|
children: [
|
|
Padding(
|
|
padding: EdgeInsets.fromLTRB(
|
|
22 * scale,
|
|
18 * scale,
|
|
22 * scale,
|
|
14 * scale,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
state.products.isEmpty ? '欢迎光临' : '请核对商品信息',
|
|
style: TextStyle(
|
|
color: XfjDesignTokens.ink,
|
|
fontSize: 24 * scale,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
SizedBox(height: 4 * scale),
|
|
Text(
|
|
state.products.isEmpty
|
|
? '商品加入购物车后将在这里实时展示'
|
|
: '本次消费共 ${state.products.length} 项商品',
|
|
style: TextStyle(
|
|
color: XfjDesignTokens.muted,
|
|
fontSize: 15 * scale,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
_StatusBadge(scenario: state.scenario, scale: scale),
|
|
],
|
|
),
|
|
),
|
|
Divider(height: 1, color: XfjDesignTokens.line),
|
|
if (state.products.isEmpty)
|
|
Expanded(child: _EmptyProducts(scale: scale))
|
|
else ...[
|
|
_ProductHeader(scale: scale),
|
|
Expanded(
|
|
child: ListView.separated(
|
|
padding: EdgeInsets.zero,
|
|
itemCount: state.products.length,
|
|
separatorBuilder: (_, __) =>
|
|
Divider(height: 1, color: XfjDesignTokens.line),
|
|
itemBuilder: (context, index) => _ProductRow(
|
|
product: state.products[index],
|
|
scale: scale,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
Container(
|
|
width: double.infinity,
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 22 * scale,
|
|
vertical: 12 * scale,
|
|
),
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFFFAFAFB),
|
|
border: Border(top: BorderSide(color: XfjDesignTokens.line)),
|
|
),
|
|
child: Text(
|
|
'商品和金额由收银员实时更新,如有疑问请及时告知收银员',
|
|
style: TextStyle(
|
|
color: XfjDesignTokens.muted,
|
|
fontSize: 14 * scale,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _EmptyProducts extends StatelessWidget {
|
|
const _EmptyProducts({required this.scale});
|
|
|
|
final double scale;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 92 * scale,
|
|
height: 92 * scale,
|
|
alignment: Alignment.center,
|
|
decoration: const BoxDecoration(
|
|
color: XfjDesignTokens.primarySoft,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.shopping_basket_outlined,
|
|
color: XfjDesignTokens.primary,
|
|
size: 46 * scale,
|
|
),
|
|
),
|
|
SizedBox(height: 18 * scale),
|
|
Text(
|
|
'等待添加商品',
|
|
style: TextStyle(
|
|
color: XfjDesignTokens.ink,
|
|
fontSize: 24 * scale,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
SizedBox(height: 6 * scale),
|
|
Text(
|
|
'请将商品交给收银员进行结算',
|
|
style: TextStyle(
|
|
color: XfjDesignTokens.muted,
|
|
fontSize: 16 * scale,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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),
|
|
color: const Color(0xFFFAFAFB),
|
|
child: Row(
|
|
children: [
|
|
_tableText('商品', 5, scale, align: TextAlign.left),
|
|
_tableText('数量 / 重量', 2, scale),
|
|
_tableText('单价', 2, scale),
|
|
_tableText('小计', 2, scale),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ProductRow extends StatelessWidget {
|
|
const _ProductRow({required this.product, required this.scale});
|
|
|
|
final CustomerDisplayProduct product;
|
|
final double scale;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
constraints: BoxConstraints(minHeight: 74 * scale),
|
|
padding:
|
|
EdgeInsets.symmetric(horizontal: 22 * scale, vertical: 10 * scale),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
flex: 5,
|
|
child: Text(
|
|
product.name,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
color: XfjDesignTokens.ink,
|
|
fontSize: 18 * scale,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
_valueCell(product.quantityLabel, 2, scale),
|
|
_valueCell(_money(product.unitPrice), 2, scale),
|
|
_valueCell(
|
|
_money(product.totalPrice),
|
|
2,
|
|
scale,
|
|
emphasized: true,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Widget _tableText(
|
|
String value,
|
|
int flex,
|
|
double scale, {
|
|
TextAlign align = TextAlign.center,
|
|
}) {
|
|
return Expanded(
|
|
flex: flex,
|
|
child: Text(
|
|
value,
|
|
textAlign: align,
|
|
style: TextStyle(
|
|
color: XfjDesignTokens.muted,
|
|
fontSize: 14 * scale,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _valueCell(
|
|
String value,
|
|
int flex,
|
|
double scale, {
|
|
bool emphasized = false,
|
|
}) {
|
|
return Expanded(
|
|
flex: flex,
|
|
child: Text(
|
|
value,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: emphasized ? XfjDesignTokens.ink : XfjDesignTokens.muted,
|
|
fontSize: 17 * scale,
|
|
fontWeight: emphasized ? FontWeight.w800 : FontWeight.w600,
|
|
fontFeatures: const [FontFeature.tabularFigures()],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
class _SettlementPanel extends StatelessWidget {
|
|
const _SettlementPanel({required this.state, required this.scale});
|
|
|
|
final CustomerDisplayState state;
|
|
final double scale;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: EdgeInsets.all(20 * scale),
|
|
decoration: _surfaceDecoration(radius: 16 * scale),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text(
|
|
'实际扣款',
|
|
style: TextStyle(
|
|
color: XfjDesignTokens.muted,
|
|
fontSize: 15 * scale,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
SizedBox(height: 6 * scale),
|
|
Text(
|
|
_money(state.payablePrice),
|
|
key: const ValueKey('customer-payable-price'),
|
|
style: TextStyle(
|
|
color: XfjDesignTokens.primary,
|
|
fontSize: 54 * scale,
|
|
height: 1.05,
|
|
fontWeight: FontWeight.w900,
|
|
fontFeatures: const [FontFeature.tabularFigures()],
|
|
),
|
|
),
|
|
SizedBox(height: 12 * scale),
|
|
_PriceLine(label: '商品总价', price: state.totalPrice, scale: scale),
|
|
SizedBox(height: 7 * scale),
|
|
_PriceLine(
|
|
label: '额外支付',
|
|
price: state.amountMealPrice,
|
|
scale: scale,
|
|
),
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 14 * scale),
|
|
child: Divider(height: 1, color: XfjDesignTokens.line),
|
|
),
|
|
Expanded(child: _PaymentStatePanel(state: state, scale: scale)),
|
|
SizedBox(height: 14 * scale),
|
|
_CheckoutSteps(current: state.scenario, scale: scale),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _PriceLine extends StatelessWidget {
|
|
const _PriceLine({
|
|
required this.label,
|
|
required this.price,
|
|
required this.scale,
|
|
});
|
|
|
|
final String label;
|
|
final int price;
|
|
final double scale;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: XfjDesignTokens.muted,
|
|
fontSize: 16 * scale,
|
|
),
|
|
),
|
|
Text(
|
|
_money(price),
|
|
style: TextStyle(
|
|
color: XfjDesignTokens.ink,
|
|
fontSize: 18 * scale,
|
|
fontWeight: FontWeight.w800,
|
|
fontFeatures: const [FontFeature.tabularFigures()],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _PaymentStatePanel extends StatelessWidget {
|
|
const _PaymentStatePanel({required this.state, required this.scale});
|
|
|
|
final CustomerDisplayState state;
|
|
final double scale;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (state.scenario == CustomerDisplayScenario.paying &&
|
|
state.qrCodeData != null) {
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final qrSize = math.min(
|
|
constraints.maxWidth * 0.54,
|
|
constraints.maxHeight * 0.6,
|
|
);
|
|
return Row(
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.all(10 * scale),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
border: Border.all(color: XfjDesignTokens.line),
|
|
borderRadius: BorderRadius.circular(14 * scale),
|
|
),
|
|
child: QrImageView(
|
|
data: state.qrCodeData!,
|
|
version: QrVersions.auto,
|
|
size: qrSize,
|
|
padding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
SizedBox(width: 18 * scale),
|
|
Expanded(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'请扫码支付',
|
|
style: TextStyle(
|
|
color: XfjDesignTokens.ink,
|
|
fontSize: 26 * scale,
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
),
|
|
SizedBox(height: 10 * scale),
|
|
Text(
|
|
_countdown(state.countdownSeconds),
|
|
key: const ValueKey('customer-payment-countdown'),
|
|
style: TextStyle(
|
|
color: XfjDesignTokens.danger,
|
|
fontSize: 34 * scale,
|
|
fontWeight: FontWeight.w900,
|
|
fontFeatures: const [FontFeature.tabularFigures()],
|
|
),
|
|
),
|
|
SizedBox(height: 8 * scale),
|
|
Text(
|
|
'支付完成后请勿重复扫码',
|
|
style: TextStyle(
|
|
color: XfjDesignTokens.muted,
|
|
fontSize: 14 * scale,
|
|
height: 1.4,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
final visual = _scenarioVisual(state.scenario);
|
|
return Container(
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: visual.color.withValues(alpha: 0.06),
|
|
border: Border.all(color: visual.color.withValues(alpha: 0.2)),
|
|
borderRadius: BorderRadius.circular(16 * scale),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 76 * scale,
|
|
height: 76 * scale,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: visual.color,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(visual.icon, color: Colors.white, size: 40 * scale),
|
|
),
|
|
SizedBox(height: 16 * scale),
|
|
Text(
|
|
visual.title,
|
|
style: TextStyle(
|
|
color: XfjDesignTokens.ink,
|
|
fontSize: 28 * scale,
|
|
fontWeight: FontWeight.w900,
|
|
),
|
|
),
|
|
SizedBox(height: 7 * scale),
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 20 * scale),
|
|
child: Text(
|
|
visual.description,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: XfjDesignTokens.muted,
|
|
fontSize: 15 * scale,
|
|
height: 1.45,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CheckoutSteps extends StatelessWidget {
|
|
const _CheckoutSteps({required this.current, required this.scale});
|
|
|
|
final CustomerDisplayScenario current;
|
|
final double scale;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final activeStep = switch (current) {
|
|
CustomerDisplayScenario.idle => 0,
|
|
CustomerDisplayScenario.review => 1,
|
|
CustomerDisplayScenario.paying => 2,
|
|
_ => 3,
|
|
};
|
|
return Row(
|
|
children: [
|
|
_Step(index: 1, label: '核对商品', active: activeStep >= 1, scale: scale),
|
|
_Step(index: 2, label: '完成支付', active: activeStep >= 2, scale: scale),
|
|
_Step(index: 3, label: '带走商品', active: activeStep >= 3, scale: scale),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _Step extends StatelessWidget {
|
|
const _Step({
|
|
required this.index,
|
|
required this.label,
|
|
required this.active,
|
|
required this.scale,
|
|
});
|
|
|
|
final int index;
|
|
final String label;
|
|
final bool active;
|
|
final double scale;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Expanded(
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
width: 30 * scale,
|
|
height: 30 * scale,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: active ? XfjDesignTokens.primary : XfjDesignTokens.control,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Text(
|
|
'$index',
|
|
style: TextStyle(
|
|
color: active ? Colors.white : XfjDesignTokens.muted,
|
|
fontSize: 14 * scale,
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 5 * scale),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: active ? XfjDesignTokens.ink : XfjDesignTokens.muted,
|
|
fontSize: 13 * scale,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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),
|
|
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),
|
|
Text(
|
|
scenario.title,
|
|
style: TextStyle(
|
|
color: visual.color,
|
|
fontSize: 14 * scale,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ScenarioVisual {
|
|
const _ScenarioVisual({
|
|
required this.title,
|
|
required this.description,
|
|
required this.icon,
|
|
required this.color,
|
|
});
|
|
|
|
final String title;
|
|
final String description;
|
|
final IconData icon;
|
|
final Color color;
|
|
}
|
|
|
|
_ScenarioVisual _scenarioVisual(CustomerDisplayScenario scenario) {
|
|
return switch (scenario) {
|
|
CustomerDisplayScenario.idle => const _ScenarioVisual(
|
|
title: '欢迎使用',
|
|
description: '请将商品交给收银员,结算信息会实时显示在这里',
|
|
icon: Icons.waving_hand_outlined,
|
|
color: XfjDesignTokens.primary,
|
|
),
|
|
CustomerDisplayScenario.review => const _ScenarioVisual(
|
|
title: '请核对商品',
|
|
description: '请确认商品、数量和金额,确认无误后由收银员发起支付',
|
|
icon: Icons.receipt_long_outlined,
|
|
color: XfjDesignTokens.primary,
|
|
),
|
|
CustomerDisplayScenario.paying => const _ScenarioVisual(
|
|
title: '请扫码支付',
|
|
description: '请在倒计时结束前完成支付',
|
|
icon: Icons.qr_code_2_rounded,
|
|
color: XfjDesignTokens.primary,
|
|
),
|
|
CustomerDisplayScenario.success => const _ScenarioVisual(
|
|
title: '支付成功',
|
|
description: '本次支付已完成,请确认商品后带走,感谢您的使用',
|
|
icon: Icons.check_rounded,
|
|
color: XfjDesignTokens.success,
|
|
),
|
|
CustomerDisplayScenario.cancelled => const _ScenarioVisual(
|
|
title: '支付已取消',
|
|
description: '本次支付没有完成,请等待收银员重新发起支付',
|
|
icon: Icons.close_rounded,
|
|
color: XfjDesignTokens.danger,
|
|
),
|
|
CustomerDisplayScenario.timeout => const _ScenarioVisual(
|
|
title: '支付已超时',
|
|
description: '二维码已经失效,请等待收银员重新发起支付',
|
|
icon: Icons.schedule_rounded,
|
|
color: XfjDesignTokens.warning,
|
|
),
|
|
};
|
|
}
|
|
|
|
BoxDecoration _surfaceDecoration({required double radius}) {
|
|
return BoxDecoration(
|
|
color: XfjDesignTokens.surface,
|
|
border: Border.all(color: XfjDesignTokens.line),
|
|
borderRadius: BorderRadius.circular(radius),
|
|
boxShadow: XfjDesignTokens.surfaceShadow,
|
|
);
|
|
}
|
|
|
|
String _money(int cents) => '¥ ${(cents / 100).toStringAsFixed(2)}';
|
|
|
|
String _countdown(int seconds) {
|
|
final minutes = seconds ~/ 60;
|
|
final remainingSeconds = seconds % 60;
|
|
return '${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}';
|
|
}
|