handy пре 2 месеци
родитељ
комит
e54f631cac
2 измењених фајлова са 282 додато и 47 уклоњено
  1. 86 16
      src/components/WarehouseMap.vue
  2. 196 31
      src/components/warehouse-layout-enhancers.ts

+ 86 - 16
src/components/WarehouseMap.vue

@@ -15,7 +15,7 @@
           @click="handleCellClick(cell)"
           @contextmenu.prevent="handleCellContextMenu(cell)"
         >
-          <div v-if="cell && isCellVisible(cell)" class="cell-content">
+          <div v-if="isLocationCell(cell) && isCellVisible(cell)" class="cell-content">
             <div class="category-badge" :style="getCategoryStyle(cell)">
               {{ getHeatLabel(cell) }}
             </div>
@@ -39,6 +39,12 @@
               {{ cell.containerCode }}
             </div>
           </div>
+          <div
+            v-else-if="isSpecialCell(cell) && cell.label"
+            :class="['special-cell-label', `special-cell-label-${cell.type}`]"
+          >
+            {{ cell.label }}
+          </div>
         </div>
       </div>
       <div
@@ -57,7 +63,11 @@
 <script setup lang="ts">
 import { computed, onBeforeUnmount, onMounted, ref, watch, type CSSProperties } from 'vue'
 import type { LocationResourceDataVO } from '../types'
-import { applyWarehouseLayoutEnhancement } from './warehouse-layout-enhancers'
+import {
+  applyWarehouseLayoutEnhancement,
+  getWarehouseLayoutSpecialCells,
+  type WarehouseLayoutSpecialCell
+} from './warehouse-layout-enhancers'
 
 interface Props {
   locations: LocationResourceDataVO[]
@@ -87,6 +97,8 @@ interface GridCell extends LocationResourceDataVO {
   categoryMismatch: boolean
 }
 
+type MapCell = GridCell | WarehouseLayoutSpecialCell | null
+
 const props = defineProps<Props>()
 const emit = defineEmits<{
   (event: 'select-loc-group', locGroup1: string): void
@@ -203,6 +215,15 @@ const parsedLocations = computed<GridCell[]>(() => {
     })
 })
 
+const specialCellMap = computed(() => {
+  return new Map(
+    getWarehouseLayoutSpecialCells(props.currentLevel).map((cell) => [
+      `${cell.gridRow}-${cell.gridCol}`,
+      cell
+    ])
+  )
+})
+
 const locationMap = computed(() => {
   const map = new Map<string, GridCell>()
   parsedLocations.value.forEach((loc) => {
@@ -235,12 +256,24 @@ const gridMetrics = computed(() => {
 const gridData = computed(() => {
   if (!gridBounds.value) return []
 
-  // 构建网格:按行(X)和列(Y)排列
-  const grid: (GridCell | null)[] = []
+  // 构建网格:按行(X)和列(Y)排列,空位根据规则显示为过道或特殊区域。
+  const grid: MapCell[] = []
   for (let x = gridBounds.value.minX; x <= gridBounds.value.maxX; x++) {
     for (let y = gridBounds.value.minY; y <= gridBounds.value.maxY; y++) {
       const key = `${x}-${y}`
-      grid.push(locationMap.value.get(key) || null)
+      const locationCell = locationMap.value.get(key)
+      if (locationCell) {
+        grid.push(locationCell)
+        continue
+      }
+
+      const specialCell = specialCellMap.value.get(key)
+      if (specialCell) {
+        grid.push(specialCell)
+        continue
+      }
+
+      grid.push(null)
     }
   }
 
@@ -348,7 +381,16 @@ const isCellVisible = (cell: GridCell) => {
   return isCellMatched(cell) && isCellInSelectedRange(cell)
 }
 
-const getCellClass = (cell: GridCell | null) => {
+const isSpecialCell = (cell: MapCell): cell is WarehouseLayoutSpecialCell => {
+  return Boolean(cell && 'type' in cell)
+}
+
+const isLocationCell = (cell: MapCell): cell is GridCell => {
+  return Boolean(cell && !('type' in cell))
+}
+
+const getCellClass = (cell: MapCell) => {
+  if (isSpecialCell(cell)) return [cell.type]
   if (!cell) return ['aisle']
   if (!isCellVisible(cell)) return ['aisle']
 
@@ -455,8 +497,10 @@ const hasSameBorderNeighbor = (cell: GridCell) => {
     || isSameBorderNeighbor(cell, 0, 1)
 }
 
-const getCellStyle = (cell: GridCell | null): CSSProperties => {
-  if (!cell || !isCellVisible(cell) || !hasActiveBorder() || !hasSameBorderNeighbor(cell)) return {}
+const getCellStyle = (cell: MapCell): CSSProperties => {
+  if (!isLocationCell(cell) || !isCellVisible(cell) || !hasActiveBorder() || !hasSameBorderNeighbor(cell)) {
+    return {}
+  }
 
   const borderWidth = 'var(--group-outline-width, 2px)'
 
@@ -473,12 +517,12 @@ const clearSelectedRange = () => {
   selectedRange.value = null
 }
 
-const handleCellClick = (cell: GridCell | null) => {
+const handleCellClick = (cell: MapCell) => {
   if (suppressNextClick.value) {
     suppressNextClick.value = false
     return
   }
-  if (!cell || !isCellVisible(cell)) {
+  if (!isLocationCell(cell) || !isCellVisible(cell)) {
     if (selectedRange.value) {
       clearSelectedRange()
     }
@@ -487,8 +531,8 @@ const handleCellClick = (cell: GridCell | null) => {
   emit('select-loc-group', cell.locGroup1)
 }
 
-const handleCellContextMenu = (cell: GridCell | null) => {
-  if (!cell || !isCellVisible(cell)) return
+const handleCellContextMenu = (cell: MapCell) => {
+  if (!isLocationCell(cell) || !isCellVisible(cell)) return
   emit('select-location-id', cell.locationId)
 }
 
@@ -545,7 +589,7 @@ const updateTooltipPosition = (event: MouseEvent) => {
   }
 }
 
-const handleCellMouseEnter = (cell: GridCell | null, index: number, event: MouseEvent) => {
+const handleCellMouseEnter = (cell: MapCell, index: number, event: MouseEvent) => {
   if (selectionStart.value && (event.buttons & 1) === 1) {
     const point = getGridPointByIndex(index)
     if (point) {
@@ -558,7 +602,11 @@ const handleCellMouseEnter = (cell: GridCell | null, index: number, event: Mouse
     return
   }
 
-  if (!props.showTooltip) return
+  if (!props.showTooltip || !isLocationCell(cell)) {
+    tooltipVisible.value = false
+    hoveredCell.value = null
+    return
+  }
   hoveredCell.value = cell
   tooltipVisible.value = true
   updateTooltipPosition(event)
@@ -585,7 +633,7 @@ const handleWindowMouseUp = () => {
   if (didSelectionMove.value) {
     selectedRange.value = buildSelectionRange(selectionStart.value, selectionEnd.value)
     const selectedLocationIds = gridData.value
-      .filter((cell): cell is GridCell => Boolean(cell))
+      .filter(isLocationCell)
       .filter((cell) => isCellMatched(cell) && isCellInSelectedRange(cell))
       .map((cell) => cell.locationId)
 
@@ -698,6 +746,16 @@ onBeforeUnmount(() => {
   background: #18202a;
 }
 
+.grid-cell.wall {
+  background: #323841;
+  cursor: default;
+}
+
+.grid-cell.elevator {
+  background: #20303b;
+  cursor: default;
+}
+
 .grid-cell.category-a {
   background: rgba(0, 128, 0, 0.16);
 }
@@ -710,7 +768,7 @@ onBeforeUnmount(() => {
   background: rgba(255, 255, 0, 0.16);
 }
 
-.grid-cell:not(.aisle):hover {
+.grid-cell:not(.aisle):not(.wall):not(.elevator):hover {
   transform: scale(1.03);
   box-shadow: 0 0 12px rgba(223, 231, 239, 0.12);
   z-index: 10;
@@ -794,6 +852,18 @@ onBeforeUnmount(() => {
   text-overflow: ellipsis;
 }
 
+.special-cell-label {
+  font-size: calc(var(--cell-id-font-size, 11px) - 1px);
+  font-weight: 700;
+  color: rgba(237, 243, 247, 0.82);
+  letter-spacing: 1px;
+  user-select: none;
+}
+
+.special-cell-label-elevator {
+  color: rgba(221, 234, 244, 0.92);
+}
+
 .cell-tooltip {
   position: fixed;
   z-index: 1000;

+ 196 - 31
src/components/warehouse-layout-enhancers.ts

@@ -10,6 +10,18 @@ export interface WarehouseLayoutPosition {
   gridCol: number
 }
 
+export interface WarehouseLayoutWallCell {
+  gridRow: number
+  gridCol: number
+}
+
+export interface WarehouseLayoutSpecialCell {
+  gridRow: number
+  gridCol: number
+  type: 'wall' | 'elevator'
+  label?: string
+}
+
 type RowRange = readonly [number, number]
 
 interface RowScopedRule {
@@ -38,6 +50,21 @@ interface WarehouseLayoutRuleSet {
   globalColumnAisles: readonly GlobalColumnAisleRule[]
   localColumnAisles: readonly LocalColumnAisleRule[]
   localColumnShifts: readonly LocalColumnShiftRule[]
+  verticalWallBreaks?: readonly VerticalWallBreakRule[]
+  elevatorAreas?: readonly ElevatorAreaRule[]
+}
+
+interface VerticalWallBreakRule {
+  column: number
+  betweenRows: readonly [number, number]
+}
+
+interface ElevatorAreaRule {
+  leftColumn: number
+  rightColumn: number
+  upperRow: number
+  lowerRow: number
+  label: string
 }
 
 interface WarehouseLayoutEnhancer {
@@ -56,16 +83,19 @@ const FIRST_FLOOR_RULE_SET: WarehouseLayoutRuleSet = {
       breakpoint: 19,
       excludeRows: [8, 9, 10, 11]
     },
-    { breakpoint: 26 }
+    {
+      breakpoint: 26,
+      excludeRows: [8, 11]
+    }
   ],
   localColumnAisles: [
     {
-      rowRange: [0, 8],
+      rowRange: [0, 7],
       afterColumn: 7,
       gapCols: 1
     },
     {
-      rowRange: [11, 22],
+      rowRange: [12, 22],
       afterColumn: 7,
       gapCols: 1
     },
@@ -81,12 +111,7 @@ const FIRST_FLOOR_RULE_SET: WarehouseLayoutRuleSet = {
     },
     {
       rows: [8, 11],
-      afterColumn: 12,
-      gapCols: 1
-    },
-    {
-      rows: [8, 11],
-      afterColumn: 17,
+      afterColumn: 13,
       gapCols: 1
     },
     {
@@ -96,7 +121,7 @@ const FIRST_FLOOR_RULE_SET: WarehouseLayoutRuleSet = {
     },
     {
       rows: [8, 11],
-      afterColumn: 24,
+      afterColumn: 26,
       gapCols: 1
     },
     {
@@ -131,6 +156,40 @@ const FIRST_FLOOR_RULE_SET: WarehouseLayoutRuleSet = {
       startColumn: 20,
       shiftCols: 1
     }
+  ],
+  verticalWallBreaks: [
+    {
+      column: 0,
+      betweenRows: [19, 20]
+    },
+    {
+      column: 6,
+      betweenRows: [19, 20]
+    },
+    {
+      column: 13,
+      betweenRows: [19, 20]
+    },
+    {
+      column: 20,
+      betweenRows: [19, 20]
+    }
+  ],
+  elevatorAreas: [
+    {
+      leftColumn: 7,
+      rightColumn: 9,
+      upperRow: 8,
+      lowerRow: 11,
+      label: '提升机'
+    },
+    {
+      leftColumn: 18,
+      rightColumn: 20,
+      upperRow: 8,
+      lowerRow: 11,
+      label: '提升机'
+    }
   ]
 }
 
@@ -191,34 +250,45 @@ const sumLocalColumnShiftOffsets = (
     return total
   }, 0)
 
+const applyConfiguredRuleSet = (
+  currentPosition: WarehouseLayoutPosition,
+  parsedLocation: WarehouseLayoutParsedLocation,
+  ruleSet: WarehouseLayoutRuleSet
+) => {
+  const columnShiftCount = sumLocalColumnShiftOffsets(parsedLocation, ruleSet.localColumnShifts)
+  const effectiveColumn = parsedLocation.x + columnShiftCount
+  const gridColOffset =
+    columnShiftCount +
+    sumGlobalColumnAisleOffsets(
+      parsedLocation.y,
+      effectiveColumn,
+      ruleSet.globalColumnAisles
+    ) +
+    sumLocalColumnAisleOffsets(parsedLocation.y, effectiveColumn, ruleSet.localColumnAisles)
+  const gridRowOffset = sumRowAisleOffsets(parsedLocation.y, ruleSet.rowAisleBreakpoints)
+
+  return {
+    gridRow: currentPosition.gridRow + gridRowOffset,
+    gridCol: currentPosition.gridCol + gridColOffset
+  }
+}
+
 const createConfiguredEnhancer = (
   level: number,
   ruleSet: WarehouseLayoutRuleSet
 ): WarehouseLayoutEnhancer => ({
   supports: (currentLevel) => currentLevel === level,
-  apply: (currentPosition, parsedLocation) => {
-    const columnShiftCount = sumLocalColumnShiftOffsets(parsedLocation, ruleSet.localColumnShifts)
-    const effectiveColumn = parsedLocation.x + columnShiftCount
-    const gridColOffset =
-      columnShiftCount +
-      sumGlobalColumnAisleOffsets(
-        parsedLocation.y,
-        effectiveColumn,
-        ruleSet.globalColumnAisles
-      ) +
-      sumLocalColumnAisleOffsets(parsedLocation.y, effectiveColumn, ruleSet.localColumnAisles)
-    const gridRowOffset = sumRowAisleOffsets(parsedLocation.y, ruleSet.rowAisleBreakpoints)
-
-    return {
-      gridRow: currentPosition.gridRow + gridRowOffset,
-      gridCol: currentPosition.gridCol + gridColOffset
-    }
-  }
+  apply: (currentPosition, parsedLocation) =>
+    applyConfiguredRuleSet(currentPosition, parsedLocation, ruleSet)
 })
 
-const LAYOUT_ENHANCERS: WarehouseLayoutEnhancer[] = [
-  createConfiguredEnhancer(1, FIRST_FLOOR_RULE_SET)
-]
+const CONFIGURED_RULE_SETS: Record<number, WarehouseLayoutRuleSet> = {
+  1: FIRST_FLOOR_RULE_SET
+}
+
+const LAYOUT_ENHANCERS: WarehouseLayoutEnhancer[] = Object.entries(CONFIGURED_RULE_SETS).map(
+  ([level, ruleSet]) => createConfiguredEnhancer(Number(level), ruleSet)
+)
 
 export const applyWarehouseLayoutEnhancement = (
   level: number,
@@ -232,3 +302,98 @@ export const applyWarehouseLayoutEnhancement = (
       basePosition
     )
 }
+
+export const getWarehouseLayoutWallCells = (level: number): WarehouseLayoutWallCell[] => {
+  return getWarehouseLayoutSpecialCells(level)
+    .filter((cell): cell is WarehouseLayoutSpecialCell & { type: 'wall' } => cell.type === 'wall')
+    .map((cell) => ({
+      gridRow: cell.gridRow,
+      gridCol: cell.gridCol
+    }))
+}
+
+const getEnhancedBasePosition = (
+  level: number,
+  ruleSet: WarehouseLayoutRuleSet,
+  column: number,
+  row: number
+) => {
+  return applyConfiguredRuleSet(
+    {
+      gridRow: row,
+      gridCol: column
+    },
+    {
+      floor: level,
+      x: column,
+      y: row,
+      depth: 1
+    },
+    ruleSet
+  )
+}
+
+const buildWallCells = (
+  level: number,
+  ruleSet: WarehouseLayoutRuleSet
+): WarehouseLayoutSpecialCell[] => {
+  if (!ruleSet?.verticalWallBreaks?.length) {
+    return []
+  }
+
+  return ruleSet.verticalWallBreaks.map((rule) => {
+    const upperRow = rule.betweenRows[0]
+    const upperCellPosition = getEnhancedBasePosition(level, ruleSet, rule.column, upperRow)
+
+    return {
+      type: 'wall',
+      gridRow: upperCellPosition.gridRow + 1,
+      gridCol: upperCellPosition.gridCol,
+      label: '墙'
+    }
+  })
+}
+
+const buildElevatorAreaCells = (
+  level: number,
+  ruleSet: WarehouseLayoutRuleSet
+): WarehouseLayoutSpecialCell[] => {
+  if (!ruleSet.elevatorAreas?.length) {
+    return []
+  }
+
+  return ruleSet.elevatorAreas.flatMap((rule) => {
+    const upperLeft = getEnhancedBasePosition(level, ruleSet, rule.leftColumn, rule.upperRow)
+    const upperRight = getEnhancedBasePosition(level, ruleSet, rule.rightColumn, rule.upperRow)
+    const lowerLeft = getEnhancedBasePosition(level, ruleSet, rule.leftColumn, rule.lowerRow)
+    const rowStart = upperLeft.gridRow + 1
+    const rowEnd = lowerLeft.gridRow - 1
+    const colStart = upperLeft.gridCol
+    const colEnd = upperRight.gridCol
+    const centerRow = Math.floor((rowStart + rowEnd) / 2)
+    const centerCol = Math.floor((colStart + colEnd) / 2)
+    const cells: WarehouseLayoutSpecialCell[] = []
+
+    for (let gridRow = rowStart; gridRow <= rowEnd; gridRow++) {
+      for (let gridCol = colStart; gridCol <= colEnd; gridCol++) {
+        cells.push({
+          type: 'elevator',
+          gridRow,
+          gridCol,
+          label: gridRow === centerRow && gridCol === centerCol ? rule.label : undefined
+        })
+      }
+    }
+
+    return cells
+  })
+}
+
+export const getWarehouseLayoutSpecialCells = (level: number): WarehouseLayoutSpecialCell[] => {
+  const ruleSet = CONFIGURED_RULE_SETS[level]
+  if (!ruleSet) {
+    return []
+  }
+
+  return [...buildWallCells(level, ruleSet), ...buildElevatorAreaCells(level, ruleSet)]
+}