(function () {
"use strict";
function nowDate() {
return new Date();
}
function startOfToday() {
var date = new Date();
date.setHours(0, 0, 0, 0);
return date;
}
function createDefaultFilters() {
return {
mode: "TASK",
keyword: "",
ioType: "",
finalWrkSts: "",
sourceStaNo: "",
staNo: "",
deviceType: "",
timeField: "finish_time",
timeRange: [startOfToday(), nowDate()]
};
}
function createEmptyAnalysis() {
return {
summary: {
taskCount: 0,
taskStartTime: null,
taskStartTime$: "",
taskEndTime: null,
taskEndTime$: "",
taskDurationMs: null,
avgTaskBeatDurationMs: null,
avgTotalDurationMs: null,
avgStationDurationMs: null,
avgCraneDurationMs: null,
faultTaskCount: 0,
faultDurationMs: 0,
partialTaskCount: 0
},
durationCompare: [],
trend: [],
faultPie: [],
faultDuration: [],
detail: []
};
}
new Vue({
el: "#app",
data: function () {
return {
options: {
ioTypes: [],
statuses: [],
stations: [],
deviceTypes: [],
timeFields: []
},
filters: createDefaultFilters(),
tableData: [],
currentPage: 1,
pageSize: 20,
pageTotal: 0,
listLoading: false,
analyzeLoading: false,
exportingPdf: false,
selectedWrkNoMap: {},
analysis: createEmptyAnalysis(),
analysisReady: false,
charts: {
duration: null,
trend: null,
faultPie: null,
faultDuration: null
},
resizeHandler: null
};
},
computed: {
selectedWrkNos: function () {
return Object.keys(this.selectedWrkNoMap).map(function (key) {
return Number(key);
}).filter(function (value) {
return !!value;
});
}
},
mounted: function () {
var self = this;
this.loadOptions();
this.loadList();
this.resizeHandler = function () {
self.resizeCharts();
};
window.addEventListener("resize", this.resizeHandler);
},
beforeDestroy: function () {
if (this.resizeHandler) {
window.removeEventListener("resize", this.resizeHandler);
}
this.disposeCharts();
},
methods: {
loadOptions: function () {
var self = this;
$.ajax({
url: baseUrl + "/wrkAnalysis/options/auth",
headers: { token: localStorage.getItem("token") },
method: "GET",
success: function (res) {
if (res && res.code === 200) {
self.options = Object.assign(self.options, res.data || {});
return;
}
self.$message.error((res && res.msg) || "分析选项加载失败");
},
error: function () {
self.$message.error("分析选项加载失败");
}
});
},
buildListParams: function () {
var params = {
curr: this.currentPage,
limit: this.pageSize,
keyword: this.filters.keyword,
ioType: this.filters.ioType,
finalWrkSts: this.filters.finalWrkSts,
sourceStaNo: this.filters.sourceStaNo,
staNo: this.filters.staNo,
deviceType: this.filters.deviceType
};
if (this.filters.timeRange && this.filters.timeRange.length === 2) {
if (this.filters.timeField === "appe_time") {
params.appeTimeRange = this.formatRange(this.filters.timeRange);
} else {
params.finishTimeRange = this.formatRange(this.filters.timeRange);
}
}
return this.cleanParams(params);
},
loadList: function () {
var self = this;
this.listLoading = true;
$.ajax({
url: baseUrl + "/wrkAnalysis/list/auth",
headers: { token: localStorage.getItem("token") },
method: "GET",
data: self.buildListParams(),
success: function (res) {
if (res && res.code === 200) {
var data = res.data || {};
self.tableData = data.records || [];
self.pageTotal = data.total || 0;
self.$nextTick(function () {
self.restoreSelection();
});
return;
}
self.$message.error((res && res.msg) || "历史任务加载失败");
},
error: function () {
self.$message.error("历史任务加载失败");
},
complete: function () {
self.listLoading = false;
}
});
},
handleSearch: function () {
this.currentPage = 1;
this.loadList();
},
handleReset: function () {
this.filters = createDefaultFilters();
this.currentPage = 1;
this.pageSize = 20;
this.selectedWrkNoMap = {};
this.analysis = createEmptyAnalysis();
this.analysisReady = false;
this.disposeCharts();
this.loadList();
},
handleSizeChange: function (size) {
this.pageSize = size;
this.currentPage = 1;
this.loadList();
},
handleCurrentChange: function (page) {
this.currentPage = page;
this.loadList();
},
restoreSelection: function () {
var table = this.$refs.historyTable;
var self = this;
if (!table) {
return;
}
table.clearSelection();
(this.tableData || []).forEach(function (row) {
if (self.selectedWrkNoMap[row.wrkNo]) {
table.toggleRowSelection(row, true);
}
});
},
syncCurrentPageSelection: function (selection) {
var nextMap = Object.assign({}, this.selectedWrkNoMap);
var selectedMap = {};
(selection || []).forEach(function (row) {
selectedMap[row.wrkNo] = true;
});
(this.tableData || []).forEach(function (row) {
delete nextMap[row.wrkNo];
});
Object.keys(selectedMap).forEach(function (key) {
nextMap[key] = true;
});
this.selectedWrkNoMap = nextMap;
},
runAnalysis: function () {
var self = this;
var request = {
mode: this.filters.mode,
ioType: this.filters.ioType,
finalWrkSts: this.filters.finalWrkSts,
sourceStaNo: this.filters.sourceStaNo,
staNo: this.filters.staNo,
deviceType: this.filters.deviceType
};
if (this.filters.mode === "TASK") {
if (!this.selectedWrkNos.length) {
this.$message.warning("请先勾选要分析的任务");
return;
}
request.wrkNos = this.selectedWrkNos;
request.timeField = this.filters.timeField;
} else {
if (!this.filters.timeRange || this.filters.timeRange.length !== 2) {
this.$message.warning("请先选择分析时间范围");
return;
}
request.timeField = this.filters.timeField;
request.startTime = this.filters.timeRange[0].getTime();
request.endTime = this.filters.timeRange[1].getTime();
}
this.analyzeLoading = true;
$.ajax({
url: baseUrl + "/wrkAnalysis/analyze/auth",
headers: {
token: localStorage.getItem("token"),
"Content-Type": "application/json"
},
method: "POST",
data: JSON.stringify(this.cleanParams(request)),
success: function (res) {
if (res && res.code === 200) {
self.analysis = Object.assign(createEmptyAnalysis(), res.data || {});
self.analysisReady = true;
self.$nextTick(function () {
self.updateCharts();
});
return;
}
self.$message.error((res && res.msg) || "分析失败");
},
error: function () {
self.$message.error("分析失败");
},
complete: function () {
self.analyzeLoading = false;
}
});
},
exportAnalysisPdf: function () {
var self = this;
if (!this.analysisReady) {
this.$message.warning("请先执行分析,再导出PDF");
return;
}
if (!window.html2canvas || !window.jspdf || !window.jspdf.jsPDF) {
this.$message.error("PDF导出组件加载失败");
return;
}
this.exportingPdf = true;
this.$nextTick(function () {
self.resizeCharts();
window.setTimeout(function () {
self.generatePdf();
}, 300);
});
},
generatePdf: function () {
var self = this;
var visualRoot = this.$refs.analysisVisualRoot;
var detailRoot = this.$refs.exportDetailRoot;
var detailTable = this.$refs.exportDetailTable;
var cleanup = function () {
self.exportingPdf = false;
self.$nextTick(function () {
self.resizeCharts();
});
};
if (!visualRoot || !detailRoot || !detailTable) {
this.$message.error("未找到可导出的分析区域");
cleanup();
return;
}
var jsPDF = window.jspdf.jsPDF;
var pdf = new jsPDF("p", "mm", "a4");
Promise.all([
window.html2canvas(visualRoot, this.buildCaptureOptions(visualRoot)),
window.html2canvas(detailRoot, this.buildCaptureOptions(detailRoot))
]).then(function (results) {
var visualAvoidSplitBounds = self.collectAvoidSplitBounds(visualRoot, results[0], [
".quality-banner",
".chart-card"
]);
self.appendCanvasSlicesToPdf(pdf, results[0], {
margin: 8,
startY: 8,
addNewPage: false,
avoidSplitBounds: visualAvoidSplitBounds
});
self.appendDetailTableToPdf(pdf, results[1], detailRoot, detailTable, 8);
pdf.save(self.buildPdfFileName());
self.$message.success("PDF导出成功");
cleanup();
}).catch(function (error) {
console.error(error);
self.$message.error("PDF导出失败");
cleanup();
});
},
buildCaptureOptions: function (target) {
return {
scale: 2,
useCORS: true,
backgroundColor: "#ffffff",
logging: false,
scrollX: 0,
scrollY: -window.scrollY,
width: target.scrollWidth,
height: target.scrollHeight,
windowWidth: Math.max(document.documentElement.clientWidth, target.scrollWidth),
windowHeight: Math.max(document.documentElement.clientHeight, target.scrollHeight)
};
},
appendCanvasSlicesToPdf: function (pdf, canvas, options) {
var settings = options || {};
var pageWidth = pdf.internal.pageSize.getWidth();
var pageHeight = pdf.internal.pageSize.getHeight();
var margin = settings.margin == null ? 8 : settings.margin;
var startY = settings.startY == null ? margin : settings.startY;
var usableWidth = pageWidth - margin * 2;
var pxPerMm = canvas.width / usableWidth;
var renderedHeight = 0;
var currentY = startY;
var pageCanvas = document.createElement("canvas");
var pageContext = pageCanvas.getContext("2d");
while (renderedHeight < canvas.height) {
var currentPageHeightPx = Math.max(1, Math.floor((pageHeight - margin - currentY) * pxPerMm));
var sliceHeight = Math.min(currentPageHeightPx, canvas.height - renderedHeight);
sliceHeight = this.resolveSliceHeight(renderedHeight, sliceHeight, settings.avoidSplitBounds);
pageCanvas.width = canvas.width;
pageCanvas.height = sliceHeight;
pageContext.fillStyle = "#ffffff";
pageContext.fillRect(0, 0, pageCanvas.width, pageCanvas.height);
pageContext.drawImage(
canvas,
0,
renderedHeight,
canvas.width,
sliceHeight,
0,
0,
pageCanvas.width,
pageCanvas.height
);
if (renderedHeight === 0 && settings.addNewPage) {
pdf.addPage();
} else if (renderedHeight > 0) {
pdf.addPage();
currentY = margin;
}
pdf.addImage(
pageCanvas.toDataURL("image/jpeg", 0.95),
"JPEG",
margin,
currentY,
usableWidth,
sliceHeight / pxPerMm,
undefined,
"FAST"
);
renderedHeight += sliceHeight;
currentY = margin;
}
},
collectAvoidSplitBounds: function (rootElement, rootCanvas, selectors) {
if (!rootElement || !rootCanvas || !selectors || !selectors.length) {
return [];
}
var rootRect = rootElement.getBoundingClientRect();
if (!rootRect.width) {
return [];
}
var scale = rootCanvas.width / rootRect.width;
var elements = [];
selectors.forEach(function (selector) {
Array.prototype.push.apply(elements, Array.prototype.slice.call(rootElement.querySelectorAll(selector)));
});
return elements.map(function (element) {
var rect = element.getBoundingClientRect();
return {
top: Math.max(0, Math.round((rect.top - rootRect.top) * scale)),
bottom: Math.max(0, Math.round((rect.bottom - rootRect.top) * scale))
};
}).filter(function (item) {
return item.bottom > item.top;
}).sort(function (a, b) {
return a.top - b.top;
});
},
resolveSliceHeight: function (renderedHeight, desiredHeight, avoidSplitBounds) {
if (!avoidSplitBounds || !avoidSplitBounds.length) {
return desiredHeight;
}
var sliceEnd = renderedHeight + desiredHeight;
var adjustedHeight = desiredHeight;
avoidSplitBounds.forEach(function (bound) {
if (bound.top <= renderedHeight) {
return;
}
if (bound.top >= sliceEnd || bound.bottom <= sliceEnd) {
return;
}
var candidateHeight = bound.top - renderedHeight;
if (candidateHeight > 0 && candidateHeight < adjustedHeight) {
adjustedHeight = candidateHeight;
}
});
return adjustedHeight > 0 ? adjustedHeight : desiredHeight;
},
appendDetailTableToPdf: function (pdf, rootCanvas, detailRoot, detailTable, margin) {
var tbody = detailTable && detailTable.tBodies && detailTable.tBodies[0];
var rows = tbody ? Array.prototype.slice.call(tbody.rows) : [];
if (!rows.length) {
return;
}
var pageWidth = pdf.internal.pageSize.getWidth();
var pageHeight = pdf.internal.pageSize.getHeight();
var usableWidth = pageWidth - margin * 2;
var usableHeight = pageHeight - margin * 2;
var rootRect = detailRoot.getBoundingClientRect();
var tableRect = detailTable.getBoundingClientRect();
var scale = rootCanvas.width / rootRect.width;
var pxPerMm = rootCanvas.width / usableWidth;
var pageHeightPx = Math.max(1, Math.floor(usableHeight * pxPerMm));
var titleHeightPx = Math.max(0, Math.round((tableRect.top - rootRect.top) * scale));
var headerHeightPx = Math.max(1, Math.round(detailTable.tHead.getBoundingClientRect().height * scale));
var rowBounds = rows.map(function (row) {
var rect = row.getBoundingClientRect();
return {
top: Math.round((rect.top - tableRect.top) * scale),
bottom: Math.round((rect.bottom - tableRect.top) * scale)
};
});
var firstPage = true;
var startIndex = 0;
while (startIndex < rowBounds.length) {
var bodyCapacityPx = pageHeightPx - headerHeightPx - (firstPage ? titleHeightPx : 0);
var endIndex = this.findLastFittingRowIndex(rowBounds, startIndex, bodyCapacityPx);
var pageCanvas = this.createDetailPageCanvas(
rootCanvas,
titleHeightPx,
headerHeightPx,
rowBounds[startIndex].top,
rowBounds[endIndex].bottom,
firstPage
);
pdf.addPage();
pdf.addImage(
pageCanvas.toDataURL("image/jpeg", 0.95),
"JPEG",
margin,
margin,
usableWidth,
pageCanvas.height / pxPerMm,
undefined,
"FAST"
);
firstPage = false;
startIndex = endIndex + 1;
}
},
findLastFittingRowIndex: function (rowBounds, startIndex, bodyCapacityPx) {
var lastIndex = startIndex;
for (var i = startIndex; i < rowBounds.length; i++) {
if (rowBounds[i].bottom - rowBounds[startIndex].top > bodyCapacityPx) {
break;
}
lastIndex = i;
}
return lastIndex;
},
createDetailPageCanvas: function (rootCanvas, titleHeightPx, headerHeightPx, bodyStartPx, bodyEndPx, includeTitle) {
var width = rootCanvas.width;
var titleHeight = includeTitle ? titleHeightPx : 0;
var bodyHeight = Math.max(1, bodyEndPx - bodyStartPx);
var pageCanvas = document.createElement("canvas");
var pageContext = pageCanvas.getContext("2d");
pageCanvas.width = width;
pageCanvas.height = titleHeight + headerHeightPx + bodyHeight;
pageContext.fillStyle = "#ffffff";
pageContext.fillRect(0, 0, pageCanvas.width, pageCanvas.height);
var offsetY = 0;
if (titleHeight > 0) {
pageContext.drawImage(
rootCanvas,
0,
0,
width,
titleHeight,
0,
0,
width,
titleHeight
);
offsetY += titleHeight;
}
pageContext.drawImage(
rootCanvas,
0,
titleHeightPx,
width,
headerHeightPx,
0,
offsetY,
width,
headerHeightPx
);
offsetY += headerHeightPx;
pageContext.drawImage(
rootCanvas,
0,
titleHeightPx + bodyStartPx,
width,
bodyHeight,
0,
offsetY,
width,
bodyHeight
);
return pageCanvas;
},
buildPdfFileName: function () {
var now = new Date();
return "任务执行分析_" +
now.getFullYear() +
this.pad(now.getMonth() + 1) +
this.pad(now.getDate()) + "_" +
this.pad(now.getHours()) +
this.pad(now.getMinutes()) +
this.pad(now.getSeconds()) + ".pdf";
},
updateCharts: function () {
if (!this.analysisReady) {
this.disposeCharts();
return;
}
this.ensureCharts();
this.renderDurationChart();
this.renderTrendChart();
this.renderFaultPieChart();
this.renderFaultDurationChart();
},
ensureCharts: function () {
if (this.$refs.durationChart && !this.charts.duration) {
this.charts.duration = echarts.init(this.$refs.durationChart);
}
if (this.$refs.trendChart && !this.charts.trend) {
this.charts.trend = echarts.init(this.$refs.trendChart);
}
if (this.$refs.faultPieChart && !this.charts.faultPie) {
this.charts.faultPie = echarts.init(this.$refs.faultPieChart);
}
if (this.$refs.faultDurationChart && !this.charts.faultDuration) {
this.charts.faultDuration = echarts.init(this.$refs.faultDurationChart);
}
},
renderDurationChart: function () {
if (!this.charts.duration) {
return;
}
var self = this;
var rows = this.analysis.durationCompare || [];
this.charts.duration.setOption({
tooltip: {
trigger: "axis",
formatter: function (params) {
if (!params || !params.length) {
return "";
}
var lines = [params[0].axisValue];
params.forEach(function (item) {
lines.push(item.marker + item.seriesName + ": " + self.formatChartSeconds(item.value));
});
return lines.join("
");
}
},
legend: { data: ["站点耗时", "堆垛机耗时", "总耗时"] },
grid: { left: 88, right: 20, top: 40, bottom: 70, containLabel: true },
xAxis: {
type: "category",
data: rows.map(function (item) { return String(item.wrkNo); }),
axisLabel: { rotate: rows.length > 8 ? 30 : 0 }
},
yAxis: {
type: "value",
axisLabel: {
formatter: function (value) {
return self.formatChartSeconds(value);
}
}
},
series: [
{ name: "站点耗时", type: "bar", barMaxWidth: 28, data: rows.map(function (item) { return self.toChartSeconds(item.stationDurationMs); }) },
{ name: "堆垛机耗时", type: "bar", barMaxWidth: 28, data: rows.map(function (item) { return self.toChartSeconds(item.craneDurationMs); }) },
{ name: "总耗时", type: "bar", barMaxWidth: 28, data: rows.map(function (item) { return self.toChartSeconds(item.totalDurationMs); }) }
]
}, true);
},
renderTrendChart: function () {
if (!this.charts.trend) {
return;
}
var self = this;
var rows = this.analysis.trend || [];
this.charts.trend.setOption({
tooltip: {
trigger: "axis",
formatter: function (params) {
if (!params || !params.length) {
return "";
}
var lines = [params[0].axisValue];
params.forEach(function (item) {
lines.push(item.marker + item.seriesName + ": " + self.formatChartSeconds(item.value));
});
return lines.join("
");
}
},
legend: { data: ["平均总耗时", "平均站点耗时", "平均堆垛机耗时"] },
grid: { left: 88, right: 20, top: 40, bottom: 70, containLabel: true },
xAxis: {
type: "category",
data: rows.map(function (item) { return item.bucketLabel; }),
axisLabel: { rotate: rows.length > 8 ? 25 : 0 }
},
yAxis: {
type: "value",
axisLabel: {
formatter: function (value) {
return self.formatChartSeconds(value);
}
}
},
series: [
{ name: "平均总耗时", type: "line", smooth: true, data: rows.map(function (item) { return self.toChartSeconds(item.avgTotalDurationMs); }) },
{ name: "平均站点耗时", type: "line", smooth: true, data: rows.map(function (item) { return self.toChartSeconds(item.avgStationDurationMs); }) },
{ name: "平均堆垛机耗时", type: "line", smooth: true, data: rows.map(function (item) { return self.toChartSeconds(item.avgCraneDurationMs); }) }
]
}, true);
},
renderFaultPieChart: function () {
if (!this.charts.faultPie) {
return;
}
this.charts.faultPie.setOption({
tooltip: { trigger: "item" },
legend: { bottom: 0 },
series: [{
type: "pie",
radius: ["42%", "68%"],
center: ["50%", "46%"],
label: { formatter: "{b}\n{d}%" },
data: this.analysis.faultPie || []
}]
}, true);
},
renderFaultDurationChart: function () {
if (!this.charts.faultDuration) {
return;
}
var self = this;
var rows = this.analysis.faultDuration || [];
this.charts.faultDuration.setOption({
tooltip: {
trigger: "axis",
formatter: function (params) {
if (!params || !params.length) {
return "";
}
var lines = [params[0].axisValue];
params.forEach(function (item) {
lines.push(item.marker + item.seriesName + ": " + self.formatChartSeconds(item.value));
});
return lines.join("
");
}
},
grid: { left: 88, right: 20, top: 20, bottom: 68, containLabel: true },
xAxis: {
type: "category",
data: rows.map(function (item) { return item.name; }),
axisLabel: {
interval: 0,
rotate: rows.length > 6 ? 30 : 0
}
},
yAxis: {
type: "value",
axisLabel: {
formatter: function (value) {
return self.formatChartSeconds(value);
}
}
},
series: [{
name: "故障耗时",
type: "bar",
barMaxWidth: 36,
data: rows.map(function (item) { return self.toChartSeconds(item.value); })
}]
}, true);
},
resizeCharts: function () {
Object.keys(this.charts).forEach(function (key) {
if (this.charts[key]) {
this.charts[key].resize();
}
}, this);
},
disposeCharts: function () {
Object.keys(this.charts).forEach(function (key) {
if (this.charts[key]) {
this.charts[key].dispose();
this.charts[key] = null;
}
}, this);
},
formatRange: function (range) {
return this.formatDateTime(range[0]) + " ~ " + this.formatDateTime(range[1]);
},
formatDateTime: function (date) {
if (!date) {
return "";
}
var year = date.getFullYear();
var month = this.pad(date.getMonth() + 1);
var day = this.pad(date.getDate());
var hour = this.pad(date.getHours());
var minute = this.pad(date.getMinutes());
var second = this.pad(date.getSeconds());
return year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second;
},
pad: function (value) {
return value < 10 ? "0" + value : String(value);
},
cleanParams: function (params) {
var result = {};
Object.keys(params || {}).forEach(function (key) {
var value = params[key];
if (value === "" || value === null || value === undefined) {
return;
}
if (Array.isArray(value) && !value.length) {
return;
}
result[key] = value;
});
return result;
},
formatNumber: function (value) {
var num = Number(value || 0);
if (!isFinite(num)) {
return "0";
}
return num.toLocaleString("zh-CN");
},
toChartSeconds: function (value) {
var num = Number(value || 0);
if (!isFinite(num)) {
return 0;
}
return Number((num / 1000).toFixed(3));
},
formatChartSeconds: function (value) {
var num = Number(value || 0);
if (!isFinite(num)) {
return "0s";
}
return this.formatDurationBySeconds(num);
},
formatDuration: function (value) {
if (value === null || value === undefined || value === "") {
return "--";
}
var ms = Number(value);
if (!isFinite(ms)) {
return "--";
}
if (ms < 1000) {
return Math.round(ms) + " ms";
}
return this.formatDurationBySeconds(ms / 1000);
},
formatDurationBySeconds: function (seconds) {
var totalSeconds = Number(seconds || 0);
if (!isFinite(totalSeconds)) {
return "0s";
}
var safeSeconds = Math.max(0, totalSeconds);
if (safeSeconds < 60) {
return this.trimTrailingZeros(safeSeconds) + "s";
}
var hours = Math.floor(safeSeconds / 3600);
var minutes = Math.floor((safeSeconds % 3600) / 60);
var remainSeconds = safeSeconds - hours * 3600 - minutes * 60;
var secondText = this.trimTrailingZeros(remainSeconds);
if (hours > 0) {
return hours + "h" + this.pad(minutes) + "m" + this.padSeconds(secondText) + "s";
}
return minutes + "m" + this.padSeconds(secondText) + "s";
},
trimTrailingZeros: function (value) {
var text = String(Number(Number(value).toFixed(3)));
if (text.indexOf(".") >= 0) {
text = text.replace(/0+$/, "").replace(/\.$/, "");
}
return text;
},
padSeconds: function (value) {
var text = String(value);
if (text.indexOf(".") >= 0) {
var parts = text.split(".");
return (parts[0].length < 2 ? "0" + parts[0] : parts[0]) + "." + parts[1];
}
return text.length < 2 ? "0" + text : text;
}
}
});
})();