feat(web-admin): add batch outbound selection state and toolbar

Wire selected slots map, confirm dialog, and serial remove occupancy calls on storage shelf.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-28 19:13:26 +08:00
1 parent 0a055557cb
commit 789c6443f3
1 file changed
+160 -14
@@ -121,21 +121,26 @@
</div>
<div class="storage-toolbar">
<el-button
type="primary"
:disabled="slotList.length > 0"
@click="batchDialogVisible = true"
>
<template v-if="slotList.length > 0">
<span class="storage-toolbar__selected">已选 {{ selectedOutboundCount }} 项</span>
<el-button
type="primary"
:disabled="selectedOutboundCount === 0"
:loading="batchOutboundLoading"
@click="handleBatchOutbound"
>
<el-icon><Top /></el-icon>
出库已选
</el-button>
<el-button class="btn-ghost" :loading="clearSlotsLoading" @click="handleClearSlots">
<el-icon><Delete /></el-icon>
清空格子
</el-button>
</template>
<el-button v-else type="primary" @click="batchDialogVisible = true">
<el-icon><Plus /></el-icon>
批量生成格子
</el-button>
<el-button
v-if="slotList.length > 0"
class="btn-ghost"
:loading="clearSlotsLoading"
@click="handleClearSlots"
>
清空格子
</el-button>
</div>
<div v-if="slotList.length === 0" class="storage-empty-cells">
@@ -1048,7 +1053,7 @@
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, Top } from '@element-plus/icons-vue'
import { Plus, Close, Check, Top, Delete } from '@element-plus/icons-vue'
import {
fetchStorageFacilityList,
fetchCreateStorageFacility,
@@ -1229,6 +1234,7 @@
}
const handleNodeClick = (data: any) => {
clearOutboundSelection()
selectedNode.value = data
resetSlotFilter()
if (data.isLeaf === 1) {
@@ -1243,9 +1249,11 @@
try {
const res: any = await fetchStorageSlotList({ facilityNodeId })
slotList.value = res?.list || res || []
pruneOutboundSelection(slotList.value)
} catch (error) {
console.error('加载格子数据失败:', error)
slotList.value = []
pruneOutboundSelection(slotList.value)
} finally {
slotLoading.value = false
}
@@ -1254,6 +1262,76 @@
// ==================== 右侧格子检索 ====================
const slotFilterKeyword = ref('')
type OutboundSelection = {
slotId: string
occupancyId: string
slotCode: string
itemName: string
}
const selectedOutboundMap = ref<Map<string, OutboundSelection>>(new Map())
const batchOutboundLoading = ref(false)
const selectedOutboundCount = computed(() => selectedOutboundMap.value.size)
function slotDisplayItemName(cell: any): string {
return (
cell?.storedCellProduct?.productName ||
cell?.storedItemName ||
cell?.storedItemCode ||
'—'
)
}
function isSlotSelectableForOutbound(cell: any): boolean {
return cell?.slotStatus === '占用' && !!(cell?.storedOccupancyId || '').toString().trim()
}
// Task 3 shelf-cell checkbox UI will call these helpers
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function isSlotOutboundSelected(slotId: string): boolean {
return selectedOutboundMap.value.has(slotId)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function toggleSlotOutboundSelect(cell: any, ev?: Event) {
ev?.stopPropagation?.()
if (!isSlotSelectableForOutbound(cell)) return
const id = cell.id
const next = new Map(selectedOutboundMap.value)
if (next.has(id)) {
next.delete(id)
} else {
next.set(id, {
slotId: id,
occupancyId: String(cell.storedOccupancyId),
slotCode: cell.fullSlotCode || cell.slotCode || id,
itemName: slotDisplayItemName(cell)
})
}
selectedOutboundMap.value = next
}
function clearOutboundSelection() {
selectedOutboundMap.value = new Map()
}
function pruneOutboundSelection(slots: any[]) {
const byId = new Map(slots.map((s) => [s.id, s]))
const next = new Map<string, OutboundSelection>()
for (const [slotId, sel] of selectedOutboundMap.value) {
const cell = byId.get(slotId)
if (cell && isSlotSelectableForOutbound(cell)) {
next.set(slotId, {
...sel,
occupancyId: String(cell.storedOccupancyId),
slotCode: cell.fullSlotCode || cell.slotCode || sel.slotCode,
itemName: slotDisplayItemName(cell)
})
}
}
selectedOutboundMap.value = next
}
const slotFilterActive = computed(() => !!slotFilterKeyword.value.trim())
function slotMatchesFilter(slot: any): boolean {
@@ -1284,6 +1362,62 @@
slotFilterKeyword.value = ''
}
watch(
() => [slotFilterKeyword.value, slotList.value] as const,
() => {
const visibleIds = new Set(filteredSlotList.value.map((s) => s.id))
const next = new Map<string, OutboundSelection>()
for (const [id, sel] of selectedOutboundMap.value) {
if (!visibleIds.has(id)) continue
const cell = slotList.value.find((s) => s.id === id)
if (cell && isSlotSelectableForOutbound(cell)) next.set(id, sel)
}
selectedOutboundMap.value = next
}
)
const handleBatchOutbound = async () => {
const items = [...selectedOutboundMap.value.values()]
if (!items.length) return
const preview = items
.slice(0, 20)
.map((i) => `${i.slotCode} ${i.itemName}`)
.join('\n')
const more =
items.length > 20 ? `\n…等共 ${items.length} 项` : items.length > 1 ? `\n共 ${items.length} 项` : ''
try {
await ElMessageBox.confirm(
`确认将以下储物格物品出库?\n\n${preview}${more}`,
'批量出库确认',
{ type: 'warning', confirmButtonText: '确认出库', cancelButtonText: '取消' }
)
} catch {
return
}
batchOutboundLoading.value = true
let done = 0
try {
for (const item of items) {
try {
await fetchRemoveOccupancy(item.occupancyId)
done += 1
} catch {
if (done > 0) {
ElMessage.warning(`已出库 ${done} 项,后续项失败已中止`)
}
if (selectedNode.value?.id) await loadSlots(selectedNode.value.id)
pruneOutboundSelection(slotList.value)
return
}
}
ElMessage.success(`出库成功,共 ${done} 项`)
clearOutboundSelection()
if (selectedNode.value?.id) await loadSlots(selectedNode.value.id)
} finally {
batchOutboundLoading.value = false
}
}
// ==================== 货架行计算(按 order_num 布局分组) ====================
const batchForm = reactive({
rows: 4,
@@ -1794,6 +1928,7 @@
const res: any = await fetchClearStorageSlots({ facilityNodeId: selectedNode.value.id })
const clearedCount = Number(res?.clearedCount ?? 0)
ElMessage.success(clearedCount > 0 ? `已清空 ${clearedCount} 个格子` : '已清空所有格子')
clearOutboundSelection()
await loadSlots(selectedNode.value.id)
} catch (error: any) {
if (error !== 'cancel') console.error('清空失败:', error)
@@ -2060,6 +2195,11 @@
})
await fetchRemoveOccupancy(row.id)
ElMessage.success('出库成功')
if (currentSlot.value?.id) {
const next = new Map(selectedOutboundMap.value)
next.delete(currentSlot.value.id)
selectedOutboundMap.value = next
}
loadOccupancies()
loadHistory()
if (selectedNode.value?.id) loadSlots(selectedNode.value.id)
@@ -2272,6 +2412,12 @@
align-items: center;
gap: 8px;
margin-bottom: 16px;
&__selected {
font-size: 13px;
color: var(--mint-text-muted, #64748b);
margin-right: 4px;
}
}
.storage-empty-cells {