1
wankeda
2026-03-16 d5538af4a0bbc5511990aceb3431fb1caa9bbc65
ÏîÄ¿´úÂë/WMS/WIDESEA_WMSClient/src/views/Home.vue
@@ -1,24 +1,589 @@
<template>
  <div class="title"></div>
  <div class="container" id="vol-main"> <!-- æ–°å¢žid,匹配JS中的获取逻辑 -->
    <div class="header">
      <h2 class="title">货位排图</h2>
    </div>
    <div class="content-wrapper">
      <!-- æŽ§åˆ¶é¢æ¿åŒºåŸŸ -->
      <div class="control-panel">
        <div class="form-group">
          <label>区域:</label>
          <el-select size="mini" filterable v-model="Area.shelf_code" placeholder="请选择" class="full-width">
            <el-option v-for="item in slectData" :value="item.shelf_code" :label="item.house_name"
              :key="item.shelf_code"></el-option> <!-- ä¿®å¤key值,避免重复 -->
          </el-select>
        </div>
        <div class="form-group">
          <label>排:</label>
          <el-select size="mini" clearable filterable @change="SCChange" v-model="Area.tunnel" placeholder="请选择"
            class="full-width">
            <el-option v-for="item in scList" :value="item" :label="item" :key="item"></el-option>
          </el-select>
        </div>
        <el-button type="success" class="refresh-btn" @click="GetViewData">
          åˆ·æ–°
        </el-button>
        <el-button plain class="notify-trigger-btn" @click="open2">
          è­¦å‘Š
        </el-button>
        <div class="legend-section">
          <h4>说明</h4>
          <div class="legend-grid">
            <div class="legend-item" v-for="item in infoMsg" :key="item.state"> <!-- ä¿®å¤key值,用state更唯一 -->
              <span class="color-box" :style="{ 'background-color': item.bgcolor }"></span>
              <span class="legend-label">{{ item.msg }} {{ item.quantity }}</span>
            </div>
          </div>
        </div>
        <!-- é¥¼å›¾å®¹å™¨ï¼šä¿®æ”¹æ ·å¼ä½¿å…¶å±…中 -->
        <div class="echarts-container">
          <div ref="myChart" class="chart-inner"></div>
        </div>
      </div>
      <!-- è´§ä½å±•示区域 -->
      <div class="location-view">
        <!-- å¢žåŠ æ— æ•°æ®æç¤º -->
        <div v-if="locationData.length === 0" class="empty-tip">暂无货位数据,请选择区域并点击刷新</div>
        <div class="layer-container" v-for="layer in locationData" :key="layer.index">
          <h3 class="layer-title">第{{ layer.index }}层</h3>
          <div class="row" v-for="row in layer.rows" :key="row.index">
            <div class="location-cell" :style="{ 'background-color': GetBgColor(col) }" v-for="col in row.cols"
              :key="col.index" @mouseenter="showTooltip(col, $event)" @mouseleave="hideTooltip">
              {{ row.index }}-{{ col.index }}-{{ layer.index }}
            </div>
          </div>
        </div>
      </div>
      <!-- æ‚¬æµ®æç¤ºæ¡† -->
      <div v-if="showTooltipFlag" class="location-tooltip" :style="{
        left: tooltipPosition.x + 'px',
        top: tooltipPosition.y + 'px',
      }">
        <div v-if="currentLocation">
          <p><strong>货位号:</strong>{{ currentLocation.locationCode || '无' }}</p> <!-- å¢žåŠ é»˜è®¤å€¼ -->
          <p>
            <strong>货位排列层:</strong> {{ currentLocation.row || 0 }}排{{
              currentLocation.index || 0
            }}列{{ currentLocation.layer || 0 }}层
          </p>
          <p><strong>状态:</strong> {{ getStatusText(currentLocation) }}</p>
          <p>
            <strong>禁用:</strong>
            {{ currentLocation.location_lock == 3 ? "是" : "否" }}
          </p>
          <p v-if="currentLocation.location_state === 2">
            <strong>物料编码:</strong>
            {{ currentLocation.material_code || "无" }}
          </p>
          <p v-if="currentLocation.location_state === 2">
            <strong>数量:</strong> {{ currentLocation.quantity || "无" }}
          </p>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import { ref, reactive } from 'vue'
import { ElButton, ElSelect, ElOption } from "element-plus"; // å®Œæ•´å¼•入需要的组件
import * as echarts from 'echarts';
export default {
  setup() {
  components: { ElButton, ElSelect, ElOption }, // æ³¨å†Œæ‰€æœ‰ç”¨åˆ°çš„组件
  data() {
    return {
      slectData: [],
      scList: [],
      Area: { house_name: "", tunnel: "", shelf_code: "" },
      mian_height: "",
      infoMsg: [
        { bgcolor: "lightgreen", msg: "空货位", state: 0, quantity: 0 },
        { bgcolor: "orange", msg: "有货", state: 2, quantity: 0 },
        { bgcolor: "#2BB3D5", msg: "锁定", state: "InAssigned", quantity: 0 },
        { bgcolor: "#ccc", msg: "禁用", state: 3, quantity: 0 },
        { bgcolor: "#b7ba6b", msg: "其它", state: "else", quantity: 0 },
      ],
      locationData: [],
      showTooltipFlag: false,
      currentLocation: null,
      tooltipPosition: { x: 0, y: 0 },
      chart: null,
      notifyOffset: 0, // æ–°å¢žï¼šé€šçŸ¥åç§»é‡è®¡æ•°å™¨
      notifyHeight: 80, // é€šçŸ¥ç»„件高度(可根据实际调整)
      notifyGap: 15, // é€šçŸ¥ä¹‹é—´çš„间距
    };
  },
  computed: {
    GetBgColor() {
      return (col) => {
        // ç®€åŒ–逻辑,提高可读性
        if (col.location_lock == 3) {
          return this.infoMsg.find(item => item.state === 3).bgcolor;
        }
        if (col.location_state === 2) {
          return this.infoMsg.find(item => item.state === 2).bgcolor;
        }
        if (col.location_state > 0 && col.location_state < 100) {
          return this.infoMsg.find(item => item.state === "InAssigned").bgcolor;
        }
        if (col.location_state === 0) {
          return this.infoMsg.find(item => item.state === 0).bgcolor;
        }
        return this.infoMsg.find(item => item.state === "else").bgcolor;
      };
    },
    // æ–°å¢žï¼šå°†infoMsg转换为饼图需要的数据格式
    chartData() {
      return this.infoMsg.map(item => ({
        name: item.msg,
        value: item.quantity,
        itemStyle: {
          color: item.bgcolor // è®©é¥¼å›¾é¢œè‰²å’Œå›¾ä¾‹ä¿æŒä¸€è‡´
        }
      })).filter(item => item.value > 0); // è¿‡æ»¤æŽ‰æ•°é‡ä¸º0的项
    }
  }
}
  },
  watch: {
    "Area.shelf_code"(newValue, oldValue) {
      if (!newValue) return; // ç©ºå€¼æ—¶ä¸æ‰§è¡Œ
      this.scList = [];
      const target = this.slectData.find(e => e.shelf_code === newValue);
      if (target) {
        this.Area.tunnel = target.tunnel?.[0] || ""; // å¯é€‰é“¾é¿å…æŠ¥é”™
        this.scList = target.tunnel || [];
        this.GetViewData(); // æ•°æ®åŠ è½½å®ŒæˆåŽå†è°ƒç”¨
      }
    },
    // ç›‘听chartData变化,更新饼图
    chartData: {
      deep: true,
      handler() {
        this.updateChart();
      }
    },
  },
  methods: {
    async GetViewData() {
      this.warinngNotification();
      // å¢žåŠ å‚æ•°æ ¡éªŒ
      if (!this.Area.shelf_code || !this.Area.tunnel) {
        this.$message?.warning("请先选择区域和排!"); // Element Plus æç¤º
        return;
      }
      try {
        const res = await this.http.post(
          "/api/LocationInfoRow/GetLocationStatu",
          this.Area,
          "查询中"
        );
        this.locationData = res || [];
        console.log("后端返回:", this.locationData);
        // é‡ç½®ç»Ÿè®¡æ•°é‡
        this.infoMsg.forEach(item => item.quantity = 0);
        // ç»Ÿè®¡å„状态数量
        this.locationData.forEach(layer => {
          (layer.rows || []).forEach(row => {
            (row.cols || []).forEach(col => {
              if (col.location_lock == 3) {
                const item = this.infoMsg.find(el => el.state === 3);
                if (item) item.quantity++;
              } else if (col.location_state === 2) {
                const item = this.infoMsg.find(el => el.state === 2);
                if (item) item.quantity++;
              } else if (col.location_state > 0 && col.location_state < 100) {
                const item = this.infoMsg.find(el => el.state === "InAssigned");
                if (item) item.quantity++;
              } else if (col.location_state === 0) {
                const item = this.infoMsg.find(el => el.state === 0);
                if (item) item.quantity++;
              } else {
                const item = this.infoMsg.find(el => el.state === "else");
                if (item) item.quantity++;
              }
            });
          });
        });
      } catch (error) {
        console.error("获取货位数据失败:", error);
        this.$message?.error("获取数据失败,请重试!");
        this.locationData = [];
      }
    },
    SCChange() {
      this.GetViewData();
    },
    showTooltip(location, event) {
      this.currentLocation = location;
      this.showTooltipFlag = true;
      this.tooltipPosition = {
        x: event.clientX + 10,
        y: event.clientY + 10,
      };
    },
    hideTooltip() {
      this.showTooltipFlag = false;
      this.currentLocation = null;
    },
    getStatusText(location) {
      const stateMap = {
        0: "空货位",
        1: "锁定",
        2: "有货",
        10: "有货锁定",
        20: "空闲锁定",
        99: "大托盘锁定"
      };
      return stateMap[location.location_state] || "其他";
    },
    // åˆå§‹åŒ–饼图
    initChart() {
      // æ­£ç¡®èŽ·å–myChart元素
      const chartDom = this.$refs.myChart;
      if (!chartDom) return;
      this.chart = echarts.init(chartDom);
      // è®¾ç½®é¥¼å›¾åŸºç¡€é…ç½®
      const option = {
        // ä¼˜åŒ–tooltip配置,防止被遮挡
        tooltip: {
          trigger: 'item',
          formatter: '{a} <br/>{b}: {c} ({d}%)', // æ˜¾ç¤ºåç§°ã€æ•°é‡ã€ç™¾åˆ†æ¯”
          // å…³é”®é…ç½®ï¼šé˜²æ­¢tooltip被遮挡
          zIndex: 99999, // è®¾ç½®æžé«˜çš„层级,确保在最上层
          confine: true, // å°†tooltip限制在图表容器内
          position: function (point, params, dom, rect, size) {
            // è‡ªå®šä¹‰tooltip位置,避免超出容器
            const x = point[0];
            const y = point[1];
            // è®¡ç®—tooltip的显示位置,优先显示在右侧,超出则显示在左侧
            const ret = {
              left: x + size.contentSize[0] > size.viewSize[0]
                ? (x - size.contentSize[0] - 10) + 'px'
                : (x + 10) + 'px',
              top: y + size.contentSize[1] > size.viewSize[1]
                ? (y - size.contentSize[1] - 10) + 'px'
                : (y + 10) + 'px'
            };
            return ret;
          },
          // å¢žåŠ tooltip样式,增强视觉效果
          textStyle: {
            fontSize: 12
          },
          backgroundColor: 'rgba(255,255,255,0.95)',
          borderColor: '#ddd',
          borderWidth: 1,
          padding: 8,
          shadowBlur: 5,
          shadowColor: 'rgba(0,0,0,0.1)'
        },
        series: [
          {
            name: '',
            type: 'pie',
            radius: '64%',
            label: {
              show: true,
              position: 'outside', // æ ‡ç­¾æ˜¾ç¤ºåœ¨å¤–部
              formatter: '{b}:{d}%' // æ˜¾ç¤ºåç§°å’Œæ•°é‡
            },
            data: this.chartData,
            emphasis: {
              itemStyle: {
                shadowBlur: 10,
                shadowOffsetX: 0,
                shadowColor: 'rgba(0, 0, 0, 0.5)'
              }
            }
          }
        ]
      };
      this.chart.setOption(option);
      // ç›‘听窗口大小变化,自适应图表
      window.addEventListener('resize', () => this.chart?.resize());
    },
    // æ›´æ–°é¥¼å›¾æ•°æ®
    updateChart() {
      if (!this.chart) return;
      this.chart.setOption({
        series: [
          {
            data: this.chartData
          }
        ]
      });
    },
    warinngNotification() {
      this.http.post("/api/LocationInfo/LocationWarning", {}, true).then((result) => {
        if (!result.status) {
          this.$Message.$error(x.message);
        } else {
          console.log(result.data)
          result.data.forEach(item => {
            this.open2(item);
          })
          console.log(this.messageList)
        }
      });
    },
    open2(locationName) {
      // 1. è®¡ç®—当前通知的偏移量
      const currentOffset = this.notifyOffset;
      // 2. æ˜¾ç¤ºé€šçŸ¥
      const notifyInstance = this.$notify({
        title: '警告 [' + locationName + ']',
        message: "仓库占有率已达到80%或80%以上",
        type: 'warning',
        duration: 5000,
        offset: currentOffset, // æ‰‹åŠ¨æŒ‡å®šåç§»é‡
        // 3. é€šçŸ¥å…³é—­åŽï¼Œé‡ç½®åç§»é‡ï¼ˆé¿å…åŽç»­é€šçŸ¥ä½ç½®é”™ä¹±ï¼‰
        onClose: () => {
          this.notifyOffset -= (this.notifyHeight + this.notifyGap);
          // é˜²æ­¢åç§»é‡ä¸ºè´Ÿæ•°
          if (this.notifyOffset < 0) this.notifyOffset = 0;
        }
      });
      // 4. æ›´æ–°ä¸‹ä¸€ä¸ªé€šçŸ¥çš„偏移量
      this.notifyOffset += (this.notifyHeight + this.notifyGap);
    },
  },
  mounted() {
    // ç¡®ä¿DOM加载完成后获取元素
    this.$nextTick(() => {
      const mainHeight = document.getElementById("vol-main");
      if (mainHeight) {
        this.mian_height = mainHeight.offsetHeight - 40 + "px";
      }
      // this.warinngNotification();
      // åˆå§‹åŒ–下拉数据
      this.http.get("/api/LocationInfoRow/GetArea", {}, "查询中")
        .then((x) => {
          this.slectData = x || [];
          if (this.slectData.length > 0) {
            this.Area.shelf_code = this.slectData[0].shelf_code;
            this.scList = this.slectData[0].tunnel || [];
            this.Area.tunnel = this.scList[0] || "";
            // åˆå§‹åŒ–图表
            this.initChart();
          }
        })
        .catch(error => {
          console.error("获取区域数据失败:", error);
          this.$message?.error("加载区域数据失败!");
        });
    });
  },
  beforeUnmount() {
    // é”€æ¯å›¾è¡¨ï¼Œé¿å…å†…存泄漏
    if (this.chart) {
      this.chart.dispose();
      this.chart = null;
    }
  },
};
</script>
<style scoped>
.title {
  line-height: 70vh;
/* åŽŸæœ‰æ ·å¼ä¿æŒä¸å˜ï¼Œæ–°å¢žä»¥ä¸‹æ ·å¼ */
#vol-main {
  height: 100vh;
  /* ç¡®ä¿å®¹å™¨æœ‰é«˜åº¦ */
  box-sizing: border-box;
}
.empty-tip {
  text-align: center;
  font-size: 28px;
  color: orange;
  padding: 50px 0;
  color: #999;
  font-size: 14px;
}
/* å…³é”®ä¿®æ”¹ï¼šé¥¼å›¾å®¹å™¨æ ·å¼ï¼Œå®žçŽ°åœ¨æŽ§åˆ¶é¢æ¿åŒºåŸŸå±…ä¸­ */
.echarts-container {
  margin-top: 20px;
  width: 100%;
  flex: 1;
  /* è®©é¥¼å›¾å®¹å™¨å æ®æŽ§åˆ¶é¢æ¿å‰©ä½™ç©ºé—´ */
  display: flex;
  /* Flex布局实现水平+垂直居中 */
  justify-content: center;
  /* æ°´å¹³å±…中 */
  align-items: center;
  /* åž‚直居中 */
  position: relative;
  z-index: 1;
}
/* é¥¼å›¾å†…部容器 */
.chart-inner {
  width: 400px;
  height: 400px;
  /* é¥¼å›¾å®žé™…大小 */
}
/* æ–°å¢žï¼šç»™ECharts实例容器增加样式,确保tooltip不被遮挡 */
:deep(.echarts-tooltip) {
  z-index: 99999 !important;
  /* å¼ºåˆ¶æœ€é«˜å±‚级 */
  pointer-events: none;
  /* é˜²æ­¢tooltip遮挡鼠标事件 */
}
/* åŽŸæœ‰æ ·å¼ */
.container {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding: 10px;
}
.header {
  text-align: center;
  margin-bottom: 20px;
}
.title {
  font-size: 20px;
  font-weight: bold;
  margin: 0;
}
.content-wrapper {
  display: flex;
  flex: 1;
  min-height: 0;
}
.control-panel {
  width: 320px;
  padding: 15px;
  background-color: #f5f7fa;
  border-radius: 4px;
  margin-right: 15px;
  display: flex;
  flex-direction: column;
  /* ç¡®ä¿æŽ§åˆ¶é¢æ¿å†…的元素层级正确 */
  position: relative;
  z-index: 10;
}
.form-group {
  margin-bottom: 15px;
}
.full-width {
  width: 100%;
}
.refresh-btn {
  margin-top: 10px;
  width: 100%;
}
.legend-section {
  margin-top: 30px;
}
.legend-section h4 {
  margin-bottom: 10px;
}
.legend-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
}
.legend-item {
  display: flex;
  align-items: center;
}
.color-box {
  display: inline-block;
  width: 20px;
  height: 20px;
  margin-right: 8px;
  border-radius: 3px;
}
.legend-label {
  font-size: 13px;
}
.location-view {
  flex: 1;
  overflow: auto;
  padding: 10px;
  background-color: white;
  border-radius: 4px;
  /* è®¾ç½®åˆç†çš„层级,低于tooltip */
  z-index: 1;
}
.layer-container {
  margin-bottom: 25px;
}
.layer-title {
  margin: 0 0 10px 0;
  font-size: 16px;
  color: #333;
}
.row {
  display: flex;
  flex-wrap: wrap;
  margin-bottom: 8px;
  cursor: pointer;
}
.location-cell {
  width: 66px;
  height: 38px;
  margin: 3px;
  text-align: center;
  font-size: 14px;
  border-radius: 3px;
  line-height: 38px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.location-tooltip {
  position: fixed;
  z-index: 9999;
  background-color: white;
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 10px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  pointer-events: none;
  max-width: 250px;
}
.location-tooltip p {
  margin: 5px 0;
  font-size: 13px;
  line-height: 1.4;
}
.location-tooltip strong {
  display: inline-block;
  width: 70px;
  color: #666;
}
.notify-trigger-btn {
  display: none !important;
}
</style>