Files
jiu/client/lib/widgets/order_detail_drawer.dart
T
wangjia a3a722689e feat(client): 出入库列表按原型重建(KPI/详情抽屉/详细搜索/工具栏/选中复制/窗口尺寸)
- 列表重建:KPI 卡、状态+时间列、操作改眼睛开右侧详情抽屉、重置右置
- 详情抽屉(order_detail_drawer) 100% 还原原型 + SelectionArea 可选中复制
- 详细搜索弹窗:ComboSearchField/滚轮日期、字段重构、白底盒子按钮
- 工具栏:搜索框×清除+占位精简、供应商/客户下拉取全部、状态菜单收窄
- 加载不白屏(半透明遮罩)、输入框与下拉等高、windows/macOS 默认窗口尺寸调大

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 08:48:01 +08:00

351 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../core/theme/context_tokens.dart';
import '../core/theme/app_dims.g.dart';
/// 右侧滑入抽屉呈现(对齐原型 .drawer / .drawer-mask)。
/// useRootNavigator:false → 只覆盖内容区(分支 Navigator),不盖侧栏/顶栏。
Future<T?> showOrderDetailDrawer<T>(
BuildContext context, {
required WidgetBuilder builder,
}) {
return showGeneralDialog<T>(
context: context,
useRootNavigator: false,
barrierDismissible: true,
barrierLabel: '关闭',
barrierColor: Colors.black.withValues(alpha: 0.32), // .drawer-mask scrim
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<Offset>(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;
final bool pending; // 单价<=0:单价/金额显示待定价样式
const OrderLine({
required this.name,
required this.code,
required this.series,
required this.spec,
required this.qty,
required this.price,
required this.amount,
this.pending = false,
});
}
/// 抽屉底部操作分组(对齐原型 .dact-group:标签 + 一排按钮)。
class DrawerActionGroup {
final String label;
final List<Widget> 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<OrderLine> lines;
final String totalText; // 合计金额(已格式化)
final List<DrawerActionGroup> actionGroups;
const OrderDetailDrawer({
super.key,
required this.title,
required this.infoRows,
required this.linesLabel,
required this.lines,
required this.totalText,
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(Icons.close, 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),
// ── 合计(.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: 'monospace',
fontWeight: FontWeight.w700,
color: t.accent)),
],
),
),
// ── 操作组(.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<OrderLine> lines;
const _LinesTable({required this.lines});
@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: Text('单价', textAlign: TextAlign.right, style: _hStyle(t)),
c5: Text('金额', textAlign: TextAlign.right, style: _hStyle(t)),
),
),
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: 'monospace',
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: 'monospace')),
c4: _priceCell(context, lines[i].price, lines[i].pending),
c5: _priceCell(context, lines[i].amount, lines[i].pending,
accent: true),
),
),
],
),
);
}
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: 'monospace',
fontWeight: accent ? FontWeight.w700 : FontWeight.w400,
color: accent ? t.text : t.text));
}
// 5 列布局(对齐原型 grid 1fr 124 40 64 78)。
Widget _lineRow(BuildContext context,
{required Widget c1,
required Widget c2,
required Widget c3,
required Widget c4,
required Widget c5}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: c1),
const SizedBox(width: 8),
SizedBox(width: 96, child: c2),
const SizedBox(width: 8),
SizedBox(width: 34, child: c3),
const SizedBox(width: 8),
SizedBox(width: 60, child: c4),
const SizedBox(width: 8),
SizedBox(width: 72, child: c5),
],
);
}
}
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),
],
);
}
}