|
|
@@ -75,9 +75,11 @@
|
|
|
<label class="filter-item">
|
|
|
<select
|
|
|
v-model="selectedCategory"
|
|
|
+ ref="categorySelectRef"
|
|
|
:class="['level-select', { 'level-select-placeholder': !selectedCategory }]"
|
|
|
+ :style="{ width: selectWidths.category ? `${selectWidths.category}px` : 'auto' }"
|
|
|
>
|
|
|
- <option value="">库位类型</option>
|
|
|
+ <option value="">热度</option>
|
|
|
<option
|
|
|
v-for="category in categoryOptions"
|
|
|
:key="category"
|
|
|
@@ -90,9 +92,11 @@
|
|
|
<label class="filter-item">
|
|
|
<select
|
|
|
v-model="selectedLocationAttribute"
|
|
|
+ ref="locationAttributeSelectRef"
|
|
|
:class="['level-select', { 'level-select-placeholder': !selectedLocationAttribute }]"
|
|
|
+ :style="{ width: selectWidths.attribute ? `${selectWidths.attribute}px` : 'auto' }"
|
|
|
>
|
|
|
- <option value="">库位属性</option>
|
|
|
+ <option value="">状态</option>
|
|
|
<option
|
|
|
v-for="attribute in locationAttributeOptions"
|
|
|
:key="attribute"
|
|
|
@@ -105,27 +109,29 @@
|
|
|
<label class="filter-item">
|
|
|
<select
|
|
|
v-model="selectedHasContainer"
|
|
|
+ ref="hasContainerSelectRef"
|
|
|
:class="['level-select', { 'level-select-placeholder': !selectedHasContainer }]"
|
|
|
+ :style="{ width: selectWidths.container ? `${selectWidths.container}px` : 'auto' }"
|
|
|
>
|
|
|
<option value="">容器</option>
|
|
|
- <option value="Y">有容器</option>
|
|
|
- <option value="N">无容器</option>
|
|
|
+ <option value="Y">有</option>
|
|
|
+ <option value="N">无</option>
|
|
|
</select>
|
|
|
</label>
|
|
|
<label class="filter-item filter-input-item">
|
|
|
<span class="filter-input-wrap">
|
|
|
<input
|
|
|
- v-model="locGroupKeywordInput"
|
|
|
+ v-model="locationIdKeywordInput"
|
|
|
class="filter-input"
|
|
|
type="text"
|
|
|
- placeholder="库位组"
|
|
|
- @keydown.enter="applyLocGroupFilter"
|
|
|
+ placeholder="库位号"
|
|
|
+ @keydown.enter="applyLocationIdFilter"
|
|
|
>
|
|
|
<button
|
|
|
- v-if="locGroupKeywordInput"
|
|
|
+ v-if="locationIdKeywordInput"
|
|
|
class="filter-clear-btn"
|
|
|
type="button"
|
|
|
- @click="clearLocGroupFilter"
|
|
|
+ @click="clearLocationIdFilter"
|
|
|
>
|
|
|
<svg
|
|
|
viewBox="0 0 16 16"
|
|
|
@@ -141,7 +147,7 @@
|
|
|
<button
|
|
|
class="filter-confirm-btn"
|
|
|
type="button"
|
|
|
- @click="applyLocGroupFilter"
|
|
|
+ @click="applyLocationIdFilter"
|
|
|
>
|
|
|
<svg
|
|
|
viewBox="0 0 16 16"
|
|
|
@@ -156,20 +162,37 @@
|
|
|
</button>
|
|
|
</span>
|
|
|
</label>
|
|
|
+ <label class="filter-item">
|
|
|
+ <select
|
|
|
+ v-model="selectedZoneId"
|
|
|
+ ref="zoneSelectRef"
|
|
|
+ :class="['level-select', { 'level-select-placeholder': !selectedZoneId }]"
|
|
|
+ :style="{ width: selectWidths.zone ? `${selectWidths.zone}px` : 'auto' }"
|
|
|
+ >
|
|
|
+ <option value="">库区</option>
|
|
|
+ <option
|
|
|
+ v-for="zoneId in zoneOptions"
|
|
|
+ :key="zoneId"
|
|
|
+ :value="zoneId"
|
|
|
+ >
|
|
|
+ {{ zoneId }}
|
|
|
+ </option>
|
|
|
+ </select>
|
|
|
+ </label>
|
|
|
<label class="filter-item filter-input-item">
|
|
|
<span class="filter-input-wrap">
|
|
|
<input
|
|
|
- v-model="locationIdKeywordInput"
|
|
|
+ v-model="locGroupKeywordInput"
|
|
|
class="filter-input"
|
|
|
type="text"
|
|
|
- placeholder="库位号"
|
|
|
- @keydown.enter="applyLocationIdFilter"
|
|
|
+ placeholder="库位组"
|
|
|
+ @keydown.enter="applyLocGroupFilter"
|
|
|
>
|
|
|
<button
|
|
|
- v-if="locationIdKeywordInput"
|
|
|
+ v-if="locGroupKeywordInput"
|
|
|
class="filter-clear-btn"
|
|
|
type="button"
|
|
|
- @click="clearLocationIdFilter"
|
|
|
+ @click="clearLocGroupFilter"
|
|
|
>
|
|
|
<svg
|
|
|
viewBox="0 0 16 16"
|
|
|
@@ -185,7 +208,7 @@
|
|
|
<button
|
|
|
class="filter-confirm-btn"
|
|
|
type="button"
|
|
|
- @click="applyLocationIdFilter"
|
|
|
+ @click="applyLocGroupFilter"
|
|
|
>
|
|
|
<svg
|
|
|
viewBox="0 0 16 16"
|
|
|
@@ -200,24 +223,11 @@
|
|
|
</button>
|
|
|
</span>
|
|
|
</label>
|
|
|
- <label class="filter-item">
|
|
|
- <select
|
|
|
- v-model="selectedZoneId"
|
|
|
- :class="['level-select', { 'level-select-placeholder': !selectedZoneId }]"
|
|
|
- >
|
|
|
- <option value="">库区</option>
|
|
|
- <option
|
|
|
- v-for="zoneId in zoneOptions"
|
|
|
- :key="zoneId"
|
|
|
- :value="zoneId"
|
|
|
- >
|
|
|
- {{ zoneId }}
|
|
|
- </option>
|
|
|
- </select>
|
|
|
- </label>
|
|
|
<select
|
|
|
v-model.number="currentLevel"
|
|
|
class="level-select level-select-floor"
|
|
|
+ ref="levelSelectRef"
|
|
|
+ :style="{ width: selectWidths.level ? `${selectWidths.level}px` : 'auto' }"
|
|
|
@change="handleLevelChange"
|
|
|
>
|
|
|
<option
|
|
|
@@ -228,6 +238,65 @@
|
|
|
{{ level }}层
|
|
|
</option>
|
|
|
</select>
|
|
|
+ <button
|
|
|
+ class="selection-btn"
|
|
|
+ :class="{ 'selection-btn-active': selectionMode }"
|
|
|
+ type="button"
|
|
|
+ @click="toggleSelectionMode"
|
|
|
+ >
|
|
|
+ <svg
|
|
|
+ viewBox="0 0 16 16"
|
|
|
+ aria-hidden="true"
|
|
|
+ class="selection-btn-icon"
|
|
|
+ >
|
|
|
+ <path
|
|
|
+ d="M2 3.5a1.5 1.5 0 0 1 1.5-1.5H6v1.5H3.5V6H2V3.5zm8-1.5h2.5A1.5 1.5 0 0 1 14 3.5V6h-1.5V3.5H10V2zm2.5 8.5H14v2.5a1.5 1.5 0 0 1-1.5 1.5H10v-1.5h2.5V10.5zM2 10.5h1.5V13H6v1.5H3.5A1.5 1.5 0 0 1 2 13v-2.5z"
|
|
|
+ fill="currentColor"
|
|
|
+ />
|
|
|
+ <path
|
|
|
+ d="M6.5 6.5h3v3h-3z"
|
|
|
+ fill="currentColor"
|
|
|
+ opacity="0.5"
|
|
|
+ />
|
|
|
+ </svg>
|
|
|
+ <span>{{ selectionMode ? '框选中' : '框选' }}</span>
|
|
|
+ </button>
|
|
|
+ <div class="toggle-group">
|
|
|
+ <label class="toggle-item">
|
|
|
+ <span class="toggle-hint">组框</span>
|
|
|
+ <input
|
|
|
+ :checked="showGroupBorder"
|
|
|
+ class="toggle-input"
|
|
|
+ type="checkbox"
|
|
|
+ @change="handleGroupBorderToggle"
|
|
|
+ >
|
|
|
+ <span class="toggle-track">
|
|
|
+ <span class="toggle-thumb" />
|
|
|
+ </span>
|
|
|
+ </label>
|
|
|
+ <label class="toggle-item">
|
|
|
+ <span class="toggle-hint">卡片</span>
|
|
|
+ <input
|
|
|
+ v-model="showTooltip"
|
|
|
+ class="toggle-input"
|
|
|
+ type="checkbox"
|
|
|
+ >
|
|
|
+ <span class="toggle-track">
|
|
|
+ <span class="toggle-thumb" />
|
|
|
+ </span>
|
|
|
+ </label>
|
|
|
+ <label class="toggle-item theme-toggle">
|
|
|
+ <span class="toggle-hint">主题</span>
|
|
|
+ <input
|
|
|
+ v-model="isLightTheme"
|
|
|
+ class="toggle-input"
|
|
|
+ type="checkbox"
|
|
|
+ >
|
|
|
+ <span class="toggle-track">
|
|
|
+ <span class="toggle-thumb" />
|
|
|
+ </span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
<div
|
|
|
ref="refreshControlRef"
|
|
|
class="refresh-control"
|
|
|
@@ -271,42 +340,6 @@
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="toggle-group">
|
|
|
- <label class="toggle-item">
|
|
|
- <span class="toggle-hint">组框</span>
|
|
|
- <input
|
|
|
- :checked="showGroupBorder"
|
|
|
- class="toggle-input"
|
|
|
- type="checkbox"
|
|
|
- @change="handleGroupBorderToggle"
|
|
|
- >
|
|
|
- <span class="toggle-track">
|
|
|
- <span class="toggle-thumb" />
|
|
|
- </span>
|
|
|
- </label>
|
|
|
- <label class="toggle-item">
|
|
|
- <span class="toggle-hint">卡片</span>
|
|
|
- <input
|
|
|
- v-model="showTooltip"
|
|
|
- class="toggle-input"
|
|
|
- type="checkbox"
|
|
|
- >
|
|
|
- <span class="toggle-track">
|
|
|
- <span class="toggle-thumb" />
|
|
|
- </span>
|
|
|
- </label>
|
|
|
- <label class="toggle-item theme-toggle">
|
|
|
- <span class="toggle-hint">主题</span>
|
|
|
- <input
|
|
|
- v-model="isLightTheme"
|
|
|
- class="toggle-input"
|
|
|
- type="checkbox"
|
|
|
- >
|
|
|
- <span class="toggle-track">
|
|
|
- <span class="toggle-thumb" />
|
|
|
- </span>
|
|
|
- </label>
|
|
|
- </div>
|
|
|
<button
|
|
|
class="logout-btn"
|
|
|
@click="handleLogout"
|
|
|
@@ -360,8 +393,10 @@
|
|
|
: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"
|
|
|
/>
|
|
|
</WorkingHighlight>
|
|
|
</div>
|
|
|
@@ -434,6 +469,18 @@ const selectedCategory = ref('')
|
|
|
const selectedLocationAttribute = ref<LocationAttributeCode | ''>('')
|
|
|
const selectedHasContainer = ref<'Y' | 'N' | ''>('')
|
|
|
const selectedZoneId = ref('')
|
|
|
+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 selectWidths = ref({
|
|
|
+ category: 0,
|
|
|
+ attribute: 0,
|
|
|
+ container: 0,
|
|
|
+ zone: 0,
|
|
|
+ level: 0
|
|
|
+})
|
|
|
const categoryColorVisibility = ref<Record<'A' | 'B' | 'C', boolean>>({
|
|
|
A: true,
|
|
|
B: true,
|
|
|
@@ -445,6 +492,7 @@ const locationIdKeywordInput = ref('')
|
|
|
const appliedLocationIdKeyword = ref('')
|
|
|
const showGroupBorder = ref(false)
|
|
|
const showTooltip = ref(true)
|
|
|
+const selectionMode = ref(false)
|
|
|
const refreshIntervalMs = ref(getInitialRefreshInterval())
|
|
|
const showRefreshPopover = ref(false)
|
|
|
const refreshIntervalInput = ref(String(Math.max(Math.floor(refreshIntervalMs.value / 1000), 1)))
|
|
|
@@ -583,6 +631,18 @@ const getLocationAttributeLabel = (attribute: LocationAttributeCode) => {
|
|
|
return LOCATION_ATTRIBUTE_LABEL_MAP[attribute] || attribute
|
|
|
}
|
|
|
|
|
|
+const categoryLabel = computed(() => selectedCategory.value || '热度')
|
|
|
+const locationAttributeLabel = computed(() =>
|
|
|
+ selectedLocationAttribute.value ? getLocationAttributeLabel(selectedLocationAttribute.value) : '状态'
|
|
|
+)
|
|
|
+const hasContainerLabel = computed(() => {
|
|
|
+ if (selectedHasContainer.value === 'Y') return '有'
|
|
|
+ if (selectedHasContainer.value === 'N') return '无'
|
|
|
+ return '容器'
|
|
|
+})
|
|
|
+const zoneLabel = computed(() => selectedZoneId.value || '库区')
|
|
|
+const levelLabel = computed(() => `${currentLevel.value}层`)
|
|
|
+
|
|
|
const toggleCategoryColorVisibility = (category: 'A' | 'B' | 'C') => {
|
|
|
categoryColorVisibility.value = {
|
|
|
...categoryColorVisibility.value,
|
|
|
@@ -590,6 +650,40 @@ const toggleCategoryColorVisibility = (category: 'A' | 'B' | 'C') => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+const measureTextWidth = (() => {
|
|
|
+ const canvas = document.createElement('canvas')
|
|
|
+ const context = canvas.getContext('2d')
|
|
|
+ return (text: string, font: string) => {
|
|
|
+ if (!context) return text.length * 8
|
|
|
+ context.font = font
|
|
|
+ return context.measureText(text).width
|
|
|
+ }
|
|
|
+})()
|
|
|
+
|
|
|
+const updateSelectWidth = (
|
|
|
+ key: keyof typeof selectWidths.value,
|
|
|
+ element: HTMLSelectElement | null,
|
|
|
+ text: string
|
|
|
+) => {
|
|
|
+ if (!element) return
|
|
|
+ const style = window.getComputedStyle(element)
|
|
|
+ const font = `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`
|
|
|
+ const paddingLeft = Number.parseFloat(style.paddingLeft) || 0
|
|
|
+ const paddingRight = Number.parseFloat(style.paddingRight) || 0
|
|
|
+ const extra = 18
|
|
|
+ const textWidth = measureTextWidth(text, font)
|
|
|
+ const width = Math.ceil(textWidth + paddingLeft + paddingRight + extra)
|
|
|
+ selectWidths.value[key] = Math.max(width, 44)
|
|
|
+}
|
|
|
+
|
|
|
+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)
|
|
|
+}
|
|
|
+
|
|
|
const updateFillRateTooltipPosition = (event: MouseEvent) => {
|
|
|
const tooltipWidth = 120
|
|
|
const tooltipHeight = 42
|
|
|
@@ -666,6 +760,14 @@ const handleGroupBorderToggle = (event: Event) => {
|
|
|
showGroupBorder.value = (event.target as HTMLInputElement).checked
|
|
|
}
|
|
|
|
|
|
+const toggleSelectionMode = () => {
|
|
|
+ selectionMode.value = !selectionMode.value
|
|
|
+}
|
|
|
+
|
|
|
+const handleSelectionComplete = () => {
|
|
|
+ selectionMode.value = false
|
|
|
+}
|
|
|
+
|
|
|
const refreshCountdownText = computed(() => {
|
|
|
const remainMs = Math.max(nextRefreshAt.value - now.value, 0)
|
|
|
const totalSeconds = Math.floor(remainMs / 1000)
|
|
|
@@ -850,6 +952,7 @@ onMounted(() => {
|
|
|
}, 1000)
|
|
|
document.addEventListener('mousedown', handleDocumentClick)
|
|
|
window.addEventListener(AUTH_INVALID_EVENT, handleAuthInvalid)
|
|
|
+ window.addEventListener('resize', updateSelectWidths)
|
|
|
|
|
|
if (!isAuthenticated()) {
|
|
|
showLoginModal.value = true
|
|
|
@@ -857,6 +960,9 @@ onMounted(() => {
|
|
|
loadLocationData()
|
|
|
scheduleNextRefresh()
|
|
|
}
|
|
|
+ nextTick(() => {
|
|
|
+ updateSelectWidths()
|
|
|
+ })
|
|
|
})
|
|
|
|
|
|
watchEffect(() => {
|
|
|
@@ -867,6 +973,15 @@ watch(isLightTheme, (isLight) => {
|
|
|
window.localStorage.setItem(THEME_STORAGE_KEY, isLight ? 'light' : 'dark')
|
|
|
})
|
|
|
|
|
|
+watch(
|
|
|
+ [categoryLabel, locationAttributeLabel, hasContainerLabel, zoneLabel, levelLabel],
|
|
|
+ () => {
|
|
|
+ nextTick(() => {
|
|
|
+ updateSelectWidths()
|
|
|
+ })
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
onBeforeUnmount(() => {
|
|
|
if (refreshTimer !== null) {
|
|
|
window.clearTimeout(refreshTimer)
|
|
|
@@ -876,6 +991,7 @@ onBeforeUnmount(() => {
|
|
|
}
|
|
|
document.removeEventListener('mousedown', handleDocumentClick)
|
|
|
window.removeEventListener(AUTH_INVALID_EVENT, handleAuthInvalid)
|
|
|
+ window.removeEventListener('resize', updateSelectWidths)
|
|
|
})
|
|
|
</script>
|
|
|
|
|
|
@@ -1171,7 +1287,8 @@ onBeforeUnmount(() => {
|
|
|
}
|
|
|
|
|
|
.level-select {
|
|
|
- min-width: 76px;
|
|
|
+ min-width: 0;
|
|
|
+ width: auto;
|
|
|
padding: 6px 24px 6px 8px;
|
|
|
background: var(--input-bg);
|
|
|
border: 1px solid var(--input-border);
|
|
|
@@ -1200,7 +1317,7 @@ onBeforeUnmount(() => {
|
|
|
}
|
|
|
|
|
|
.level-select-floor {
|
|
|
- min-width: 64px;
|
|
|
+ min-width: 0;
|
|
|
}
|
|
|
|
|
|
.filter-input-wrap {
|
|
|
@@ -1395,6 +1512,43 @@ onBeforeUnmount(() => {
|
|
|
cursor: wait;
|
|
|
}
|
|
|
|
|
|
+.selection-btn {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 6px 8px;
|
|
|
+ border: 1px solid var(--btn-neutral-border);
|
|
|
+ background: var(--btn-neutral-bg);
|
|
|
+ color: var(--btn-neutral-text);
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 11px;
|
|
|
+ line-height: 1;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.selection-btn:hover {
|
|
|
+ background: var(--btn-neutral-hover-bg);
|
|
|
+ border-color: var(--btn-neutral-hover-border);
|
|
|
+}
|
|
|
+
|
|
|
+.selection-btn-active {
|
|
|
+ background: rgba(71, 129, 255, 0.18);
|
|
|
+ border-color: rgba(71, 129, 255, 0.6);
|
|
|
+ color: #cfe0ff;
|
|
|
+}
|
|
|
+
|
|
|
+.theme-light .selection-btn-active {
|
|
|
+ background: rgba(47, 95, 215, 0.12);
|
|
|
+ border-color: rgba(47, 95, 215, 0.65);
|
|
|
+ color: #2f5fd7;
|
|
|
+}
|
|
|
+
|
|
|
+.selection-btn-icon {
|
|
|
+ width: 14px;
|
|
|
+ height: 14px;
|
|
|
+}
|
|
|
+
|
|
|
.logout-btn {
|
|
|
padding: 6px 10px;
|
|
|
background: var(--danger-bg);
|