Ver Fonte

feat(warehouse-map): 支持多库区和多楼层筛选与展示

handy há 1 semana atrás
pai
commit
1b5f650577
2 ficheiros alterados com 405 adições e 79 exclusões
  1. 393 75
      src/App.vue
  2. 12 4
      src/components/WarehouseMap.vue

+ 393 - 75
src/App.vue

@@ -177,38 +177,63 @@
             <option value="N">无</option>
           </select>
         </label>
-        <label class="filter-item">
-          <select
-            ref="zoneSelectRef"
-            v-model="selectedZoneId"
-            :class="['level-select', { 'level-select-placeholder': !selectedZoneId }]"
+        <div
+          ref="zoneDropdownRef"
+          class="multi-select"
+        >
+          <button
+            class="level-select multi-select-trigger"
+            :class="{ 'level-select-placeholder': selectedZoneIds.length === 0 }"
             :style="{ width: selectWidths.zone ? `${selectWidths.zone}px` : 'auto' }"
+            type="button"
+            @click.stop="toggleZoneDropdown"
           >
-            <option value="">库区</option>
-            <option
+            {{ zoneLabel }}
+          </button>
+          <div
+            v-if="zoneDropdownVisible"
+            class="multi-select-menu"
+          >
+            <button
               v-for="zoneId in zoneOptions"
               :key="zoneId"
-              :value="zoneId"
+              class="multi-select-option"
+              type="button"
+              @click.stop="toggleZone(zoneId)"
             >
-              {{ zoneId }}
-            </option>
-          </select>
-        </label>
-        <select
-          ref="levelSelectRef"
-          v-model.number="currentLevel"
-          class="level-select level-select-floor"
-          :style="{ width: selectWidths.level ? `${selectWidths.level}px` : 'auto' }"
-          @change="handleLevelChange"
+              <span class="multi-select-check">{{ selectedZoneIds.includes(zoneId) ? '✓' : '' }}</span>
+              <span>{{ zoneId }}</span>
+            </button>
+          </div>
+        </div>
+        <div
+          ref="levelDropdownRef"
+          class="multi-select"
         >
-          <option
-            v-for="level in levelRange"
-            :key="level"
-            :value="level"
+          <button
+            class="level-select multi-select-trigger"
+            :style="{ width: selectWidths.level ? `${selectWidths.level}px` : 'auto' }"
+            type="button"
+            @click.stop="toggleLevelDropdown"
+          >
+            {{ levelLabel }}
+          </button>
+          <div
+            v-if="levelDropdownVisible"
+            class="multi-select-menu"
           >
-            {{ level }}层
-          </option>
-        </select>
+            <button
+              v-for="level in levelRange"
+              :key="level"
+              class="multi-select-option"
+              type="button"
+              @click.stop="toggleLevel(level)"
+            >
+              <span class="multi-select-check">{{ selectedLevels.includes(level) ? '✓' : '' }}</span>
+              <span>{{ level }}层</span>
+            </button>
+          </div>
+        </div>
         <button
           class="selection-btn"
           :class="{ 'selection-btn-active': selectionMode }"
@@ -379,26 +404,77 @@
         class="map-container"
       >
         <WorkingHighlight>
-          <WarehouseMap
-            :locations="locations"
-            :current-level="currentLevel"
-            :selected-category="selectedCategory"
-            :selected-location-attribute="selectedLocationAttribute"
-            :selected-has-container="selectedHasContainer"
-            :selected-zone-id="selectedZoneId"
-            :loc-group-keyword="appliedLocGroupKeyword"
-            :location-id-keyword="appliedLocationIdKeyword"
-            :wcs-location-id-keyword="appliedWcsLocationIdKeyword"
-            :show-group-border="showGroupBorder"
-            :show-tooltip="showTooltip"
-            :category-color-visibility="categoryColorVisibility"
-            :theme-mode="isLightTheme ? 'light' : 'dark'"
-            :selection-mode="selectionMode"
-            @select-loc-group="handleSelectLocGroup"
-            @select-location-id="handleSelectLocationId"
-            @selection-complete="handleSelectionComplete"
-            @create-location-sql="openCreateLocationSqlModal"
-          />
+          <div
+            class="floor-map-grid"
+            :class="floorMapGridClass"
+          >
+            <section
+              v-for="level in selectedLevels"
+              :key="level"
+              :class="[
+                'floor-map-panel',
+                {
+                  'floor-map-panel-hidden': expandedLevel !== null && expandedLevel !== level,
+                  'floor-map-panel-expanded': expandedLevel === level
+                }
+              ]"
+            >
+              <div class="floor-map-title">
+                <span>{{ level }}层</span>
+                <button
+                  v-if="selectedLevels.length > 1"
+                  class="floor-map-expand-btn"
+                  type="button"
+                  :title="expandedLevel === level ? '收起' : '展开'"
+                  @click="toggleExpandedLevel(level)"
+                >
+                  <svg
+                    v-if="expandedLevel === level"
+                    viewBox="0 0 16 16"
+                    aria-hidden="true"
+                    class="floor-map-expand-icon"
+                  >
+                    <path
+                      d="M6.5 3.25v3.25H3.25V5h1.7L2.72 2.78l1.06-1.06L6 3.95v-1.7h1.5zm2.98 9.5V9.5h3.27V11h-1.72l2.25 2.22-1.06 1.06L10 12.08v1.67H8.48zM3.25 9.5H6.5v3.25H5v-1.7l-2.22 2.23-1.06-1.06L3.95 10H2.25V9.5zm9.5-3H9.5V3.25H11v1.7l2.22-2.23 1.06 1.06L12.05 6h1.7v.5z"
+                      fill="currentColor"
+                    />
+                  </svg>
+                  <svg
+                    v-else
+                    viewBox="0 0 16 16"
+                    aria-hidden="true"
+                    class="floor-map-expand-icon"
+                  >
+                    <path
+                      d="M2.5 6.5V2.5h4V4H5.05l2.22 2.22-1.05 1.06L4 5.05V6.5H2.5zm7-4h4v4H12V5.05L9.78 7.28 8.72 6.22 10.95 4H9.5V2.5zm-7 7H4v1.45l2.22-2.23 1.06 1.06L5.05 12H6.5v1.5h-4v-4zm9.5 0h1.5v4h-4V12h1.45L8.72 9.78l1.06-1.06L12 10.95V9.5z"
+                      fill="currentColor"
+                    />
+                  </svg>
+                </button>
+              </div>
+              <WarehouseMap
+                :locations="locationsByLevel[level] || []"
+                :current-level="level"
+                :selected-category="selectedCategory"
+                :selected-location-attribute="selectedLocationAttribute"
+                :selected-has-container="selectedHasContainer"
+                :selected-zone-ids="selectedZoneIds"
+                :loc-group-keyword="appliedLocGroupKeyword"
+                :location-id-keyword="appliedLocationIdKeyword"
+                :wcs-location-id-keyword="appliedWcsLocationIdKeyword"
+                :container-code-keyword="appliedContainerCodeKeyword"
+                :show-group-border="showGroupBorder"
+                :show-tooltip="showTooltip"
+                :category-color-visibility="categoryColorVisibility"
+                :theme-mode="isLightTheme ? 'light' : 'dark'"
+                :selection-mode="selectionMode"
+                @select-loc-group="handleSelectLocGroup"
+                @select-location-id="handleSelectLocationId"
+                @selection-complete="handleSelectionComplete"
+                @create-location-sql="openCreateLocationSqlModal"
+              />
+            </section>
+          </div>
         </WorkingHighlight>
       </div>
     </main>
@@ -432,19 +508,33 @@ import {
   removeToken
 } from './utils/auth'
 
-const LEVEL_STORAGE_KEY = 'warehouse-map.current-level'
+const LEVEL_STORAGE_KEY = 'warehouse-map.selected-levels'
 const REFRESH_INTERVAL_STORAGE_KEY = 'warehouse-map.refresh-interval-ms'
-const getInitialLevel = () => {
-  const savedLevel = window.localStorage.getItem(LEVEL_STORAGE_KEY)
-  const parsedLevel = savedLevel ? Number(savedLevel) : NaN
+const getInitialLevels = () => {
+  const savedLevels = window.localStorage.getItem(LEVEL_STORAGE_KEY)
+  const parsedLevels = savedLevels
+    ? savedLevels
+        .split(',')
+        .map((level) => Number(level))
+        .filter(
+          (level) =>
+            Number.isInteger(level) && level >= config.minLevel && level <= config.maxLevel
+        )
+    : []
+
+  if (parsedLevels.length > 0) {
+    return [...new Set(parsedLevels)].sort((a, b) => a - b)
+  }
+
+  const legacyLevel = Number(window.localStorage.getItem('warehouse-map.current-level'))
   if (
-    Number.isInteger(parsedLevel) &&
-    parsedLevel >= config.minLevel &&
-    parsedLevel <= config.maxLevel
+    Number.isInteger(legacyLevel) &&
+    legacyLevel >= config.minLevel &&
+    legacyLevel <= config.maxLevel
   ) {
-    return parsedLevel
+    return [legacyLevel]
   }
-  return config.minLevel
+  return [config.minLevel]
 }
 
 const getInitialRefreshInterval = () => {
@@ -456,7 +546,7 @@ const getInitialRefreshInterval = () => {
   return config.refreshInterval
 }
 
-const currentLevel = ref(getInitialLevel())
+const selectedLevels = ref<number[]>(getInitialLevels())
 const locations = ref<LocationResourceDataVO[]>([])
 const loading = ref(false)
 const refreshing = ref(false)
@@ -477,12 +567,12 @@ const isLightTheme = ref(getInitialTheme() === 'light')
 const selectedCategory = ref('')
 const selectedLocationAttribute = ref<LocationAttributeCode | ''>('')
 const selectedHasContainer = ref<'Y' | 'N' | ''>('')
-const selectedZoneId = ref('')
+const selectedZoneIds = ref<string[]>([])
 const categorySelectRef = ref<HTMLSelectElement | null>(null)
 const locationAttributeSelectRef = ref<HTMLSelectElement | null>(null)
 const hasContainerSelectRef = ref<HTMLSelectElement | null>(null)
-const zoneSelectRef = ref<HTMLSelectElement | null>(null)
-const levelSelectRef = ref<HTMLSelectElement | null>(null)
+const zoneDropdownRef = ref<HTMLElement | null>(null)
+const levelDropdownRef = ref<HTMLElement | null>(null)
 const selectWidths = ref({
   category: 0,
   attribute: 0,
@@ -499,10 +589,14 @@ const locationKeywordInput = ref('')
 const appliedLocGroupKeyword = ref('')
 const appliedLocationIdKeyword = ref('')
 const appliedWcsLocationIdKeyword = ref('')
+const appliedContainerCodeKeyword = ref('')
 const showGroupBorder = ref(false)
 const showTooltip = ref(true)
 const selectionMode = ref(false)
 const capacityMonitorVisible = ref(false)
+const zoneDropdownVisible = ref(false)
+const levelDropdownVisible = ref(false)
+const expandedLevel = ref<number | null>(null)
 const createLocationSqlModalVisible = ref(false)
 const createLocationSqlPayload = ref({
   floor: 1,
@@ -557,6 +651,13 @@ const categoryOptions = computed(() => {
   return [...new Set(locations.value.map((loc) => loc.category).filter(Boolean))].sort()
 })
 
+const locationsByLevel = computed<Record<number, LocationResourceDataVO[]>>(() => {
+  return selectedLevels.value.reduce<Record<number, LocationResourceDataVO[]>>((result, level) => {
+    result[level] = locations.value.filter((loc) => loc.locLevel === level)
+    return result
+  }, {})
+})
+
 const hasContainer = (containerCode: string | null) => {
   return Boolean(containerCode && containerCode.trim())
 }
@@ -585,7 +686,8 @@ const isLocationMatchedByFilters = (loc: LocationResourceDataVO) => {
     !selectedHasContainer.value ||
     (selectedHasContainer.value === 'Y' && hasContainerFlag) ||
     (selectedHasContainer.value === 'N' && !hasContainerFlag)
-  const matchedZoneId = !selectedZoneId.value || String(loc.zoneId || '') === selectedZoneId.value
+  const matchedZoneId =
+    selectedZoneIds.value.length === 0 || selectedZoneIds.value.includes(String(loc.zoneId || ''))
   const normalizedLocGroupKeyword = appliedLocGroupKeyword.value.trim().toUpperCase()
   const matchedLocGroup =
     !normalizedLocGroupKeyword ||
@@ -604,6 +706,12 @@ const isLocationMatchedByFilters = (loc: LocationResourceDataVO) => {
     String(loc.wcsLocationId || '')
       .toUpperCase()
       .includes(normalizedWcsLocationIdKeyword)
+  const normalizedContainerCodeKeyword = appliedContainerCodeKeyword.value.trim().toUpperCase()
+  const matchedContainerCode =
+    !normalizedContainerCodeKeyword ||
+    String(loc.containerCode || '')
+      .toUpperCase()
+      .includes(normalizedContainerCodeKeyword)
 
   return (
     matchedCategory &&
@@ -612,7 +720,8 @@ const isLocationMatchedByFilters = (loc: LocationResourceDataVO) => {
     matchedZoneId &&
     matchedLocGroup &&
     matchedLocationId &&
-    matchedWcsLocationId
+    matchedWcsLocationId &&
+    matchedContainerCode
   )
 }
 
@@ -712,8 +821,17 @@ const hasContainerLabel = computed(() => {
   if (selectedHasContainer.value === 'N') return '无'
   return '容器'
 })
-const zoneLabel = computed(() => selectedZoneId.value || '库区')
-const levelLabel = computed(() => `${currentLevel.value}层`)
+const zoneLabel = computed(() =>
+  selectedZoneIds.value.length > 1 ? `${selectedZoneIds.value.length}个库区` : selectedZoneIds.value[0] || '库区'
+)
+const levelLabel = computed(() =>
+  selectedLevels.value.length === levelRange.value.length
+    ? '全部楼层'
+    : selectedLevels.value.map((level) => `${level}层`).join(',')
+)
+const floorMapGridClass = computed(() =>
+  expandedLevel.value === null ? `floor-map-grid-${selectedLevels.value.length}` : 'floor-map-grid-expanded'
+)
 
 const toggleCategoryColorVisibility = (category: 'A' | 'B' | 'C') => {
   categoryColorVisibility.value = {
@@ -752,8 +870,8 @@ const updateSelectWidths = () => {
   updateSelectWidth('category', categorySelectRef.value, categoryLabel.value)
   updateSelectWidth('attribute', locationAttributeSelectRef.value, locationAttributeLabel.value)
   updateSelectWidth('container', hasContainerSelectRef.value, hasContainerLabel.value)
-  updateSelectWidth('zone', zoneSelectRef.value, zoneLabel.value)
-  updateSelectWidth('level', levelSelectRef.value, levelLabel.value)
+  selectWidths.value.zone = Math.max(Math.ceil(measureTextWidth(zoneLabel.value, '12px sans-serif') + 40), 58)
+  selectWidths.value.level = Math.max(Math.ceil(measureTextWidth(levelLabel.value, '12px sans-serif') + 40), 58)
 }
 
 const updateFillRateTooltipPosition = (event: MouseEvent) => {
@@ -840,22 +958,31 @@ const applyLocationKeywordFilter = () => {
   const keyword = locationKeywordInput.value.trim()
   const hyphenCount = countHyphen(keyword)
 
-  if (hyphenCount === 3) {
+  if (/^(TP|HJ)/i.test(keyword)) {
+    appliedContainerCodeKeyword.value = keyword
+    appliedLocGroupKeyword.value = ''
+    appliedLocationIdKeyword.value = ''
+    appliedWcsLocationIdKeyword.value = ''
+  } else if (hyphenCount === 3) {
     appliedLocationIdKeyword.value = keyword
     appliedLocGroupKeyword.value = ''
     appliedWcsLocationIdKeyword.value = ''
+    appliedContainerCodeKeyword.value = ''
   } else if (hyphenCount === 2) {
     appliedLocGroupKeyword.value = keyword
     appliedLocationIdKeyword.value = ''
     appliedWcsLocationIdKeyword.value = ''
+    appliedContainerCodeKeyword.value = ''
   } else if (keyword.length >= 13 && hyphenCount === 0) {
     appliedWcsLocationIdKeyword.value = keyword
     appliedLocationIdKeyword.value = ''
     appliedLocGroupKeyword.value = ''
+    appliedContainerCodeKeyword.value = ''
   } else {
     appliedLocGroupKeyword.value = ''
     appliedLocationIdKeyword.value = ''
     appliedWcsLocationIdKeyword.value = ''
+    appliedContainerCodeKeyword.value = ''
   }
 }
 
@@ -864,6 +991,7 @@ const clearLocationKeywordFilter = () => {
   appliedLocGroupKeyword.value = ''
   appliedLocationIdKeyword.value = ''
   appliedWcsLocationIdKeyword.value = ''
+  appliedContainerCodeKeyword.value = ''
 }
 
 const handleGroupBorderToggle = (event: Event) => {
@@ -878,6 +1006,28 @@ const toggleCapacityMonitorVisible = () => {
   capacityMonitorVisible.value = !capacityMonitorVisible.value
 }
 
+const toggleExpandedLevel = (level: number) => {
+  expandedLevel.value = expandedLevel.value === level ? null : level
+}
+
+const toggleZoneDropdown = () => {
+  zoneDropdownVisible.value = !zoneDropdownVisible.value
+  levelDropdownVisible.value = false
+}
+
+const toggleLevelDropdown = () => {
+  levelDropdownVisible.value = !levelDropdownVisible.value
+  zoneDropdownVisible.value = false
+}
+
+const toggleZone = (zoneId: string) => {
+  if (selectedZoneIds.value.includes(zoneId)) {
+    selectedZoneIds.value = selectedZoneIds.value.filter((selectedZoneId) => selectedZoneId !== zoneId)
+    return
+  }
+  selectedZoneIds.value = [...selectedZoneIds.value, zoneId].sort()
+}
+
 const handleSelectionComplete = () => {
   selectionMode.value = false
 }
@@ -903,6 +1053,10 @@ const refreshCountdownText = computed(() => {
   return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
 })
 
+const saveSelectedLevels = () => {
+  window.localStorage.setItem(LEVEL_STORAGE_KEY, selectedLevels.value.join(','))
+}
+
 const loadLocationData = async (options: { silent?: boolean } = {}) => {
   const { silent = false } = options
   if (!isAuthenticated()) {
@@ -920,11 +1074,15 @@ const loadLocationData = async (options: { silent?: boolean } = {}) => {
 
   let loaded = false
   try {
-    const data = await fetchLocationData({
-      warehouse: config.warehouse,
-      locLevel: currentLevel.value
-    })
-    locations.value = data
+    const levelDataList = await Promise.all(
+      selectedLevels.value.map((level) =>
+        fetchLocationData({
+          warehouse: config.warehouse,
+          locLevel: level
+        })
+      )
+    )
+    locations.value = levelDataList.flat()
     hasLoadedOnce.value = true
     loaded = true
   } catch (err: unknown) {
@@ -960,8 +1118,16 @@ const scheduleNextRefresh = () => {
   }, refreshIntervalMs.value)
 }
 
-const handleLevelChange = () => {
-  window.localStorage.setItem(LEVEL_STORAGE_KEY, String(currentLevel.value))
+const toggleLevel = (level: number) => {
+  if (selectedLevels.value.includes(level)) {
+    if (selectedLevels.value.length === 1) {
+      return
+    }
+    selectedLevels.value = selectedLevels.value.filter((selectedLevel) => selectedLevel !== level)
+  } else {
+    selectedLevels.value = [...selectedLevels.value, level].sort((a, b) => a - b)
+  }
+  saveSelectedLevels()
   loadLocationData()
   scheduleNextRefresh()
 }
@@ -993,11 +1159,20 @@ const handleRefreshContextMenu = async () => {
 }
 
 const handleDocumentClick = (event: MouseEvent) => {
+  const target = event.target as Node | null
+  if (target && zoneDropdownRef.value?.contains(target)) {
+    return
+  }
+  if (target && levelDropdownRef.value?.contains(target)) {
+    return
+  }
+  zoneDropdownVisible.value = false
+  levelDropdownVisible.value = false
+
   if (!showRefreshPopover.value) {
     return
   }
 
-  const target = event.target as Node | null
   if (target && refreshControlRef.value?.contains(target)) {
     return
   }
@@ -1108,6 +1283,12 @@ watch([categoryLabel, locationAttributeLabel, hasContainerLabel, zoneLabel, leve
   })
 })
 
+watch(selectedLevels, (levels) => {
+  if (expandedLevel.value !== null && !levels.includes(expandedLevel.value)) {
+    expandedLevel.value = null
+  }
+})
+
 onBeforeUnmount(() => {
   if (refreshTimer !== null) {
     window.clearTimeout(refreshTimer)
@@ -1481,8 +1662,55 @@ onBeforeUnmount(() => {
   color: var(--label-text);
 }
 
-.level-select-floor {
-  min-width: 0;
+.multi-select {
+  position: relative;
+  display: inline-flex;
+}
+
+.multi-select-trigger {
+  text-align: left;
+}
+
+.multi-select-menu {
+  position: absolute;
+  top: calc(100% + 6px);
+  left: 0;
+  z-index: 40;
+  min-width: 100%;
+  padding: 4px;
+  border: 1px solid var(--popover-border);
+  border-radius: 4px;
+  background: var(--popover-bg);
+  box-shadow: var(--panel-shadow);
+}
+
+.multi-select-option {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 8px;
+  border: none;
+  border-radius: 3px;
+  background: transparent;
+  color: var(--input-text);
+  cursor: pointer;
+  font-size: 12px;
+  line-height: 1;
+  white-space: nowrap;
+  text-align: left;
+}
+
+.multi-select-option:hover {
+  background: var(--input-hover-bg);
+}
+
+.multi-select-check {
+  width: 12px;
+  color: var(--input-text);
+  font-size: 11px;
+  line-height: 1;
+  text-align: center;
 }
 
 .filter-input-wrap {
@@ -1769,4 +1997,94 @@ onBeforeUnmount(() => {
   width: 100%;
   height: 100%;
 }
+
+.floor-map-grid {
+  display: grid;
+  gap: 10px;
+  width: 100%;
+  height: 100%;
+  min-height: 0;
+}
+
+.floor-map-grid-1 {
+  grid-template-columns: minmax(0, 1fr);
+}
+
+.floor-map-grid-expanded {
+  grid-template-columns: minmax(0, 1fr);
+}
+
+.floor-map-grid-2 {
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.floor-map-grid-3,
+.floor-map-grid-4 {
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  grid-template-rows: repeat(2, minmax(0, 1fr));
+}
+
+.floor-map-grid-5,
+.floor-map-grid-6 {
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  grid-template-rows: repeat(2, minmax(0, 1fr));
+}
+
+.floor-map-panel {
+  min-width: 0;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  border: 1px solid var(--header-border);
+  background: var(--bg);
+}
+
+.floor-map-panel > .warehouse-map {
+  flex: 1;
+  min-height: 0;
+  height: auto;
+}
+
+.floor-map-panel-hidden {
+  display: none;
+}
+
+.floor-map-title {
+  flex: 0 0 auto;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 8px;
+  padding: 6px 8px;
+  border-bottom: 1px solid var(--header-border);
+  color: var(--text-muted);
+  font-size: 12px;
+  font-weight: 700;
+  line-height: 1;
+}
+
+.floor-map-expand-btn {
+  flex: 0 0 auto;
+  width: 20px;
+  height: 20px;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0;
+  border: 1px solid var(--btn-neutral-border);
+  border-radius: 4px;
+  background: var(--btn-neutral-bg);
+  color: var(--btn-neutral-text);
+  cursor: pointer;
+}
+
+.floor-map-expand-btn:hover {
+  background: var(--btn-neutral-hover-bg);
+  border-color: var(--btn-neutral-hover-border);
+}
+
+.floor-map-expand-icon {
+  width: 12px;
+  height: 12px;
+}
 </style>

+ 12 - 4
src/components/WarehouseMap.vue

@@ -140,10 +140,11 @@ interface Props {
   categoryColorVisibility: Record<string, boolean>
   selectedLocationAttribute: string
   selectedHasContainer: string
-  selectedZoneId: string
+  selectedZoneIds: string[]
   locGroupKeyword: string
   locationIdKeyword: string
   wcsLocationIdKeyword: string
+  containerCodeKeyword: string
   showGroupBorder: boolean
   showTooltip: boolean
   selectionMode?: boolean
@@ -411,7 +412,8 @@ const isCellMatched = (cell: GridCell) => {
     !props.selectedHasContainer ||
     (props.selectedHasContainer === 'Y' && hasContainer) ||
     (props.selectedHasContainer === 'N' && !hasContainer)
-  const matchedZoneId = !props.selectedZoneId || String(cell.zoneId || '') === props.selectedZoneId
+  const matchedZoneId =
+    props.selectedZoneIds.length === 0 || props.selectedZoneIds.includes(String(cell.zoneId || ''))
   const normalizedKeyword = props.locGroupKeyword.trim().toUpperCase()
   const matchedLocGroup =
     !normalizedKeyword || cell.locGroup1.toUpperCase().includes(normalizedKeyword)
@@ -423,6 +425,10 @@ const isCellMatched = (cell: GridCell) => {
   const matchedWcsLocationId =
     !normalizedWcsLocationIdKeyword ||
     (cell.wcsLocationId || '').toUpperCase().includes(normalizedWcsLocationIdKeyword)
+  const normalizedContainerCodeKeyword = props.containerCodeKeyword.trim().toUpperCase()
+  const matchedContainerCode =
+    !normalizedContainerCodeKeyword ||
+    (cell.containerCode || '').toUpperCase().includes(normalizedContainerCodeKeyword)
 
   return (
     matchedCategory &&
@@ -431,7 +437,8 @@ const isCellMatched = (cell: GridCell) => {
     matchedZoneId &&
     matchedLocGroup &&
     matchedLocationId &&
-    matchedWcsLocationId
+    matchedWcsLocationId &&
+    matchedContainerCode
   )
 }
 
@@ -498,10 +505,11 @@ const shouldHideNonLocationCells = computed(() => {
     Boolean(props.selectedCategory) ||
     Boolean(props.selectedLocationAttribute) ||
     Boolean(props.selectedHasContainer) ||
-    Boolean(props.selectedZoneId) ||
+    props.selectedZoneIds.length > 0 ||
     Boolean(props.locGroupKeyword.trim()) ||
     Boolean(props.locationIdKeyword.trim()) ||
     Boolean(props.wcsLocationIdKeyword.trim()) ||
+    Boolean(props.containerCodeKeyword.trim()) ||
     Boolean(selectedRange.value)
   )
 })