| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666 |
- <!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>
|