handy 1 месяц назад
Родитель
Сommit
9dd22d914c
2 измененных файлов с 151 добавлено и 9 удалено
  1. 149 7
      src/components/WarehouseMap.vue
  2. 2 2
      src/config/index.ts

+ 149 - 7
src/components/WarehouseMap.vue

@@ -8,13 +8,14 @@
           :key="index"
           :class="['grid-cell', getCellClass(cell)]"
           :style="getCellStyle(cell)"
-          @mouseenter="handleCellMouseEnter(cell, $event)"
+          @mousedown.left="handleCellMouseDown(index, $event)"
+          @mouseenter="handleCellMouseEnter(cell, index, $event)"
           @mousemove="handleCellMouseMove($event)"
           @mouseleave="handleCellMouseLeave"
           @click="handleCellClick(cell)"
           @contextmenu.prevent="handleCellContextMenu(cell)"
         >
-          <div v-if="cell && isCellMatched(cell)" class="cell-content">
+          <div v-if="cell && isCellVisible(cell)" class="cell-content">
             <div class="category-badge" :style="getCategoryStyle(cell)">
               {{ getHeatLabel(cell) }}
             </div>
@@ -101,6 +102,16 @@ const tooltipPosition = ref({
   x: 0,
   y: 0
 })
+const selectionStart = ref<{ row: number; col: number } | null>(null)
+const selectionEnd = ref<{ row: number; col: number } | null>(null)
+const selectedRange = ref<{
+  rowStart: number
+  rowEnd: number
+  colStart: number
+  colEnd: number
+} | null>(null)
+const didSelectionMove = ref(false)
+const suppressNextClick = ref(false)
 
 let resizeObserver: ResizeObserver | null = null
 
@@ -289,9 +300,48 @@ const isCellMatched = (cell: GridCell) => {
     && matchedLocationId
 }
 
+const getGridPointByIndex = (index: number) => {
+  if (!gridBounds.value || !gridMetrics.value) {
+    return null
+  }
+
+  const rowOffset = Math.floor(index / gridMetrics.value.cols)
+  const colOffset = index % gridMetrics.value.cols
+
+  return {
+    row: gridBounds.value.minX + rowOffset,
+    col: gridBounds.value.minY + colOffset
+  }
+}
+
+const buildSelectionRange = (
+  start: { row: number; col: number },
+  end: { row: number; col: number }
+) => ({
+  rowStart: Math.min(start.row, end.row),
+  rowEnd: Math.max(start.row, end.row),
+  colStart: Math.min(start.col, end.col),
+  colEnd: Math.max(start.col, end.col)
+})
+
+const isCellInSelectedRange = (cell: GridCell) => {
+  if (!selectedRange.value) {
+    return true
+  }
+
+  return cell.parsed.gridRow >= selectedRange.value.rowStart
+    && cell.parsed.gridRow <= selectedRange.value.rowEnd
+    && cell.parsed.gridCol >= selectedRange.value.colStart
+    && cell.parsed.gridCol <= selectedRange.value.colEnd
+}
+
+const isCellVisible = (cell: GridCell) => {
+  return isCellMatched(cell) && isCellInSelectedRange(cell)
+}
+
 const getCellClass = (cell: GridCell | null) => {
   if (!cell) return ['aisle']
-  if (!isCellMatched(cell)) return ['aisle']
+  if (!isCellVisible(cell)) return ['aisle']
 
   const classNames = [`category-${cell.category.toLowerCase()}`]
   if (hasActiveBorder() && hasSameBorderNeighbor(cell)) {
@@ -318,6 +368,7 @@ const getLocationAttributeLabel = (cell: GridCell) => {
 const getCellTitle = (cell: GridCell | null) => {
   if (!cell) return '过道'
   if (!isCellMatched(cell)) return '未命中筛选条件'
+  if (!isCellInSelectedRange(cell)) return '未命中当前选区'
   const mismatchText = cell.categoryMismatch && cell.expectedCategory
     ? `\n异常: 深度${cell.parsed.depth} 期望热度${cell.expectedCategory}`
     : cell.categoryMismatch
@@ -396,7 +447,7 @@ const hasSameBorderNeighbor = (cell: GridCell) => {
 }
 
 const getCellStyle = (cell: GridCell | null): CSSProperties => {
-  if (!cell || !isCellMatched(cell) || !hasActiveBorder() || !hasSameBorderNeighbor(cell)) return {}
+  if (!cell || !isCellVisible(cell) || !hasActiveBorder() || !hasSameBorderNeighbor(cell)) return {}
 
   const borderWidth = 'var(--group-outline-width, 2px)'
 
@@ -409,16 +460,69 @@ const getCellStyle = (cell: GridCell | null): CSSProperties => {
   }
 }
 
+const clearSelectedRange = () => {
+  selectedRange.value = null
+}
+
 const handleCellClick = (cell: GridCell | null) => {
-  if (!cell || !isCellMatched(cell)) return
+  if (suppressNextClick.value) {
+    suppressNextClick.value = false
+    return
+  }
+  if (!cell || !isCellVisible(cell)) {
+    if (selectedRange.value) {
+      clearSelectedRange()
+    }
+    return
+  }
   emit('select-loc-group', cell.locGroup1)
 }
 
 const handleCellContextMenu = (cell: GridCell | null) => {
-  if (!cell || !isCellMatched(cell)) return
+  if (!cell || !isCellVisible(cell)) return
   emit('select-location-id', cell.locationId)
 }
 
+const copyText = async (text: string) => {
+  if (!text) {
+    return
+  }
+
+  try {
+    await navigator.clipboard.writeText(text)
+    return
+  } catch (error) {
+    console.warn('Clipboard API copy failed, fallback to execCommand.', error)
+  }
+
+  const textarea = document.createElement('textarea')
+  textarea.value = text
+  textarea.setAttribute('readonly', 'true')
+  textarea.style.position = 'fixed'
+  textarea.style.top = '-9999px'
+  document.body.appendChild(textarea)
+  textarea.select()
+  document.execCommand('copy')
+  document.body.removeChild(textarea)
+}
+
+const handleCellMouseDown = (index: number, event: MouseEvent) => {
+  if (event.button !== 0) {
+    return
+  }
+
+  const point = getGridPointByIndex(index)
+  if (!point) {
+    return
+  }
+
+  selectionStart.value = point
+  selectionEnd.value = point
+  didSelectionMove.value = false
+  tooltipVisible.value = false
+  hoveredCell.value = null
+}
+
 const updateTooltipPosition = (event: MouseEvent) => {
   const tooltipWidth = 300
   const tooltipHeight = 260
@@ -432,7 +536,19 @@ const updateTooltipPosition = (event: MouseEvent) => {
   }
 }
 
-const handleCellMouseEnter = (cell: GridCell | null, event: MouseEvent) => {
+const handleCellMouseEnter = (cell: GridCell | null, index: number, event: MouseEvent) => {
+  if (selectionStart.value && (event.buttons & 1) === 1) {
+    const point = getGridPointByIndex(index)
+    if (point) {
+      selectionEnd.value = point
+      didSelectionMove.value = point.row !== selectionStart.value.row
+        || point.col !== selectionStart.value.col
+    }
+    tooltipVisible.value = false
+    hoveredCell.value = null
+    return
+  }
+
   if (!props.showTooltip) return
   hoveredCell.value = cell
   tooltipVisible.value = true
@@ -445,10 +561,34 @@ const handleCellMouseMove = (event: MouseEvent) => {
 }
 
 const handleCellMouseLeave = () => {
+  if (selectionStart.value) {
+    return
+  }
   tooltipVisible.value = false
   hoveredCell.value = null
 }
 
+const handleWindowMouseUp = () => {
+  if (!selectionStart.value || !selectionEnd.value) {
+    return
+  }
+
+  if (didSelectionMove.value) {
+    selectedRange.value = buildSelectionRange(selectionStart.value, selectionEnd.value)
+    const selectedLocationIds = gridData.value
+      .filter((cell): cell is GridCell => Boolean(cell))
+      .filter((cell) => isCellMatched(cell) && isCellInSelectedRange(cell))
+      .map((cell) => cell.locationId)
+
+    void copyText(selectedLocationIds.join('\n'))
+    suppressNextClick.value = true
+  }
+
+  selectionStart.value = null
+  selectionEnd.value = null
+  didSelectionMove.value = false
+}
+
 const updateWrapperSize = () => {
   if (!mapWrapperRef.value) return
   wrapperSize.value = {
@@ -459,6 +599,7 @@ const updateWrapperSize = () => {
 
 onMounted(() => {
   updateWrapperSize()
+  window.addEventListener('mouseup', handleWindowMouseUp)
   if (!mapWrapperRef.value) return
 
   resizeObserver = new ResizeObserver(() => {
@@ -478,6 +619,7 @@ watch(
 )
 
 onBeforeUnmount(() => {
+  window.removeEventListener('mouseup', handleWindowMouseUp)
   resizeObserver?.disconnect()
 })
 </script>

+ 2 - 2
src/config/index.ts

@@ -1,7 +1,7 @@
 // 系统配置文件
 export const config = {
   // API 基础地址
-  apiBaseUrl: 'http://localhost:8114',
+  apiBaseUrl: 'https://sit-api.baoshi56.com',
 
   // 登录接口
   loginApi: '/api/user/login',
@@ -10,7 +10,7 @@ export const config = {
   warehouse: 'WH01',
 
   // 数据刷新间隔(毫秒)
-  refreshInterval: 300000,
+  refreshInterval: 60000,
 
   // 楼层范围
   minLevel: 1,