From f964c7638205e16f3c623b860d9d2913df9314be Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Mon, 18 May 2026 13:58:50 +0800 Subject: [PATCH] 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 --- docs/multi-code-boxing-ui-preview.html | 1189 ++++++++++++++++++++++++ docs/multi-code-boxing-ui-v2.html | 1146 +++++++++++++++++++++++ docs/多码凑箱UI布局优化PRD.md | 191 ++++ lib/pages/boxing_page.dart | 893 +++++++++++------- lib/widgets/status_bar.dart | 10 +- 5 files changed, 3094 insertions(+), 335 deletions(-) create mode 100644 docs/multi-code-boxing-ui-preview.html create mode 100644 docs/multi-code-boxing-ui-v2.html create mode 100644 docs/多码凑箱UI布局优化PRD.md diff --git a/docs/multi-code-boxing-ui-preview.html b/docs/multi-code-boxing-ui-preview.html new file mode 100644 index 0000000..7682402 --- /dev/null +++ b/docs/multi-code-boxing-ui-preview.html @@ -0,0 +1,1189 @@ + + + + + +装箱编号 - 多码凑箱模式 UI 展示 + + + + +

装箱编号 — 多码凑箱模式 UI 展示

+

基于 boxing_page.dart 实际代码还原 | 源文件:lib/pages/boxing_page.dart (2312行)

+ + +
+
初始状态
+
已扫码识别
+
已装入多项
+
编辑/删除已装入项
+
排产号切换
+
自动同步转运
+
+ + +
+ + +
+
场景1:初始等待扫码
+
+ +
+ 装箱编号 +
+ + 多码凑箱 + P2 +
+
+ + +
+ +
+
+ 当前箱号 + +
+
+
+ + +
已装入本箱(0项)
+
(空)
+ +
+ + +
+ 📷 + 请扫描执行卡二维码 +
+ +
+ + +
+
+
数量
+ +
+ +
+
+ + +
+ +
+ + +
+
+ 等待扫码 +
+
+
+ + + + + + + + + + + + + + + + +
+ + +
+ +
+

状态栏颜色含义

+
+
蓝色 — 空闲/等待扫码
+
橙色 — 提交中/同步中
+
绿色 — 操作成功
+
红色 — 错误(无效码/提交失败)
+
黄色 — 网络异常
+
琥珀 — 警告(重复箱号)
+
+
+ +
+

多码凑箱模式 UI 结构

+
┌──────────────────────────────────────────┐ +│ AppBar: "装箱编号" + [多码凑箱] 模式按钮 (P2) │ +├──────────────────────────────────────────┤ +│ [提示条区域] │ +│ - 橙色同步条:正在同步转运数据... │ +│ - 黄色警告条:自动装箱失败的警告信息 │ +│ - 蓝色通知条:排产号已切换 │ +│ - 琥珀警告条:箱号已存在 │ +├──────────────────────────────────────────┤ +│ 当前箱号: [ N ] ← 顶部大号输入框 │ +│ ─────────────────────────────────────── │ +│ W00009 已有3箱 最大箱号3 [详情 →] │ +│ │ +│ 已装入本箱(N项) │ +│ ┌──────────────────────────────────────┐ │ +│ │ 26BW0011 WO-001 80/80 ✏️ 🗑 │ │ +│ │ 26BW0012 WO-002 60/60 ✏️ 🗑 │ │ +│ │ 26BW0013 WO-003 40/40 ✏️ 🗑 │ │ +│ └──────────────────────────────────────┘ │ +│ ─────────────────────────────────────── │ +│ [✓ 26BW0014 WO-004 50件] ← 扫码显示区 │ +│ │ +│ 数量 │ +│ [ 50 ] [确认] │ +├──────────────────────────────────────────┤ +│ [ 完成本箱 ] ← 全宽按钮 │ +├──────────────────────────────────────────┤ +│ ● 等待扫码 / 请继续扫码或完成本箱 │ +└──────────────────────────────────────────┘
+
+ +
+

核心业务流程

+
多码凑箱流程(_buildManyToOneBody): + +1. 扫码 → 解析总排号 → 调用 fetchBoxInfo API + │ + ▼ +2. 判断排产号上下文(decideMultiCodePaichanContext) + ├─ firstScan → 自动填入箱号 (maxBoxNo+1) + ├─ samePaichan → 保持当前箱号 + └─ switchedPaichan → 清空已装入列表,重置箱号 (maxBoxNo+1) + │ + ▼ +3. 自动填充剩余数量 → 等待用户确认 + ├─ 修改箱号 → 清除编辑/删除状态 + └─ 修改数量 → 实时校验是否超额 + │ + ▼ +4. 点击 [确认] → saveBoxRecord API + ├─ 成功 → 添加到 _manyToOnePackedItems + │ phase 回到 waiting + │ 状态: "装箱成功,请继续扫码" + └─ 失败 → 显示错误状态 + │ + ▼ +5. 继续扫码 / 完成本箱 + ├─ 继续扫码 → 回到步骤1 + └─ 完成本箱 → 清空已装入列表 + 箱号 +1,回到初始状态
+
+ +
+

与单码装箱模式的差异

+
    +
  • UI 布局不同:多码凑箱使用独立的 _buildManyToOneBody,将箱号提升到顶部大号输入;单码装箱使用 _buildScanArea + _buildInfoArea + _buildInputArea 传统布局。
  • +
  • 箱号管理:多码模式下不检测重复箱号(_boxNoIsDuplicate 对 multiCode 直接返回 false),允许多个总排号装入同一箱。
  • +
  • 底部按钮:多码模式显示"完成本箱"按钮(OutlinedButton),无已分配列表编辑功能;单码模式显示"返回"按钮和已分配箱的编辑/删除。
  • +
  • 提交成功后行为:多码模式将结果存入 _manyToOnePackedItems 列表,phase 回到 waiting,继续扫码;单码模式保持在 scanned 阶段,箱号 +1。
  • +
  • 排产号切换感知:多码模式通过 decideMultiCodePaichanContext 检测排产号变化,切换时清空已装入列表并弹出蓝色通知。
  • +
+
+ +
+

关键代码引用

+
    +
  • 模式枚举:BoxingMode.multiCode — boxing_page.dart:18
  • +
  • 多码体布局:_buildManyToOneBody() — boxing_page.dart:1656
  • +
  • 箱号面板:_buildManyToOneBoxNoPanel() — boxing_page.dart:1679
  • +
  • 已装入列表:_buildManyToOnePackedList() — boxing_page.dart:1756
  • +
  • 扫码区:_buildManyToOneScanArea() — boxing_page.dart:1898
  • +
  • 提交区:_buildManyToOneSubmitArea() — boxing_page.dart:1944
  • +
  • 排产号上下文:decideMultiCodePaichanContext() — boxing_context.dart:12
  • +
  • 自动填充逻辑:_applyAutoFill() — boxing_page.dart:400
  • +
  • 提交成功回调:_onSubmitSuccess() — boxing_page.dart:637
  • +
  • 完成本箱:_finishManyToOneBox() — boxing_page.dart:821
  • +
  • 状态栏组件:StatusBar — widgets/status_bar.dart:14
  • +
+
+ +
+

提示条(Alert Bars)触发条件

+
    +
  • 橙色同步条:从转运页自动跳转时,_isAutoProcessing = true 时显示
  • +
  • 黄色警告条:自动处理中部分总排号失败时,累积在 _autoWarnings 中显示
  • +
  • 蓝色通知条:排产号切换时,_paichanSwitchNotice 非空时显示
  • +
  • 琥珀重复条:箱号与已有记录冲突时显示(多码模式下不会触发)
  • +
+
+ +
+ + + + + diff --git a/docs/multi-code-boxing-ui-v2.html b/docs/multi-code-boxing-ui-v2.html new file mode 100644 index 0000000..92fcebe --- /dev/null +++ b/docs/multi-code-boxing-ui-v2.html @@ -0,0 +1,1146 @@ + + + + + +多码凑箱 UI v2 — 优化布局 + + + + +
装箱编号 — 多码凑箱 · 优化布局 v2
+

紧凑信息密度 · 固定操作底栏 · 清晰状态层次

+ + +
+
① 初始
+
② 扫码识别
+
③ 已装多项
+
④ 编辑/删除
+
⑤ 排产号切换
+
⑥ 自动同步
+
+ + +
+ + +
+
场景1 · 初始状态(等待扫码)
+
+ +
+ 装箱编号 +
+
⇄ 多码凑箱 P2
+
+ + +
+
+ 箱号 + +
+
+
+
W00009
+
已有 0 箱 · 最大箱号 0
+
+ +
+ + +
+
+ 已装入本箱 + 0 项 +
+
尚未装入任何物料
+
+ + +
+
+ 📷 + 请扫描执行卡二维码 +
+
+
+ 数量 + +
+ +
+
+ +
+
+ +
+
+ 等待扫码 +
+
+
+ + + + + + + + + + + + + + + + +
+ + + +
+ +
+

v2 布局优化要点

+
    +
  • AppBar 高度从 ~52px 压缩至 44px,标题与模式徽章并排,省去独占一行的模式按钮区
  • +
  • 箱号 + 排产信息合并为一个 header-bar(高度 ~48px),原版两行拆开共需 ~80px;节省约 32px 垂直空间
  • +
  • 已装入列表每行压缩至 40px(原 ~48-52px),信息分两行展示(总排号 / 工单号)但视觉上紧凑
  • +
  • 扫码区 + 数量输入 + 确认按钮 + 完成本箱组成固定底栏,不参与滚动,列表区可充分展示多条记录
  • +
  • 删除确认行内联展开(不弹框),高度约 26px,减少视觉干扰
  • +
  • 通知条高度从 ~36px 压缩至 ~30px,字号 12px,满足阅读同时节省空间
  • +
  • 状态栏高度 ~28px(原 ~38px),仅保留状态点 + 简短文字
  • +
+
+ +
+

状态颜色含义

+
+
蓝 — 空闲/等待
+
橙 — 提交中/同步中
+
绿 — 操作成功
+
红 — 错误
+
琥珀 — 警告
+
+
+ +
+

UI 结构(v2)

+
┌──────────────────────────────────────────┐ 44px +│ "装箱编号" [⇄ 多码凑箱 P2] │ AppBar +├──────────────────────────────────────────┤ +│ [通知条] 可叠加显示,每条 ~30px │ 可选 +├──────────────────────────────────────────┤ 48px +│ 箱号 [ 1 ] │ W00009 已有1箱·最大箱1 详情│ header-bar +├──────────────────────────────────────────┤ +│ 已装入本箱 3 项 │ list-header (sticky) +│ 1 ▌ 26BW0011 WO-2026-001 80/80 ✏🗑 │ 40px/行 +│ 2 ▌ 26BW0012 WO-2026-002 60/60 ✏🗑 │ +│ 3 ▌ 26BW0013 WO-2026-003 40/40 ✏🗑 │ ← 可滚动区域 +│ ... │ +├──────────────────────────────────────────┤ +│ [✓ 26BW0014 · WO-004 · 50件] │ ~38px 扫码条 +│ 数量 [ 50 ] [确认] │ ~48px 提交行 +│ [ 完成本箱 ] │ ~50px 完成行 +├──────────────────────────────────────────┤ ~28px +│ ● 装箱成功,请继续扫码 │ 状态栏 +└──────────────────────────────────────────┘
+
+ +
+ + + + diff --git a/docs/多码凑箱UI布局优化PRD.md b/docs/多码凑箱UI布局优化PRD.md new file mode 100644 index 0000000..8c133dc --- /dev/null +++ b/docs/多码凑箱UI布局优化PRD.md @@ -0,0 +1,191 @@ +# PRD · 多码凑箱页面布局优化 + +**模块**:装箱编号 > 多码凑箱模式(BoxingMode.multiCode) +**文件**:`lib/pages/boxing_page.dart`(主要影响 `_buildManyToOneBody` 及相关子方法) +**版本**:v2 +**状态**:待评审 + +--- + +## 1. 背景与目标 + +多码凑箱页面在手持 PDA / 手机屏幕(宽度约 375–390px)上使用时,原版布局存在以下问题: + +- **垂直空间浪费**:AppBar、箱号区、排产信息区各自独占一行,三块合计高度约 140px,挤压了中间列表区的可视行数。 +- **操作区随列表滚动**:扫码区和提交区位于列表下方,装入项目多时需要下拉才能看到操作控件,增加操作摩擦。 +- **信息层次不清晰**:删除确认使用弹窗,打断了列表的整体感知;通知条与内容区边界不明确。 + +**优化目标**:在同一屏内展示更多已装入项目,同时保持操作控件始终可见,整体界面整洁、层次分明。 + +--- + +## 2. 改动总览 + +| 区域 | 原版高度(估算) | v2 高度(估算) | 节省 | +|------|--------------|--------------|------| +| AppBar | ~52px | 44px | 8px | +| 箱号区 + 排产信息区 | ~80px(两行) | 48px(合并一行) | 32px | +| 列表每行 | ~48–52px | 40px | 8–12px/行 | +| 通知条(每条) | ~36px | 30px | 6px | +| 状态栏 | ~38px | 28px | 10px | + +三项以上改动叠加后,典型场景下可多显示 **1–2 条**已装入记录,且操作底栏固定可见。 + +--- + +## 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` 高度约 48–52px,内含 `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`)的布局不做调整。 +- 字体、主题色、品牌色系不做变更。 +- 横屏/平板适配留作后续专项。 diff --git a/lib/pages/boxing_page.dart b/lib/pages/boxing_page.dart index 1fb585f..0810aef 100644 --- a/lib/pages/boxing_page.dart +++ b/lib/pages/boxing_page.dart @@ -1293,14 +1293,12 @@ class _BoxingPageState extends State { onKeyEvent: _onKeyEvent, child: Scaffold( appBar: AppBar( - title: Row( - children: [ - const Text('装箱编号'), - const SizedBox(width: 12), - Expanded(child: _buildModeHeader()), - ], - ), + title: const Text('装箱编号'), titleSpacing: 12, + actions: [ + _buildModePill(), + const SizedBox(width: 8), + ], ), body: Column( children: [ @@ -1308,26 +1306,26 @@ class _BoxingPageState extends State { Container( width: double.infinity, padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, + vertical: 6, + horizontal: 12, ), color: Colors.orange.shade50, child: Row( children: [ SizedBox( - width: 16, - height: 16, + width: 12, + height: 12, child: CircularProgressIndicator( - strokeWidth: 2, + strokeWidth: 1.5, color: Colors.orange.shade700, ), ), - const SizedBox(width: 8), + const SizedBox(width: 6), Text( '正在同步转运数据...', style: TextStyle( color: Colors.orange.shade900, - fontSize: 13, + fontSize: 12, ), ), ], @@ -1337,40 +1335,39 @@ class _BoxingPageState extends State { Container( width: double.infinity, padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, + vertical: 6, + horizontal: 12, ), color: Colors.amber.shade100, child: Text( _autoWarnings.join(';'), - style: TextStyle(color: Colors.amber.shade900, fontSize: 13), + style: TextStyle(color: Colors.amber.shade900, fontSize: 12), ), ), - // 重复箱号警告条(保留,因为这是输入区关联的即时反馈) if (_paichanSwitchNotice != null) Container( width: double.infinity, padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, + vertical: 6, + horizontal: 12, ), color: Colors.blue.shade50, child: Text( _paichanSwitchNotice!, - style: TextStyle(color: Colors.blue.shade900, fontSize: 13), + style: TextStyle(color: Colors.blue.shade900, fontSize: 12), ), ), if (_isDuplicateBoxNo && _phase == _Phase.scanned) Container( width: double.infinity, padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, + vertical: 6, + horizontal: 12, ), color: Colors.amber.shade100, child: 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 { ), ), - 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), ], @@ -1463,53 +1443,49 @@ class _BoxingPageState extends State { ); } - Widget _buildModeHeader() { + Widget _buildModePill() { final isSingle = _mode == BoxingMode.singleCode; - final backgroundColor = isSingle - ? Colors.blue.shade700 - : Colors.orange.shade700; - final foregroundColor = Colors.white; + final bgColor = + isSingle ? Colors.blue.shade700 : Colors.orange.shade700; - return Material( - borderRadius: BorderRadius.circular(8), - color: backgroundColor, - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: _isAutoProcessing ? null : _cycleMode, - child: Container( - width: double.infinity, - height: 36, - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.swap_horiz, color: foregroundColor, size: 18), - const SizedBox(width: 6), - Flexible( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - _modeLabel, - maxLines: 1, - style: TextStyle( - color: foregroundColor, - fontSize: 18, - fontWeight: FontWeight.w800, - ), - ), - ), + return GestureDetector( + onTap: _isAutoProcessing ? null : _cycleMode, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.swap_horiz, color: Colors.white, size: 14), + const SizedBox(width: 4), + Text( + _modeLabel, + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w700, ), - const SizedBox(width: 8), - Text( + ), + 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: foregroundColor.withValues(alpha: 0.88), - fontSize: 12, - fontWeight: FontWeight.w700, + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.w600, ), ), - ], - ), + ), + ], ), ), ); @@ -1655,157 +1631,262 @@ class _BoxingPageState extends State { Widget _buildManyToOneBody(ColorScheme colorScheme) { final hasScan = _zongpaiNo != null && _phase == _Phase.scanned; - return SingleChildScrollView( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + final visibleItems = _visibleManyToOnePackedItems; + + return Column( + children: [ + // Header bar: box number + paichan info merged + _buildManyToOneHeaderBar(), + // List header (sticky above scroll area) + _buildManyToOneListHeader(visibleItems.length), + // Scrollable list + Expanded( + child: visibleItems.isEmpty + ? Center( + child: Text( + '尚未装入任何物料', + 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), + ], + ); + } + + // ── Header bar: box number + paichan info ── + + Widget _buildManyToOneHeaderBar() { + final hasDetail = _existingBoxes.isNotEmpty; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: Colors.grey.shade50, + border: Border( + bottom: BorderSide(color: Colors.grey.shade300), + ), + ), + child: Row( children: [ - _buildManyToOneBoxNoPanel(), - if (_paichanNo != null) ...[ - const SizedBox(height: 8), - _buildManyToOneInfoRow(colorScheme), - ], - const SizedBox(height: 12), - _buildManyToOnePackedList(), - const Divider(height: 24), - _buildManyToOneScanArea(hasScan), - const SizedBox(height: 10), - _buildManyToOneSubmitArea(hasScan), + // Box number + const Text( + '箱号', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Colors.black54, + ), + ), + const SizedBox(width: 6), + SizedBox( + width: 52, + height: 32, + child: TextField( + controller: _boxNoController, + focusNode: _boxNoFocusNode, + enabled: !_isSubmitting, + keyboardType: TextInputType.none, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + textAlign: TextAlign.center, + onEditingComplete: () {}, + onSubmitted: (_) => _submitFromKeyboard(), + decoration: InputDecoration( + contentPadding: EdgeInsets.zero, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + 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( + fontSize: 13, + fontWeight: FontWeight.w700, + ), + overflow: TextOverflow.ellipsis, + ), + Text( + '已有 ${_existingBoxes.length} 箱 · 最大箱号 $_maxBoxNo', + style: TextStyle(fontSize: 11, color: Colors.grey.shade500), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + // Detail link + TextButton( + onPressed: hasDetail ? _openDetail : null, + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 4), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text( + '详情', + style: TextStyle( + fontSize: 11, + color: hasDetail + ? Theme.of(context).colorScheme.primary + : Colors.grey.shade400, + ), + ), + ), ], ), ); } - Widget _buildManyToOneBoxNoPanel() { - return Column( - children: [ - Row( - children: [ - const Text( - '当前箱号', - style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700), - ), - const SizedBox(width: 12), - SizedBox( - width: 96, - height: 44, - child: TextField( - controller: _boxNoController, - focusNode: _boxNoFocusNode, - enabled: !_isSubmitting, - keyboardType: TextInputType.none, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - textAlign: TextAlign.center, - onEditingComplete: () {}, - onSubmitted: (_) => _submitFromKeyboard(), - decoration: InputDecoration( - contentPadding: EdgeInsets.zero, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(6), - borderSide: BorderSide(color: Colors.grey.shade500), - ), - ), - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.w700, - ), + // ── List header ── + + Widget _buildManyToOneListHeader(int count) { + 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: [ + const Expanded( + child: Text( + '已装入本箱', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: Colors.black54, + letterSpacing: 0.5, ), ), - ], - ), - 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, ), - ), - TextButton( - onPressed: _existingBoxes.isEmpty ? null : _openDetail, - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric(horizontal: 8), - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: Text( - '详情 →', + Text( + '$count 项', style: TextStyle( - fontSize: 13, - color: _existingBoxes.isEmpty - ? Colors.grey.shade400 - : colorScheme.primary, + fontSize: 11, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.primary, ), ), - ), - ], + ], + ), ); } - Widget _buildManyToOnePackedList() { - final visibleItems = _visibleManyToOnePackedItems; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '已装入本箱(${visibleItems.length}项)', - style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700), - ), - const SizedBox(height: 6), - if (visibleItems.isEmpty) - Text( - '(空)', - style: TextStyle(fontSize: 13, color: Colors.grey.shade600), - ) - else - ...visibleItems.map(_buildManyToOnePackedRow), - ], - ); - } + // ── Packed item row ── - Widget _buildManyToOnePackedRow(_ManyToOnePackedItem item) { + Widget _buildManyToOnePackedRow(_ManyToOnePackedItem item, int index) { final editing = _editingPackedItemId == item.boxItemId; final deleting = _deletingPackedItemId == item.boxItemId; final totalText = item.totalQuantity?.toString() ?? '--'; - return Container( - margin: const EdgeInsets.only(bottom: 6), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - decoration: BoxDecoration( - color: editing ? Colors.orange.shade50 : Colors.grey.shade50, - border: Border.all( - color: editing ? Colors.orange : Colors.grey.shade300, - ), - borderRadius: BorderRadius.circular(6), - ), - child: Column( - children: [ - Row( + + final barColor = + editing ? Colors.amber : (deleting ? Colors.red : Colors.green); + + return Column( + children: [ + Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: editing + ? Colors.yellow.shade50 + : (deleting ? Colors.red.shade50 : null), + border: Border( + bottom: BorderSide(color: Colors.grey.shade200), + ), + ), + child: Row( children: [ - Expanded( + // Index + SizedBox( + width: 14, child: Text( - '${item.zongpaiNo} ${item.workOrderNo ?? "--"}', - style: const TextStyle( - fontSize: 13, + '$index', + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 10, fontWeight: FontWeight.w600, + color: Colors.grey.shade500, ), - overflow: TextOverflow.ellipsis, ), ), + 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, + ), + Text( + item.workOrderNo ?? '--', + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade500, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + // Quantity / edit input if (editing) SizedBox( - width: 56, - height: 32, + width: 52, + height: 26, child: TextField( keyboardType: TextInputType.none, inputFormatters: [FilteringTextInputFormatter.digitsOnly], @@ -1816,45 +1897,54 @@ class _BoxingPageState extends State { offset: _editingPackedQuantity.length, ), onChanged: (value) => _editingPackedQuantity = value, - decoration: const InputDecoration( - contentPadding: EdgeInsets.symmetric(horizontal: 6), - border: OutlineInputBorder(), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 4), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4), + borderSide: + const BorderSide(color: Colors.amber, width: 1.5), + ), + ), + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, ), ), ) else Text( - '${item.quantity}/$totalText', - style: const TextStyle(fontSize: 13), + '${item.quantity} / $totalText', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: deleting ? Colors.red : Colors.green.shade700, + ), ), - const SizedBox(width: 4), + // Action buttons if (editing) ...[ - IconButton( - tooltip: '保存', - visualDensity: VisualDensity.compact, - icon: const Icon(Icons.check, size: 18), - onPressed: _isSubmitting ? null : () => _savePackedItem(item), + _iconBtn( + Icons.check, + color: Colors.green, + onTap: _isSubmitting ? null : () => _savePackedItem(item), ), - IconButton( - tooltip: '取消', - visualDensity: VisualDensity.compact, - icon: const Icon(Icons.close, size: 18), - onPressed: _isSubmitting ? null : _cancelEditPackedItem, + _iconBtn( + Icons.close, + color: Colors.grey, + onTap: + _isSubmitting ? null : _cancelEditPackedItem, ), ] else ...[ - IconButton( - tooltip: '修改', - visualDensity: VisualDensity.compact, - icon: const Icon(Icons.edit, size: 18), - onPressed: _isSubmitting + _iconBtn( + Icons.edit, + color: Colors.orange.shade700, + onTap: _isSubmitting ? null : () => _startEditPackedItem(item), ), - IconButton( - tooltip: '删除', - visualDensity: VisualDensity.compact, - icon: const Icon(Icons.delete_outline, size: 18), - onPressed: _isSubmitting + _iconBtn( + Icons.delete_outline, + color: Colors.red, + onTap: _isSubmitting ? null : () { setState(() { @@ -1867,51 +1957,165 @@ class _BoxingPageState extends State { ], ], ), - if (deleting) - Padding( - padding: const EdgeInsets.only(top: 4), - child: Row( - children: [ - const Expanded( - child: Text('确认删除此条记录?', style: TextStyle(fontSize: 13)), - ), - TextButton( - onPressed: _isSubmitting - ? null - : () => _deletePackedItem(item), - child: const Text('是'), - ), - TextButton( - onPressed: _isSubmitting - ? null - : () => setState(() => _deletingPackedItemId = null), - child: const Text('否'), - ), - ], + ), + // Inline delete confirmation + if (deleting) + Container( + 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( + children: [ + const Expanded( + child: Text( + '确认删除?', + style: TextStyle( + fontSize: 12, + color: Colors.red, + fontWeight: FontWeight.w500, + ), + ), + ), + TextButton( + onPressed: _isSubmitting + ? null + : () => _deletePackedItem(item), + 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( + onPressed: _isSubmitting + ? null + : () => setState(() => _deletingPackedItemId = null), + 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) { return Container( - width: double.infinity, - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(8), - ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), + color: Colors.grey.shade50, child: const Row( - mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.qr_code_scanner, color: Colors.grey, size: 20), + Icon(Icons.qr_code_scanner, color: Colors.grey, size: 16), SizedBox(width: 8), 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 { } return Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), decoration: BoxDecoration( - border: Border.all(color: Colors.green, width: 1.5), - borderRadius: BorderRadius.circular(8), + color: Colors.green.shade50, + border: Border( + bottom: BorderSide(color: Colors.green.shade200), + ), ), child: Row( 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), Expanded( child: Text( - '${_zongpaiNo ?? ""} ${_workOrderNo ?? "--"} ${_erpQuantity ?? "--"}件', - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700), + '${_zongpaiNo ?? ""} · ${_workOrderNo ?? "--"} · ${_erpQuantity ?? "--"}件', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.green.shade700, + ), overflow: TextOverflow.ellipsis, ), ), @@ -1941,73 +2150,97 @@ class _BoxingPageState extends State { ); } - Widget _buildManyToOneSubmitArea(bool hasScan) { - return Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '数量', - style: TextStyle( - fontSize: 13, - color: hasScan ? Colors.black54 : Colors.grey, - ), - ), - const SizedBox(height: 4), - SizedBox( - height: 40, - child: TextField( - controller: _quantityController, - focusNode: _quantityFocusNode, - enabled: hasScan && !_isSubmitting, - keyboardType: TextInputType.none, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - onChanged: (_) => setState(() {}), - onEditingComplete: () {}, - onSubmitted: (_) => _submitFromKeyboard(), - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric(horizontal: 10), - border: const OutlineInputBorder(), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: _quantityTooHigh - ? Colors.red - : Colors.grey.shade400, - ), + Widget _buildManyToOneSubmitRow(bool hasScan) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '数量', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: hasScan ? Colors.black54 : Colors.grey.shade400, + ), + ), + const SizedBox(width: 6), + Expanded( + child: SizedBox( + height: 34, + child: TextField( + controller: _quantityController, + focusNode: _quantityFocusNode, + enabled: hasScan && !_isSubmitting, + keyboardType: TextInputType.none, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + onChanged: (_) => setState(() {}), + onEditingComplete: () {}, + onSubmitted: (_) => _submitFromKeyboard(), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: BorderSide( + color: _quantityTooHigh + ? Colors.red + : 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), - SizedBox( - height: 40, - child: ElevatedButton( - onPressed: _canSubmit ? _submit : null, - style: ElevatedButton.styleFrom( - backgroundColor: _canSubmit - ? Theme.of(context).colorScheme.primary - : Colors.grey.shade300, - foregroundColor: _canSubmit ? Colors.white : Colors.grey.shade600, ), - child: _isSubmitting - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ) - : const Text('确认', style: TextStyle(fontSize: 15)), ), - ), - ], + const SizedBox(width: 8), + SizedBox( + height: 34, + child: ElevatedButton( + onPressed: _canSubmit ? _submit : null, + style: ElevatedButton.styleFrom( + backgroundColor: _canSubmit + ? Theme.of(context).colorScheme.primary + : Colors.grey.shade200, + foregroundColor: _canSubmit + ? Colors.white + : Colors.grey.shade400, + padding: const EdgeInsets.symmetric(horizontal: 18), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + ), + ), + child: _isSubmitting + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text( + '确认', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + ), + ), + ), + ), + ], + ), ); } diff --git a/lib/widgets/status_bar.dart b/lib/widgets/status_bar.dart index 0cbc83b..2177ab7 100644 --- a/lib/widgets/status_bar.dart +++ b/lib/widgets/status_bar.dart @@ -33,23 +33,23 @@ class StatusBar extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; return Container( width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), color: colorScheme.surfaceContainerHighest, child: Row( children: [ Container( - width: 8, - height: 8, + width: 7, + height: 7, decoration: BoxDecoration( color: _resolveDotColor(context), shape: BoxShape.circle, ), ), - const SizedBox(width: 8), + const SizedBox(width: 7), Expanded( child: Text( text, - style: const TextStyle(fontSize: 14), + style: const TextStyle(fontSize: 12), overflow: TextOverflow.ellipsis, ), ),