1802 lines
51 KiB
HTML
1802 lines
51 KiB
HTML
<!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>
|