feat:同时核销几笔订单

feat:人脸质量检测
This commit is contained in:
dzc committed 2026-08-31 17:59:14 +08:00
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),
],
),
);
}
}