Переглянути джерело

库满度鼠标移动上去显示具体数据

handy 1 місяць тому
батько
коміт
3a23834e42
1 змінених файлів з 135 додано та 5 видалено
  1. 135 5
      src/App.vue

+ 135 - 5
src/App.vue

@@ -12,10 +12,38 @@
         <span class="title-meta">· 总库位 {{ locations.length }}</span>
         <span class="title-meta">· 可用库位 {{ availableLocationsCount }}</span>
         <span class="title-fill-rate">
-          <span class="title-meta">· 总库满度 {{ overallFillRate }}</span>
-          <span class="title-meta title-meta-a">A {{ categoryFillRateMap.A }}</span>
-          <span class="title-meta title-meta-b">B {{ categoryFillRateMap.B }}</span>
-          <span class="title-meta title-meta-c">C {{ categoryFillRateMap.C }}</span>
+          <span
+            class="title-meta title-fill-rate-item"
+            @mouseenter="handleFillRateMouseEnter($event, overallFillRateDetail)"
+            @mousemove="handleFillRateMouseMove"
+            @mouseleave="handleFillRateMouseLeave"
+          >
+            · 总库满度 {{ overallFillRate }}
+          </span>
+          <span
+            class="title-meta title-meta-a title-fill-rate-item"
+            @mouseenter="handleFillRateMouseEnter($event, categoryFillRateDetailMap.A)"
+            @mousemove="handleFillRateMouseMove"
+            @mouseleave="handleFillRateMouseLeave"
+          >
+            A {{ categoryFillRateMap.A }}
+          </span>
+          <span
+            class="title-meta title-meta-b title-fill-rate-item"
+            @mouseenter="handleFillRateMouseEnter($event, categoryFillRateDetailMap.B)"
+            @mousemove="handleFillRateMouseMove"
+            @mouseleave="handleFillRateMouseLeave"
+          >
+            B {{ categoryFillRateMap.B }}
+          </span>
+          <span
+            class="title-meta title-meta-c title-fill-rate-item"
+            @mouseenter="handleFillRateMouseEnter($event, categoryFillRateDetailMap.C)"
+            @mousemove="handleFillRateMouseMove"
+            @mouseleave="handleFillRateMouseLeave"
+          >
+            C {{ categoryFillRateMap.C }}
+          </span>
         </span>
         <span class="title-legend">
           <button
@@ -291,6 +319,16 @@
       </div>
     </header>
 
+    <div
+      v-if="fillRateTooltipVisible && fillRateTooltipText"
+      class="fill-rate-tooltip"
+      :style="fillRateTooltipStyle"
+    >
+      <div class="fill-rate-tooltip-line">
+        {{ fillRateTooltipText }}
+      </div>
+    </div>
+
     <main class="main-content">
       <div
         v-if="loading"
@@ -329,7 +367,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted, computed, onBeforeUnmount, nextTick } from 'vue'
+import { ref, onMounted, computed, onBeforeUnmount, nextTick, type CSSProperties } from 'vue'
 import { fetchLocationData } from './api/location'
 import type { LocationAttributeCode, LocationResourceDataVO } from './types'
 import WarehouseMap from './components/WarehouseMap.vue'
@@ -390,6 +428,12 @@ const now = ref(Date.now())
 const nextRefreshAt = ref(Date.now() + refreshIntervalMs.value)
 const refreshControlRef = ref<HTMLElement | null>(null)
 const refreshIntervalInputRef = ref<HTMLInputElement | null>(null)
+const fillRateTooltipVisible = ref(false)
+const fillRateTooltipText = ref('')
+const fillRateTooltipPosition = ref({
+  x: 0,
+  y: 0
+})
 let refreshTimer: number | null = null
 let countdownTimer: number | null = null
 
@@ -423,6 +467,10 @@ const formatFillRate = (total: number, occupied: number) => {
   return `${((occupied / total) * 100).toFixed(1)}%`
 }
 
+const formatFillRateDetail = (occupied: number, total: number) => {
+  return `${occupied}/${total}`
+}
+
 const availableLocationsCount = computed(() => {
   return locations.value.filter((loc) => loc.locationAttribute === 'OK').length
 })
@@ -432,6 +480,11 @@ const overallFillRate = computed(() => {
   return formatFillRate(locations.value.length, occupiedCount)
 })
 
+const overallFillRateDetail = computed(() => {
+  const occupiedCount = locations.value.filter((loc) => hasContainer(loc.containerCode)).length
+  return formatFillRateDetail(occupiedCount, locations.value.length)
+})
+
 const categoryFillRateMap = computed<Record<'A' | 'B' | 'C', string>>(() => {
   const categories: Array<'A' | 'B' | 'C'> = ['A', 'B', 'C']
   return categories.reduce(
@@ -449,6 +502,28 @@ const categoryFillRateMap = computed<Record<'A' | 'B' | 'C', string>>(() => {
   )
 })
 
+const categoryFillRateDetailMap = computed<Record<'A' | 'B' | 'C', string>>(() => {
+  const categories: Array<'A' | 'B' | 'C'> = ['A', 'B', 'C']
+  return categories.reduce(
+    (result, category) => {
+      const categoryLocations = locations.value.filter((loc) => loc.category === category)
+      const occupiedCount = categoryLocations.filter((loc) => hasContainer(loc.containerCode)).length
+      result[category] = formatFillRateDetail(occupiedCount, categoryLocations.length)
+      return result
+    },
+    {
+      A: '0/0',
+      B: '0/0',
+      C: '0/0'
+    }
+  )
+})
+
+const fillRateTooltipStyle = computed<CSSProperties>(() => ({
+  left: `${fillRateTooltipPosition.value.x}px`,
+  top: `${fillRateTooltipPosition.value.y}px`
+}))
+
 const locationAttributeOptions = computed<LocationAttributeCode[]>(() => {
   return [
     ...new Set(locations.value.map((loc) => loc.locationAttribute).filter(Boolean))
@@ -470,6 +545,37 @@ const toggleCategoryColorVisibility = (category: 'A' | 'B' | 'C') => {
   }
 }
 
+const updateFillRateTooltipPosition = (event: MouseEvent) => {
+  const tooltipWidth = 120
+  const tooltipHeight = 42
+  const offset = 14
+  const maxX = window.innerWidth - tooltipWidth - 12
+  const maxY = window.innerHeight - tooltipHeight - 12
+
+  fillRateTooltipPosition.value = {
+    x: Math.max(12, Math.min(event.clientX + offset, maxX)),
+    y: Math.max(12, Math.min(event.clientY + offset, maxY))
+  }
+}
+
+const handleFillRateMouseEnter = (event: MouseEvent, text: string) => {
+  fillRateTooltipText.value = text
+  fillRateTooltipVisible.value = true
+  updateFillRateTooltipPosition(event)
+}
+
+const handleFillRateMouseMove = (event: MouseEvent) => {
+  if (!fillRateTooltipVisible.value) {
+    return
+  }
+  updateFillRateTooltipPosition(event)
+}
+
+const handleFillRateMouseLeave = () => {
+  fillRateTooltipVisible.value = false
+  fillRateTooltipText.value = ''
+}
+
 const copyText = async (text: string) => {
   if (!text) return
 
@@ -738,6 +844,30 @@ onBeforeUnmount(() => {
   margin-left: 2px;
 }
 
+.title-fill-rate-item {
+  cursor: help;
+}
+
+.fill-rate-tooltip {
+  position: fixed;
+  z-index: 1000;
+  max-width: 160px;
+  padding: 10px 12px;
+  border: 1px solid #1f1f1f;
+  border-radius: 8px;
+  background: rgba(0, 0, 0, 0.96);
+  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.32);
+  color: #f2f2f2;
+  font-size: 12px;
+  line-height: 1.45;
+  pointer-events: none;
+  backdrop-filter: blur(6px);
+}
+
+.fill-rate-tooltip-line + .fill-rate-tooltip-line {
+  margin-top: 3px;
+}
+
 .title-meta-a {
   color: rgba(201, 242, 215, 0.78);
 }