Files
docs/悦/PDA拣货/pda-picking.html
T

1802 lines
51 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scale=no">
<title>PDA集装箱拣货</title>
<style>
:root {
--primary: #1a73e8;
--primary-light: #e8f0fe;
--success: #0d9488;
--success-light: #d1fae5;
--danger: #dc2626;
--danger-light: #fee2e2;
--warning: #f59e0b;
--warning-light: #fef3c7;
--skip: #6b7280;
--skip-light: #f3f4f6;
--bg: #f5f7fa;
--card: #ffffff;
--text: #1f2937;
--text-secondary: #6b7280;
--border: #e5e7eb;
--shadow: 0 1px 3px rgba(0,0,0,0.08);
--radius: 12px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Noto Sans SC', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
max-width: 480px;
margin: 0 auto;
overflow-x: hidden;
}
/* 顶部导航 */
.header {
background: var(--primary);
color: white;
padding: 14px 16px;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 2px 8px rgba(26,115,232,0.3);
}
.header-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.header-title {
font-size: 18px;
font-weight: 600;
}
.header-badge {
background: rgba(255,255,255,0.2);
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
}
.header-sub {
margin-top: 6px;
font-size: 13px;
opacity: 0.9;
}
.header-sub span {
margin-right: 12px;
}
/* 扫码/选择区 */
.scan-section {
background: var(--card);
margin: 12px;
padding: 16px;
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.scan-row {
display: flex;
gap: 10px;
align-items: center;
}
.scan-input-wrap {
flex: 1;
position: relative;
}
.scan-input {
width: 100%;
padding: 10px 14px 10px 38px;
border: 2px solid var(--border);
border-radius: 8px;
font-size: 15px;
outline: none;
transition: border-color 0.2s;
}
.scan-input:focus { border-color: var(--primary); }
.scan-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 18px;
height: 18px;
opacity: 0.5;
}
.scan-btn {
width: 44px;
height: 44px;
background: var(--primary);
color: white;
border: none;
border-radius: 8px;
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.scan-btn:active { transform: scale(0.95); }
/* 箱号/订单信息卡 */
.info-cards {
margin: 12px;
display: flex;
gap: 10px;
}
.info-card {
flex: 1;
background: var(--card);
padding: 12px 14px;
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.info-card-label {
font-size: 11px;
color: var(--text-secondary);
margin-bottom: 4px;
}
.info-card-value {
font-size: 16px;
font-weight: 600;
}
.info-card-value.box-num { color: var(--primary); }
.info-card-value.order-num { color: var(--success); }
/* 进度条 */
.progress-section {
margin: 12px 12px 8px;
background: var(--card);
padding: 14px 16px;
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.progress-title {
font-size: 14px;
font-weight: 600;
}
.progress-stats {
font-size: 13px;
color: var(--text-secondary);
}
.progress-stats strong {
color: var(--success);
}
.progress-bar-bg {
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--success), #34d399);
border-radius: 4px;
transition: width 0.4s ease;
}
/* 拣货列表 */
.pick-list {
margin: 8px 12px;
}
.pick-item {
background: var(--card);
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-bottom: 10px;
padding: 14px 16px;
position: relative;
transition: all 0.3s;
overflow: hidden;
}
.pick-item.picked {
opacity: 0.85;
border-left: 4px solid var(--success);
}
.pick-item.skipped {
opacity: 0.6;
border-left: 4px solid var(--skip);
background: var(--skip-light);
}
.pick-item.active {
border-left: 4px solid var(--primary);
box-shadow: 0 2px 12px rgba(26,115,232,0.15);
}
.pick-item-row1 {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.pick-item-sku {
font-size: 15px;
font-weight: 600;
}
.pick-item-qty {
font-size: 14px;
color: var(--text-secondary);
}
.pick-item-qty strong {
color: var(--text);
font-weight: 700;
}
.pick-item-row2 {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 10px;
}
.pick-item-desc {
font-size: 13px;
color: var(--text-secondary);
flex: 1;
}
.pick-item-location {
font-size: 12px;
background: var(--primary-light);
color: var(--primary);
padding: 3px 8px;
border-radius: 6px;
font-weight: 500;
}
.pick-item-order {
font-size: 12px;
background: var(--success-light);
color: var(--success);
padding: 3px 8px;
border-radius: 6px;
font-weight: 500;
}
.pick-item-actions {
display: flex;
gap: 10px;
}
.pick-btn {
flex: 1;
padding: 9px 0;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.pick-btn:active { transform: scale(0.97); }
.pick-btn-confirm {
background: var(--success);
color: white;
}
.pick-btn-confirm:disabled {
background: var(--border);
color: var(--text-secondary);
}
.pick-btn-skip {
background: var(--skip-light);
color: var(--skip);
border: 1px solid var(--border);
}
.pick-btn-skip:disabled {
opacity: 0.4;
}
.pick-btn-partial {
background: #e8f0fe;
color: #4a7dff;
border: 1px solid #4a7dff;
}
.pick-btn-partial:disabled {
opacity: 0.4;
}
.pick-btn-undo {
background: var(--warning-light);
color: var(--warning);
border: 1px solid var(--warning);
flex: 0.6;
}
/* 已拣/已跳标签 */
.pick-status-tag {
position: absolute;
top: 0;
right: 0;
padding: 3px 10px;
font-size: 12px;
font-weight: 500;
border-radius: 0 var(--radius) 0 8px;
}
.tag-picked {
background: var(--success);
color: white;
}
.tag-skipped {
background: var(--skip);
color: white;
}
.tag-partial {
background: #4a7dff;
color: white;
}
/* 部分拣货弹窗 */
.partial-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
z-index: 200;
display: none;
align-items: flex-end;
justify-content: center;
}
.partial-modal-overlay.show { display: flex; }
.partial-modal {
background: var(--card);
width: 100%;
max-width: 480px;
border-radius: 16px 16px 0 0;
padding: 20px 16px 24px;
animation: slideUp 0.25s ease;
}
.partial-modal-title {
font-size: 17px;
font-weight: 600;
text-align: center;
margin-bottom: 16px;
}
.partial-info {
background: var(--bg);
border-radius: 10px;
padding: 12px;
margin-bottom: 16px;
}
.partial-info-sku {
font-size: 15px;
font-weight: 600;
margin-bottom: 4px;
}
.partial-info-desc {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 6px;
}
.partial-info-qty {
font-size: 14px;
color: var(--text-primary);
}
.partial-input-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.partial-input-row label {
font-size: 14px;
color: var(--text-secondary);
white-space: nowrap;
}
.partial-input {
flex: 1;
border: 2px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
font-size: 18px;
font-weight: 600;
text-align: center;
outline: none;
background: var(--card);
color: var(--text-primary);
-webkit-appearance: none;
}
.partial-input:focus {
border-color: #4a7dff;
}
.partial-input-unit {
font-size: 14px;
color: var(--text-secondary);
min-width: 30px;
}
.partial-quick-row {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.partial-quick-btn {
flex: 1;
padding: 8px 0;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--card);
font-size: 13px;
color: var(--text-primary);
cursor: pointer;
transition: all 0.2s;
}
.partial-quick-btn:active { transform: scale(0.96); }
.partial-modal-actions {
display: flex;
gap: 10px;
}
.partial-modal-cancel {
flex: 1;
padding: 12px 0;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--card);
font-size: 15px;
color: var(--text-secondary);
cursor: pointer;
}
.partial-modal-confirm {
flex: 1;
padding: 12px 0;
border: none;
border-radius: 10px;
background: #4a7dff;
font-size: 15px;
color: white;
font-weight: 500;
cursor: pointer;
}
/* 跳拣原因弹窗 */
.skip-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
z-index: 200;
display: none;
align-items: flex-end;
justify-content: center;
}
.skip-modal-overlay.show { display: flex; }
.skip-modal {
background: var(--card);
width: 100%;
max-width: 480px;
border-radius: 16px 16px 0 0;
padding: 20px 16px 24px;
animation: slideUp 0.25s ease;
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
.skip-modal-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 14px;
text-align: center;
}
.skip-reasons {
display: flex;
flex-direction: column;
gap: 10px;
}
.skip-reason-btn {
padding: 12px 16px;
border: 2px solid var(--border);
border-radius: 10px;
font-size: 14px;
cursor: pointer;
background: white;
text-align: left;
transition: all 0.2s;
}
.skip-reason-btn:active,
.skip-reason-btn.selected {
border-color: var(--danger);
background: var(--danger-light);
color: var(--danger);
}
.skip-modal-actions {
display: flex;
gap: 10px;
margin-top: 16px;
}
.skip-modal-cancel {
flex: 1;
padding: 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: white;
font-size: 14px;
cursor: pointer;
}
.skip-modal-confirm {
flex: 1;
padding: 12px;
border: none;
border-radius: 10px;
background: var(--danger);
color: white;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
/* 底部提交区 */
.submit-section {
position: sticky;
bottom: 0;
background: var(--card);
padding: 14px 16px;
border-top: 1px solid var(--border);
box-shadow: 0 -2px 8px rgba(0,0,0,0.06);
z-index: 50;
}
/* 集装箱总览 - 紧凑版 */
.container-summary {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.container-stat {
flex: 1;
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.container-stat:active { transform: scale(0.97); }
.container-stat-done {
background: var(--success-light);
border: 1px solid rgba(13,148,136,0.3);
}
.container-stat-pending {
background: var(--warning-light);
border: 1px solid rgba(245,158,11,0.3);
}
.container-stat-icon {
font-size: 14px;
line-height: 1;
}
.container-stat-num {
font-size: 15px;
font-weight: 700;
line-height: 1;
}
.container-stat-num.done-num { color: var(--success); }
.container-stat-num.pending-num { color: var(--warning); }
.container-stat-label {
font-size: 11px;
color: var(--text-secondary);
line-height: 1;
}
/* 已拣箱号列表弹窗 */
.boxlist-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
z-index: 250;
display: none;
align-items: flex-end;
justify-content: center;
}
.boxlist-modal-overlay.show { display: flex; }
.boxlist-modal {
background: var(--card);
width: 100%;
max-width: 480px;
border-radius: 16px 16px 0 0;
padding: 20px 16px 24px;
animation: slideUp 0.25s ease;
max-height: 70vh;
overflow-y: auto;
}
.boxlist-modal-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 14px;
text-align: center;
}
.boxlist-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
border-radius: 10px;
margin-bottom: 8px;
}
.boxlist-item-done {
background: var(--success-light);
border: 1px solid rgba(13,148,136,0.2);
}
.boxlist-item-pending {
background: var(--warning-light);
border: 1px solid rgba(245,158,11,0.2);
}
.boxlist-item-info {
font-size: 14px;
font-weight: 500;
}
.boxlist-item-info small {
color: var(--text-secondary);
font-weight: 400;
}
.boxlist-item-tag {
font-size: 12px;
padding: 3px 10px;
border-radius: 6px;
font-weight: 500;
}
.boxlist-item-tag-done {
background: var(--success);
color: white;
}
.boxlist-item-tag-pending {
background: var(--warning);
color: white;
}
.boxlist-close-btn {
width: 100%;
padding: 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: white;
font-size: 14px;
cursor: pointer;
margin-top: 8px;
}
.submit-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.submit-label {
font-size: 13px;
color: var(--text-secondary);
}
.submit-counts {
font-size: 13px;
}
.submit-counts .count-pick { color: var(--success); font-weight: 600; }
.submit-counts .count-skip { color: var(--skip); font-weight: 600; }
.submit-counts .count-pending { color: var(--warning); font-weight: 600; }
.submit-btn {
width: 100%;
padding: 14px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
background: linear-gradient(135deg, var(--primary), #4285f4);
color: white;
}
.submit-btn:disabled {
background: var(--border);
color: var(--text-secondary);
}
.submit-btn:active:not(:disabled) { transform: scale(0.98); }
/* 完成弹窗 */
.complete-modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.45);
z-index: 300;
display: none;
align-items: center;
justify-content: center;
}
.complete-modal-overlay.show { display: flex; }
.complete-modal {
background: var(--card);
width: 90%;
max-width: 380px;
border-radius: 20px;
padding: 32px 24px 24px;
text-align: center;
animation: popIn 0.3s ease;
}
@keyframes popIn {
from { transform: scale(0.85); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.complete-icon {
width: 64px;
height: 64px;
margin: 0 auto 16px;
background: var(--success-light);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
}
.complete-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 6px;
}
.complete-desc {
font-size: 14px;
color: var(--text-secondary);
margin-bottom: 20px;
line-height: 1.5;
}
.complete-stats {
display: flex;
gap: 20px;
justify-content: center;
margin-bottom: 20px;
}
.complete-stat {
text-align: center;
}
.complete-stat-num {
font-size: 24px;
font-weight: 700;
}
.complete-stat-num.green { color: var(--success); }
.complete-stat-num.gray { color: var(--skip); }
.complete-stat-num.red { color: var(--danger); }
.complete-stat-label {
font-size: 12px;
color: var(--text-secondary);
margin-top: 2px;
}
.complete-btns {
display: flex;
gap: 10px;
}
.complete-btn-back {
flex: 1;
padding: 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: white;
font-size: 14px;
cursor: pointer;
}
.complete-btn-next {
flex: 1.5;
padding: 12px;
border: none;
border-radius: 10px;
background: var(--primary);
color: white;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
/* 切换箱号下拉 */
.box-select-wrap {
margin: 12px;
position: relative;
}
.box-select-btn {
width: 100%;
padding: 12px 14px;
background: var(--card);
border: 2px solid var(--primary);
border-radius: var(--radius);
font-size: 14px;
font-weight: 500;
color: var(--primary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: var(--shadow);
}
.box-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
z-index: 60;
display: none;
max-height: 240px;
overflow-y: auto;
}
.box-dropdown.show { display: block; }
.box-option {
padding: 12px 14px;
font-size: 14px;
cursor: pointer;
border-bottom: 1px solid var(--border);
}
.box-option:last-child { border-bottom: none; }
.box-option:hover,
.box-option.active {
background: var(--primary-light);
color: var(--primary);
font-weight: 500;
}
/* Toast提示 */
.toast {
position: fixed;
top: 70px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: var(--text);
color: white;
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
z-index: 400;
opacity: 0;
transition: all 0.3s;
pointer-events: none;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
.toast.success { background: var(--success); }
.toast.warning { background: var(--warning); }
.toast.error { background: var(--danger); }
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: var(--text-secondary);
}
.empty-state-icon {
font-size: 48px;
margin-bottom: 12px;
opacity: 0.4;
}
.empty-state-text {
font-size: 15px;
}
/* ========== 登录页 ========== */
.login-page {
display: flex;
flex-direction: column;
justify-content: center;
min-height: 100vh;
padding: 32px 24px;
background: linear-gradient(160deg, #1a73e8 0%, #0d47a1 100%);
}
.login-page.hidden { display: none; }
.main-page.hidden { display: none; }
.login-logo {
text-align: center;
margin-bottom: 36px;
}
.login-logo-icon {
width: 72px;
height: 72px;
background: rgba(255,255,255,0.15);
border-radius: 20px;
margin: 0 auto 14px;
display: flex;
align-items: center;
justify-content: center;
}
.login-logo-icon svg {
width: 40px;
height: 40px;
}
.login-logo-title {
color: white;
font-size: 22px;
font-weight: 700;
}
.login-logo-sub {
color: rgba(255,255,255,0.7);
font-size: 13px;
margin-top: 4px;
}
.login-card {
background: var(--card);
border-radius: 16px;
padding: 24px 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.18);
}
.login-field {
margin-bottom: 16px;
}
.login-field-label {
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 6px;
}
.login-field-input {
width: 100%;
padding: 11px 14px;
border: 2px solid var(--border);
border-radius: 10px;
font-size: 15px;
outline: none;
transition: border-color 0.2s;
}
.login-field-input:focus { border-color: var(--primary); }
.login-field-select {
width: 100%;
padding: 11px 14px;
border: 2px solid var(--border);
border-radius: 10px;
font-size: 15px;
outline: none;
background: white;
cursor: pointer;
transition: border-color 0.2s;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%236b7280' stroke-width='1.5' fill='none'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 14px center;
}
.login-field-select:focus { border-color: var(--primary); }
.login-error {
font-size: 13px;
color: var(--danger);
margin-bottom: 12px;
display: none;
}
.login-error.show { display: block; }
.login-btn {
width: 100%;
padding: 13px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
background: linear-gradient(135deg, var(--primary), #4285f4);
color: white;
margin-top: 6px;
transition: all 0.2s;
}
.login-btn:active { transform: scale(0.97); }
.login-accounts {
margin-top: 18px;
padding-top: 16px;
border-top: 1px solid var(--border);
}
.login-accounts-title {
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 10px;
text-align: center;
}
.login-quick-btns {
display: flex;
flex-direction: column;
gap: 8px;
}
.login-quick-btn {
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: white;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
text-align: left;
}
.login-quick-btn:active { transform: scale(0.97); border-color: var(--primary); background: var(--primary-light); }
.login-quick-btn-name { font-weight: 500; }
.login-quick-btn-role { font-size: 12px; color: var(--text-secondary); }
/* 退出按钮 */
.logout-btn {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 4px 8px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
line-height: 1;
}
.logout-btn:active { transform: scale(0.95); }
</style>
</head>
<body>
<!-- Toast -->
<div class="toast" id="toast"></div>
<!-- ===== 登录页 ===== -->
<div class="login-page" id="loginPage">
<div class="login-logo">
<div class="login-logo-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<path d="M3 9h18M9 3v18"/>
</svg>
</div>
<div class="login-logo-title">柏悦物流 · PDA拣货</div>
<div class="login-logo-sub">集装箱拣货作业系统</div>
</div>
<div class="login-card">
<div class="login-field">
<div class="login-field-label">工号 / 用户名</div>
<input class="login-field-input" id="loginUser" placeholder="请输入工号" autocomplete="off">
</div>
<div class="login-field">
<div class="login-field-label">密码</div>
<input class="login-field-input" id="loginPass" type="password" placeholder="请输入密码">
</div>
<div class="login-field">
<div class="login-field-label">作业仓库</div>
<select class="login-field-select" id="loginWarehouse">
<option value="仓库 A">仓库 A</option>
<option value="仓库 B">仓库 B</option>
<option value="仓库 C">仓库 C</option>
</select>
</div>
<div class="login-error" id="loginError">工号或密码错误</div>
<button class="login-btn" onclick="handleLogin()">登录</button>
<div class="login-accounts">
<div class="login-accounts-title">快捷登录(演示账号)</div>
<div class="login-quick-btns">
<button class="login-quick-btn" onclick="quickLogin('W001','张伟','拣货员')">
<span class="login-quick-btn-name">W001 张伟</span>
<span class="login-quick-btn-role"> · 拣货员</span>
</button>
<button class="login-quick-btn" onclick="quickLogin('W002','李敏','拣货组长')">
<span class="login-quick-btn-name">W002 李敏</span>
<span class="login-quick-btn-role"> · 拣货组长</span>
</button>
<button class="login-quick-btn" onclick="quickLogin('W003','王磊','仓管员')">
<span class="login-quick-btn-name">W003 王磊</span>
<span class="login-quick-btn-role"> · 仓管员</span>
</button>
</div>
</div>
</div>
</div>
<!-- ===== 拣货主页面 ===== -->
<div class="main-page hidden" id="mainPage">
<div class="header">
<div class="header-top">
<div class="header-title">集装箱拣货</div>
<div style="display:flex;align-items:center;gap:8px;">
<div class="header-badge" id="headerBadge">仓库 A</div>
<button class="logout-btn" onclick="handleLogout()">退出</button>
</div>
</div>
<div class="header-sub">
<span id="headerBox">箱号:CBX-20260709-001</span>
<span id="headerOrder">订单:SO-78542</span>
<span id="headerUser" style="margin-left:4px;color:rgba(255,255,255,0.7)"></span>
</div>
</div>
<!-- 箱号选择 -->
<div class="box-select-wrap">
<button class="box-select-btn" id="boxSelectBtn" onclick="toggleBoxDropdown()">
<span id="boxSelectLabel">CBX-20260709-001 · 订单 SO-78542</span>
<span>▼</span>
</button>
<div class="box-dropdown" id="boxDropdown"></div>
</div>
<!-- 扫码区 -->
<div class="scan-section">
<div class="scan-row">
<div class="scan-input-wrap">
<svg class="scan-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 7V5a2 2 0 012-2h2M17 3h2a2 2 0 012 2v2M21 17v2a2 2 0 01-2 2h-2M7 21H5a2 2 0 01-2-2v-2"/>
<line x1="7" y1="12" x2="17" y2="12"/>
</svg>
<input class="scan-input" id="scanInput" placeholder="扫描或输入SKU码" autocomplete="off">
</div>
<button class="scan-btn" onclick="handleScan()">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
</svg>
</button>
</div>
</div>
<!-- 信息卡 -->
<div class="info-cards">
<div class="info-card">
<div class="info-card-label">装箱箱号</div>
<div class="info-card-value box-num" id="infoBoxNum">CBX-20260709-001</div>
</div>
<div class="info-card">
<div class="info-card-label">关联订单</div>
<div class="info-card-value order-num" id="infoOrderNum">SO-78542</div>
</div>
</div>
<!-- 进度 -->
<div class="progress-section">
<div class="progress-header">
<div class="progress-title">拣货进度</div>
<div class="progress-stats" id="progressStats">已拣 <strong>0</strong> / 8 件</div>
</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill" id="progressBar" style="width: 0%"></div>
</div>
</div>
<!-- 拣货列表 -->
<div class="pick-list" id="pickList"></div>
<!-- 底部提交 -->
<div class="submit-section">
<div class="container-summary">
<div class="container-stat container-stat-done" id="containerStatDone" onclick="showBoxList('done')">
<span class="container-stat-icon">✅</span>
<span class="container-stat-num done-num" id="containerDoneNum">0</span>
<span class="container-stat-label">已拣集装箱</span>
</div>
<div class="container-stat container-stat-pending" id="containerStatPending" onclick="showBoxList('pending')">
<span class="container-stat-icon">⏳</span>
<span class="container-stat-num pending-num" id="containerPendingNum">3</span>
<span class="container-stat-label">待拣集装箱</span>
</div>
</div>
<div class="submit-info">
<div class="submit-label">拣货汇总</div>
<div class="submit-counts" id="submitCounts">
已拣 <span class="count-pick">0</span> · 跳拣 <span class="count-skip">0</span> · 待拣 <span class="count-pending">8</span>
</div>
</div>
<button class="submit-btn" id="submitBtn" disabled onclick="handleSubmit()">提交拣货完成</button>
</div>
<!-- 集装箱列表弹窗 -->
<div class="boxlist-modal-overlay" id="boxlistModal">
<div class="boxlist-modal">
<div class="boxlist-modal-title" id="boxlistModalTitle">集装箱列表</div>
<div id="boxlistContent"></div>
<button class="boxlist-close-btn" onclick="closeBoxList()">关闭</button>
</div>
</div>
<!-- 跳拣原因弹窗 -->
<div class="skip-modal-overlay" id="skipModal">
<div class="skip-modal">
<div class="skip-modal-title">选择跳拣原因</div>
<div class="skip-reasons" id="skipReasons"></div>
<div class="skip-modal-actions">
<button class="skip-modal-cancel" onclick="closeSkipModal()">取消</button>
<button class="skip-modal-confirm" id="skipConfirmBtn" onclick="confirmSkip()">确认跳拣</button>
</div>
</div>
</div>
<!-- 部分拣货弹窗 -->
<div class="partial-modal-overlay" id="partialModal">
<div class="partial-modal">
<div class="partial-modal-title">部分拣货</div>
<div class="partial-info" id="partialInfo"></div>
<div class="partial-input-row">
<label>实际拣出</label>
<input type="number" class="partial-input" id="partialInput" min="0" />
<span class="partial-input-unit" id="partialUnit">个</span>
</div>
<div class="partial-quick-row" id="partialQuick"></div>
<div class="partial-modal-actions">
<button class="partial-modal-cancel" onclick="closePartialModal()">取消</button>
<button class="partial-modal-confirm" onclick="confirmPartial()">确认</button>
</div>
</div>
</div>
<!-- 完成弹窗 -->
<div class="complete-modal-overlay" id="completeModal">
<div class="complete-modal">
<div class="complete-icon">✓</div>
<div class="complete-title">拣货完成</div>
<div class="complete-desc" id="completeDesc"></div>
<div class="complete-stats" id="completeStats"></div>
<div class="complete-btns">
<button class="complete-btn-back" onclick="closeCompleteModal()">返回查看</button>
<button class="complete-btn-next" onclick="nextBox()">下一箱</button>
</div>
</div>
</div>
</div><!-- end main-page -->
<script>
// ===== 登录逻辑 =====
const DEMO_ACCOUNTS = {
'W001': { pass: '123456', name: '张伟', role: '拣货员' },
'W002': { pass: '123456', name: '李敏', role: '拣货组长' },
'W003': { pass: '123456', name: '王磊', role: '仓管员' },
};
let currentUser = null;
function handleLogin() {
const userId = document.getElementById('loginUser').value.trim();
const password = document.getElementById('loginPass').value;
const warehouse = document.getElementById('loginWarehouse').value;
if (!userId) { showLoginError('请输入工号'); return; }
if (!password) { showLoginError('请输入密码'); return; }
// 检查账号
const account = DEMO_ACCOUNTS[userId];
if (!account || account.pass !== password) {
showLoginError('工号或密码错误');
return;
}
currentUser = { id: userId, name: account.name, role: account.role, warehouse: warehouse };
enterMainPage();
}
function quickLogin(id, name, role) {
currentUser = { id: id, name: name, role: role, warehouse: document.getElementById('loginWarehouse').value };
enterMainPage();
}
function enterMainPage() {
document.getElementById('loginPage').classList.add('hidden');
document.getElementById('mainPage').classList.remove('hidden');
document.getElementById('headerBadge').textContent = currentUser.warehouse;
document.getElementById('headerUser').textContent = currentUser.name + ' · ' + currentUser.role;
hideLoginError();
initStates();
render();
showToast(currentUser.name + ',欢迎登录', 'success');
}
function handleLogout() {
currentUser = null;
document.getElementById('loginPage').classList.remove('hidden');
document.getElementById('mainPage').classList.add('hidden');
// 清空表单
document.getElementById('loginUser').value = '';
document.getElementById('loginPass').value = '';
hideLoginError();
// 重置拣货状态
currentBoxIdx = 0;
boxCompleted = {};
BOXES.forEach(b => {
b.items.forEach(it => { it.skipReason = ''; it._finalState = ''; it._partialQty = 0; });
});
partialQty = {};
}
function showLoginError(msg) {
const el = document.getElementById('loginError');
el.textContent = msg;
el.classList.add('show');
}
function hideLoginError() {
document.getElementById('loginError').classList.remove('show');
}
// ===== 数据 =====
const BOXES = [
{
boxNo: 'CBX-20260709-001',
orderNo: 'SO-78542',
items: [
{ id: 1, sku: 'EL-38201', desc: '电子元件·电容 100μF/25V', qty: 120, unit: '个', location: 'A-03-12' },
{ id: 2, sku: 'EL-38202', desc: '电子元件·电阻 10kΩ', qty: 200, unit: '个', location: 'A-03-15' },
{ id: 3, sku: 'ME-55010', desc: '机械配件·M8不锈钢螺栓', qty: 50, unit: '套', location: 'B-01-08' },
{ id: 4, sku: 'PL-22001', desc: '塑料件·PP密封盖 50mm', qty: 80, unit: '个', location: 'C-02-04' },
{ id: 5, sku: 'EL-38203', desc: '电子元件·LED灯珠 白光', qty: 300, unit: '个', location: 'A-04-01' },
{ id: 6, sku: 'ME-55011', desc: '机械配件·弹簧垫圈 M10', qty: 60, unit: '个', location: 'B-01-10' },
{ id: 7, sku: 'PA-11001', desc: '包装材料·瓦楞纸箱 40×30×25', qty: 15, unit: '箱', location: 'D-01-02' },
{ id: 8, sku: 'EL-38204', desc: '电子元件·PCB基板 FR4', qty: 30, unit: '片', location: 'A-05-06' },
]
},
{
boxNo: 'CBX-20260709-002',
orderNo: 'SO-78543',
items: [
{ id: 9, sku: 'ME-55020', desc: '机械配件·铜质接头 DN15', qty: 40, unit: '个', location: 'B-02-03' },
{ id: 10, sku: 'EL-38205', desc: '电子元件·继电器 5V DC', qty: 25, unit: '个', location: 'A-03-20' },
{ id: 11, sku: 'PL-22002', desc: '塑料件·尼龙法兰 80mm', qty: 100, unit: '个', location: 'C-03-01' },
{ id: 12, sku: 'SA-33001', desc: '安防配件·门磁开关', qty: 20, unit: '套', location: 'E-01-05' },
]
},
{
boxNo: 'CBX-20260709-003',
orderNo: 'SO-78544',
items: [
{ id: 13, sku: 'EL-38206', desc: '电子元件·温控芯片 NTC', qty: 50, unit: '个', location: 'A-06-02' },
{ id: 14, sku: 'ME-55030', desc: '机械配件·铝合金支架 L型', qty: 10, unit: '件', location: 'B-03-07' },
{ id: 15, sku: 'PA-11002', desc: '包装材料·气泡膜 60cm宽', qty: 8, unit: '卷', location: 'D-01-05' },
{ id: 16, sku: 'EL-38207', desc: '电子元件·变压器 220V→12V', qty: 15, unit: '个', location: 'A-07-01' },
{ id: 17, sku: 'PL-22003', desc: '塑料件·PVC管件 90°弯头', qty: 60, unit: '个', location: 'C-04-03' },
]
},
];
const SKIP_REASONS = [
'货柜已装满',
'货物破损/异常,需质检复核',
'库位找不到货',
'货物与订单规格不符',
'其他原因',
];
// ===== 状态 =====
let currentBoxIdx = 0;
let itemStates = {}; // { itemId: 'pending'|'picked'|'skipped'|'partial' }
let partialQty = {}; // { itemId: number } 部分拣货的实际数量
let boxCompleted = {}; // { boxIdx: true } 标记已提交完成的箱
let skipReason = ''; // 当前跳拣选择的原因
let skipTargetId = null; // 当前跳拣目标
let partialTargetId = null; // 当前部分拣货目标
function initStates() {
itemStates = {};
partialQty = {};
BOXES[currentBoxIdx].items.forEach(it => {
itemStates[it.id] = 'pending';
});
}
// ===== 渲染 =====
function render() {
const box = BOXES[currentBoxIdx];
// 头部信息
document.getElementById('headerBox').textContent = '箱号:' + box.boxNo;
document.getElementById('headerOrder').textContent = '订单:' + box.orderNo;
document.getElementById('boxSelectLabel').textContent = box.boxNo + ' · 订单 ' + box.orderNo;
document.getElementById('infoBoxNum').textContent = box.boxNo;
document.getElementById('infoOrderNum').textContent = box.orderNo;
renderDropdown();
renderList();
updateProgress();
updateSubmit();
updateContainerStats();
}
function renderDropdown() {
const dd = document.getElementById('boxDropdown');
dd.innerHTML = BOXES.map((b, i) => {
const cls = i === currentBoxIdx ? 'active' : '';
return `<div class="box-option ${cls}" onclick="selectBox(${i})">${b.boxNo} · ${b.orderNo}</div>`;
}).join('');
}
function renderList() {
const box = BOXES[currentBoxIdx];
const list = document.getElementById('pickList');
const pendingItems = box.items.filter(it => itemStates[it.id] === 'pending');
const processedItems = box.items.filter(it => itemStates[it.id] !== 'pending');
if (pendingItems.length === 0 && processedItems.length === 0) {
list.innerHTML = '<div class="empty-state"><div class="empty-state-icon">📦</div><div class="empty-state-text">该箱号暂无拣货任务</div></div>';
return;
}
let html = '';
// 待拣项先显示
pendingItems.forEach(it => {
html += `
<div class="pick-item active" data-id="${it.id}">
<div class="pick-item-row1">
<div class="pick-item-sku">${it.sku}</div>
<div class="pick-item-qty">需拣 <strong>${it.qty}</strong> ${it.unit}</div>
</div>
<div class="pick-item-row2">
<div class="pick-item-desc">${it.desc}</div>
<div class="pick-item-location">${it.location}</div>
</div>
<div class="pick-item-actions">
<button class="pick-btn pick-btn-confirm" onclick="pickItem(${it.id})">确认拣出</button>
<button class="pick-btn pick-btn-partial" onclick="openPartialModal(${it.id})">部分拣货</button>
<button class="pick-btn pick-btn-skip" onclick="openSkipModal(${it.id})">跳拣</button>
</div>
</div>`;
});
// 已处理项后显示
processedItems.forEach(it => {
const st = itemStates[it.id];
let tagHtml;
if (st === 'picked') {
tagHtml = '<div class="pick-status-tag tag-picked">已拣 ✓</div>';
} else if (st === 'partial') {
tagHtml = `<div class="pick-status-tag tag-partial">部分拣 ${partialQty[it.id] || 0}/${it.qty}</div>`;
} else {
tagHtml = '<div class="pick-status-tag tag-skipped">已跳拣</div>';
}
const undoHtml = `<button class="pick-btn pick-btn-undo" onclick="undoItem(${it.id})">撤销</button>`;
const qtyDisplay = st === 'partial'
? `需拣 <strong>${it.qty}</strong> ${it.unit} · 实拣 <strong style="color:#4a7dff">${partialQty[it.id] || 0}</strong>`
: `需拣 <strong>${it.qty}</strong> ${it.unit}`;
html += `
<div class="pick-item ${st}" data-id="${it.id}">
${tagHtml}
<div class="pick-item-row1">
<div class="pick-item-sku">${it.sku}</div>
<div class="pick-item-qty">${qtyDisplay}</div>
</div>
<div class="pick-item-row2">
<div class="pick-item-desc">${it.desc}${st === 'skipped' ? ' · ' + (it.skipReason || '') : ''}</div>
<div class="pick-item-location">${it.location}</div>
</div>
<div class="pick-item-actions">
${undoHtml}
</div>
</div>`;
});
list.innerHTML = html;
}
function updateProgress() {
const box = BOXES[currentBoxIdx];
const total = box.items.length;
const picked = box.items.filter(it => itemStates[it.id] === 'picked').length;
const partial = box.items.filter(it => itemStates[it.id] === 'partial').length;
const pct = total === 0 ? 0 : Math.round((picked + partial + box.items.filter(it => itemStates[it.id] === 'skipped').length) / total * 100);
const progressText = partial > 0
? `已拣 <strong>${picked}</strong> · 部分拣 <strong>${partial}</strong> / ${total} 件`
: `已拣 <strong>${picked}</strong> / ${total} 件`;
document.getElementById('progressStats').innerHTML = progressText;
document.getElementById('progressBar').style.width = pct + '%';
}
function updateSubmit() {
const box = BOXES[currentBoxIdx];
const total = box.items.length;
const picked = box.items.filter(it => itemStates[it.id] === 'picked').length;
const partial = box.items.filter(it => itemStates[it.id] === 'partial').length;
const skipped = box.items.filter(it => itemStates[it.id] === 'skipped').length;
const pending = total - picked - partial - skipped;
let countHtml = `已拣 <span class="count-pick">${picked}</span>`;
if (partial > 0) countHtml += ` · 部分拣 <span style="color:#4a7dff;font-weight:600">${partial}</span>`;
countHtml += ` · 跳拣 <span class="count-skip">${skipped}</span> · 待拣 <span class="count-pending">${pending}</span>`;
document.getElementById('submitCounts').innerHTML = countHtml;
const btn = document.getElementById('submitBtn');
// 至少拣了一件才能提交,或者所有都已处理
if (pending === 0 && total > 0) {
btn.disabled = false;
btn.textContent = '提交拣货完成';
} else if (picked > 0 || partial > 0) {
btn.disabled = false;
btn.textContent = `提交拣货完成(${pending}件待拣将视为未拣)`;
} else {
btn.disabled = true;
btn.textContent = '提交拣货完成';
}
}
// ===== 操作 =====
function pickItem(id) {
itemStates[id] = 'picked';
showToast('已确认拣出', 'success');
render();
}
function openSkipModal(id) {
skipTargetId = id;
skipReason = '';
const modal = document.getElementById('skipModal');
const reasonsDiv = document.getElementById('skipReasons');
reasonsDiv.innerHTML = SKIP_REASONS.map((r, i) =>
`<button class="skip-reason-btn" onclick="selectSkipReason(${i}, this)">${r}</button>`
).join('');
modal.classList.add('show');
}
function selectSkipReason(idx, el) {
skipReason = SKIP_REASONS[idx];
document.querySelectorAll('.skip-reason-btn').forEach(b => b.classList.remove('selected'));
el.classList.add('selected');
}
function closeSkipModal() {
document.getElementById('skipModal').classList.remove('show');
skipTargetId = null;
skipReason = '';
}
function confirmSkip() {
if (!skipReason) {
showToast('请先选择跳拣原因', 'warning');
return;
}
itemStates[skipTargetId] = 'skipped';
// 保存原因
const item = BOXES[currentBoxIdx].items.find(it => it.id === skipTargetId);
if (item) item.skipReason = skipReason;
closeSkipModal();
showToast('已标记跳拣:' + skipReason, 'warning');
render();
}
function undoItem(id) {
const item = BOXES[currentBoxIdx].items.find(it => it.id === id);
if (item) item.skipReason = '';
itemStates[id] = 'pending';
delete partialQty[id];
showToast('已撤销操作', 'success');
render();
}
// ===== 部分拣货 =====
function openPartialModal(id) {
partialTargetId = id;
const item = BOXES[currentBoxIdx].items.find(it => it.id === id);
if (!item) return;
document.getElementById('partialInfo').innerHTML = `
<div class="partial-info-sku">${item.sku}</div>
<div class="partial-info-desc">${item.desc}</div>
<div class="partial-info-qty">需拣总量:<strong>${item.qty}</strong> ${item.unit}</div>
`;
document.getElementById('partialUnit').textContent = item.unit;
// 快捷输入按钮:1/4、1/2、3/4、全量
const q = item.qty;
const quickDiv = document.getElementById('partialQuick');
const quickVals = [
{ label: '1/4', val: Math.floor(q * 0.25) },
{ label: '1/2', val: Math.floor(q * 0.5) },
{ label: '3/4', val: Math.floor(q * 0.75) },
{ label: '全部', val: q },
];
quickDiv.innerHTML = quickVals.map(v =>
`<button class="partial-quick-btn" onclick="setPartialInput(${v.val})">${v.label} (${v.val})</button>`
).join('');
// 默认填入0
const input = document.getElementById('partialInput');
input.value = '';
input.max = item.qty;
document.getElementById('partialModal').classList.add('show');
setTimeout(() => input.focus(), 300);
}
function setPartialInput(val) {
document.getElementById('partialInput').value = val;
}
function closePartialModal() {
document.getElementById('partialModal').classList.remove('show');
partialTargetId = null;
}
function confirmPartial() {
const item = BOXES[currentBoxIdx].items.find(it => it.id === partialTargetId);
if (!item) return;
const val = parseInt(document.getElementById('partialInput').value);
if (isNaN(val) || val <= 0) {
showToast('请输入大于0的数量', 'warning');
return;
}
if (val >= item.qty) {
showToast('数量已达需拣总量,请直接点击"确认拣出"', 'warning');
return;
}
itemStates[partialTargetId] = 'partial';
partialQty[partialTargetId] = val;
closePartialModal();
showToast(`已部分拣货:${val}/${item.qty} ${item.unit}`, 'success');
render();
}
function handleScan() {
const val = document.getElementById('scanInput').value.trim();
if (!val) { showToast('请输入或扫描SKU码', 'warning'); return; }
const box = BOXES[currentBoxIdx];
const matched = box.items.find(it => it.sku === val && itemStates[it.id] === 'pending');
if (matched) {
pickItem(matched.id);
document.getElementById('scanInput').value = '';
} else {
showToast('未找到匹配的待拣SKU:' + val, 'error');
}
}
function updateContainerStats() {
const doneCount = Object.keys(boxCompleted).length;
const totalCount = BOXES.length;
const pendingCount = totalCount - doneCount;
document.getElementById('containerDoneNum').textContent = doneCount;
document.getElementById('containerPendingNum').textContent = pendingCount;
}
function showBoxList(type) {
const modal = document.getElementById('boxlistModal');
const title = document.getElementById('boxlistModalTitle');
const content = document.getElementById('boxlistContent');
if (type === 'done') {
title.textContent = '已拣集装箱';
const doneBoxes = BOXES.filter((b, i) => boxCompleted[i]);
if (doneBoxes.length === 0) {
content.innerHTML = '<div style="text-align:center;padding:24px;color:var(--text-secondary)">暂无已拣完成的集装箱</div>';
} else {
content.innerHTML = doneBoxes.map(b => {
const idx = BOXES.indexOf(b);
const pickedCount = b.items.filter(it => it._finalState === 'picked').length;
const partialCount = b.items.filter(it => it._finalState === 'partial').length;
const partialInfo = partialCount > 0 ? ` · 部分拣${partialCount}件` : '';
return `<div class="boxlist-item boxlist-item-done">
<div class="boxlist-item-info">${b.boxNo}<br><small>${b.orderNo} · 拣出${pickedCount}件${partialInfo}</small></div>
<div class="boxlist-item-tag boxlist-item-tag-done">已完成 ✓</div>
</div>`;
}).join('');
}
} else {
title.textContent = '待拣集装箱';
const pendingBoxes = BOXES.filter((b, i) => !boxCompleted[i]);
if (pendingBoxes.length === 0) {
content.innerHTML = '<div style="text-align:center;padding:24px;color:var(--text-secondary)">所有集装箱已拣完成 🎉</div>';
} else {
content.innerHTML = pendingBoxes.map(b => {
const idx = BOXES.indexOf(b);
const isCurrent = idx === currentBoxIdx;
return `<div class="boxlist-item boxlist-item-pending" ${!isCurrent ? `onclick="selectBoxFromList(${idx})"` : ''}>
<div class="boxlist-item-info">${b.boxNo}${isCurrent ? ' (当前)' : ''}<br><small>${b.orderNo} · ${b.items.length}件待拣</small></div>
<div class="boxlist-item-tag boxlist-item-tag-pending">${isCurrent ? '拣货中' : '待拣'}</div>
</div>`;
}).join('');
}
}
modal.classList.add('show');
}
function selectBoxFromList(idx) {
closeBoxList();
currentBoxIdx = idx;
initStates();
render();
showToast('切换至 ' + BOXES[idx].boxNo, 'success');
}
function closeBoxList() {
document.getElementById('boxlistModal').classList.remove('show');
}
function handleSubmit() {
const box = BOXES[currentBoxIdx];
const total = box.items.length;
const picked = box.items.filter(it => itemStates[it.id] === 'picked').length;
const partial = box.items.filter(it => itemStates[it.id] === 'partial').length;
const skipped = box.items.filter(it => itemStates[it.id] === 'skipped').length;
const pending = total - picked - partial - skipped;
if (pending > 0) {
// 未处理的自动标记为未拣
box.items.forEach(it => {
if (itemStates[it.id] === 'pending') {
itemStates[it.id] = 'skipped';
it.skipReason = '未拣出(提交时自动标记)';
}
});
}
// 记录最终状态并标记箱已完成
box.items.forEach(it => {
it._finalState = itemStates[it.id];
if (itemStates[it.id] === 'partial') it._partialQty = partialQty[it.id] || 0;
});
boxCompleted[currentBoxIdx] = true;
// 部分拣货的缺货数量
const partialShort = box.items
.filter(it => it._finalState === 'partial')
.reduce((sum, it) => sum + (it.qty - (partialQty[it.id] || 0)), 0);
const newSkipped = box.items.filter(it => it._finalState === 'skipped').length;
document.getElementById('completeDesc').textContent =
`箱号 ${box.boxNo} · 订单 ${box.orderNo}`;
let statsHtml = `
<div class="complete-stat"><div class="complete-stat-num green">${picked}</div><div class="complete-stat-label">已拣出</div></div>
<div class="complete-stat"><div class="complete-stat-num" style="color:#4a7dff">${partial}</div><div class="complete-stat-label">部分拣货</div></div>
<div class="complete-stat"><div class="complete-stat-num gray">${newSkipped}</div><div class="complete-stat-label">跳拣/未拣</div></div>
`;
if (pending > 0) {
statsHtml += `<div class="complete-stat"><div class="complete-stat-num red">${pending}</div><div class="complete-stat-label">自动未拣</div></div>`;
}
document.getElementById('completeStats').innerHTML = statsHtml;
document.getElementById('completeModal').classList.add('show');
updateContainerStats();
}
function closeCompleteModal() {
document.getElementById('completeModal').classList.remove('show');
render();
}
function nextBox() {
document.getElementById('completeModal').classList.remove('show');
if (currentBoxIdx < BOXES.length - 1) {
currentBoxIdx++;
initStates();
render();
showToast('切换至下一箱', 'success');
} else {
showToast('已是最后一个箱号', 'warning');
render();
}
}
function selectBox(idx) {
currentBoxIdx = idx;
initStates();
document.getElementById('boxDropdown').classList.remove('show');
render();
}
function toggleBoxDropdown() {
const dd = document.getElementById('boxDropdown');
dd.classList.toggle('show');
}
// ===== Toast =====
let toastTimer = null;
function showToast(msg, type = '') {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = 'toast show ' + type;
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { t.className = 'toast'; }, 2200);
}
// ===== 键盘回车触发扫码 =====
document.getElementById('scanInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter') handleScan();
});
// ===== 登录页回车触发登录 =====
document.getElementById('loginUser').addEventListener('keydown', function(e) {
if (e.key === 'Enter') document.getElementById('loginPass').focus();
});
document.getElementById('loginPass').addEventListener('keydown', function(e) {
if (e.key === 'Enter') handleLogin();
});
// ===== 初始化(显示登录页)=====
// 不在这里 initStates + render,登录后才初始化
</script>
</body>
</html>