Quellcode durchsuchen

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>
zlwu vor 3 Monaten
Ursprung
Commit
789c6443f3
1 geänderte Dateien mit 160 neuen und 14 gelöschten Zeilen
  1. 160 14
      web-admin/src/views/operations/storage-facility/index.vue

+ 160 - 14
web-admin/src/views/operations/storage-facility/index.vue

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