diff --git a/lib/common/widgets/order/order_record_collapse.dart b/lib/common/widgets/order/order_record_collapse.dart new file mode 100644 index 0000000..317e6b7 --- /dev/null +++ b/lib/common/widgets/order/order_record_collapse.dart @@ -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 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: [ + 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: [ + FIcon( + FAssets.icons.circleAlert, + size: 28.sp, + color: XfjDesignTokens.danger, + ), + SizedBox(width: 12.w), + [ + 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 [ + _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 [ + [total.expanded(), extra.expanded()].toRow(), + SizedBox(height: 8.h), + actual.alignRight(), + ].toColumn(crossAxisAlignment: CrossAxisAlignment.stretch); + } + return [ + 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 [ + 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 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), + ], + ), + ); + } +} diff --git a/lib/pages/terminal/verification_records/widgets/verification_order_detail_drawer.dart b/lib/pages/terminal/verification_records/widgets/verification_order_detail_drawer.dart new file mode 100644 index 0000000..f014b32 --- /dev/null +++ b/lib/pages/terminal/verification_records/widgets/verification_order_detail_drawer.dart @@ -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 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: [ + 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 [ + 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 [ + 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 [ + orderInfo.expanded(flex: 5), + SizedBox(height: 12.h), + productInfo.expanded(flex: 7), + ].toColumn(crossAxisAlignment: CrossAxisAlignment.stretch); + } + return [ + 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) + [ + 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: [ + _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? 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: [ + 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), + ); + } +}