|
|
@@ -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);
|
|
|
}
|