|
|
@@ -0,0 +1,666 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="zh-CN">
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8" />
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
|
+ <title>库位管理 · 批量出库 Demo</title>
|
|
|
+ <style>
|
|
|
+ :root {
|
|
|
+ --primary: #10b981;
|
|
|
+ --primary-dark: #059669;
|
|
|
+ --primary-bg: #ecfdf5;
|
|
|
+ --text: #1e293b;
|
|
|
+ --muted: #64748b;
|
|
|
+ --border: #e2e8f0;
|
|
|
+ --danger: #ef4444;
|
|
|
+ --danger-bg: #fef2f2;
|
|
|
+ --danger-border: #fecaca;
|
|
|
+ --orange: #ea580c;
|
|
|
+ --orange-border: #fed7aa;
|
|
|
+ --orange-soft: #fff7ed;
|
|
|
+ }
|
|
|
+ * { box-sizing: border-box; }
|
|
|
+ body {
|
|
|
+ margin: 0;
|
|
|
+ font-family: Inter, "PingFang SC", "Helvetica Neue", Arial, sans-serif;
|
|
|
+ background: #f1f5f9;
|
|
|
+ color: var(--text);
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+ .page { max-width: 1100px; margin: 0 auto; padding: 20px 16px 80px; }
|
|
|
+ .intro {
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 16px 18px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ }
|
|
|
+ .intro h1 { margin: 0 0 6px; font-size: 18px; }
|
|
|
+ .intro p { margin: 0; color: var(--muted); font-size: 13px; }
|
|
|
+ .approaches {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ }
|
|
|
+ @media (max-width: 900px) {
|
|
|
+ .approaches { grid-template-columns: 1fr; }
|
|
|
+ }
|
|
|
+ .approach {
|
|
|
+ background: #fff;
|
|
|
+ border: 2px solid var(--border);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 14px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: border-color .15s, box-shadow .15s;
|
|
|
+ }
|
|
|
+ .approach:hover { border-color: #a7f3d0; }
|
|
|
+ .approach.active {
|
|
|
+ border-color: var(--primary);
|
|
|
+ box-shadow: 0 0 0 3px rgba(16,185,129,.15);
|
|
|
+ }
|
|
|
+ .approach .badge {
|
|
|
+ display: inline-block;
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 600;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 999px;
|
|
|
+ background: var(--primary-bg);
|
|
|
+ color: var(--primary-dark);
|
|
|
+ margin-bottom: 8px;
|
|
|
+ }
|
|
|
+ .approach .badge.rec { background: #d1fae5; }
|
|
|
+ .approach h3 { margin: 0 0 6px; font-size: 15px; }
|
|
|
+ .approach p { margin: 0; font-size: 12px; color: var(--muted); }
|
|
|
+ .panel {
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 16px 18px 20px;
|
|
|
+ display: flex;
|
|
|
+ gap: 16px;
|
|
|
+ min-height: 520px;
|
|
|
+ }
|
|
|
+ .aside {
|
|
|
+ width: 220px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ border-right: 1px solid var(--border);
|
|
|
+ padding-right: 14px;
|
|
|
+ }
|
|
|
+ .aside h2 {
|
|
|
+ margin: 0 0 12px;
|
|
|
+ font-size: 15px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+ .aside h2::before {
|
|
|
+ content: "";
|
|
|
+ width: 3px;
|
|
|
+ height: 14px;
|
|
|
+ background: var(--primary);
|
|
|
+ border-radius: 2px;
|
|
|
+ }
|
|
|
+ .tree-node {
|
|
|
+ padding: 8px 10px;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: var(--muted);
|
|
|
+ }
|
|
|
+ .tree-node.active {
|
|
|
+ background: var(--primary-bg);
|
|
|
+ color: var(--primary-dark);
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+ .tree-node .leaf {
|
|
|
+ float: right;
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--primary);
|
|
|
+ background: #d1fae5;
|
|
|
+ padding: 0 6px;
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
+ .main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
|
+ .header { margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid #f0f0f0; }
|
|
|
+ .header h3 { margin: 0; font-size: 15px; }
|
|
|
+ .stats { font-size: 12px; color: var(--muted); margin-top: 4px; }
|
|
|
+ .stats strong { color: var(--text); }
|
|
|
+ .filter {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ }
|
|
|
+ .filter input {
|
|
|
+ width: 100%;
|
|
|
+ max-width: 360px;
|
|
|
+ height: 36px;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 0 12px;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+ .toolbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ margin-bottom: 14px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ min-height: 36px;
|
|
|
+ }
|
|
|
+ .toolbar .selected-hint {
|
|
|
+ font-size: 13px;
|
|
|
+ color: var(--muted);
|
|
|
+ margin-right: 4px;
|
|
|
+ }
|
|
|
+ .toolbar .selected-hint em {
|
|
|
+ font-style: normal;
|
|
|
+ color: var(--primary-dark);
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+ .btn {
|
|
|
+ height: 32px;
|
|
|
+ padding: 0 14px;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ background: #fff;
|
|
|
+ color: var(--text);
|
|
|
+ font-size: 13px;
|
|
|
+ cursor: pointer;
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ }
|
|
|
+ .btn:disabled { opacity: .45; cursor: not-allowed; }
|
|
|
+ .btn-primary {
|
|
|
+ background: var(--primary);
|
|
|
+ border-color: var(--primary);
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+ .btn-primary:hover:not(:disabled) { background: var(--primary-dark); }
|
|
|
+ .btn-danger {
|
|
|
+ background: #fff;
|
|
|
+ border-color: #fca5a5;
|
|
|
+ color: #dc2626;
|
|
|
+ }
|
|
|
+ .btn-danger:hover:not(:disabled) { background: var(--danger-bg); }
|
|
|
+ .btn-ghost:hover:not(:disabled) { background: #f8fafc; }
|
|
|
+ .mode-banner {
|
|
|
+ display: none;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ background: #fff7ed;
|
|
|
+ border: 1px solid #fdba74;
|
|
|
+ color: #9a3412;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 8px 12px;
|
|
|
+ font-size: 13px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ }
|
|
|
+ .mode-banner.show { display: flex; }
|
|
|
+ .mode-banner strong { color: #c2410c; }
|
|
|
+ .shelf { display: flex; flex-direction: column; gap: 10px; }
|
|
|
+ .row { display: flex; align-items: stretch; }
|
|
|
+ .row-label {
|
|
|
+ width: 36px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ background: var(--orange);
|
|
|
+ color: #fff;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-weight: 600;
|
|
|
+ border-radius: 4px 0 0 4px;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+ .cells { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
|
+ .cell {
|
|
|
+ width: 80px;
|
|
|
+ height: 80px;
|
|
|
+ border: 1px solid var(--orange-border);
|
|
|
+ border-radius: 4px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ cursor: pointer;
|
|
|
+ position: relative;
|
|
|
+ padding: 4px 2px;
|
|
|
+ gap: 1px;
|
|
|
+ background: #fff;
|
|
|
+ transition: all .15s;
|
|
|
+ user-select: none;
|
|
|
+ }
|
|
|
+ .cell:hover { background: var(--orange-soft); }
|
|
|
+ .cell.occupied {
|
|
|
+ background: var(--danger-bg);
|
|
|
+ border-color: var(--danger-border);
|
|
|
+ }
|
|
|
+ .cell.occupied:hover { background: #fee2e2; }
|
|
|
+ .cell .name {
|
|
|
+ font-size: 11px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #991b1b;
|
|
|
+ text-align: center;
|
|
|
+ line-height: 1.2;
|
|
|
+ max-width: 72px;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+ .cell .type {
|
|
|
+ font-size: 10px;
|
|
|
+ color: #b91c1c;
|
|
|
+ max-width: 72px;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+ .cell .empty { font-size: 12px; font-weight: 600; color: #303133; }
|
|
|
+ .cell .check {
|
|
|
+ display: none;
|
|
|
+ position: absolute;
|
|
|
+ top: 3px;
|
|
|
+ left: 3px;
|
|
|
+ width: 18px;
|
|
|
+ height: 18px;
|
|
|
+ border-radius: 4px;
|
|
|
+ border: 1.5px solid #cbd5e1;
|
|
|
+ background: #fff;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ font-size: 11px;
|
|
|
+ color: transparent;
|
|
|
+ z-index: 2;
|
|
|
+ }
|
|
|
+ body.select-mode .cell.occupied .check { display: flex; }
|
|
|
+ body.select-mode .cell.empty-slot {
|
|
|
+ opacity: .4;
|
|
|
+ cursor: not-allowed;
|
|
|
+ }
|
|
|
+ body.select-mode .cell.occupied.selected {
|
|
|
+ outline: 2px solid var(--primary);
|
|
|
+ outline-offset: 1px;
|
|
|
+ background: #ecfdf5;
|
|
|
+ border-color: #a7f3d0;
|
|
|
+ }
|
|
|
+ body.select-mode .cell.occupied.selected .name,
|
|
|
+ body.select-mode .cell.occupied.selected .type { color: var(--primary-dark); }
|
|
|
+ body.select-mode .cell.occupied.selected .check {
|
|
|
+ background: var(--primary);
|
|
|
+ border-color: var(--primary);
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+ body.select-mode .cell.occupied:not(.selected):hover .check {
|
|
|
+ border-color: var(--primary);
|
|
|
+ }
|
|
|
+ .toast {
|
|
|
+ position: fixed;
|
|
|
+ top: 20px;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%) translateY(-20px);
|
|
|
+ background: #065f46;
|
|
|
+ color: #fff;
|
|
|
+ padding: 10px 18px;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 13px;
|
|
|
+ opacity: 0;
|
|
|
+ pointer-events: none;
|
|
|
+ transition: all .25s;
|
|
|
+ z-index: 100;
|
|
|
+ }
|
|
|
+ .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
|
+ .mask {
|
|
|
+ display: none;
|
|
|
+ position: fixed;
|
|
|
+ inset: 0;
|
|
|
+ background: rgba(15, 23, 42, .35);
|
|
|
+ z-index: 90;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+ .mask.show { display: flex; }
|
|
|
+ .dialog {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 10px;
|
|
|
+ width: 420px;
|
|
|
+ max-width: 92vw;
|
|
|
+ padding: 20px;
|
|
|
+ box-shadow: 0 20px 40px rgba(0,0,0,.12);
|
|
|
+ }
|
|
|
+ .dialog h4 { margin: 0 0 8px; font-size: 16px; }
|
|
|
+ .dialog p { margin: 0 0 14px; color: var(--muted); font-size: 13px; }
|
|
|
+ .dialog .list {
|
|
|
+ max-height: 160px;
|
|
|
+ overflow: auto;
|
|
|
+ background: #f8fafc;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 8px 10px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ .dialog .list div { padding: 3px 0; }
|
|
|
+ .dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
|
+ .hint-bar {
|
|
|
+ margin-top: 14px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--muted);
|
|
|
+ background: #f8fafc;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 10px 12px;
|
|
|
+ }
|
|
|
+ .hint-bar code {
|
|
|
+ background: #e2e8f0;
|
|
|
+ padding: 1px 5px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 11px;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <div class="page">
|
|
|
+ <div class="intro">
|
|
|
+ <h1>库位管理 · 批量出库交互 Demo</h1>
|
|
|
+ <p>先点下方方案切换预览,再在货架上试操作。推荐方案 A(工具栏进入多选模式)。</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="approaches">
|
|
|
+ <div class="approach active" data-choice="a" onclick="pickApproach(this)">
|
|
|
+ <span class="badge rec">推荐 · A</span>
|
|
|
+ <h3>工具栏「批量出库」→ 多选格子</h3>
|
|
|
+ <p>点按钮进入选择模式,只可选占用格;确认后一次性出库。空格不可选。</p>
|
|
|
+ </div>
|
|
|
+ <div class="approach" data-choice="b" onclick="pickApproach(this)">
|
|
|
+ <span class="badge">B</span>
|
|
|
+ <h3>格子常驻勾选框</h3>
|
|
|
+ <p>占用格左上角始终显示勾选框,选完后点工具栏「出库」。更直接,但日常浏览稍吵。</p>
|
|
|
+ </div>
|
|
|
+ <div class="approach" data-choice="c" onclick="pickApproach(this)">
|
|
|
+ <span class="badge">C</span>
|
|
|
+ <h3>框选 / 起止区间</h3>
|
|
|
+ <p>拖拽或选起止格批量勾选同盒连续占用格。适合密集出库,交互更重。</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="panel">
|
|
|
+ <aside class="aside">
|
|
|
+ <h2>库位</h2>
|
|
|
+ <div class="tree-node">A区冷库</div>
|
|
|
+ <div class="tree-node" style="padding-left:22px">1层</div>
|
|
|
+ <div class="tree-node active" style="padding-left:34px">
|
|
|
+ 盒-01 <span class="leaf">存</span>
|
|
|
+ </div>
|
|
|
+ </aside>
|
|
|
+
|
|
|
+ <div class="main">
|
|
|
+ <div class="header">
|
|
|
+ <h3>A区冷库 / 1层 / 盒-01</h3>
|
|
|
+ <div class="stats">
|
|
|
+ 总数 <strong id="statTotal">12</strong>
|
|
|
+ · 空闲 <strong id="statEmpty">4</strong>
|
|
|
+ · 占用 <strong id="statOcc">8</strong>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="filter">
|
|
|
+ <input type="text" placeholder="物品名称 / 产品类型 / 批次号" disabled />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="mode-banner" id="modeBanner">
|
|
|
+ <strong>批量出库模式</strong>
|
|
|
+ <span>请勾选要出库的占用储物格,空格不可选。</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="toolbar" id="toolbar"></div>
|
|
|
+
|
|
|
+ <div class="shelf" id="shelf"></div>
|
|
|
+
|
|
|
+ <div class="hint-bar" id="hint">
|
|
|
+ 当前方案 A:先点「批量出库」进入选择模式,再点占用格勾选,最后确认出库。
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="toast" id="toast"></div>
|
|
|
+
|
|
|
+ <div class="mask" id="mask">
|
|
|
+ <div class="dialog">
|
|
|
+ <h4>批量出库确认</h4>
|
|
|
+ <p id="confirmText">确认将选中储物格中的物品出库?</p>
|
|
|
+ <div class="list" id="confirmList"></div>
|
|
|
+ <div class="dialog-actions">
|
|
|
+ <button class="btn btn-ghost" onclick="closeConfirm()">取消</button>
|
|
|
+ <button class="btn btn-primary" onclick="doOutbound()">确认出库</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ const HINTS = {
|
|
|
+ a: '当前方案 A:先点「批量出库」进入选择模式,再点占用格勾选,最后确认出库。',
|
|
|
+ b: '当前方案 B:占用格左上角常驻勾选框,勾选后点工具栏「出库已选」。',
|
|
|
+ c: '当前方案 C:演示简化为「按行全选占用格」+ 手动点选;正式版可再做拖拽框选/起止区间。'
|
|
|
+ }
|
|
|
+
|
|
|
+ let approach = 'a'
|
|
|
+ let selectMode = false
|
|
|
+ let selected = new Set()
|
|
|
+
|
|
|
+ const initialCells = [
|
|
|
+ // row A
|
|
|
+ { id: 'A1', row: 'A', name: 'CP-1001', type: 'MSC', occupied: true },
|
|
|
+ { id: 'A2', row: 'A', name: 'CP-1002', type: 'MSC', occupied: true },
|
|
|
+ { id: 'A3', row: 'A', name: '', type: '', occupied: false },
|
|
|
+ { id: 'A4', row: 'A', name: 'CP-1004', type: 'NK', occupied: true },
|
|
|
+ // row B
|
|
|
+ { id: 'B1', row: 'B', name: 'CP-2001', type: 'CAR-T', occupied: true },
|
|
|
+ { id: 'B2', row: 'B', name: '', type: '', occupied: false },
|
|
|
+ { id: 'B3', row: 'B', name: 'CP-2003', type: 'MSC', occupied: true },
|
|
|
+ { id: 'B4', row: 'B', name: 'CP-2004', type: 'MSC', occupied: true },
|
|
|
+ // row C
|
|
|
+ { id: 'C1', row: 'C', name: '', type: '', occupied: false },
|
|
|
+ { id: 'C2', row: 'C', name: 'CP-3002', type: 'NK', occupied: true },
|
|
|
+ { id: 'C3', row: 'C', name: 'CP-3003', type: 'NK', occupied: true },
|
|
|
+ { id: 'C4', row: 'C', name: '', type: '', occupied: false }
|
|
|
+ ]
|
|
|
+
|
|
|
+ let cells = JSON.parse(JSON.stringify(initialCells))
|
|
|
+
|
|
|
+ function pickApproach(el) {
|
|
|
+ document.querySelectorAll('.approach').forEach(a => a.classList.remove('active'))
|
|
|
+ el.classList.add('active')
|
|
|
+ approach = el.dataset.choice
|
|
|
+ selectMode = approach === 'b' // B always selectable
|
|
|
+ selected.clear()
|
|
|
+ document.getElementById('hint').textContent = HINTS[approach]
|
|
|
+ render()
|
|
|
+ if (window.toggleSelect) {
|
|
|
+ // companion helper if present
|
|
|
+ try { window.toggleSelect(el) } catch (_) {}
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateStats() {
|
|
|
+ const total = cells.length
|
|
|
+ const occ = cells.filter(c => c.occupied).length
|
|
|
+ document.getElementById('statTotal').textContent = total
|
|
|
+ document.getElementById('statEmpty').textContent = total - occ
|
|
|
+ document.getElementById('statOcc').textContent = occ
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderToolbar() {
|
|
|
+ const tb = document.getElementById('toolbar')
|
|
|
+ const n = selected.size
|
|
|
+ if (approach === 'a') {
|
|
|
+ if (!selectMode) {
|
|
|
+ tb.innerHTML = `
|
|
|
+ <button class="btn btn-primary" onclick="enterSelectMode()">⬆ 批量出库</button>
|
|
|
+ <button class="btn btn-ghost" disabled>批量生成格子</button>
|
|
|
+ <button class="btn btn-ghost" disabled>清空格子</button>
|
|
|
+ `
|
|
|
+ } else {
|
|
|
+ tb.innerHTML = `
|
|
|
+ <span class="selected-hint">已选 <em>${n}</em> 项</span>
|
|
|
+ <button class="btn btn-primary" ${n ? '' : 'disabled'} onclick="openConfirm()">确认出库</button>
|
|
|
+ <button class="btn btn-ghost" onclick="selectAllOccupied()">全选占用</button>
|
|
|
+ <button class="btn btn-ghost" onclick="clearSelection()">清空选择</button>
|
|
|
+ <button class="btn btn-ghost" onclick="exitSelectMode()">取消</button>
|
|
|
+ `
|
|
|
+ }
|
|
|
+ } else if (approach === 'b') {
|
|
|
+ tb.innerHTML = `
|
|
|
+ <span class="selected-hint">已选 <em>${n}</em> 项</span>
|
|
|
+ <button class="btn btn-primary" ${n ? '' : 'disabled'} onclick="openConfirm()">⬆ 出库已选</button>
|
|
|
+ <button class="btn btn-ghost" onclick="selectAllOccupied()">全选占用</button>
|
|
|
+ <button class="btn btn-ghost" onclick="clearSelection()">清空选择</button>
|
|
|
+ `
|
|
|
+ } else {
|
|
|
+ tb.innerHTML = `
|
|
|
+ <span class="selected-hint">已选 <em>${n}</em> 项</span>
|
|
|
+ <button class="btn btn-primary" ${n ? '' : 'disabled'} onclick="openConfirm()">⬆ 确认出库</button>
|
|
|
+ <button class="btn btn-ghost" onclick="selectRow('A')">全选 A 行</button>
|
|
|
+ <button class="btn btn-ghost" onclick="selectRow('B')">全选 B 行</button>
|
|
|
+ <button class="btn btn-ghost" onclick="selectRow('C')">全选 C 行</button>
|
|
|
+ <button class="btn btn-ghost" onclick="clearSelection()">清空选择</button>
|
|
|
+ `
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderShelf() {
|
|
|
+ const shelf = document.getElementById('shelf')
|
|
|
+ const rows = ['A', 'B', 'C']
|
|
|
+ shelf.innerHTML = rows.map(row => {
|
|
|
+ const rowCells = cells.filter(c => c.row === row)
|
|
|
+ return `
|
|
|
+ <div class="row">
|
|
|
+ <div class="row-label">${row}</div>
|
|
|
+ <div class="cells">
|
|
|
+ ${rowCells.map(c => {
|
|
|
+ const occ = c.occupied
|
|
|
+ const sel = selected.has(c.id)
|
|
|
+ return `
|
|
|
+ <div class="cell ${occ ? 'occupied' : 'empty-slot'} ${sel ? 'selected' : ''}"
|
|
|
+ data-id="${c.id}"
|
|
|
+ onclick="onCellClick('${c.id}')"
|
|
|
+ title="${occ ? c.name + ' · 点击选择/取消' : '空格不可出库'}">
|
|
|
+ <span class="check">✓</span>
|
|
|
+ ${occ
|
|
|
+ ? `<span class="name">${c.name}</span><span class="type">${c.type}</span>`
|
|
|
+ : `<span class="empty">—</span>`}
|
|
|
+ </div>
|
|
|
+ `
|
|
|
+ }).join('')}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `
|
|
|
+ }).join('')
|
|
|
+ }
|
|
|
+
|
|
|
+ function render() {
|
|
|
+ const showBanner = approach === 'a' && selectMode
|
|
|
+ document.getElementById('modeBanner').classList.toggle('show', showBanner)
|
|
|
+ document.body.classList.toggle('select-mode', approach === 'a' ? selectMode : true)
|
|
|
+ updateStats()
|
|
|
+ renderToolbar()
|
|
|
+ renderShelf()
|
|
|
+ }
|
|
|
+
|
|
|
+ function enterSelectMode() {
|
|
|
+ selectMode = true
|
|
|
+ selected.clear()
|
|
|
+ render()
|
|
|
+ toast('已进入批量出库模式')
|
|
|
+ }
|
|
|
+
|
|
|
+ function exitSelectMode() {
|
|
|
+ selectMode = false
|
|
|
+ selected.clear()
|
|
|
+ render()
|
|
|
+ }
|
|
|
+
|
|
|
+ function onCellClick(id) {
|
|
|
+ const cell = cells.find(c => c.id === id)
|
|
|
+ if (!cell || !cell.occupied) {
|
|
|
+ if (approach === 'a' && selectMode) toast('空格不可出库')
|
|
|
+ else if (approach !== 'a') toast('空格不可出库')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 方案 A:非选择模式 → 模拟打开详情(提示)
|
|
|
+ if (approach === 'a' && !selectMode) {
|
|
|
+ toast(`打开储物格明细 — ${cell.name}(现有行为)`)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (selected.has(id)) selected.delete(id)
|
|
|
+ else selected.add(id)
|
|
|
+ render()
|
|
|
+ }
|
|
|
+
|
|
|
+ function selectAllOccupied() {
|
|
|
+ cells.filter(c => c.occupied).forEach(c => selected.add(c.id))
|
|
|
+ render()
|
|
|
+ }
|
|
|
+
|
|
|
+ function selectRow(row) {
|
|
|
+ cells.filter(c => c.row === row && c.occupied).forEach(c => selected.add(c.id))
|
|
|
+ render()
|
|
|
+ }
|
|
|
+
|
|
|
+ function clearSelection() {
|
|
|
+ selected.clear()
|
|
|
+ render()
|
|
|
+ }
|
|
|
+
|
|
|
+ function openConfirm() {
|
|
|
+ if (!selected.size) return
|
|
|
+ const list = [...selected].map(id => {
|
|
|
+ const c = cells.find(x => x.id === id)
|
|
|
+ return `<div>${id} ${c.name} ${c.type}</div>`
|
|
|
+ }).join('')
|
|
|
+ document.getElementById('confirmText').textContent =
|
|
|
+ `确认将选中的 ${selected.size} 个储物格物品出库?`
|
|
|
+ document.getElementById('confirmList').innerHTML = list
|
|
|
+ document.getElementById('mask').classList.add('show')
|
|
|
+ }
|
|
|
+
|
|
|
+ function closeConfirm() {
|
|
|
+ document.getElementById('mask').classList.remove('show')
|
|
|
+ }
|
|
|
+
|
|
|
+ function doOutbound() {
|
|
|
+ const ids = [...selected]
|
|
|
+ cells.forEach(c => {
|
|
|
+ if (ids.includes(c.id)) {
|
|
|
+ c.occupied = false
|
|
|
+ c.name = ''
|
|
|
+ c.type = ''
|
|
|
+ }
|
|
|
+ })
|
|
|
+ selected.clear()
|
|
|
+ if (approach === 'a') selectMode = false
|
|
|
+ closeConfirm()
|
|
|
+ render()
|
|
|
+ toast(`出库成功,共 ${ids.length} 项`)
|
|
|
+ }
|
|
|
+
|
|
|
+ function toast(msg) {
|
|
|
+ const el = document.getElementById('toast')
|
|
|
+ el.textContent = msg
|
|
|
+ el.classList.add('show')
|
|
|
+ clearTimeout(toast._t)
|
|
|
+ toast._t = setTimeout(() => el.classList.remove('show'), 1800)
|
|
|
+ }
|
|
|
+
|
|
|
+ // reset helper for demo
|
|
|
+ window.resetDemo = function () {
|
|
|
+ cells = JSON.parse(JSON.stringify(initialCells))
|
|
|
+ selected.clear()
|
|
|
+ selectMode = approach === 'b'
|
|
|
+ render()
|
|
|
+ toast('已重置货架数据')
|
|
|
+ }
|
|
|
+
|
|
|
+ render()
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+</html>
|