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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+624
@@ -0,0 +1,624 @@
|
||||
import 'package:common_utils/common_utils.dart';
|
||||
import 'package:ducafe_ui_core/ducafe_ui_core.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:forui/forui.dart';
|
||||
|
||||
import 'package:pj_supermarket_cloud/common/index.dart';
|
||||
|
||||
/// 核销订单完整详情抽屉,承载展开区不展示的订单基础字段和远程购物车快照。
|
||||
class VerificationOrderDetailDrawer extends StatelessWidget {
|
||||
/// 创建由页面状态驱动的核销订单完整详情抽屉。
|
||||
const VerificationOrderDetailDrawer({
|
||||
super.key,
|
||||
required this.record,
|
||||
required this.status,
|
||||
required this.order,
|
||||
required this.items,
|
||||
required this.errorMessage,
|
||||
required this.onRetry,
|
||||
required this.onClose,
|
||||
});
|
||||
|
||||
/// 列表中的核销记录,作为远程详情缺省字段和抽屉标题数据源。
|
||||
final ServiceVerificationRecord record;
|
||||
|
||||
/// 订单和购物车远程查询状态。
|
||||
final OrderRecordDetailLoadStatus status;
|
||||
|
||||
/// 服务端返回的完整订单信息,加载失败或加载中时为空。
|
||||
final ServiceHardOrder? order;
|
||||
|
||||
/// 服务端返回的订单购物车快照。
|
||||
final List<ServiceCar> items;
|
||||
|
||||
/// 查询失败时展示给操作员的恢复提示。
|
||||
final String errorMessage;
|
||||
|
||||
/// 重新请求订单和购物车数据。
|
||||
final VoidCallback onRetry;
|
||||
|
||||
/// 关闭右侧详情抽屉。
|
||||
final VoidCallback onClose;
|
||||
|
||||
/// 构建带遮罩的右侧宽抽屉,并适配收银终端安全区域。
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = FTheme.of(context);
|
||||
return Positioned.fill(
|
||||
child: ColoredBox(
|
||||
color: Colors.black.withValues(alpha: 0.35),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 1240.w),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: SafeArea(
|
||||
child: ColoredBox(
|
||||
color: XfjDesignTokens.canvas,
|
||||
child: Column(
|
||||
children: [
|
||||
_header(context, theme),
|
||||
_body(context, theme).expanded(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建紧凑标题区域,保留长订单号提示和标准关闭触控范围。
|
||||
Widget _header(BuildContext context, FThemeData theme) {
|
||||
final orderCode = _text(order?.orderNo, fallback: record.orderNo);
|
||||
return Container(
|
||||
constraints: BoxConstraints(minHeight: 64.h),
|
||||
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(
|
||||
bottom: BorderSide(color: XfjDesignTokens.line),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40.w,
|
||||
height: 40.w,
|
||||
decoration: BoxDecoration(
|
||||
color: XfjDesignTokens.primarySoft,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: FIcon(
|
||||
FAssets.icons.fileText,
|
||||
color: XfjDesignTokens.primary,
|
||||
).center(),
|
||||
),
|
||||
SizedBox(width: 12.w),
|
||||
Expanded(
|
||||
child: Tooltip(
|
||||
message: '订单号:$orderCode',
|
||||
child: <Widget>[
|
||||
Text(
|
||||
'订单详情',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.typography.xl.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'订单号:$orderCode',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.typography.sm.copyWith(
|
||||
color: XfjDesignTokens.muted,
|
||||
),
|
||||
),
|
||||
].toColumn(crossAxisAlignment: CrossAxisAlignment.start),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Semantics(
|
||||
button: true,
|
||||
label: '关闭订单详情',
|
||||
child: InkWell(
|
||||
onTap: onClose,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: FIcon(
|
||||
FAssets.icons.x,
|
||||
color: XfjDesignTokens.muted,
|
||||
size: 24.w,
|
||||
).center().constrained(
|
||||
minWidth: 48.w,
|
||||
minHeight: 48.h,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 根据远程请求状态切换加载、失败和完整详情内容。
|
||||
Widget _body(BuildContext context, FThemeData theme) {
|
||||
return switch (status) {
|
||||
OrderRecordDetailLoadStatus.loading => _loading(context),
|
||||
OrderRecordDetailLoadStatus.failure => _failure(context),
|
||||
OrderRecordDetailLoadStatus.success when order == null => _failure(
|
||||
context,
|
||||
detail: '服务端未返回订单详情,请重试',
|
||||
),
|
||||
OrderRecordDetailLoadStatus.success => _content(context, theme, order!),
|
||||
};
|
||||
}
|
||||
|
||||
/// 构建远程订单查询中的反馈状态。
|
||||
Widget _loading(BuildContext context) {
|
||||
return <Widget>[
|
||||
SizedBox(
|
||||
width: 32.w,
|
||||
height: 32.w,
|
||||
child: const CircularProgressIndicator(
|
||||
color: XfjDesignTokens.primary,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 14.h),
|
||||
Text(
|
||||
'正在查询订单完整详情...',
|
||||
style: FTheme.of(context).typography.base.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
].toColumn(mainAxisAlignment: MainAxisAlignment.center).center();
|
||||
}
|
||||
|
||||
/// 构建可重试的远程详情加载失败状态。
|
||||
Widget _failure(BuildContext context, {String? detail}) {
|
||||
return <Widget>[
|
||||
FIcon(
|
||||
FAssets.icons.circleAlert,
|
||||
size: 42.sp,
|
||||
color: XfjDesignTokens.danger,
|
||||
),
|
||||
SizedBox(height: 14.h),
|
||||
Text(
|
||||
'订单详情加载失败',
|
||||
style: FTheme.of(context).typography.lg.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
Text(
|
||||
detail ?? (errorMessage.isEmpty ? '请检查网络连接后重试' : errorMessage),
|
||||
textAlign: TextAlign.center,
|
||||
style: FTheme.of(context).typography.sm.copyWith(
|
||||
color: XfjDesignTokens.muted,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
TerminalToolbarButton(
|
||||
label: '重新加载',
|
||||
icon: FAssets.icons.refreshCw,
|
||||
style: TerminalToolbarButtonStyle.secondary,
|
||||
onTap: onRetry,
|
||||
),
|
||||
].toColumn(mainAxisAlignment: MainAxisAlignment.center).center();
|
||||
}
|
||||
|
||||
/// 按可用宽度横向或纵向排列订单信息与商品明细。
|
||||
Widget _content(
|
||||
BuildContext context,
|
||||
FThemeData theme,
|
||||
ServiceHardOrder detail,
|
||||
) {
|
||||
final orderInfo = _orderInfo(detail);
|
||||
final productInfo = _productInfo(context, theme, detail);
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(16.w),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth < 720.w) {
|
||||
return <Widget>[
|
||||
orderInfo.expanded(flex: 5),
|
||||
SizedBox(height: 12.h),
|
||||
productInfo.expanded(flex: 7),
|
||||
].toColumn(crossAxisAlignment: CrossAxisAlignment.stretch);
|
||||
}
|
||||
return <Widget>[
|
||||
orderInfo.expanded(flex: 4),
|
||||
SizedBox(width: 12.w),
|
||||
productInfo.expanded(flex: 6),
|
||||
].toRow(crossAxisAlignment: CrossAxisAlignment.stretch);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建可独立滚动的订单与核销基础信息区域。
|
||||
Widget _orderInfo(ServiceHardOrder detail) {
|
||||
final mealType = ProductHourType.values.fromCode(
|
||||
detail.hourType ?? detail.apptHourType ?? '',
|
||||
);
|
||||
return TerminalSurface(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
_VerificationDetailLine(
|
||||
label: '服务端订单 ID',
|
||||
value: _text(detail.id, fallback: '${record.orderId}'),
|
||||
),
|
||||
_VerificationDetailLine(
|
||||
label: '业务订单号',
|
||||
value: _text(detail.orderNo, fallback: record.orderNo),
|
||||
),
|
||||
_VerificationDetailLine(
|
||||
label: '用户唯一标识',
|
||||
value: _text(detail.userId),
|
||||
),
|
||||
_VerificationDetailLine(
|
||||
label: '用户名',
|
||||
value: _text(detail.username, fallback: record.username),
|
||||
),
|
||||
_VerificationDetailLine(
|
||||
label: '用户部门',
|
||||
value: _text(
|
||||
detail.userDeptName,
|
||||
fallback: record.userDeptName ?? '未提供',
|
||||
),
|
||||
),
|
||||
_VerificationDetailLine(
|
||||
label: '订单类型',
|
||||
value: _enumText(
|
||||
JsonTool.stringToOrderType(detail.orderType),
|
||||
detail.orderType,
|
||||
),
|
||||
),
|
||||
_VerificationDetailLine(
|
||||
label: '订单状态',
|
||||
value: _enumText(
|
||||
OrderStatus.values.fromCode(detail.orderStatus),
|
||||
detail.orderStatus,
|
||||
),
|
||||
),
|
||||
_VerificationDetailLine(
|
||||
label: '支付方式',
|
||||
value: _enumText(
|
||||
JsonTool.stringToOrderPayType(detail.payType),
|
||||
detail.payType,
|
||||
),
|
||||
),
|
||||
_VerificationDetailLine(
|
||||
label: '用餐时段',
|
||||
value: mealType?.title ??
|
||||
_text(detail.hourType ?? detail.apptHourType),
|
||||
),
|
||||
_VerificationDetailLine(
|
||||
label: '核销类型',
|
||||
value: record.verificationTypeName,
|
||||
),
|
||||
_VerificationDetailLine(
|
||||
label: '核销方式',
|
||||
value: record.payTypeName,
|
||||
),
|
||||
_VerificationDetailLine(
|
||||
label: '核销时间',
|
||||
value: DateUtil.formatDate(record.verificationTime),
|
||||
),
|
||||
_VerificationDetailLine(
|
||||
label: '商品摘要',
|
||||
value: _text(detail.productDesc, fallback: record.productDesc),
|
||||
),
|
||||
_VerificationDetailLine(
|
||||
label: '终端订单号',
|
||||
value: _text(detail.androidOrderCode),
|
||||
),
|
||||
_VerificationDetailLine(
|
||||
label: '下单时间',
|
||||
value: detail.createTime == null
|
||||
? '未提供'
|
||||
: DateUtil.formatDate(detail.createTime),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建商品列表和固定在底部的订单金额汇总。
|
||||
Widget _productInfo(
|
||||
BuildContext context,
|
||||
FThemeData theme,
|
||||
ServiceHardOrder detail,
|
||||
) {
|
||||
return TerminalSurface(
|
||||
padding: EdgeInsets.fromLTRB(16.w, 14.h, 16.w, 12.h),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'商品明细',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.typography.xl.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
).alignLeft(),
|
||||
SizedBox(height: 10.h),
|
||||
_productRow(
|
||||
context,
|
||||
theme,
|
||||
name: '商品',
|
||||
quantity: '数量/重量',
|
||||
unitPrice: '单价',
|
||||
totalPrice: '小计',
|
||||
header: true,
|
||||
),
|
||||
SizedBox(height: 4.h),
|
||||
if (items.isEmpty)
|
||||
<Widget>[
|
||||
FIcon(
|
||||
FAssets.icons.packageOpen,
|
||||
size: 24.sp,
|
||||
color: XfjDesignTokens.muted,
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'当前订单没有可展示的购物车明细',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.typography.sm.copyWith(
|
||||
color: XfjDesignTokens.muted,
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
.toRow(mainAxisAlignment: MainAxisAlignment.center)
|
||||
.center()
|
||||
.expanded()
|
||||
else
|
||||
ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (_, __) => const Divider(
|
||||
color: XfjDesignTokens.line,
|
||||
height: 1,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return _productRow(
|
||||
context,
|
||||
theme,
|
||||
name: _text(item.productName, fallback: '未命名商品'),
|
||||
quantity: _quantityText(item),
|
||||
unitPrice: item.productPrice,
|
||||
totalPrice: item.totalPrice,
|
||||
);
|
||||
},
|
||||
).expanded(),
|
||||
_priceSummary(context, theme, detail),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建商品表头或商品数据行,并稳定数量及金额列宽。
|
||||
Widget _productRow(
|
||||
BuildContext context,
|
||||
FThemeData theme, {
|
||||
required String name,
|
||||
required String quantity,
|
||||
required dynamic unitPrice,
|
||||
required dynamic totalPrice,
|
||||
bool header = false,
|
||||
}) {
|
||||
final style = theme.typography.sm.copyWith(
|
||||
color: header ? XfjDesignTokens.muted : XfjDesignTokens.ink,
|
||||
fontWeight: header ? FontWeight.w700 : FontWeight.w500,
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
);
|
||||
return SizedBox(
|
||||
height: header ? 34.h : 58.h,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Tooltip(
|
||||
message: name,
|
||||
child: Text(
|
||||
name,
|
||||
maxLines: header ? 1 : 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: style,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
SizedBox(
|
||||
width: 82.w,
|
||||
child: Text(
|
||||
quantity,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
style: style,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
SizedBox(
|
||||
width: 92.w,
|
||||
child: header
|
||||
? Text(
|
||||
unitPrice as String,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.right,
|
||||
style: style,
|
||||
)
|
||||
: _priceValue(context, unitPrice, style),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
SizedBox(
|
||||
width: 92.w,
|
||||
child: header
|
||||
? Text(
|
||||
totalPrice as String,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.right,
|
||||
style: style,
|
||||
)
|
||||
: _priceValue(context, totalPrice, style),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 将金额限制在固定列宽内,避免大额订单撑破表格。
|
||||
Widget _priceValue(BuildContext context, dynamic price, TextStyle style) {
|
||||
return Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: appPrice(context, price ?? 0, textStyle: style),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建商品总价、额外支付和实际扣款汇总。
|
||||
Widget _priceSummary(
|
||||
BuildContext context,
|
||||
FThemeData theme,
|
||||
ServiceHardOrder detail,
|
||||
) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 10.h),
|
||||
padding: EdgeInsets.only(top: 10.h),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(top: BorderSide(color: XfjDesignTokens.primary)),
|
||||
),
|
||||
child: <Widget>[
|
||||
_summaryLine(
|
||||
context, theme, '商品总价', detail.totalPrice ?? record.totalPrice),
|
||||
_summaryLine(context, theme, '额外支付', detail.extraPrice ?? 0),
|
||||
_summaryLine(
|
||||
context,
|
||||
theme,
|
||||
'实际扣款',
|
||||
detail.payPrice ?? record.totalPrice,
|
||||
emphasized: true,
|
||||
),
|
||||
].toColumn(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建一行金额汇总,并可突出实际扣款。
|
||||
Widget _summaryLine(
|
||||
BuildContext context,
|
||||
FThemeData theme,
|
||||
String label,
|
||||
int price, {
|
||||
bool emphasized = false,
|
||||
}) {
|
||||
final style =
|
||||
(emphasized ? theme.typography.lg : theme.typography.base).copyWith(
|
||||
color: emphasized ? XfjDesignTokens.ink : XfjDesignTokens.muted,
|
||||
fontWeight: emphasized ? FontWeight.w800 : FontWeight.w600,
|
||||
);
|
||||
return Container(
|
||||
constraints: BoxConstraints(minHeight: 38.h),
|
||||
padding: EdgeInsets.symmetric(vertical: 4.h),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: style.copyWith(height: 1.15),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
Flexible(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: appPrice(context, price, textStyle: style),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 按商品性质生成件数或重量展示文本。
|
||||
String _quantityText(ServiceCar item) {
|
||||
if (item.productNature == ProductNature.WEIGHT.code ||
|
||||
item.productNature == ProductNature.WEIGHT.name) {
|
||||
final weight = item.productWeight;
|
||||
return weight == null ? '称重商品' : '${weight.toStringAsFixed(2)} kg';
|
||||
}
|
||||
return item.productCount?.toString() ?? '-';
|
||||
}
|
||||
|
||||
/// 优先展示已识别枚举标题,未知编码回退为接口原值。
|
||||
String _enumText<T extends BaseEnum>(T? value, String? raw) {
|
||||
return value?.title ?? _text(raw);
|
||||
}
|
||||
|
||||
/// 清理接口字符串,并为缺失字段提供统一占位文本。
|
||||
String _text(String? value, {String fallback = '未提供'}) {
|
||||
final result = value?.trim();
|
||||
return result == null || result.isEmpty ? fallback : result;
|
||||
}
|
||||
}
|
||||
|
||||
/// 详情抽屉中的只读字段,长标识支持省略并可通过悬浮提示查看完整值。
|
||||
class _VerificationDetailLine extends StatelessWidget {
|
||||
/// 创建一项订单详情只读字段。
|
||||
const _VerificationDetailLine({required this.label, required this.value});
|
||||
|
||||
/// 字段名称。
|
||||
final String label;
|
||||
|
||||
/// 字段展示值。
|
||||
final String value;
|
||||
|
||||
/// 构建可省略长值并通过悬浮提示查看全文的字段。
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12.h),
|
||||
child: <Widget>[
|
||||
Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: FTheme.of(context).typography.sm.copyWith(
|
||||
color: XfjDesignTokens.muted,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6.h),
|
||||
Tooltip(
|
||||
message: value,
|
||||
child: Text(
|
||||
value,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: FTheme.of(context).typography.base.copyWith(
|
||||
color: XfjDesignTokens.ink,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
].toColumn(crossAxisAlignment: CrossAxisAlignment.start),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user