Browse Source

添加 echarts 处理监控时的图表

reghao 1 month ago
parent
commit
809f656db4
5 changed files with 500 additions and 241 deletions
  1. 1 0
      package.json
  2. 185 0
      src/components/ChartItem.vue
  3. 2 2
      src/main.js
  4. 44 239
      src/views/admin/Dashboard.vue
  5. 268 0
      src/views/admin/Dashboard1.vue

+ 1 - 0
package.json

@@ -13,6 +13,7 @@
     "core-js": "^3.6.4",
     "crypto-js": "^4.1.1",
     "element-ui": "^2.15.14",
+    "echarts": "^5.6.0",
     "js-cookie": "2.2.0",
     "jsencrypt": "^3.2.1",
     "nprogress": "^0.2.0",

+ 185 - 0
src/components/ChartItem.vue

@@ -0,0 +1,185 @@
+<template>
+  <div class="chart-wrapper">
+    <div ref="chartDom" class="chart-container" />
+
+    <el-empty
+      v-if="isEmpty"
+      :description="`${title} 暂无数据`"
+      :image-size="60"
+    />
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+
+export default {
+  name: 'ChartItem',
+  props: {
+    title: { type: String, default: '' },
+    unit: { type: String, default: '' },
+    timeLabels: { type: Array, default: () => [] },
+    // dataGroup 格式: { today: { containerA: [...] }, yesterday: { containerA: [...] } }
+    dataGroup: { type: Object, default: () => ({ today: {}, yesterday: {}}) }
+  },
+  data() {
+    return {
+      chartInstance: null,
+      // 调色盘:确保同一容器名在“今日”和“昨日”使用同一种色相
+      colors: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4']
+    }
+  },
+  computed: {
+    isEmpty() {
+      return Object.keys(this.dataGroup.today || {}).length === 0 &&
+        Object.keys(this.dataGroup.yesterday || {}).length === 0
+    }
+  },
+  watch: {
+    // 如果数据是异步获取的,监听变化并重绘
+    dataGroup: {
+      deep: true,
+      handler() {
+        this.renderChart()
+      }
+    }
+  },
+  mounted() {
+    this.initChart()
+    window.addEventListener('resize', this.handleResize)
+  },
+  beforeDestroy() {
+    // 销毁实例,防止内存泄漏
+    window.removeEventListener('resize', this.handleResize)
+    if (this.chartInstance) {
+      this.chartInstance.dispose()
+      this.chartInstance = null
+    }
+  },
+  methods: {
+    initChart() {
+      if (this.isEmpty) return
+      this.chartInstance = echarts.init(this.$refs.chartDom)
+      this.renderChart()
+    },
+
+    renderChart() {
+      if (!this.chartInstance) return
+
+      const series = []
+      const todayData = this.dataGroup.today || {}
+      const yesterdayData = this.dataGroup.yesterday || {}
+
+      // 1. 获取所有出现的容器名称并去重,用于分配颜色
+      const allContainerNames = Array.from(new Set([
+        ...Object.keys(todayData),
+        ...Object.keys(yesterdayData)
+      ]))
+
+      // 2. 构造 Series
+      allContainerNames.forEach((name, index) => {
+        const baseColor = this.colors[index % this.colors.length]
+
+        // 今日数据:实线,加宽
+        if (todayData[name]) {
+          series.push({
+            name: `${name} (今日)`,
+            type: 'line',
+            smooth: true,
+            showSymbol: false,
+            lineStyle: { width: 2.5 },
+            itemStyle: { color: baseColor },
+            data: todayData[name]
+          })
+        }
+
+        // 昨日数据:虚线,细线,低透明度
+        if (yesterdayData[name]) {
+          series.push({
+            name: `${name} (昨日)`,
+            type: 'line',
+            smooth: true,
+            showSymbol: false,
+            lineStyle: {
+              type: 'dashed',
+              width: 1.5,
+              opacity: 0.5
+            },
+            itemStyle: { color: baseColor }, // 同名容器颜色保持一致
+            data: yesterdayData[name]
+          })
+        }
+      })
+
+      const option = {
+        title: {
+          text: this.title,
+          left: 'center',
+          textStyle: { fontSize: 14, color: '#606266' }
+        },
+        tooltip: {
+          trigger: 'axis',
+          backgroundColor: 'rgba(255, 255, 255, 0.9)',
+          confine: true, // 解决 Tooltip 超出容器被遮挡问题
+          axisPointer: { type: 'cross' }
+        },
+        legend: {
+          bottom: 0,
+          type: 'scroll',
+          textStyle: { fontSize: 11 }
+        },
+        grid: {
+          top: 40,
+          left: '3%',
+          right: '4%',
+          bottom: '15%',
+          containLabel: true
+        },
+        xAxis: {
+          type: 'category',
+          boundaryGap: false,
+          data: this.timeLabels,
+          axisLabel: { color: '#999' }
+        },
+        yAxis: {
+          type: 'value',
+          name: this.unit,
+          splitLine: { lineStyle: { type: 'dashed' }}
+        },
+        series: series
+      }
+
+      this.chartInstance.setOption(option, true) // true 表示不合并旧数据
+    },
+
+    handleResize() {
+      if (this.chartInstance) {
+        this.chartInstance.resize()
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.chart-wrapper {
+  width: 100%;
+  height: 400px;
+  position: relative;
+  background: #fff;
+  padding: 10px;
+  box-sizing: border-box;
+}
+.chart-container {
+  width: 100%;
+  height: 100%;
+}
+/* 覆盖 ElementUI Empty 居中样式 */
+.el-empty {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  padding: 0;
+}
+</style>

+ 2 - 2
src/main.js

@@ -12,8 +12,8 @@ import 'element-ui/lib/theme-chalk/index.css'
 import 'element-ui/lib/theme-chalk/display.css'
 Vue.use(ElementUI)
 
-/* import * as echarts from 'echarts'
-Vue.prototype.$echarts = echarts*/
+import * as echarts from 'echarts'
+Vue.prototype.$echarts = echarts
 
 import '@/permission'
 

+ 44 - 239
src/views/admin/Dashboard.vue

@@ -1,268 +1,73 @@
 <template>
-  <div class="dashboard-wrapper">
-    <el-row :gutter="20" class="stat-row">
-      <el-col :span="24">
-        <el-card shadow="never" class="role-card">
-          <div class="role-container">
-            <span class="role-label"><i class="el-icon-medal"></i> 当前权限角色:</span>
-            <el-tag
-              v-for="(item, index) in roles"
-              :key="index"
-              effect="dark"
-              size="medium"
-              class="role-tag"
-              @click="goToRole(item)"
-            >
-              {{ item }}
-            </el-tag>
-          </div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <el-row :gutter="20" class="process-row">
-      <el-col :span="24">
-        <el-card shadow="hover">
-          <div slot="header" class="card-header">
-            <span><i class="el-icon-refresh"></i> CI/CD 流水线</span>
-          </div>
-          <div class="steps-wrapper">
-            <el-steps :active="1" finish-status="success" align-center>
-              <el-step title="更新代码" icon="el-icon-edit-outline" />
-              <el-step title="编译代码" icon="el-icon-set-up" />
-              <el-step title="应用打包" icon="el-icon-box" />
-              <el-step title="推送应用" icon="el-icon-upload" />
-              <el-step title="拉取应用" icon="el-icon-download" />
-              <el-step title="部署应用" icon="el-icon-monitor" />
-            </el-steps>
-          </div>
-        </el-card>
-      </el-col>
-    </el-row>
-
-    <el-row :gutter="20">
-      <el-col :md="12" :sm="24">
-        <el-card shadow="hover" class="data-card">
-          <div slot="header" class="card-header">
-            <span><i class="el-icon-cpu"></i> 机器节点状态</span>
-          </div>
-          <el-table :data="machineStatList" border stripe size="small">
-            <el-table-column prop="env" label="运行环境">
-              <template slot-scope="scope">
-                <el-tag size="mini" effect="plain">{{ scope.row.env }}</el-tag>
-              </template>
-            </el-table-column>
-            <el-table-column prop="total" label="资产总数" align="center" />
-            <el-table-column label="健康状况" align="center">
-              <template slot-scope="scope">
-                <div class="status-cell">
-                  <span class="status-dot online"></span>
-                  <span class="status-count">{{ scope.row.onlineCount }}</span>
-                  <span class="status-divider">/</span>
-                  <span class="status-dot offline"></span>
-                  <span class="status-count">{{ scope.row.offlineCount }}</span>
-                </div>
-              </template>
-            </el-table-column>
-          </el-table>
-        </el-card>
-      </el-col>
-
-      <el-col :md="12" :sm="24">
-        <el-card shadow="hover" class="data-card">
-          <div slot="header" class="card-header">
-            <span><i class="el-icon-info"></i> 系统核心信息</span>
-          </div>
-          <el-descriptions v-if="sysInfo !== null" :column="1" border size="small">
-            <el-descriptions-item label="应用版本">
-              <el-link
-                type="primary"
-                target="_blank"
-                :href="`https://git.reghao.cn/reghao/devops/commit/${sysInfo.commitId}`"
-                icon="el-icon-link"
-              >
-                {{ sysInfo.commitId.substring(0, 8) }}
-              </el-link>
-            </el-descriptions-item>
-            <el-descriptions-item label="机器地址">
-              <code>{{ sysInfo.ipv4 }}</code>
-            </el-descriptions-item>
-            <el-descriptions-item label="操作系统">
-              <i class="el-icon-monitor"></i> {{ sysInfo.osInfo }}
-            </el-descriptions-item>
-            <el-descriptions-item label="JVM 环境">
-              <el-tooltip effect="dark" :content="sysInfo.jvmInfo" placement="top">
-                <span class="text-truncate">{{ sysInfo.jvmInfo }}</span>
-              </el-tooltip>
-            </el-descriptions-item>
-            <el-descriptions-item label="启动/PID">
-              <el-tag size="mini" type="success">{{ sysInfo.startAt }}</el-tag>
-              <el-tag size="mini" type="warning" style="margin-left:5px">PID: {{ sysInfo.pid }}</el-tag>
-            </el-descriptions-item>
-          </el-descriptions>
-        </el-card>
-      </el-col>
-    </el-row>
+  <div class="report-container">
+    <el-card class="header-card">
+      <h1>📊 容器资源同比巡检报告</h1>
+      <p>实线: 今日 | 虚线: 昨日</p>
+    </el-card>
+
+    <div v-for="item in instances" :key="item.name" class="node-card">
+      <el-card shadow="hover">
+        <div slot="header" class="node-title">
+          <span>节点实例: {{ item.name }}</span>
+        </div>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <chart-item
+              title="CPU 同比 (%)"
+              :time-labels="timeLabels"
+              :data-group="item.cpu"
+              unit="%"
+            />
+          </el-col>
+          <el-col :span="12">
+            <chart-item
+              title="内存同比 (MB)"
+              :time-labels="timeLabels"
+              :data-group="item.mem"
+              unit="MB"
+            />
+          </el-col>
+        </el-row>
+      </el-card>
+    </div>
   </div>
 </template>
 
 <script>
+import ChartItem from '@/components/ChartItem.vue'
 import { getDashboard } from '@/api/devops'
-import { getAuthedUser } from '@/utils/auth'
 
 export default {
-  name: 'Dashboard',
+  components: { ChartItem },
   data() {
     return {
-      roles: [],
-      machineStatList: [],
-      sysInfo: null
+      timeLabels: [], // 后端返回
+      instances: [] // 后端返回
     }
   },
-  created() {
-    const { roles } = getAuthedUser()
-    this.roles = roles
-    document.title = 'Dashboard'
-    this.getData()
+  mounted() {
+    this.fetchData()
   },
   methods: {
-    getData() {
-      this.getDevopsDashboard()
-    },
-    getDevopsDashboard() {
+    fetchData() {
       getDashboard().then(resp => {
         if (resp.code === 0) {
-          this.sysInfo = resp.data.sysInfo
-          this.machineStatList = resp.data.machineStatList
+          this.timeLabels = resp.data.timeLabels
+          this.instances = resp.data.instances
         } else {
           this.$message.error(resp.msg)
         }
       }).catch(error => {
         this.$message.error(error.message)
       })
-    },
-    goToRole(data) {
-      this.$message.info('当前操作角色: ' + data)
-    },
-    // 其余逻辑保持不变...
-    goToDisk() { const path = '/disk'; this.handleRoute(path) },
-    goToVod() { const path = '/vod'; this.handleRoute(path) },
-    goToBlog() { const path = '/blog'; this.handleRoute(path) },
-    handleRoute(path) {
-      if (this.$route.path === path) { this.$router.go(0); return }
-      this.$router.push(path)
     }
   }
 }
 </script>
 
 <style scoped>
-.dashboard-wrapper {
-  padding: 10px;
-}
-
-.stat-row {
-  margin-bottom: 20px;
-}
-
-.role-card {
-  background: #fcfcfc;
-  border-left: 4px solid #409EFF;
-}
-
-.role-container {
-  display: flex;
-  align-items: center;
-}
-
-.role-label {
-  font-weight: bold;
-  color: #606266;
-  margin-right: 15px;
-}
-
-.role-tag {
-  margin-right: 10px;
-  cursor: pointer;
-  transition: all 0.3s;
-}
-
-.role-tag:hover {
-  opacity: 0.8;
-  transform: translateY(-1px);
-}
-
-.card-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  font-weight: bold;
-}
-
-.process-row {
-  margin-bottom: 20px;
-}
-
-.steps-wrapper {
-  padding: 20px 0;
-}
-
-.data-card {
-  min-height: 380px;
-}
-
-/* 状态点样式 */
-.status-cell {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.status-dot {
-  width: 8px;
-  height: 8px;
-  border-radius: 50%;
-  margin: 0 5px;
-}
-
-.online {
-  background-color: #67C23A;
-  box-shadow: 0 0 5px #67C23A;
-}
-
-.offline {
-  background-color: #F56C6C;
-  box-shadow: 0 0 5px #F56C6C;
-}
-
-.status-count {
-  font-weight: bold;
-}
-
-.status-divider {
-  margin: 0 8px;
-  color: #DCDFE6;
-}
-
-.text-truncate {
-  display: inline-block;
-  max-width: 250px;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-  vertical-align: middle;
-}
-
-code {
-  background-color: #f5f7fa;
-  padding: 2px 4px;
-  border-radius: 4px;
-  color: #e6a23c;
-  font-family: monospace;
-}
-
-::v-deep .el-descriptions-item__label {
-  width: 120px;
-  background-color: #fafafa !important;
-}
+.report-container { padding: 20px; background: #f4f7f9; }
+.node-card { margin-bottom: 25px; }
+.node-title { font-weight: bold; color: #1890ff; }
 </style>

+ 268 - 0
src/views/admin/Dashboard1.vue

@@ -0,0 +1,268 @@
+<template>
+  <div class="dashboard-wrapper">
+    <el-row :gutter="20" class="stat-row">
+      <el-col :span="24">
+        <el-card shadow="never" class="role-card">
+          <div class="role-container">
+            <span class="role-label"><i class="el-icon-medal"></i> 当前权限角色:</span>
+            <el-tag
+              v-for="(item, index) in roles"
+              :key="index"
+              effect="dark"
+              size="medium"
+              class="role-tag"
+              @click="goToRole(item)"
+            >
+              {{ item }}
+            </el-tag>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="20" class="process-row">
+      <el-col :span="24">
+        <el-card shadow="hover">
+          <div slot="header" class="card-header">
+            <span><i class="el-icon-refresh"></i> CI/CD 流水线</span>
+          </div>
+          <div class="steps-wrapper">
+            <el-steps :active="1" finish-status="success" align-center>
+              <el-step title="更新代码" icon="el-icon-edit-outline" />
+              <el-step title="编译代码" icon="el-icon-set-up" />
+              <el-step title="应用打包" icon="el-icon-box" />
+              <el-step title="推送应用" icon="el-icon-upload" />
+              <el-step title="拉取应用" icon="el-icon-download" />
+              <el-step title="部署应用" icon="el-icon-monitor" />
+            </el-steps>
+          </div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <el-row :gutter="20">
+      <el-col :md="12" :sm="24">
+        <el-card shadow="hover" class="data-card">
+          <div slot="header" class="card-header">
+            <span><i class="el-icon-cpu"></i> 机器节点状态</span>
+          </div>
+          <el-table :data="machineStatList" border stripe size="small">
+            <el-table-column prop="env" label="运行环境">
+              <template slot-scope="scope">
+                <el-tag size="mini" effect="plain">{{ scope.row.env }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="total" label="资产总数" align="center" />
+            <el-table-column label="健康状况" align="center">
+              <template slot-scope="scope">
+                <div class="status-cell">
+                  <span class="status-dot online"></span>
+                  <span class="status-count">{{ scope.row.onlineCount }}</span>
+                  <span class="status-divider">/</span>
+                  <span class="status-dot offline"></span>
+                  <span class="status-count">{{ scope.row.offlineCount }}</span>
+                </div>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-card>
+      </el-col>
+
+      <el-col :md="12" :sm="24">
+        <el-card shadow="hover" class="data-card">
+          <div slot="header" class="card-header">
+            <span><i class="el-icon-info"></i> 系统核心信息</span>
+          </div>
+          <el-descriptions v-if="sysInfo !== null" :column="1" border size="small">
+            <el-descriptions-item label="应用版本">
+              <el-link
+                type="primary"
+                target="_blank"
+                :href="`https://git.reghao.cn/reghao/devops/commit/${sysInfo.commitId}`"
+                icon="el-icon-link"
+              >
+                {{ sysInfo.commitId.substring(0, 8) }}
+              </el-link>
+            </el-descriptions-item>
+            <el-descriptions-item label="机器地址">
+              <code>{{ sysInfo.ipv4 }}</code>
+            </el-descriptions-item>
+            <el-descriptions-item label="操作系统">
+              <i class="el-icon-monitor"></i> {{ sysInfo.osInfo }}
+            </el-descriptions-item>
+            <el-descriptions-item label="JVM 环境">
+              <el-tooltip effect="dark" :content="sysInfo.jvmInfo" placement="top">
+                <span class="text-truncate">{{ sysInfo.jvmInfo }}</span>
+              </el-tooltip>
+            </el-descriptions-item>
+            <el-descriptions-item label="启动/PID">
+              <el-tag size="mini" type="success">{{ sysInfo.startAt }}</el-tag>
+              <el-tag size="mini" type="warning" style="margin-left:5px">PID: {{ sysInfo.pid }}</el-tag>
+            </el-descriptions-item>
+          </el-descriptions>
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import { getDashboard } from '@/api/devops'
+import { getAuthedUser } from '@/utils/auth'
+
+export default {
+  name: 'Dashboard',
+  data() {
+    return {
+      roles: [],
+      machineStatList: [],
+      sysInfo: null
+    }
+  },
+  created() {
+    const { roles } = getAuthedUser()
+    this.roles = roles
+    document.title = 'Dashboard'
+    this.getData()
+  },
+  methods: {
+    getData() {
+      this.getDevopsDashboard()
+    },
+    getDevopsDashboard() {
+      getDashboard().then(resp => {
+        if (resp.code === 0) {
+          this.sysInfo = resp.data.sysInfo
+          this.machineStatList = resp.data.machineStatList
+        } else {
+          this.$message.error(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
+    goToRole(data) {
+      this.$message.info('当前操作角色: ' + data)
+    },
+    // 其余逻辑保持不变...
+    goToDisk() { const path = '/disk'; this.handleRoute(path) },
+    goToVod() { const path = '/vod'; this.handleRoute(path) },
+    goToBlog() { const path = '/blog'; this.handleRoute(path) },
+    handleRoute(path) {
+      if (this.$route.path === path) { this.$router.go(0); return }
+      this.$router.push(path)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.dashboard-wrapper {
+  padding: 10px;
+}
+
+.stat-row {
+  margin-bottom: 20px;
+}
+
+.role-card {
+  background: #fcfcfc;
+  border-left: 4px solid #409EFF;
+}
+
+.role-container {
+  display: flex;
+  align-items: center;
+}
+
+.role-label {
+  font-weight: bold;
+  color: #606266;
+  margin-right: 15px;
+}
+
+.role-tag {
+  margin-right: 10px;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+
+.role-tag:hover {
+  opacity: 0.8;
+  transform: translateY(-1px);
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-weight: bold;
+}
+
+.process-row {
+  margin-bottom: 20px;
+}
+
+.steps-wrapper {
+  padding: 20px 0;
+}
+
+.data-card {
+  min-height: 380px;
+}
+
+/* 状态点样式 */
+.status-cell {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.status-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  margin: 0 5px;
+}
+
+.online {
+  background-color: #67C23A;
+  box-shadow: 0 0 5px #67C23A;
+}
+
+.offline {
+  background-color: #F56C6C;
+  box-shadow: 0 0 5px #F56C6C;
+}
+
+.status-count {
+  font-weight: bold;
+}
+
+.status-divider {
+  margin: 0 8px;
+  color: #DCDFE6;
+}
+
+.text-truncate {
+  display: inline-block;
+  max-width: 250px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  vertical-align: middle;
+}
+
+code {
+  background-color: #f5f7fa;
+  padding: 2px 4px;
+  border-radius: 4px;
+  color: #e6a23c;
+  font-family: monospace;
+}
+
+::v-deep .el-descriptions-item__label {
+  width: 120px;
+  background-color: #fafafa !important;
+}
+</style>