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:
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 {
|
||||
|
||||
Reference in new issue
Block a user