Compare commits
8 Commits
a7570891c5
...
7d22c23dfe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d22c23dfe | ||
|
|
a6ee6a718b | ||
|
|
7a95f49acc | ||
|
|
8c967a0f99 | ||
|
|
d9f8e066b3 | ||
|
|
f60745607f | ||
|
|
9e4667d68f | ||
|
|
9b9becb008 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -49,3 +49,4 @@ app.*.map.json
|
||||
# AI Agents
|
||||
.claude/
|
||||
.agents/
|
||||
.sisyphus/
|
||||
482
docs/registration-redesign-preview.html
Normal file
482
docs/registration-redesign-preview.html
Normal file
@@ -0,0 +1,482 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>上架登记模块 - UI 重构预览</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, 'Segoe UI', 'Noto Sans SC', 'Roboto', sans-serif;
|
||||
background: #eef1f5; height: 100vh; display: flex; overflow: hidden;
|
||||
}
|
||||
|
||||
/* ========== Layout ========== */
|
||||
.sidebar {
|
||||
width: 280px; min-width: 280px; background: #fff; border-right: 1px solid #ddd;
|
||||
overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 12px;
|
||||
}
|
||||
.main {
|
||||
flex: 1; display: flex; align-items: center; justify-content: center; padding: 20px; gap: 40px;
|
||||
}
|
||||
|
||||
/* ========== Control Panel ========== */
|
||||
.group { border: 1px solid #e0e0e0; border-radius: 8px; padding: 10px; background: #fafbfc; }
|
||||
.group-title { font-size: 12px; font-weight: 700; color: #333; margin-bottom: 6px; padding-bottom: 5px; border-bottom: 1px solid #eee; }
|
||||
.btn-row { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 5px; }
|
||||
.btn-row:last-child { margin-bottom: 0; }
|
||||
.btn { padding: 4px 9px; font-size: 11px; border: 1px solid #ccc; border-radius: 5px; background: #fff; cursor: pointer; transition: all .15s; white-space: nowrap; color: #333; }
|
||||
.btn:hover { background: #e8f0fe; border-color: #90b4f0; }
|
||||
.btn.active { background: #1a73e8; color: #fff; border-color: #1a73e8; }
|
||||
|
||||
/* ========== Device Frame ========== */
|
||||
.device {
|
||||
width: 400px; height: 700px; border-radius: 16px; overflow: hidden;
|
||||
box-shadow: 0 6px 30px rgba(0,0,0,.22); background: #fff;
|
||||
display: flex; flex-direction: column; position: relative; border: 2px solid #222;
|
||||
}
|
||||
|
||||
/* ========== AppBar ========== */
|
||||
.appbar { height: 44px; min-height: 44px; background: #1565C0; color: #fff; display: flex; align-items: center; padding: 0 4px 0 12px; }
|
||||
.appbar-title { font-size: 17px; font-weight: 600; white-space: nowrap; }
|
||||
.appbar-gap { width: 8px; }
|
||||
.mode-pill { flex: 1; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center; gap: 5px; font-size: 14px; font-weight: 800; color: #fff; border: none; transition: background .2s; }
|
||||
.mode-pill.unlocked { background: #1976D2; }
|
||||
.mode-pill.locked { background: #F57C00; }
|
||||
.mode-pill .icon { font-size: 15px; }
|
||||
.mode-pill .shortcut { font-size: 11px; font-weight: 700; opacity: .85; margin-left: 3px; }
|
||||
|
||||
/* ========== Body ========== */
|
||||
.body { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||||
|
||||
/* ========== Paicha Header ========== */
|
||||
.paicha-header { padding: 8px 12px 6px; background: #FAFAFA; border-bottom: 1px solid #E0E0E0; display: flex; align-items: center; gap: 12px; }
|
||||
.paicha-header .left { flex-shrink: 0; }
|
||||
.paicha-header .paicha-no { font-size: 24px; font-weight: 800; color: rgba(0,0,0,.87); }
|
||||
.paicha-header .paicha-no.placeholder { color: #BDBDBD; font-weight: 700; }
|
||||
.paicha-header .right { flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: flex-end; gap: 3px; }
|
||||
.loc-info { display: flex; align-items: center; gap: 5px; font-size: 12px; color: #757575; }
|
||||
.loc-info .loc-code { font-weight: 600; color: #555; }
|
||||
.loc-chip { padding: 1px 6px; border-radius: 4px; font-size: 10px; font-weight: 700; white-space: nowrap; }
|
||||
.loc-chip.normal { background: rgba(33,150,243,.12); color: #1976D2; }
|
||||
.loc-chip.transit { background: rgba(255,152,0,.12); color: #E65100; }
|
||||
.stats { display: flex; align-items: center; gap: 5px; justify-content: flex-end; flex-wrap: wrap; }
|
||||
.stat-tag { padding: 1px 7px; border-radius: 10px; font-size: 10px; font-weight: 700; white-space: nowrap; }
|
||||
.stat-tag.total { background: #E3F2FD; color: #1565C0; }
|
||||
.stat-tag.shelved { background: #E8F5E9; color: #2E7D32; }
|
||||
.stat-tag.transferred { background: #FFF3E0; color: #E65100; }
|
||||
.stat-tag.pending { background: #F5F5F5; color: #757575; }
|
||||
.stat-tag.placeholder { background: #F5F5F5; color: #BDBDBD; }
|
||||
|
||||
/* ========== Table Head ========== */
|
||||
.table-head { height: 28px; padding: 0 12px; background: #EEEEEE; border-bottom: 1px solid #BDBDBD; display: flex; align-items: center; }
|
||||
.table-head span { font-size: 10px; font-weight: 700; color: #666; }
|
||||
.table-head .th1 { flex: 22; }
|
||||
.table-head .th2 { flex: 18; }
|
||||
.table-head .th3 { flex: 12; text-align: center; }
|
||||
.table-head .th4 { flex: 28; text-align: right; }
|
||||
|
||||
/* ========== List Body ========== */
|
||||
.list-body { flex: 1; overflow-y: auto; }
|
||||
|
||||
/* Table rows */
|
||||
.table-row { height: 36px; padding: 0 12px; display: flex; align-items: center; border-bottom: 1px solid #EEEEEE; position: relative; }
|
||||
.table-row .color-bar { position: absolute; left: 0; top: 4px; bottom: 4px; width: 3px; border-radius: 2px; }
|
||||
.table-row .cell { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.table-row .cell.c1 { flex: 22; font-size: 12px; font-weight: 600; color: rgba(0,0,0,.87); }
|
||||
.table-row .cell.c2 { flex: 18; font-size: 12px; color: rgba(0,0,0,.6); }
|
||||
.table-row .cell.c3 { flex: 12; font-size: 12px; color: rgba(0,0,0,.6); text-align: center; }
|
||||
.table-row .cell.c4 { flex: 28; font-size: 12px; font-weight: 600; color: rgba(0,0,0,.87); text-align: right; }
|
||||
|
||||
/* Scanned row — always green indicator, regardless of underlying status */
|
||||
.table-row.scanned { background: #F1F8E9; }
|
||||
.table-row.scanned .color-bar { background: #43A047; }
|
||||
.table-row.scanned .cell.c1 { font-weight: 700; }
|
||||
|
||||
/* Unscanned rows — status-based */
|
||||
.table-row.on_shelf { background: #fff; }
|
||||
.table-row.on_shelf .color-bar { background: #2196F3; }
|
||||
.table-row.transferred { background: #FFF8E1; }
|
||||
.table-row.transferred .color-bar { background: #FF9800; }
|
||||
.table-row.not_shelved { background: #FAFAFA; }
|
||||
.table-row.not_shelved .color-bar { background: #BDBDBD; }
|
||||
|
||||
/* Section divider */
|
||||
.section-divider {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 0 12px; height: 24px;
|
||||
border-bottom: 1px solid #EEEEEE;
|
||||
}
|
||||
.section-divider .line { flex: 1; height: 0; border-top: 1px dashed #CCC; }
|
||||
.section-divider .label { font-size: 10px; font-weight: 600; color: #9E9E9E; white-space: nowrap; letter-spacing: .3px; }
|
||||
|
||||
.overview-msg { display: flex; align-items: center; justify-content: center; font-size: 13px; color: #9E9E9E; font-style: italic; height: 100%; }
|
||||
|
||||
/* ========== Bottom Bar ========== */
|
||||
.bottom-bar { min-height: 50px; padding: 8px 12px; background: #fff; border-top: 1px solid #E0E0E0; display: flex; align-items: center; gap: 8px; }
|
||||
.submit-btn { flex: 1; height: 34px; border-radius: 6px; border: none; font-size: 14px; font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background .15s; }
|
||||
.submit-btn.disabled { background: #E0E0E0; color: #9E9E9E; cursor: default; }
|
||||
.submit-btn.primary { background: #1976D2; color: #fff; }
|
||||
.submit-btn.transit { background: #FF9800; color: #fff; }
|
||||
.submit-btn .spinner { width: 18px; height: 18px; border: 2px solid rgba(255,255,255,.3); border-top-color: #fff; border-radius: 50%; animation: spin .8s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* ========== Status Bar ========== */
|
||||
.status-bar { height: 28px; min-height: 28px; padding: 0 12px; background: #F5F5F5; display: flex; align-items: center; gap: 7px; border-top: 1px solid #E0E0E0; }
|
||||
.status-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
|
||||
.status-dot.blue { background: #2196F3; }
|
||||
.status-dot.orange { background: #FF9800; }
|
||||
.status-dot.green { background: #4CAF50; }
|
||||
.status-dot.red { background: #F44336; }
|
||||
.status-dot.yellow { background: #FBC02D; }
|
||||
.status-text { font-size: 11px; color: #333; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
||||
|
||||
/* ========== Dialog ========== */
|
||||
.dialog-overlay { position: absolute; inset: 0; background: rgba(0,0,0,.4); display: flex; align-items: center; justify-content: center; z-index: 100; }
|
||||
.dialog-overlay.hidden { display: none; }
|
||||
.dialog { background: #FFEBEE; border-radius: 12px; padding: 20px; width: 300px; box-shadow: 0 8px 30px rgba(0,0,0,.25); }
|
||||
.dialog-title { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
||||
.dialog-title .icon { color: #F44336; font-size: 20px; }
|
||||
.dialog-title .text { font-size: 17px; font-weight: 600; color: #F44336; }
|
||||
.dialog-body { font-size: 13px; color: #333; line-height: 1.7; }
|
||||
.dialog-body strong { font-weight: 700; }
|
||||
.dialog-actions { margin-top: 14px; text-align: right; }
|
||||
.dialog-close { padding: 5px 14px; font-size: 13px; border: none; border-radius: 5px; background: transparent; color: #1976D2; cursor: pointer; font-weight: 600; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ==================== Sidebar ==================== -->
|
||||
<div class="sidebar">
|
||||
<div style="font-size:15px;font-weight:700;color:#1565C0;text-align:center;padding-bottom:8px;border-bottom:2px solid #1565C0;">
|
||||
上架登记 - 重构预览控制
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<div class="group-title">1. 锁定模式</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn active" onclick="setState('locked',false)">未锁定</button>
|
||||
<button class="btn" onclick="setState('locked',true)">已锁定</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<div class="group-title">2. 货位</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn active" onclick="setState('location',null)">无</button>
|
||||
<button class="btn" onclick="setState('location','normal')">普通 A-01-03</button>
|
||||
<button class="btn" onclick="setState('location','transit')">转运 TRANS-001</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<div class="group-title">3. 总排号</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn active" onclick="setState('zongpai',0)">无</button>
|
||||
<button class="btn" onclick="setState('zongpai',1)">1 条</button>
|
||||
<button class="btn" onclick="setState('zongpai',3)">3 条</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<div class="group-title">4. 提交中</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn active" onclick="setState('submitting',false)">空闲</button>
|
||||
<button class="btn" onclick="setState('submitting',true)">Loading</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<div class="group-title">5. 总览</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn active" onclick="setState('overview','empty')">未扫描</button>
|
||||
<button class="btn" onclick="setState('overview','loaded')">正常</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<div class="group-title">6. 状态栏</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn active" onclick="setState('status','auto')">自动</button>
|
||||
<button class="btn" onclick="setState('status','success')">上架成功</button>
|
||||
<button class="btn" onclick="setState('status','offShelf')">下架成功</button>
|
||||
<button class="btn" onclick="setState('status','batch')">批量成功(3条)</button>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn" onclick="setState('status','invalid')">无效码</button>
|
||||
<button class="btn" onclick="setState('status','network')">网络异常</button>
|
||||
<button class="btn" onclick="setState('status','fail')">提交失败</button>
|
||||
<button class="btn" onclick="setState('status','duplicate')">重复(弹窗)</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== Device ==================== -->
|
||||
<div class="main">
|
||||
<div class="device" id="device">
|
||||
|
||||
<!-- AppBar -->
|
||||
<div class="appbar">
|
||||
<span class="appbar-title">上架登记</span>
|
||||
<span class="appbar-gap"></span>
|
||||
<button class="mode-pill unlocked" id="modePill">
|
||||
<span class="icon">⇄</span>
|
||||
<span id="modeLabel">未锁定</span>
|
||||
<span class="shortcut">P2</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="body">
|
||||
|
||||
<!-- Paicha Header -->
|
||||
<div class="paicha-header" id="paichaHeader">
|
||||
<div class="left">
|
||||
<span class="paicha-no placeholder" id="paichaNo">--</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<span class="loc-info" id="locInfo" style="display:none;"></span>
|
||||
<span class="stats" id="paichaStats"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table Head -->
|
||||
<div class="table-head" id="tableHead" style="display:none;">
|
||||
<span class="th1">总排号</span>
|
||||
<span class="th2">工令号</span>
|
||||
<span class="th3">数量</span>
|
||||
<span class="th4">货位号</span>
|
||||
</div>
|
||||
|
||||
<!-- List Body -->
|
||||
<div class="list-body" id="listBody">
|
||||
<div class="overview-msg">扫描总排号后自动显示排产号上架情况</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Bar -->
|
||||
<div class="bottom-bar">
|
||||
<button class="submit-btn disabled" id="submitBtn">
|
||||
<span id="submitText">确 认 上 架</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<div class="status-bar">
|
||||
<span class="status-dot blue" id="statusDot"></span>
|
||||
<span class="status-text" id="statusTextEl">等待扫描总排号或货位号…</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dialog -->
|
||||
<div class="dialog-overlay hidden" id="dialogOverlay">
|
||||
<div class="dialog">
|
||||
<div class="dialog-title"><span class="icon">⚠</span><span class="text">重复上架</span></div>
|
||||
<div class="dialog-body">
|
||||
<div>总排号:<strong>26B35</strong></div>
|
||||
<div style="margin-top:3px;">已登记货位:<strong>A-02-05</strong></div>
|
||||
<div style="margin-top:3px;">登记时间:<strong>2026-05-19 14:30:22</strong></div>
|
||||
<div style="margin-top:10px;font-weight:700;">请核查实物,确认是否操作错误。</div>
|
||||
</div>
|
||||
<div class="dialog-actions"><button class="dialog-close">关闭</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const S = { locked: false, location: null, zongpai: 0, submitting: false, overview: 'empty', status: 'auto' };
|
||||
|
||||
const DATA = {
|
||||
paichaNo: 'R00015', totalCount: 5, shelvedCount: 2, transferredCount: 1, pendingCount: 2,
|
||||
items: [
|
||||
{ zp: '26B35', wo: '3-1', qty: 50, loc: 'A-01-03', status: 'on_shelf' },
|
||||
{ zp: '26B36', wo: '3-2-1', qty: 100, loc: 'A-01-05', status: 'on_shelf' },
|
||||
{ zp: '26B37', wo: '3-2-2', qty: 100, loc: null, status: 'not_shelved' },
|
||||
{ zp: '26B38', wo: '3-2-3', qty: 50, loc: 'TRANS-001', status: 'transferred' },
|
||||
{ zp: '26B39', wo: '3-3', qty: 45, loc: null, status: 'not_shelved' },
|
||||
]
|
||||
};
|
||||
|
||||
// scanned items are identified by index: when zongpai=1 → [0], zongpai=3 → [0,2,4]
|
||||
const SCANNED_INDICES = { 1: [0], 3: [0, 2, 4] };
|
||||
|
||||
const STATUS_MAP = {
|
||||
success: { dot: 'green', text: '上架成功' },
|
||||
offShelf: { dot: 'green', text: '下架成功' },
|
||||
batch: { dot: 'green', text: '批量上架成功(3 条)' },
|
||||
invalid: { dot: 'red', text: '无效码:ABCXYZ123' },
|
||||
network: { dot: 'yellow', text: '网络异常,请检查网络连接' },
|
||||
fail: { dot: 'red', text: '请求参数错误' },
|
||||
duplicate: { dot: 'red', text: '请确认信息并提交' },
|
||||
};
|
||||
|
||||
function setState(key, val) {
|
||||
S[key] = val;
|
||||
document.querySelectorAll('.btn').forEach(btn => {
|
||||
const m = btn.getAttribute('onclick')?.match(/setState\('(\w+)',\s*(.+)\)/);
|
||||
if (!m) return;
|
||||
btn.classList.toggle('active', String(val) === m[2].replace(/'/g, '') && m[1] === key);
|
||||
});
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderAppBar();
|
||||
renderPaichaHeader();
|
||||
renderList();
|
||||
renderBottomBar();
|
||||
renderStatusBar();
|
||||
renderDialog();
|
||||
}
|
||||
|
||||
function renderAppBar() {
|
||||
const pill = document.getElementById('modePill');
|
||||
const label = document.getElementById('modeLabel');
|
||||
pill.className = 'mode-pill ' + (S.locked ? 'locked' : 'unlocked');
|
||||
label.textContent = S.locked ? '货位锁定' : '未锁定';
|
||||
}
|
||||
|
||||
function renderPaichaHeader() {
|
||||
const no = document.getElementById('paichaNo');
|
||||
const locInfo = document.getElementById('locInfo');
|
||||
const stats = document.getElementById('paichaStats');
|
||||
const hasLoc = S.location !== null;
|
||||
const locCode = S.location === 'normal' ? 'A-01-03' : S.location === 'transit' ? 'TRANS-001' : null;
|
||||
|
||||
// Paicha number (left)
|
||||
if (S.overview === 'loaded') {
|
||||
no.textContent = DATA.paichaNo; no.className = 'paicha-no';
|
||||
} else {
|
||||
no.textContent = '--'; no.className = 'paicha-no placeholder';
|
||||
}
|
||||
|
||||
// Location info (right, top row)
|
||||
if (hasLoc) {
|
||||
locInfo.style.display = 'flex';
|
||||
locInfo.innerHTML = `<span class="loc-code">${locCode}</span><span class="loc-chip ${S.location}">${S.location === 'transit' ? '转运区域' : '普通货架'}</span>`;
|
||||
} else {
|
||||
locInfo.style.display = 'none';
|
||||
}
|
||||
|
||||
// Stats tags (right, bottom row)
|
||||
if (S.overview === 'loaded') {
|
||||
stats.innerHTML =
|
||||
`<span class="stat-tag total">共 ${DATA.totalCount}</span>` +
|
||||
`<span class="stat-tag shelved">上架 ${DATA.shelvedCount}</span>` +
|
||||
`<span class="stat-tag transferred">转运 ${DATA.transferredCount}</span>` +
|
||||
`<span class="stat-tag pending">待上架 ${DATA.pendingCount}</span>`;
|
||||
} else {
|
||||
stats.innerHTML =
|
||||
`<span class="stat-tag placeholder">共 -</span>` +
|
||||
`<span class="stat-tag placeholder">上架 -</span>` +
|
||||
`<span class="stat-tag placeholder">转运 -</span>` +
|
||||
`<span class="stat-tag placeholder">待上架 -</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
const tableHead = document.getElementById('tableHead');
|
||||
const body = document.getElementById('listBody');
|
||||
|
||||
if (S.overview !== 'loaded') {
|
||||
tableHead.style.display = 'none';
|
||||
body.innerHTML = '<div class="overview-msg">扫描总排号后自动显示排产号上架情况</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
tableHead.style.display = 'flex';
|
||||
|
||||
// Determine which items are scanned
|
||||
const scannedSet = new Set(S.zongpai > 0 ? (SCANNED_INDICES[S.zongpai] || []) : []);
|
||||
|
||||
// Split into scanned and unscanned groups
|
||||
const scanned = [], unscanned = [];
|
||||
DATA.items.forEach((item, idx) => {
|
||||
if (scannedSet.has(idx)) scanned.push({ ...item, origIdx: idx });
|
||||
else unscanned.push({ ...item, origIdx: idx });
|
||||
});
|
||||
|
||||
let html = '';
|
||||
|
||||
// Scanned items (always at top)
|
||||
scanned.forEach(item => {
|
||||
html += rowHtml(item, true);
|
||||
});
|
||||
|
||||
// Divider between groups (only when both groups exist)
|
||||
if (scanned.length > 0 && unscanned.length > 0) {
|
||||
html += `<div class="section-divider">
|
||||
<span class="line"></span>
|
||||
<span class="label">以下 ${unscanned.length} 项未操作</span>
|
||||
<span class="line"></span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Unscanned items
|
||||
unscanned.forEach(item => {
|
||||
html += rowHtml(item, false);
|
||||
});
|
||||
|
||||
body.innerHTML = html;
|
||||
}
|
||||
|
||||
function rowHtml(item, isScanned) {
|
||||
const cls = isScanned ? 'scanned' : item.status;
|
||||
return `<div class="table-row ${cls}">
|
||||
<div class="color-bar"></div>
|
||||
<span class="cell c1" style="padding-left:10px;">${item.zp}</span>
|
||||
<span class="cell c2">${item.wo}</span>
|
||||
<span class="cell c3">${item.qty}</span>
|
||||
<span class="cell c4">${item.loc || '\u2014'}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderBottomBar() {
|
||||
const btn = document.getElementById('submitBtn');
|
||||
const text = document.getElementById('submitText');
|
||||
const hasLoc = S.location !== null, hasZp = S.zongpai > 0;
|
||||
const isTransit = S.location === 'transit';
|
||||
const canSubmit = hasLoc && hasZp && !S.submitting;
|
||||
|
||||
let label;
|
||||
if (S.submitting) { label = null; }
|
||||
else if (isTransit) { label = S.locked && S.zongpai > 1 ? `批量转运并凑箱(${S.zongpai} 条)` : '转运并装箱'; }
|
||||
else { label = S.locked && S.zongpai > 1 ? `批量上架(${S.zongpai} 条)` : '确 认 上 架'; }
|
||||
|
||||
if (S.submitting) {
|
||||
btn.className = 'submit-btn ' + (isTransit ? 'transit' : (canSubmit ? 'primary' : 'disabled'));
|
||||
text.innerHTML = '<span class="spinner"></span>';
|
||||
} else if (!canSubmit) {
|
||||
btn.className = 'submit-btn disabled'; text.textContent = label;
|
||||
} else {
|
||||
btn.className = 'submit-btn ' + (isTransit ? 'transit' : 'primary'); text.textContent = label;
|
||||
}
|
||||
}
|
||||
|
||||
function renderStatusBar() {
|
||||
const dot = document.getElementById('statusDot');
|
||||
const text = document.getElementById('statusTextEl');
|
||||
|
||||
if (S.status !== 'auto' && STATUS_MAP[S.status]) {
|
||||
const o = STATUS_MAP[S.status]; dot.className = 'status-dot ' + o.dot; text.textContent = o.text; return;
|
||||
}
|
||||
if (S.submitting) { dot.className = 'status-dot orange'; text.textContent = '正在提交…'; return; }
|
||||
const hasLoc = S.location !== null, hasZp = S.zongpai > 0;
|
||||
if (S.locked && hasLoc && !hasZp) { dot.className = 'status-dot blue'; text.textContent = '货位已锁定,请扫描下一张执行卡'; }
|
||||
else if (hasLoc && hasZp) { dot.className = 'status-dot blue'; text.textContent = '请确认信息并提交'; }
|
||||
else if (hasZp) { dot.className = 'status-dot blue'; text.textContent = '请扫描目标货位号'; }
|
||||
else if (hasLoc) { dot.className = 'status-dot blue'; text.textContent = '请扫描执行卡'; }
|
||||
else { dot.className = 'status-dot blue'; text.textContent = '等待扫描总排号或货位号…'; }
|
||||
}
|
||||
|
||||
function renderDialog() {
|
||||
document.getElementById('dialogOverlay').classList.toggle('hidden', S.status !== 'duplicate');
|
||||
}
|
||||
|
||||
document.querySelector('.dialog-close').addEventListener('click', () => setState('status', 'auto'));
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
901
docs/registration-ui-states.html
Normal file
901
docs/registration-ui-states.html
Normal file
@@ -0,0 +1,901 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>上架登记模块 - UI 状态全览</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, 'Segoe UI', 'Noto Sans SC', 'Roboto', sans-serif;
|
||||
background: #eef1f5; height: 100vh; display: flex; overflow: hidden;
|
||||
}
|
||||
|
||||
/* ========== Layout ========== */
|
||||
.sidebar {
|
||||
width: 310px; min-width: 310px; background: #fff; border-right: 1px solid #ddd;
|
||||
overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 14px;
|
||||
}
|
||||
.main {
|
||||
flex: 1; display: flex; align-items: center; justify-content: center; padding: 20px;
|
||||
}
|
||||
|
||||
/* ========== Control Panel ========== */
|
||||
.group { border: 1px solid #e0e0e0; border-radius: 10px; padding: 12px; background: #fafbfc; }
|
||||
.group-title {
|
||||
font-size: 13px; font-weight: 700; color: #333; margin-bottom: 8px;
|
||||
padding-bottom: 6px; border-bottom: 1px solid #eee;
|
||||
}
|
||||
.btn-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }
|
||||
.btn-row:last-child { margin-bottom: 0; }
|
||||
.btn {
|
||||
padding: 5px 10px; font-size: 12px; border: 1px solid #ccc; border-radius: 6px;
|
||||
background: #fff; cursor: pointer; transition: all .15s; white-space: nowrap;
|
||||
color: #333;
|
||||
}
|
||||
.btn:hover { background: #e8f0fe; border-color: #90b4f0; }
|
||||
.btn.active { background: #1a73e8; color: #fff; border-color: #1a73e8; }
|
||||
.btn.active:hover { background: #1565c0; }
|
||||
.label { font-size: 11px; color: #888; margin-bottom: 4px; margin-top: 4px; }
|
||||
|
||||
/* ========== Device Frame ========== */
|
||||
.device {
|
||||
width: 400px; height: 720px; border-radius: 18px; overflow: hidden;
|
||||
box-shadow: 0 6px 30px rgba(0,0,0,.22); background: #fff;
|
||||
display: flex; flex-direction: column; position: relative;
|
||||
border: 2px solid #222;
|
||||
}
|
||||
|
||||
/* ========== Flutter UI Simulation ========== */
|
||||
|
||||
/* AppBar */
|
||||
.fl-appbar {
|
||||
height: 56px; min-height: 56px; background: #1565C0; color: #fff;
|
||||
display: flex; align-items: center; padding: 0 4px 0 12px; gap: 0;
|
||||
}
|
||||
.fl-appbar-title { font-size: 18px; font-weight: 600; white-space: nowrap; }
|
||||
.fl-appbar-spacer { width: 12px; }
|
||||
.fl-lock-btn {
|
||||
flex: 1; height: 36px; border-radius: 8px; display: flex; align-items: center;
|
||||
justify-content: center; gap: 6px; cursor: pointer; font-size: 15px;
|
||||
font-weight: 800; color: #fff; border: none; transition: background .2s;
|
||||
}
|
||||
.fl-lock-btn.unlocked { background: #1976D2; }
|
||||
.fl-lock-btn.locked { background: #F57C00; }
|
||||
.fl-lock-btn .shortcut {
|
||||
font-size: 12px; font-weight: 700; opacity: .88; margin-left: 4px;
|
||||
}
|
||||
.fl-lock-btn .icon { font-size: 16px; }
|
||||
|
||||
/* Body */
|
||||
.fl-body { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||||
|
||||
/* Operation Area */
|
||||
.fl-op-area { padding: 10px 12px 8px; }
|
||||
.fl-section-title {
|
||||
font-size: 14px; font-weight: 700; color: #333; margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* Input Panel - Single */
|
||||
.fl-input-single {
|
||||
min-height: 58px; padding: 10px; border-radius: 8px;
|
||||
display: flex; align-items: center; gap: 0;
|
||||
transition: border .2s;
|
||||
}
|
||||
.fl-input-single.empty { border: 1px solid #BDBDBD; }
|
||||
.fl-input-single.filled { border: 2px solid #4CAF50; }
|
||||
.fl-input-single .loc-part {
|
||||
flex: 5; display: flex; align-items: center; gap: 6px; min-width: 0;
|
||||
}
|
||||
.fl-input-single .loc-text {
|
||||
font-size: 18px; font-weight: 700; color: rgba(0,0,0,.87);
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.fl-input-single .loc-text.placeholder { color: #9E9E9E; }
|
||||
.fl-input-single .divider {
|
||||
padding: 0 8px; font-size: 20px; color: #9E9E9E; flex-shrink: 0;
|
||||
}
|
||||
.fl-input-single .zp-part {
|
||||
flex: 3; text-align: right; min-width: 0;
|
||||
}
|
||||
.fl-input-single .zp-text {
|
||||
font-size: 18px; font-weight: 700; color: rgba(0,0,0,.87);
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.fl-input-single .zp-text.placeholder { color: #9E9E9E; }
|
||||
|
||||
/* Input Panel - Locked */
|
||||
.fl-input-locked {
|
||||
min-height: 82px; padding: 8px 10px; border-radius: 8px;
|
||||
transition: border .2s;
|
||||
}
|
||||
.fl-input-locked.empty { border: 1px solid #BDBDBD; }
|
||||
.fl-input-locked.filled { border: 2px solid #4CAF50; }
|
||||
.fl-input-locked .loc-row {
|
||||
display: flex; align-items: center; gap: 6px; margin-bottom: 8px;
|
||||
}
|
||||
.fl-input-locked .loc-text {
|
||||
flex: 1; font-size: 18px; font-weight: 700; color: rgba(0,0,0,.87);
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.fl-lock-icon { color: #FF9800; font-size: 16px; flex-shrink: 0; }
|
||||
.fl-chips-area { display: flex; flex-wrap: wrap; gap: 6px; min-height: 30px; }
|
||||
.fl-chips-area.empty-space { height: 24px; }
|
||||
|
||||
/* InputChip */
|
||||
.fl-chip {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
padding: 2px 4px 2px 10px; background: #E8E8E8; border-radius: 16px;
|
||||
font-size: 13px; font-weight: 500; color: #333;
|
||||
}
|
||||
.fl-chip-del {
|
||||
width: 18px; height: 18px; border-radius: 50%; border: none;
|
||||
background: rgba(0,0,0,.12); color: #555; font-size: 11px;
|
||||
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Location Chip */
|
||||
.fl-loc-chip {
|
||||
padding: 2px 6px; border-radius: 4px; font-size: 11px; font-weight: 700;
|
||||
white-space: nowrap; flex-shrink: 0;
|
||||
}
|
||||
.fl-loc-chip.normal { background: rgba(33,150,243,.15); color: #2196F3; }
|
||||
.fl-loc-chip.transit { background: rgba(255,152,0,.15); color: #FF9800; }
|
||||
|
||||
/* Submit Button */
|
||||
.fl-submit {
|
||||
width: 100%; height: 46px; border-radius: 8px; border: none;
|
||||
font-size: 17px; font-weight: 500; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
margin-top: 10px; transition: background .15s;
|
||||
}
|
||||
.fl-submit.disabled { background: #E0E0E0; color: #757575; cursor: default; }
|
||||
.fl-submit.primary { background: #1976D2; color: #fff; }
|
||||
.fl-submit.transit { background: #FF9800; color: #fff; }
|
||||
.fl-submit.submitting { cursor: wait; }
|
||||
|
||||
/* Spinner */
|
||||
.fl-spinner {
|
||||
width: 22px; height: 22px; border: 2.5px solid rgba(255,255,255,.3);
|
||||
border-top-color: #fff; border-radius: 50%;
|
||||
animation: fl-spin .8s linear infinite;
|
||||
}
|
||||
.fl-spinner.small {
|
||||
width: 16px; height: 16px; border-width: 2px;
|
||||
border-color: rgba(0,0,0,.08); border-top-color: #666;
|
||||
}
|
||||
@keyframes fl-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Divider */
|
||||
.fl-divider { height: 1px; background: #E0E0E0; }
|
||||
|
||||
/* Overview Section */
|
||||
.fl-overview { flex: 1; padding: 8px 12px; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.fl-overview-header-row {
|
||||
display: flex; align-items: center; gap: 8px; margin-bottom: 6px;
|
||||
}
|
||||
.fl-overview-title { font-size: 14px; font-weight: 700; color: #333; flex: 1; }
|
||||
.fl-overview-body { flex: 1; overflow-y: auto; }
|
||||
|
||||
.fl-overview-msg {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
height: 100%; text-align: center; font-size: 13px; color: #757575;
|
||||
}
|
||||
.fl-overview-error {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
height: 100%; gap: 6px;
|
||||
}
|
||||
.fl-overview-error .msg { font-size: 13px; color: #F44336; text-align: center; }
|
||||
.fl-retry-btn {
|
||||
padding: 4px 12px; font-size: 13px; border: 1px solid #ccc; border-radius: 6px;
|
||||
background: #fff; cursor: pointer; color: #1976D2;
|
||||
}
|
||||
|
||||
/* Overview table */
|
||||
.fl-ov-info { font-size: 13px; font-weight: 600; color: #333; margin-bottom: 6px; }
|
||||
.fl-ov-error-bar {
|
||||
padding-bottom: 6px; display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.fl-ov-error-bar .msg { flex: 1; font-size: 12px; color: #F44336; }
|
||||
.fl-table-head {
|
||||
height: 32px; padding: 0 6px; background: #EEEEEE;
|
||||
border: 1px solid #BDBDBD; display: flex; align-items: center;
|
||||
}
|
||||
.fl-table-head span { font-size: 12px; font-weight: 700; color: #333; }
|
||||
.fl-table-head .c1 { flex: 20; }
|
||||
.fl-table-head .c2 { flex: 20; }
|
||||
.fl-table-head .c3 { flex: 14; text-align: center; }
|
||||
.fl-table-head .c4 { flex: 26; text-align: right; }
|
||||
|
||||
.fl-table-row {
|
||||
min-height: 34px; padding: 6px; display: flex; align-items: center;
|
||||
border-left: 1px solid #E0E0E0; border-right: 1px solid #E0E0E0;
|
||||
border-bottom: 1px solid #E0E0E0;
|
||||
}
|
||||
.fl-table-row.current {
|
||||
border-left-color: #4CAF50; border-left-width: 2px;
|
||||
border-right-color: #4CAF50; border-right-width: 2px;
|
||||
border-bottom-color: #4CAF50; border-bottom-width: 2px;
|
||||
}
|
||||
.fl-table-row.bg-onshelf { background: #E3F2FD; }
|
||||
.fl-table-row.bg-transferred { background: #FFF3E0; }
|
||||
.fl-table-row.bg-default { background: #F5F5F5; }
|
||||
.fl-table-row.bg-white { background: #fff; }
|
||||
.fl-table-row span {
|
||||
font-size: 12px; color: rgba(0,0,0,.87); overflow: hidden;
|
||||
text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.fl-table-row span.bold { font-weight: 700; }
|
||||
.fl-table-row .c1 { flex: 20; }
|
||||
.fl-table-row .c2 { flex: 20; }
|
||||
.fl-table-row .c3 { flex: 14; text-align: center; }
|
||||
.fl-table-row .c4 { flex: 26; text-align: right; }
|
||||
|
||||
/* Status Bar */
|
||||
.fl-statusbar {
|
||||
height: 28px; min-height: 28px; padding: 0 12px;
|
||||
background: #F5F5F5; display: flex; align-items: center; gap: 7px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
.fl-status-dot {
|
||||
width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0;
|
||||
}
|
||||
.fl-status-dot.blue { background: #2196F3; }
|
||||
.fl-status-dot.orange { background: #FF9800; }
|
||||
.fl-status-dot.green { background: #4CAF50; }
|
||||
.fl-status-dot.red { background: #F44336; }
|
||||
.fl-status-dot.yellow { background: #FBC02D; }
|
||||
.fl-status-dot.amber { background: #FFC107; }
|
||||
.fl-status-text { font-size: 11px; color: #333; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
||||
|
||||
/* Dialog overlay */
|
||||
.fl-dialog-overlay {
|
||||
position: absolute; inset: 0; background: rgba(0,0,0,.4);
|
||||
display: flex; align-items: center; justify-content: center; z-index: 100;
|
||||
}
|
||||
.fl-dialog-overlay.hidden { display: none; }
|
||||
.fl-dialog {
|
||||
background: #FFEBEE; border-radius: 12px; padding: 20px;
|
||||
width: 320px; box-shadow: 0 8px 30px rgba(0,0,0,.25);
|
||||
}
|
||||
.fl-dialog-title {
|
||||
display: flex; align-items: center; gap: 8px; margin-bottom: 14px;
|
||||
}
|
||||
.fl-dialog-title .icon { color: #F44336; font-size: 22px; }
|
||||
.fl-dialog-title .text { font-size: 18px; font-weight: 600; color: #F44336; }
|
||||
.fl-dialog-body { font-size: 14px; color: #333; line-height: 1.7; }
|
||||
.fl-dialog-body strong { font-weight: 700; }
|
||||
.fl-dialog-actions { margin-top: 16px; text-align: right; }
|
||||
.fl-dialog-close {
|
||||
padding: 6px 16px; font-size: 14px; border: none; border-radius: 6px;
|
||||
background: transparent; color: #1976D2; cursor: pointer; font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ==================== Sidebar Control Panel ==================== -->
|
||||
<div class="sidebar">
|
||||
|
||||
<div style="font-size:16px;font-weight:700;color:#1565C0;text-align:center;padding-bottom:10px;border-bottom:2px solid #1565C0;">
|
||||
上架登记 - UI 状态控制面板
|
||||
</div>
|
||||
|
||||
<!-- Lock Mode -->
|
||||
<div class="group">
|
||||
<div class="group-title">1. 锁定模式</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn active" onclick="setLock(false)">未锁定</button>
|
||||
<button class="btn" onclick="setLock(true)">已锁定</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location Type -->
|
||||
<div class="group">
|
||||
<div class="group-title">2. 货位类型</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn active" onclick="setLocation(null)">无</button>
|
||||
<button class="btn" onclick="setLocation('normal')">普通货架 A-01-03</button>
|
||||
<button class="btn" onclick="setLocation('transit')">转运区域 TRANS-001</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zongpai Count -->
|
||||
<div class="group">
|
||||
<div class="group-title">3. 总排号数量</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn active" onclick="setZongpai(0)">无</button>
|
||||
<button class="btn" onclick="setZongpai(1)">1 条</button>
|
||||
<button class="btn" onclick="setZongpai(3)">3 条</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submitting -->
|
||||
<div class="group">
|
||||
<div class="group-title">4. 提交中状态</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn active" onclick="setSubmitting(false)">空闲</button>
|
||||
<button class="btn" onclick="setSubmitting(true)">提交中 (Loading)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overview State -->
|
||||
<div class="group">
|
||||
<div class="group-title">5. 排产总览区域</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn active" onclick="setOverview('empty')">未扫描</button>
|
||||
<button class="btn" onclick="setOverview('loading')">加载中</button>
|
||||
<button class="btn" onclick="setOverview('notFound')">暂无排产信息</button>
|
||||
<button class="btn" onclick="setOverview('error')">加载失败</button>
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn" onclick="setOverview('loaded')">正常显示</button>
|
||||
<button class="btn" onclick="setOverview('errorWithData')">失败(有缓存)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Bar Override -->
|
||||
<div class="group">
|
||||
<div class="group-title">6. 状态栏覆盖状态</div>
|
||||
<div class="label">点击后状态栏显示对应覆盖文本(持续显示至切换)</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn active" onclick="setStatusBarOverride(null)">自动 (跟随基础状态)</button>
|
||||
</div>
|
||||
<div class="label">-- 扫描反馈 --</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn" onclick="setStatusBarOverride('invalid')">无效码</button>
|
||||
</div>
|
||||
<div class="label">-- 配置/操作错误 --</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn" onclick="setStatusBarOverride('noApi')">未配置API</button>
|
||||
<button class="btn" onclick="setStatusBarOverride('noLocation')">请先扫描货位号</button>
|
||||
<button class="btn" onclick="setStatusBarOverride('paichaMismatch')">排产号不一致</button>
|
||||
</div>
|
||||
<div class="label">-- 提交成功 --</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn" onclick="setStatusBarOverride('success')">上架成功</button>
|
||||
<button class="btn" onclick="setStatusBarOverride('successLocked')">锁定成功</button>
|
||||
<button class="btn" onclick="setStatusBarOverride('offShelf')">下架成功</button>
|
||||
<button class="btn" onclick="setStatusBarOverride('batchSuccess')">批量上架成功</button>
|
||||
<button class="btn" onclick="setStatusBarOverride('batchOffShelf')">批量下架成功</button>
|
||||
</div>
|
||||
<div class="label">-- 提交失败 --</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn" onclick="setStatusBarOverride('duplicate')">重复上架(弹窗)</button>
|
||||
<button class="btn" onclick="setStatusBarOverride('alreadyOffShelf')">已下架冲突</button>
|
||||
<button class="btn" onclick="setStatusBarOverride('networkError')">网络异常</button>
|
||||
<button class="btn" onclick="setStatusBarOverride('submitFail')">提交失败</button>
|
||||
<button class="btn" onclick="setStatusBarOverride('batchPartial')">批量部分失败</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dialog -->
|
||||
<div class="group">
|
||||
<div class="group-title">7. 弹窗</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn active" onclick="setDialog(false)">关闭</button>
|
||||
<button class="btn" onclick="setDialog(true)">重复上架弹窗</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- State Summary -->
|
||||
<div class="group" style="background:#eef6ff;">
|
||||
<div class="group-title" style="color:#1565C0;">当前状态摘要</div>
|
||||
<div id="stateSummary" style="font-size:11px;color:#555;line-height:1.8;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ==================== Device Simulation ==================== -->
|
||||
<div class="main">
|
||||
<div class="device" id="device">
|
||||
|
||||
<!-- AppBar -->
|
||||
<div class="fl-appbar">
|
||||
<span class="fl-appbar-title">上架登记</span>
|
||||
<span class="fl-appbar-spacer"></span>
|
||||
<button class="fl-lock-btn unlocked" id="lockBtn">
|
||||
<span class="icon">⇄</span>
|
||||
<span id="lockLabel">未锁定</span>
|
||||
<span class="shortcut">P2</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="fl-body">
|
||||
|
||||
<!-- Operation Area -->
|
||||
<div class="fl-op-area">
|
||||
<div class="fl-section-title">登记操作区</div>
|
||||
|
||||
<!-- Input Panel: Single Mode -->
|
||||
<div class="fl-input-single empty" id="singlePanel">
|
||||
<div class="loc-part">
|
||||
<span class="loc-text placeholder" id="singleLoc"></span>
|
||||
<span id="singleLocChip"></span>
|
||||
</div>
|
||||
<span class="divider">|</span>
|
||||
<div class="zp-part">
|
||||
<span class="zp-text placeholder" id="singleZp"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Panel: Locked Mode -->
|
||||
<div class="fl-input-locked empty" id="lockedPanel" style="display:none;">
|
||||
<div class="loc-row">
|
||||
<span class="loc-text" id="lockedLoc"></span>
|
||||
<span class="fl-lock-icon">🔒</span>
|
||||
<span id="lockedLocChip"></span>
|
||||
</div>
|
||||
<div class="fl-chips-area empty-space" id="chipArea"></div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button class="fl-submit disabled" id="submitBtn">
|
||||
<span id="submitText">确 认 上 架</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="fl-divider"></div>
|
||||
|
||||
<!-- Overview Section -->
|
||||
<div class="fl-overview">
|
||||
<div class="fl-overview-header-row">
|
||||
<span class="fl-overview-title">排产号货架总览</span>
|
||||
<span id="overviewSpinner" style="display:none;"><span class="fl-spinner small"></span></span>
|
||||
</div>
|
||||
<div class="fl-overview-body" id="overviewBody">
|
||||
<div class="fl-overview-msg">扫描总排号后自动显示排产号上架情况</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<div class="fl-statusbar">
|
||||
<span class="fl-status-dot blue" id="statusDot"></span>
|
||||
<span class="fl-status-text" id="statusText">等待扫描总排号或货位号…</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dialog Overlay -->
|
||||
<div class="fl-dialog-overlay hidden" id="dialogOverlay">
|
||||
<div class="fl-dialog">
|
||||
<div class="fl-dialog-title">
|
||||
<span class="icon">⚠</span>
|
||||
<span class="text">重复上架</span>
|
||||
</div>
|
||||
<div class="fl-dialog-body">
|
||||
<div>总排号:<strong>26B35</strong></div>
|
||||
<div style="margin-top:4px;">已登记货位:<strong>A-02-05</strong></div>
|
||||
<div style="margin-top:4px;">登记时间:<strong>2026-05-19 14:30:22</strong></div>
|
||||
<div style="margin-top:12px;"><strong>请核查实物,确认是否操作错误。</strong></div>
|
||||
</div>
|
||||
<div class="fl-dialog-actions">
|
||||
<button class="fl-dialog-close">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ===================== State =====================
|
||||
const state = {
|
||||
locked: false,
|
||||
locationType: null, // null | 'normal' | 'transit'
|
||||
zongpaiCount: 0, // 0, 1, 3
|
||||
submitting: false,
|
||||
overviewState: 'empty',
|
||||
statusOverride: null,
|
||||
showDialog: false,
|
||||
};
|
||||
|
||||
const sampleData = {
|
||||
paichaNo: 'R00015',
|
||||
totalCount: 5,
|
||||
items: [
|
||||
{ zongpaiNo: '26B35', workOrderNo: '3-1', quantity: 50, locationCode: 'A-01-03', status: 'on_shelf' },
|
||||
{ zongpaiNo: '26B36', workOrderNo: '3-2-1', quantity: 100, locationCode: 'A-01-05', status: 'on_shelf' },
|
||||
{ zongpaiNo: '26B37', workOrderNo: '3-2-2', quantity: 100, locationCode: null, status: 'not_shelved' },
|
||||
{ zongpaiNo: '26B38', workOrderNo: '3-2-3', quantity: 50, locationCode: 'TRANS-001', status: 'transferred' },
|
||||
{ zongpaiNo: '26B39', workOrderNo: '3-3', quantity: 45, locationCode: null, status: 'not_shelved' },
|
||||
]
|
||||
};
|
||||
|
||||
const zongpaiSamples = ['26B35', '26B37', '26B39'];
|
||||
|
||||
// ===================== State Setters =====================
|
||||
function setLock(v) {
|
||||
state.locked = v;
|
||||
updateGroup('setLock', v);
|
||||
updateUI();
|
||||
}
|
||||
function setLocation(v) {
|
||||
state.locationType = v;
|
||||
updateGroup('setLocation', v);
|
||||
updateUI();
|
||||
}
|
||||
function setZongpai(v) {
|
||||
state.zongpaiCount = v;
|
||||
updateGroup('setZongpai', v);
|
||||
updateUI();
|
||||
}
|
||||
function setSubmitting(v) {
|
||||
state.submitting = v;
|
||||
updateGroup('setSubmitting', v);
|
||||
updateUI();
|
||||
}
|
||||
function setOverview(v) {
|
||||
state.overviewState = v;
|
||||
updateGroup('setOverview', v);
|
||||
updateUI();
|
||||
}
|
||||
|
||||
const statusOverrideMap = {
|
||||
invalid: { text: '无效码:ABCXYZ123', dot: 'red' },
|
||||
noApi: { text: '未配置 API 地址,请前往设置', dot: 'red' },
|
||||
noLocation: { text: '请先扫描货位号', dot: 'red' },
|
||||
paichaMismatch:{ text: '批量凑箱仅支持同排产号产品,请移除不属于同一批次的总排号', dot: 'red' },
|
||||
success: { text: '上架成功', dot: 'green' },
|
||||
successLocked: { text: '货位已锁定,请扫描下一张执行卡', dot: 'green' },
|
||||
offShelf: { text: '下架成功', dot: 'green' },
|
||||
batchSuccess: { text: '批量上架成功(3 条)', dot: 'green' },
|
||||
batchOffShelf: { text: '批量下架成功(3 条)', dot: 'green' },
|
||||
duplicate: { text: '请确认信息并提交', dot: 'red', dialog: true },
|
||||
alreadyOffShelf:{ text: '该总排号已下架至转运区域,不可重新上架', dot: 'red' },
|
||||
networkError: { text: '网络异常,请检查网络连接', dot: 'yellow' },
|
||||
submitFail: { text: '请求参数错误', dot: 'red' },
|
||||
batchPartial: { text: '成功 2 条,失败 1 条', dot: 'red' },
|
||||
};
|
||||
|
||||
function setStatusBarOverride(key) {
|
||||
state.statusOverride = key;
|
||||
updateGroup('setStatusBarOverride', key);
|
||||
// auto show dialog for duplicate
|
||||
if (key === 'duplicate') {
|
||||
state.showDialog = true;
|
||||
updateGroup('setDialog', true);
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
|
||||
function setDialog(v) {
|
||||
state.showDialog = v;
|
||||
updateGroup('setDialog', v);
|
||||
updateUI();
|
||||
}
|
||||
|
||||
// ===================== Button Group Helper =====================
|
||||
function updateGroup(fnName, activeValue) {
|
||||
// Find all buttons in the same group by looking at the onclick pattern
|
||||
const allBtns = document.querySelectorAll('.btn');
|
||||
allBtns.forEach(btn => {
|
||||
const onclick = btn.getAttribute('onclick') || '';
|
||||
if (onclick.includes(fnName + '(')) {
|
||||
// Extract the argument
|
||||
const match = onclick.match(fnName + '\\(([^)]+)\\)');
|
||||
if (match) {
|
||||
let arg = match[1].trim().replace(/'/g, '').replace(/"/g, '');
|
||||
let isActive = false;
|
||||
if (arg === String(activeValue) ||
|
||||
(activeValue === null && arg === 'null') ||
|
||||
(activeValue === true && arg === 'true') ||
|
||||
(activeValue === false && arg === 'false')) {
|
||||
isActive = true;
|
||||
}
|
||||
if (isActive) {
|
||||
btn.classList.add('active');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===================== UI Update =====================
|
||||
function updateUI() {
|
||||
updateAppBar();
|
||||
updateInputPanel();
|
||||
updateSubmitButton();
|
||||
updateOverview();
|
||||
updateStatusBar();
|
||||
updateDialogVisibility();
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
function updateAppBar() {
|
||||
const btn = document.getElementById('lockBtn');
|
||||
const label = document.getElementById('lockLabel');
|
||||
if (state.locked) {
|
||||
btn.className = 'fl-lock-btn locked';
|
||||
label.textContent = '货位锁定';
|
||||
} else {
|
||||
btn.className = 'fl-lock-btn unlocked';
|
||||
label.textContent = '未锁定';
|
||||
}
|
||||
}
|
||||
|
||||
function updateInputPanel() {
|
||||
const singlePanel = document.getElementById('singlePanel');
|
||||
const lockedPanel = document.getElementById('lockedPanel');
|
||||
const hasLoc = state.locationType !== null;
|
||||
const hasZp = state.zongpaiCount > 0;
|
||||
const locCode = state.locationType === 'normal' ? 'A-01-03' : state.locationType === 'transit' ? 'TRANS-001' : null;
|
||||
const locFilled = hasLoc || hasZp;
|
||||
|
||||
if (!state.locked) {
|
||||
// Single mode
|
||||
singlePanel.style.display = 'flex';
|
||||
lockedPanel.style.display = 'none';
|
||||
|
||||
singlePanel.className = 'fl-input-single ' + (locFilled ? 'filled' : 'empty');
|
||||
|
||||
const singleLoc = document.getElementById('singleLoc');
|
||||
const singleLocChip = document.getElementById('singleLocChip');
|
||||
const singleZp = document.getElementById('singleZp');
|
||||
|
||||
if (hasLoc) {
|
||||
singleLoc.textContent = locCode;
|
||||
singleLoc.className = 'loc-text';
|
||||
singleLocChip.innerHTML = buildLocChip(state.locationType);
|
||||
} else {
|
||||
singleLoc.textContent = '';
|
||||
singleLoc.className = 'loc-text placeholder';
|
||||
singleLocChip.innerHTML = '';
|
||||
}
|
||||
|
||||
if (hasZp) {
|
||||
singleZp.textContent = zongpaiSamples[0];
|
||||
singleZp.className = 'zp-text';
|
||||
} else {
|
||||
singleZp.textContent = '';
|
||||
singleZp.className = 'zp-text placeholder';
|
||||
}
|
||||
} else {
|
||||
// Locked mode
|
||||
singlePanel.style.display = 'none';
|
||||
lockedPanel.style.display = 'block';
|
||||
|
||||
lockedPanel.className = 'fl-input-locked ' + (locFilled ? 'filled' : 'empty');
|
||||
|
||||
const lockedLoc = document.getElementById('lockedLoc');
|
||||
const lockedLocChip = document.getElementById('lockedLocChip');
|
||||
const chipArea = document.getElementById('chipArea');
|
||||
|
||||
if (hasLoc) {
|
||||
lockedLoc.textContent = locCode;
|
||||
lockedLocChip.innerHTML = buildLocChip(state.locationType);
|
||||
} else {
|
||||
lockedLoc.textContent = '';
|
||||
lockedLocChip.innerHTML = '';
|
||||
}
|
||||
|
||||
if (state.zongpaiCount === 0) {
|
||||
chipArea.className = 'fl-chips-area empty-space';
|
||||
chipArea.innerHTML = '';
|
||||
} else {
|
||||
chipArea.className = 'fl-chips-area';
|
||||
let html = '';
|
||||
for (let i = 0; i < state.zongpaiCount; i++) {
|
||||
html += `<span class="fl-chip">${zongpaiSamples[i]}<span class="fl-chip-del">x</span></span>`;
|
||||
}
|
||||
chipArea.innerHTML = html;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildLocChip(type) {
|
||||
if (!type) return '';
|
||||
const cls = type === 'transit' ? 'transit' : 'normal';
|
||||
const label = type === 'transit' ? '转运区域' : '普通货架';
|
||||
return `<span class="fl-loc-chip ${cls}">${label}</span>`;
|
||||
}
|
||||
|
||||
function updateSubmitButton() {
|
||||
const btn = document.getElementById('submitBtn');
|
||||
const textEl = document.getElementById('submitText');
|
||||
const hasLoc = state.locationType !== null;
|
||||
const hasZp = state.zongpaiCount > 0;
|
||||
const isTransit = state.locationType === 'transit';
|
||||
const canSubmit = hasLoc && hasZp && !state.submitting;
|
||||
|
||||
// Determine label
|
||||
let label;
|
||||
if (state.submitting) {
|
||||
label = null; // show spinner
|
||||
} else if (!canSubmit) {
|
||||
if (isTransit) {
|
||||
label = state.locked && state.zongpaiCount > 1
|
||||
? `批量转运并凑箱(${state.zongpaiCount} 条)`
|
||||
: '转运并装箱';
|
||||
} else {
|
||||
label = state.locked && state.zongpaiCount > 1
|
||||
? `批量上架(${state.zongpaiCount} 条)`
|
||||
: '确 认 上 架';
|
||||
}
|
||||
} else {
|
||||
if (isTransit) {
|
||||
label = state.locked && state.zongpaiCount > 1
|
||||
? `批量转运并凑箱(${state.zongpaiCount} 条)`
|
||||
: '转运并装箱';
|
||||
} else {
|
||||
label = state.locked && state.zongpaiCount > 1
|
||||
? `批量上架(${state.zongpaiCount} 条)`
|
||||
: '确 认 上 架';
|
||||
}
|
||||
}
|
||||
|
||||
// Determine style
|
||||
let cls = 'fl-submit';
|
||||
if (state.submitting) {
|
||||
cls += isTransit ? ' transit submitting' : (canSubmit || (hasLoc && hasZp) ? ' primary submitting' : ' disabled submitting');
|
||||
} else if (!canSubmit) {
|
||||
cls += ' disabled';
|
||||
} else if (isTransit) {
|
||||
cls += ' transit';
|
||||
} else {
|
||||
cls += ' primary';
|
||||
}
|
||||
|
||||
btn.className = cls;
|
||||
|
||||
if (state.submitting) {
|
||||
textEl.innerHTML = '<span class="fl-spinner"></span>';
|
||||
} else {
|
||||
textEl.textContent = label;
|
||||
}
|
||||
}
|
||||
|
||||
function updateOverview() {
|
||||
const body = document.getElementById('overviewBody');
|
||||
const spinner = document.getElementById('overviewSpinner');
|
||||
|
||||
switch (state.overviewState) {
|
||||
case 'empty':
|
||||
spinner.style.display = 'none';
|
||||
body.innerHTML = '<div class="fl-overview-msg">扫描总排号后自动显示排产号上架情况</div>';
|
||||
break;
|
||||
case 'loading':
|
||||
spinner.style.display = 'inline-block';
|
||||
body.innerHTML = '<div class="fl-overview-msg">正在加载排产号上架情况…</div>';
|
||||
break;
|
||||
case 'notFound':
|
||||
spinner.style.display = 'none';
|
||||
body.innerHTML = '<div class="fl-overview-msg">暂无排产信息</div>';
|
||||
break;
|
||||
case 'error':
|
||||
spinner.style.display = 'none';
|
||||
body.innerHTML = '<div class="fl-overview-error"><span class="msg">加载失败,点击重试</span><button class="fl-retry-btn">重试</button></div>';
|
||||
break;
|
||||
case 'loaded':
|
||||
spinner.style.display = 'none';
|
||||
renderOverviewTable(body, false);
|
||||
break;
|
||||
case 'errorWithData':
|
||||
spinner.style.display = 'none';
|
||||
renderOverviewTable(body, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function renderOverviewTable(container, withError) {
|
||||
let html = '';
|
||||
if (withError) {
|
||||
html += `<div class="fl-ov-error-bar">
|
||||
<span class="msg">加载失败,点击重试</span>
|
||||
<button class="fl-retry-btn">重试</button>
|
||||
</div>`;
|
||||
}
|
||||
html += `<div class="fl-ov-info">${sampleData.paichaNo} 共 ${sampleData.totalCount} 个总排号</div>`;
|
||||
html += `<div class="fl-table-head">
|
||||
<span class="c1">总排号</span>
|
||||
<span class="c2">工令号</span>
|
||||
<span class="c3">数量</span>
|
||||
<span class="c4">货位号</span>
|
||||
</div>`;
|
||||
|
||||
sampleData.items.forEach((item, idx) => {
|
||||
const isCurrent = idx === 0; // first item is current
|
||||
let bgCls = 'bg-white';
|
||||
if (item.status === 'on_shelf') bgCls = 'bg-onshelf';
|
||||
else if (item.status === 'transferred') bgCls = 'bg-transferred';
|
||||
else bgCls = 'bg-default';
|
||||
|
||||
const rowCls = `fl-table-row ${bgCls}` + (isCurrent ? ' current' : '');
|
||||
const boldCls = isCurrent ? ' bold' : '';
|
||||
html += `<div class="${rowCls}">
|
||||
<span class="c1${boldCls}">${item.zongpaiNo}</span>
|
||||
<span class="c2${boldCls}">${item.workOrderNo || '--'}</span>
|
||||
<span class="c3${boldCls}">${item.quantity}</span>
|
||||
<span class="c4${boldCls}">${item.locationCode || '\u2014'}</span>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function updateStatusBar() {
|
||||
const dot = document.getElementById('statusDot');
|
||||
const text = document.getElementById('statusText');
|
||||
|
||||
// Check override first
|
||||
if (state.statusOverride && statusOverrideMap[state.statusOverride]) {
|
||||
const ov = statusOverrideMap[state.statusOverride];
|
||||
dot.className = 'fl-status-dot ' + ov.dot;
|
||||
text.textContent = ov.text;
|
||||
return;
|
||||
}
|
||||
|
||||
// Base status (mirrors _updateBaseStatus)
|
||||
if (state.submitting) {
|
||||
dot.className = 'fl-status-dot orange';
|
||||
text.textContent = '正在提交…';
|
||||
return;
|
||||
}
|
||||
if (state.locked && state.locationType !== null && state.zongpaiCount === 0) {
|
||||
dot.className = 'fl-status-dot blue';
|
||||
text.textContent = '货位已锁定,请扫描下一张执行卡';
|
||||
return;
|
||||
}
|
||||
const hasZp = state.zongpaiCount > 0;
|
||||
const hasLoc = state.locationType !== null;
|
||||
if (hasZp && hasLoc) {
|
||||
dot.className = 'fl-status-dot blue';
|
||||
text.textContent = '请确认信息并提交';
|
||||
} else if (hasZp) {
|
||||
dot.className = 'fl-status-dot blue';
|
||||
text.textContent = '请扫描目标货位号';
|
||||
} else if (hasLoc) {
|
||||
dot.className = 'fl-status-dot blue';
|
||||
text.textContent = '请扫描执行卡';
|
||||
} else {
|
||||
dot.className = 'fl-status-dot blue';
|
||||
text.textContent = '等待扫描总排号或货位号…';
|
||||
}
|
||||
}
|
||||
|
||||
function updateDialogVisibility() {
|
||||
const overlay = document.getElementById('dialogOverlay');
|
||||
if (state.showDialog) {
|
||||
overlay.classList.remove('hidden');
|
||||
} else {
|
||||
overlay.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function updateSummary() {
|
||||
const el = document.getElementById('stateSummary');
|
||||
const locLabel = state.locationType === 'normal' ? '普通货架' : state.locationType === 'transit' ? '转运区域' : '无';
|
||||
const ovLabels = {
|
||||
empty: '未扫描', loading: '加载中', notFound: '暂无信息',
|
||||
error: '加载失败', loaded: '正常显示', errorWithData: '失败(有缓存)'
|
||||
};
|
||||
const hasLoc = state.locationType !== null;
|
||||
const hasZp = state.zongpaiCount > 0;
|
||||
const canSubmit = hasLoc && hasZp && !state.submitting;
|
||||
|
||||
el.innerHTML = `
|
||||
<div><b>锁定:</b>${state.locked ? '已锁定' : '未锁定'}</div>
|
||||
<div><b>货位:</b>${locLabel}</div>
|
||||
<div><b>总排号:</b>${state.zongpaiCount === 0 ? '无' : state.zongpaiCount + ' 条'}</div>
|
||||
<div><b>提交中:</b>${state.submitting ? '是' : '否'}</div>
|
||||
<div><b>可提交:</b>${canSubmit ? '是' : '否'}</div>
|
||||
<div><b>总览:</b>${ovLabels[state.overviewState]}</div>
|
||||
<div><b>状态覆盖:</b>${state.statusOverride || '(自动)'}</div>
|
||||
<div><b>弹窗:</b>${state.showDialog ? '显示中' : '无'}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Close dialog via button
|
||||
document.querySelector('.fl-dialog-close').addEventListener('click', () => {
|
||||
setDialog(false);
|
||||
});
|
||||
|
||||
// Initial render
|
||||
updateUI();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -189,7 +189,7 @@ body {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.scan-code {
|
||||
font-size: 13px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
flex: 1;
|
||||
@@ -197,6 +197,14 @@ body {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.scan-zongpai-extra {
|
||||
font-size: 11px;
|
||||
color: var(--text-2);
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.scan-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
@@ -216,7 +224,7 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
padding: 8px 86px 8px 12px;
|
||||
padding: 8px 12px 8px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
background: #fafafa;
|
||||
@@ -248,13 +256,19 @@ body {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 进度条+详情同行 */
|
||||
.progress-with-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
/* 进度条 */
|
||||
.progress-wrap {
|
||||
flex: 1;
|
||||
height: 3px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin: 2px 0;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
@@ -264,29 +278,13 @@ body {
|
||||
}
|
||||
.progress-fill.full { background: var(--green); }
|
||||
|
||||
.info-row2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.info-meta-text {
|
||||
font-size: 11px;
|
||||
color: var(--text-3);
|
||||
flex: 1;
|
||||
}
|
||||
.info-meta-text .hl { color: var(--amber); font-weight: 700; }
|
||||
.info-meta-text .packed { color: var(--blue); font-weight: 600; }
|
||||
|
||||
.detail-link {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 34px;
|
||||
font-size: 12px;
|
||||
justify-content: center;
|
||||
height: 28px;
|
||||
min-width: 48px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--blue);
|
||||
background: var(--blue-lt);
|
||||
@@ -309,10 +307,21 @@ body {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.info-row2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.info-meta-text {
|
||||
font-size: 11px;
|
||||
color: var(--text-3);
|
||||
flex: 1;
|
||||
}
|
||||
.info-meta-text .hl { color: var(--amber); font-weight: 700; }
|
||||
.info-meta-text .packed { color: var(--blue); font-weight: 600; }
|
||||
|
||||
/* 空状态的 info-bar */
|
||||
.info-bar.empty .info-paichan,
|
||||
.info-bar.empty .info-wo,
|
||||
.info-bar.empty .info-erp,
|
||||
.info-bar.empty .info-meta-text { color: #ccc; }
|
||||
|
||||
/* ── 已分配列表区 (可滚动) ── */
|
||||
@@ -661,15 +670,15 @@ body {
|
||||
<div class="info-bar empty">
|
||||
<div class="info-row1">
|
||||
<span class="info-paichan">排产号 —</span>
|
||||
<span class="info-wo">工单 —</span>
|
||||
<span class="info-erp">— 件</span>
|
||||
</div>
|
||||
<div class="progress-with-detail">
|
||||
<div class="progress-wrap">
|
||||
<div class="progress-fill" style="width:0%"></div>
|
||||
</div>
|
||||
<button class="detail-link disabled">详情</button>
|
||||
</div>
|
||||
<div class="info-row2">
|
||||
<span class="info-meta-text">已有 — 箱 · 最大箱号 —</span>
|
||||
<button class="detail-link disabled">详情</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -718,6 +727,7 @@ body {
|
||||
<div class="scan-bar scanned">
|
||||
<div class="scan-check">✓</div>
|
||||
<span class="scan-code">ZP202605180001</span>
|
||||
<span class="scan-zongpai-extra">WO-2026-0882 / 200件</span>
|
||||
<span class="scan-badge">已识别</span>
|
||||
</div>
|
||||
|
||||
@@ -725,15 +735,15 @@ body {
|
||||
<div class="info-bar">
|
||||
<div class="info-row1">
|
||||
<span class="info-paichan">PC-2605-A101</span>
|
||||
<span class="info-wo">WO-2026-0882</span>
|
||||
<span class="info-erp">200 件</span>
|
||||
</div>
|
||||
<div class="progress-with-detail">
|
||||
<div class="progress-wrap">
|
||||
<div class="progress-fill" style="width:25%"></div>
|
||||
</div>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
<div class="info-row2">
|
||||
<span class="info-meta-text">已有 <span class="packed">1 箱</span> · 最大箱号 <span class="hl">2</span> · 已装 50/200</span>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -781,21 +791,22 @@ body {
|
||||
<div class="scan-bar scanned">
|
||||
<div class="scan-check">✓</div>
|
||||
<span class="scan-code">ZP202605180001</span>
|
||||
<span class="scan-zongpai-extra">WO-2026-0882 / 200件</span>
|
||||
<span class="scan-badge">已识别</span>
|
||||
</div>
|
||||
|
||||
<div class="info-bar">
|
||||
<div class="info-row1">
|
||||
<span class="info-paichan">PC-2605-A101</span>
|
||||
<span class="info-wo">WO-2026-0882</span>
|
||||
<span class="info-erp">200 件</span>
|
||||
</div>
|
||||
<div class="progress-with-detail">
|
||||
<div class="progress-wrap">
|
||||
<div class="progress-fill" style="width:75%"></div>
|
||||
</div>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
<div class="info-row2">
|
||||
<span class="info-meta-text">已有 <span class="packed">3 箱</span> · 最大箱号 <span class="hl">7</span> · 已装 150/200</span>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -866,21 +877,22 @@ body {
|
||||
<div class="scan-bar scanned">
|
||||
<div class="scan-check">✓</div>
|
||||
<span class="scan-code">ZP202605180001</span>
|
||||
<span class="scan-zongpai-extra">WO-2026-0882 / 200件</span>
|
||||
<span class="scan-badge">已识别</span>
|
||||
</div>
|
||||
|
||||
<div class="info-bar">
|
||||
<div class="info-row1">
|
||||
<span class="info-paichan">PC-2605-A101</span>
|
||||
<span class="info-wo">WO-2026-0882</span>
|
||||
<span class="info-erp">200 件</span>
|
||||
</div>
|
||||
<div class="progress-with-detail">
|
||||
<div class="progress-wrap">
|
||||
<div class="progress-fill" style="width:75%"></div>
|
||||
</div>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
<div class="info-row2">
|
||||
<span class="info-meta-text">已有 <span class="packed">3 箱</span> · 最大箱号 <span class="hl">7</span> · 已装 150/200</span>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -951,21 +963,22 @@ body {
|
||||
<div class="scan-bar scanned">
|
||||
<div class="scan-check">✓</div>
|
||||
<span class="scan-code">ZP202605180001</span>
|
||||
<span class="scan-zongpai-extra">WO-2026-0882 / 200件</span>
|
||||
<span class="scan-badge">已识别</span>
|
||||
</div>
|
||||
|
||||
<div class="info-bar">
|
||||
<div class="info-row1">
|
||||
<span class="info-paichan">PC-2605-A101</span>
|
||||
<span class="info-wo">WO-2026-0882</span>
|
||||
<span class="info-erp">200 件</span>
|
||||
</div>
|
||||
<div class="progress-with-detail">
|
||||
<div class="progress-wrap">
|
||||
<div class="progress-fill" style="width:75%"></div>
|
||||
</div>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
<div class="info-row2">
|
||||
<span class="info-meta-text">已有 <span class="packed">3 箱</span> · 最大箱号 <span class="hl">7</span></span>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1041,21 +1054,22 @@ body {
|
||||
<div class="scan-bar scanned">
|
||||
<div class="scan-check">✓</div>
|
||||
<span class="scan-code">ZP202605180001</span>
|
||||
<span class="scan-zongpai-extra">WO-2026-0882 / 200件</span>
|
||||
<span class="scan-badge">已识别</span>
|
||||
</div>
|
||||
|
||||
<div class="info-bar">
|
||||
<div class="info-row1">
|
||||
<span class="info-paichan">PC-2605-A101</span>
|
||||
<span class="info-wo">WO-2026-0882</span>
|
||||
<span class="info-erp">200 件</span>
|
||||
</div>
|
||||
<div class="progress-with-detail">
|
||||
<div class="progress-wrap">
|
||||
<div class="progress-fill" style="width:25%"></div>
|
||||
</div>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
<div class="info-row2">
|
||||
<span class="info-meta-text">已有 <span class="packed">1 箱</span> · 最大箱号 <span class="hl">2</span></span>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1109,21 +1123,22 @@ body {
|
||||
<div class="scan-bar scanned">
|
||||
<div class="scan-check">✓</div>
|
||||
<span class="scan-code">ZP202605180001</span>
|
||||
<span class="scan-zongpai-extra">WO-2026-0882 / 200件</span>
|
||||
<span class="scan-badge">已识别</span>
|
||||
</div>
|
||||
|
||||
<div class="info-bar">
|
||||
<div class="info-row1">
|
||||
<span class="info-paichan">PC-2605-A101</span>
|
||||
<span class="info-wo">WO-2026-0882</span>
|
||||
<span class="info-erp">200 件</span>
|
||||
</div>
|
||||
<div class="progress-with-detail">
|
||||
<div class="progress-wrap">
|
||||
<div class="progress-fill" style="width:25%"></div>
|
||||
</div>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
<div class="info-row2">
|
||||
<span class="info-meta-text">已有 <span class="packed">2 箱</span> · 最大箱号 <span class="hl">5</span></span>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1175,21 +1190,22 @@ body {
|
||||
<div class="scan-bar scanned">
|
||||
<div class="scan-check">✓</div>
|
||||
<span class="scan-code">ZP202605180001</span>
|
||||
<span class="scan-zongpai-extra">WO-2026-0882 / 200件</span>
|
||||
<span class="scan-badge">已识别</span>
|
||||
</div>
|
||||
|
||||
<div class="info-bar">
|
||||
<div class="info-row1">
|
||||
<span class="info-paichan">PC-2605-A101</span>
|
||||
<span class="info-wo">WO-2026-0882</span>
|
||||
<span class="info-erp">200 件</span>
|
||||
</div>
|
||||
<div class="progress-with-detail">
|
||||
<div class="progress-wrap">
|
||||
<div class="progress-fill" style="width:25%"></div>
|
||||
</div>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
<div class="info-row2">
|
||||
<span class="info-meta-text">已有 <span class="packed">1 箱</span> · 最大箱号 <span class="hl">2</span> · 剩余可装 150</span>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1241,21 +1257,22 @@ body {
|
||||
<div class="scan-bar scanned">
|
||||
<div class="scan-check">✓</div>
|
||||
<span class="scan-code">ZP202605180002</span>
|
||||
<span class="scan-zongpai-extra">WO-2026-0901 / 100件</span>
|
||||
<span class="scan-badge">已识别</span>
|
||||
</div>
|
||||
|
||||
<div class="info-bar">
|
||||
<div class="info-row1">
|
||||
<span class="info-paichan">PC-2605-B202</span>
|
||||
<span class="info-wo">WO-2026-0901</span>
|
||||
<span class="info-erp">100 件</span>
|
||||
</div>
|
||||
<div class="progress-with-detail">
|
||||
<div class="progress-wrap">
|
||||
<div class="progress-fill full" style="width:100%"></div>
|
||||
</div>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
<div class="info-row2">
|
||||
<span class="info-meta-text">已有 <span class="packed">3 箱</span> · 最大箱号 <span class="hl">3</span> · <span style="color:var(--green);font-weight:700">100/100 已装完</span></span>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1347,21 +1364,22 @@ body {
|
||||
<div class="scan-bar scanned">
|
||||
<div class="scan-check">✓</div>
|
||||
<span class="scan-code">ZP202605180001</span>
|
||||
<span class="scan-zongpai-extra">WO-2026-0882 / 200件</span>
|
||||
<span class="scan-badge">已识别</span>
|
||||
</div>
|
||||
|
||||
<div class="info-bar">
|
||||
<div class="info-row1">
|
||||
<span class="info-paichan">PC-2605-A101</span>
|
||||
<span class="info-wo">WO-2026-0882</span>
|
||||
<span class="info-erp">200 件</span>
|
||||
</div>
|
||||
<div class="progress-with-detail">
|
||||
<div class="progress-wrap">
|
||||
<div class="progress-fill" style="width:75%"></div>
|
||||
</div>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
<div class="info-row2">
|
||||
<span class="info-meta-text">已有 <span class="packed">3 箱</span> · 最大箱号 <span class="hl">7</span> · 已装 150/200</span>
|
||||
<button class="detail-link">详情</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1456,10 +1474,10 @@ body {
|
||||
│ "装箱编号" [⇄ 单码装箱 P2] │ AppBar
|
||||
├──────────────────────────────────────────┤
|
||||
│ [通知条] 可叠加显示,每条 ~30px │ 可选
|
||||
├──────────────────────────────────────────┤ 38px
|
||||
│ ✓ ZP202605180001 已识别 │ 扫码条
|
||||
├──────────────────────────────────────────┤ ~52px
|
||||
│ PC-2605-A101 WO-2026-0882 200件 [详情]│
|
||||
├──────────────────────────────────────────┤ ~50px
|
||||
│ ✓ ZP202605180001 WO-0882 / 200件 已识别 │ 扫码条
|
||||
├──────────────────────────────────────────┤ ~45px
|
||||
│ PC-2605-A101 [详情] │
|
||||
│ ▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 进度条 │ info-bar
|
||||
│ 已有 2箱 · 最大箱号 5 · 已装50/200 │
|
||||
├──────────────────────────────────────────┤
|
||||
|
||||
@@ -83,8 +83,9 @@ class BoxingPage extends StatefulWidget {
|
||||
State<BoxingPage> createState() => _BoxingPageState();
|
||||
}
|
||||
|
||||
class _BoxingPageState extends State<BoxingPage> {
|
||||
class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
|
||||
static const _androidKeyCodeButtonR1 = 103;
|
||||
static const _androidKeyCodeButtonL2 = 104;
|
||||
|
||||
final _scannerService = ScannerService();
|
||||
final _apiService = ApiService();
|
||||
@@ -147,6 +148,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
final arguments = widget.arguments;
|
||||
if (arguments != null) {
|
||||
_mode = arguments.initialMode;
|
||||
@@ -163,6 +165,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_boxNoController.removeListener(_onBoxNoTextChanged);
|
||||
_scanSubscription?.cancel();
|
||||
_focusNode.dispose();
|
||||
@@ -180,11 +183,24 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void _onKeyEvent(KeyEvent event) {
|
||||
if (event is! KeyDownEvent || _isAutoProcessing) return;
|
||||
if (_isModeSwitchKey(event)) {
|
||||
_cycleMode();
|
||||
}
|
||||
if (_isFinishBoxKey(event)) {
|
||||
final hasItems = _visibleManyToOnePackedItems.isNotEmpty;
|
||||
if (hasItems && _mode == BoxingMode.multiCode) {
|
||||
_finishManyToOneBox();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _isModeSwitchKey(KeyEvent event) {
|
||||
@@ -194,6 +210,12 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
_androidKeyCodeFromLogicalKey(key) == _androidKeyCodeButtonR1;
|
||||
}
|
||||
|
||||
bool _isFinishBoxKey(KeyEvent event) {
|
||||
final key = event.logicalKey;
|
||||
return key == LogicalKeyboardKey.gameButtonLeft2 ||
|
||||
_androidKeyCodeFromLogicalKey(key) == _androidKeyCodeButtonL2;
|
||||
}
|
||||
|
||||
int? _androidKeyCodeFromLogicalKey(LogicalKeyboardKey key) {
|
||||
final keyId = key.keyId;
|
||||
const androidPlane = LogicalKeyboardKey.androidPlane;
|
||||
@@ -322,7 +344,19 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
return false;
|
||||
}
|
||||
|
||||
_feedbackService.trigger(FeedbackEvent.scanValid);
|
||||
// 判断该总牌号是否已全部装箱完毕
|
||||
final packedQuantity = result.currentZongpaiBoxes.fold<int>(
|
||||
0,
|
||||
(sum, item) => sum + item.quantity,
|
||||
);
|
||||
final totalQuantity = result.quantity ?? 0;
|
||||
final alreadyCompleted = totalQuantity > 0 && packedQuantity >= totalQuantity;
|
||||
|
||||
_feedbackService.trigger(
|
||||
alreadyCompleted
|
||||
? FeedbackEvent.alreadyCompleted
|
||||
: FeedbackEvent.scanValid,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_zongpaiNo = zongpai;
|
||||
@@ -664,9 +698,6 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
StatusDotColor.green,
|
||||
const Duration(milliseconds: 1500),
|
||||
);
|
||||
Future.delayed(const Duration(milliseconds: 1500), () {
|
||||
if (mounted) setState(() => _resetState());
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_phase = _Phase.scanned;
|
||||
@@ -1263,7 +1294,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
case _Phase.scanned:
|
||||
_statusDot = StatusDotColor.blue;
|
||||
if (_remainingQuantity <= 0) {
|
||||
_statusDot = StatusDotColor.green;
|
||||
_statusDot = StatusDotColor.red;
|
||||
_statusText = '该总排号已全部装箱完毕';
|
||||
return;
|
||||
}
|
||||
@@ -1296,10 +1327,15 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
onKeyEvent: _onKeyEvent,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('装箱编号'),
|
||||
title: Row(
|
||||
children: [
|
||||
const Text('装箱编号'),
|
||||
const SizedBox(width: 8),
|
||||
_buildModePill(),
|
||||
],
|
||||
),
|
||||
toolbarHeight: 44,
|
||||
titleSpacing: 12,
|
||||
actions: [_buildModePill(), const SizedBox(width: 8)],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
@@ -1396,11 +1432,11 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
_remainingQuantity <= 0) {
|
||||
banners.add(
|
||||
_noticeBanner(
|
||||
icon: Icon(Icons.check, size: 14, color: Colors.green.shade800),
|
||||
icon: Icon(Icons.warning, size: 14, color: Colors.red.shade800),
|
||||
text: '${_zongpaiNo!} 已全部装箱完毕,无需操作',
|
||||
background: Colors.green.shade50,
|
||||
foreground: Colors.green.shade800,
|
||||
border: Colors.green.shade100,
|
||||
background: Colors.red.shade50,
|
||||
foreground: Colors.red.shade800,
|
||||
border: Colors.red.shade100,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1437,27 +1473,10 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSingleCodeBody(ColorScheme colorScheme) {
|
||||
final isWaiting = _phase == _Phase.waiting && _zongpaiNo == null;
|
||||
final isFinished =
|
||||
_phase == _Phase.scanned &&
|
||||
_zongpaiNo != null &&
|
||||
_remainingQuantity <= 0;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildSingleScanBar(isWaiting),
|
||||
_buildSingleInfoBar(isWaiting, colorScheme),
|
||||
_buildSingleAssignedList(isWaiting, isFinished),
|
||||
_buildSingleBottomArea(isWaiting || isFinished),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSingleScanBar(bool isWaiting) {
|
||||
if (isWaiting) {
|
||||
return Container(
|
||||
height: 38,
|
||||
height: 50,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
@@ -1477,7 +1496,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
}
|
||||
|
||||
return Container(
|
||||
height: 38,
|
||||
height: 50,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade50,
|
||||
@@ -1490,23 +1509,17 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
_zongpaiNo ?? '',
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w700),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'已识别',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.green.shade700,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${_workOrderNo ?? "--"} / ${_erpQuantity ?? 0}件 / 已装$_packedQuantity',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1514,6 +1527,23 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSingleCodeBody(ColorScheme colorScheme) {
|
||||
final isWaiting = _phase == _Phase.waiting && _zongpaiNo == null;
|
||||
final isFinished =
|
||||
_phase == _Phase.scanned &&
|
||||
_zongpaiNo != null &&
|
||||
_remainingQuantity <= 0;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
_buildSingleScanBar(isWaiting),
|
||||
_buildSingleInfoBar(isWaiting, colorScheme),
|
||||
_buildSingleAssignedList(isWaiting, isFinished),
|
||||
_buildSingleBottomArea(isWaiting || isFinished),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSingleInfoBar(bool isWaiting, ColorScheme colorScheme) {
|
||||
final total = _erpQuantity ?? 0;
|
||||
final packed = isWaiting ? 0 : _packedQuantity;
|
||||
@@ -1522,57 +1552,50 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
final detailEnabled = !isWaiting && _existingBoxes.isNotEmpty;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
Text(
|
||||
isWaiting ? '排产号 --' : (_paichanNo ?? '--'),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: isWaiting
|
||||
? Colors.grey.shade400
|
||||
: Colors.black87,
|
||||
color: isWaiting ? Colors.grey.shade400 : Colors.black87,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
isWaiting ? '工单 --' : (_workOrderNo ?? '--'),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
isWaiting
|
||||
? '已有 -- 箱 · 最大箱号 --'
|
||||
: '已有 ${_existingBoxes.length} 箱 · 最大箱号 $_maxBoxNo',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontSize: 10,
|
||||
color: isWaiting
|
||||
? Colors.grey.shade400
|
||||
: Colors.black54,
|
||||
: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
isWaiting ? '-- 件' : '$total 件',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: isWaiting
|
||||
? Colors.grey.shade400
|
||||
: Colors.black87,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
const SizedBox(height: 4),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: LinearProgressIndicator(
|
||||
@@ -1584,22 +1607,6 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
isWaiting
|
||||
? '已有 -- 箱 · 最大箱号 --'
|
||||
: '已有 ${_existingBoxes.length} 箱 · 最大箱号 $_maxBoxNo · 已装 $packed/$total',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: isWaiting
|
||||
? Colors.grey.shade400
|
||||
: Colors.grey.shade600,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1607,6 +1614,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
_buildDetailButton(detailEnabled, colorScheme),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1676,31 +1684,23 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
|
||||
Widget _buildSingleAssignedHeader(int count) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||
height: 28,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
|
||||
color: Colors.grey.shade200,
|
||||
border: Border(bottom: BorderSide(color: Colors.grey.shade400)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Expanded(flex: 22, child: Text('箱号', style: _headerStyle)),
|
||||
const Expanded(flex: 24, child: Text('工令号', style: _headerStyle)),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'已分配(此总排号)',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black54,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$count 条',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
flex: 14,
|
||||
child: Text('数量', textAlign: TextAlign.center, style: _headerStyle),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 64,
|
||||
child: Text('操作', textAlign: TextAlign.center, style: _headerStyle),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1714,81 +1714,74 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
? Colors.amber
|
||||
: (deleting ? Colors.red : Colors.green);
|
||||
|
||||
final Color bgColor;
|
||||
if (editing) {
|
||||
bgColor = Colors.yellow.shade50;
|
||||
} else if (deleting) {
|
||||
bgColor = Colors.red.shade50;
|
||||
} else {
|
||||
bgColor = Colors.transparent;
|
||||
}
|
||||
|
||||
final rowStyle = TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
constraints: const BoxConstraints(minHeight: 36),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: editing
|
||||
? Colors.yellow.shade50
|
||||
: (deleting ? Colors.red.shade50 : null),
|
||||
border: Border(bottom: BorderSide(color: Colors.grey.shade200)),
|
||||
color: bgColor,
|
||||
border: Border(
|
||||
left: BorderSide(color: barColor, width: 3),
|
||||
bottom: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 14,
|
||||
child: Text(
|
||||
'$index',
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
width: 3,
|
||||
height: 22,
|
||||
decoration: BoxDecoration(
|
||||
color: barColor,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
flex: 22,
|
||||
child: Text(
|
||||
'${item.boxNo} 号箱',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
'本次扫码',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey.shade500,
|
||||
style: rowStyle.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
flex: 24,
|
||||
child: Text(
|
||||
_workOrderNo ?? '--',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: rowStyle,
|
||||
),
|
||||
),
|
||||
if (editing)
|
||||
SizedBox(
|
||||
Expanded(
|
||||
flex: 14,
|
||||
child: editing
|
||||
? SizedBox(
|
||||
width: 52,
|
||||
height: 28,
|
||||
child: TextField(
|
||||
keyboardType: TextInputType.none,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
textAlign: TextAlign.center,
|
||||
controller:
|
||||
TextEditingController(text: _editingAssignedQuantity)
|
||||
..selection = TextSelection.collapsed(
|
||||
controller: TextEditingController(
|
||||
text: _editingAssignedQuantity,
|
||||
)..selection = TextSelection.collapsed(
|
||||
offset: _editingAssignedQuantity.length,
|
||||
),
|
||||
onChanged: (value) => _editingAssignedQuantity = value,
|
||||
onChanged: (v) => _editingAssignedQuantity = v,
|
||||
onSubmitted: (_) => _saveAssignedItem(item),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
borderSide: const BorderSide(
|
||||
@@ -1803,20 +1796,28 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
: Text(
|
||||
'${item.quantity} 件',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: deleting ? Colors.red : Colors.black54,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 64,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (editing) ...[
|
||||
_iconBtn(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
onTap: _isSubmitting ? null : () => _saveAssignedItem(item),
|
||||
onTap: _isSubmitting
|
||||
? null
|
||||
: () => _saveAssignedItem(item),
|
||||
),
|
||||
_iconBtn(
|
||||
Icons.close,
|
||||
@@ -1827,7 +1828,9 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
_iconBtn(
|
||||
Icons.edit,
|
||||
color: Colors.grey.shade700,
|
||||
onTap: _isSubmitting ? null : () => _startEditAssigned(item),
|
||||
onTap: _isSubmitting
|
||||
? null
|
||||
: () => _startEditAssigned(item),
|
||||
),
|
||||
_iconBtn(
|
||||
Icons.delete_outline,
|
||||
@@ -1846,10 +1849,13 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (deleting)
|
||||
Container(
|
||||
height: 28,
|
||||
padding: const EdgeInsets.only(left: 34, right: 12),
|
||||
padding: const EdgeInsets.only(left: 15, right: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
border: Border(bottom: BorderSide(color: Colors.red.shade200)),
|
||||
@@ -2055,46 +2061,47 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
final isSingle = _mode == BoxingMode.singleCode;
|
||||
final bgColor = isSingle ? Colors.blue.shade700 : Colors.orange.shade700;
|
||||
|
||||
return GestureDetector(
|
||||
return Material(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: bgColor,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: _isAutoProcessing ? null : _cycleMode,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
height: 36,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.swap_horiz, color: Colors.white, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
const Icon(Icons.swap_horiz, color: Colors.white, size: 18),
|
||||
const SizedBox(width: 6),
|
||||
Flexible(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
_modeLabel,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'P2',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.88),
|
||||
fontSize: 12,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2228,31 +2235,23 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
|
||||
Widget _buildManyToOneListHeader(int count) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||
height: 28,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
|
||||
color: Colors.grey.shade200,
|
||||
border: Border(bottom: BorderSide(color: Colors.grey.shade400)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Expanded(flex: 22, child: Text('总排号', style: _headerStyle)),
|
||||
const Expanded(flex: 24, child: Text('工令号', style: _headerStyle)),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'已装入本箱',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.black54,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$count 项',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
flex: 14,
|
||||
child: Text('数量', textAlign: TextAlign.center, style: _headerStyle),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 64,
|
||||
child: Text('操作', textAlign: TextAlign.center, style: _headerStyle),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -2270,85 +2269,73 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
? Colors.amber
|
||||
: (deleting ? Colors.red : Colors.green);
|
||||
|
||||
final Color bgColor;
|
||||
if (editing) {
|
||||
bgColor = Colors.yellow.shade50;
|
||||
} else if (deleting) {
|
||||
bgColor = Colors.red.shade50;
|
||||
} else {
|
||||
bgColor = Colors.transparent;
|
||||
}
|
||||
|
||||
final rowStyle = TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
height: 40,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
constraints: const BoxConstraints(minHeight: 36),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: editing
|
||||
? Colors.yellow.shade50
|
||||
: (deleting ? Colors.red.shade50 : null),
|
||||
border: Border(bottom: BorderSide(color: Colors.grey.shade200)),
|
||||
color: bgColor,
|
||||
border: Border(
|
||||
left: BorderSide(color: barColor, width: 3),
|
||||
bottom: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Index
|
||||
SizedBox(
|
||||
width: 14,
|
||||
child: Text(
|
||||
'$index',
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
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(
|
||||
flex: 22,
|
||||
child: Text(
|
||||
item.zongpaiNo,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: rowStyle.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
Text(
|
||||
),
|
||||
Expanded(
|
||||
flex: 24,
|
||||
child: Text(
|
||||
item.workOrderNo ?? '--',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
style: rowStyle,
|
||||
),
|
||||
),
|
||||
// Quantity / edit input
|
||||
if (editing)
|
||||
SizedBox(
|
||||
Expanded(
|
||||
flex: 14,
|
||||
child: editing
|
||||
? SizedBox(
|
||||
width: 52,
|
||||
height: 26,
|
||||
child: TextField(
|
||||
keyboardType: TextInputType.none,
|
||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
textAlign: TextAlign.center,
|
||||
controller:
|
||||
TextEditingController(text: _editingPackedQuantity)
|
||||
..selection = TextSelection.collapsed(
|
||||
controller: TextEditingController(
|
||||
text: _editingPackedQuantity,
|
||||
)..selection = TextSelection.collapsed(
|
||||
offset: _editingPackedQuantity.length,
|
||||
),
|
||||
onChanged: (value) => _editingPackedQuantity = value,
|
||||
onChanged: (v) => _editingPackedQuantity = v,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
borderSide: const BorderSide(
|
||||
@@ -2363,21 +2350,28 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
: Text(
|
||||
'${item.quantity} / $totalText',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: deleting ? Colors.red : Colors.green.shade700,
|
||||
),
|
||||
),
|
||||
// Action buttons
|
||||
),
|
||||
SizedBox(
|
||||
width: 64,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (editing) ...[
|
||||
_iconBtn(
|
||||
Icons.check,
|
||||
color: Colors.green,
|
||||
onTap: _isSubmitting ? null : () => _savePackedItem(item),
|
||||
onTap: _isSubmitting
|
||||
? null
|
||||
: () => _savePackedItem(item),
|
||||
),
|
||||
_iconBtn(
|
||||
Icons.close,
|
||||
@@ -2387,7 +2381,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
] else ...[
|
||||
_iconBtn(
|
||||
Icons.edit,
|
||||
color: Colors.orange.shade700,
|
||||
color: Colors.grey.shade700,
|
||||
onTap: _isSubmitting
|
||||
? null
|
||||
: () => _startEditPackedItem(item),
|
||||
@@ -2409,25 +2403,23 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
// Inline delete confirmation
|
||||
],
|
||||
),
|
||||
),
|
||||
if (deleting)
|
||||
Container(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 34,
|
||||
right: 12,
|
||||
top: 2,
|
||||
bottom: 5,
|
||||
),
|
||||
height: 28,
|
||||
padding: const EdgeInsets.only(left: 15, right: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
border: Border(bottom: BorderSide(color: Colors.red.shade200)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
Expanded(
|
||||
child: Text(
|
||||
'确认删除?',
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -2517,35 +2509,7 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildManyToOneSubmitRow(hasScan, hasItems),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -2599,9 +2563,15 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildManyToOneSubmitRow(bool hasScan) {
|
||||
Widget _buildManyToOneSubmitRow(bool hasScan, bool hasItems) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||
padding: const EdgeInsets.fromLTRB(12, 7, 12, 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Left half: quantity label + input + confirm
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@@ -2690,6 +2660,63 @@ class _BoxingPageState extends State<BoxingPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Right half: 完成本箱
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: SizedBox(
|
||||
height: 34,
|
||||
child: OutlinedButton(
|
||||
onPressed: hasItems ? _finishManyToOneBox : null,
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: hasItems
|
||||
? Colors.grey.shade600
|
||||
: Colors.grey.shade300,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'完成本箱',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: hasItems ? Colors.black87 : Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: hasItems
|
||||
? Colors.grey.shade200
|
||||
: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
'P3',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: hasItems ? Colors.black54 : Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const _headerStyle = TextStyle(fontSize: 10, fontWeight: FontWeight.w700);
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:pad_scanner/services/code_parser.dart';
|
||||
import 'package:pad_scanner/services/api_service.dart';
|
||||
import 'package:pad_scanner/services/feedback_service.dart';
|
||||
import 'package:pad_scanner/services/registration_linking.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
import 'package:pad_scanner/pages/boxing_page.dart';
|
||||
import 'package:pad_scanner/widgets/status_bar.dart';
|
||||
|
||||
@@ -45,6 +46,9 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
PaichaOverviewResult? _overview;
|
||||
int _overviewRequestId = 0;
|
||||
|
||||
/// Whether a cross-paicha warning dialog is currently showing.
|
||||
bool _crossPaichaPending = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -136,6 +140,23 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
}
|
||||
|
||||
void _handleZongpaiScan(String zongpaiNo) {
|
||||
// If cross-paicha dialog is showing, just vibrate and discard
|
||||
if (_crossPaichaPending) {
|
||||
_triggerDoubleVibration();
|
||||
return;
|
||||
}
|
||||
|
||||
// In locked mode with existing data, check for cross-paicha scan
|
||||
if (_isLocked &&
|
||||
_zongpaiNos.isNotEmpty &&
|
||||
_overview?.success == true &&
|
||||
!_overview!.items.any((item) => item.zongpaiNo == zongpaiNo)) {
|
||||
_crossPaichaPending = true;
|
||||
_triggerDoubleVibration();
|
||||
_showCrossPaichaDialog(zongpaiNo);
|
||||
return;
|
||||
}
|
||||
|
||||
final shouldRefresh = _overviewZongpaiNo != zongpaiNo;
|
||||
_feedbackService.trigger(FeedbackEvent.scanValid);
|
||||
setState(() {
|
||||
@@ -157,6 +178,10 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
}
|
||||
}
|
||||
|
||||
void _triggerDoubleVibration() {
|
||||
Vibration.vibrate(pattern: [0, 200, 100, 200]);
|
||||
}
|
||||
|
||||
void _removeZongpai(String zongpaiNo) {
|
||||
setState(() {
|
||||
_zongpaiNos.remove(zongpaiNo);
|
||||
@@ -211,6 +236,18 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
|
||||
bool get _isTransitTarget => isTransitTarget(_locationType, _locationCode);
|
||||
|
||||
/// Find the first scanned item that is already on shelf (for batch submit blocking).
|
||||
PaichaOverviewItem? _findOnShelfInScanned() {
|
||||
if (_overview?.success != true) return null;
|
||||
final scannedSet = _zongpaiNos.toSet();
|
||||
for (final item in _overview!.items) {
|
||||
if (scannedSet.contains(item.zongpaiNo) && item.status == 'on_shelf') {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String?> _baseUrl() async {
|
||||
final configService = AppConfigService();
|
||||
final baseUrl = await configService.getString('api_url') ?? '';
|
||||
@@ -298,6 +335,19 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Pre-check: block entire batch if any item is already on shelf (normal shelf)
|
||||
if (!_isTransitTarget) {
|
||||
final onShelfItem = _findOnShelfInScanned();
|
||||
if (onShelfItem != null) {
|
||||
setState(() => _isSubmitting = false);
|
||||
_feedbackService.trigger(FeedbackEvent.duplicateError);
|
||||
_showDuplicateDialog(
|
||||
onShelfItem.zongpaiNo,
|
||||
{'location_code': onShelfItem.locationCode ?? '未知'},
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
await _submitBatch(baseUrl);
|
||||
} else {
|
||||
await _submitOne(baseUrl, _zongpaiNos.first);
|
||||
@@ -595,6 +645,144 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showCrossPaichaDialog(String newZongpaiNo) {
|
||||
final currentPaicha = _overview?.paichaNo ?? '--';
|
||||
final dialogFocus = FocusNode();
|
||||
var dismissed = false;
|
||||
var selectedIndex = 0; // 0 = "否" (default), 1 = "是"
|
||||
|
||||
void dismissAsNo() {
|
||||
if (dismissed) return;
|
||||
dismissed = true;
|
||||
Navigator.of(context).pop();
|
||||
setState(() {
|
||||
_crossPaichaPending = false;
|
||||
});
|
||||
}
|
||||
|
||||
void confirmSwitch() {
|
||||
if (dismissed) return;
|
||||
dismissed = true;
|
||||
Navigator.of(context).pop();
|
||||
_switchToNewPaicha(newZongpaiNo);
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setDialogState) {
|
||||
return KeyboardListener(
|
||||
focusNode: dialogFocus,
|
||||
onKeyEvent: (event) {
|
||||
if (event is! KeyDownEvent) return;
|
||||
final androidKey =
|
||||
_androidKeyCodeFromLogicalKey(event.logicalKey);
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowUp ||
|
||||
androidKey == 19) {
|
||||
setDialogState(() => selectedIndex = 0);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown ||
|
||||
androidKey == 20) {
|
||||
setDialogState(() => selectedIndex = 1);
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||
selectedIndex == 0 ? dismissAsNo() : confirmSwitch();
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
dismissAsNo();
|
||||
}
|
||||
},
|
||||
child: PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) dismissAsNo();
|
||||
},
|
||||
child: AlertDialog(
|
||||
backgroundColor: Colors.orange.shade50,
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber, color: Colors.orange.shade800),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'跨排产号扫描',
|
||||
style: TextStyle(color: Colors.orange.shade900),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'总排号 $newZongpaiNo 不属于当前排产号($currentPaicha),'
|
||||
'是否切换到新的排产号?\n\n'
|
||||
'选择「是」将放弃当前已扫描的所有数据。',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_dialogOptionBtn(
|
||||
label: '否',
|
||||
selected: selectedIndex == 0,
|
||||
onTap: dismissAsNo,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_dialogOptionBtn(
|
||||
label: '是,切换排产号',
|
||||
selected: selectedIndex == 1,
|
||||
onTap: confirmSwitch,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
dialogFocus.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
Widget _dialogOptionBtn({
|
||||
required String label,
|
||||
required bool selected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 40,
|
||||
child: ElevatedButton(
|
||||
onPressed: onTap,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
selected ? Colors.blue.shade700 : Colors.grey.shade200,
|
||||
foregroundColor: selected ? Colors.white : Colors.black87,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontWeight: selected ? FontWeight.w800 : FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _switchToNewPaicha(String newZongpaiNo) {
|
||||
_feedbackService.trigger(FeedbackEvent.paichanSwitch);
|
||||
setState(() {
|
||||
_crossPaichaPending = false;
|
||||
_zongpaiNos.clear();
|
||||
_zongpaiNos.add(newZongpaiNo);
|
||||
_overview = null;
|
||||
_overviewNotFound = false;
|
||||
_overviewError = null;
|
||||
_clearStatusOverride();
|
||||
});
|
||||
_loadOverview(newZongpaiNo);
|
||||
}
|
||||
|
||||
void _toggleLock(bool value) {
|
||||
if (value && _locationCode == null) {
|
||||
_feedbackService.trigger(FeedbackEvent.submitFailure);
|
||||
@@ -623,17 +811,6 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
return Colors.blue;
|
||||
}
|
||||
|
||||
Color _overviewRowColor(String status) {
|
||||
switch (status) {
|
||||
case 'on_shelf':
|
||||
return const Color(0xFFE3F2FD);
|
||||
case 'transferred':
|
||||
return const Color(0xFFFFF3E0);
|
||||
default:
|
||||
return const Color(0xFFF5F5F5);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
@@ -657,62 +834,10 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'登记操作区',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
_isLocked
|
||||
? _buildLockedInputPanel()
|
||||
: _buildSingleInputPanel(),
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 46,
|
||||
child: ElevatedButton(
|
||||
onPressed: _canSubmit ? _submit : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: registrationSubmitColor(
|
||||
canSubmit: _canSubmit,
|
||||
isTransitTarget: _isTransitTarget,
|
||||
primaryColor: colorScheme.primary,
|
||||
),
|
||||
foregroundColor: _canSubmit
|
||||
? Colors.white
|
||||
: Colors.grey.shade600,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 22,
|
||||
height: 22,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
registrationSubmitLabel(
|
||||
isTransitTarget: _isTransitTarget,
|
||||
isLocked: _isLocked,
|
||||
zongpaiCount: _zongpaiNos.length,
|
||||
),
|
||||
style: const TextStyle(fontSize: 17),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(height: 1, color: Colors.grey.shade300),
|
||||
Expanded(child: _buildOverviewSection()),
|
||||
_buildPaichaHeader(),
|
||||
if (_overview?.success == true) _buildTableHead(),
|
||||
Expanded(child: _buildListBody()),
|
||||
_buildBottomBar(colorScheme),
|
||||
StatusBar(dotColor: effectiveDot, text: effectiveText),
|
||||
],
|
||||
),
|
||||
@@ -770,130 +895,6 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSingleInputPanel() {
|
||||
final hasLocation = _locationCode != null;
|
||||
final hasZongpai = _zongpaiNos.isNotEmpty;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(minHeight: 58),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: hasLocation || hasZongpai
|
||||
? Colors.green
|
||||
: Colors.grey.shade400,
|
||||
width: hasLocation || hasZongpai ? 2 : 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 5,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
_locationCode ?? '',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: hasLocation ? Colors.black87 : Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasLocation) ...[
|
||||
const SizedBox(width: 6),
|
||||
_buildLocationChip(),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
'|',
|
||||
style: TextStyle(fontSize: 20, color: Colors.grey.shade500),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
hasZongpai ? _zongpaiNos.first : '',
|
||||
textAlign: TextAlign.right,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: hasZongpai ? Colors.black87 : Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLockedInputPanel() {
|
||||
final hasLocation = _locationCode != null;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(minHeight: 82),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: hasLocation || _zongpaiNos.isNotEmpty
|
||||
? Colors.green
|
||||
: Colors.grey.shade400,
|
||||
width: hasLocation || _zongpaiNos.isNotEmpty ? 2 : 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_locationCode ?? '',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: hasLocation ? Colors.black87 : Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.lock, color: Colors.orange, size: 18),
|
||||
const SizedBox(width: 6),
|
||||
if (hasLocation) _buildLocationChip(),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (_zongpaiNos.isEmpty)
|
||||
const SizedBox(height: 24)
|
||||
else
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: _zongpaiNos
|
||||
.map(
|
||||
(zongpaiNo) => InputChip(
|
||||
label: Text(zongpaiNo),
|
||||
visualDensity: VisualDensity.compact,
|
||||
onDeleted: () => _removeZongpai(zongpaiNo),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLocationChip() {
|
||||
final color = _locationLabelColor(_locationType);
|
||||
return Container(
|
||||
@@ -913,59 +914,188 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverviewSection() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
|
||||
// === Paicha Header ===
|
||||
|
||||
Widget _buildPaichaHeader() {
|
||||
final overview = _overview;
|
||||
final hasData = overview?.success == true;
|
||||
final hasLoc = _locationCode != null;
|
||||
|
||||
int totalCount = 0, shelvedCount = 0, transferredCount = 0;
|
||||
if (hasData && overview != null) {
|
||||
totalCount = overview.totalCount;
|
||||
for (final item in overview.items) {
|
||||
switch (item.status) {
|
||||
case 'on_shelf':
|
||||
shelvedCount++;
|
||||
case 'transferred':
|
||||
transferredCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
border: Border(bottom: BorderSide(color: Colors.grey.shade300)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
hasData ? (overview!.paichaNo ?? '--') : '--',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: hasData ? Colors.black87 : Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (hasLoc)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'排产号货架总览',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700),
|
||||
Text(
|
||||
_locationCode!,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
if (_overviewLoading)
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
const SizedBox(width: 5),
|
||||
_buildLocationChip(),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildStatTag(
|
||||
'共 $totalCount',
|
||||
const Color(0xFFE3F2FD),
|
||||
const Color(0xFF1565C0),
|
||||
hasData,
|
||||
),
|
||||
_buildStatTag(
|
||||
'上架 $shelvedCount',
|
||||
const Color(0xFFE8F5E9),
|
||||
const Color(0xFF2E7D32),
|
||||
hasData,
|
||||
),
|
||||
_buildStatTag(
|
||||
'转运 $transferredCount',
|
||||
const Color(0xFFFFF3E0),
|
||||
const Color(0xFFE65100),
|
||||
hasData,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Expanded(child: _buildOverviewBody()),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverviewBody() {
|
||||
if (_overviewZongpaiNo == null) {
|
||||
return _buildOverviewMessage('扫描总排号后自动显示排产号上架情况');
|
||||
}
|
||||
if (_overviewLoading && _overview == null) {
|
||||
return _buildOverviewMessage('正在加载排产号上架情况…');
|
||||
}
|
||||
if (_overviewNotFound) {
|
||||
return _buildOverviewMessage('暂无排产信息');
|
||||
}
|
||||
if (_overviewError != null && _overview == null) {
|
||||
return _buildRetryMessage(_overviewError!);
|
||||
}
|
||||
final overview = _overview;
|
||||
if (overview == null) {
|
||||
return _buildOverviewMessage('扫描总排号后自动显示排产号上架情况');
|
||||
Widget _buildStatTag(String text, Color bg, Color fg, bool hasData) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 1),
|
||||
margin: const EdgeInsets.only(left: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: hasData ? bg : const Color(0xFFF5F5F5),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: hasData ? fg : Colors.grey.shade400,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// === Table Head ===
|
||||
|
||||
Widget _buildTableHead() {
|
||||
return Container(
|
||||
height: 28,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
border: Border(bottom: BorderSide(color: Colors.grey.shade400)),
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
Expanded(flex: 22, child: Text('总排号', style: _headerStyle)),
|
||||
Expanded(flex: 18, child: Text('工令号', style: _headerStyle)),
|
||||
Expanded(
|
||||
flex: 12,
|
||||
child: Text(
|
||||
'数量',
|
||||
textAlign: TextAlign.center,
|
||||
style: _headerStyle,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 28,
|
||||
child: Text(
|
||||
'货位号',
|
||||
textAlign: TextAlign.right,
|
||||
style: _headerStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// === List Body ===
|
||||
|
||||
Widget _buildListBody() {
|
||||
if (_overviewZongpaiNo == null) {
|
||||
return _buildListMessage('扫描总排号后自动显示排产号上架情况');
|
||||
}
|
||||
if (_overviewLoading && _overview == null) {
|
||||
return _buildListMessage('正在加载排产号上架情况…');
|
||||
}
|
||||
if (_overviewNotFound) {
|
||||
return _buildListMessage('暂无排产信息');
|
||||
}
|
||||
if (_overviewError != null && _overview == null) {
|
||||
return _buildListError(_overviewError!);
|
||||
}
|
||||
final overview = _overview;
|
||||
if (overview == null || !overview.success) {
|
||||
return _buildListMessage('扫描总排号后自动显示排产号上架情况');
|
||||
}
|
||||
|
||||
final scannedSet = _zongpaiNos.toSet();
|
||||
final scanned = <PaichaOverviewItem>[];
|
||||
final unscanned = <PaichaOverviewItem>[];
|
||||
for (final item in overview.items) {
|
||||
if (scannedSet.contains(item.zongpaiNo)) {
|
||||
scanned.add(item);
|
||||
} else {
|
||||
unscanned.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
final hasDivider = scanned.isNotEmpty && unscanned.isNotEmpty;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (_overviewError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
color: Colors.red.shade50,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -983,17 +1113,21 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${overview.paichaNo ?? "--"} 共 ${overview.totalCount} 个总排号',
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
_buildOverviewHeader(),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: overview.items.length,
|
||||
itemCount: scanned.length +
|
||||
(hasDivider ? 1 : 0) +
|
||||
unscanned.length,
|
||||
itemBuilder: (context, index) {
|
||||
return _buildOverviewRow(overview.items[index]);
|
||||
if (index < scanned.length) {
|
||||
return _buildListRow(scanned[index], isScanned: true);
|
||||
}
|
||||
if (index == scanned.length && hasDivider) {
|
||||
return _buildSectionDivider(unscanned.length);
|
||||
}
|
||||
final unscannedIdx =
|
||||
index - scanned.length - (hasDivider ? 1 : 0);
|
||||
return _buildListRow(unscanned[unscannedIdx], isScanned: false);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -1001,62 +1135,70 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverviewHeader() {
|
||||
Widget _buildSectionDivider(int count) {
|
||||
return Container(
|
||||
height: 32,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
height: 24,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
border: Border(bottom: BorderSide(color: Colors.grey.shade200)),
|
||||
),
|
||||
child: const Row(
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(flex: 20, child: Text('总排号', style: _headerStyle)),
|
||||
Expanded(flex: 20, child: Text('工令号', style: _headerStyle)),
|
||||
Expanded(
|
||||
flex: 14,
|
||||
child: Text('数量', textAlign: TextAlign.center, style: _headerStyle),
|
||||
const Expanded(child: Divider(height: 0)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
'以下 $count 项未操作',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
Expanded(
|
||||
flex: 26,
|
||||
child: Text('货位号', textAlign: TextAlign.right, style: _headerStyle),
|
||||
),
|
||||
),
|
||||
const Expanded(child: Divider(height: 0)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverviewRow(PaichaOverviewItem item) {
|
||||
final current = item.zongpaiNo == _overviewZongpaiNo;
|
||||
Widget _buildListRow(PaichaOverviewItem item, {required bool isScanned}) {
|
||||
final rowStyle = TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: current ? FontWeight.w700 : FontWeight.w500,
|
||||
fontWeight: isScanned ? FontWeight.w700 : FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
);
|
||||
|
||||
final Color barColor;
|
||||
final Color bgColor;
|
||||
if (isScanned) {
|
||||
final alreadyOnShelf = item.status == 'on_shelf' && !_isTransitTarget;
|
||||
if (alreadyOnShelf) {
|
||||
barColor = Colors.red;
|
||||
bgColor = Colors.red.shade50;
|
||||
} else {
|
||||
barColor = const Color(0xFF43A047);
|
||||
bgColor = const Color(0xFFF1F8E9);
|
||||
}
|
||||
} else {
|
||||
barColor = _barColor(item.status);
|
||||
bgColor = _rowBgColor(item.status);
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minHeight: 34),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
|
||||
constraints: const BoxConstraints(minHeight: 36),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: _overviewRowColor(item.status),
|
||||
color: bgColor,
|
||||
border: Border(
|
||||
left: BorderSide(
|
||||
color: current ? Colors.green : Colors.grey.shade300,
|
||||
width: current ? 2 : 1,
|
||||
),
|
||||
right: BorderSide(
|
||||
color: current ? Colors.green : Colors.grey.shade300,
|
||||
width: current ? 2 : 1,
|
||||
),
|
||||
bottom: BorderSide(
|
||||
color: current ? Colors.green : Colors.grey.shade300,
|
||||
width: current ? 2 : 1,
|
||||
),
|
||||
left: BorderSide(color: barColor, width: 3),
|
||||
bottom: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 20,
|
||||
flex: 22,
|
||||
child: Text(
|
||||
item.zongpaiNo,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -1064,7 +1206,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 20,
|
||||
flex: 18,
|
||||
child: Text(
|
||||
item.workOrderNo ?? '--',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@@ -1072,7 +1214,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 14,
|
||||
flex: 12,
|
||||
child: Text(
|
||||
item.quantity.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
@@ -1081,20 +1223,56 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 26,
|
||||
flex: 28,
|
||||
child: Text(
|
||||
item.locationCode ?? '—',
|
||||
item.locationCode ?? '\u2014',
|
||||
textAlign: TextAlign.right,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: rowStyle,
|
||||
),
|
||||
),
|
||||
if (isScanned && _isLocked) ...[
|
||||
const SizedBox(width: 6),
|
||||
SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.delete_outline, size: 16),
|
||||
color: Colors.red,
|
||||
onPressed: () => _removeZongpai(item.zongpaiNo),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverviewMessage(String text) {
|
||||
Color _barColor(String status) {
|
||||
switch (status) {
|
||||
case 'on_shelf':
|
||||
return const Color(0xFF2196F3);
|
||||
case 'transferred':
|
||||
return const Color(0xFFFF9800);
|
||||
default:
|
||||
return Colors.grey.shade400;
|
||||
}
|
||||
}
|
||||
|
||||
Color _rowBgColor(String status) {
|
||||
switch (status) {
|
||||
case 'on_shelf':
|
||||
return const Color(0xFFE3F2FD);
|
||||
case 'transferred':
|
||||
return const Color(0xFFFFF3E0);
|
||||
default:
|
||||
return const Color(0xFFF5F5F5);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildListMessage(String text) {
|
||||
return Center(
|
||||
child: Text(
|
||||
text,
|
||||
@@ -1104,7 +1282,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRetryMessage(String text) {
|
||||
Widget _buildListError(String text) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -1125,6 +1303,57 @@ class _RegistrationPageState extends State<RegistrationPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// === Bottom Bar ===
|
||||
|
||||
Widget _buildBottomBar(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(top: BorderSide(color: Colors.grey.shade300)),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 34,
|
||||
child: ElevatedButton(
|
||||
onPressed: _canSubmit ? _submit : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: registrationSubmitColor(
|
||||
canSubmit: _canSubmit,
|
||||
isTransitTarget: _isTransitTarget,
|
||||
primaryColor: colorScheme.primary,
|
||||
),
|
||||
foregroundColor:
|
||||
_canSubmit ? Colors.white : Colors.grey.shade600,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
child: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
registrationSubmitLabel(
|
||||
isTransitTarget: _isTransitTarget,
|
||||
isLocked: _isLocked,
|
||||
zongpaiCount: _zongpaiNos.length,
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const _headerStyle = TextStyle(fontSize: 12, fontWeight: FontWeight.w700);
|
||||
const _headerStyle = TextStyle(fontSize: 10, fontWeight: FontWeight.w700);
|
||||
|
||||
@@ -10,6 +10,7 @@ enum FeedbackEvent {
|
||||
duplicateError, // Duplicate (shelf) → alert loop + continuous vibrate
|
||||
networkError, // Network error → failure sound + short vibrate
|
||||
duplicateBoxNo, // Duplicate box no → failure sound + short vibrate
|
||||
alreadyCompleted, // Zongpai already boxed → beep + double vibrate
|
||||
modeSwitch, // Mode toggle → beep only
|
||||
paichanSwitch, // Paichan changed → beep + short vibrate
|
||||
}
|
||||
@@ -55,6 +56,10 @@ class FeedbackService {
|
||||
_soundService.playFailure();
|
||||
_vibrateShort();
|
||||
|
||||
case FeedbackEvent.alreadyCompleted:
|
||||
_soundService.playBeep();
|
||||
_vibrateDouble();
|
||||
|
||||
case FeedbackEvent.modeSwitch:
|
||||
_soundService.playBeep();
|
||||
|
||||
@@ -82,6 +87,11 @@ class FeedbackService {
|
||||
Vibration.vibrate(pattern: [0, 300, 100, 300, 100, 300]);
|
||||
}
|
||||
|
||||
/// Two short bursts to signal an already-completed item.
|
||||
void _vibrateDouble() {
|
||||
Vibration.vibrate(pattern: [0, 200, 100, 200]);
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _soundService.dispose();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user