714 lines
23 KiB
Dart
714 lines
23 KiB
Dart
import 'dart:math' as math;
|
||
|
||
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
|
||
import 'package:flutter/material.dart';
|
||
|
||
import 'package:pj_supermarket_cloud/common/index.dart';
|
||
import 'package:pj_supermarket_cloud/sub_screen/common/models/reservation_query_display_models.dart';
|
||
import 'package:pj_supermarket_cloud/sub_screen/common/utils/reservation_query_coordinator.dart';
|
||
|
||
/// 真实副屏和 Debug 预览共用的预约菜单查询屏。
|
||
class ReservationQueryDisplayPage extends StatelessWidget {
|
||
const ReservationQueryDisplayPage({
|
||
super.key,
|
||
required this.coordinator,
|
||
this.previewMode = false,
|
||
this.onExit,
|
||
});
|
||
|
||
final ReservationQueryCoordinator 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, _) => _ReservationQueryDisplayView(
|
||
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 _ReservationQueryDisplayView extends StatelessWidget {
|
||
const _ReservationQueryDisplayView({required this.state});
|
||
|
||
final ReservationQueryDisplayState 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();
|
||
return ColoredBox(
|
||
key: ValueKey('reservation-scenario-${state.scenario.name}'),
|
||
color: XfjDesignTokens.canvas,
|
||
child: SafeArea(
|
||
child: Padding(
|
||
padding: EdgeInsets.all(24 * scale),
|
||
child: Column(
|
||
children: [
|
||
_ReservationHeader(state: state, scale: scale),
|
||
SizedBox(height: 14 * scale),
|
||
Expanded(
|
||
child: state.continuousFaceRecognition
|
||
? _ContinuousFaceReservationBody(
|
||
state: state,
|
||
scale: scale,
|
||
)
|
||
: _ReservationBody(state: state, scale: scale),
|
||
),
|
||
SizedBox(height: 12 * scale),
|
||
_ReservationFooter(state: state, scale: scale),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ReservationHeader extends StatelessWidget {
|
||
const _ReservationHeader({required this.state, required this.scale});
|
||
|
||
final ReservationQueryDisplayState state;
|
||
final double scale;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
height: 82 * scale,
|
||
padding: EdgeInsets.symmetric(horizontal: 22 * scale),
|
||
decoration: _surfaceDecoration(16 * scale),
|
||
child: Row(
|
||
children: [
|
||
Container(
|
||
width: 48 * scale,
|
||
height: 48 * scale,
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
color: XfjDesignTokens.primary,
|
||
borderRadius: BorderRadius.circular(13 * scale),
|
||
),
|
||
child: Icon(
|
||
Icons.calendar_month_rounded,
|
||
color: Colors.white,
|
||
size: 27 * scale,
|
||
),
|
||
),
|
||
SizedBox(width: 14 * scale),
|
||
Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'预约查询',
|
||
style: TextStyle(
|
||
color: XfjDesignTokens.ink,
|
||
fontSize: 24 * scale,
|
||
fontWeight: FontWeight.w900,
|
||
),
|
||
),
|
||
Text(
|
||
state.merchantName ?? '请使用饭卡或人脸识别本人预约',
|
||
style: TextStyle(
|
||
color: XfjDesignTokens.muted,
|
||
fontSize: 14 * scale,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const Spacer(),
|
||
if (state.maskedName != null) ...[
|
||
_HeaderPill(
|
||
icon: Icons.person_outline_rounded,
|
||
label: [state.maskedName, state.deptName]
|
||
.whereType<String>()
|
||
.where((value) => value.trim().isNotEmpty)
|
||
.join(' · '),
|
||
color: XfjDesignTokens.primary,
|
||
scale: scale,
|
||
),
|
||
SizedBox(width: 10 * scale),
|
||
],
|
||
_HeaderPill(
|
||
icon: _scenarioIcon(state.scenario),
|
||
label: state.scenario.title,
|
||
color: _scenarioColor(state.scenario),
|
||
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(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(icon, color: color, size: 20 * scale),
|
||
SizedBox(width: 7 * scale),
|
||
Text(
|
||
label,
|
||
style: TextStyle(
|
||
color: color,
|
||
fontSize: 15 * scale,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 持续人脸副屏组合区,左侧长期保留实时预览,右侧按状态切换等待或预约菜单。
|
||
class _ContinuousFaceReservationBody extends StatelessWidget {
|
||
/// 创建与预约快照同步的持续人脸组合布局。
|
||
const _ContinuousFaceReservationBody({
|
||
required this.state,
|
||
required this.scale,
|
||
});
|
||
|
||
/// 当前主屏发送并经 Coordinator 校验的预约展示快照。
|
||
final ReservationQueryDisplayState state;
|
||
|
||
/// 基于副屏实际尺寸计算的统一缩放系数。
|
||
final double scale;
|
||
|
||
/// 构建固定宽度摄像头区域和弹性预约结果区域,状态变化不会销毁 Texture。
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final presentation = _continuousFacePresentation(state.scenario);
|
||
final countdown = state.remainingSeconds;
|
||
final cameraPanel = <Widget>[
|
||
AspectRatio(
|
||
aspectRatio: 16 / 9,
|
||
child: ClipRRect(
|
||
borderRadius: BorderRadius.circular(12 * scale),
|
||
child: FaceLiveCameraPreview(
|
||
key: const ValueKey('sub-reservation-live-face-preview'),
|
||
statusText: presentation.text,
|
||
statusColor: presentation.color,
|
||
scale: scale,
|
||
),
|
||
),
|
||
),
|
||
SizedBox(height: 18 * scale),
|
||
<Widget>[
|
||
Icon(
|
||
presentation.icon,
|
||
color: presentation.color,
|
||
size: 34 * scale,
|
||
),
|
||
<Widget>[
|
||
Text(
|
||
presentation.title,
|
||
style: TextStyle(
|
||
color: XfjDesignTokens.ink,
|
||
fontSize: 25 * scale,
|
||
fontWeight: FontWeight.w900,
|
||
),
|
||
),
|
||
SizedBox(height: 5 * scale),
|
||
Text(
|
||
countdown > 0
|
||
? '${presentation.detail},$countdown 秒后识别下一位'
|
||
: presentation.detail,
|
||
style: TextStyle(
|
||
color: XfjDesignTokens.muted,
|
||
fontSize: 16 * scale,
|
||
height: 1.45,
|
||
fontFeatures: const [FontFeature.tabularFigures()],
|
||
),
|
||
),
|
||
].toColumn(crossAxisAlignment: CrossAxisAlignment.start).expanded(),
|
||
].toRowSpace(space: 14 * scale).padding(all: 16 * scale).decorated(
|
||
color: presentation.color.withValues(alpha: 0.08),
|
||
border: Border.all(
|
||
color: presentation.color.withValues(alpha: 0.22),
|
||
),
|
||
borderRadius: BorderRadius.circular(12 * scale),
|
||
),
|
||
const Spacer(),
|
||
<Widget>[
|
||
Icon(
|
||
Icons.privacy_tip_outlined,
|
||
color: XfjDesignTokens.primary,
|
||
size: 21 * scale,
|
||
),
|
||
Text(
|
||
'实时画面仅用于本人识别,不保存影像',
|
||
style: TextStyle(
|
||
color: XfjDesignTokens.muted,
|
||
fontSize: 14 * scale,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
).expanded(),
|
||
].toRowSpace(space: 9 * scale),
|
||
]
|
||
.toColumn(crossAxisAlignment: CrossAxisAlignment.stretch)
|
||
.padding(all: 16 * scale)
|
||
.decorated(
|
||
color: XfjDesignTokens.surface,
|
||
border: Border.all(color: XfjDesignTokens.line),
|
||
borderRadius: BorderRadius.circular(16 * scale),
|
||
boxShadow: XfjDesignTokens.surfaceShadow,
|
||
);
|
||
|
||
return <Widget>[
|
||
cameraPanel.expanded(flex: 3),
|
||
_ReservationBody(state: state, scale: scale).expanded(flex: 8),
|
||
].toRowSpace(
|
||
space: 14 * scale,
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
);
|
||
}
|
||
|
||
/// 把预约阶段转换为摄像头区域的独立状态,菜单展示期明确暂停下一位识别。
|
||
({
|
||
String text,
|
||
String title,
|
||
String detail,
|
||
Color color,
|
||
IconData icon,
|
||
}) _continuousFacePresentation(ReservationQueryDisplayScenario scenario) {
|
||
return switch (scenario) {
|
||
ReservationQueryDisplayScenario.loading => (
|
||
text: '正在查询菜单',
|
||
title: '身份识别成功',
|
||
detail: '正在读取本人预约菜单',
|
||
color: XfjDesignTokens.primary,
|
||
icon: Icons.search_rounded,
|
||
),
|
||
ReservationQueryDisplayScenario.success ||
|
||
ReservationQueryDisplayScenario.empty =>
|
||
(
|
||
text: '当前结果展示中',
|
||
title: '预约结果展示中',
|
||
detail: '摄像头保持开启,暂不识别其他人员',
|
||
color: XfjDesignTokens.success,
|
||
icon: Icons.check_circle_outline_rounded,
|
||
),
|
||
ReservationQueryDisplayScenario.error => (
|
||
text: '持续识别已暂停',
|
||
title: '识别暂时中断',
|
||
detail: '请等待工作人员处理后重新识别',
|
||
color: XfjDesignTokens.warning,
|
||
icon: Icons.error_outline_rounded,
|
||
),
|
||
_ => (
|
||
text: '持续识别中',
|
||
title: '请正对摄像头',
|
||
detail: '正在等待并识别下一位用户',
|
||
color: XfjDesignTokens.primary,
|
||
icon: Icons.face_retouching_natural_rounded,
|
||
),
|
||
};
|
||
}
|
||
}
|
||
|
||
class _ReservationBody extends StatelessWidget {
|
||
const _ReservationBody({required this.state, required this.scale});
|
||
|
||
final ReservationQueryDisplayState state;
|
||
final double scale;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (state.scenario != ReservationQueryDisplayScenario.success) {
|
||
return _ReservationStatePanel(state: state, scale: scale);
|
||
}
|
||
return Container(
|
||
padding: EdgeInsets.all(14 * scale),
|
||
decoration: _surfaceDecoration(16 * scale),
|
||
child: Column(
|
||
children: [
|
||
_SummaryBar(state: state, scale: scale),
|
||
SizedBox(height: 12 * scale),
|
||
Expanded(
|
||
child: _ReservationGrid(days: state.days, scale: scale),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SummaryBar extends StatelessWidget {
|
||
const _SummaryBar({required this.state, required this.scale});
|
||
|
||
final ReservationQueryDisplayState state;
|
||
final double scale;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
height: 58 * scale,
|
||
padding: EdgeInsets.symmetric(horizontal: 18 * scale),
|
||
decoration: BoxDecoration(
|
||
color: XfjDesignTokens.primarySoft,
|
||
border: Border.all(
|
||
color: XfjDesignTokens.primary.withValues(alpha: 0.2),
|
||
),
|
||
borderRadius: BorderRadius.circular(12 * scale),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
_SummaryItem(
|
||
icon: Icons.event_available_outlined,
|
||
label: '共 ${state.totalDayCount} 个预约日',
|
||
scale: scale,
|
||
),
|
||
SizedBox(width: 24 * scale),
|
||
_SummaryItem(
|
||
icon: Icons.restaurant_menu_rounded,
|
||
label: '${state.totalMealCount} 个餐次',
|
||
scale: scale,
|
||
),
|
||
const Spacer(),
|
||
if (state.totalPages > 1)
|
||
Text(
|
||
'第 ${state.currentPage} / ${state.totalPages} 页',
|
||
key: const ValueKey('reservation-page-indicator'),
|
||
style: TextStyle(
|
||
color: XfjDesignTokens.primary,
|
||
fontSize: 17 * scale,
|
||
fontWeight: FontWeight.w800,
|
||
fontFeatures: const [FontFeature.tabularFigures()],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _SummaryItem extends StatelessWidget {
|
||
const _SummaryItem({
|
||
required this.icon,
|
||
required this.label,
|
||
required this.scale,
|
||
});
|
||
|
||
final IconData icon;
|
||
final String label;
|
||
final double scale;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Row(
|
||
children: [
|
||
Icon(icon, color: XfjDesignTokens.primary, size: 21 * scale),
|
||
SizedBox(width: 7 * scale),
|
||
Text(
|
||
label,
|
||
style: TextStyle(
|
||
color: XfjDesignTokens.ink,
|
||
fontSize: 17 * scale,
|
||
fontWeight: FontWeight.w800,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ReservationGrid extends StatelessWidget {
|
||
const _ReservationGrid({required this.days, required this.scale});
|
||
|
||
final List<ReservationMenuDayData> days;
|
||
final double scale;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final rows = days.length <= 5 ? 1 : 2;
|
||
return Column(
|
||
children: List.generate(rows, (rowIndex) {
|
||
final start = rowIndex * 5;
|
||
final rowDays = days.skip(start).take(5).toList();
|
||
return Expanded(
|
||
child: Padding(
|
||
padding: EdgeInsets.only(top: rowIndex == 0 ? 0 : 10 * scale),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: List.generate(5, (columnIndex) {
|
||
final day =
|
||
columnIndex < rowDays.length ? rowDays[columnIndex] : null;
|
||
return Expanded(
|
||
child: Padding(
|
||
padding: EdgeInsets.only(
|
||
left: columnIndex == 0 ? 0 : 5 * scale,
|
||
right: columnIndex == 4 ? 0 : 5 * scale,
|
||
),
|
||
child: day == null
|
||
? const SizedBox.shrink()
|
||
: ReservationScheduleCard(day: day, customer: true),
|
||
),
|
||
);
|
||
}),
|
||
),
|
||
),
|
||
);
|
||
}),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ReservationStatePanel extends StatelessWidget {
|
||
const _ReservationStatePanel({required this.state, required this.scale});
|
||
|
||
final ReservationQueryDisplayState state;
|
||
final double scale;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final color = _scenarioColor(state.scenario);
|
||
final title = switch (state.scenario) {
|
||
ReservationQueryDisplayScenario.idle => '请刷饭卡或进行人脸识别',
|
||
ReservationQueryDisplayScenario.recognizing => '正在识别下一位用户',
|
||
ReservationQueryDisplayScenario.loading => '正在查询本人预约',
|
||
ReservationQueryDisplayScenario.empty => '当前没有有效预约',
|
||
ReservationQueryDisplayScenario.error => '暂时无法查询预约',
|
||
ReservationQueryDisplayScenario.expired => '预约信息已清除',
|
||
ReservationQueryDisplayScenario.success => '预约查询成功',
|
||
};
|
||
final description = switch (state.scenario) {
|
||
ReservationQueryDisplayScenario.idle => '识别成功后,这里将展示当前用户本人的预约菜单',
|
||
ReservationQueryDisplayScenario.recognizing => '请正对摄像头,识别成功后将展示本人预约菜单',
|
||
ReservationQueryDisplayScenario.loading => '请稍候,不要重复刷卡或离开识别区域',
|
||
ReservationQueryDisplayScenario.empty => '当前食堂今天及未来日期暂无可展示的已支付预约',
|
||
ReservationQueryDisplayScenario.error => '请在主屏检查网络或识别设备后重新尝试',
|
||
ReservationQueryDisplayScenario.expired => '为保护用户隐私,请重新刷卡或进行人脸识别',
|
||
ReservationQueryDisplayScenario.success => '',
|
||
};
|
||
return Container(
|
||
alignment: Alignment.center,
|
||
decoration: _surfaceDecoration(16 * scale),
|
||
child: Semantics(
|
||
liveRegion: state.scenario == ReservationQueryDisplayScenario.error,
|
||
label: '$title,$description',
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Container(
|
||
width: 108 * scale,
|
||
height: 108 * scale,
|
||
alignment: Alignment.center,
|
||
decoration: BoxDecoration(
|
||
color: color.withValues(alpha: 0.1),
|
||
shape: BoxShape.circle,
|
||
),
|
||
child: state.scenario == ReservationQueryDisplayScenario.loading
|
||
? SizedBox(
|
||
width: 48 * scale,
|
||
height: 48 * scale,
|
||
child: CircularProgressIndicator(
|
||
color: color,
|
||
strokeWidth: 5 * scale,
|
||
),
|
||
)
|
||
: Icon(
|
||
_scenarioIcon(state.scenario),
|
||
color: color,
|
||
size: 56 * scale,
|
||
),
|
||
),
|
||
SizedBox(height: 24 * scale),
|
||
Text(
|
||
title,
|
||
style: TextStyle(
|
||
color: XfjDesignTokens.ink,
|
||
fontSize: 34 * scale,
|
||
fontWeight: FontWeight.w900,
|
||
),
|
||
),
|
||
SizedBox(height: 10 * scale),
|
||
Text(
|
||
description,
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(
|
||
color: XfjDesignTokens.muted,
|
||
fontSize: 18 * scale,
|
||
height: 1.5,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ReservationFooter extends StatelessWidget {
|
||
const _ReservationFooter({required this.state, required this.scale});
|
||
|
||
final ReservationQueryDisplayState state;
|
||
final double scale;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
height: 54 * scale,
|
||
padding: EdgeInsets.symmetric(horizontal: 18 * scale),
|
||
decoration: _surfaceDecoration(14 * scale),
|
||
child: Row(
|
||
children: [
|
||
Icon(
|
||
Icons.privacy_tip_outlined,
|
||
color: XfjDesignTokens.primary,
|
||
size: 20 * scale,
|
||
),
|
||
SizedBox(width: 8 * scale),
|
||
Expanded(
|
||
child: Text(
|
||
state.continuousFaceRecognition
|
||
? '持续识别已开启,仅展示当前用户本人的有效预约'
|
||
: '仅展示当前用户本人的有效预约,页面不会显示卡号、手机号或订单号',
|
||
style: TextStyle(
|
||
color: XfjDesignTokens.muted,
|
||
fontSize: 14 * scale,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
if (state.remainingSeconds > 0) ...[
|
||
Icon(
|
||
Icons.timer_outlined,
|
||
color: XfjDesignTokens.warning,
|
||
size: 20 * scale,
|
||
),
|
||
SizedBox(width: 6 * scale),
|
||
Text(
|
||
'${state.remainingSeconds} 秒后清屏',
|
||
key: const ValueKey('reservation-privacy-countdown'),
|
||
style: TextStyle(
|
||
color: XfjDesignTokens.warning,
|
||
fontSize: 15 * scale,
|
||
fontWeight: FontWeight.w800,
|
||
fontFeatures: const [FontFeature.tabularFigures()],
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
IconData _scenarioIcon(ReservationQueryDisplayScenario scenario) {
|
||
return switch (scenario) {
|
||
ReservationQueryDisplayScenario.idle => Icons.badge_outlined,
|
||
ReservationQueryDisplayScenario.recognizing =>
|
||
Icons.face_retouching_natural_rounded,
|
||
ReservationQueryDisplayScenario.loading => Icons.search_rounded,
|
||
ReservationQueryDisplayScenario.success => Icons.check_circle_outline,
|
||
ReservationQueryDisplayScenario.empty => Icons.event_busy_outlined,
|
||
ReservationQueryDisplayScenario.error => Icons.error_outline_rounded,
|
||
ReservationQueryDisplayScenario.expired => Icons.lock_clock_outlined,
|
||
};
|
||
}
|
||
|
||
Color _scenarioColor(ReservationQueryDisplayScenario scenario) {
|
||
return switch (scenario) {
|
||
ReservationQueryDisplayScenario.idle ||
|
||
ReservationQueryDisplayScenario.recognizing ||
|
||
ReservationQueryDisplayScenario.loading ||
|
||
ReservationQueryDisplayScenario.success =>
|
||
XfjDesignTokens.primary,
|
||
ReservationQueryDisplayScenario.empty ||
|
||
ReservationQueryDisplayScenario.expired =>
|
||
XfjDesignTokens.warning,
|
||
ReservationQueryDisplayScenario.error => XfjDesignTokens.danger,
|
||
};
|
||
}
|
||
|
||
BoxDecoration _surfaceDecoration(double radius) {
|
||
return BoxDecoration(
|
||
color: XfjDesignTokens.surface,
|
||
border: Border.all(color: XfjDesignTokens.line),
|
||
borderRadius: BorderRadius.circular(radius),
|
||
boxShadow: XfjDesignTokens.surfaceShadow,
|
||
);
|
||
}
|