feat:同时核销几笔订单
feat:人脸质量检测
This commit is contained in:
1 parent
9cc99deed7
commit
4d1b3aa123
2 files changed
+1052
No files matched your search
@@ -0,0 +1,428 @@
|
||||
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:forui/forui.dart';
|
||||
|
||||
import '../../style/xfj_design_tokens.dart';
|
||||
import '../../utils/app/app_price_tool.dart';
|
||||
import '../app/app_price.dart';
|
||||
import '../app/terminal_toolbar.dart';
|
||||
|
||||
enum OrderRecordDetailLoadStatus { loading, success, failure }
|
||||
|
||||
class OrderRecordDetailItem {
|
||||
const OrderRecordDetailItem({
|
||||
required this.name,
|
||||
required this.quantity,
|
||||
required this.unitPrice,
|
||||
required this.totalPrice,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String quantity;
|
||||
final int? unitPrice;
|
||||
final int? totalPrice;
|
||||
}
|
||||
|
||||
/// 交易记录和核销记录共用的展开控制,统一触控尺寸、动效和无障碍状态。
|
||||
class OrderRecordExpandButton extends StatelessWidget {
|
||||
const OrderRecordExpandButton({
|
||||
super.key,
|
||||
required this.expanded,
|
||||
required this.semanticLabel,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final bool expanded;
|
||||
final String semanticLabel;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final duration = MediaQuery.disableAnimationsOf(context)
|
||||
? Duration.zero
|
||||
: const Duration(milliseconds: 180);
|
||||
return Semantics(
|
||||
button: true,
|
||||
expanded: expanded,
|
||||
label: semanticLabel,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(minWidth: 48.w, minHeight: 48.h),
|
||||
padding: EdgeInsets.symmetric(horizontal: 6.w),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedRotation(
|
||||
turns: expanded ? 0.25 : 0,
|
||||
duration: duration,
|
||||
curve: Curves.easeOutCubic,
|
||||
child: FIcon(
|
||||
FAssets.icons.chevronRight,
|
||||
size: 19.sp,
|
||||
color: XfjDesignTokens.primary,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4.w),
|
||||
Text(
|
||||
expanded ? '收起' : '展开',
|
||||
style: FTheme.of(context).typography.sm.copyWith(
|
||||
color: XfjDesignTokens.primary,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 交易记录和核销记录共用的行内订单详情,保持两类列表的展开层级和异常反馈一致。
|
||||
class OrderRecordCollapsePanel extends StatelessWidget {
|
||||
const OrderRecordCollapsePanel({
|
||||
super.key,
|
||||
required this.status,
|
||||
required this.items,
|
||||
required this.totalPrice,
|
||||
required this.extraPrice,
|
||||
required this.payPrice,
|
||||
this.errorMessage = '',
|
||||
this.onRetry,
|
||||
this.cartEmptyMessage = '当前订单没有可展示的购物车明细',
|
||||
});
|
||||
|
||||
final OrderRecordDetailLoadStatus status;
|
||||
final List<OrderRecordDetailItem> items;
|
||||
final int totalPrice;
|
||||
final int extraPrice;
|
||||
final int payPrice;
|
||||
final String errorMessage;
|
||||
final VoidCallback? onRetry;
|
||||
final String cartEmptyMessage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
|
||||
decoration: BoxDecoration(
|
||||
color: XfjDesignTokens.primarySoft.withValues(alpha: 0.34),
|
||||
border: const Border(
|
||||
top: BorderSide(color: Color(0x2E0094FF)),
|
||||
),
|
||||
),
|
||||
child: switch (status) {
|
||||
OrderRecordDetailLoadStatus.loading => _loading(context),
|
||||
OrderRecordDetailLoadStatus.failure => _failure(context),
|
||||
OrderRecordDetailLoadStatus.success => _content(context),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _loading(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 136.h,
|
||||
child: <Widget>[
|
||||
SizedBox(
|
||||
width: 28.w,
|
||||
height: 28.w,
|
||||
child: const CircularProgressIndicator(
|
||||
color: XfjDesignTokens.primary,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Text(
|
||||
'正在查询购物车明细...',
|
||||
style: FTheme.of(context).typography.base.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
].toRow(mainAxisAlignment: MainAxisAlignment.center),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _failure(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 136.h,
|
||||
child: <Widget>[
|
||||
FIcon(
|
||||
FAssets.icons.circleAlert,
|
||||
size: 28.sp,
|
||||
color: XfjDesignTokens.danger,
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
<Widget>[
|
||||
Text(
|
||||
'购物车明细加载失败',
|
||||
style: FTheme.of(context).typography.base.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
Text(
|
||||
errorMessage.isEmpty ? '请检查数据或网络连接后重试' : errorMessage,
|
||||
style: FTheme.of(context).typography.sm.copyWith(
|
||||
color: XfjDesignTokens.muted,
|
||||
),
|
||||
),
|
||||
].toColumn(crossAxisAlignment: CrossAxisAlignment.start).expanded(),
|
||||
if (onRetry != null)
|
||||
TerminalToolbarButton(
|
||||
label: '重新加载',
|
||||
icon: FAssets.icons.refreshCw,
|
||||
style: TerminalToolbarButtonStyle.secondary,
|
||||
onTap: onRetry!,
|
||||
),
|
||||
].toRow(crossAxisAlignment: CrossAxisAlignment.center),
|
||||
);
|
||||
}
|
||||
|
||||
/// 展开区仅展示购物车,订单完整字段统一由完整详情承载。
|
||||
Widget _content(BuildContext context) {
|
||||
return _cartDetails(context);
|
||||
}
|
||||
|
||||
/// 构建购物车商品表格及金额汇总。
|
||||
Widget _cartDetails(BuildContext context) {
|
||||
return <Widget>[
|
||||
_sectionTitle(
|
||||
context,
|
||||
icon: FAssets.icons.shoppingCart,
|
||||
title: '购物车明细',
|
||||
suffix: '${items.length} 种商品',
|
||||
),
|
||||
SizedBox(height: 10.h),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border.all(color: XfjDesignTokens.line),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Column(
|
||||
children: [
|
||||
_cartRow(
|
||||
context,
|
||||
const ['商品', '数量/重量', '单价', '小计'],
|
||||
header: true,
|
||||
),
|
||||
if (items.isEmpty)
|
||||
SizedBox(
|
||||
height: 72.h,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FIcon(
|
||||
FAssets.icons.packageOpen,
|
||||
size: 22.sp,
|
||||
color: XfjDesignTokens.muted,
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Flexible(
|
||||
child: Text(
|
||||
cartEmptyMessage,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: FTheme.of(context).typography.sm.copyWith(
|
||||
color: XfjDesignTokens.muted,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
...items.map(
|
||||
(item) => _cartRow(
|
||||
context,
|
||||
[
|
||||
item.name,
|
||||
item.quantity,
|
||||
item.unitPrice == null
|
||||
? '-'
|
||||
: AppPriceTool.fenToYuan(item.unitPrice!),
|
||||
item.totalPrice == null
|
||||
? '-'
|
||||
: AppPriceTool.fenToYuan(item.totalPrice!),
|
||||
],
|
||||
),
|
||||
),
|
||||
_priceSummary(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
].toColumn(crossAxisAlignment: CrossAxisAlignment.stretch);
|
||||
}
|
||||
|
||||
/// 根据购物车区域宽度调整金额汇总行数,避免金额与标签互相覆盖。
|
||||
Widget _priceSummary(BuildContext context) {
|
||||
final theme = FTheme.of(context);
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.h),
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFFAFBFC),
|
||||
border: Border(top: BorderSide(color: XfjDesignTokens.line)),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final compact = constraints.maxWidth < 640.w;
|
||||
final total = _summaryMetric(
|
||||
context,
|
||||
label: '商品总价',
|
||||
price: totalPrice,
|
||||
textStyle: theme.typography.sm,
|
||||
);
|
||||
final extra = _summaryMetric(
|
||||
context,
|
||||
label: '额外支付',
|
||||
price: extraPrice,
|
||||
textStyle: theme.typography.sm,
|
||||
);
|
||||
final actual = _summaryMetric(
|
||||
context,
|
||||
label: '实际扣款',
|
||||
price: payPrice,
|
||||
textStyle: theme.typography.xl2.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
emphasized: true,
|
||||
);
|
||||
if (compact) {
|
||||
return <Widget>[
|
||||
<Widget>[total.expanded(), extra.expanded()].toRow(),
|
||||
SizedBox(height: 8.h),
|
||||
actual.alignRight(),
|
||||
].toColumn(crossAxisAlignment: CrossAxisAlignment.stretch);
|
||||
}
|
||||
return <Widget>[
|
||||
total.expanded(),
|
||||
extra.expanded(),
|
||||
actual.expanded(),
|
||||
].toRow();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建单项金额指标,超长金额会在自身区域内缩放而不撑破布局。
|
||||
Widget _summaryMetric(
|
||||
BuildContext context, {
|
||||
required String label,
|
||||
required int price,
|
||||
required TextStyle textStyle,
|
||||
bool emphasized = false,
|
||||
}) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment:
|
||||
emphasized ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: textStyle.copyWith(
|
||||
color: emphasized ? XfjDesignTokens.ink : XfjDesignTokens.muted,
|
||||
fontWeight: emphasized ? FontWeight.w700 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Flexible(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: appPrice(context, price, textStyle: textStyle),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _sectionTitle(
|
||||
BuildContext context, {
|
||||
required SvgAsset icon,
|
||||
required String title,
|
||||
String? suffix,
|
||||
}) {
|
||||
return <Widget>[
|
||||
FIcon(icon, size: 20.sp, color: XfjDesignTokens.primary),
|
||||
SizedBox(width: 8.w),
|
||||
Text(
|
||||
title,
|
||||
style: FTheme.of(context).typography.base.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
if (suffix != null) ...[
|
||||
SizedBox(width: 8.w),
|
||||
Text(
|
||||
suffix,
|
||||
style: FTheme.of(context).typography.sm.copyWith(
|
||||
color: XfjDesignTokens.muted,
|
||||
),
|
||||
),
|
||||
],
|
||||
].toRow(mainAxisSize: MainAxisSize.min);
|
||||
}
|
||||
|
||||
Widget _cartRow(
|
||||
BuildContext context,
|
||||
List<String> values, {
|
||||
bool header = false,
|
||||
}) {
|
||||
final style = FTheme.of(context).typography.sm.copyWith(
|
||||
color: header ? XfjDesignTokens.muted : XfjDesignTokens.ink,
|
||||
fontWeight: header ? FontWeight.w700 : FontWeight.w500,
|
||||
);
|
||||
return Container(
|
||||
constraints: BoxConstraints(minHeight: header ? 40.h : 48.h),
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 6.h),
|
||||
decoration: BoxDecoration(
|
||||
color: header ? const Color(0xFFF7F8FA) : Colors.white,
|
||||
border: header
|
||||
? null
|
||||
: const Border(top: BorderSide(color: XfjDesignTokens.line)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Tooltip(
|
||||
message: values[0],
|
||||
child: Text(
|
||||
values[0],
|
||||
maxLines: header ? 1 : 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: style.copyWith(
|
||||
fontWeight: header ? FontWeight.w700 : FontWeight.w600,
|
||||
),
|
||||
),
|
||||
).expanded(flex: 5),
|
||||
Text(
|
||||
values[1],
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: style,
|
||||
).expanded(flex: 2),
|
||||
Text(
|
||||
values[2],
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
style: style,
|
||||
).expanded(flex: 2),
|
||||
Text(
|
||||
values[3],
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
style: style.copyWith(fontWeight: FontWeight.w700),
|
||||
).expanded(flex: 2),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user