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

2185 lines
80 KiB
HTML
Raw Normal View History

2026-07-31 10:43:30 +08:00
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
2026-08-05 09:49:02 +08:00
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>柏悦物流 PDA · 仓储作业平台</title>
2026-07-31 10:43:30 +08:00
<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); }
2026-08-05 09:49:02 +08:00
/* ========== 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; }
}
2026-07-31 10:43:30 +08:00
</style>
</head>
<body>
<!-- Toast -->
<div class="toast" id="toast"></div>
<!-- ===== 登录页 ===== -->
<div class="login-page" id="loginPage">
<div class="login-logo">
2026-08-05 09:49:02 +08:00
<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>
2026-07-31 10:43:30 +08:00
</div>
2026-08-05 09:49:02 +08:00
<div class="login-status">作业系统运行正常</div>
2026-07-31 10:43:30 +08:00
</div>
<div class="login-card">
2026-08-05 09:49:02 +08:00
<div class="login-card-title">账号登录</div>
<div class="login-card-desc">请输入作业人员账号进入工作台</div>
2026-07-31 10:43:30 +08:00
<div class="login-field">
2026-08-05 09:49:02 +08:00
<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>
2026-07-31 10:43:30 +08:00
</div>
<div class="login-field">
<div class="login-field-label">密码</div>
2026-08-05 09:49:02 +08:00
<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>
2026-07-31 10:43:30 +08:00
</button>
</div>
</div>
2026-08-05 09:49:02 +08:00
<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>
2026-07-31 10:43:30 +08:00
</div>
</div>
<!-- ===== 拣货主页面 ===== -->
<div class="main-page hidden" id="mainPage">
<div class="header">
<div class="header-top">
2026-08-05 09:49:02 +08:00
<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>
2026-07-31 10:43:30 +08:00
<div style="display:flex;align-items:center;gap:8px;">
<div class="header-badge" id="headerBadge">仓库 A</div>
2026-08-05 09:49:02 +08:00
<button class="logout-btn" id="pickingAvatar" onclick="openProfile()">张</button>
2026-07-31 10:43:30 +08:00
</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 = {
2026-08-05 09:49:02 +08:00
'W001': { pass: '123456', name: '张伟', role: '拣货员', warehouse: '洛杉矶仓' },
'W002': { pass: '123456', name: '李敏', role: '拣货组长', warehouse: '洛杉矶仓' },
'W003': { pass: '123456', name: '王磊', role: '仓管员', warehouse: '新泽西仓' },
2026-07-31 10:43:30 +08:00
};
let currentUser = null;
2026-08-05 09:49:02 +08:00
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 ? '显示密码' : '隐藏密码');
}
2026-07-31 10:43:30 +08:00
function handleLogin() {
const userId = document.getElementById('loginUser').value.trim();
const password = document.getElementById('loginPass').value;
2026-08-05 09:49:02 +08:00
if (!userId) { showLoginError('请输入用户名'); return; }
2026-07-31 10:43:30 +08:00
if (!password) { showLoginError('请输入密码'); return; }
// 检查账号
const account = DEMO_ACCOUNTS[userId];
if (!account || account.pass !== password) {
2026-08-05 09:49:02 +08:00
showLoginError('用户名或密码错误');
2026-07-31 10:43:30 +08:00
return;
}
2026-08-05 09:49:02 +08:00
currentUser = { id: userId, name: account.name, role: account.role, warehouse: account.warehouse };
2026-07-31 10:43:30 +08:00
enterMainPage();
}
function quickLogin(id, name, role) {
2026-08-05 09:49:02 +08:00
currentUser = { id: id, name: name, role: role, warehouse: (DEMO_ACCOUNTS[id] && DEMO_ACCOUNTS[id].warehouse) || '洛杉矶仓' };
2026-07-31 10:43:30 +08:00
enterMainPage();
}
function enterMainPage() {
document.getElementById('loginPage').classList.add('hidden');
2026-08-05 09:49:02 +08:00
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);
2026-07-31 10:43:30 +08:00
document.getElementById('headerBadge').textContent = currentUser.warehouse;
document.getElementById('headerUser').textContent = currentUser.name + ' · ' + currentUser.role;
2026-08-05 09:49:02 +08:00
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');
2026-07-31 10:43:30 +08:00
render();
2026-08-05 09:49:02 +08:00
}
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');
2026-07-31 10:43:30 +08:00
}
function handleLogout() {
currentUser = null;
document.getElementById('loginPage').classList.remove('hidden');
2026-08-05 09:49:02 +08:00
['workbenchPage','operationPage','profilePage','mainPage'].forEach(id => document.getElementById(id).classList.add('hidden'));
2026-07-31 10:43:30 +08:00
// 清空表单
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');
}
2026-08-05 09:49:02 +08:00
document.getElementById('loginPass').addEventListener('keydown', event => {
if (event.key === 'Enter') handleLogin();
});
2026-07-31 10:43:30 +08:00
// ===== 数据 =====
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>