Files
docs/悦/PDA拣货/pda-picking.html
T
2026-08-05 09:49:02 +08:00

2185 lines
80 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-scalable=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); }
/* ========== PDA 工作台与业务导航 ========== */
.workbench-page.hidden,
.operation-page.hidden,
.profile-page.hidden { display: none; }
.app-shell { min-height: 100vh; background: var(--bg); padding-bottom: 28px; }
.app-topbar {
background: linear-gradient(135deg, #155eef 0%, #2878ff 100%);
color: white;
padding: 16px 16px 22px;
border-radius: 0 0 22px 22px;
box-shadow: 0 8px 24px rgba(21,94,239,.22);
}
.app-topbar-row { display:flex; align-items:center; justify-content:space-between; gap:12px; }
.app-brand { display:flex; align-items:center; gap:10px; }
.app-brand-mark { width:38px; height:38px; border-radius:12px; background:rgba(255,255,255,.18); display:flex; align-items:center; justify-content:center; font-size:20px; }
.app-brand-title { font-size:18px; font-weight:700; letter-spacing:.5px; }
.app-brand-sub { font-size:11px; opacity:.72; margin-top:2px; }
.profile-entry { width:42px; height:42px; border-radius:14px; border:1px solid rgba(255,255,255,.25); background:rgba(255,255,255,.16); color:#fff; cursor:pointer; display:flex; align-items:center; justify-content:center; font-size:17px; font-weight:700; }
.profile-entry:active { transform:scale(.96); }
.welcome-card { margin-top:18px; display:flex; justify-content:space-between; align-items:flex-end; gap:12px; }
.welcome-kicker { font-size:12px; opacity:.75; }
.welcome-name { margin-top:3px; font-size:21px; font-weight:700; }
.welcome-meta { margin-top:5px; font-size:12px; opacity:.82; }
.shift-badge { flex:none; padding:7px 10px; border-radius:10px; background:rgba(255,255,255,.15); font-size:11px; }
.workbench-content { padding:14px 12px 0; }
.today-panel { display:grid; grid-template-columns:repeat(3,1fr); gap:8px; margin-top:-2px; margin-bottom:14px; }
.today-stat { background:#fff; border:1px solid #e5edf8; border-radius:12px; padding:12px 8px; text-align:center; box-shadow:var(--shadow); }
.today-stat-num { font-size:20px; font-weight:750; color:#163b68; line-height:1.1; }
.today-stat-label { margin-top:5px; font-size:11px; color:var(--text-secondary); }
.phase-notice { display:flex; align-items:center; gap:11px; margin:0 0 16px; padding:12px 13px; border:1px solid #cfe0fb; border-radius:13px; background:linear-gradient(135deg,#f6f9ff,#edf4ff); }
.phase-notice-icon { flex:none; width:34px; height:34px; border-radius:11px; background:#155eef; color:#fff; display:flex; align-items:center; justify-content:center; font-size:16px; box-shadow:0 5px 12px rgba(21,94,239,.2); }
.phase-notice-title { font-size:12px; font-weight:700; color:#163b68; }
.phase-notice-desc { margin-top:2px; font-size:10px; color:#6b7f99; }
.section-heading { display:flex; align-items:center; justify-content:space-between; margin:17px 2px 9px; }
.section-heading-title { display:flex; align-items:center; gap:8px; font-size:16px; font-weight:700; color:#172b4d; }
.section-heading-title::before { content:''; width:4px; height:16px; border-radius:3px; background:var(--section-color,#1a73e8); }
.section-heading-count { font-size:11px; color:#8a97a8; }
.module-card { background:#fff; border:1px solid #e6ebf2; border-radius:16px; padding:5px 8px; box-shadow:0 3px 12px rgba(20,49,82,.05); }
.module-grid { display:grid; grid-template-columns:repeat(4,1fr); }
.module-item { position:relative; border:0; background:transparent; padding:12px 3px 11px; min-width:0; cursor:pointer; border-radius:11px; color:#2f3e52; }
.module-item:active { background:#f0f5ff; transform:scale(.97); }
.module-icon { width:42px; height:42px; margin:0 auto 7px; border-radius:13px; display:flex; align-items:center; justify-content:center; font-size:21px; background:var(--icon-bg,#eaf2ff); color:var(--icon-color,#155eef); }
.module-name { font-size:12px; line-height:1.3; white-space:nowrap; }
.module-badge { position:absolute; top:7px; right:5px; min-width:17px; height:17px; padding:0 4px; border-radius:9px; background:#ef4444; color:#fff; font-size:9px; line-height:17px; }
.module-card.inbound { --icon-bg:#eaf2ff; --icon-color:#155eef; }
.module-card.outbound { --icon-bg:#e7f8f3; --icon-color:#0d9488; }
.module-card.internal { --icon-bg:#fff4e5; --icon-color:#e27a16; }
.module-item.disabled { cursor:not-allowed; color:#a8b1bd; opacity:.72; }
.module-item.disabled .module-icon { background:#f1f3f5; color:#aeb7c2; filter:grayscale(1); }
.module-item.disabled:active { background:transparent; transform:none; }
.module-lock { position:absolute; top:8px; right:7px; width:16px; height:16px; border-radius:50%; background:#e7eaee; color:#8d98a6; display:flex; align-items:center; justify-content:center; font-size:8px; }
.module-item.active-menu { margin:5px 3px; padding-top:7px; padding-bottom:7px; border:1px solid #8dd8cd; background:linear-gradient(145deg,#f0fdfa,#e7f8f3); box-shadow:0 5px 13px rgba(13,148,136,.13); color:#0b6f68; }
.module-item.active-menu .module-icon { background:#0d9488; color:#fff; box-shadow:0 6px 12px rgba(13,148,136,.22); }
.module-open-badge { position:absolute; top:-7px; right:-3px; padding:2px 5px; border-radius:7px; background:#0d9488; color:#fff; font-size:8px; font-weight:700; box-shadow:0 3px 8px rgba(13,148,136,.22); }
.section-status { display:inline-flex; align-items:center; gap:4px; padding:3px 7px; border-radius:8px; background:#f0f2f5; color:#929eac; font-size:10px; }
.section-status.open { background:#e7f8f3; color:#0b7b70; font-weight:600; }
.page-topbar { position:sticky; top:0; z-index:90; height:60px; padding:0 12px; display:flex; align-items:center; justify-content:space-between; background:#fff; border-bottom:1px solid var(--border); box-shadow:0 2px 8px rgba(20,49,82,.05); }
.page-back { width:38px; height:38px; border:0; border-radius:11px; background:#f1f5f9; color:#334155; font-size:20px; cursor:pointer; }
.page-title { font-size:17px; font-weight:700; color:#172b4d; }
.page-user { width:36px; height:36px; border:0; border-radius:12px; background:#eaf2ff; color:#155eef; font-size:13px; font-weight:700; cursor:pointer; }
.operation-content { padding:14px 12px 30px; }
.operation-hero { padding:18px; border-radius:17px; color:#fff; background:linear-gradient(135deg,#155eef,#2878ff); box-shadow:0 8px 20px rgba(21,94,239,.18); }
.operation-hero-icon { width:46px; height:46px; border-radius:14px; display:flex; align-items:center; justify-content:center; background:rgba(255,255,255,.17); font-size:24px; margin-bottom:12px; }
.operation-hero-title { font-size:20px; font-weight:700; }
.operation-hero-desc { margin-top:5px; font-size:12px; line-height:1.6; opacity:.83; }
.task-card { margin-top:12px; background:#fff; border:1px solid #e4eaf2; border-radius:15px; padding:15px; box-shadow:var(--shadow); }
.task-card-title { font-size:14px; font-weight:700; color:#243b53; margin-bottom:12px; }
.task-row { display:flex; align-items:center; gap:11px; padding:11px 0; border-top:1px solid #edf1f5; }
.task-row:first-of-type { border-top:0; }
.task-row-icon { width:34px; height:34px; border-radius:10px; background:#edf4ff; display:flex; align-items:center; justify-content:center; }
.task-row-main { flex:1; min-width:0; }
.task-row-name { font-size:13px; font-weight:600; color:#334e68; }
.task-row-desc { margin-top:3px; font-size:11px; color:#829ab1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.task-row-arrow { color:#9fb0c3; }
.scan-action { width:100%; margin-top:14px; padding:13px; border:0; border-radius:12px; background:#155eef; color:#fff; font-size:14px; font-weight:700; cursor:pointer; }
.profile-cover { background:linear-gradient(135deg,#155eef,#2878ff); height:150px; padding:16px; color:#fff; }
.profile-card { margin:-62px 14px 14px; background:#fff; border-radius:18px; padding:18px; box-shadow:0 10px 28px rgba(20,49,82,.13); text-align:center; position:relative; }
.profile-avatar { width:68px; height:68px; border-radius:22px; margin:-50px auto 10px; display:flex; align-items:center; justify-content:center; background:#eaf2ff; border:4px solid #fff; color:#155eef; font-size:25px; font-weight:800; }
.profile-name { font-size:19px; font-weight:700; color:#172b4d; }
.profile-role { margin-top:4px; font-size:12px; color:#728196; }
.profile-stats { display:grid; grid-template-columns:repeat(3,1fr); margin-top:16px; padding-top:14px; border-top:1px solid #edf1f5; }
.profile-stat strong { display:block; color:#163b68; font-size:17px; }
.profile-stat span { font-size:10px; color:#8a97a8; }
.profile-list { margin:12px; background:#fff; border-radius:16px; padding:2px 14px; box-shadow:var(--shadow); }
.profile-list-item { display:flex; align-items:center; gap:12px; padding:15px 0; border-bottom:1px solid #edf1f5; font-size:13px; color:#334e68; }
.profile-list-item:last-child { border-bottom:0; }
.profile-list-icon { width:32px; height:32px; border-radius:10px; background:#eef4ff; display:flex; align-items:center; justify-content:center; }
.profile-list-value { margin-left:auto; color:#829ab1; font-size:12px; }
.profile-logout { width:calc(100% - 24px); margin:4px 12px 24px; padding:13px; border:1px solid #fecaca; border-radius:12px; background:#fff; color:#dc2626; font-size:14px; font-weight:600; cursor:pointer; }
/* ========== 统一视觉升级 ========== */
:root {
--primary:#1769e0;
--primary-light:#eaf2ff;
--success:#078775;
--success-light:#e1f6f1;
--bg:#f3f6fa;
--text:#172033;
--text-secondary:#68758a;
--border:#dfe5ee;
--shadow:0 3px 12px rgba(23,32,51,.07);
--radius:10px;
}
button, input { font-family:inherit; }
button { -webkit-tap-highlight-color:transparent; }
button:focus-visible, input:focus-visible { outline:3px solid rgba(23,105,224,.18); outline-offset:2px; }
/* 登录 */
.login-page {
justify-content:flex-start;
padding:0 22px 28px;
background:linear-gradient(180deg,#123b72 0 258px,#f3f6fa 258px 100%);
}
.login-logo { margin:0 -22px; padding:54px 22px 42px; text-align:left; }
.login-brand-line { display:flex; align-items:center; gap:14px; }
.login-logo-icon {
flex:none; width:58px; height:58px; margin:0; border-radius:14px;
background:rgba(255,255,255,.14); border:1px solid rgba(255,255,255,.2);
box-shadow:none;
}
.login-logo-icon svg { width:31px; height:31px; }
.login-logo-title { font-size:21px; letter-spacing:0; }
.login-logo-sub { color:rgba(255,255,255,.72); font-size:12px; margin-top:5px; }
.login-status { display:flex; align-items:center; gap:6px; margin-top:25px; color:rgba(255,255,255,.8); font-size:11px; }
.login-status::before { content:''; width:7px; height:7px; border-radius:50%; background:#48d5bc; box-shadow:0 0 0 4px rgba(72,213,188,.12); }
.login-card {
margin-top:-18px; border:1px solid #e2e7ef; border-radius:12px; padding:22px 20px 18px;
box-shadow:0 12px 30px rgba(18,59,114,.12);
}
.login-card-title { font-size:18px; font-weight:750; color:#172033; }
.login-card-desc { margin:5px 0 20px; color:#78869a; font-size:12px; }
.login-field { margin-bottom:15px; }
.login-field-label { color:#445168; font-weight:600; margin-bottom:7px; }
.login-input-wrap { position:relative; }
.login-input-icon { position:absolute; left:13px; top:50%; transform:translateY(-50%); color:#8290a4; display:flex; }
.login-input-icon svg { width:18px; height:18px; }
.login-field-input {
height:48px; padding:0 42px; border:1px solid #d8e0eb; border-radius:8px;
background:#f9fbfd; color:#172033; font-size:16px;
}
.login-field-input:focus { border-color:var(--primary); background:#fff; box-shadow:0 0 0 3px rgba(23,105,224,.08); }
.password-toggle { position:absolute; right:4px; top:4px; width:40px; height:40px; border:0; background:transparent; color:#6f7d91; border-radius:7px; cursor:pointer; display:flex; align-items:center; justify-content:center; }
.password-toggle svg { width:19px; height:19px; }
.login-error { padding:9px 11px; border-radius:7px; background:#fff1f1; }
.login-btn { height:48px; padding:0; margin-top:5px; border-radius:8px; background:#1769e0; box-shadow:0 6px 14px rgba(23,105,224,.2); }
.login-demo-tip { margin-top:13px; padding:10px 12px; border-radius:7px; background:#f2f5f9; color:#6b788c; font-size:11px; text-align:center; }
/* 工作台 */
.app-topbar { padding:15px 16px 24px; border-radius:0 0 16px 16px; background:linear-gradient(140deg,#123b72,#1769e0); box-shadow:0 7px 20px rgba(18,59,114,.2); }
.app-brand-mark { width:36px; height:36px; border-radius:9px; font-size:18px; }
.app-brand-title { font-size:17px; letter-spacing:0; }
.profile-entry { width:40px; height:40px; border-radius:10px; }
.welcome-card { margin-top:20px; align-items:center; }
.welcome-name { font-size:20px; }
.shift-badge { display:flex; align-items:center; gap:6px; border:1px solid rgba(255,255,255,.16); border-radius:8px; }
.shift-badge::before { content:''; width:6px; height:6px; border-radius:50%; background:#5ee6c8; }
.workbench-content { padding:14px 12px 22px; }
.today-panel { gap:7px; margin-top:-1px; }
.today-stat { border-radius:9px; padding:12px 6px; }
.today-stat-num { font-size:21px; color:#123b72; }
.phase-notice { border-radius:9px; background:#eef5ff; }
.phase-notice-icon { width:32px; height:32px; border-radius:8px; box-shadow:none; }
.section-heading { margin:18px 2px 8px; }
.section-heading-title { font-size:15px; }
.module-card { border-radius:10px; padding:4px 6px; box-shadow:var(--shadow); }
.module-item { min-height:78px; border-radius:8px; }
.module-icon { width:40px; height:40px; border-radius:10px; font-size:20px; }
.module-item.disabled { opacity:.58; }
.module-lock { background:#dfe4ea; }
.module-item.active-menu { margin:3px; border-radius:9px; border-color:#74cfbf; background:#e8f8f4; box-shadow:0 4px 10px rgba(7,135,117,.14); }
.module-open-badge { top:-6px; right:-2px; border-radius:5px; }
/* 个人中心 */
.profile-cover { height:132px; background:linear-gradient(140deg,#123b72,#1769e0); }
.profile-cover .page-back { background:rgba(255,255,255,.14); color:#fff; }
.profile-card { border-radius:12px; margin:-49px 12px 12px; padding:18px; }
.profile-avatar { border-radius:16px; }
.profile-list { border:1px solid #e3e8ef; border-radius:10px; box-shadow:none; }
.profile-list-icon { border-radius:8px; }
.profile-logout { border-radius:9px; }
/* 集装箱拣货 */
.header { padding:13px 14px 12px; background:linear-gradient(135deg,#123b72,#1769e0); box-shadow:0 3px 12px rgba(18,59,114,.22); }
.header-title { font-size:17px; }
.header-badge { border-radius:6px; }
.header-sub { display:flex; flex-wrap:wrap; gap:3px 10px; margin-top:7px; }
.header-sub span { margin-right:0; }
.box-select-wrap { margin:12px 12px 8px; }
.box-select-btn { min-height:46px; border:1px solid #c8d9f4; border-radius:9px; color:#174d91; box-shadow:none; }
.scan-section { margin:8px 12px; padding:12px; border:1px solid #d8e4f4; border-radius:10px; box-shadow:var(--shadow); }
.scan-section::before { content:'扫码拣货'; display:block; margin-bottom:8px; color:#394860; font-size:12px; font-weight:700; }
.scan-input { height:46px; padding-top:0; padding-bottom:0; border:1px solid #cfd9e7; border-radius:8px; background:#f8fafc; }
.scan-btn { width:46px; height:46px; border-radius:8px; background:#1769e0; }
.info-cards { margin:8px 12px; gap:8px; }
.info-card { min-width:0; border:1px solid #e0e6ee; border-radius:9px; padding:11px 12px; box-shadow:none; }
.info-card-value { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:14px; }
.progress-section { margin:8px 12px; border:1px solid #e0e6ee; border-radius:9px; box-shadow:none; }
.progress-bar-bg { height:7px; }
.progress-bar-fill { background:#078775; }
.pick-list { margin:8px 12px 12px; }
.pick-item { border:1px solid #e1e7ef; border-radius:9px; padding:14px; box-shadow:0 2px 8px rgba(23,32,51,.05); }
.pick-item.active { border-left:4px solid var(--primary); box-shadow:0 3px 12px rgba(23,105,224,.1); }
.pick-item-location, .pick-item-order { border-radius:5px; }
.pick-btn { min-height:42px; border-radius:7px; font-weight:600; }
.pick-btn-confirm { background:#078775; }
.pick-btn-partial { background:#eef4ff; color:#1769e0; border-color:#9ebcec; }
.submit-section { padding:10px 12px max(12px,env(safe-area-inset-bottom)); box-shadow:0 -4px 14px rgba(23,32,51,.08); }
.container-stat { border-radius:7px; min-height:34px; }
.submit-btn { min-height:48px; border-radius:8px; background:#1769e0; }
.partial-modal, .skip-modal, .boxlist-modal { border-radius:14px 14px 0 0; }
.complete-modal { border-radius:12px; }
@media (max-width:360px) {
.module-name { font-size:11px; }
.module-icon { width:38px; height:38px; }
.welcome-name { font-size:19px; }
.login-page { padding-left:16px; padding-right:16px; }
.login-logo { margin-left:-16px; margin-right:-16px; padding-left:18px; padding-right:18px; }
.pick-item-actions { gap:6px; }
.pick-btn { font-size:12px; }
}
</style>
</head>
<body>
<!-- Toast -->
<div class="toast" id="toast"></div>
<!-- ===== 登录页 ===== -->
<div class="login-page" id="loginPage">
<div class="login-logo">
<div class="login-brand-line">
<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><div class="login-logo-title">柏悦物流 PDA</div><div class="login-logo-sub">仓储移动作业平台</div></div>
</div>
<div class="login-status">作业系统运行正常</div>
</div>
<div class="login-card">
<div class="login-card-title">账号登录</div>
<div class="login-card-desc">请输入作业人员账号进入工作台</div>
<div class="login-field">
<div class="login-field-label">用户名</div>
<div class="login-input-wrap">
<span class="login-input-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0116 0"/></svg></span>
<input class="login-field-input" id="loginUser" placeholder="请输入用户名" autocomplete="username">
</div>
</div>
<div class="login-field">
<div class="login-field-label">密码</div>
<div class="login-input-wrap">
<span class="login-input-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="10" width="16" height="11" rx="2"/><path d="M8 10V7a4 4 0 018 0v3"/></svg></span>
<input class="login-field-input" id="loginPass" type="password" placeholder="请输入密码" autocomplete="current-password">
<button class="password-toggle" id="passwordToggle" type="button" onclick="togglePassword()" aria-label="显示密码">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12z"/><circle cx="12" cy="12" r="2.5"/></svg>
</button>
</div>
</div>
<div class="login-error" id="loginError">用户名或密码错误</div>
<button class="login-btn" onclick="handleLogin()">登录</button>
<div class="login-demo-tip">演示账号:W001 密码:123456</div>
</div>
</div>
<!-- ===== PDA 工作台 ===== -->
<div class="workbench-page hidden" id="workbenchPage">
<div class="app-shell">
<div class="app-topbar">
<div class="app-topbar-row">
<div class="app-brand">
<div class="app-brand-mark">▦</div>
<div><div class="app-brand-title">柏悦物流 PDA</div><div class="app-brand-sub">仓储移动作业平台</div></div>
</div>
<button class="profile-entry" id="workbenchAvatar" onclick="openProfile()" aria-label="个人中心">张</button>
</div>
<div class="welcome-card">
<div>
<div class="welcome-kicker">欢迎回来</div>
<div class="welcome-name" id="workbenchUserName">张伟</div>
<div class="welcome-meta"><span id="workbenchRole">拣货员</span> · <span id="workbenchWarehouse">洛杉矶仓</span></div>
</div>
<div class="shift-badge">白班 · 作业中</div>
</div>
</div>
<div class="workbench-content">
<div class="today-panel">
<div class="today-stat"><div class="today-stat-num">3</div><div class="today-stat-label">待拣集装箱</div></div>
<div class="today-stat"><div class="today-stat-num">0</div><div class="today-stat-label">今日已完成</div></div>
<div class="today-stat"><div class="today-stat-num">17</div><div class="today-stat-label">待拣货品</div></div>
</div>
<div class="phase-notice"><div class="phase-notice-icon">✓</div><div><div class="phase-notice-title">一期作业范围</div><div class="phase-notice-desc">当前仅开放“集装箱拣货”,其他功能将在后续版本启用</div></div></div>
<div class="section-heading" style="--section-color:#aeb7c2"><div class="section-heading-title">入库</div><div class="section-status">暂未开放</div></div>
<div class="module-card inbound"><div class="module-grid">
<button class="module-item disabled" type="button" disabled aria-disabled="true"><span class="module-lock">●</span><span class="module-icon">📥</span><span class="module-name">收货登记</span></button>
<button class="module-item disabled" type="button" disabled aria-disabled="true"><span class="module-lock">●</span><span class="module-icon">🗄️</span><span class="module-name">入库上架</span></button>
<button class="module-item disabled" type="button" disabled aria-disabled="true"><span class="module-lock">●</span><span class="module-icon">📐</span><span class="module-name">入库测量</span></button>
<button class="module-item disabled" type="button" disabled aria-disabled="true"><span class="module-lock">●</span><span class="module-icon">↩️</span><span class="module-name">退货入库</span></button>
</div></div>
<div class="section-heading" style="--section-color:#0d9488"><div class="section-heading-title">出库</div><div class="section-status open">本期开放 1 项</div></div>
<div class="module-card outbound"><div class="module-grid">
<button class="module-item disabled" type="button" disabled aria-disabled="true"><span class="module-lock">●</span><span class="module-icon">📋</span><span class="module-name">波次拣货</span></button>
<button class="module-item active-menu" type="button" onclick="openPicking()"><span class="module-open-badge">本期开放</span><span class="module-badge">3</span><span class="module-icon">📦</span><span class="module-name">集装箱拣货</span></button>
<button class="module-item disabled" type="button" disabled aria-disabled="true"><span class="module-lock">●</span><span class="module-icon">✅</span><span class="module-name">复核装箱</span></button>
<button class="module-item disabled" type="button" disabled aria-disabled="true"><span class="module-lock">●</span><span class="module-icon">🚚</span><span class="module-name">出库交接</span></button>
</div></div>
<div class="section-heading" style="--section-color:#aeb7c2"><div class="section-heading-title">库内作业</div><div class="section-status">暂未开放</div></div>
<div class="module-card internal"><div class="module-grid">
<button class="module-item disabled" type="button" disabled aria-disabled="true"><span class="module-lock">●</span><span class="module-icon">🔄</span><span class="module-name">移库作业</span></button>
<button class="module-item disabled" type="button" disabled aria-disabled="true"><span class="module-lock">●</span><span class="module-icon">📊</span><span class="module-name">库存盘点</span></button>
<button class="module-item disabled" type="button" disabled aria-disabled="true"><span class="module-lock">●</span><span class="module-icon">🔍</span><span class="module-name">库位查询</span></button>
<button class="module-item disabled" type="button" disabled aria-disabled="true"><span class="module-lock">●</span><span class="module-icon">⚠️</span><span class="module-name">异常处理</span></button>
</div></div>
</div>
</div>
</div>
<!-- ===== 通用作业页 ===== -->
<div class="operation-page hidden" id="operationPage">
<div class="app-shell">
<div class="page-topbar"><button class="page-back" onclick="backToWorkbench()">‹</button><div class="page-title" id="operationTitle">作业</div><button class="page-user" id="operationAvatar" onclick="openProfile()">张</button></div>
<div class="operation-content">
<div class="operation-hero"><div class="operation-hero-icon" id="operationIcon">📦</div><div class="operation-hero-title" id="operationHeroTitle">作业任务</div><div class="operation-hero-desc" id="operationDescription">扫描任务单或货物条码开始作业。</div></div>
<div class="task-card"><div class="task-card-title">待处理任务</div><div id="operationTaskList"></div></div>
<button class="scan-action" onclick="showToast('请扫描任务单或货物条码','success')">▦ 扫码开始作业</button>
</div>
</div>
</div>
<!-- ===== 个人中心 ===== -->
<div class="profile-page hidden" id="profilePage">
<div class="app-shell">
<div class="profile-cover"><button class="page-back" onclick="backFromProfile()">‹</button></div>
<div class="profile-card">
<div class="profile-avatar" id="profileAvatar">张</div><div class="profile-name" id="profileName">张伟</div><div class="profile-role" id="profileRole">W001 · 拣货员</div>
<div class="profile-stats"><div class="profile-stat"><strong>28</strong><span>今日完成</span></div><div class="profile-stat"><strong>156</strong><span>本月任务</span></div><div class="profile-stat"><strong>99.2%</strong><span>作业准确率</span></div></div>
</div>
<div class="profile-list">
<div class="profile-list-item"><span class="profile-list-icon">🏢</span><span>当前仓库</span><span class="profile-list-value" id="profileWarehouse">洛杉矶仓</span></div>
<div class="profile-list-item"><span class="profile-list-icon">🕘</span><span>当前班次</span><span class="profile-list-value">白班 08:00–17:00</span></div>
<div class="profile-list-item"><span class="profile-list-icon">🔐</span><span>修改密码</span><span class="profile-list-value">›</span></div>
<div class="profile-list-item"><span class="profile-list-icon">⚙️</span><span>系统设置</span><span class="profile-list-value">›</span></div>
<div class="profile-list-item"><span class="profile-list-icon">ℹ️</span><span>版本信息</span><span class="profile-list-value">V1.0.0</span></div>
</div>
<button class="profile-logout" onclick="handleLogout()">退出登录</button>
</div>
</div>
<!-- ===== 拣货主页面 ===== -->
<div class="main-page hidden" id="mainPage">
<div class="header">
<div class="header-top">
<div style="display:flex;align-items:center;gap:8px"><button class="logout-btn" onclick="backToWorkbench()" style="font-size:18px;padding:3px 9px">‹</button><div class="header-title">集装箱拣货</div></div>
<div style="display:flex;align-items:center;gap:8px;">
<div class="header-badge" id="headerBadge">仓库 A</div>
<button class="logout-btn" id="pickingAvatar" onclick="openProfile()">张</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: '拣货员', warehouse: '洛杉矶仓' },
'W002': { pass: '123456', name: '李敏', role: '拣货组长', warehouse: '洛杉矶仓' },
'W003': { pass: '123456', name: '王磊', role: '仓管员', warehouse: '新泽西仓' },
};
let currentUser = null;
let profileReturnPage = 'workbenchPage';
function togglePassword() {
const input = document.getElementById('loginPass');
const button = document.getElementById('passwordToggle');
const showing = input.type === 'text';
input.type = showing ? 'password' : 'text';
button.setAttribute('aria-label', showing ? '显示密码' : '隐藏密码');
}
function handleLogin() {
const userId = document.getElementById('loginUser').value.trim();
const password = document.getElementById('loginPass').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: account.warehouse };
enterMainPage();
}
function quickLogin(id, name, role) {
currentUser = { id: id, name: name, role: role, warehouse: (DEMO_ACCOUNTS[id] && DEMO_ACCOUNTS[id].warehouse) || '洛杉矶仓' };
enterMainPage();
}
function enterMainPage() {
document.getElementById('loginPage').classList.add('hidden');
showAppPage('workbenchPage');
updateUserUI();
initStates();
updateContainerStats();
hideLoginError();
showToast(currentUser.name + ',欢迎登录', 'success');
}
function showAppPage(pageId) {
['workbenchPage','operationPage','profilePage','mainPage'].forEach(id => {
document.getElementById(id).classList.toggle('hidden', id !== pageId);
});
window.scrollTo(0, 0);
}
function updateUserUI() {
const initial = currentUser.name.slice(0, 1);
document.getElementById('headerBadge').textContent = currentUser.warehouse;
document.getElementById('headerUser').textContent = currentUser.name + ' · ' + currentUser.role;
document.getElementById('workbenchUserName').textContent = currentUser.name;
document.getElementById('workbenchRole').textContent = currentUser.role;
document.getElementById('workbenchWarehouse').textContent = currentUser.warehouse;
document.getElementById('profileName').textContent = currentUser.name;
document.getElementById('profileRole').textContent = currentUser.id + ' · ' + currentUser.role;
document.getElementById('profileWarehouse').textContent = currentUser.warehouse;
['workbenchAvatar','operationAvatar','profileAvatar','pickingAvatar'].forEach(id => {
document.getElementById(id).textContent = initial;
});
}
function backToWorkbench() {
showAppPage('workbenchPage');
}
function openPicking() {
showAppPage('mainPage');
render();
}
function openProfile() {
const visible = ['workbenchPage','operationPage','mainPage'].find(id => !document.getElementById(id).classList.contains('hidden'));
profileReturnPage = visible || 'workbenchPage';
showAppPage('profilePage');
}
function backFromProfile() {
showAppPage(profileReturnPage || 'workbenchPage');
}
const OPERATION_CONFIG = {
'inbound-receive': { icon:'📥', desc:'扫描入库单和货物条码,完成到仓签收与数量核验。', tasks:[['IN-260728-018','义乌欧业 · 18箱'],['IN-260728-021','青岛远航 · 12箱'],['IN-260728-026','深圳璟智 · 8箱']] },
'inbound-shelve': { icon:'🗄️', desc:'根据系统推荐库位完成货物上架,并确认实际存放位置。', tasks:[['SH-260728-008','A区 · 5项待上架'],['SH-260728-011','C区 · 3项待上架']] },
'inbound-measure': { icon:'📐', desc:'采集货物长宽高、重量与箱数,提交测量结果。', tasks:[['MS-260728-015','待测量 6箱'],['MS-260728-017','待复测 2箱']] },
'return-inbound': { icon:'↩️', desc:'核验退货信息、商品状态与数量,完成退件入库。', tasks:[['RT-260728-006','客户退货 · 4件'],['RT-260728-009','FBA退件 · 7件']] },
'wave-picking': { icon:'📋', desc:'按波次任务集中拣选多个订单,提高出库作业效率。', tasks:[['WV-260728-003','12单 · 36件'],['WV-260728-005','8单 · 24件']] },
'packing-review': { icon:'✅', desc:'扫描订单与商品,复核数量后完成装箱封包。', tasks:[['PK-260728-012','待复核 5箱'],['PK-260728-014','待复核 3箱']] },
'outbound-handover': { icon:'🚚', desc:'核对承运车辆、司机和出库货物,完成交接签字。', tasks:[['HO-260728-004','顺通车队 · 8托'],['HO-260728-007','宏达车队 · 12托']] },
'stock-transfer': { icon:'🔄', desc:'扫描原库位、商品和目标库位,完成库存移位。', tasks:[['MV-260728-019','A-03 → C-08 · 6件'],['MV-260728-022','B-01 → D-02 · 4件']] },
'stock-check': { icon:'📊', desc:'按盘点任务核对系统库存与实物数量,记录差异。', tasks:[['CK-260728-002','A区循环盘点'],['CK-260728-006','C区抽样盘点']] },
'location-query': { icon:'🔍', desc:'扫描 SKU、箱号或库位码,快速查询库存位置与数量。', tasks:[['快捷查询','支持 SKU / 箱号 / 库位码']] },
'exception': { icon:'⚠️', desc:'处理破损、短少、错货、库位异常等仓内问题。', tasks:[['EX-260728-003','数量差异 · 待复核'],['EX-260728-005','包装破损 · 待处理']] }
};
function openOperation(title, key) {
const config = OPERATION_CONFIG[key] || { icon:'📦', desc:'扫描任务单或货物条码开始作业。', tasks:[] };
document.getElementById('operationTitle').textContent = title;
document.getElementById('operationHeroTitle').textContent = title;
document.getElementById('operationIcon').textContent = config.icon;
document.getElementById('operationDescription').textContent = config.desc;
document.getElementById('operationTaskList').innerHTML = config.tasks.map((task, index) => `
<div class="task-row" onclick="showToast('已选择任务:${task[0]}','success')">
<div class="task-row-icon">${index + 1}</div>
<div class="task-row-main"><div class="task-row-name">${task[0]}</div><div class="task-row-desc">${task[1]}</div></div>
<div class="task-row-arrow">›</div>
</div>`).join('') || '<div style="padding:22px 0;text-align:center;color:#829ab1;font-size:12px">暂无待处理任务</div>';
showAppPage('operationPage');
}
function handleLogout() {
currentUser = null;
document.getElementById('loginPage').classList.remove('hidden');
['workbenchPage','operationPage','profilePage','mainPage'].forEach(id => document.getElementById(id).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');
}
document.getElementById('loginPass').addEventListener('keydown', event => {
if (event.key === 'Enter') handleLogin();
});
// ===== 数据 =====
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>