<template> <div class="app-container"> <div style="margin-top: 10px"> <grid-layout :layout.sync="layout" :col-num="12" :row-height="30" :is-draggable="draggable" :is-resizable="resizable" :vertical-compact="true" :use-css-transforms="true" @layout-created="layoutCreatedEvent" @layout-before-mount="layoutBeforeMountEvent" @layout-mounted="layoutMountedEvent" @layout-ready="layoutReadyEvent" @layout-updated="layoutUpdatedEvent" > <grid-item :x="0" :y="0" :w="6" :h="5" i="1" @resize="resizeEvent" @move="moveEvent" @resized="resizedEvent" @container-resized="containerResizedEvent" @moved="movedEvent" > <p class="item-title">DEVICE</p> <div style="padding: 15px"> <el-form :inline="true"> <el-form-item label="选择设备"> <el-select size="mini" v-model="device" @change="onSelectChange" placeholder="请选择设备" > <el-option v-for="(item, index) in deviceList" :key="index" :label="item" :value="item" ></el-option> </el-select> </el-form-item> <el-form-item label="资源监控报告"> <el-button size="mini" @click="getReport">导出报告</el-button> </el-form-item> <el-form-item label="页面图片显示设置"> <el-switch v-model="pngShowMode" active-text="自动显示当前页面" inactive-text="手动选择页面" > </el-switch> </el-form-item> </el-form> </div> </grid-item> <grid-item :x="6" :y="0" :w="6" :h="5" i="2" @resize="resizeEvent" @move="moveEvent" @resized="resizedEvent" @container-resized="containerResizedEvent" @moved="movedEvent" > <p class="item-title">SYSTEM</p> <el-table element-loading-text="Loading" :data="systemList" size="mini" border stripe fit highlight-current-row > <el-table-column prop="host" label="host" min-width="150" show-overflow-tooltip ></el-table-column> <el-table-column prop="timestamp" label="timestamp" min-width="150" show-overflow-tooltip ></el-table-column> <el-table-column prop="free_size" label="free_size" min-width="150" show-overflow-tooltip > <template slot-scope="scope" >{{ scope.row.free_size }}(KB)</template > </el-table-column> <el-table-column prop="free_space_size" label="free_space_size" min-width="150" show-overflow-tooltip > <template slot-scope="scope" >{{ scope.row.free_space_size }}(KB)</template > </el-table-column> <el-table-column label="used_space_size" min-width="150" show-overflow-tooltip > <template slot-scope="scope" >{{ scope.row.used_space_size }}(KB)</template > </el-table-column> </el-table> </grid-item> <grid-item :x="0" :y="5" :w="6" :h="5" i="3" @resize="resizeEvent" @move="moveEvent" @resized="resizedEvent" @container-resized="containerResizedEvent" @moved="movedEvent" > <p class="item-title">EVM</p> <el-table element-loading-text="Loading" :data="evmList" size="mini" border stripe fit highlight-current-row > <el-table-column label="heap_map_size" min-width="150" show-overflow-tooltip > <template slot-scope="scope" >{{ scope.row.heap_map_size }}(KB)</template > </el-table-column> <el-table-column label="heap_total_size" min-width="150" show-overflow-tooltip > <template slot-scope="scope" >{{ scope.row.heap_total_size }}(KB)</template > </el-table-column> <el-table-column label="heap_used_size" min-width="150" show-overflow-tooltip > <template slot-scope="scope" >{{ scope.row.heap_used_size }}(KB)</template > </el-table-column> <el-table-column label="stack_total_size" min-width="150" show-overflow-tooltip > <template slot-scope="scope" >{{ scope.row.stack_total_size }}(KB)</template > </el-table-column> <el-table-column label="stack_used_size" min-width="150" show-overflow-tooltip > <template slot-scope="scope" >{{ scope.row.stack_used_size }}(KB)</template > </el-table-column> </el-table> </grid-item> <grid-item :x="6" :y="5" :w="6" :h="5" i="4" @resize="resizeEvent" @move="moveEvent" @resized="resizedEvent" @container-resized="containerResizedEvent" @moved="movedEvent" > <p class="item-title">LVGL</p> <el-table element-loading-text="Loading" :data="lvglList" size="mini" border stripe fit highlight-current-row > <el-table-column label="total_size" min-width="100" show-overflow-tooltip > <template slot-scope="scope" >{{ scope.row.total_size }}(KB)</template > </el-table-column> <el-table-column prop="free_cnt" label="free_cnt" min-width="100" show-overflow-tooltip ></el-table-column> <el-table-column label="free_size" min-width="120" show-overflow-tooltip > <template slot-scope="scope" >{{ scope.row.free_size }}(KB)</template > </el-table-column> <el-table-column label="free_biggest_size" min-width="120"> <template slot-scope="scope" >{{ scope.row.free_biggest_size }}(KB)</template > </el-table-column> <el-table-column label="used_cnt" min-width="100"> <template slot-scope="scope">{{ scope.row.used_cnt }}</template> </el-table-column> <el-table-column label="used_pct" min-width="100"> <template slot-scope="scope" >{{ scope.row.used_pct }}(%)</template > </el-table-column> <el-table-column label="frag_pct" min-width="100"> <template slot-scope="scope" >{{ scope.row.frag_pct }}(%)</template > </el-table-column> </el-table> </grid-item> <grid-item :x="0" :y="10" :w="8" :h="10" i="5" @resize="resizeEvent" @move="moveEvent" @resized="resizedEvent" @container-resized="containerResizedEvent" @moved="movedEvent" > <div style="width: 100%; height: 100%; overflow-y: auto"> <p class="item-title">APP</p> <el-table element-loading-text="Loading" :data="imageList" size="mini" border fit :row-class-name="tableRowClassName" @row-click="onTableRowClick" > <el-table-column prop="uri" label="uri" min-width="150" show-overflow-tooltip ></el-table-column> <el-table-column label="length" min-width="150" show-overflow-tooltip > <template slot-scope="scope" >{{ scope.row.length }}(KB)</template > </el-table-column> <el-table-column label="png_file_size" min-width="150" show-overflow-tooltip > <template slot-scope="scope" >{{ scope.row.png_file_size }}(KB)</template > </el-table-column> <el-table-column prop="png_total_count" label="png_total_count" min-width="150" show-overflow-tooltip ></el-table-column> <el-table-column label="png_uncompressed_size" min-width="150" show-overflow-tooltip > <template slot-scope="scope" >{{ scope.row.png_uncompressed_size }}(KB)</template > </el-table-column> </el-table> </div> </grid-item> <grid-item :x="8" :y="10" :w="4" :h="10" i="6" @resize="resizeEvent" @move="moveEvent" @resized="resizedEvent" @container-resized="containerResizedEvent" @moved="movedEvent" > <div style="width: 100%; height: 100%; overflow-y: auto"> <el-table :data="pngList" style="width: 100%"> <el-table-column prop="uri" label="uri" min-width="180" ></el-table-column> <el-table-column prop="filesize" label="file size" width="100" ></el-table-column> <el-table-column prop="uncompressed_size" label="origin size" width="100" ></el-table-column> <el-table-column prop="ratio" label="ratio" width="100"> </el-table-column> </el-table> </div> </grid-item> <grid-item :x="0" :y="20" :w="4" :h="14" i="7" @resize="resizeEvent" @move="moveEvent" @resized="resizedEvent" @container-resized="containerResizedEvent" @moved="movedEvent" > <div> <p class="item-title">应用大小</p> <el-table :data="appList" size="mini" style="width: 100%"> <el-table-column prop="appName" label="应用名称" width="180"></el-table-column> <el-table-column prop="fileSize" label="应用大小(KB)"></el-table-column> <el-table-column prop="fileCount" label="文件个数" width="100"> </el-table-column> </el-table> </div> </grid-item> <grid-item :x="4" :y="20" :w="8" :h="7" i="7" @resize="resizeEvent" @move="moveEvent" @resized="resizedEvent" @container-resized="containerResizedEvent" @moved="movedEvent" > <SystemChart :chartData="system" :dataList="systemHistory" ></SystemChart> </grid-item> <grid-item :x="4" :y="27" :w="8" :h="7" i="8" @resize="resizeEvent" @move="moveEvent" @resized="resizedEvent" @container-resized="containerResizedEvent" @moved="movedEvent" > <EvmChart :chartData="evm" :dataList="evmHistory"></EvmChart> </grid-item> <grid-item :x="0" :y="34" :w="12" :h="7" i="9" @resize="resizeEvent" @move="moveEvent" @resized="resizedEvent" @container-resized="containerResizedEvent" @moved="movedEvent" > <LvglChart :chartData="lvgl" :dataList="lvglHistory"></LvglChart> </grid-item> </grid-layout> </div> </div> </template> <script> import { getWatchList, getMonitorData, getReport, getTemplate, setTemplate, } from "@/api/app-store"; import EvmChart from "./components/EvmChart"; import LvglChart from "./components/LvglChart"; import SystemChart from "./components/SystemChart"; import { GridLayout, GridItem } from "vue-grid-layout"; import { wsNotify } from "@/utils/eventBus.js"; import { deepClone, download } from "@/utils/index"; import Database from "@/utils/indexedDB"; const dbObject = { dbName: "evmiot", // 数据库名 version: 1, // 版本号 primaryKey: "id", // 主键 keyNames: [ { // 需要存储的数据字段对象 key: "system.timestamp", // 字段名 unique: false, // 当前这条数据是否能重复 (最常用) 默认false }, { key: "imei", unique: false, }, ], }; const indexedDb = Database(), jsonFile = "evue-monitor.json"; let monitor = new indexedDb(dbObject); function resetResult() { return { systemMax: {}, systemMin: {}, systemAvg: {}, systemFirst: {}, systemLast: {}, evmMax: {}, evmMin: {}, evmAvg: {}, evmFirst: {}, evmLast: {}, lvglMax: {}, lvglMin: {}, lvglAvg: {}, lvglFirst: {}, lvglLast: {}, systemImg: null, evmImg: null, lvglImg: null, imageList: [], }; } const result = resetResult(); const system = { free_size: null, free_space_size: null, used_space_size: null, }, evm = { heap_map_size: null, heap_total_size: null, heap_used_size: null, stack_total_size: null, stack_used_size: null, }, lvgl = { frag_pct: null, free_biggest_size: null, free_cnt: null, free_size: null, total_size: null, used_cnt: null, used_pct: null, }; export default { name: "Monitor", data() { return { watchs: [], pngList: [], globalData: null, device: null, devices: {}, deviceList: null, system: { free_size: null, free_space_size: null, used_space_size: null, }, systemList: [], systemHistory: [], evmHistory: [], lvglHistory: [], evm: { heap_map_size: null, heap_total_size: null, heap_used_size: null, stack_total_size: null, stack_used_size: null, }, evmList: [], lvgl: {}, lvglList: [], image: {}, imageList: [], pngShowMode: true, currentPngList: [], socket: null, form: { system: ["free_size", "free_space_size", "used_space_size"], lvgl: ["total_size", "free_size", "free_biggest_size"], evm: [ "heap_map_size", "heap_total_size", "heap_used_size", "stack_total_size", "stack_used_size", ], image: ["png_uncompressed_size", "png_file_size", "length"], }, layout: [ { x: 0, y: 0, w: 6, h: 5, i: "1", static: false }, { x: 6, y: 0, w: 6, h: 5, i: "2", static: true }, { x: 0, y: 5, w: 6, h: 5, i: "3", static: false }, { x: 6, y: 5, w: 6, h: 5, i: "4", static: false }, { x: 0, y: 10, w: 8, h: 10, i: "5", static: false }, { x: 8, y: 10, w: 4, h: 10, i: "6", static: false }, { x: 0, y: 20, w: 12, h: 7, i: "7", static: false }, { x: 0, y: 27, w: 12, h: 7, i: "8", static: false }, { x: 0, y: 34, w: 12, h: 7, i: "9", static: false }, ], draggable: true, resizable: true, appList: [], }; }, components: { GridLayout, GridItem, EvmChart, LvglChart, SystemChart, }, methods: { onTableRowClick(row) { this.pngShowMode = false; this.pngList = this.currentPngList[row.uri]; }, getTemplate() { getTemplate() .then((res) => { console.log(res); }) .catch((err) => { this.$message.error(err.msg); }); }, getReport() { setTemplate({ templateName: jsonFile }) .then((res) => { console.log(res.msg); }) .catch((err) => { console.error(err.msg); }); wsNotify.eventBus.$emit("export-picture"); monitor .getAllData((params) => { return params.imei && params.imei == this.device; }) .then((res) => { console.log(res); // 最大值 最小值 平均值 第一次 最后一次 const systemList = [], evmList = [], lvglList = []; let appList = []; res.forEach((item) => { systemList.push( Object.assign( { ts: Date.parse(item.system.timestamp) }, item.system ) ); evmList.push( Object.assign({ ts: Date.parse(item.system.timestamp) }, item.evm) ); lvglList.push( Object.assign( { ts: Date.parse(item.system.timestamp) }, item.lvgl ) ); }); appList = appList.concat( this.imageList.map((img) => { if (img.png_detail && img.png_detail.length) { result.imageList = result.imageList.concat( img.png_detail.map((p) => { p.page = img.uri; return p; }) ); } return Object.assign({ ts: Date.parse(this.globalData.system.timestamp), ...img, }); }) ); result.appList = appList; Object.keys(evm).forEach((k) => { result.evmFirst[k] = 0; result.evmLast[k] = 0; let first = 0, last = Date.now(); const t = evmList.map((item) => { if (item.ts > first) { first = item.ts; result.evmFirst[k] = item[k]; } if (item.ts < last) { last = item.ts; result.evmLast[k] = item[k]; } return item[k]; }); result.evmMax[k] = Math.max.apply(null, t); result.evmMin[k] = Math.min.apply(null, t); result.evmAvg[k] = Math.ceil( t.reduce((prev, curr) => prev + curr) / t.length ); }); Object.keys(lvgl).forEach((k) => { result.lvglFirst[k] = 0; result.lvglLast[k] = 0; let first = 0, last = Date.now(); const t = lvglList.map((item) => { if (item.ts > first) { first = item.ts; result.lvglFirst[k] = item[k]; } if (item.ts < last) { last = item.ts; result.lvglLast[k] = item[k]; } return item[k]; }); result.lvglMax[k] = Math.max.apply(null, t); result.lvglMin[k] = Math.min.apply(null, t); result.lvglAvg[k] = Math.ceil( t.reduce((prev, curr) => prev + curr) / t.length ); }); Object.keys(system).forEach((k) => { result.systemFirst[k] = 0; result.systemLast[k] = 0; let first = 0, last = Date.now(); const t = systemList.map((item) => { if (item.ts > first) { first = item.ts; result.systemFirst[k] = item[k]; } if (item.ts < last) { last = item.ts; result.systemLast[k] = item[k]; } return item[k]; }); result.systemMax[k] = Math.max.apply(null, t); result.systemMin[k] = Math.min.apply(null, t); result.systemAvg[k] = Math.ceil( t.reduce((prev, curr) => prev + curr) / t.length ); }); result.imei = this.globalData.imei; result.timestamp = this.globalData.system.timestamp; return getReport({ templateJson: jsonFile, dataJson: result, }); }) .then((res) => { if (res.code == 200) { download(res.data.file, res.data.url) .then((res) => { console.log(res); }) .catch((err) => { console.error(err); }); } console.log(res); }) .catch((err) => { console.error(err); }); }, tableRowClassName({ row }) { return row.highlight ? "success-row" : ""; }, moveEvent(i, newX, newY) { console.log(i, newX, newY); }, movedEvent(i, newX, newY) { console.log(i, newX, newY); }, resizeEvent(i, newH, newW, newHPx, newWPx) { console.log(i, newH, newW, newHPx, newWPx); }, resizedEvent(i, newX, newY, newHPx, newWPx) { console.log(i, newX, newY, newHPx, newWPx); }, containerResizedEvent(i, newH, newW, newHPx, newWPx) { console.log(i, newH, newW, newHPx, newWPx); }, layoutCreatedEvent(newLayout) { console.log("Created layout: ", newLayout); }, layoutBeforeMountEvent(newLayout) { console.log("beforeMount layout: ", newLayout); }, layoutMountedEvent(newLayout) { console.log("Mounted layout: ", newLayout); }, layoutReadyEvent(newLayout) { console.log("Ready layout: ", newLayout); }, layoutUpdatedEvent(newLayout) { console.log("Updated layout: ", newLayout); }, fetchData() { this.isLoading = true; getWatchList() .then((res) => { if (res.code == 200) this.watchs = res.data; }) .catch((err) => { this.$message.warning(err.msg); }) .finally(() => { this.isLoading = false; }); }, queryData() { getMonitorData({ watch: this.device, }) .then((res) => { if (res.type == "object") { this.evmList = res.data.evm; this.lvglList = res.data.lvgl; this.imageList = res.data.image; } }) .catch((err) => { this.$message.warning(err.msg); }); }, onChange(res) { if (!res) return null; var t = this.watchs.find((item) => { return item.id == res; }); if (t) this.device = t.imei; // 清空之前数据 this.resetData(); }, onSubmit() { this.queryData(); }, onReset(formName) { this.$refs[formName].resetFields(); this.fetchData(); }, sendMsg() { let message = JSON.stringify({ type: "auth", token: this.$store.getters.token, }); this.socket.send(message); }, handleMessage(msg) { if (msg.code === 401) { this.$store.dispatch("user/removeRole"); this.$store.dispatch("user/removeToken"); // this.$router.push("/login?action=refresh"); wsNotify.$emit("reconnect"); } if (msg.type !== "report" || !msg.imei) return null; // 将设备发送过来的消息存储到浏览器中 // 这里可以优化,将所有数据,保存到indexed datebase中 const m = deepClone(msg); if (!this.deviceList) { this.deviceList = []; } if (!this.deviceList.includes(msg.imei)) { this.deviceList.push(msg.imei); } if (!this.device) { if (this.deviceList && this.deviceList.length) this.device = this.deviceList[0]; else this.device = msg.imei; } // 处理单位 this.processData(m); if (monitor.db) monitor.set(m); // 如果接收到的数据不是当前选中的设备,那么则直接丢弃 if (msg.imei != this.device) { return null; } this.globalData = msg; deepClone(msg).image.forEach((item) => { if (item.png_detail && item.png_detail.length) { this.currentPngList[item.uri] = item.png_detail; } else { this.currentPngList[item.uri] = []; } }); this.resetData(m); }, processData(msg) { function isNumber(value) { return typeof value === "number" && !isNaN(value); } if (!msg) return null; Object.keys(msg).forEach((item) => { if (this.form[item]) { var keys = this.form[item]; for (var i = 0; i < keys.length; i++) { var k = keys[i]; if (item == "image") { for (var j = 0; j < msg[item].length; j++) { if (isNumber(msg[item][j][k])) msg[item][j][k] = Math.ceil(msg[item][j][k] / 1024); } } else { msg[item][k] = Math.ceil(msg[item][k] / 1024); } } } }); msg.image.forEach((item) => { if (item.png_detail && item.png_detail.length) { item.png_detail = item.png_detail.map((png) => { png.ratio = Math.floor(png.ratio * 100) / 100; // png.filesize = Math.floor(png.filesize / 1024); // png.uncompressed_size = Math.floor(png.uncompressed_size / 1024); return png; }); } }); // 处理appList // 一、统计每个页面有多少个资源文件;二、累加每个资源文件的size;三、以表格形式展示统计信息 if (msg.appList && msg.appList.length) { this.appList = msg.appList.map(item => { item.fileCount = item.files.length item.fileSize = item.files.reduce((total, file) => { return total + file.size; }, 0) item.fileSize = item.fileSize / 1024 return item }) } }, onSelectChange(res) { this.device = res; // 清空图表数据 wsNotify.eventBus.$emit("clear-evm-chart"); wsNotify.eventBus.$emit("clear-lvgl-chart"); wsNotify.eventBus.$emit("clear-system-chart"); // 清空各个表格数据 this.imageList = []; this.pngList = []; // this.processData(this.devices[this.device]); // this.resetData(); }, resetData(m) { wsNotify.eventBus.$emit("resize"); this.evmList = [{ ...m.evm }]; this.lvglList = [{ ...m.lvgl }]; this.systemList = [ { imei: m.imei, ...m.system, }, ]; // 这里需要特殊处理下,先判断uri是否存在,不存在则添加,存在则更新 let uris = []; this.imageList.forEach((item) => { item.highlight = false; uris.push(item.uri); }); m.image && m.image.forEach((item, index) => { if (m.image.length - 1 === index) { item.highlight = true; } else { item.highlight = false; } const target = this.imageList.find((img) => img.uri === item.uri); if (target) { target.length = item.length; target.png_total_count = item.png_total_count; target.highlight = item.highlight; if (item.png_uncompressed_size) { // target.highlight = true; if (item.png_uncompressed_size !== target.png_uncompressed_size) { target.png_uncompressed_size = item.png_uncompressed_size; if (this.pngShowMode) this.pngList = item.png_detail || []; } } if ( item.png_file_size && item.png_file_size !== target.png_file_size ) target.png_file_size = item.png_file_size; } else { this.imageList.push(item); } }); if (m) { if (m.evm) this.evm = m.evm; if (m.lvgl) this.lvgl = m.lvgl; if (m.image) this.image = m.image; if (m.system) this.system = m.system; } }, }, mounted() {}, destroyed() { // 页面关闭则销毁该数据库 monitor.deleteDB(); // monitor.clearData(); // monitor.close(); }, created() { monitor .init() .then((res) => { console.log(res); if (monitor.db) { monitor.set({ system: { free_size: 0, free_space_size: 0, used_space_size: 0, }, }); } }) .catch((err) => { console.error(err); }); this.socket = wsNotify; wsNotify.eventBus.$on("exported", (res) => { if (res.type === "evm") result.evmImg = res.data; else if (res.type === "lvgl") result.lvglImg = res.data; else if (res.type === "system") result.systemImg = res.data; }); wsNotify.eventBus.$on("open", (message) => { this.sendMsg(); this.$nextTick(() => { console.log(message); }); // 这里启动一个定时器,10秒钟后,如果没有消息进来,说明当前没有在线设备 }); wsNotify.eventBus.$on("close", (message) => { this.$nextTick(() => { console.log(message); }); }); wsNotify.eventBus.$on("message", (message) => { this.$nextTick(() => { this.handleMessage(deepClone(message)); }); }); }, }; </script> <style lang="scss" scoped> @import "./style.css"; .app-container { & > div.page-wrapper { margin: 10px 0px; } } .vue-grid-layout { background: none; } .vue-grid-item:not(.vue-grid-placeholder) { background: #fff; border: 0px solid #eee; } .vue-grid-item .resizing { opacity: 0.9; } .vue-grid-item .static { background: #cce; } .vue-grid-item .text { font-size: 24px; text-align: center; position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; height: 100%; width: 100%; } .vue-grid-item .item-title { margin-left: 15px; } .vue-grid-item .no-drag { height: 100%; width: 100%; } .vue-grid-item .minMax { font-size: 12px; } .vue-grid-item .add { cursor: pointer; } .vue-draggable-handle { position: absolute; width: 20px; height: 20px; top: 0; left: 0; background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><circle cx='5' cy='5' r='5' fill='#999999'/></svg>") no-repeat; background-position: bottom right; padding: 0 8px 8px 0; background-repeat: no-repeat; background-origin: content-box; box-sizing: border-box; cursor: pointer; } </style>