#
Junjie
昨天 bf64e8016283b18c04d5392dd9c002b921021af2
src/main/webapp/components/WatchCrnCard.js
@@ -1,96 +1,86 @@
Vue.component("watch-crn-card", {
  template: `
    <div>
        <div style="display: flex;margin-bottom: 10px;">
            <div style="width: 100%;">堆垛机监控</div>
            <div style="width: 100%;text-align: right;display: flex;"><el-input size="mini" v-model="searchCrnNo" placeholder="请输入堆垛机号"></el-input><el-button @click="getCrnStateInfo" size="mini">查询</el-button></div>
    <div class="mc-root">
      <div class="mc-toolbar">
        <div class="mc-title">堆垛机监控</div>
        <div class="mc-search">
          <input class="mc-input" v-model="searchCrnNo" placeholder="请输入堆垛机号" />
          <button type="button" class="mc-btn mc-btn-ghost" @click="getCrnStateInfo">查询</button>
        </div>
        <div style="margin-bottom: 10px;" v-if="!readOnly">
            <div style="margin-bottom: 5px;">
               <el-button v-if="showControl" @click="openControl" size="mini">关闭控制中心</el-button>
               <el-button v-else @click="openControl" size="mini">打开控制中心</el-button>
      </div>
      <div v-if="!readOnly" class="mc-control-toggle">
        <button type="button" class="mc-btn mc-btn-ghost" @click="openControl">
          {{ showControl ? '收起控制中心' : '打开控制中心' }}
        </button>
      </div>
      <div v-if="showControl" class="mc-control">
        <div class="mc-control-grid">
          <label class="mc-field">
            <span class="mc-field-label">堆垛机号</span>
            <input class="mc-input" v-model="controlParam.crnNo" placeholder="例如 1" />
          </label>
          <label class="mc-field">
            <span class="mc-field-label">源库位</span>
            <input class="mc-input" v-model="controlParam.sourceLocNo" placeholder="输入源点" />
          </label>
          <label class="mc-field mc-span-2">
            <span class="mc-field-label">目标库位</span>
            <input class="mc-input" v-model="controlParam.targetLocNo" placeholder="输入目标点" />
          </label>
          <div class="mc-action-row">
            <button type="button" class="mc-btn" @click="controlCommandTransport">取放货</button>
            <button type="button" class="mc-btn mc-btn-ghost" @click="controlCommandMove">移动</button>
            <button type="button" class="mc-btn mc-btn-soft" @click="controlCommandTaskComplete">任务完成</button>
          </div>
        </div>
      </div>
      <div class="mc-collapse">
        <div
          v-for="item in displayCrnList"
          :key="item.crnNo"
          :class="['mc-item', { 'is-open': isActive(item.crnNo) }]"
        >
          <button type="button" class="mc-head" @click="toggleItem(item)">
            <div class="mc-head-main">
              <div class="mc-head-title">{{ item.crnNo }}号堆垛机</div>
              <div class="mc-head-subtitle">工作号 {{ orDash(item.workNo) }} | 目标 {{ orDash(item.locNo) }}</div>
            </div>
            <div v-if="showControl" style="display: flex;justify-content: space-between;flex-wrap: wrap;">
                <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.crnNo" placeholder="堆垛机号"></el-input></div>
                <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.sourceLocNo" placeholder="源点"></el-input></div>
                <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.targetLocNo" placeholder="目标点"></el-input></div>
                <div style="margin-bottom: 10px;"><el-button @click="controlCommandTransport()" size="mini">取放货</el-button></div>
                <div style="margin-bottom: 10px;"><el-button @click="controlCommandMove()" size="mini">移动</el-button></div>
                <div style="margin-bottom: 10px;"><el-button @click="controlCommandTaskComplete()" size="mini">任务完成</el-button></div>
            <div class="mc-head-right">
              <span :class="['mc-badge', 'is-' + getStatusTone(item)]">{{ getStatusLabel(item) }}</span>
              <span class="mc-chevron">{{ isActive(item.crnNo) ? '▾' : '▸' }}</span>
            </div>
          </button>
          <div v-if="isActive(item.crnNo)" class="mc-body">
            <div class="mc-detail-grid">
              <div v-for="entry in buildDetailEntries(item)" :key="entry.label" class="mc-detail-cell">
                <div class="mc-detail-label">{{ entry.label }}</div>
                <div class="mc-detail-value">{{ entry.value }}</div>
              </div>
            </div>
          </div>
        </div>
        <div style="max-height: 55vh; overflow:auto;">
          <el-collapse v-model="activeNames" accordion>
            <el-collapse-item v-for="(item) in displayCrnList" :name="item.crnNo">
            <template slot="title">
                <div style="width: 100%;display: flex;">
                   <div style="width: 50%;">{{ item.crnNo }}号堆垛机</div>
                   <div style="width: 50%;text-align: right;">
                      <el-tag v-if="item.deviceStatus == 'AUTO'" type="success" size="small">自动</el-tag>
                      <el-tag v-else-if="item.deviceStatus == 'WORKING'" size="small">作业中</el-tag>
                      <el-tag v-else-if="item.deviceStatus == 'ERROR'" type="danger" size="small">故障</el-tag>
                      <el-tag v-else type="warning" size="small">离线</el-tag>
                   </div>
                </div>
            </template>
            <el-descriptions border direction="vertical">
                <el-descriptions-item label="编号">{{ item.crnNo }}</el-descriptions-item>
                <el-descriptions-item label="工作号">{{ item.workNo }}</el-descriptions-item>
                <el-descriptions-item label="模式">{{ item.mode }}</el-descriptions-item>
                <el-descriptions-item label="状态">{{ item.status }}</el-descriptions-item>
                <el-descriptions-item label="源库位">{{ item.sourceLocNo }}</el-descriptions-item>
                <el-descriptions-item label="目标库位">{{ item.locNo }}</el-descriptions-item>
                <el-descriptions-item label="是否有物">{{ item.loading }}</el-descriptions-item>
                <el-descriptions-item label="任务接收">{{ item.taskReceive }}</el-descriptions-item>
                <el-descriptions-item label="列">{{ item.bay }}</el-descriptions-item>
                <el-descriptions-item label="层">{{ item.lev }}</el-descriptions-item>
                <el-descriptions-item label="货叉定位">{{ item.forkOffset }}</el-descriptions-item>
                <el-descriptions-item label="载货台定位">{{ item.liftPos }}</el-descriptions-item>
                <el-descriptions-item label="走行在定位">{{ item.walkPos }}</el-descriptions-item>
                <el-descriptions-item label="走行速度(m/min)">{{ item.xspeed }}</el-descriptions-item>
                <el-descriptions-item label="升降速度(m/min)">{{ item.yspeed }}</el-descriptions-item>
                <el-descriptions-item label="叉牙速度(m/min)">{{ item.zspeed }}</el-descriptions-item>
                <el-descriptions-item label="走行距离(Km)">{{ item.xdistance }}</el-descriptions-item>
                <el-descriptions-item label="升降距离(Km)">{{ item.ydistance }}</el-descriptions-item>
                <el-descriptions-item label="走行时长(H)">{{ item.xduration }}</el-descriptions-item>
                <el-descriptions-item label="升降时长(H)">{{ item.yduration }}</el-descriptions-item>
                <el-descriptions-item label="称重数据">{{ item.weight }}</el-descriptions-item>
                <el-descriptions-item label="条码数据">{{ item.barcode }}</el-descriptions-item>
                <el-descriptions-item label="故障代码">{{ item.warnCode }}</el-descriptions-item>
                <el-descriptions-item label="故障描述">{{ item.alarm }}</el-descriptions-item>
                <el-descriptions-item label="扩展数据">{{ item.extend }}</el-descriptions-item>
            </el-descriptions>
            </el-collapse-item>
          </el-collapse>
        </div>
        <div style="display:flex; justify-content:flex-end; margin-top:8px;">
          <el-pagination
            @current-change="handlePageChange"
            @size-change="handleSizeChange"
            :current-page="currentPage"
            :page-size="pageSize"
            :page-sizes="[10,20,50,100]"
            layout="total, prev, pager, next"
            :total="crnList.length">
          </el-pagination>
        </div>
        <div v-if="displayCrnList.length === 0" class="mc-empty">当前没有可展示的堆垛机数据</div>
      </div>
      <div class="mc-footer">
        <button type="button" class="mc-page-btn" :disabled="currentPage <= 1" @click="handlePageChange(currentPage - 1)">上一页</button>
        <span>{{ currentPage }} / {{ totalPages }}</span>
        <button type="button" class="mc-page-btn" :disabled="currentPage >= totalPages" @click="handlePageChange(currentPage + 1)">下一页</button>
      </div>
    </div>
    `,
  `,
  props: {
    param: {
      type: Object,
      default: () => ({})
    },
    autoRefresh: {
      type: Boolean,
      default: true
    },
    readOnly: {
      type: Boolean,
      default: false
    }
    param: { type: Object, default: function () { return {}; } },
    items: { type: Array, default: null },
    autoRefresh: { type: Boolean, default: true },
    readOnly: { type: Boolean, default: false }
  },
  data() {
  data: function () {
    return {
      crnList: [],
      activeNames: "",
@@ -99,159 +89,176 @@
      controlParam: {
        crnNo: "",
        sourceLocNo: "",
        targetLocNo: "",
        targetLocNo: ""
      },
      pageSize: 25,
      pageSize: 12,
      currentPage: 1,
      timer: null
    };
  },
  created() {
    if (this.autoRefresh) {
      this.timer = setInterval(() => {
        this.getCrnStateInfo();
      }, 1000);
    }
  },
  beforeDestroy() {
    if (this.timer) {
      clearInterval(this.timer);
    }
  },
  computed: {
    displayCrnList() {
      const start = (this.currentPage - 1) * this.pageSize;
      const end = start + this.pageSize;
      return this.crnList.slice(start, end);
    sourceList: function () {
      return Array.isArray(this.items) ? this.items : this.crnList;
    },
    filteredCrnList: function () {
      var keyword = String(this.searchCrnNo || "").trim();
      if (!keyword) {
        return this.sourceList;
      }
      return this.sourceList.filter(function (item) {
        return String(item.crnNo) === keyword;
      });
    },
    displayCrnList: function () {
      var start = (this.currentPage - 1) * this.pageSize;
      return this.filteredCrnList.slice(start, start + this.pageSize);
    },
    totalPages: function () {
      return Math.max(1, Math.ceil(this.filteredCrnList.length / this.pageSize) || 1);
    }
  },
  watch: {
    param: {
      handler(newVal, oldVal) {
        if (newVal && newVal.crnNo && newVal.crnNo != 0) {
          this.activeNames = newVal.crnNo;
          this.searchCrnNo = newVal.crnNo;
          const idx = this.crnList.findIndex(i => i.crnNo == newVal.crnNo);
          if (idx >= 0) { this.currentPage = Math.floor(idx / this.pageSize) + 1; }
        }
      },
      deep: true, // 深度监听嵌套属性
      immediate: true, // 立即触发一次(可选)
    items: function () {
      this.afterDataRefresh();
    },
    param: {
      deep: true,
      immediate: true,
      handler: function (newVal) {
        if (newVal && newVal.crnNo && newVal.crnNo !== 0) {
          this.focusCrn(newVal.crnNo);
        }
      }
    }
  },
  created: function () {
    MonitorCardKit.ensureStyles();
    if (this.autoRefresh) {
      this.timer = setInterval(this.getCrnStateInfo, 1000);
    }
  },
  beforeDestroy: function () {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = null;
    }
  },
  methods: {
    handlePageChange(page) {
    orDash: function (value) {
      return MonitorCardKit.orDash(value);
    },
    getStatusLabel: function (item) {
      return MonitorCardKit.deviceStatusLabel(item && item.deviceStatus);
    },
    getStatusTone: function (item) {
      return MonitorCardKit.statusTone(this.getStatusLabel(item));
    },
    isActive: function (crnNo) {
      return String(this.activeNames) === String(crnNo);
    },
    toggleItem: function (item) {
      var next = String(item.crnNo);
      this.activeNames = this.activeNames === next ? "" : next;
    },
    focusCrn: function (crnNo) {
      this.searchCrnNo = String(crnNo);
      var index = this.filteredCrnList.findIndex(function (item) {
        return String(item.crnNo) === String(crnNo);
      });
      if (index >= 0) {
        this.currentPage = Math.floor(index / this.pageSize) + 1;
      } else {
        this.currentPage = 1;
      }
      this.activeNames = String(crnNo);
    },
    afterDataRefresh: function () {
      if (this.currentPage > this.totalPages) {
        this.currentPage = this.totalPages;
      }
      if (this.activeNames) {
        var exists = this.filteredCrnList.some(function (item) {
          return String(item.crnNo) === String(this.activeNames);
        }, this);
        if (!exists) {
          this.activeNames = "";
        }
      }
    },
    handlePageChange: function (page) {
      if (page < 1 || page > this.totalPages) {
        return;
      }
      this.currentPage = page;
    },
    handleSizeChange(size) {
      this.pageSize = size;
      this.currentPage = 1;
    },
    getCrnStateInfo() {
      if (this.$root.sendWs) {
    getCrnStateInfo: function () {
      if (this.$root && this.$root.sendWs) {
        this.$root.sendWs(JSON.stringify({
          "url": "/crn/table/crn/state",
          "data": {}
          url: "/crn/table/crn/state",
          data: {}
        }));
      }
    },
    setCrnList(res) {
      let that = this;
      if (res.code == 200) {
        let list = res.data;
        if (that.searchCrnNo == "") {
          that.crnList = list;
        } else {
          let tmp = [];
          list.forEach((item) => {
            if (item.crnNo == that.searchCrnNo) {
              tmp.push(item);
            }
          });
          that.crnList = tmp;
          that.currentPage = 1;
        }
    setCrnList: function (res) {
      if (res && res.code === 200) {
        this.crnList = res.data || [];
        this.afterDataRefresh();
      }
    },
    openControl() {
    openControl: function () {
      this.showControl = !this.showControl;
    },
    controlCommandTransport() {
      let that = this;
      //取放货
    buildDetailEntries: function (item) {
      return [
        { label: "编号", value: this.orDash(item.crnNo) },
        { label: "工作号", value: this.orDash(item.workNo) },
        { label: "模式", value: this.orDash(item.mode) },
        { label: "状态", value: this.orDash(item.status) },
        { label: "源库位", value: this.orDash(item.sourceLocNo) },
        { label: "目标库位", value: this.orDash(item.locNo) },
        { label: "是否有物", value: MonitorCardKit.yesNo(item.loading) },
        { label: "任务接收", value: this.orDash(item.taskReceive) },
        { label: "列", value: this.orDash(item.bay) },
        { label: "层", value: this.orDash(item.lev) },
        { label: "货叉定位", value: this.orDash(item.forkOffset) },
        { label: "载货台定位", value: this.orDash(item.liftPos) },
        { label: "走行定位", value: this.orDash(item.walkPos) },
        { label: "走行速度", value: this.orDash(item.xspeed) },
        { label: "升降速度", value: this.orDash(item.yspeed) },
        { label: "叉牙速度", value: this.orDash(item.zspeed) },
        { label: "称重数据", value: this.orDash(item.weight) },
        { label: "条码数据", value: this.orDash(item.barcode) },
        { label: "故障代码", value: this.orDash(item.warnCode) },
        { label: "故障描述", value: this.orDash(item.alarm) },
        { label: "扩展数据", value: this.orDash(item.extend) }
      ];
    },
    postControl: function (url, payload) {
      $.ajax({
        url: baseUrl + "/crn/command/take",
        url: baseUrl + url,
        headers: {
          token: localStorage.getItem("token"),
          token: localStorage.getItem("token")
        },
        contentType: "application/json",
        method: "post",
        data: JSON.stringify(that.controlParam),
        success: (res) => {
          if (res.code == 200) {
            that.$message({
              message: res.msg,
              type: "success",
            });
        data: JSON.stringify(payload),
        success: function (res) {
          if (res && res.code === 200) {
            MonitorCardKit.showMessage(this, res.msg || "操作成功", "success");
          } else {
            that.$message({
              message: res.msg,
              type: "warning",
            });
            MonitorCardKit.showMessage(this, (res && res.msg) || "操作失败", "warning");
          }
        },
        }.bind(this)
      });
    },
    controlCommandMove() {
      let that = this;
      $.ajax({
        url: baseUrl + "/crn/command/move",
        headers: {
          token: localStorage.getItem("token"),
        },
        contentType: "application/json",
        method: "post",
        data: JSON.stringify(that.controlParam),
        success: (res) => {
          if (res.code == 200) {
            that.$message({
              message: res.msg,
              type: "success",
            });
          } else {
            that.$message({
              message: res.msg,
              type: "warning",
            });
          }
        },
      });
    controlCommandTransport: function () {
      this.postControl("/crn/command/take", this.controlParam);
    },
    controlCommandTaskComplete() {
      let that = this;
      $.ajax({
        url: baseUrl + "/crn/command/taskComplete",
        headers: {
          token: localStorage.getItem("token"),
        },
        contentType: "application/json",
        method: "post",
        data: JSON.stringify(that.controlParam),
        success: (res) => {
          if (res.code == 200) {
            that.$message({
              message: res.msg,
              type: "success",
            });
          } else {
            that.$message({
              message: res.msg,
              type: "warning",
            });
          }
        },
      });
    controlCommandMove: function () {
      this.postControl("/crn/command/move", this.controlParam);
    },
  },
    controlCommandTaskComplete: function () {
      this.postControl("/crn/command/taskComplete", this.controlParam);
    }
  }
});