Vue.component("devp-card", {
|
template: `
|
<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>
|
|
<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>
|
|
<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
|
:src="getBarcodePreview(item.barcode)"
|
:alt="'barcode-' + item.barcode"
|
style="display: block; max-width: 100%; height: auto; margin: 0 auto; image-rendering: pixelated; background: #fff;"
|
/>
|
<div style="margin-top: 4px; font-size: 12px; word-break: break-all;">{{ item.barcode }}</div>
|
</div>
|
<span
|
slot="reference"
|
@click.stop="handleBarcodeClick(item)"
|
style="cursor: pointer; color: #4677a4; font-weight: 600;"
|
>{{ entry.value }}</span>
|
</el-popover>
|
<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 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: function () { return {}; } },
|
items: { type: Array, default: null },
|
autoRefresh: { type: Boolean, default: true },
|
readOnly: { type: Boolean, default: false }
|
},
|
data: function () {
|
return {
|
stationList: [],
|
activeNames: "",
|
searchStationId: "",
|
showControl: false,
|
controlParam: {
|
stationId: "",
|
taskNo: "",
|
targetStationId: ""
|
},
|
barcodePreviewCache: {},
|
pageSize: 12,
|
currentPage: 1,
|
timer: null
|
};
|
},
|
computed: {
|
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: {
|
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: {
|
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;
|
},
|
getDevpStateInfo: function () {
|
if (this.$root && this.$root.sendWs) {
|
this.$root.sendWs(JSON.stringify({
|
url: "/console/latest/data/station",
|
data: {}
|
}));
|
}
|
},
|
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];
|
}
|
var encodeResult = this.encodeCode128(value);
|
if (!encodeResult) {
|
return "";
|
}
|
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: function (value) {
|
if (!value) {
|
return null;
|
}
|
var isNumeric = /^\d+$/.test(value);
|
if (isNumeric && value.length % 2 === 0) {
|
return this.encodeCode128C(value);
|
}
|
return this.encodeCode128B(value);
|
},
|
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;
|
}
|
codes.push(code);
|
}
|
return this.buildCode128Pattern(codes);
|
},
|
encodeCode128C: function (value) {
|
if (value.length % 2 !== 0) {
|
return null;
|
}
|
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: function (codes) {
|
var patterns = this.getCode128Patterns();
|
var checksum = codes[0];
|
for (var i = 1; i < codes.length; i++) {
|
checksum += codes[i] * i;
|
}
|
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[fullCodes[j]];
|
}
|
bars += "11";
|
return bars;
|
},
|
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">' +
|
'<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>";
|
},
|
getCode128Patterns: function () {
|
return [
|
"212222", "222122", "222221", "121223", "121322", "131222", "122213", "122312", "132212", "221213",
|
"221312", "231212", "112232", "122132", "122231", "113222", "123122", "123221", "223211", "221132",
|
"221231", "213212", "223112", "312131", "311222", "321122", "321221", "312212", "322112", "322211",
|
"212123", "212321", "232121", "111323", "131123", "131321", "112313", "132113", "132311", "211313",
|
"231113", "231311", "112133", "112331", "132131", "113123", "113321", "133121", "313121", "211331",
|
"231131", "213113", "213311", "213131", "311123", "311321", "331121", "312113", "312311", "332111",
|
"314111", "221411", "431111", "111224", "111422", "121124", "121421", "141122", "141221", "112214",
|
"112412", "122114", "122411", "142112", "142211", "241211", "221114", "413111", "241112", "134111",
|
"111242", "121142", "121241", "114212", "124112", "124211", "411212", "421112", "421211", "212141",
|
"214121", "412121", "111143", "111341", "131141", "114113", "114311", "411113", "411311", "113141",
|
"114131", "311141", "411131", "211412", "211214", "211232", "2331112"
|
];
|
},
|
escapeXml: function (text) {
|
return String(text)
|
.replace(/&/g, "&")
|
.replace(/</g, "<")
|
.replace(/>/g, ">")
|
.replace(/"/g, """)
|
.replace(/'/g, "'");
|
},
|
controlCommand: function () {
|
this.postControl("/station/command/move", this.controlParam);
|
},
|
resetCommand: function () {
|
this.postControl("/station/command/reset", this.controlParam);
|
}
|
}
|
});
|