| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975 |
- <template>
- <div class="warehouse-map">
- <div
- v-if="gridData.length === 0"
- class="no-data"
- >
- 暂无数据
- </div>
- <div
- v-else
- ref="mapWrapperRef"
- class="map-wrapper"
- >
- <div
- class="map-grid"
- :style="gridStyle"
- >
- <div
- v-for="(cell, index) in gridData"
- :key="index"
- :class="['grid-cell', getCellClass(cell)]"
- :style="getCellStyle(cell)"
- @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="isLocationCell(cell) && isCellVisible(cell)"
- class="cell-content"
- >
- <div
- class="category-badge"
- :style="getCategoryStyle(cell)"
- >
- {{ getHeatLabel(cell) }}
- </div>
- <div class="loc-group">
- {{ cell.locGroup1 }}
- </div>
- <div
- :class="[
- 'location-id',
- {
- 'location-id-mismatch':
- cell.categoryMismatch && !hasAbnormalLocationAttribute(cell),
- 'location-id-abnormal-attribute': hasAbnormalLocationAttribute(cell)
- }
- ]"
- >
- {{ cell.locationId }}
- </div>
- <div
- v-if="cell.categoryMismatch"
- class="location-attribute-tag"
- >
- 热度编号异常
- </div>
- <div
- v-if="hasAbnormalLocationAttribute(cell)"
- class="location-attribute-tag"
- >
- {{ getLocationAttributeLabel(cell) }}
- </div>
- <div
- v-if="cell.containerCode"
- class="container-code"
- >
- {{ 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
- v-if="props.showTooltip && tooltipVisible && tooltipLines.length"
- class="cell-tooltip"
- :style="tooltipStyle"
- >
- <div
- v-for="(line, index) in tooltipLines"
- :key="index"
- class="cell-tooltip-line"
- >
- {{ line }}
- </div>
- </div>
- </div>
- </div>
- </template>
- <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 {
- getWarehouseLayoutSpecialCells,
- type WarehouseLayoutSpecialCell
- } from './warehouse-layout-special-cells'
- interface Props {
- locations: LocationResourceDataVO[]
- currentLevel: number
- selectedCategory: string
- categoryColorVisibility: Record<string, boolean>
- selectedLocationAttribute: string
- selectedHasContainer: string
- selectedZoneId: string
- locGroupKeyword: string
- locationIdKeyword: string
- showGroupBorder: boolean
- showTooltip: boolean
- }
- interface ParsedLocation {
- floor: number
- x: number
- y: number
- depth: number
- gridRow: number
- gridCol: number
- }
- interface GridCell extends LocationResourceDataVO {
- parsed: ParsedLocation
- expectedCategory: string | null
- categoryMismatch: boolean
- }
- interface AisleCell {
- type: 'aisle'
- gridRow: number
- gridCol: number
- }
- type MapCell = GridCell | WarehouseLayoutSpecialCell | AisleCell
- const props = defineProps<Props>()
- const emit = defineEmits<{
- (event: 'select-loc-group', locGroup1: string): void
- (event: 'select-location-id', locationId: string): void
- }>()
- const mapWrapperRef = ref<HTMLElement | null>(null)
- const wrapperSize = ref({
- width: 0,
- height: 0
- })
- const hoveredCell = ref<GridCell | null>(null)
- const tooltipVisible = ref(false)
- 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
- const DEPTH_CATEGORY_MAP: Record<number, string> = {
- 1: 'A',
- 2: 'B',
- 3: 'C'
- }
- const CATEGORY_THEME_MAP: Record<string, { solid: string; soft: string; text: string }> = {
- A: {
- solid: '#008000',
- soft: 'rgba(0, 128, 0, 0.16)',
- text: '#f5fff7'
- },
- B: {
- solid: '#0000FF',
- soft: 'rgba(0, 0, 255, 0.16)',
- text: '#f4f8ff'
- },
- C: {
- solid: '#FFFF00',
- soft: 'rgba(255, 255, 0, 0.16)',
- text: '#5a5200'
- }
- }
- const INACTIVE_CATEGORY_THEME = {
- solid: '#5f6b7a',
- soft: 'rgba(95, 107, 122, 0.16)',
- text: '#eef4f8'
- }
- const LOCATION_ATTRIBUTE_LABEL_MAP: Record<string, string> = {
- OK: '正常',
- FI: '禁入',
- HD: '封存',
- SC: '管控'
- }
- const normalizeCategory = (category: string) => category.trim().toUpperCase()
- const formatFieldValue = (value: string | number | null | undefined) => {
- if (value === null || value === undefined || value === '') {
- return '-'
- }
- return String(value)
- }
- // 解析库位编码 H1-0-0-2 => {floor: 1, x: 0, y: 0, depth: 2}
- const parseLocationId = (locationId: string) => {
- const match = locationId.match(/H(\d+)-(\d+)-(\d+)-(\d+)/)
- if (!match) return null
- return {
- floor: parseInt(match[1], 10),
- x: parseInt(match[2], 10),
- y: parseInt(match[3], 10),
- depth: parseInt(match[4], 10),
- gridRow: 0,
- gridCol: 0
- }
- }
- const parsedLocations = computed<GridCell[]>(() => {
- return props.locations
- .map((loc) => ({
- ...loc,
- parsed: parseLocationId(loc.locationId)
- }))
- .filter(
- (loc): loc is LocationResourceDataVO & { parsed: ParsedLocation } => loc.parsed !== null
- )
- .map((loc) => {
- const normalizedCategory = normalizeCategory(loc.category)
- const expectedCategory = DEPTH_CATEGORY_MAP[loc.parsed.depth] || null
- const enhancedPosition = applyWarehouseLayoutEnhancement(props.currentLevel, loc.parsed, {
- // 原始布局规则:第一个数字是列,第二个数字是行。
- gridRow: loc.parsed.y,
- gridCol: loc.parsed.x
- })
- return {
- ...loc,
- parsed: {
- ...loc.parsed,
- ...enhancedPosition
- },
- category: normalizedCategory,
- expectedCategory,
- categoryMismatch: expectedCategory === null || normalizedCategory !== expectedCategory
- }
- })
- })
- const specialCells = computed(() => getWarehouseLayoutSpecialCells(props.currentLevel))
- const specialCellMap = computed(() => {
- return new Map(specialCells.value.map((cell) => [`${cell.gridRow}-${cell.gridCol}`, cell]))
- })
- const locationMap = computed(() => {
- const map = new Map<string, GridCell>()
- parsedLocations.value.forEach((loc) => {
- map.set(`${loc.parsed.gridRow}-${loc.parsed.gridCol}`, loc)
- })
- return map
- })
- const gridBounds = computed(() => {
- if (parsedLocations.value.length === 0) return null
- const allRows = [
- ...parsedLocations.value.map((loc) => loc.parsed.gridRow),
- ...specialCells.value.map((cell) => cell.gridRow)
- ]
- const allCols = [
- ...parsedLocations.value.map((loc) => loc.parsed.gridCol),
- ...specialCells.value.map((cell) => cell.gridCol)
- ]
- return {
- minX: Math.min(...allRows),
- maxX: Math.max(...allRows),
- minY: Math.min(...allCols),
- maxY: Math.max(...allCols)
- }
- })
- const gridMetrics = computed(() => {
- if (!gridBounds.value) return null
- return {
- rows: gridBounds.value.maxX - gridBounds.value.minX + 1,
- cols: gridBounds.value.maxY - gridBounds.value.minY + 1
- }
- })
- // 构建网格数据 - 按 X(行) / Y(列) 摆放,缺口显示为过道
- const gridData = computed(() => {
- if (!gridBounds.value) return []
- // 构建网格:按行(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}`
- 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({
- type: 'aisle',
- gridRow: x,
- gridCol: y
- })
- }
- }
- return grid
- })
- const gridStyle = computed(() => {
- if (!gridMetrics.value) return {}
- const gap = 2
- const horizontalPadding = 24
- const verticalPadding = 12
- const availableWidth = Math.max(wrapperSize.value.width - horizontalPadding, 0)
- const availableHeight = Math.max(wrapperSize.value.height - verticalPadding, 0)
- const cellWidth =
- availableWidth > 0
- ? Math.max((availableWidth - (gridMetrics.value.cols - 1) * gap) / gridMetrics.value.cols, 28)
- : 72
- const cellHeight =
- availableHeight > 0
- ? Math.max(
- (availableHeight - (gridMetrics.value.rows - 1) * gap) / gridMetrics.value.rows,
- 24
- )
- : 60
- const compactSize = Math.min(cellWidth, cellHeight)
- const showBadge = compactSize > 38 ? 'inline-flex' : 'none'
- const showGroup = compactSize > 58 ? 'block' : 'none'
- const badgeFontSize = compactSize <= 42 ? 8 : compactSize <= 56 ? 9 : 10
- const textFontSize = compactSize <= 56 ? 9 : 10
- const idFontSize = compactSize <= 34 ? 9 : compactSize <= 42 ? 10 : compactSize <= 56 ? 10 : 11
- const contentGap = compactSize <= 42 ? 2 : 4
- const contentPadding = compactSize <= 42 ? 2 : 4
- const groupBorderWidth = compactSize <= 34 ? 1 : 2
- return {
- gridTemplateColumns: `repeat(${gridMetrics.value.cols}, ${cellWidth.toFixed(2)}px)`,
- gridAutoRows: `${cellHeight.toFixed(2)}px`,
- '--cell-badge-font-size': `${badgeFontSize}px`,
- '--cell-group-font-size': `${textFontSize}px`,
- '--cell-id-font-size': `${idFontSize}px`,
- '--cell-badge-display': showBadge,
- '--cell-group-display': showGroup,
- '--cell-content-gap': `${contentGap}px`,
- '--cell-content-padding': `${contentPadding}px`,
- '--group-outline-width': `${groupBorderWidth}px`
- }
- })
- const isCellMatched = (cell: GridCell) => {
- const matchedCategory = !props.selectedCategory || cell.category === props.selectedCategory
- const matchedLocationAttribute =
- !props.selectedLocationAttribute || cell.locationAttribute === props.selectedLocationAttribute
- const hasContainer = Boolean(cell.containerCode && cell.containerCode.trim())
- const matchedHasContainer =
- !props.selectedHasContainer ||
- (props.selectedHasContainer === 'Y' && hasContainer) ||
- (props.selectedHasContainer === 'N' && !hasContainer)
- const matchedZoneId = !props.selectedZoneId || String(cell.zoneId || '') === props.selectedZoneId
- const normalizedKeyword = props.locGroupKeyword.trim().toUpperCase()
- const matchedLocGroup =
- !normalizedKeyword || cell.locGroup1.toUpperCase().includes(normalizedKeyword)
- const normalizedLocationIdKeyword = props.locationIdKeyword.trim().toUpperCase()
- const matchedLocationId =
- !normalizedLocationIdKeyword ||
- cell.locationId.toUpperCase().includes(normalizedLocationIdKeyword)
- return (
- matchedCategory &&
- matchedLocationAttribute &&
- matchedHasContainer &&
- matchedZoneId &&
- matchedLocGroup &&
- 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 isSpecialCell = (cell: MapCell): cell is WarehouseLayoutSpecialCell => {
- return Boolean(cell && 'type' in cell && cell.type !== 'aisle')
- }
- const isLocationCell = (cell: MapCell): cell is GridCell => {
- return Boolean(cell && !('type' in cell))
- }
- const getCellClass = (cell: MapCell) => {
- if (isSpecialCell(cell)) {
- return [cell.type]
- }
- if (!isLocationCell(cell)) {
- return ['aisle']
- }
- if (!isCellVisible(cell)) {
- return ['aisle']
- }
- const classNames = ['location-cell']
- if (props.categoryColorVisibility[cell.category] !== false) {
- classNames.push(`category-${cell.category.toLowerCase()}`)
- } else {
- classNames.push('category-muted')
- }
- if (props.showGroupBorder && hasSameBorderNeighbor(cell)) {
- classNames.push('grouped')
- }
- return classNames
- }
- const getHeatLabel = (cell: GridCell) => {
- return cell.category
- }
- const hasAbnormalLocationAttribute = (cell: GridCell) => {
- return Boolean(cell.locationAttribute && cell.locationAttribute !== 'OK')
- }
- const getLocationAttributeLabel = (cell: GridCell) => {
- if (!cell.locationAttribute) {
- return '-'
- }
- return LOCATION_ATTRIBUTE_LABEL_MAP[cell.locationAttribute] || cell.locationAttribute
- }
- 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
- ? `\n异常: 深度${cell.parsed.depth} 未配置对应热度`
- : ''
- const locationAttributeLabel = cell.locationAttribute
- ? LOCATION_ATTRIBUTE_LABEL_MAP[cell.locationAttribute] || cell.locationAttribute
- : '-'
- return [
- `库位号: ${formatFieldValue(cell.locationId)}`,
- `库位组: ${formatFieldValue(cell.locGroup1)}`,
- `WCS库位: ${formatFieldValue(cell.wcsLocationId)}`,
- `资源类别: ${formatFieldValue(cell.category)}`,
- `容器编码: ${formatFieldValue(cell.containerCode)}`,
- `库位楼层: ${formatFieldValue(cell.locLevel)}`,
- `库区: ${formatFieldValue(cell.zoneId)}`,
- `库位属性: ${locationAttributeLabel}${cell.locationAttribute ? `(${cell.locationAttribute})` : ''}`,
- `上架排序: ${formatFieldValue(cell.putawayLogicalSequence)}`,
- `X坐标: ${cell.parsed.x}`,
- `Y坐标: ${cell.parsed.y}`,
- `深度: ${cell.parsed.depth}${mismatchText}`
- ].join('\n')
- }
- const getCategoryStyle = (cell: GridCell) => {
- const theme =
- props.categoryColorVisibility[cell.category] === false
- ? INACTIVE_CATEGORY_THEME
- : CATEGORY_THEME_MAP[cell.category]
- return {
- background: theme?.solid || '#5f6b7a',
- color: theme?.text || '#fff'
- }
- }
- const tooltipLines = computed(() => {
- return getCellTitle(hoveredCell.value)
- .split('\n')
- .map((line) => line.trim())
- .filter(Boolean)
- })
- const tooltipStyle = computed<CSSProperties>(() => ({
- left: `${tooltipPosition.value.x}px`,
- top: `${tooltipPosition.value.y}px`
- }))
- const getBorderGroupValue = (cell: GridCell) => {
- if (props.showGroupBorder) {
- return cell.locGroup1
- }
- return ''
- }
- const isSameBorderNeighbor = (cell: GridCell, xOffset: number, yOffset: number) => {
- const currentGroupValue = getBorderGroupValue(cell)
- if (!currentGroupValue) {
- return false
- }
- const neighbor = locationMap.value.get(
- `${cell.parsed.gridRow + xOffset}-${cell.parsed.gridCol + yOffset}`
- )
- return Boolean(neighbor && getBorderGroupValue(neighbor) === currentGroupValue)
- }
- const hasSameBorderNeighbor = (cell: GridCell) => {
- return (
- isSameBorderNeighbor(cell, -1, 0) ||
- isSameBorderNeighbor(cell, 1, 0) ||
- isSameBorderNeighbor(cell, 0, -1) ||
- isSameBorderNeighbor(cell, 0, 1)
- )
- }
- const getCellStyle = (cell: MapCell): CSSProperties => {
- if (
- !isLocationCell(cell) ||
- !isCellVisible(cell) ||
- !props.showGroupBorder ||
- !hasSameBorderNeighbor(cell)
- ) {
- return {}
- }
- const borderWidth = 'var(--group-outline-width, 2px)'
- return {
- '--group-border-color': '#ffffff',
- '--group-border-top': isSameBorderNeighbor(cell, -1, 0) ? '0px' : borderWidth,
- '--group-border-right': isSameBorderNeighbor(cell, 0, 1) ? '0px' : borderWidth,
- '--group-border-bottom': isSameBorderNeighbor(cell, 1, 0) ? '0px' : borderWidth,
- '--group-border-left': isSameBorderNeighbor(cell, 0, -1) ? '0px' : borderWidth
- }
- }
- const clearSelectedRange = () => {
- selectedRange.value = null
- }
- const handleCellClick = (cell: MapCell) => {
- if (suppressNextClick.value) {
- suppressNextClick.value = false
- return
- }
- if (!isLocationCell(cell) || !isCellVisible(cell)) {
- if (selectedRange.value) {
- clearSelectedRange()
- }
- return
- }
- emit('select-loc-group', cell.locGroup1)
- }
- const handleCellContextMenu = (cell: MapCell) => {
- if (!isLocationCell(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
- const offset = 14
- const maxX = window.innerWidth - tooltipWidth - 12
- const maxY = window.innerHeight - tooltipHeight - 12
- tooltipPosition.value = {
- x: Math.max(12, Math.min(event.clientX + offset, maxX)),
- y: Math.max(12, Math.min(event.clientY + offset, maxY))
- }
- }
- const handleCellMouseEnter = (cell: MapCell, 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 || !isLocationCell(cell)) {
- tooltipVisible.value = false
- hoveredCell.value = null
- return
- }
- hoveredCell.value = cell
- tooltipVisible.value = true
- updateTooltipPosition(event)
- }
- const handleCellMouseMove = (event: MouseEvent) => {
- if (!props.showTooltip || !tooltipVisible.value) return
- updateTooltipPosition(event)
- }
- 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(isLocationCell)
- .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 = {
- width: mapWrapperRef.value.clientWidth,
- height: mapWrapperRef.value.clientHeight
- }
- }
- onMounted(() => {
- updateWrapperSize()
- window.addEventListener('mouseup', handleWindowMouseUp)
- if (!mapWrapperRef.value) return
- resizeObserver = new ResizeObserver(() => {
- updateWrapperSize()
- })
- resizeObserver.observe(mapWrapperRef.value)
- })
- watch(
- () => props.showTooltip,
- (enabled) => {
- if (!enabled) {
- tooltipVisible.value = false
- hoveredCell.value = null
- }
- }
- )
- onBeforeUnmount(() => {
- window.removeEventListener('mouseup', handleWindowMouseUp)
- resizeObserver?.disconnect()
- })
- </script>
- <style scoped>
- .warehouse-map {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- position: relative;
- }
- .no-data {
- display: flex;
- justify-content: center;
- align-items: center;
- height: 100%;
- color: #8b8b8b;
- font-size: 16px;
- }
- .map-wrapper {
- flex: 1;
- position: relative;
- overflow: auto;
- background: #000000;
- }
- .map-grid {
- display: grid;
- gap: 2px;
- padding: 0 12px 12px;
- box-sizing: border-box;
- width: 100%;
- height: 100%;
- align-content: start;
- }
- .grid-cell {
- border: none;
- border-radius: 4px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
- cursor: pointer;
- position: relative;
- }
- .grid-cell.grouped::after {
- content: '';
- position: absolute;
- inset: 0;
- border-style: solid;
- border-color: var(--group-border-color, #ffffff);
- border-top-width: var(--group-border-top, 0px);
- border-right-width: var(--group-border-right, 0px);
- border-bottom-width: var(--group-border-bottom, 0px);
- border-left-width: var(--group-border-left, 0px);
- border-radius: 4px;
- pointer-events: none;
- opacity: 0.95;
- }
- .grid-cell.aisle {
- background: #050505;
- }
- .grid-cell.wall {
- background: #151515;
- cursor: default;
- }
- .grid-cell.elevator {
- background: #101010;
- cursor: default;
- }
- .grid-cell.category-a {
- background: rgba(0, 128, 0, 0.16);
- }
- .grid-cell.category-b {
- background: rgba(0, 0, 255, 0.16);
- }
- .grid-cell.category-c {
- background: rgba(255, 255, 0, 0.16);
- }
- .grid-cell.category-muted {
- background: rgba(95, 107, 122, 0.16);
- }
- .grid-cell:not(.aisle):not(.wall):not(.elevator):hover {
- transform: scale(1.03);
- box-shadow: 0 0 12px rgba(255, 255, 255, 0.08);
- z-index: 10;
- }
- .cell-content {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: var(--cell-content-gap, 4px);
- padding: var(--cell-content-padding, 4px);
- width: 100%;
- height: 100%;
- overflow: hidden;
- }
- .category-badge {
- display: var(--cell-badge-display, inline-flex);
- align-items: center;
- justify-content: center;
- padding: 2px 8px;
- border-radius: 10px;
- font-size: var(--cell-badge-font-size, 10px);
- font-weight: bold;
- line-height: 1;
- white-space: nowrap;
- }
- .loc-group {
- display: var(--cell-group-display, -webkit-box);
- font-size: var(--cell-group-font-size, 10px);
- color: #a0a0a0;
- text-align: center;
- line-height: 1.2;
- word-break: break-all;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
- overflow: hidden;
- }
- .location-id {
- display: block;
- font-size: var(--cell-id-font-size, 11px);
- font-weight: bold;
- color: #f2f2f2;
- text-align: center;
- line-height: 1.1;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .location-id.location-id-mismatch {
- color: #ff4d4f;
- }
- .location-id.location-id-abnormal-attribute {
- color: #ff4d4f;
- }
- .location-attribute-tag {
- max-width: 100%;
- font-size: calc(var(--cell-id-font-size, 11px) - 2px);
- color: #ff4d4f;
- text-align: center;
- line-height: 1.1;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .container-code {
- max-width: 100%;
- font-size: calc(var(--cell-id-font-size, 11px) - 2px);
- color: #9a9a9a;
- text-align: center;
- line-height: 1.1;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .special-cell-label {
- font-size: calc(var(--cell-id-font-size, 11px) - 1px);
- font-weight: 700;
- color: rgba(255, 255, 255, 0.76);
- letter-spacing: 1px;
- user-select: none;
- }
- .special-cell-label-elevator {
- color: rgba(255, 255, 255, 0.86);
- }
- .cell-tooltip {
- position: fixed;
- z-index: 1000;
- max-width: 300px;
- 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);
- }
- .cell-tooltip-line + .cell-tooltip-line {
- margin-top: 3px;
- }
- </style>
|