import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'dart:math' as math; import 'package:flutter/material.dart'; import '../core/theme/context_tokens.dart'; import '../core/theme/app_dims.g.dart'; import '../core/theme/app_fonts.dart'; /// 右侧滑入抽屉呈现(对齐原型 .drawer / .drawer-mask)。 /// useRootNavigator:false → 只覆盖内容区(分支 Navigator),不盖侧栏/顶栏。 const _kDrawerMask = Color(0x52000000); // ds-ignore: .drawer-mask 遮罩固定色(黑 32%) Future showOrderDetailDrawer( BuildContext context, { required WidgetBuilder builder, }) { return showGeneralDialog( context: context, useRootNavigator: false, barrierDismissible: true, barrierLabel: '关闭', // .drawer-mask scrim(抽屉遮罩固定色) barrierColor: _kDrawerMask, transitionDuration: const Duration(milliseconds: 250), pageBuilder: (ctx, _, __) { final w = math.min(600.0, MediaQuery.of(ctx).size.width * 0.94); return Align( alignment: Alignment.centerRight, child: Material( color: ctx.tokens.surface, elevation: 16, // 抽屉在 app_shell 的全局 SelectionArea 之外(独立 overlay)→ 自带一层,文字可选中复制。 child: SelectionArea( child: SizedBox( width: w, height: double.infinity, child: builder(ctx)), ), ), ); }, transitionBuilder: (ctx, anim, _, child) => SlideTransition( position: Tween(begin: const Offset(1, 0), end: Offset.zero) .animate(CurvedAnimation(parent: anim, curve: Curves.easeOutCubic)), child: child, ), ); } /// 抽屉明细行(对齐原型 .lines .lr,5 列:商品·编码 / 系列·规格 / 数量 / 单价 / 金额)。 class OrderLine { final String name; final String code; final String series; final String spec; final String qty; final String price; // 已算好文案(或「待定价」) final String amount; // 出库管理员 6 列形态(2026-07 定价重设计):成本价 + 利润; // 两者非空时明细表渲染 数量/成本价/售价(price)/利润 四数值列,amount 不用。 final String? cost; final String? profit; final bool pending; // 单价<=0:单价/金额显示待定价样式 const OrderLine({ required this.name, required this.code, required this.series, required this.spec, required this.qty, required this.price, this.amount = '', this.cost, this.profit, this.pending = false, }); } /// 抽屉底部操作分组(对齐原型 .dact-group:标签 + 一排按钮)。 class DrawerActionGroup { final String label; final List buttons; const DrawerActionGroup(this.label, this.buttons); } /// 详情抽屉骨架(对齐原型 openDetail 布局): /// 头(单号+X) / 信息行(.drow) / 明细标题 / 明细表(.lines) / 合计(.ltotal) / 操作组(.dactions)。 class OrderDetailDrawer extends StatelessWidget { final String title; final List<({String label, Widget value})> infoRows; final String linesLabel; // 入库明细 / 出库明细 final List lines; final String totalText; // 合计金额(已格式化) final String? profitText; // 合计利润(仅出库·管理员传入;null 不渲染) final String priceLabel; // 单价列头(入库=进价(单瓶)/出库=售价) final String amountLabel; // 金额列头(入库=总进价/出库 operator=小计) final List actionGroups; const OrderDetailDrawer({ super.key, required this.title, required this.infoRows, required this.linesLabel, required this.lines, required this.totalText, this.profitText, this.priceLabel = '单价', this.amountLabel = '金额', required this.actionGroups, }); @override Widget build(BuildContext context) { final t = context.tokens; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ── 头:单号 + 关闭 ──(.drawer-head) Container( padding: const EdgeInsets.fromLTRB(20, 18, 16, 18), decoration: BoxDecoration( border: Border(bottom: BorderSide(color: t.border))), child: Row( children: [ Expanded( child: Text(title, style: TextStyle( fontSize: AppDims.fsH2, fontWeight: FontWeight.w700, color: t.heading)), ), IconButton( icon: Icon(LucideIcons.x, size: 22, color: t.muted), splashRadius: 20, onPressed: () => Navigator.of(context).pop(), ), ], ), ), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 18, 20, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ── 信息行(.drow)── for (final r in infoRows) _DrawerRow(label: r.label, value: r.value), // ── 明细标题 ── Padding( padding: const EdgeInsets.fromLTRB(0, 16, 0, 8), child: Text(linesLabel, style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)), ), _LinesTable( lines: lines, priceLabel: priceLabel, amountLabel: amountLabel), // ── 合计(.ltotal)── Padding( padding: const EdgeInsets.fromLTRB(4, 8, 4, 0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Text('合计金额 ', style: TextStyle( fontSize: AppDims.fsBody, color: t.muted)), const SizedBox(width: 6), Text(totalText, style: TextStyle( fontSize: 18, fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, fontWeight: FontWeight.w700, color: t.accent)), ], ), ), if (profitText != null) Padding( padding: const EdgeInsets.fromLTRB(4, 6, 4, 0), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Text('合计利润 ', style: TextStyle( fontSize: AppDims.fsBody, color: t.muted)), const SizedBox(width: 6), Text(profitText!, style: TextStyle( fontSize: AppDims.fsTitle, fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, fontWeight: FontWeight.w700, color: profitText!.startsWith('-') ? t.danger : t.success)), ], ), ), // ── 操作组(.dactions)── if (actionGroups.isNotEmpty) const SizedBox(height: 18), for (var i = 0; i < actionGroups.length; i++) ...[ if (i > 0) const SizedBox(height: 14), _ActionGroup(group: actionGroups[i]), ], ], ), ), ), ], ); } } class _DrawerRow extends StatelessWidget { final String label; final Widget value; const _DrawerRow({required this.label, required this.value}); @override Widget build(BuildContext context) { final t = context.tokens; return Container( padding: const EdgeInsets.symmetric(vertical: 11), decoration: BoxDecoration( border: Border(bottom: BorderSide(color: t.borderSubtle))), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: TextStyle(fontSize: AppDims.fsBody, color: t.muted)), const Spacer(), DefaultTextStyle( style: TextStyle( fontSize: AppDims.fsBody, fontWeight: FontWeight.w600, color: t.text), child: value, ), ], ), ); } } class _LinesTable extends StatelessWidget { final List lines; final String priceLabel; final String amountLabel; const _LinesTable( {required this.lines, required this.priceLabel, required this.amountLabel}); // 任一行带成本/利润 → 出库管理员 6 列形态 bool get _withProfit => lines.any((l) => l.cost != null || l.profit != null); @override Widget build(BuildContext context) { final t = context.tokens; return Container( decoration: BoxDecoration( border: Border.all(color: t.border), borderRadius: BorderRadius.circular(AppDims.rMd), ), child: Column( children: [ // 表头 Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), decoration: BoxDecoration( color: t.thBg, borderRadius: const BorderRadius.vertical(top: Radius.circular(6)), ), child: _lineRow( context, c1: Text('商品 · 编码', style: _hStyle(t), overflow: TextOverflow.ellipsis), c2: Text('系列 / 规格', style: _hStyle(t)), c3: Text('数量', textAlign: TextAlign.right, style: _hStyle(t)), c4: _withProfit ? Text('成本价', textAlign: TextAlign.right, style: _hStyle(t)) : Text(priceLabel, textAlign: TextAlign.right, style: _hStyle(t)), c5: _withProfit ? Text('售价', textAlign: TextAlign.right, style: _hStyle(t)) : Text(amountLabel, textAlign: TextAlign.right, style: _hStyle(t)), c6: _withProfit ? Text('总售价', textAlign: TextAlign.right, style: _hStyle(t)) : null, c7: _withProfit ? Text('利润', textAlign: TextAlign.right, style: _hStyle(t)) : null, ), ), for (var i = 0; i < lines.length; i++) Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), decoration: BoxDecoration( border: i == 0 ? null : Border(top: BorderSide(color: t.borderSubtle)), ), child: _lineRow( context, c1: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(lines[i].name, style: TextStyle( fontSize: AppDims.fsBody, fontWeight: FontWeight.w600, color: t.text)), Text(lines[i].code, style: TextStyle( fontSize: AppDims.fsXs, fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, color: t.faint)), ], ), c2: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(lines[i].series, style: TextStyle(fontSize: AppDims.fsSm, color: t.text)), Text(lines[i].spec, style: TextStyle(fontSize: AppDims.fsXs, color: t.muted)), ], ), c3: Text(lines[i].qty, textAlign: TextAlign.right, style: const TextStyle( fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback)), c4: _withProfit ? _priceCell( context, lines[i].cost ?? '—', lines[i].pending) : _priceCell(context, lines[i].price, lines[i].pending), c5: _withProfit ? _priceCell(context, lines[i].price, lines[i].pending) : _priceCell(context, lines[i].amount, lines[i].pending, accent: true), c6: _withProfit ? _priceCell(context, lines[i].amount, lines[i].pending, accent: true) : null, c7: _withProfit ? _profitCell( context, lines[i].profit ?? '—', lines[i].pending) : null, ), ), ], ), ); } TextStyle _hStyle(dynamic t) => TextStyle( fontSize: AppDims.fsSm, color: t.muted, fontWeight: FontWeight.w600); Widget _priceCell(BuildContext context, String text, bool pending, {bool accent = false}) { final t = context.tokens; if (pending) { return Text(text, textAlign: TextAlign.right, style: TextStyle( fontSize: AppDims.fsXs, color: t.warn, fontWeight: FontWeight.w600)); } return Text(text, textAlign: TextAlign.right, style: TextStyle( fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, fontWeight: accent ? FontWeight.w700 : FontWeight.w400, color: accent ? t.text : t.text)); } // 5 列布局(对齐原型 grid 1fr 124 40 64 78);c6/c7 非空时为出库 7 列形态 // (数量/成本价/售价/总售价/利润,数值列等宽收窄)。 Widget _lineRow(BuildContext context, {required Widget c1, required Widget c2, required Widget c3, required Widget c4, required Widget c5, Widget? c6, Widget? c7}) { final wide = c6 != null; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: c1), const SizedBox(width: 8), // 5 列(入库)时中间列加宽左移:商品列不再吃掉全部余宽, // 「进价(单瓶)」表头一行放得下(2026-07-03 用户反馈)。 SizedBox(width: wide ? 100 : 124, child: c2), const SizedBox(width: 8), SizedBox(width: wide ? 32 : 40, child: c3), const SizedBox(width: 8), SizedBox(width: wide ? 62 : 88, child: c4), const SizedBox(width: 8), SizedBox(width: wide ? 62 : 88, child: c5), if (c6 != null) ...[ const SizedBox(width: 8), SizedBox(width: 62, child: c6), ], if (c7 != null) ...[ const SizedBox(width: 8), SizedBox(width: 62, child: c7), ], ], ); } // 利润列:负数染 danger,正常 success;待定价沿 warn 样式 Widget _profitCell(BuildContext context, String text, bool pending) { final t = context.tokens; if (pending) { return Text(text, textAlign: TextAlign.right, style: TextStyle( fontSize: AppDims.fsXs, color: t.warn, fontWeight: FontWeight.w600)); } return Text(text, textAlign: TextAlign.right, style: TextStyle( // 明细数值列缩小一号(2026-07-03 用户反馈:列多时拥挤) fontSize: AppDims.fsSm, fontFamily: AppFonts.mono, fontFamilyFallback: AppFonts.monoFallback, fontWeight: FontWeight.w600, color: text.startsWith('-') ? t.danger : t.success)); } } class _ActionGroup extends StatelessWidget { final DrawerActionGroup group; const _ActionGroup({required this.group}); @override Widget build(BuildContext context) { final t = context.tokens; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(group.label, style: TextStyle( fontSize: AppDims.fsXs, color: t.muted, fontWeight: FontWeight.w600, letterSpacing: 0.4)), const SizedBox(height: 8), Wrap(spacing: 10, runSpacing: 10, children: group.buttons), ], ); } }