#
Junjie
13 小时以前 bf64e8016283b18c04d5392dd9c002b921021af2
src/main/webapp/components/DevpCard.js
@@ -1,49 +1,63 @@
Vue.component("devp-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="searchStationId" placeholder="请输入站号"></el-input><el-button @click="getDevpStateInfo" 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="searchStationId" placeholder="请输入站号" />
          <button type="button" class="mc-btn mc-btn-ghost" @click="getDevpStateInfo">查询</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="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.stationId" placeholder="站号"></el-input></div>
                <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.taskNo" placeholder="工作号"></el-input></div>
                <div style="margin-bottom: 10px;width: 33%;"><el-input size="mini" v-model="controlParam.targetStationId" placeholder="目标站"></el-input></div>
                <div style="margin-bottom: 10px;"><el-button @click="controlCommand()" size="mini">下发</el-button></div>
                <div style="margin-bottom: 10px;"><el-button @click="resetCommand()" size="mini">复位</el-button></div>
            </div>
      </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.stationId" placeholder="例如 101" />
          </label>
          <label class="mc-field">
            <span class="mc-field-label">工作号</span>
            <input class="mc-input" v-model="controlParam.taskNo" placeholder="输入工作号" />
          </label>
          <label class="mc-field mc-span-2">
            <span class="mc-field-label">目标站</span>
            <input class="mc-input" v-model="controlParam.targetStationId" placeholder="输入目标站号" />
          </label>
          <div class="mc-action-row">
            <button type="button" class="mc-btn" @click="controlCommand">下发</button>
            <button type="button" class="mc-btn mc-btn-soft" @click="resetCommand">复位</button>
          </div>
        </div>
        <div style="max-height: 55vh; overflow:auto;">
          <el-collapse v-model="activeNames" accordion>
            <el-collapse-item v-for="(item) in displayStationList" :name="item.stationId">
            <template slot="title">
                <div style="width: 100%;display: flex;">
                   <div style="width: 50%;">{{ item.stationId }}站</div>
                   <div style="width: 50%;text-align: right;">
                      <el-tag v-if="item.autoing" type="success" 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.stationId }}</el-descriptions-item>
                <el-descriptions-item label="工作号">{{ item.taskNo }}</el-descriptions-item>
                <el-descriptions-item label="目标站">{{ item.targetStaNo }}</el-descriptions-item>
                <el-descriptions-item label="模式">{{ item.autoing ? '自动' : '手动' }}</el-descriptions-item>
                <el-descriptions-item label="有物">{{ item.loading ? '有' : '无' }}</el-descriptions-item>
                <el-descriptions-item label="可入">{{ item.inEnable ? 'Y' : 'N' }}</el-descriptions-item>
                <el-descriptions-item label="可出">{{ item.outEnable ? 'Y' : 'N' }}</el-descriptions-item>
                <el-descriptions-item label="空板信号">{{ item.emptyMk ? 'Y' : 'N' }}</el-descriptions-item>
                <el-descriptions-item label="满板信号">{{ item.fullPlt ? 'Y' : 'N' }}</el-descriptions-item>
                <el-descriptions-item label="运行阻塞">{{ item.runBlock ? 'Y' : 'N' }}</el-descriptions-item>
                <el-descriptions-item label="启动入库">{{ item.enableIn ? 'Y' : 'N' }}</el-descriptions-item>
                <el-descriptions-item label="托盘高度">{{ item.palletHeight }}</el-descriptions-item>
                <el-descriptions-item label="条码">
      </div>
      <div class="mc-collapse">
        <div
          v-for="item in displayStationList"
          :key="item.stationId"
          :class="['mc-item', { 'is-open': isActive(item.stationId) }]"
        >
          <button type="button" class="mc-head" @click="toggleItem(item)">
            <div class="mc-head-main">
              <div class="mc-head-title">{{ item.stationId }}站</div>
              <div class="mc-head-subtitle">任务 {{ orDash(item.taskNo) }} | 目标站 {{ orDash(item.targetStaNo) }}</div>
            </div>
            <div class="mc-head-right">
              <span :class="['mc-badge', 'is-' + getStatusTone(item)]">{{ getStatusLabel(item) }}</span>
              <span class="mc-chevron">{{ isActive(item.stationId) ? '▾' : '▸' }}</span>
            </div>
          </button>
          <div v-if="isActive(item.stationId)" 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 v-if="entry.type === 'barcode'" class="mc-detail-value mc-code">
                  <el-popover v-if="item.barcode" placement="top" width="460" trigger="hover">
                    <div style="text-align: center;">
                      <img
@@ -53,134 +67,287 @@
                      />
                      <div style="margin-top: 4px; font-size: 12px; word-break: break-all;">{{ item.barcode }}</div>
                    </div>
                    <span slot="reference" style="cursor: pointer; color: #409EFF;">{{ item.barcode }}</span>
                    <span
                      slot="reference"
                      @click.stop="handleBarcodeClick(item)"
                      style="cursor: pointer; color: #4677a4; font-weight: 600;"
                    >{{ entry.value }}</span>
                  </el-popover>
                  <span v-else>-</span>
                </el-descriptions-item>
                <el-descriptions-item label="重量">{{ item.weight }}</el-descriptions-item>
                <el-descriptions-item label="故障代码">{{ item.error }}</el-descriptions-item>
                <el-descriptions-item label="故障信息">{{ item.errorMsg }}</el-descriptions-item>
                <el-descriptions-item label="扩展数据">{{ item.extend }}</el-descriptions-item>
            </el-descriptions>
            </el-collapse-item>
          </el-collapse>
                  <span
                    v-else
                    @click.stop="handleBarcodeClick(item)"
                    style="cursor: pointer; color: #4677a4; font-weight: 600;"
                  >{{ entry.value }}</span>
                </div>
                <div v-else class="mc-detail-value" :class="{ 'mc-code': entry.code }">{{ entry.value }}</div>
              </div>
            </div>
          </div>
        </div>
        <div style="display:flex; justify-content:flex-end; margin-top:8px;">
          <el-pagination
            small
            @current-change="handlePageChange"
            @size-change="handleSizeChange"
            :current-page="currentPage"
            :page-size="pageSize"
            :page-sizes="[10,20,50,100]"
            layout="total, prev, pager, next"
            :total="stationList.length">
          </el-pagination>
        </div>
        <div v-if="displayStationList.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 {
      stationList: [],
      fullStationList: [],
      activeNames: "",
      searchStationId: "",
      showControl: false,
      controlParam: {
        stationId: "",
        taskNo: "",
        targetStationId: "",
        targetStationId: ""
      },
      barcodePreviewCache: {},
      pageSize: 25,
      pageSize: 12,
      currentPage: 1,
      timer: null
    };
  },
  created() {
    if (this.autoRefresh) {
      this.timer = setInterval(() => {
        this.getDevpStateInfo();
      }, 1000);
    }
  },
  beforeDestroy() {
    if (this.timer) {
      clearInterval(this.timer);
    }
  },
  computed: {
    displayStationList() {
      const start = (this.currentPage - 1) * this.pageSize;
      const end = start + this.pageSize;
      return this.stationList.slice(start, end);
    sourceList: function () {
      return Array.isArray(this.items) ? this.items : this.stationList;
    },
    filteredStationList: function () {
      var keyword = String(this.searchStationId || "").trim();
      if (!keyword) {
        return this.sourceList;
      }
      return this.sourceList.filter(function (item) {
        return String(item.stationId) === keyword;
      });
    },
    displayStationList: function () {
      var start = (this.currentPage - 1) * this.pageSize;
      return this.filteredStationList.slice(start, start + this.pageSize);
    },
    totalPages: function () {
      return Math.max(1, Math.ceil(this.filteredStationList.length / this.pageSize) || 1);
    }
  },
  watch: {
    param: {
      handler(newVal, oldVal) {
        if (newVal && newVal.stationId && newVal.stationId != 0) {
          this.activeNames = newVal.stationId;
          this.searchStationId = newVal.stationId;
        }
      },
      deep: true, // 深度监听嵌套属性
      immediate: true, // 立即触发一次(可选)
    items: function () {
      this.afterDataRefresh();
    },
    param: {
      deep: true,
      immediate: true,
      handler: function (newVal) {
        if (newVal && newVal.stationId && newVal.stationId !== 0) {
          this.focusStation(newVal.stationId);
        }
      }
    }
  },
  created: function () {
    MonitorCardKit.ensureStyles();
    if (this.autoRefresh) {
      this.timer = setInterval(this.getDevpStateInfo, 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 item && item.autoing ? "自动" : "手动";
    },
    getStatusTone: function (item) {
      return MonitorCardKit.statusTone(this.getStatusLabel(item));
    },
    isActive: function (stationId) {
      return String(this.activeNames) === String(stationId);
    },
    toggleItem: function (item) {
      var next = String(item.stationId);
      this.activeNames = this.activeNames === next ? "" : next;
    },
    focusStation: function (stationId) {
      this.searchStationId = String(stationId);
      var index = this.filteredStationList.findIndex(function (item) {
        return String(item.stationId) === String(stationId);
      });
      this.currentPage = index >= 0 ? Math.floor(index / this.pageSize) + 1 : 1;
      this.activeNames = String(stationId);
    },
    afterDataRefresh: function () {
      if (this.currentPage > this.totalPages) {
        this.currentPage = this.totalPages;
      }
      if (this.activeNames) {
        var exists = this.filteredStationList.some(function (item) {
          return String(item.stationId) === 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;
    getDevpStateInfo: function () {
      if (this.$root && this.$root.sendWs) {
        this.$root.sendWs(JSON.stringify({
          url: "/console/latest/data/station",
          data: {}
        }));
      }
    },
    getBarcodePreview(barcode) {
      const value = String(barcode || "").trim();
    setStationList: function (res) {
      if (res && res.code === 200) {
        this.stationList = res.data || [];
        this.afterDataRefresh();
      }
    },
    openControl: function () {
      this.showControl = !this.showControl;
    },
    buildDetailEntries: function (item) {
      return [
        { label: "编号", value: this.orDash(item.stationId) },
        { label: "工作号", value: this.orDash(item.taskNo) },
        { label: "目标站", value: this.orDash(item.targetStaNo) },
        { label: "模式", value: item.autoing ? "自动" : "手动" },
        { label: "有物", value: MonitorCardKit.yesNo(item.loading) },
        { label: "可入", value: MonitorCardKit.yesNo(item.inEnable) },
        { label: "可出", value: MonitorCardKit.yesNo(item.outEnable) },
        { label: "空板信号", value: MonitorCardKit.yesNo(item.emptyMk) },
        { label: "满板信号", value: MonitorCardKit.yesNo(item.fullPlt) },
        { label: "运行阻塞", value: MonitorCardKit.yesNo(item.runBlock) },
        { label: "启动入库", value: MonitorCardKit.yesNo(item.enableIn) },
        { label: "托盘高度", value: this.orDash(item.palletHeight) },
        { label: "条码", value: this.orDash(item.barcode), code: true, type: "barcode" },
        { label: "重量", value: this.orDash(item.weight) },
        { label: "任务可写区", value: this.orDash(item.taskWriteIdx) },
        { label: "故障代码", value: this.orDash(item.error) },
        { label: "故障信息", value: this.orDash(item.errorMsg) },
        { label: "扩展数据", value: this.orDash(item.extend) }
      ];
    },
    postControl: function (url, payload) {
      $.ajax({
        url: baseUrl + url,
        headers: {
          token: localStorage.getItem("token")
        },
        contentType: "application/json",
        method: "post",
        data: JSON.stringify(payload),
        success: function (res) {
          if (res && res.code === 200) {
            MonitorCardKit.showMessage(this, res.msg || "操作成功", "success");
          } else {
            MonitorCardKit.showMessage(this, (res && res.msg) || "操作失败", "warning");
          }
        }.bind(this)
      });
    },
    handleBarcodeClick: function (item) {
      if (this.readOnly || !item || item.stationId == null) {
        return;
      }
      this.$prompt("请输入新的条码值(可留空清空)", "修改条码", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        inputValue: item.barcode || "",
        inputPlaceholder: "请输入条码"
      }).then(function (result) {
        this.updateStationBarcode(item.stationId, result && result.value == null ? "" : String(result.value).trim());
      }.bind(this)).catch(function () {});
    },
    updateStationBarcode: function (stationId, barcode) {
      $.ajax({
        url: baseUrl + "/station/command/barcode",
        headers: {
          token: localStorage.getItem("token")
        },
        contentType: "application/json",
        method: "post",
        data: JSON.stringify({
          stationId: stationId,
          barcode: barcode
        }),
        success: function (res) {
          if (res && res.code === 200) {
            this.syncLocalBarcode(stationId, barcode);
            MonitorCardKit.showMessage(this, "条码修改成功", "success");
          } else {
            MonitorCardKit.showMessage(this, (res && res.msg) || "条码修改失败", "warning");
          }
        }.bind(this)
      });
    },
    syncLocalBarcode: function (stationId, barcode) {
      var update = function (list) {
        if (!list || !list.length) {
          return;
        }
        list.forEach(function (item) {
          if (item.stationId == stationId) {
            item.barcode = barcode;
          }
        });
      };
      update(this.stationList);
      if (Array.isArray(this.items)) {
        update(this.items);
      }
    },
    getBarcodePreview: function (barcode) {
      var value = String(barcode || "").trim();
      if (!value) {
        return "";
      }
      if (this.barcodePreviewCache[value]) {
        return this.barcodePreviewCache[value];
      }
      const encodeResult = this.encodeCode128(value);
      var encodeResult = this.encodeCode128(value);
      if (!encodeResult) {
        return "";
      }
      const svg = this.buildCode128Svg(encodeResult, value);
      const dataUrl = "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(svg);
      var svg = this.buildCode128Svg(encodeResult, value);
      var dataUrl = "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(svg);
      this.$set(this.barcodePreviewCache, value, dataUrl);
      return dataUrl;
    },
    encodeCode128(value) {
    encodeCode128: function (value) {
      if (!value) {
        return null;
      }
      const isNumeric = /^\d+$/.test(value);
      var isNumeric = /^\d+$/.test(value);
      if (isNumeric && value.length % 2 === 0) {
        return this.encodeCode128C(value);
      }
      return this.encodeCode128B(value);
    },
    encodeCode128B(value) {
      const codes = [104];
      for (let i = 0; i < value.length; i++) {
        const code = value.charCodeAt(i) - 32;
    encodeCode128B: function (value) {
      var codes = [104];
      for (var i = 0; i < value.length; i++) {
        var code = value.charCodeAt(i) - 32;
        if (code < 0 || code > 94) {
          return null;
        }
@@ -188,64 +355,62 @@
      }
      return this.buildCode128Pattern(codes);
    },
    encodeCode128C(value) {
    encodeCode128C: function (value) {
      if (value.length % 2 !== 0) {
        return null;
      }
      const codes = [105];
      for (let i = 0; i < value.length; i += 2) {
      var codes = [105];
      for (var i = 0; i < value.length; i += 2) {
        codes.push(parseInt(value.substring(i, i + 2), 10));
      }
      return this.buildCode128Pattern(codes);
    },
    buildCode128Pattern(codes) {
      const patterns = this.getCode128Patterns();
      let checksum = codes[0];
      for (let i = 1; i < codes.length; i++) {
    buildCode128Pattern: function (codes) {
      var patterns = this.getCode128Patterns();
      var checksum = codes[0];
      for (var i = 1; i < codes.length; i++) {
        checksum += codes[i] * i;
      }
      const checkCode = checksum % 103;
      const fullCodes = codes.concat([checkCode, 106]);
      let bars = "";
      for (let i = 0; i < fullCodes.length; i++) {
        const code = fullCodes[i];
        if (patterns[code] == null) {
      var checkCode = checksum % 103;
      var fullCodes = codes.concat([checkCode, 106]);
      var bars = "";
      for (var j = 0; j < fullCodes.length; j++) {
        if (patterns[fullCodes[j]] == null) {
          return null;
        }
        bars += patterns[code];
        bars += patterns[fullCodes[j]];
      }
      bars += "11";
      return bars;
    },
    buildCode128Svg(bars, text) {
      const quietModules = 20;
      const modules = quietModules * 2 + bars.split("").reduce((sum, n) => sum + parseInt(n, 10), 0);
      const moduleWidth = modules > 300 ? 1 : 2;
      const width = modules * moduleWidth;
      const barTop = 10;
      const barHeight = 110;
      let x = quietModules * moduleWidth;
      let black = true;
      let rects = "";
      for (let i = 0; i < bars.length; i++) {
        const w = parseInt(bars[i], 10) * moduleWidth;
    buildCode128Svg: function (bars, text) {
      var quietModules = 20;
      var modules = quietModules * 2 + bars.split("").reduce(function (sum, n) {
        return sum + parseInt(n, 10);
      }, 0);
      var moduleWidth = modules > 300 ? 1 : 2;
      var width = modules * moduleWidth;
      var barTop = 10;
      var barHeight = 110;
      var x = quietModules * moduleWidth;
      var black = true;
      var rects = "";
      for (var i = 0; i < bars.length; i++) {
        var w = parseInt(bars[i], 10) * moduleWidth;
        if (black) {
          rects += '<rect x="' + x + '" y="' + barTop + '" width="' + w + '" height="' + barHeight + '" fill="#000" shape-rendering="crispEdges" />';
        }
        x += w;
        black = !black;
      }
      return (
        '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="145" viewBox="0 0 ' + width + ' 145">' +
      return '<svg xmlns="http://www.w3.org/2000/svg" width="' + width + '" height="145" viewBox="0 0 ' + width + ' 145">' +
        '<rect width="100%" height="100%" fill="#fff" />' +
        rects +
        '<text x="' + (width / 2) + '" y="136" text-anchor="middle" font-family="monospace" font-size="14" fill="#111">' +
        this.escapeXml(text) +
        "</text>" +
        "</svg>"
      );
        "</text></svg>";
    },
    getCode128Patterns() {
    getCode128Patterns: function () {
      return [
        "212222", "222122", "222221", "121223", "121322", "131222", "122213", "122312", "132212", "221213",
        "221312", "231212", "112232", "122132", "122231", "113222", "123122", "123221", "223211", "221132",
@@ -260,7 +425,7 @@
        "114131", "311141", "411131", "211412", "211214", "211232", "2331112"
      ];
    },
    escapeXml(text) {
    escapeXml: function (text) {
      return String(text)
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
@@ -268,95 +433,11 @@
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&apos;");
    },
    getDevpStateInfo() {
      if (this.readOnly) {
          // Frontend filtering for readOnly mode
          if (this.searchStationId == "") {
              this.stationList = this.fullStationList;
          } else {
              this.stationList = this.fullStationList.filter(item => item.stationId == this.searchStationId);
              this.currentPage = 1;
          }
      } else if (this.$root.sendWs) {
        this.$root.sendWs(JSON.stringify({
          "url": "/console/latest/data/station",
          "data": {}
        }));
      }
    controlCommand: function () {
      this.postControl("/station/command/move", this.controlParam);
    },
    setStationList(res) {
      let that = this;
      if (res.code == 200) {
        let list = res.data;
        that.fullStationList = list;
        if (that.searchStationId == "") {
          that.stationList = list;
        } else {
          let tmp = [];
          list.forEach((item) => {
            if (item.stationId == that.searchStationId) {
              tmp.push(item);
            }
          });
          that.stationList = tmp;
          that.currentPage = 1;
        }
      }
    },
    openControl() {
      this.showControl = !this.showControl;
    },
    controlCommand() {
      let that = this;
      //下发命令
      $.ajax({
        url: baseUrl + "/station/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",
            });
          }
        },
      });
    },
    resetCommand() {
      let that = this;
      //下发命令
      $.ajax({
        url: baseUrl + "/station/command/reset",
        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",
            });
          }
        },
      });
    },
  },
    resetCommand: function () {
      this.postControl("/station/command/reset", this.controlParam);
    }
  }
});