(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; } } }); })();