<template>
|
<div class="container">
|
<div class="content-wrapper">
|
<!-- 控制面板区域 -->
|
<div class="control-panel">
|
<div class="panel-header">
|
<h3>控制面板</h3>
|
</div>
|
|
<div class="panel-body">
|
<div class="form-group">
|
<label class="form-label">仓库:</label>
|
<el-select size="mini" filterable v-model="selectedWarehouse" placeholder="请选择仓库" class="full-width"
|
@change="handleWarehouseChange">
|
<el-option v-for="item in warehouseList" :key="item.warehouseId" :value="item.warehouseId"
|
:label="getWarehouseName(item.warehouseId)">
|
</el-option>
|
</el-select>
|
</div>
|
|
<div class="form-group">
|
<label class="form-label">巷道:</label>
|
<el-select size="mini" clearable filterable v-model="selectedRoadwayNo" placeholder="请选择巷道" class="full-width"
|
@change="handleRoadwayNoChange">
|
<el-option v-for="item in roadwayNoList" :key="item" :value="item" :label="'巷道 ' + item"></el-option>
|
</el-select>
|
</div>
|
|
<el-button type="success" class="refresh-btn" @click="fetchLocationStatus" :loading="loading">
|
刷新
|
</el-button>
|
|
<div class="legend-section">
|
<div class="legend-header">
|
<h4>图例说明</h4>
|
</div>
|
<div class="legend-list">
|
<div class="legend-item" v-for="(item, index) in infoMsg" :key="index">
|
<span class="color-box" :style="{ 'background-color': item.bgcolor }"></span>
|
<span class="legend-label">{{ item.msg }}</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 货位展示区域 -->
|
<div class="main-content">
|
<div v-if="loading" class="loading-container">
|
<el-skeleton :rows="6" animated />
|
</div>
|
|
<div v-else-if="locationData.length > 0" class="location-container">
|
<div class="location-header">
|
<div class="location-info">
|
<span>当前查看:{{ getWarehouseName(selectedWarehouse) }} - 巷道 {{ selectedRoadwayNo }}</span>
|
<span class="total-count">共 {{ totalLocations }} 个货位</span>
|
</div>
|
</div>
|
|
<div class="layers-container">
|
<div class="layer-row" v-for="layer in sortedLayerData" :key="layer.layer">
|
<div class="layer-title-area">
|
<h3 class="layer-title">层 {{ layer.layer }}</h3>
|
<span class="layer-count">{{ getLayerLocations(layer) }} 个货位</span>
|
</div>
|
|
<div class="layer-content-wrap">
|
<div class="layer-content">
|
<div class="location-column" v-for="column in sortedColumns(layer.columns)" :key="column.column">
|
<div class="column-label">列 {{ column.column }}</div>
|
<div class="locations-wrapper">
|
<div class="location-item" v-for="depth in sortedDepthsByRow(column.depths)" :key="depth.depth"
|
:class="getLocationStatusClass(depth)"
|
@mouseenter="showTooltip(depth, column.column, layer.layer, $event)" @mouseleave="hideTooltip"
|
@click="handleLocationClick(depth)">
|
<div class="location-code">
|
{{ depth.row || '?' }}排-{{ column.column }}列-{{ layer.layer }}层-{{ depth.depth == 1 ? '浅' : '深' }}
|
</div>
|
<div class="location-status" v-if="depth.enableStatus !== 0">
|
禁用
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 无数据提示 -->
|
<div v-else class="empty-container">
|
<el-empty description="暂无货位数据">
|
<template #description>
|
<p>请选择仓库和巷道来查看货位信息</p>
|
</template>
|
</el-empty>
|
</div>
|
|
<!-- 悬浮提示框 -->
|
<div v-if="showTooltipFlag" class="location-tooltip" :style="{
|
left: tooltipPosition.x + 'px',
|
top: tooltipPosition.y + 'px',
|
}">
|
<div class="tooltip-content">
|
<div class="tooltip-header">
|
<h4>货位详情</h4>
|
</div>
|
<div class="tooltip-body">
|
<div class="tooltip-row">
|
<span class="tooltip-label">位置:</span>
|
<span class="tooltip-value">{{ getLocationCode(currentLocation) }}</span>
|
</div>
|
<div class="tooltip-row">
|
<span class="tooltip-label">巷道:</span>
|
<span class="tooltip-value">{{ selectedRoadwayNo }}</span>
|
</div>
|
<div class="tooltip-row">
|
<span class="tooltip-label">深度:</span>
|
<span class="tooltip-value">{{ currentLocation.depth === 1 ? '浅' : '深' }}</span>
|
</div>
|
<div class="tooltip-row">
|
<span class="tooltip-label">类型:</span>
|
<span class="tooltip-value">{{ getLocationTypeText(currentLocation.locationType) }}</span>
|
</div>
|
<div class="tooltip-row">
|
<span class="tooltip-label">状态:</span>
|
<span class="tooltip-value" :class="getStatusClass(currentLocation.locationStatus)">
|
{{ getStatusText(currentLocation.locationStatus) }}
|
</span>
|
</div>
|
<div class="tooltip-row">
|
<span class="tooltip-label">启用状态:</span>
|
<span class="tooltip-value" :class="{ 'status-disabled': currentLocation.enableStatus !== 0 }">
|
{{ getEnableStatusText(currentLocation.enableStatus) }}
|
</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
import { ElButton, ElMessage, ElSelect, ElOption, ElEmpty, ElSkeleton } from "element-plus";
|
|
export default {
|
data() {
|
return {
|
warehouseList: [],
|
roadwayNoList: [],
|
selectedWarehouse: null,
|
selectedRoadwayNo: null,
|
infoMsg: [
|
{ bgcolor: "lightgreen", msg: "空货位" },
|
{ bgcolor: "orange", msg: "有货" },
|
{ bgcolor: "#2BB3D5", msg: "锁定" },
|
{ bgcolor: "#ccc", msg: "禁用" },
|
{ bgcolor: "#b7ba6b", msg: "其它" },
|
],
|
locationData: [],
|
showTooltipFlag: false,
|
currentLocation: null,
|
tooltipPosition: { x: 0, y: 0 },
|
currentColumn: null,
|
currentLayer: null,
|
loading: false
|
};
|
},
|
computed: {
|
sortedLayerData() {
|
const layerMap = {};
|
|
this.locationData.forEach(layer => {
|
if (!layerMap[layer.layer]) {
|
layerMap[layer.layer] = {
|
layer: layer.layer,
|
columns: []
|
};
|
}
|
|
layer.columns?.forEach(column => {
|
layerMap[layer.layer].columns.push({
|
column: column.column,
|
depths: column.depths || []
|
});
|
});
|
});
|
|
return Object.values(layerMap).sort((a, b) => a.layer - b.layer);
|
},
|
|
totalLocations() {
|
let count = 0;
|
this.locationData.forEach(layer => {
|
layer.columns?.forEach(column => {
|
count += column.depths?.length || 0;
|
});
|
});
|
return count;
|
}
|
},
|
methods: {
|
sortedColumns(columns) {
|
// 按列号排序
|
return columns?.sort((a, b) => a.column - b.column) || [];
|
},
|
|
sortedDepthsByRow(depths) {
|
// 首先按行(row)排序,然后按深度(depth)排序
|
return depths?.sort((a, b) => {
|
// 如果行号相同,按深度排序
|
if (a.row === b.row) {
|
return a.depth - b.depth;
|
}
|
// 按行号排序(这里假设行号是数字或可比较的字符串)
|
return this.compareRows(a.row, b.row);
|
}) || [];
|
},
|
|
// 比较行号的辅助方法
|
compareRows(rowA, rowB) {
|
// 如果都是数字,按数字比较
|
if (!isNaN(rowA) && !isNaN(rowB)) {
|
return Number(rowA) - Number(rowB);
|
}
|
// 否则按字符串比较
|
return String(rowA).localeCompare(String(rowB));
|
},
|
|
async fetchWarehouseData() {
|
try {
|
this.loading = true;
|
const response = await this.http.get("/api/LocationInfo/GetArea");
|
this.warehouseList = response.data || [];
|
|
if (this.warehouseList.length > 0) {
|
this.selectedWarehouse = this.warehouseList[0].warehouseId;
|
this.roadwayNoList = this.warehouseList[0].roadwayNo || [];
|
|
if (this.roadwayNoList.length > 0) {
|
this.selectedRoadwayNo = this.roadwayNoList[0];
|
await this.fetchLocationStatus();
|
}
|
}
|
} catch (error) {
|
console.error("获取仓库数据失败:", error);
|
this.warehouseList = [];
|
this.roadwayNoList = [];
|
ElMessage.error("获取仓库数据失败,请稍后重试");
|
} finally {
|
this.loading = false;
|
}
|
},
|
|
async fetchLocationStatus() {
|
if (!this.selectedWarehouse || !this.selectedRoadwayNo) {
|
ElMessage.warning("请选择仓库和巷道");
|
return;
|
}
|
|
try {
|
this.loading = true;
|
const response = await this.http.get(
|
`/api/LocationInfo/GetLocationStatus?WarehouseId=${this.selectedWarehouse}&RoadwayNo=${this.selectedRoadwayNo}`
|
);
|
|
if (response.data && response.status) {
|
this.locationData = response.data || [];
|
|
// 验证数据中是否有row字段
|
console.log("货位数据示例:", this.locationData[0]);
|
if (this.locationData.length > 0 && this.locationData[0].columns) {
|
console.log("第一个货位的row字段:", this.locationData[0].columns[0]?.depths?.[0]?.row);
|
}
|
|
if (this.locationData.length === 0) {
|
ElMessage.info("该巷道没有货位数据");
|
}
|
} else {
|
this.locationData = [];
|
ElMessage.error(response.data?.message || "获取货位状态失败");
|
}
|
} catch (error) {
|
console.error("获取货位状态失败:", error);
|
this.locationData = [];
|
ElMessage.error("获取货位状态失败,请稍后重试");
|
} finally {
|
this.loading = false;
|
}
|
},
|
|
handleWarehouseChange() {
|
const selectedWarehouse = this.warehouseList.find(
|
w => w.warehouseId === this.selectedWarehouse
|
);
|
this.roadwayNoList = selectedWarehouse ? selectedWarehouse.roadwayNo : [];
|
this.selectedRoadwayNo = this.roadwayNoList.length > 0 ? this.roadwayNoList[0] : null;
|
this.fetchLocationStatus();
|
},
|
|
getWarehouseName(warehouseId) {
|
const warehouseMap = {
|
1: '原材料仓',
|
2: '成品仓'
|
};
|
return warehouseMap[warehouseId] || `仓库 ${warehouseId}`;
|
},
|
|
handleRoadwayNoChange() {
|
this.fetchLocationStatus();
|
},
|
|
getLocationStatusClass(depth) {
|
if (depth.enableStatus !== 0) {
|
return 'location-disabled';
|
}
|
|
switch (depth.locationStatus) {
|
case 0: return 'location-empty'; // 空货位
|
case 1: return 'location-locked'; // 锁定
|
case 100: return 'location-occupied'; // 有货
|
default: return 'location-other'; // 其他状态
|
}
|
},
|
|
getLayerLocations(layer) {
|
let count = 0;
|
layer.columns?.forEach(column => {
|
count += column.depths?.length || 0;
|
});
|
return count;
|
},
|
|
showTooltip(depth, column, layer, event) {
|
this.currentLocation = depth;
|
this.currentColumn = column;
|
this.currentLayer = layer;
|
this.showTooltipFlag = true;
|
|
const offsetX = 15;
|
const offsetY = 15;
|
this.tooltipPosition = {
|
x: event.clientX + offsetX,
|
y: event.clientY + offsetY,
|
};
|
},
|
|
hideTooltip() {
|
this.showTooltipFlag = false;
|
this.currentLocation = null;
|
this.currentColumn = null;
|
this.currentLayer = null;
|
},
|
|
handleLocationClick(depth) {
|
console.log('点击货位:', depth);
|
},
|
|
getLocationCode(location) {
|
if (!location) return '';
|
return `${location.row || '?'}排-${this.currentColumn}列-${this.currentLayer}层-${location.depth === 1 ? '浅' : '深'}`;
|
},
|
|
getStatusText(status) {
|
const statusMap = {
|
0: "空货位",
|
1: "锁定",
|
100: "有货",
|
};
|
return statusMap[status] || "未知状态";
|
},
|
|
getStatusClass(status) {
|
const classMap = {
|
0: 'status-empty',
|
1: 'status-locked',
|
100: 'status-occupied'
|
};
|
return classMap[status] || '';
|
},
|
|
getLocationTypeText(type) {
|
const typeMap = {
|
1: "布卷",
|
2: "松布卷",
|
3: "成品货位"
|
};
|
return typeMap[type] || "未知类型";
|
},
|
|
getEnableStatusText(status) {
|
return status === 0 ? '启用' : '禁用';
|
}
|
},
|
mounted() {
|
this.fetchWarehouseData();
|
},
|
components: {
|
ElButton,
|
ElSelect,
|
ElOption,
|
ElEmpty,
|
ElSkeleton
|
},
|
};
|
</script>
|
|
<style scoped>
|
/* 样式部分保持不变,与之前相同 */
|
.container {
|
display: flex;
|
flex-direction: column;
|
height: 100vh;
|
padding: 16px;
|
box-sizing: border-box;
|
background-color: #f0f2f5;
|
}
|
|
.header {
|
text-align: center;
|
margin-bottom: 20px;
|
padding-bottom: 16px;
|
border-bottom: 1px solid #e8e8e8;
|
}
|
|
.title {
|
font-size: 22px;
|
font-weight: 600;
|
margin: 0;
|
color: #1890ff;
|
}
|
|
.content-wrapper {
|
display: flex;
|
flex: 1;
|
min-height: 0;
|
gap: 16px;
|
}
|
|
.control-panel {
|
width: 260px;
|
background-color: white;
|
border-radius: 8px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
display: flex;
|
flex-direction: column;
|
flex-shrink: 0;
|
overflow: hidden;
|
}
|
|
.panel-header {
|
padding: 16px;
|
border-bottom: 1px solid #e8e8e8;
|
background-color: #fafafa;
|
}
|
|
.panel-header h3 {
|
margin: 0;
|
font-size: 16px;
|
font-weight: 600;
|
color: #333;
|
}
|
|
.panel-body {
|
padding: 16px;
|
display: flex;
|
flex-direction: column;
|
gap: 20px;
|
}
|
|
.form-group {
|
margin-bottom: 0;
|
}
|
|
.form-label {
|
display: block;
|
margin-bottom: 6px;
|
font-size: 14px;
|
color: #666;
|
font-weight: 500;
|
}
|
|
.full-width {
|
width: 100%;
|
}
|
|
.refresh-btn {
|
margin-top: 8px;
|
width: 100%;
|
height: 32px;
|
}
|
|
.legend-section {
|
margin-top: 8px;
|
padding-top: 16px;
|
border-top: 1px solid #e8e8e8;
|
}
|
|
.legend-header h4 {
|
margin: 0 0 12px 0;
|
font-size: 15px;
|
font-weight: 600;
|
color: #333;
|
}
|
|
.legend-list {
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
}
|
|
.legend-item {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
}
|
|
.color-box {
|
width: 20px;
|
height: 20px;
|
border-radius: 4px;
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
flex-shrink: 0;
|
}
|
|
.depth-shallow-legend {
|
background-color: rgba(255, 255, 255, 0.7);
|
border: 1px solid #2c3e50;
|
}
|
|
.depth-deep-legend {
|
background-color: rgba(0, 0, 0, 0.7);
|
border: 1px solid #000;
|
}
|
|
.legend-label {
|
font-size: 13px;
|
color: #666;
|
}
|
|
.main-content {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
min-width: 0;
|
}
|
|
.loading-container {
|
flex: 1;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
background: white;
|
border-radius: 8px;
|
padding: 24px;
|
}
|
|
.location-container {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
background: white;
|
border-radius: 8px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
overflow: hidden;
|
min-height: 0;
|
}
|
|
.location-header {
|
padding: 16px 20px;
|
background: #fafafa;
|
border-bottom: 1px solid #e8e8e8;
|
flex-shrink: 0;
|
}
|
|
.location-info {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
font-size: 14px;
|
}
|
|
.total-count {
|
font-weight: 600;
|
color: #1890ff;
|
}
|
|
.layers-container {
|
width: 100%;
|
height: 100%;
|
flex: 1;
|
overflow-y: auto;
|
padding: 20px;
|
display: flex;
|
flex-direction: column;
|
gap: 24px;
|
}
|
|
.layer-row {
|
background: #f9f9f9;
|
border-radius: 8px;
|
border: 1px solid #e8e8e8;
|
display: flex;
|
flex-direction: column;
|
min-height: auto;
|
}
|
|
.layer-title-area {
|
padding: 12px 16px;
|
background: #f0f0f0;
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
flex-shrink: 0;
|
}
|
|
.layer-title {
|
margin: 0;
|
font-size: 16px;
|
font-weight: 600;
|
color: #333;
|
}
|
|
.layer-count {
|
font-size: 12px;
|
color: #666;
|
background: white;
|
padding: 4px 10px;
|
border-radius: 12px;
|
border: 1px solid #ddd;
|
}
|
|
.layer-content-wrap {
|
width: 100%;
|
overflow: hidden;
|
flex: 1;
|
}
|
|
.layer-content {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 16px;
|
padding: 16px;
|
min-width: 0;
|
box-sizing: border-box;
|
}
|
|
.location-column {
|
flex: 0 0 auto;
|
width: 140px;
|
min-height: 120px;
|
display: flex;
|
flex-direction: column;
|
box-sizing: border-box;
|
}
|
|
.column-label {
|
font-size: 13px;
|
font-weight: 600;
|
color: #666;
|
text-align: center;
|
margin-bottom: 8px;
|
padding-bottom: 4px;
|
border-bottom: 1px dashed #ddd;
|
flex-shrink: 0;
|
}
|
|
.locations-wrapper {
|
display: flex;
|
flex-direction: column;
|
gap: 6px;
|
flex: 1;
|
min-height: 0;
|
}
|
|
.location-item {
|
min-height: 40px;
|
padding: 6px 4px;
|
border-radius: 4px;
|
text-align: center;
|
cursor: pointer;
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
transition: all 0.2s;
|
overflow: hidden;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
gap: 2px;
|
flex: 1;
|
}
|
|
.location-item:hover {
|
transform: scale(1.05);
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
z-index: 10;
|
}
|
|
.location-code {
|
font-size: 11px;
|
font-weight: 500;
|
line-height: 1.2;
|
word-break: break-all;
|
}
|
|
.depth-indicator {
|
font-size: 10px;
|
padding: 2px 4px;
|
border-radius: 3px;
|
margin-top: 2px;
|
font-weight: bold;
|
transition: all 0.2s;
|
}
|
|
.depth-shallow {
|
background-color: rgba(255, 255, 255, 0.7);
|
color: #2c3e50;
|
}
|
|
.depth-deep {
|
background-color: rgba(0, 0, 0, 0.7);
|
color: white;
|
}
|
|
.location-item:hover .depth-indicator {
|
transform: scale(1.1);
|
}
|
|
.location-status {
|
font-size: 10px;
|
padding: 1px 4px;
|
border-radius: 2px;
|
background: rgba(0, 0, 0, 0.1);
|
}
|
|
.location-empty {
|
background-color: lightgreen;
|
color: #333;
|
}
|
|
.location-occupied {
|
background-color: orange;
|
color: white;
|
}
|
|
.location-locked {
|
background-color: #2BB3D5;
|
color: white;
|
}
|
|
.location-disabled {
|
background-color: #ccc;
|
color: #666;
|
cursor: not-allowed;
|
}
|
|
.location-other {
|
background-color: #b7ba6b;
|
color: white;
|
}
|
|
.empty-container {
|
flex: 1;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
background: white;
|
border-radius: 8px;
|
}
|
|
.location-tooltip {
|
position: fixed;
|
z-index: 9999;
|
background-color: white;
|
border: 1px solid #ddd;
|
border-radius: 6px;
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
pointer-events: none;
|
min-width: 220px;
|
max-width: 300px;
|
}
|
|
.tooltip-content {
|
overflow: hidden;
|
}
|
|
.tooltip-header {
|
padding: 12px 16px;
|
background-color: #1890ff;
|
color: white;
|
}
|
|
.tooltip-header h4 {
|
margin: 0;
|
font-size: 14px;
|
font-weight: 600;
|
}
|
|
.tooltip-body {
|
padding: 12px 16px;
|
}
|
|
.tooltip-row {
|
display: flex;
|
margin-bottom: 8px;
|
font-size: 13px;
|
line-height: 1.4;
|
}
|
|
.tooltip-row:last-child {
|
margin-bottom: 0;
|
}
|
|
.tooltip-label {
|
flex: 0 0 80px;
|
color: #666;
|
font-weight: 500;
|
}
|
|
.tooltip-value {
|
flex: 1;
|
color: #333;
|
word-break: break-all;
|
}
|
|
.status-disabled {
|
color: #f5222d;
|
}
|
|
@media (max-width: 1200px) {
|
.location-column {
|
width: 130px;
|
}
|
}
|
|
@media (max-width: 768px) {
|
.content-wrapper {
|
flex-direction: column;
|
}
|
|
.control-panel {
|
width: 100%;
|
}
|
|
.location-column {
|
width: 120px;
|
}
|
|
.layer-content {
|
gap: 12px;
|
}
|
}
|
|
@media (max-width: 480px) {
|
.location-column {
|
width: 110px;
|
}
|
|
.location-code {
|
font-size: 10px;
|
}
|
|
.layer-content {
|
gap: 10px;
|
padding: 12px;
|
}
|
|
@media (max-width: 380px) {
|
.location-column {
|
width: 100px;
|
}
|
}
|
}
|
|
.layers-container::-webkit-scrollbar {
|
width: 8px;
|
}
|
|
.layers-container::-webkit-scrollbar-track {
|
background: #f1f1f1;
|
border-radius: 4px;
|
}
|
|
.layers-container::-webkit-scrollbar-thumb {
|
background: #c1c1c1;
|
border-radius: 4px;
|
}
|
|
.layers-container::-webkit-scrollbar-thumb:hover {
|
background: #a8a8a8;
|
}
|
</style>
|