|
|
@@ -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 {
|