feat: refactor multi-code boxing UI with compact v2 layout

Merge box number and paichan info into a single header bar,
compress list rows to 40px with left color bars, fix bottom
operation area (scan/submit/finish) above status bar, and
shrink notification bars and status bar for more list space.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-05-18 13:58:50 +08:00
parent 3710e5955d
commit f964c76382
5 changed files with 3094 additions and 335 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,191 @@
# PRD · 多码凑箱页面布局优化
**模块**:装箱编号 > 多码凑箱模式BoxingMode.multiCode
**文件**`lib/pages/boxing_page.dart`(主要影响 `_buildManyToOneBody` 及相关子方法)
**版本**v2
**状态**:待评审
---
## 1. 背景与目标
多码凑箱页面在手持 PDA / 手机屏幕(宽度约 375390px上使用时原版布局存在以下问题
- **垂直空间浪费**AppBar、箱号区、排产信息区各自独占一行三块合计高度约 140px挤压了中间列表区的可视行数。
- **操作区随列表滚动**:扫码区和提交区位于列表下方,装入项目多时需要下拉才能看到操作控件,增加操作摩擦。
- **信息层次不清晰**:删除确认使用弹窗,打断了列表的整体感知;通知条与内容区边界不明确。
**优化目标**:在同一屏内展示更多已装入项目,同时保持操作控件始终可见,整体界面整洁、层次分明。
---
## 2. 改动总览
| 区域 | 原版高度(估算) | v2 高度(估算) | 节省 |
|------|--------------|--------------|------|
| AppBar | ~52px | 44px | 8px |
| 箱号区 + 排产信息区 | ~80px两行 | 48px合并一行 | 32px |
| 列表每行 | ~4852px | 40px | 812px/行 |
| 通知条(每条) | ~36px | 30px | 6px |
| 状态栏 | ~38px | 28px | 10px |
三项以上改动叠加后,典型场景下可多显示 **12 条**已装入记录,且操作底栏固定可见。
---
## 3. 详细改动说明
### 3.1 AppBar`_buildAppBar`
**原版**:高度约 52px标题与模式切换按钮分布松散模式徽章内边距较大。
**v2**
- 高度收紧至 **44px**Material 标准最小可触高度)。
- 标题(`装箱编号`)与模式徽章(`⇄ 多码凑箱 P2`)左右对齐,中间用弹性空白(`Spacer`)分隔。
- 模式徽章内边距从 `6px 10px` 收窄至 `3px 9px`,字号保持 `11px`,快捷键角标用半透明背景小标签呈现。
---
### 3.2 箱号区 + 排产信息区 → 合并为 `header-bar``_buildManyToOneBoxNoPanel`
**原版**箱号输入框22px 标签 + 44px 输入框)与排产信息行(排产号、已有箱数、最大箱号、详情按钮)分两个独立区块,共约 80px。
**v2**:合并为单行 `header-bar`,高度约 **48px**,布局如下:
```
[ 箱号 ██ 1 ██ ] │ W00009 已有 1 箱 · 最大箱号 1 详情
```
- 箱号输入框:宽 52px高 32px字号 18px 加粗,左侧配 `11px` 灰色小标签"箱号"。
- 竖向分隔线将箱号区与排产信息区视觉隔开。
- 排产号(`13px` 加粗)+ 统计信息(`11px` 灰色)两行叠放,`flex: 1` 撑满中间。
- `详情` 链接按钮右对齐,无数据时置灰。
**影响代码**`_buildManyToOneBoxNoPanel()`、相关 `_boxNoController` 绑定逻辑不变,仅 UI 结构调整。
---
### 3.3 已装入列表(`_buildManyToOnePackedList`
**原版**:每行 `packed-item` 高度约 4852px内含 `8px 8px` 内边距,信息横排一行。
**v2**:每行高度压缩至 **40px**,采用左侧色条 + 两行叠放的结构:
```
序号 ▌ 26BW0011 80 / 80 ✏️ 🗑
▌ WO-2026-001
```
- **左侧色条**(宽 3px高 26px圆角正常 = 绿色,编辑中 = 琥珀色,删除确认 = 红色。用色条替代原版整行背景色染色,减少视觉干扰。
- **序号列**(宽 14px灰色小字方便操作者快速定位。
- **主信息列**:总排号 `12px` 加粗(上行)+ 工单号 `11px` 灰色(下行),`overflow: ellipsis` 防止溢出。
- **数量列**`12px` 加粗绿色,格式 `已装 / 总量`
- **操作按钮**:✏️(编辑)/ 🗑(删除),触摸区域 28×28px悬停背景 `#eeeeee`
**编辑态**:数量文字替换为 52px 宽输入框,操作按钮变为 ✓(保存)/ ✕(取消)。
**删除确认态**:废弃弹窗方案,改为**行内展开次级条**(高度约 26px与触发行紧邻包含`删除` / `取消` 两个小按钮。行背景变淡红,色条变红,视觉上整体标记该条目处于危险操作中。
**列表头**sticky高度约 30px`已装入本箱` 左对齐,`N 项` 蓝色右对齐,固定在列表顶部,滚动时不消失。
---
### 3.4 底部操作区固定化(`_buildManyToOneScanArea` + `_buildManyToOneSubmitArea`
**原版**:扫码区、数量输入、确认按钮、完成本箱按钮位于列表下方,随列表内容延伸,装入项目多时操作区不可见。
**v2**:底部操作区改为**固定底栏(不参与滚动)**,从上至下分三行:
```
┌──────────────────────────────────────────┐
│ [✓ 26BW0014 · WO-004 · 50件] │ 扫码条 ~38px
│ 数量 [ 50 ] [确认] │ 提交行 ~48px
│ [ 完成本箱 ] │ 完成行 ~50px
└──────────────────────────────────────────┘
```
**扫码条**
- 等待状态:灰色背景,`📷 请扫描执行卡二维码`,文字灰色。
- 已扫码状态:浅绿背景,`✓ 总排号 · 工单号 · N件`,文字绿色,绿色边框。
**提交行**
- `数量` 标签(`11px`+ 输入框(`flex:1`,高 34px+ 确认按钮(高 34px`padding 0 18px`)。
- 未扫码时:输入框禁用,确认按钮灰色不可点。
- 已扫码时:输入框激活,确认按钮蓝色可点。
**完成本箱行**
- 全宽描边按钮(高 36px圆角 8px有已装入项时激活否则灰色。
---
### 3.5 通知条Alert Bars
**原版**:每条高度约 36px字号 13px内边距 `8px 16px`
**v2**
- 高度压缩至约 **30px**,内边距 `6px 12px`,字号 `12px`
- 多条通知可叠加显示(同步条 + 警告条并存场景6
- 各通知类型颜色语义不变:橙(同步中)/ 琥珀(警告)/ 蓝(信息)/ 黄(重复箱号)。
- 同步条保留旋转动画小图标(`12px``0.7s` 线性)。
---
### 3.6 状态栏(`StatusBar`
**原版**:高度约 38px背景 `#f0f0f0`,内边距 `12px 16px`
**v2**
- 高度压缩至约 **28px**,内边距 `6px 12px`
- 状态点直径从 8px 缩至 7px文字从 14px 缩至 12px。
- 颜色语义不变(蓝/绿/橙/红/琥珀)。
---
## 4. 整体布局结构v2
```
┌──────────────────────────────────────────┐ 44px
│ 装箱编号 [⇄ 多码凑箱 P2] │ AppBar
├──────────────────────────────────────────┤
│ [通知条,可叠加,每条 ~30px] │ 可选
├──────────────────────────────────────────┤ 48px
│ 箱号 [ 1 ] │ W00009 已有1箱·最大箱1 详情│ header-bar
├──────────────────────────────────────────┤
│ 已装入本箱sticky 3 项 │ ~30px
│ 1 ▌ 26BW0011 WO-001 80/80 ✏️ 🗑 │ 40px
│ 2 ▌ 26BW0012 WO-002 60/60 ✏️ 🗑 │ 40px
│ 3 ▌ 26BW0013 WO-003 40/40 ✏️ 🗑 │ 40px ← 可滚动
│ ... │
├──────────────────────────────────────────┤
│ [✓ 26BW0014 · WO-004 · 50件] │ ~38px 固定
│ 数量 [ 50 ] [确认] │ ~48px 固定
│ [ 完成本箱 ] │ ~50px 固定
├──────────────────────────────────────────┤ 28px
│ ● 装箱成功,请继续扫码 │ 状态栏
└──────────────────────────────────────────┘
```
---
## 5. 影响范围
| 方法/组件 | 改动类型 |
|-----------|---------|
| `_buildManyToOneBody()` | 结构重组(列表区 + 固定底栏) |
| `_buildManyToOneBoxNoPanel()` | 重写(合并为 header-bar |
| `_buildManyToOnePackedList()` | 重写(行高、色条、内联删除确认) |
| `_buildManyToOneScanArea()` | 移入固定底栏,样式调整 |
| `_buildManyToOneSubmitArea()` | 移入固定底栏,高度调整 |
| `_buildAppBar()` 多码分支 | 高度和内边距调整 |
| `StatusBar` widget | 尺寸参数调整 |
| Alert bar 区域 | 内边距和字号调整 |
业务逻辑(`decideMultiCodePaichanContext``_applyAutoFill``_onSubmitSuccess``_finishManyToOneBox` 等)**不受影响**。
---
## 6. 不在本次范围内
- 单码装箱模式(`BoxingMode.single`)的布局不做调整。
- 字体、主题色、品牌色系不做变更。
- 横屏/平板适配留作后续专项。

View File

@@ -1293,14 +1293,12 @@ class _BoxingPageState extends State<BoxingPage> {
onKeyEvent: _onKeyEvent, onKeyEvent: _onKeyEvent,
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Row( title: const Text('装箱编号'),
children: [
const Text('装箱编号'),
const SizedBox(width: 12),
Expanded(child: _buildModeHeader()),
],
),
titleSpacing: 12, titleSpacing: 12,
actions: [
_buildModePill(),
const SizedBox(width: 8),
],
), ),
body: Column( body: Column(
children: [ children: [
@@ -1308,26 +1306,26 @@ class _BoxingPageState extends State<BoxingPage> {
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8, vertical: 6,
horizontal: 16, horizontal: 12,
), ),
color: Colors.orange.shade50, color: Colors.orange.shade50,
child: Row( child: Row(
children: [ children: [
SizedBox( SizedBox(
width: 16, width: 12,
height: 16, height: 12,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2, strokeWidth: 1.5,
color: Colors.orange.shade700, color: Colors.orange.shade700,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 6),
Text( Text(
'正在同步转运数据...', '正在同步转运数据...',
style: TextStyle( style: TextStyle(
color: Colors.orange.shade900, color: Colors.orange.shade900,
fontSize: 13, fontSize: 12,
), ),
), ),
], ],
@@ -1337,40 +1335,39 @@ class _BoxingPageState extends State<BoxingPage> {
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8, vertical: 6,
horizontal: 16, horizontal: 12,
), ),
color: Colors.amber.shade100, color: Colors.amber.shade100,
child: Text( child: Text(
_autoWarnings.join(''), _autoWarnings.join(''),
style: TextStyle(color: Colors.amber.shade900, fontSize: 13), style: TextStyle(color: Colors.amber.shade900, fontSize: 12),
), ),
), ),
// 重复箱号警告条(保留,因为这是输入区关联的即时反馈)
if (_paichanSwitchNotice != null) if (_paichanSwitchNotice != null)
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8, vertical: 6,
horizontal: 16, horizontal: 12,
), ),
color: Colors.blue.shade50, color: Colors.blue.shade50,
child: Text( child: Text(
_paichanSwitchNotice!, _paichanSwitchNotice!,
style: TextStyle(color: Colors.blue.shade900, fontSize: 13), style: TextStyle(color: Colors.blue.shade900, fontSize: 12),
), ),
), ),
if (_isDuplicateBoxNo && _phase == _Phase.scanned) if (_isDuplicateBoxNo && _phase == _Phase.scanned)
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 8, vertical: 6,
horizontal: 16, horizontal: 12,
), ),
color: Colors.amber.shade100, color: Colors.amber.shade100,
child: Text( child: Text(
'箱号 ${_boxNoController.text} 已存在,请重新输入', '箱号 ${_boxNoController.text} 已存在,请重新输入',
style: TextStyle(color: Colors.amber.shade900, fontSize: 13), style: TextStyle(color: Colors.amber.shade900, fontSize: 12),
), ),
), ),
@@ -1438,23 +1435,6 @@ class _BoxingPageState extends State<BoxingPage> {
), ),
), ),
if (_mode == BoxingMode.multiCode)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
child: OutlinedButton(
onPressed: _visibleManyToOnePackedItems.isEmpty
? null
: _finishManyToOneBox,
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(40),
),
child: const Text('完成本箱'),
),
),
// 底部状态栏 // 底部状态栏
StatusBar(dotColor: effectiveDot, text: effectiveText), StatusBar(dotColor: effectiveDot, text: effectiveText),
], ],
@@ -1463,53 +1443,49 @@ class _BoxingPageState extends State<BoxingPage> {
); );
} }
Widget _buildModeHeader() { Widget _buildModePill() {
final isSingle = _mode == BoxingMode.singleCode; final isSingle = _mode == BoxingMode.singleCode;
final backgroundColor = isSingle final bgColor =
? Colors.blue.shade700 isSingle ? Colors.blue.shade700 : Colors.orange.shade700;
: Colors.orange.shade700;
final foregroundColor = Colors.white;
return Material( return GestureDetector(
borderRadius: BorderRadius.circular(8),
color: backgroundColor,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: _isAutoProcessing ? null : _cycleMode, onTap: _isAutoProcessing ? null : _cycleMode,
child: Container( child: Container(
width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3),
height: 36, decoration: BoxDecoration(
padding: const EdgeInsets.symmetric(horizontal: 10), color: bgColor,
borderRadius: BorderRadius.circular(12),
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Icons.swap_horiz, color: foregroundColor, size: 18), const Icon(Icons.swap_horiz, color: Colors.white, size: 14),
const SizedBox(width: 6), const SizedBox(width: 4),
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
child: Text(
_modeLabel,
maxLines: 1,
style: TextStyle(
color: foregroundColor,
fontSize: 18,
fontWeight: FontWeight.w800,
),
),
),
),
const SizedBox(width: 8),
Text( Text(
'P2', _modeLabel,
style: TextStyle( style: const TextStyle(
color: foregroundColor.withValues(alpha: 0.88), color: Colors.white,
fontSize: 12, fontSize: 11,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
), ),
], const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.22),
borderRadius: BorderRadius.circular(4),
), ),
child: const Text(
'P2',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
),
],
), ),
), ),
); );
@@ -1655,40 +1631,70 @@ class _BoxingPageState extends State<BoxingPage> {
Widget _buildManyToOneBody(ColorScheme colorScheme) { Widget _buildManyToOneBody(ColorScheme colorScheme) {
final hasScan = _zongpaiNo != null && _phase == _Phase.scanned; final hasScan = _zongpaiNo != null && _phase == _Phase.scanned;
return SingleChildScrollView( final visibleItems = _visibleManyToOnePackedItems;
padding: const EdgeInsets.all(12),
child: Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildManyToOneBoxNoPanel(), // Header bar: box number + paichan info merged
if (_paichanNo != null) ...[ _buildManyToOneHeaderBar(),
const SizedBox(height: 8), // List header (sticky above scroll area)
_buildManyToOneInfoRow(colorScheme), _buildManyToOneListHeader(visibleItems.length),
], // Scrollable list
const SizedBox(height: 12), Expanded(
_buildManyToOnePackedList(), child: visibleItems.isEmpty
const Divider(height: 24), ? Center(
_buildManyToOneScanArea(hasScan), child: Text(
const SizedBox(height: 10), '尚未装入任何物料',
_buildManyToOneSubmitArea(hasScan), style: TextStyle(
], fontSize: 13,
color: Colors.grey.shade500,
fontStyle: FontStyle.italic,
), ),
),
)
: ListView.builder(
itemCount: visibleItems.length,
itemBuilder: (context, index) {
return _buildManyToOnePackedRow(
visibleItems[index],
index + 1,
);
},
),
),
// Fixed bottom area: scan bar + submit + finish
_buildManyToOneBottomArea(hasScan),
],
); );
} }
Widget _buildManyToOneBoxNoPanel() { // ── Header bar: box number + paichan info ──
return Column(
children: [ Widget _buildManyToOneHeaderBar() {
Row( final hasDetail = _existingBoxes.isNotEmpty;
children: [ return Container(
const Text( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
'当前箱号', decoration: BoxDecoration(
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700), color: Colors.grey.shade50,
border: Border(
bottom: BorderSide(color: Colors.grey.shade300),
), ),
const SizedBox(width: 12), ),
child: Row(
children: [
// Box number
const Text(
'箱号',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Colors.black54,
),
),
const SizedBox(width: 6),
SizedBox( SizedBox(
width: 96, width: 52,
height: 44, height: 32,
child: TextField( child: TextField(
controller: _boxNoController, controller: _boxNoController,
focusNode: _boxNoFocusNode, focusNode: _boxNoFocusNode,
@@ -1705,107 +1711,182 @@ class _BoxingPageState extends State<BoxingPage> {
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
borderSide: BorderSide(color: Colors.grey.shade500), borderSide: BorderSide(color: Colors.grey.shade400),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide(color: Colors.blue, width: 1.5),
), ),
), ),
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w800),
),
),
// Divider
Container(
width: 1,
height: 28,
margin: const EdgeInsets.symmetric(horizontal: 10),
color: Colors.grey.shade300,
),
// Paichan info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
_paichanNo ?? '--',
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontSize: 13,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
),
),
],
),
const SizedBox(height: 12),
Divider(height: 1, color: Colors.grey.shade300),
],
);
}
Widget _buildManyToOneInfoRow(ColorScheme colorScheme) {
return Row(
children: [
Expanded(
child: Text(
'${_paichanNo ?? "--"} 已有${_existingBoxes.length}箱 最大箱号$_maxBoxNo',
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
Text(
'已有 ${_existingBoxes.length} 箱 · 最大箱号 $_maxBoxNo',
style: TextStyle(fontSize: 11, color: Colors.grey.shade500),
overflow: TextOverflow.ellipsis,
), ),
],
),
),
// Detail link
TextButton( TextButton(
onPressed: _existingBoxes.isEmpty ? null : _openDetail, onPressed: hasDetail ? _openDetail : null,
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 4),
minimumSize: Size.zero, minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap, tapTargetSize: MaterialTapTargetSize.shrinkWrap,
), ),
child: Text( child: Text(
'详情', '详情',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 11,
color: _existingBoxes.isEmpty color: hasDetail
? Colors.grey.shade400 ? Theme.of(context).colorScheme.primary
: colorScheme.primary, : Colors.grey.shade400,
), ),
), ),
), ),
], ],
),
); );
} }
Widget _buildManyToOnePackedList() { // ── List header ──
final visibleItems = _visibleManyToOnePackedItems;
return Column( Widget _buildManyToOneListHeader(int count) {
crossAxisAlignment: CrossAxisAlignment.start, return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
decoration: BoxDecoration(
color: Colors.white,
border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
),
child: Row(
children: [ children: [
Text( const Expanded(
'已装入本箱(${visibleItems.length}项)', child: Text(
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700), '已装入本箱',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: Colors.black54,
letterSpacing: 0.5,
),
),
), ),
const SizedBox(height: 6),
if (visibleItems.isEmpty)
Text( Text(
'(空)', '$count',
style: TextStyle(fontSize: 13, color: Colors.grey.shade600), style: TextStyle(
) fontSize: 11,
else fontWeight: FontWeight.w700,
...visibleItems.map(_buildManyToOnePackedRow), color: Theme.of(context).colorScheme.primary,
),
),
], ],
),
); );
} }
Widget _buildManyToOnePackedRow(_ManyToOnePackedItem item) { // ── Packed item row ──
Widget _buildManyToOnePackedRow(_ManyToOnePackedItem item, int index) {
final editing = _editingPackedItemId == item.boxItemId; final editing = _editingPackedItemId == item.boxItemId;
final deleting = _deletingPackedItemId == item.boxItemId; final deleting = _deletingPackedItemId == item.boxItemId;
final totalText = item.totalQuantity?.toString() ?? '--'; final totalText = item.totalQuantity?.toString() ?? '--';
return Container(
margin: const EdgeInsets.only(bottom: 6), final barColor =
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), editing ? Colors.amber : (deleting ? Colors.red : Colors.green);
return Column(
children: [
Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: editing ? Colors.orange.shade50 : Colors.grey.shade50, color: editing
border: Border.all( ? Colors.yellow.shade50
color: editing ? Colors.orange : Colors.grey.shade300, : (deleting ? Colors.red.shade50 : null),
border: Border(
bottom: BorderSide(color: Colors.grey.shade200),
), ),
borderRadius: BorderRadius.circular(6),
), ),
child: Column( child: Row(
children: [ children: [
Row( // Index
children: [ SizedBox(
Expanded( width: 14,
child: Text( child: Text(
'${item.zongpaiNo} ${item.workOrderNo ?? "--"}', '$index',
style: const TextStyle( textAlign: TextAlign.right,
fontSize: 13, style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.grey.shade500,
),
),
),
const SizedBox(width: 6),
// Color bar
Container(
width: 3,
height: 26,
decoration: BoxDecoration(
color: barColor,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 6),
// Main info
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.zongpaiNo,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
Text(
item.workOrderNo ?? '--',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade500,
), ),
overflow: TextOverflow.ellipsis,
),
],
),
),
// Quantity / edit input
if (editing) if (editing)
SizedBox( SizedBox(
width: 56, width: 52,
height: 32, height: 26,
child: TextField( child: TextField(
keyboardType: TextInputType.none, keyboardType: TextInputType.none,
inputFormatters: [FilteringTextInputFormatter.digitsOnly], inputFormatters: [FilteringTextInputFormatter.digitsOnly],
@@ -1816,45 +1897,54 @@ class _BoxingPageState extends State<BoxingPage> {
offset: _editingPackedQuantity.length, offset: _editingPackedQuantity.length,
), ),
onChanged: (value) => _editingPackedQuantity = value, onChanged: (value) => _editingPackedQuantity = value,
decoration: const InputDecoration( decoration: InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 6), contentPadding: const EdgeInsets.symmetric(horizontal: 4),
border: OutlineInputBorder(), border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide:
const BorderSide(color: Colors.amber, width: 1.5),
),
),
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
), ),
), ),
) )
else else
Text( Text(
'${item.quantity} / $totalText', '${item.quantity} / $totalText',
style: const TextStyle(fontSize: 13), style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: deleting ? Colors.red : Colors.green.shade700,
), ),
const SizedBox(width: 4), ),
// Action buttons
if (editing) ...[ if (editing) ...[
IconButton( _iconBtn(
tooltip: '保存', Icons.check,
visualDensity: VisualDensity.compact, color: Colors.green,
icon: const Icon(Icons.check, size: 18), onTap: _isSubmitting ? null : () => _savePackedItem(item),
onPressed: _isSubmitting ? null : () => _savePackedItem(item),
), ),
IconButton( _iconBtn(
tooltip: '取消', Icons.close,
visualDensity: VisualDensity.compact, color: Colors.grey,
icon: const Icon(Icons.close, size: 18), onTap:
onPressed: _isSubmitting ? null : _cancelEditPackedItem, _isSubmitting ? null : _cancelEditPackedItem,
), ),
] else ...[ ] else ...[
IconButton( _iconBtn(
tooltip: '修改', Icons.edit,
visualDensity: VisualDensity.compact, color: Colors.orange.shade700,
icon: const Icon(Icons.edit, size: 18), onTap: _isSubmitting
onPressed: _isSubmitting
? null ? null
: () => _startEditPackedItem(item), : () => _startEditPackedItem(item),
), ),
IconButton( _iconBtn(
tooltip: '删除', Icons.delete_outline,
visualDensity: VisualDensity.compact, color: Colors.red,
icon: const Icon(Icons.delete_outline, size: 18), onTap: _isSubmitting
onPressed: _isSubmitting
? null ? null
: () { : () {
setState(() { setState(() {
@@ -1867,51 +1957,165 @@ class _BoxingPageState extends State<BoxingPage> {
], ],
], ],
), ),
),
// Inline delete confirmation
if (deleting) if (deleting)
Padding( Container(
padding: const EdgeInsets.only(top: 4), padding: const EdgeInsets.only(
left: 34,
right: 12,
top: 2,
bottom: 5,
),
decoration: BoxDecoration(
color: Colors.red.shade50,
border: Border(
bottom: BorderSide(color: Colors.red.shade200),
),
),
child: Row( child: Row(
children: [ children: [
const Expanded( const Expanded(
child: Text('确认删除此条记录?', style: TextStyle(fontSize: 13)), child: Text(
'确认删除?',
style: TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w500,
),
),
), ),
TextButton( TextButton(
onPressed: _isSubmitting onPressed: _isSubmitting
? null ? null
: () => _deletePackedItem(item), : () => _deletePackedItem(item),
child: const Text(''), style: TextButton.styleFrom(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
backgroundColor: Colors.red.shade100,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
), ),
),
child: const Text(
'删除',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Colors.red,
),
),
),
const SizedBox(width: 4),
TextButton( TextButton(
onPressed: _isSubmitting onPressed: _isSubmitting
? null ? null
: () => setState(() => _deletingPackedItemId = null), : () => setState(() => _deletingPackedItemId = null),
child: const Text(''), style: TextButton.styleFrom(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: const Text(
'取消',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Colors.grey,
),
),
), ),
], ],
), ),
), ),
], ],
);
}
// ── Compact icon button helper ──
Widget _iconBtn(IconData icon, {Color? color, VoidCallback? onTap}) {
return SizedBox(
width: 28,
height: 28,
child: IconButton(
icon: Icon(icon, size: 16),
color: color,
onPressed: onTap,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
),
);
}
// ── Fixed bottom area ──
Widget _buildManyToOneBottomArea(bool hasScan) {
final hasItems = _visibleManyToOnePackedItems.isNotEmpty;
return Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border(top: BorderSide(color: Colors.grey.shade300)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildManyToOneScanBar(hasScan),
_buildManyToOneSubmitRow(hasScan),
Padding(
padding: const EdgeInsets.fromLTRB(12, 6, 12, 8),
child: SizedBox(
width: double.infinity,
height: 36,
child: OutlinedButton(
onPressed: hasItems ? _finishManyToOneBox : null,
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
side: BorderSide(
color: hasItems
? Colors.grey.shade600
: Colors.grey.shade300,
),
),
child: Text(
'完成本箱',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: hasItems
? Colors.black87
: Colors.grey.shade400,
),
),
),
),
),
],
), ),
); );
} }
Widget _buildManyToOneScanArea(bool hasScan) { Widget _buildManyToOneScanBar(bool hasScan) {
if (!hasScan) { if (!hasScan) {
return Container( return Container(
width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
padding: const EdgeInsets.all(14), color: Colors.grey.shade50,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: const Row( child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(Icons.qr_code_scanner, color: Colors.grey, size: 20), Icon(Icons.qr_code_scanner, color: Colors.grey, size: 16),
SizedBox(width: 8), SizedBox(width: 8),
Text( Text(
'请扫描执行卡二维码', '请扫描执行卡二维码',
style: TextStyle(color: Colors.grey, fontSize: 14), style: TextStyle(
color: Colors.grey,
fontSize: 13,
fontWeight: FontWeight.w400,
),
), ),
], ],
), ),
@@ -1919,20 +2123,25 @@ class _BoxingPageState extends State<BoxingPage> {
} }
return Container( return Container(
width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: Colors.green, width: 1.5), color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8), border: Border(
bottom: BorderSide(color: Colors.green.shade200),
),
), ),
child: Row( child: Row(
children: [ children: [
const Icon(Icons.check_circle, color: Colors.green, size: 18), const Icon(Icons.check_circle, color: Colors.green, size: 15),
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded( Expanded(
child: Text( child: Text(
'${_zongpaiNo ?? ""} ${_workOrderNo ?? "--"} ${_erpQuantity ?? "--"}', '${_zongpaiNo ?? ""} · ${_workOrderNo ?? "--"} · ${_erpQuantity ?? "--"}',
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700), style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.green.shade700,
),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
@@ -1941,24 +2150,24 @@ class _BoxingPageState extends State<BoxingPage> {
); );
} }
Widget _buildManyToOneSubmitArea(bool hasScan) { Widget _buildManyToOneSubmitRow(bool hasScan) {
return Row( return Padding(
crossAxisAlignment: CrossAxisAlignment.end, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
children: [ child: Row(
Expanded( crossAxisAlignment: CrossAxisAlignment.center,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'数量', '数量',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 11,
color: hasScan ? Colors.black54 : Colors.grey, fontWeight: FontWeight.w600,
color: hasScan ? Colors.black54 : Colors.grey.shade400,
), ),
), ),
const SizedBox(height: 4), const SizedBox(width: 6),
SizedBox( Expanded(
height: 40, child: SizedBox(
height: 34,
child: TextField( child: TextField(
controller: _quantityController, controller: _quantityController,
focusNode: _quantityFocusNode, focusNode: _quantityFocusNode,
@@ -1970,30 +2179,47 @@ class _BoxingPageState extends State<BoxingPage> {
onSubmitted: (_) => _submitFromKeyboard(), onSubmitted: (_) => _submitFromKeyboard(),
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(horizontal: 10), contentPadding: const EdgeInsets.symmetric(horizontal: 10),
border: const OutlineInputBorder(), border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide( borderSide: BorderSide(
color: _quantityTooHigh color: _quantityTooHigh
? Colors.red ? Colors.red
: Colors.grey.shade400, : Colors.grey.shade300,
), ),
), ),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
borderSide: BorderSide(color: Colors.grey.shade200),
),
filled: !hasScan,
fillColor: Colors.grey.shade100,
),
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
), ),
), ),
), ),
],
),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
SizedBox( SizedBox(
height: 40, height: 34,
child: ElevatedButton( child: ElevatedButton(
onPressed: _canSubmit ? _submit : null, onPressed: _canSubmit ? _submit : null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: _canSubmit backgroundColor: _canSubmit
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Colors.grey.shade300, : Colors.grey.shade200,
foregroundColor: _canSubmit ? Colors.white : Colors.grey.shade600, foregroundColor: _canSubmit
? Colors.white
: Colors.grey.shade400,
padding: const EdgeInsets.symmetric(horizontal: 18),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
), ),
child: _isSubmitting child: _isSubmitting
? const SizedBox( ? const SizedBox(
@@ -2004,10 +2230,17 @@ class _BoxingPageState extends State<BoxingPage> {
color: Colors.white, color: Colors.white,
), ),
) )
: const Text('确认', style: TextStyle(fontSize: 15)), : const Text(
'确认',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
), ),
), ),
], ],
),
); );
} }

View File

@@ -33,23 +33,23 @@ class StatusBar extends StatelessWidget {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
color: colorScheme.surfaceContainerHighest, color: colorScheme.surfaceContainerHighest,
child: Row( child: Row(
children: [ children: [
Container( Container(
width: 8, width: 7,
height: 8, height: 7,
decoration: BoxDecoration( decoration: BoxDecoration(
color: _resolveDotColor(context), color: _resolveDotColor(context),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 7),
Expanded( Expanded(
child: Text( child: Text(
text, text,
style: const TextStyle(fontSize: 14), style: const TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),