2547 lines
78 KiB
Vue
2547 lines
78 KiB
Vue
<template>
|
||
<div class="warehouse-screen">
|
||
<div class="scale-wrapper" :style="scaleWrapperStyle">
|
||
<!-- 动态背景 -->
|
||
<div class="dynamic-bg">
|
||
<!-- 网格线 -->
|
||
<div class="grid-container">
|
||
<div class="grid-lines horizontal"></div>
|
||
<div class="grid-lines vertical"></div>
|
||
</div>
|
||
<!-- 粒子 -->
|
||
<div class="particles-container">
|
||
<div v-for="i in 30" :key="i" class="particle" :style="getParticleStyle(i)"></div>
|
||
</div>
|
||
<!-- 扫描线 -->
|
||
<div class="scan-line"></div>
|
||
<!-- 光晕效果 -->
|
||
<div class="glow-globe globe-1"></div>
|
||
<div class="glow-globe globe-2"></div>
|
||
<div class="glow-globe globe-3"></div>
|
||
</div>
|
||
|
||
<!-- 头部 -->
|
||
<header class="screen-header">
|
||
<dv-border-box-1 class="title-border">
|
||
<div class="title-box">
|
||
<span class="title-icon"><span class="diamond"></span></span>
|
||
<h1 class="screen-title">库区总览大屏</h1>
|
||
<span class="subtitle">Warehouse Overview Dashboard</span>
|
||
<span class="clock-text">{{ currentDate }}</span>
|
||
</div>
|
||
</dv-border-box-1>
|
||
|
||
</header>
|
||
|
||
<!-- 主内容 -->
|
||
<main class="screen-body">
|
||
<!-- 左侧边栏 -->
|
||
<aside class="sidebar">
|
||
|
||
<!-- 仓库结构下拉 -->
|
||
|
||
<div class="tree-panel">
|
||
<dv-border-box-8 :reverse="true">
|
||
<div class="panel-header header-compact">
|
||
<span class="header-icon">
|
||
<svg class="header-icon-svg" viewBox="0 0 24 24" width="18" height="18" color="#00d4ff">
|
||
<circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="1" opacity="0.35"/>
|
||
<circle cx="12" cy="12" r="5" fill="none" stroke="currentColor" stroke-width="0.5" opacity="0.2"/>
|
||
<line x1="2" y1="12" x2="22" y2="12" stroke="currentColor" stroke-width="0.5" opacity="0.2"/>
|
||
<line x1="12" y1="2" x2="12" y2="22" stroke="currentColor" stroke-width="0.5" opacity="0.2"/>
|
||
<g class="radar-beam">
|
||
<path d="M12,12 L12,3 A9,9 0 0,1 21,12 Z" fill="currentColor" opacity="0.25"/>
|
||
</g>
|
||
<circle cx="12" cy="12" r="1.5" fill="#00d4ff" opacity="0.9"/>
|
||
</svg>
|
||
</span>
|
||
<span class="panel-title">仓库结构</span>
|
||
</div>
|
||
<div class="select-body">
|
||
<el-select
|
||
v-model="selectedNodeId"
|
||
placeholder="请选择仓库分区"
|
||
filterable
|
||
clearable
|
||
:teleported="false"
|
||
@change="handleSelectChange"
|
||
class="warehouse-select"
|
||
popper-class="warehouse-select-popper"
|
||
>
|
||
<el-option-group
|
||
v-for="group in warehouseOptions"
|
||
:key="group.label"
|
||
:label="group.label"
|
||
>
|
||
<el-option
|
||
v-for="item in group.options"
|
||
:key="item.id"
|
||
:label="item.name"
|
||
:value="item.id"
|
||
/>
|
||
</el-option-group>
|
||
</el-select>
|
||
</div>
|
||
</dv-border-box-8>
|
||
</div>
|
||
|
||
|
||
<!-- 统计卡片 -->
|
||
<div class="stats-wrapper">
|
||
<dv-border-box-8 :reverse="true">
|
||
<div class="stats-sidebar">
|
||
<div class="total-row">
|
||
<span class="total-num">{{ animatedTotal }}</span>
|
||
<span class="total-lbl">总库位数</span>
|
||
</div>
|
||
<div class="ring-chart-wrapper">
|
||
<div ref="chartRef" class="ring-chart"></div>
|
||
</div>
|
||
<div class="util-text">
|
||
<span class="util-number">{{ statistics.utilization }}%</span>
|
||
<span class="util-label">利用率</span>
|
||
</div>
|
||
<div class="chart-legend">
|
||
<div class="legend-item">
|
||
<span class="chart-dot occupied"></span>
|
||
已占用 <span class="legend-val">{{ statistics.occupied }}</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<span class="chart-dot error"></span>
|
||
异常 <span class="legend-val">{{ statistics.error }}</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<span class="chart-dot available"></span>
|
||
空闲 <span class="legend-val">{{ statistics.available }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="status-bar">
|
||
<span v-for="item in statusBarData" :key="item.label"
|
||
class="status-bar-fill"
|
||
:style="{ width: item.pct + '%', background: item.color }"></span>
|
||
</div>
|
||
<div class="status-bar-labels">
|
||
<div class="status-bar-label" v-for="item in statusBarData" :key="item.label">
|
||
<span class="sbl-dot" :style="{ background: item.color }"></span>
|
||
<span class="sbl-text">{{ item.label }} {{ item.pct }}%</span>
|
||
</div>
|
||
</div>
|
||
<div class="breath-indicators">
|
||
<div v-for="item in statusBarData" :key="'b'+item.label"
|
||
class="breath-dot"
|
||
:style="{
|
||
background: item.color,
|
||
animationDuration: Math.max(1.5, 4 - item.pct * 0.03) + 's',
|
||
boxShadow: '0 0 ' + Math.round(3 + item.pct * 0.12) + 'px ' + item.color
|
||
}"
|
||
:title="item.label + ' ' + item.pct + '%'">
|
||
</div>
|
||
</div>
|
||
<div class="micro-eq">
|
||
<span v-for="i in 18" :key="i" class="micro-eq-bar"
|
||
:style="{
|
||
animationDelay: (i * 0.08) + 's',
|
||
animationDuration: (1.2 + (i % 5) * 0.15) + 's'
|
||
}"></span>
|
||
</div>
|
||
<div class="scan-line-red"><div class="scan-beam"></div></div>
|
||
<div class="mini-chart">
|
||
<div class="mini-chart-bars">
|
||
<div v-for="i in 10" :key="i" class="mini-bar"
|
||
:style="{
|
||
height: (18 + (i * 7) % 52) + '%',
|
||
animationDelay: i * 0.12 + 's',
|
||
animationDuration: (1.8 + (i % 3) * 0.4) + 's'
|
||
}"></div>
|
||
</div>
|
||
<div class="mini-chart-axis"></div>
|
||
</div>
|
||
</div>
|
||
</dv-border-box-8>
|
||
</div>
|
||
<div class="split-wrapper">
|
||
<dv-border-box-8 :reverse="true">
|
||
<div class="split-stats">
|
||
|
||
<div class="split-header">
|
||
<span class="split-title-icon">
|
||
<svg class="flow-icon-svg" viewBox="0 0 24 24" width="16" height="16" color="#00d4ff">
|
||
<line x1="5" y1="7" x2="15" y2="7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.25"/>
|
||
<line x1="2" y1="12" x2="20" y2="12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.4"/>
|
||
<line x1="5" y1="17" x2="15" y2="17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.25"/>
|
||
<circle r="2" fill="#00d4ff" opacity="0.9">
|
||
<animate attributeName="cx" values="2;20;2" dur="2s" repeatCount="indefinite"/>
|
||
<animate attributeName="cy" values="12;12;12" dur="2s" repeatCount="indefinite"/>
|
||
</circle>
|
||
<circle r="4" fill="none" stroke="#00d4ff" stroke-width="0.8" opacity="0.2">
|
||
<animate attributeName="cx" values="2;20;2" dur="2s" repeatCount="indefinite"/>
|
||
<animate attributeName="cy" values="12;12;12" dur="2s" repeatCount="indefinite"/>
|
||
</circle>
|
||
</svg>
|
||
</span>
|
||
<span class="split-title-text">库位拆分状态</span>
|
||
</div>
|
||
<div class="split-body">
|
||
<div class="split-item">
|
||
<span class="split-num unsplit">{{ splitStats.unsplit }}</span>
|
||
<span class="split-lbl">大库位</span>
|
||
<span class="split-pct">({{ splitStats.unsplitPct }}%)</span>
|
||
</div>
|
||
<div class="split-vr"></div>
|
||
<div class="split-item">
|
||
<span class="split-num split">{{ splitStats.split }}</span>
|
||
<span class="split-lbl">小库位</span>
|
||
<span class="split-pct">({{ splitStats.splitPct }}%)</span>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div class="split-bar">
|
||
<div class="split-bar-fill unsplit" :style="{ width: splitStats.unsplitPct + '%' }"></div>
|
||
<div class="split-bar-fill split" :style="{ width: splitStats.splitPct + '%' }"></div>
|
||
</div>
|
||
<div class="split-bar-labels">
|
||
<span class="sbl-item">
|
||
<span class="sbl-dot unsplit-dot"></span>大库位 {{ splitStats.unsplitPct }}%
|
||
</span>
|
||
<span class="sbl-item">
|
||
<span class="sbl-dot split-dot"></span>小库位 {{ splitStats.splitPct }}%
|
||
</span>
|
||
</div>
|
||
|
||
</div>
|
||
</dv-border-box-8>
|
||
</div>
|
||
|
||
<!-- 层列信息 -->
|
||
<div class="layer-wrapper">
|
||
<div class="layer-stats">
|
||
<dv-border-box-8 :reverse="true">
|
||
<div class="layer-header">
|
||
<span class="layer-title-icon">
|
||
<svg class="flow-icon-svg" viewBox="0 0 24 24" width="16" height="16" color="#00d4ff">
|
||
<line x1="5" y1="7" x2="15" y2="7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.25"/>
|
||
<line x1="2" y1="12" x2="20" y2="12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.4"/>
|
||
<line x1="5" y1="17" x2="15" y2="17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.25"/>
|
||
<circle r="2" fill="#00d4ff" opacity="0.9">
|
||
<animate attributeName="cx" values="2;20;2" dur="2.5s" repeatCount="indefinite"/>
|
||
<animate attributeName="cy" values="12;12;12" dur="2.5s" repeatCount="indefinite"/>
|
||
</circle>
|
||
<circle r="4" fill="none" stroke="#00d4ff" stroke-width="0.8" opacity="0.2">
|
||
<animate attributeName="cx" values="2;20;2" dur="2.5s" repeatCount="indefinite"/>
|
||
<animate attributeName="cy" values="12;12;12" dur="2.5s" repeatCount="indefinite"/>
|
||
</circle>
|
||
</svg>
|
||
</span>
|
||
<span class="layer-title-text">层列信息</span>
|
||
</div>
|
||
<div class="layer-body">
|
||
<div class="layer-row">
|
||
<span class="layer-lbl">总列数</span>
|
||
<span class="layer-val">{{ columnStats.totalColumns }}</span>
|
||
</div>
|
||
<div class="layer-row">
|
||
<span class="layer-lbl">1层库位</span>
|
||
<span class="layer-val">{{ columnStats.totalLayer1 }} 个</span>
|
||
</div>
|
||
<div class="layer-row">
|
||
<span class="layer-lbl">2层库位</span>
|
||
<span class="layer-val">{{ columnStats.totalLayer2 }} 个</span>
|
||
</div>
|
||
<div class="layer-row">
|
||
<span class="layer-lbl">每列平均</span>
|
||
<span class="layer-val">{{ columnStats.avgPerColumn }} 个</span>
|
||
</div>
|
||
</div>
|
||
</dv-border-box-8>
|
||
</div>
|
||
</div>
|
||
|
||
</aside>
|
||
|
||
<!-- 中间大框:库位列表 -->
|
||
<section class="main-content">
|
||
<dv-border-box1 ref="borderRef">
|
||
<div class="panel">
|
||
<div class="panel-header">
|
||
<span class="header-icon">
|
||
<svg class="header-icon-svg" viewBox="0 0 24 24" width="18" height="18" color="#00d4ff">
|
||
<line x1="4" y1="8" x2="18" y2="8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.25"/>
|
||
<line x1="4" y1="12" x2="18" y2="12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.4"/>
|
||
<line x1="4" y1="16" x2="18" y2="16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.25"/>
|
||
<circle r="2" fill="#00d4ff" opacity="0.9">
|
||
<animate attributeName="cx" values="4;18;4" dur="2s" repeatCount="indefinite"/>
|
||
<animate attributeName="cy" values="12;12;12" dur="2s" repeatCount="indefinite"/>
|
||
</circle>
|
||
<circle r="4" fill="none" stroke="#00d4ff" stroke-width="0.8" opacity="0.2">
|
||
<animate attributeName="cx" values="4;18;4" dur="2s" repeatCount="indefinite"/>
|
||
<animate attributeName="cy" values="12;12;12" dur="2s" repeatCount="indefinite"/>
|
||
</circle>
|
||
</svg>
|
||
</span>
|
||
<span class="panel-title">{{ selectedNodeName || '库位列表' }}</span>
|
||
<span class="panel-count">共 {{ warehouseList.length }} 条</span>
|
||
</div>
|
||
<div class="panel-body table-container">
|
||
<div v-if="warehouseList.length === 0" class="empty-tip">
|
||
<span class="empty-icon">📭</span>
|
||
<div class="empty-text">请从左侧选择仓库分区</div>
|
||
</div>
|
||
<template v-else>
|
||
<!-- 图例 -->
|
||
<div class="grid-legend">
|
||
<div class="legend-item">
|
||
<span class="legend-dot available"></span>
|
||
<span>空闲</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<span class="legend-dot occupied"></span>
|
||
<span>已占用</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<span class="legend-dot error"></span>
|
||
<span>异常</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<span class="legend-dot has-coil-icon"></span>
|
||
<span>有钢卷</span>
|
||
</div>
|
||
</div>
|
||
<!-- 库位网格:外层容器负责裁剪,内层负责 flex 布局 + transform 移动 -->
|
||
<div ref="gridContainerRef" class="warehouse-grid" @mouseenter="onGridMouseEnter" @mouseleave="onGridMouseLeave">
|
||
<div class="grid-inner" :class="{ 'grid-centered': isGridCentered }" ref="gridInnerRef" @mousedown="onGridDragStart">
|
||
<div
|
||
v-for="colNum in sortedColumnKeys"
|
||
:key="colNum"
|
||
class="grid-column"
|
||
:style="{ minWidth: columnWidth + 'px' }"
|
||
>
|
||
<div class="column-header">{{ colNum }}列</div>
|
||
<div class="column-pair">
|
||
<div class="layer-cells layer1-cells">
|
||
<div
|
||
v-for="wh in gridData[colNum].layer1"
|
||
:key="wh.actualWarehouseId"
|
||
class="grid-cell"
|
||
:class="[getStatusClass(wh), 'cell-layer-1', { 'has-coil': !!wh.currentCoilNo }]"
|
||
:style="cellStyle"
|
||
@click="showDetail(wh, $event)"
|
||
>
|
||
<span class="cell-code">{{ wh.actualWarehouseCode }}</span>
|
||
<span v-if="wh.currentCoilNo" class="cell-coil">{{ wh.currentCoilNo }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="layer-cells layer2-cells" :style="{ marginTop: cellMarginOffset + 'px' }">
|
||
<div
|
||
v-for="wh in gridData[colNum].layer2"
|
||
:key="wh.actualWarehouseId"
|
||
class="grid-cell"
|
||
:class="[getStatusClass(wh), 'cell-layer-2', { 'has-coil': !!wh.currentCoilNo }]"
|
||
:style="cellStyle"
|
||
@click="showDetail(wh, $event)"
|
||
>
|
||
<span class="cell-code">{{ wh.actualWarehouseCode }}</span>
|
||
<span v-if="wh.currentCoilNo" class="cell-coil">{{ wh.currentCoilNo }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</dv-border-box1>
|
||
</section>
|
||
</main>
|
||
|
||
<!-- 底部跃动方块 -->
|
||
<div class="eq-bar-container">
|
||
<div
|
||
v-for="i in 40"
|
||
:key="i"
|
||
class="eq-bar"
|
||
:style="getEqBarStyle(i)"
|
||
></div>
|
||
</div>
|
||
|
||
</div><!-- /scale-wrapper -->
|
||
|
||
<!-- 库位详情浮层 -->
|
||
<div
|
||
v-show="!!detailWarehouse"
|
||
ref="tooltipRef"
|
||
class="warehouse-tooltip"
|
||
:style="tooltipStyle"
|
||
@mousedown.stop
|
||
>
|
||
<template v-if="detailWarehouse">
|
||
<div class="tooltip-header">
|
||
<span class="tooltip-code">{{ detailWarehouse.actualWarehouseCode }}</span>
|
||
<span class="tooltip-status" :class="getStatusClass(detailWarehouse)">{{ getStatusText(detailWarehouse) }}</span>
|
||
</div>
|
||
<div class="tooltip-divider"></div>
|
||
<div class="tooltip-body">
|
||
<div class="tooltip-row">
|
||
<span class="tooltip-label">库位名称</span>
|
||
<span class="tooltip-value">{{ detailWarehouse.actualWarehouseName }}</span>
|
||
</div>
|
||
<div class="tooltip-row">
|
||
<span class="tooltip-label">钢卷编号</span>
|
||
<span class="tooltip-value highlight" v-if="detailWarehouse.currentCoilNo">{{ detailWarehouse.currentCoilNo }}</span>
|
||
<span class="tooltip-value dim" v-else>无</span>
|
||
</div>
|
||
<div class="tooltip-row">
|
||
<span class="tooltip-label">数量</span>
|
||
<span class="tooltip-value">{{ detailWarehouse.coilQuantity || 0 }}</span>
|
||
</div>
|
||
<div class="tooltip-row" v-if="detailWarehouse.parsedInfo">
|
||
<span class="tooltip-label">位置</span>
|
||
<span class="tooltip-value">{{ detailWarehouse.parsedInfo.column }}列 {{ detailWarehouse.parsedInfo.row }}行 {{ detailWarehouse.parsedInfo.layer }}层</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
</div><!-- /warehouse-screen -->
|
||
</template>
|
||
|
||
<script>
|
||
import { ref, reactive, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||
import * as echarts from 'echarts'
|
||
import { treeActualWarehouseTwoLevel, listActualWarehouse } from '@/api/wms/actualWarehouse'
|
||
|
||
export default {
|
||
name: 'WarehouseOverview',
|
||
setup() {
|
||
// 时间显示
|
||
const currentDate = ref('')
|
||
let timeInterval = null
|
||
|
||
// 树形数据
|
||
const warehouseTree = ref([])
|
||
|
||
// 下拉选项(树形结构展平为分组)
|
||
const warehouseOptions = computed(() => {
|
||
return (warehouseTree.value || []).map(parent => ({
|
||
label: parent.actualWarehouseName,
|
||
options: (parent.children || []).map(child => ({
|
||
id: child.actualWarehouseId,
|
||
name: child.actualWarehouseName
|
||
}))
|
||
}))
|
||
})
|
||
|
||
// 选中状态
|
||
const selectedNodeId = ref('')
|
||
const selectedNodeName = ref('')
|
||
|
||
// 库位列表(Mock数据)
|
||
const warehouseList = ref([])
|
||
|
||
// 统计数据
|
||
const statistics = reactive({
|
||
total: 0,
|
||
error: 0,
|
||
occupied: 0,
|
||
available: 0,
|
||
utilization: 0
|
||
})
|
||
|
||
// 拆分状态统计
|
||
const splitStats = computed(() => {
|
||
const total = warehouseList.value.length
|
||
const split = warehouseList.value.filter(w => w.splitStatus === 1).length
|
||
const unsplit = total - split
|
||
return {
|
||
total,
|
||
split,
|
||
unsplit,
|
||
splitPct: total > 0 ? Math.round((split / total) * 100) : 0,
|
||
unsplitPct: total > 0 ? Math.round((unsplit / total) * 100) : 0
|
||
}
|
||
})
|
||
|
||
// 层列信息统计
|
||
const columnStats = computed(() => {
|
||
const keys = Object.keys(gridData.value).sort((a, b) => Number(a) - Number(b))
|
||
const totalLayer1 = keys.reduce((sum, k) => sum + (gridData.value[k]?.layer1?.length || 0), 0)
|
||
const totalLayer2 = keys.reduce((sum, k) => sum + (gridData.value[k]?.layer2?.length || 0), 0)
|
||
return {
|
||
totalColumns: keys.length,
|
||
totalLayer1,
|
||
totalLayer2,
|
||
avgPerColumn: keys.length > 0 ? Math.round((totalLayer1 + totalLayer2) / keys.length) : 0
|
||
}
|
||
})
|
||
|
||
// 状态占比条数据
|
||
const statusBarData = computed(() => {
|
||
const total = statistics.total || 1
|
||
return [
|
||
{ label: '已占用', pct: Math.round(statistics.occupied / total * 100), color: '#7c63ff' },
|
||
{ label: '异常', pct: Math.round(statistics.error / total * 100), color: '#ff6b6b' },
|
||
{ label: '空闲', pct: Math.round(statistics.available / total * 100), color: '#00ff88' }
|
||
]
|
||
})
|
||
|
||
// 动画数字
|
||
const animatedTotal = ref(0)
|
||
const animatedOccupied = ref(0)
|
||
const animatedError = ref(0)
|
||
const animatedAvailable = ref(0)
|
||
const animatedUtilization = ref(0)
|
||
|
||
// 加载状态
|
||
const loading = ref(false)
|
||
|
||
// 缩放比例(transform scale 适配超大屏)
|
||
const scaleRatio = ref(1)
|
||
const scaleWrapperStyle = computed(() => ({
|
||
transform: `scale(${scaleRatio.value})`
|
||
}))
|
||
const updateScale = () => {
|
||
scaleRatio.value = Math.min(window.innerWidth / 1920, window.innerHeight / 1080)
|
||
}
|
||
|
||
// 网格数据
|
||
const gridData = ref({})
|
||
|
||
// 排序的列键
|
||
const sortedColumnKeys = computed(() => {
|
||
return Object.keys(gridData.value).sort((a, b) => Number(a) - Number(b))
|
||
})
|
||
|
||
// 详情浮层
|
||
const detailWarehouse = ref(null)
|
||
const tooltipRef = ref(null)
|
||
const tooltipPos = reactive({ x: 0, y: 0 })
|
||
const tooltipStyle = computed(() => ({
|
||
left: tooltipPos.x + 'px',
|
||
top: tooltipPos.y + 'px'
|
||
}))
|
||
|
||
// 容器尺寸观测(自适应网格)
|
||
const gridContainerRef = ref(null)
|
||
const containerRect = reactive({ width: 800, height: 600 })
|
||
let resizeObserver = null
|
||
let chartResizeObserver = null
|
||
|
||
// 自适应格子尺寸(错层布局:高宽独立)
|
||
const maxLayerRows = computed(() => {
|
||
const keys = sortedColumnKeys.value
|
||
let m = 0
|
||
keys.forEach(k => {
|
||
const col = gridData.value[k]
|
||
if (col) {
|
||
m = Math.max(m, col.layer1.length, col.layer2.length)
|
||
}
|
||
})
|
||
return Math.max(m, 1)
|
||
})
|
||
|
||
const cellHeight = computed(() => {
|
||
const keys = sortedColumnKeys.value
|
||
const colCount = keys.length
|
||
if (!colCount) return 60
|
||
const mRows = maxLayerRows.value
|
||
const ch = containerRect.height
|
||
const h = (ch - 120 - (mRows - 1) * 4) / mRows
|
||
// 大屏下按容器高度比例放大,不再固定封顶 120
|
||
const maxH = Math.floor(ch * 0.15)
|
||
return Math.max(48, Math.min(Math.max(maxH, 120), Math.floor(h)))
|
||
})
|
||
|
||
const cellWidth = computed(() => {
|
||
const keys = sortedColumnKeys.value
|
||
const colCount = keys.length
|
||
if (!colCount) return 60
|
||
const cw = containerRect.width
|
||
const byWidth = (cw - 30 - (colCount - 1) * 8) / colCount
|
||
let w = (byWidth - 6) / 2
|
||
if (colCount <= 15) {
|
||
w = Math.max(w, cellHeight.value)
|
||
}
|
||
// 大屏下按列宽比例放大,不再固定封顶 140
|
||
const maxW = Math.floor(byWidth * 0.45)
|
||
return Math.max(30, Math.min(Math.max(maxW, 140), Math.floor(w)))
|
||
})
|
||
|
||
// 错层偏移量(二层下移半格)
|
||
const cellMarginOffset = computed(() => Math.round(cellHeight.value / 2))
|
||
|
||
// 网格居中(列数少于15时居中更美观)
|
||
const isGridCentered = computed(() => {
|
||
return sortedColumnKeys.value.length <= 15
|
||
})
|
||
|
||
const cellStyle = computed(() => {
|
||
const h = cellHeight.value
|
||
const w = cellWidth.value
|
||
const fs = Math.max(9, Math.min(11, Math.floor(Math.min(w, h) * 0.14)))
|
||
return { width: w + 'px', height: h + 'px', fontSize: fs + 'px' }
|
||
})
|
||
|
||
const columnWidth = computed(() => cellWidth.value * 2 + 14)
|
||
|
||
// 测量网格容器尺寸(始终使用 pre-scale 尺寸,避免缩放反馈循环)
|
||
const measureGridContainer = () => {
|
||
nextTick(() => {
|
||
const el = gridContainerRef.value
|
||
if (!el) return
|
||
containerRect.width = el.clientWidth
|
||
containerRect.height = el.clientHeight
|
||
// 持久化 ResizeObserver,监听容器尺寸变化
|
||
if (!resizeObserver) {
|
||
resizeObserver = new ResizeObserver(entries => {
|
||
for (const entry of entries) {
|
||
containerRect.width = entry.contentRect.width
|
||
containerRect.height = entry.contentRect.height
|
||
}
|
||
nextTick(() => checkOverflow())
|
||
})
|
||
resizeObserver.observe(el)
|
||
}
|
||
// 检查是否需要自动滚动(立即 + 延迟,确保布局完成)
|
||
checkOverflow()
|
||
setTimeout(() => checkOverflow(), 400)
|
||
})
|
||
}
|
||
|
||
// --- 自动轮播滚动(transform 驱动,不依赖 overflow) ---
|
||
const gridInnerRef = ref(null)
|
||
let scrollTimer = null
|
||
let pauseTimer = null
|
||
let stayTimer = null
|
||
let isScrollPaused = false
|
||
|
||
// 滚动共享状态(自动轮播 + 拖拽共用)
|
||
const scrollPos = { x: 0, y: 0 }
|
||
const overflowDist = { x: 0, y: 0 }
|
||
const dragState = { active: false, maybeClick: false, startX: 0, startY: 0, startScrollX: 0, startScrollY: 0 }
|
||
|
||
const applyTransform = () => {
|
||
const inner = gridInnerRef.value
|
||
if (!inner) return
|
||
inner.style.transform = `translate(${-scrollPos.x}px, ${-scrollPos.y}px)`
|
||
}
|
||
|
||
const startAutoScroll = () => {
|
||
stopAutoScroll()
|
||
const container = gridContainerRef.value
|
||
const inner = gridInnerRef.value
|
||
if (!container || !inner) return
|
||
|
||
// 使用预测量的溢出距离
|
||
if (overflowDist.x <= 0 && overflowDist.y <= 0) return
|
||
|
||
const step = () => {
|
||
if (isScrollPaused) return
|
||
|
||
let changed = false
|
||
const inner = gridInnerRef.value
|
||
|
||
if (overflowDist.x > 0) {
|
||
// 渐进减速:距终点 100px 内线性降低速度
|
||
const endX = overflowDist.x + 40
|
||
const remainingX = endX - scrollPos.x
|
||
const decelDist = 100
|
||
const speedX = remainingX < decelDist
|
||
? Math.max(0.2, remainingX / decelDist)
|
||
: 1
|
||
scrollPos.x += speedX
|
||
if (scrollPos.x >= endX) {
|
||
scrollPos.x = 0
|
||
changed = true
|
||
// CSS 过渡实现平滑回弹
|
||
if (inner) {
|
||
inner.style.transition = 'transform 0.35s ease'
|
||
setTimeout(() => {
|
||
if (inner) inner.style.transition = ''
|
||
}, 400)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (overflowDist.y > 0) {
|
||
const endY = overflowDist.y + 30
|
||
const remainingY = endY - scrollPos.y
|
||
const speedY = remainingY < 100
|
||
? Math.max(0.1, remainingY / 100)
|
||
: 0.5
|
||
scrollPos.y += speedY
|
||
if (scrollPos.y >= endY) {
|
||
scrollPos.y = 0
|
||
changed = true
|
||
if (inner) {
|
||
inner.style.transition = 'transform 0.35s ease'
|
||
setTimeout(() => {
|
||
if (inner) inner.style.transition = ''
|
||
}, 400)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (changed) {
|
||
isScrollPaused = true
|
||
clearTimeout(pauseTimer)
|
||
pauseTimer = setTimeout(() => { isScrollPaused = false }, 2000)
|
||
}
|
||
|
||
applyTransform()
|
||
}
|
||
|
||
scrollTimer = setInterval(step, 30)
|
||
}
|
||
|
||
const stopAutoScroll = () => {
|
||
if (scrollTimer) {
|
||
clearInterval(scrollTimer)
|
||
scrollTimer = null
|
||
}
|
||
if (pauseTimer) {
|
||
clearTimeout(pauseTimer)
|
||
pauseTimer = null
|
||
}
|
||
if (stayTimer) {
|
||
clearTimeout(stayTimer)
|
||
stayTimer = null
|
||
}
|
||
}
|
||
|
||
const resetScroll = () => {
|
||
scrollPos.x = 0
|
||
scrollPos.y = 0
|
||
applyTransform()
|
||
}
|
||
|
||
const checkOverflow = () => {
|
||
const el = gridContainerRef.value
|
||
const inner = gridInnerRef.value
|
||
if (!el || !inner) return
|
||
stopAutoScroll()
|
||
resetScroll()
|
||
// 等待一帧让布局稳定再测量溢出
|
||
requestAnimationFrame(() => {
|
||
requestAnimationFrame(() => {
|
||
overflowDist.x = Math.max(inner.scrollWidth - el.clientWidth, 0)
|
||
overflowDist.y = Math.max(inner.scrollHeight - el.clientHeight, 0)
|
||
startAutoScroll()
|
||
})
|
||
})
|
||
}
|
||
|
||
const onGridMouseEnter = () => { isScrollPaused = true }
|
||
const onGridMouseLeave = () => { isScrollPaused = false }
|
||
|
||
// --- 鼠标拖拽滑动网格(带阈值 + 停留窗口) ---
|
||
const onGridDragStart = (e) => {
|
||
if (e.button !== 0) return
|
||
e.preventDefault()
|
||
dragState.maybeClick = true
|
||
dragState.startX = e.clientX
|
||
dragState.startY = e.clientY
|
||
dragState.startScrollX = scrollPos.x
|
||
dragState.startScrollY = scrollPos.y
|
||
isScrollPaused = true
|
||
clearTimeout(stayTimer)
|
||
document.addEventListener('mousemove', onGridDragMove)
|
||
document.addEventListener('mouseup', onGridDragEnd)
|
||
}
|
||
|
||
const onGridDragMove = (e) => {
|
||
const dx = e.clientX - dragState.startX
|
||
const dy = e.clientY - dragState.startY
|
||
|
||
// 阈值判断:移动超过 8px 才真正进入拖拽模式
|
||
if (dragState.maybeClick) {
|
||
if (Math.abs(dx) < 8 && Math.abs(dy) < 8) return
|
||
dragState.maybeClick = false
|
||
dragState.active = true
|
||
const inner = gridInnerRef.value
|
||
if (inner) inner.style.cursor = 'grabbing'
|
||
}
|
||
|
||
if (!dragState.active) return
|
||
|
||
const s = scaleRatio.value
|
||
scrollPos.x = Math.max(0, Math.min(overflowDist.x + 40, dragState.startScrollX - dx / s))
|
||
scrollPos.y = Math.max(0, Math.min(overflowDist.y + 30, dragState.startScrollY - dy * 0.5 / s))
|
||
applyTransform()
|
||
}
|
||
|
||
const onGridDragEnd = () => {
|
||
document.removeEventListener('mousemove', onGridDragMove)
|
||
document.removeEventListener('mouseup', onGridDragEnd)
|
||
|
||
// 未超过阈值,视为点击,不干涉
|
||
if (dragState.maybeClick) return
|
||
|
||
dragState.active = false
|
||
const inner = gridInnerRef.value
|
||
if (inner) inner.style.cursor = ''
|
||
|
||
// 停留窗口:4 秒后恢复自动轮播,让用户有足够时间点击
|
||
clearTimeout(stayTimer)
|
||
stayTimer = setTimeout(() => {
|
||
isScrollPaused = false
|
||
}, 4000)
|
||
}
|
||
|
||
// 监听仓库列表变化,数据就绪后启动滚动
|
||
const stopWatchList = watch(warehouseList, () => {
|
||
nextTick(() => checkOverflow())
|
||
})
|
||
|
||
// 更新时间
|
||
const updateTime = () => {
|
||
const now = new Date()
|
||
currentDate.value = now.toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit'
|
||
})
|
||
}
|
||
|
||
// 获取粒子样式
|
||
const getParticleStyle = (index) => ({
|
||
left: `${Math.random() * 100}%`,
|
||
animationDelay: `${index * 0.3}s`,
|
||
animationDuration: `${6 + Math.random() * 4}s`
|
||
})
|
||
|
||
const getEqBarStyle = (index) => ({
|
||
animationDelay: `${index * 0.06}s`,
|
||
animationDuration: `${1.8 + (index % 7) * 0.25}s`
|
||
})
|
||
|
||
// 数字动画
|
||
const animateValue = (start, end, callback, duration = 1000) => {
|
||
const startTime = performance.now()
|
||
const animate = (currentTime) => {
|
||
const elapsed = currentTime - startTime
|
||
const progress = Math.min(elapsed / duration, 1)
|
||
const easeOut = 1 - Math.pow(1 - progress, 4)
|
||
const currentValue = Math.round(start + (end - start) * easeOut)
|
||
callback(currentValue)
|
||
if (progress < 1) {
|
||
requestAnimationFrame(animate)
|
||
}
|
||
}
|
||
requestAnimationFrame(animate)
|
||
}
|
||
|
||
// ===== 环图 =====
|
||
const chartRef = ref(null)
|
||
let chartInstance = null
|
||
let chartInitTimer = null
|
||
|
||
const initChart = () => {
|
||
// dv-border-box8 渲染较慢,需要延迟以确保容器有尺寸
|
||
if (chartInitTimer) clearTimeout(chartInitTimer)
|
||
chartInitTimer = setTimeout(() => {
|
||
const el = chartRef.value
|
||
if (!el || chartInstance) return
|
||
chartInstance = echarts.init(el, null, { renderer: 'canvas' })
|
||
updateChart()
|
||
// 监听 chart 容器尺寸变化,大屏缩放时实时重绘
|
||
const parent = el.parentElement
|
||
if (parent && !chartResizeObserver) {
|
||
chartResizeObserver = new ResizeObserver(() => {
|
||
if (chartInstance) chartInstance.resize()
|
||
})
|
||
chartResizeObserver.observe(parent)
|
||
}
|
||
}, 300)
|
||
}
|
||
|
||
const updateChart = () => {
|
||
if (!chartInstance) return
|
||
chartInstance.setOption({
|
||
backgroundColor: 'transparent',
|
||
tooltip: { trigger: 'item', formatter: '{b}: {c} 个 ({d}%)' },
|
||
series: [{
|
||
type: 'pie',
|
||
radius: ['50%', '72%'],
|
||
center: ['50%', '50%'],
|
||
avoidLabelOverlap: false,
|
||
label: { show: false },
|
||
emphasis: {
|
||
label: { show: true, fontSize: 14, fontWeight: 'bold', color: '#fff' }
|
||
},
|
||
animationDuration: 1200,
|
||
data: [
|
||
{ value: statistics.occupied || 0, name: '已占用', itemStyle: { color: '#7c63ff' } },
|
||
{ value: statistics.error || 0, name: '异常', itemStyle: { color: '#ff6b6b' } },
|
||
{ value: statistics.available || 0, name: '空闲', itemStyle: { color: '#00ff88' } }
|
||
]
|
||
}],
|
||
})
|
||
}
|
||
|
||
const handleChartResize = () => {
|
||
if (chartInstance) chartInstance.resize()
|
||
}
|
||
|
||
// 获取仓库树形结构(两级)
|
||
const fetchWarehouseTree = async () => {
|
||
try {
|
||
loading.value = true
|
||
const data = await treeActualWarehouseTwoLevel()
|
||
warehouseTree.value = data || []
|
||
} catch (error) {
|
||
console.error('获取仓库树形结构失败:', error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 下拉选择变更
|
||
const handleSelectChange = (value) => {
|
||
if (!value) {
|
||
selectedNodeId.value = ''
|
||
selectedNodeName.value = ''
|
||
warehouseList.value = []
|
||
gridData.value = {}
|
||
return
|
||
}
|
||
// 找到选中节点名称
|
||
for (const parent of warehouseTree.value) {
|
||
if (parent.children) {
|
||
const child = parent.children.find(c => c.actualWarehouseId === value)
|
||
if (child) {
|
||
selectedNodeName.value = child.actualWarehouseName
|
||
getWarehouseList(value)
|
||
return
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 获取库位列表
|
||
const getWarehouseList = async (parentId) => {
|
||
try {
|
||
loading.value = true
|
||
const data = await listActualWarehouse({ parentId })
|
||
warehouseList.value = data || []
|
||
buildGridData()
|
||
measureGridContainer()
|
||
|
||
// 确保环图已初始化(数据加载时 DOM 必定就绪)
|
||
nextTick(() => {
|
||
if (!chartInstance) initChart()
|
||
else updateChart()
|
||
})
|
||
|
||
// 统计数据 — 与 getStatusClass 逻辑一一对应
|
||
const total = warehouseList.value.length
|
||
const occupied = warehouseList.value.filter(w =>
|
||
w.isEnabled === 0 && w.currentCoilNo
|
||
).length
|
||
const available = warehouseList.value.filter(w =>
|
||
w.isEnabled === 1 && !w.currentCoilNo
|
||
).length
|
||
const error = total - occupied - available
|
||
const usable = occupied + available
|
||
const utilization = usable > 0 ? Math.round((occupied / usable) * 100) : 0
|
||
|
||
// 触发动画
|
||
animateValue(animatedTotal.value, total, (val) => { animatedTotal.value = val })
|
||
animateValue(animatedOccupied.value, occupied, (val) => { animatedOccupied.value = val })
|
||
animateValue(animatedError.value, error, (val) => { animatedError.value = val })
|
||
animateValue(animatedAvailable.value, available, (val) => { animatedAvailable.value = val })
|
||
animateValue(animatedUtilization.value, utilization, (val) => { animatedUtilization.value = val })
|
||
|
||
statistics.total = total
|
||
statistics.occupied = occupied
|
||
statistics.error = error
|
||
statistics.available = available
|
||
statistics.utilization = utilization
|
||
updateChart()
|
||
} catch (error) {
|
||
console.error('获取库位列表失败:', error)
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 获取状态文本
|
||
const getStatusText = (row) => {
|
||
if (row.isEnabled === 0 && row.currentCoilNo) {
|
||
return '已占用'
|
||
} else if (row.isEnabled === 1 && !row.currentCoilNo) {
|
||
return '空闲'
|
||
} else {
|
||
return '异常'
|
||
}
|
||
}
|
||
|
||
// 获取状态样式类
|
||
const getStatusClass = (row) => {
|
||
if (row.isEnabled === 0 && row.currentCoilNo) {
|
||
return 'occupied'
|
||
} else if (row.isEnabled === 1 && !row.currentCoilNo) {
|
||
return 'available'
|
||
} else {
|
||
return 'error'
|
||
}
|
||
}
|
||
|
||
// 解析库位编码:支持三级编码 F2B101-01-1 和四级编码 F2B3-X38-2
|
||
const parseWarehouseCode = (code) => {
|
||
if (!code) return null
|
||
// 先尝试四级编码 F2B3-X38-2
|
||
const reg4 = /^([A-Za-z0-9]{3})([^-]+)-X(\d{2})-(\d+)$/
|
||
const match4 = code.match(reg4)
|
||
if (match4) {
|
||
return {
|
||
level: 4,
|
||
column: Number(match4[2]),
|
||
row: Number(match4[3]),
|
||
layer: match4[4]
|
||
}
|
||
}
|
||
// 再尝试三级编码 F2B101-01-1
|
||
const reg3 = /^([A-Za-z0-9]{3})([^-]+)-(\d{2})-(\d+)$/
|
||
const match3 = code.match(reg3)
|
||
if (match3) {
|
||
return {
|
||
level: 3,
|
||
column: Number(match3[2]),
|
||
row: Number(match3[3]),
|
||
layer: match3[4]
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
// 构建网格数据结构:按列分组 → 按层分行
|
||
const buildGridData = () => {
|
||
const columns = {}
|
||
warehouseList.value.forEach(wh => {
|
||
const parsed = parseWarehouseCode(wh.actualWarehouseCode)
|
||
if (!parsed) return
|
||
wh.parsedInfo = parsed
|
||
if (!columns[parsed.column]) {
|
||
columns[parsed.column] = { layer1: [], layer2: [] }
|
||
}
|
||
if (String(parsed.layer) === '1') {
|
||
columns[parsed.column].layer1.push(wh)
|
||
} else {
|
||
columns[parsed.column].layer2.push(wh)
|
||
}
|
||
})
|
||
Object.values(columns).forEach(col => {
|
||
col.layer1.sort((a, b) => a.parsedInfo.row - b.parsedInfo.row)
|
||
col.layer2.sort((a, b) => a.parsedInfo.row - b.parsedInfo.row)
|
||
})
|
||
gridData.value = columns
|
||
}
|
||
|
||
// 显示库位详情浮层(基于视口坐标,tooltip 使用 position: fixed)
|
||
const showDetail = (wh, event) => {
|
||
detailWarehouse.value = wh
|
||
const cellRect = event.currentTarget.getBoundingClientRect()
|
||
const vw = window.innerWidth
|
||
const vh = window.innerHeight
|
||
let left = cellRect.right + 8
|
||
let top = cellRect.top - 10
|
||
const tw = 260
|
||
const th = 180
|
||
const pad = 10
|
||
if (left + tw > vw - pad) {
|
||
left = cellRect.left - tw - 8
|
||
}
|
||
if (top + th > vh - pad) {
|
||
top = vh - th - pad
|
||
}
|
||
tooltipPos.x = Math.round(Math.max(pad, left))
|
||
tooltipPos.y = Math.round(Math.max(pad, top))
|
||
}
|
||
const closeTooltip = () => {
|
||
detailWarehouse.value = null
|
||
}
|
||
const handleDocumentMousedown = (e) => {
|
||
if (!detailWarehouse.value) return
|
||
const el = tooltipRef.value
|
||
if (el && !el.contains(e.target)) {
|
||
closeTooltip()
|
||
}
|
||
}
|
||
|
||
// 全屏切换 → 刷新 popper 位置
|
||
const handleFullscreenChange = () => {
|
||
setTimeout(() => window.dispatchEvent(new Event('resize')), 100)
|
||
}
|
||
|
||
// 初始化
|
||
onMounted(() => {
|
||
updateScale()
|
||
window.addEventListener('resize', updateScale)
|
||
updateTime()
|
||
timeInterval = setInterval(updateTime, 1000)
|
||
// 加载仓库树形结构
|
||
fetchWarehouseTree()
|
||
// 初始化环图
|
||
initChart()
|
||
document.addEventListener('mousedown', handleDocumentMousedown)
|
||
window.addEventListener('resize', handleChartResize)
|
||
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
||
document.addEventListener('webkitfullscreenchange', handleFullscreenChange)
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
stopWatchList()
|
||
if (timeInterval) clearInterval(timeInterval)
|
||
if (resizeObserver) resizeObserver.disconnect()
|
||
if (chartResizeObserver) chartResizeObserver.disconnect()
|
||
stopAutoScroll()
|
||
resetScroll()
|
||
window.removeEventListener('resize', updateScale)
|
||
document.removeEventListener('mousemove', onGridDragMove)
|
||
document.removeEventListener('mouseup', onGridDragEnd)
|
||
if (chartInitTimer) clearTimeout(chartInitTimer)
|
||
document.removeEventListener('mousedown', handleDocumentMousedown)
|
||
window.removeEventListener('resize', handleChartResize)
|
||
document.removeEventListener('fullscreenchange', handleFullscreenChange)
|
||
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
|
||
if (chartInstance) {
|
||
chartInstance.dispose()
|
||
chartInstance = null
|
||
}
|
||
})
|
||
|
||
return {
|
||
scaleWrapperStyle,
|
||
currentDate,
|
||
warehouseTree,
|
||
warehouseOptions,
|
||
warehouseList,
|
||
selectedNodeName,
|
||
statistics,
|
||
splitStats,
|
||
columnStats,
|
||
animatedTotal,
|
||
animatedOccupied,
|
||
animatedError,
|
||
animatedAvailable,
|
||
animatedUtilization,
|
||
statusBarData,
|
||
getParticleStyle,
|
||
getEqBarStyle,
|
||
handleSelectChange,
|
||
getStatusText,
|
||
getStatusClass,
|
||
// 网格
|
||
gridData,
|
||
sortedColumnKeys,
|
||
detailWarehouse,
|
||
showDetail,
|
||
tooltipRef,
|
||
tooltipStyle,
|
||
cellStyle,
|
||
cellMarginOffset,
|
||
columnWidth,
|
||
isGridCentered,
|
||
onGridMouseEnter,
|
||
onGridMouseLeave,
|
||
onGridDragStart,
|
||
chartRef,
|
||
gridContainerRef,
|
||
gridInnerRef
|
||
};
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.warehouse-screen {
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background: linear-gradient(135deg, #050a15 0%, #0a1428 50%, #0d1b34 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
|
||
.scale-wrapper {
|
||
width: 1920px;
|
||
height: 1080px;
|
||
flex-shrink: 0;
|
||
position: relative;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
transform-origin: center center;
|
||
}
|
||
|
||
/* 动态背景 */
|
||
.dynamic-bg {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
pointer-events: none;
|
||
overflow: hidden;
|
||
z-index: 0;
|
||
}
|
||
|
||
/* 网格背景 */
|
||
.grid-container {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.grid-lines {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 100%;
|
||
opacity: 0.15;
|
||
}
|
||
|
||
.grid-lines.horizontal {
|
||
background: repeating-linear-gradient(
|
||
0deg,
|
||
transparent,
|
||
transparent 50px,
|
||
rgba(0, 212, 255, 0.1) 50px,
|
||
rgba(0, 212, 255, 0.1) 51px
|
||
);
|
||
animation: gridMoveHorizontal 20s linear infinite;
|
||
}
|
||
|
||
.grid-lines.vertical {
|
||
background: repeating-linear-gradient(
|
||
90deg,
|
||
transparent,
|
||
transparent 50px,
|
||
rgba(0, 212, 255, 0.1) 50px,
|
||
rgba(0, 212, 255, 0.1) 51px
|
||
);
|
||
animation: gridMoveVertical 20s linear infinite;
|
||
}
|
||
|
||
@keyframes gridMoveHorizontal {
|
||
0% { transform: translateY(0); }
|
||
100% { transform: translateY(50px); }
|
||
}
|
||
|
||
@keyframes gridMoveVertical {
|
||
0% { transform: translateX(0); }
|
||
100% { transform: translateX(50px); }
|
||
}
|
||
|
||
/* 粒子效果 */
|
||
.particles-container {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.particle {
|
||
position: absolute;
|
||
width: 6px;
|
||
height: 6px;
|
||
background: radial-gradient(circle, #00d4ff 0%, rgba(0, 212, 255, 0.5) 50%, transparent 100%);
|
||
border-radius: 50%;
|
||
animation: particleFloat 10s ease-in-out infinite;
|
||
box-shadow:
|
||
0 0 10px #00d4ff,
|
||
0 0 20px rgba(0, 212, 255, 0.6),
|
||
0 0 40px rgba(0, 212, 255, 0.3);
|
||
}
|
||
|
||
@keyframes particleFloat {
|
||
0%, 100% {
|
||
top: 100%;
|
||
transform: translateX(0) scale(0.5);
|
||
opacity: 0;
|
||
}
|
||
10% {
|
||
top: 80%;
|
||
transform: translateX(0) scale(1);
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
top: 50%;
|
||
transform: translateX(30px) scale(1.2);
|
||
}
|
||
90% {
|
||
opacity: 0.8;
|
||
top: 10%;
|
||
transform: translateX(-20px) scale(0.8);
|
||
}
|
||
}
|
||
|
||
/* 扫描线 */
|
||
.scan-line {
|
||
position: absolute;
|
||
top: -10%;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100px;
|
||
background: linear-gradient(
|
||
to bottom,
|
||
transparent 0%,
|
||
rgba(0, 212, 255, 0.1) 50%,
|
||
transparent 100%
|
||
);
|
||
animation: scanDown 8s linear infinite;
|
||
}
|
||
|
||
@keyframes scanDown {
|
||
0% { top: -10%; }
|
||
100% { top: 110%; }
|
||
}
|
||
|
||
/* 光晕效果 */
|
||
.glow-globe {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
filter: blur(100px);
|
||
opacity: 0.3;
|
||
animation: globePulse 8s ease-in-out infinite;
|
||
}
|
||
|
||
.glow-globe.globe-1 {
|
||
width: 400px;
|
||
height: 400px;
|
||
background: radial-gradient(circle, rgba(0, 212, 255, 0.4) 0%, transparent 70%);
|
||
top: 10%;
|
||
right: 10%;
|
||
animation-delay: 0s;
|
||
}
|
||
|
||
.glow-globe.globe-2 {
|
||
width: 300px;
|
||
height: 300px;
|
||
background: radial-gradient(circle, rgba(124, 99, 255, 0.3) 0%, transparent 70%);
|
||
bottom: 20%;
|
||
left: 5%;
|
||
animation-delay: 2s;
|
||
}
|
||
|
||
.glow-globe.globe-3 {
|
||
width: 350px;
|
||
height: 350px;
|
||
background: radial-gradient(circle, rgba(0, 255, 136, 0.2) 0%, transparent 70%);
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
animation-delay: 4s;
|
||
}
|
||
|
||
@keyframes globePulse {
|
||
0%, 100% {
|
||
transform: scale(1);
|
||
opacity: 0.3;
|
||
}
|
||
50% {
|
||
transform: scale(1.3);
|
||
opacity: 0.5;
|
||
}
|
||
}
|
||
|
||
.bg-border {
|
||
position: absolute;
|
||
top: 10px;
|
||
left: 10px;
|
||
right: 10px;
|
||
bottom: 10px;
|
||
pointer-events: none;
|
||
z-index: 100;
|
||
}
|
||
|
||
/* 头部 */
|
||
.screen-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
position: relative;
|
||
z-index: 10;
|
||
}
|
||
|
||
.title-border {
|
||
flex: 1;
|
||
}
|
||
|
||
.title-box {
|
||
padding: 15px 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
background: rgba(10, 20, 40, 0.8);
|
||
}
|
||
|
||
.title-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 28px;
|
||
height: 28px;
|
||
}
|
||
|
||
.diamond {
|
||
width: 14px;
|
||
height: 14px;
|
||
background: #00d4ff;
|
||
transform: rotate(45deg);
|
||
border-radius: 2px;
|
||
box-shadow: 0 0 8px #00d4ff, 0 0 20px rgba(0, 212, 255, 0.4);
|
||
animation: diamondPulse 2s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes diamondPulse {
|
||
0%, 100% {
|
||
box-shadow: 0 0 8px #00d4ff, 0 0 20px rgba(0, 212, 255, 0.4);
|
||
transform: rotate(45deg) scale(1);
|
||
}
|
||
50% {
|
||
box-shadow: 0 0 16px #00d4ff, 0 0 40px rgba(0, 212, 255, 0.6), 0 0 60px rgba(0, 212, 255, 0.3);
|
||
transform: rotate(45deg) scale(1.1);
|
||
}
|
||
}
|
||
|
||
.screen-title {
|
||
font-size: 32px;
|
||
color: #00d4ff;
|
||
margin: 0;
|
||
text-shadow: 0 0 20px rgba(0, 212, 255, 0.8), 0 0 40px rgba(0, 212, 255, 0.4);
|
||
letter-spacing: 8px;
|
||
}
|
||
|
||
.subtitle {
|
||
font-size: 14px;
|
||
color: #6a8cb5;
|
||
margin-left: 10px;
|
||
}
|
||
|
||
.date-border {
|
||
padding: 5px;
|
||
}
|
||
|
||
.date-box {
|
||
padding: 10px 20px;
|
||
background: rgba(10, 20, 40, 0.8);
|
||
}
|
||
|
||
/* 时钟 */
|
||
.clock-text {
|
||
margin-left: auto;
|
||
font-size: 14px;
|
||
color: #00d4ff;
|
||
font-family: 'Courier New', monospace;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
/* 主内容 */
|
||
.screen-body {
|
||
display: flex;
|
||
gap: 20px;
|
||
flex: 1;
|
||
min-height: 0;
|
||
position: relative;
|
||
z-index: 10;
|
||
overflow: visible;
|
||
}
|
||
|
||
.sidebar {
|
||
width: 300px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
min-height: 0;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.tree-panel {
|
||
flex-shrink: 0;
|
||
position: relative;
|
||
background: rgba(10, 20, 40, 0.85);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.tree-panel .header-compact {
|
||
padding: 8px 10px 0;
|
||
border-bottom: none;
|
||
background: none;
|
||
}
|
||
|
||
.tree-panel .header-compact .header-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
|
||
.header-icon-svg {
|
||
display: block;
|
||
}
|
||
|
||
.radar-beam {
|
||
transform-origin: 12px 12px;
|
||
animation: radarSpin 3s linear infinite;
|
||
}
|
||
|
||
@keyframes radarSpin {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
.tree-panel .header-compact .panel-title {
|
||
font-size: 15px;
|
||
}
|
||
|
||
.panel {
|
||
background: rgba(10, 20, 40, 0.85);
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.panel-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 15px 20px;
|
||
background: linear-gradient(90deg, rgba(0, 212, 255, 0.15) 0%, transparent 100%);
|
||
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
|
||
}
|
||
|
||
.header-icon {
|
||
color: #00d4ff;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 16px;
|
||
color: #00d4ff;
|
||
font-weight: 600;
|
||
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
||
}
|
||
|
||
.panel-count {
|
||
font-size: 12px;
|
||
color: #6a8cb5;
|
||
}
|
||
|
||
.panel-body {
|
||
flex: 1;
|
||
padding: 15px 10px;
|
||
overflow: hidden;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* 下拉选择器样式 */
|
||
.select-body {
|
||
padding: 2px 10px 10px;
|
||
}
|
||
|
||
.warehouse-select {
|
||
width: 100%;
|
||
}
|
||
|
||
/* 统计卡片包装 */
|
||
.stats-wrapper {
|
||
flex: 2;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* 侧边栏统计 - 环图 */
|
||
.stats-sidebar {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 6px 8px 0;
|
||
flex: 1;
|
||
background: rgba(10, 20, 40, 0.85);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.total-row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.total-num {
|
||
font-size: 32px;
|
||
font-weight: bold;
|
||
color: #00d4ff;
|
||
text-shadow: 0 0 20px rgba(0, 212, 255, 0.6);
|
||
line-height: 1.1;
|
||
}
|
||
|
||
.total-lbl {
|
||
font-size: 11px;
|
||
color: #6a8cb5;
|
||
margin-top: 1px;
|
||
}
|
||
|
||
.ring-chart {
|
||
width: 100%;
|
||
flex: 1;
|
||
min-height: 80px;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* 环图外圈旋转流光 */
|
||
.ring-chart-wrapper {
|
||
position: relative;
|
||
width: 100%;
|
||
flex: 1;
|
||
min-height: 80px;
|
||
}
|
||
|
||
.ring-chart-wrapper::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
width: 44%;
|
||
height: 44%;
|
||
transform: translate(-50%, -50%) rotate(0deg);
|
||
border-radius: 50%;
|
||
background: conic-gradient(
|
||
transparent 0deg,
|
||
transparent 310deg,
|
||
rgba(0, 212, 255, 0.08) 330deg,
|
||
rgba(0, 212, 255, 0.5) 345deg,
|
||
rgba(0, 212, 255, 0.7) 350deg,
|
||
rgba(0, 212, 255, 0.15) 355deg,
|
||
transparent 360deg
|
||
);
|
||
animation: radarSweep 2.5s linear infinite;
|
||
z-index: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
@keyframes radarSweep {
|
||
from { transform: translate(-50%, -50%) rotate(0deg); }
|
||
to { transform: translate(-50%, -50%) rotate(360deg); }
|
||
}
|
||
|
||
.ring-chart-wrapper::after {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
border-radius: 50%;
|
||
background: conic-gradient(
|
||
transparent 0deg,
|
||
transparent 300deg,
|
||
rgba(0, 212, 255, 0.15) 315deg,
|
||
rgba(0, 212, 255, 0.8) 335deg,
|
||
rgba(0, 212, 255, 0.9) 345deg,
|
||
rgba(0, 212, 255, 0.4) 355deg,
|
||
transparent 360deg
|
||
);
|
||
-webkit-mask: radial-gradient(circle, transparent 44%, #fff 47%, #fff 70%, transparent 72%);
|
||
mask: radial-gradient(circle, transparent 44%, #fff 47%, #fff 70%, transparent 72%);
|
||
animation: ringGlowSpin 2s linear infinite;
|
||
pointer-events: none;
|
||
z-index: 2;
|
||
}
|
||
|
||
@keyframes ringGlowSpin {
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.util-text {
|
||
text-align: center;
|
||
padding: 4px 0 2px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 1px;
|
||
}
|
||
|
||
.util-number {
|
||
font-size: 22px;
|
||
font-weight: bold;
|
||
color: #00d4ff;
|
||
font-family: Arial, sans-serif;
|
||
line-height: 1.2;
|
||
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
|
||
}
|
||
|
||
.util-label {
|
||
font-size: 12px;
|
||
color: #6a8cb5;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.chart-legend {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
width: 100%;
|
||
padding: 0 6px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.chart-legend .legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 11px;
|
||
color: #a0c4e8;
|
||
}
|
||
|
||
.chart-legend .legend-val {
|
||
margin-left: auto;
|
||
font-weight: 600;
|
||
color: #fff;
|
||
}
|
||
|
||
.chart-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.chart-dot.occupied { background: #7c63ff; }
|
||
.chart-dot.error { background: #ff6b6b; }
|
||
.chart-dot.available { background: #00ff88; }
|
||
|
||
/* 状态占比条 */
|
||
.status-bar {
|
||
display: flex;
|
||
width: 100%;
|
||
height: 8px;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
margin: 6px 8px 0;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
gap: 2px;
|
||
}
|
||
|
||
.status-bar-fill {
|
||
height: 100%;
|
||
border-radius: 4px;
|
||
transition: width 0.6s ease;
|
||
min-width: 0;
|
||
}
|
||
|
||
.status-bar-labels {
|
||
display: flex;
|
||
width: 100%;
|
||
justify-content: space-around;
|
||
padding: 3px 8px 0;
|
||
}
|
||
|
||
.status-bar-label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 10px;
|
||
color: #8aa8c8;
|
||
}
|
||
|
||
.sbl-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.sbl-text {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 数据呼吸指示器 */
|
||
.breath-indicators {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 20px;
|
||
padding: 6px 0 0;
|
||
width: 100%;
|
||
}
|
||
|
||
.breath-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
animation: breathPulse 2s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes breathPulse {
|
||
0%, 100% { transform: scale(0.5); opacity: 0.3; }
|
||
50% { transform: scale(1.4); opacity: 1; }
|
||
}
|
||
|
||
/* 微频谱条 */
|
||
.micro-eq {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
gap: 3px;
|
||
padding: 6px 10px 0;
|
||
width: 100%;
|
||
height: 18px;
|
||
}
|
||
|
||
.micro-eq-bar {
|
||
width: 3px;
|
||
min-height: 2px;
|
||
border-radius: 1px;
|
||
background: linear-gradient(to top, transparent, rgba(0, 212, 255, 0.3), #00d4ff);
|
||
animation: microEqBounce 1s ease-in-out infinite alternate;
|
||
}
|
||
|
||
@keyframes microEqBounce {
|
||
0% { height: 2px; opacity: 0.1; }
|
||
50% { height: 14px; opacity: 0.7; }
|
||
100% { height: 3px; opacity: 0.15; }
|
||
}
|
||
|
||
/* 红色扫描线 */
|
||
.scan-line-red {
|
||
position: relative;
|
||
height: 20px;
|
||
width: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.scan-line-red .scan-beam {
|
||
position: absolute;
|
||
top: 0;
|
||
left: -10%;
|
||
width: 8px;
|
||
height: 100%;
|
||
background: linear-gradient(90deg, transparent, rgba(255, 50, 50, 0.8), transparent);
|
||
box-shadow: 0 0 12px rgba(255, 50, 50, 0.6), 0 0 30px rgba(255, 50, 50, 0.2);
|
||
border-radius: 4px;
|
||
animation: scanRedBeam 3s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes scanRedBeam {
|
||
0% { left: -10%; opacity: 0; }
|
||
10% { opacity: 1; }
|
||
50% { left: 100%; opacity: 1; }
|
||
60% { opacity: 0.3; }
|
||
100% { left: 100%; opacity: 0; }
|
||
}
|
||
|
||
/* 动态柱状图 */
|
||
.mini-chart {
|
||
width: 100%;
|
||
padding: 4px 12px 0;
|
||
margin-top: auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.mini-chart-bars {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: space-around;
|
||
height: 36px;
|
||
gap: 2px;
|
||
}
|
||
|
||
.mini-bar {
|
||
width: 8px;
|
||
min-height: 2px;
|
||
border-radius: 2px 2px 0 0;
|
||
background: linear-gradient(to top, rgba(0, 212, 255, 0.15), rgba(0, 212, 255, 0.6));
|
||
animation: miniBarPulse 2s ease-in-out infinite alternate;
|
||
}
|
||
|
||
.mini-chart-axis {
|
||
height: 1px;
|
||
background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.2), transparent);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
@keyframes miniBarPulse {
|
||
0% { opacity: 0.3; transform: scaleY(0.4); transform-origin: bottom; }
|
||
50% { opacity: 0.9; transform: scaleY(1); transform-origin: bottom; }
|
||
100% { opacity: 0.4; transform: scaleY(0.6); transform-origin: bottom; }
|
||
}
|
||
|
||
/* 中间主内容区 */
|
||
.main-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
min-width: 0;
|
||
}
|
||
|
||
.main-panel {
|
||
flex: 1;
|
||
height: 100%;
|
||
border: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.table-container {
|
||
padding: 15px;
|
||
height: calc(100% - 60px);
|
||
overflow: hidden;
|
||
background: rgba(10, 20, 40, 0.85);
|
||
border-radius: 6px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.empty-tip {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 80px 20px;
|
||
color: #6a8cb5;
|
||
background: rgba(10, 20, 40, 0.6);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 64px;
|
||
margin-bottom: 20px;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 16px;
|
||
color: #8892a6;
|
||
}
|
||
|
||
/* 图例 */
|
||
.grid-legend {
|
||
display: flex;
|
||
gap: 24px;
|
||
margin-bottom: 12px;
|
||
padding: 8px 16px;
|
||
background: rgba(0, 212, 255, 0.05);
|
||
border-radius: 4px;
|
||
border: 1px solid rgba(0, 212, 255, 0.1);
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
color: #a0c4e8;
|
||
}
|
||
|
||
.legend-dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 2px;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.legend-dot.available { background: #00ff88; }
|
||
.legend-dot.occupied { background: #7c63ff; }
|
||
.legend-dot.error { background: #ff6b6b; }
|
||
.legend-dot.has-coil-icon {
|
||
background: rgba(0, 212, 255, 0.5);
|
||
border-color: #00d4ff;
|
||
}
|
||
|
||
/* 库位网格 */
|
||
.warehouse-grid {
|
||
overflow: hidden;
|
||
flex: 1;
|
||
min-height: 0;
|
||
position: relative;
|
||
}
|
||
|
||
/* 内层内容容器:flex 布局 + transform 驱动轮播 */
|
||
.grid-inner {
|
||
display: flex;
|
||
gap: 8px;
|
||
padding-bottom: 8px;
|
||
will-change: transform;
|
||
cursor: grab;
|
||
}
|
||
|
||
.grid-inner.grid-centered {
|
||
justify-content: center;
|
||
}
|
||
|
||
.grid-column {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.column-header {
|
||
font-size: 12px;
|
||
color: #6a8cb5;
|
||
text-align: center;
|
||
padding: 4px 0;
|
||
margin-bottom: 4px;
|
||
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
|
||
width: 100%;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.grid-cell {
|
||
border-radius: 4px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
position: relative;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.grid-cell.available {
|
||
background: rgba(0, 255, 136, 0.15);
|
||
border-color: rgba(0, 255, 136, 0.3);
|
||
box-shadow: 0 0 8px rgba(0, 255, 136, 0.1);
|
||
}
|
||
|
||
.grid-cell.occupied {
|
||
background: rgba(124, 99, 255, 0.2);
|
||
border-color: rgba(124, 99, 255, 0.3);
|
||
box-shadow: 0 0 8px rgba(124, 99, 255, 0.15);
|
||
}
|
||
|
||
.grid-cell.error {
|
||
background: rgba(255, 107, 107, 0.15);
|
||
border-color: rgba(255, 107, 107, 0.3);
|
||
box-shadow: 0 0 8px rgba(255, 107, 107, 0.1);
|
||
}
|
||
|
||
.grid-cell.has-coil {
|
||
border-width: 2px;
|
||
}
|
||
|
||
.grid-cell.has-coil.available,
|
||
.grid-cell.has-coil.occupied {
|
||
border-color: #00d4ff;
|
||
box-shadow: 0 0 12px rgba(0, 212, 255, 0.3);
|
||
}
|
||
|
||
.grid-cell:hover {
|
||
transform: scale(1.15);
|
||
z-index: 2;
|
||
}
|
||
|
||
.grid-cell:hover.available {
|
||
box-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
|
||
}
|
||
|
||
.grid-cell:hover.occupied {
|
||
box-shadow: 0 0 20px rgba(124, 99, 255, 0.5);
|
||
}
|
||
|
||
.grid-cell:hover.error {
|
||
box-shadow: 0 0 20px rgba(255, 107, 107, 0.5);
|
||
}
|
||
|
||
.cell-code {
|
||
font-weight: 600;
|
||
color: #fff;
|
||
text-shadow: 0 0 6px rgba(255, 255, 255, 0.3);
|
||
line-height: 1.2;
|
||
text-align: center;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.cell-coil {
|
||
font-size: 0.75em;
|
||
color: #4fc3f7;
|
||
line-height: 1.2;
|
||
text-align: center;
|
||
word-break: break-all;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 1;
|
||
-webkit-box-orient: vertical;
|
||
max-width: 100%;
|
||
padding: 0 2px;
|
||
}
|
||
|
||
.column-pair {
|
||
display: flex;
|
||
gap: 6px;
|
||
width: 100%;
|
||
flex: 1;
|
||
}
|
||
|
||
.layer-cells {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.layer1-cells .grid-cell {
|
||
background: rgba(255, 167, 38, 0.06);
|
||
border-color: rgba(255, 167, 38, 0.2);
|
||
}
|
||
|
||
.layer1-cells .grid-cell.occupied {
|
||
background: rgba(124, 99, 255, 0.2);
|
||
border-color: rgba(124, 99, 255, 0.3);
|
||
}
|
||
|
||
.layer1-cells .grid-cell.available {
|
||
background: rgba(255, 167, 38, 0.12);
|
||
border-color: rgba(0, 255, 136, 0.3);
|
||
}
|
||
|
||
.layer1-cells .grid-cell.error {
|
||
background: rgba(255, 107, 107, 0.2);
|
||
border-color: rgba(255, 107, 107, 0.3);
|
||
}
|
||
|
||
.layer2-cells .grid-cell {
|
||
background: rgba(0, 212, 255, 0.06);
|
||
border-color: rgba(0, 212, 255, 0.2);
|
||
}
|
||
|
||
.layer2-cells .grid-cell.occupied {
|
||
background: rgba(124, 99, 255, 0.2);
|
||
border-color: rgba(124, 99, 255, 0.3);
|
||
}
|
||
|
||
.layer2-cells .grid-cell.available {
|
||
background: rgba(0, 212, 255, 0.1);
|
||
border-color: rgba(0, 255, 136, 0.3);
|
||
}
|
||
|
||
.layer2-cells .grid-cell.error {
|
||
background: rgba(255, 107, 107, 0.2);
|
||
border-color: rgba(255, 107, 107, 0.3);
|
||
}
|
||
|
||
/* 拆分状态卡片 */
|
||
.split-wrapper {
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
.split-stats {
|
||
padding: 8px 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
background: rgba(10, 20, 40, 0.85);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.split-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.split-title-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 20px;
|
||
height: 20px;
|
||
color: #00d4ff;
|
||
}
|
||
|
||
.flow-icon-svg {
|
||
display: block;
|
||
}
|
||
|
||
.split-title-text {
|
||
font-size: 13px;
|
||
color: #00d4ff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.split-body {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-around;
|
||
gap: 8px;
|
||
}
|
||
|
||
.split-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 2px;
|
||
}
|
||
|
||
.split-num {
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.split-num.unsplit {
|
||
color: #ffa726;
|
||
text-shadow: 0 0 10px rgba(255, 167, 38, 0.5);
|
||
}
|
||
|
||
.split-num.split {
|
||
color: #42a5f5;
|
||
text-shadow: 0 0 10px rgba(66, 165, 245, 0.5);
|
||
}
|
||
|
||
.split-lbl {
|
||
font-size: 11px;
|
||
color: #6a8cb5;
|
||
}
|
||
|
||
.split-pct {
|
||
font-size: 10px;
|
||
color: #6a8cb5;
|
||
}
|
||
|
||
.split-vr {
|
||
width: 1px;
|
||
height: 40px;
|
||
flex-shrink: 0;
|
||
background: linear-gradient(to bottom, transparent, rgba(0, 212, 255, 0.3), transparent);
|
||
}
|
||
|
||
.split-bar {
|
||
display: flex;
|
||
height: 8px;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
margin: 8px 12px 0;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.split-bar-fill {
|
||
height: 100%;
|
||
transition: width 0.6s ease;
|
||
min-width: 0;
|
||
}
|
||
|
||
.split-bar-fill.unsplit {
|
||
background: linear-gradient(90deg, #ffa726, #ffb74d);
|
||
}
|
||
|
||
.split-bar-fill.split {
|
||
background: linear-gradient(90deg, #42a5f5, #64b5f6);
|
||
}
|
||
|
||
.split-bar-labels {
|
||
display: flex;
|
||
justify-content: space-around;
|
||
padding: 3px 12px 0;
|
||
font-size: 10px;
|
||
color: #8aa8c8;
|
||
}
|
||
|
||
.sbl-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.sbl-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.unsplit-dot { background: #ffa726; }
|
||
.split-dot { background: #42a5f5; }
|
||
|
||
/* 层列信息卡片 */
|
||
.layer-wrapper {
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
.layer-stats {
|
||
background: rgba(10, 20, 40, 0.85);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.layer-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 8px 12px 0;
|
||
}
|
||
|
||
.layer-title-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 20px;
|
||
height: 20px;
|
||
color: #00d4ff;
|
||
}
|
||
|
||
.layer-title-text {
|
||
font-size: 13px;
|
||
color: #00d4ff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.layer-body {
|
||
padding: 6px 12px 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.layer-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.layer-lbl {
|
||
font-size: 13px;
|
||
color: #6a8cb5;
|
||
}
|
||
|
||
.layer-val {
|
||
font-size: 12px;
|
||
color: #fff;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* ===== 库位详情浮层(毛玻璃 tooltip) ===== */
|
||
.warehouse-tooltip {
|
||
position: fixed;
|
||
z-index: 2000;
|
||
min-width: 220px;
|
||
background: rgba(8, 18, 38, 0.88);
|
||
backdrop-filter: blur(14px);
|
||
-webkit-backdrop-filter: blur(14px);
|
||
border: 1px solid rgba(0, 212, 255, 0.25);
|
||
border-radius: 6px;
|
||
padding: 12px 14px;
|
||
box-shadow:
|
||
0 8px 32px rgba(0, 0, 0, 0.6),
|
||
0 0 20px rgba(0, 212, 255, 0.08);
|
||
animation: tooltipFadeIn 0.2s ease;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
@keyframes tooltipFadeIn {
|
||
from { opacity: 0; transform: translateY(4px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.warehouse-tooltip .tooltip-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.warehouse-tooltip .tooltip-code {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: #00d4ff;
|
||
text-shadow: 0 0 8px rgba(0, 212, 255, 0.4);
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.warehouse-tooltip .tooltip-status {
|
||
font-size: 11px;
|
||
padding: 1px 8px;
|
||
border-radius: 3px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.warehouse-tooltip .tooltip-status.occupied {
|
||
background: rgba(124, 99, 255, 0.2);
|
||
color: #7c63ff;
|
||
border: 1px solid rgba(124, 99, 255, 0.3);
|
||
}
|
||
|
||
.warehouse-tooltip .tooltip-status.available {
|
||
background: rgba(0, 255, 136, 0.2);
|
||
color: #00ff88;
|
||
border: 1px solid rgba(0, 255, 136, 0.3);
|
||
}
|
||
|
||
.warehouse-tooltip .tooltip-status.error {
|
||
background: rgba(255, 107, 107, 0.2);
|
||
color: #ff6b6b;
|
||
border: 1px solid rgba(255, 107, 107, 0.3);
|
||
}
|
||
|
||
.warehouse-tooltip .tooltip-divider {
|
||
height: 1px;
|
||
background: linear-gradient(90deg, rgba(0, 212, 255, 0.3), transparent);
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.warehouse-tooltip .tooltip-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.warehouse-tooltip .tooltip-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.warehouse-tooltip .tooltip-label {
|
||
font-size: 12px;
|
||
color: #6a8cb5;
|
||
}
|
||
|
||
.warehouse-tooltip .tooltip-value {
|
||
font-size: 12px;
|
||
color: #e0e8f0;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.warehouse-tooltip .tooltip-value.highlight {
|
||
color: #00d4ff;
|
||
}
|
||
|
||
.warehouse-tooltip .tooltip-value.dim {
|
||
color: #4a5568;
|
||
}
|
||
|
||
/* ===== 底部跃动方块 ===== */
|
||
.eq-bar-container {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
gap: 3px;
|
||
padding: 0 20px;
|
||
box-sizing: border-box;
|
||
z-index: 20;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.eq-bar {
|
||
width: 3px;
|
||
min-height: 2px;
|
||
border-radius: 2px;
|
||
background: linear-gradient(to top, rgba(0, 212, 255, 0.1), #00d4ff);
|
||
box-shadow: 0 0 6px rgba(0, 212, 255, 0.5);
|
||
animation: eqBounce 1s ease-in-out infinite alternate;
|
||
}
|
||
|
||
@keyframes eqBounce {
|
||
0% { height: 3px; opacity: 0.2; }
|
||
20% { height: 22px; opacity: 0.9; }
|
||
40% { height: 8px; opacity: 0.4; }
|
||
60% { height: 30px; opacity: 1; }
|
||
80% { height: 14px; opacity: 0.6; }
|
||
100% { height: 2px; opacity: 0.15; }
|
||
}
|
||
|
||
</style>
|
||
|
||
<!-- 全局样式:弹窗 + 下拉框 teleport 到 body,不受 scoped 限制 -->
|
||
<style>
|
||
/* 仓库结构下拉框深色主题 */
|
||
.warehouse-select-popper {
|
||
background: rgba(10, 20, 40, 0.95) !important;
|
||
border: 1px solid rgba(0, 212, 255, 0.3) !important;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.warehouse-select-popper .el-select-dropdown {
|
||
background: transparent !important;
|
||
}
|
||
|
||
.warehouse-select-popper .el-scrollbar {
|
||
background: transparent !important;
|
||
}
|
||
|
||
.warehouse-select-popper .el-select-dropdown__item {
|
||
background: transparent !important;
|
||
color: #a0c4e8 !important;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.warehouse-select-popper .el-select-dropdown__item.hover,
|
||
.warehouse-select-popper .el-select-dropdown__item:hover {
|
||
background: rgba(0, 212, 255, 0.15) !important;
|
||
color: #00d4ff !important;
|
||
}
|
||
|
||
.warehouse-select-popper .el-select-dropdown__item.selected {
|
||
background: rgba(0, 212, 255, 0.1) !important;
|
||
color: #00d4ff !important;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.warehouse-select-popper .el-select-group__title {
|
||
background: transparent !important;
|
||
color: #6a8cb5 !important;
|
||
font-size: 12px;
|
||
padding-left: 15px;
|
||
}
|
||
|
||
.warehouse-select-popper .el-select-dropdown__wrap {
|
||
background: transparent !important;
|
||
}
|
||
|
||
.warehouse-select-popper .el-select-dropdown__list {
|
||
background: transparent !important;
|
||
}
|
||
|
||
.warehouse-select .el-select__wrapper {
|
||
background-color: rgba(10, 20, 40, 0.95) !important;
|
||
box-shadow: 0 0 0 1px rgba(0, 212, 255, 0.3) inset !important;
|
||
border-radius: 4px;
|
||
min-height: 40px;
|
||
padding: 0 12px;
|
||
}
|
||
|
||
.warehouse-select .el-select__wrapper:hover {
|
||
box-shadow: 0 0 0 1px rgba(0, 212, 255, 0.5) inset !important;
|
||
}
|
||
|
||
.warehouse-select .el-select__wrapper.is-focus {
|
||
box-shadow: 0 0 0 1px #00d4ff inset !important;
|
||
}
|
||
|
||
.warehouse-select .el-select__input {
|
||
color: #00d4ff !important;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.warehouse-select .el-select__input::placeholder {
|
||
color: #6a8cb5 !important;
|
||
}
|
||
|
||
.warehouse-select .el-select__caret {
|
||
color: #00d4ff !important;
|
||
font-size: 14px;
|
||
}
|
||
|
||
</style>
|