Browse Source

Dashboard.vue 添加告警指标

reghao 1 month ago
parent
commit
488d290082

+ 8 - 0
src/api/devops.js

@@ -73,6 +73,10 @@ export function getMachineUsedList(machineId) {
   return get(devopsApi.getMachineList + '/app?machineId=' + machineId)
 }
 
+export function getMachineMon(machineId) {
+  return get(devopsApi.getMachineList + '/mon?machineId=' + machineId)
+}
+
 export function getAliyunOss() {
   return get(devopsApi.getMachineList + '/oss/list')
 }
@@ -390,6 +394,10 @@ export function getAppStatDetail(appId) {
   return get(devopsApi.getAppStatList + '/detail?appId=' + appId)
 }
 
+export function getAppMonData(appId) {
+  return get(devopsApi.getAppStatList + '/mon?appId=' + appId)
+}
+
 export function restartAppStat(formData) {
   return postForm(devopsApi.getAppStatList + '/restart', formData)
 }

+ 169 - 0
src/components/CpuChart.vue

@@ -0,0 +1,169 @@
+<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: 'CpuThrottleChart',
+  props: {
+    title: { type: String, default: 'CPU 性能与限流分析' },
+    timeLabels: { type: Array, default: () => [] },
+    // cpuGroup: { today: {...}, yesterday: {...} }
+    cpuGroup: { type: Object, default: () => ({ today: {}, yesterday: {} }) },
+    // throttleGroup: { today: {...}, yesterday: {} }
+    throttleGroup: { type: Object, default: () => ({ today: {}, yesterday: {} }) }
+  },
+  data() {
+    return {
+      chartInstance: null,
+      colors: ['#5470c6', '#91cc75', '#fac858', '#73c0de', '#3ba272', '#9a60b4']
+    }
+  },
+  computed: {
+    isEmpty() {
+      return Object.keys(this.cpuGroup.today || {}).length === 0;
+    }
+  },
+  watch: {
+    cpuGroup: { deep: true, handler() { this.renderChart() } },
+    throttleGroup: { 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()
+    }
+  },
+  methods: {
+    initChart() {
+      if (this.isEmpty) return
+      this.chartInstance = echarts.init(this.$refs.chartDom)
+      this.renderChart()
+    },
+
+    renderChart() {
+      if (!this.chartInstance) return
+
+      const series = []
+      const cpuToday = this.cpuGroup.today || {}
+      const cpuYesterday = this.cpuGroup.yesterday || {}
+      const throttleToday = this.throttleGroup.today || {}
+
+      const allContainers = Array.from(new Set([
+        ...Object.keys(cpuToday),
+        ...Object.keys(throttleToday)
+      ]))
+
+      allContainers.forEach((name, index) => {
+        const baseColor = this.colors[index % this.colors.length]
+
+        // 1. CPU 今日使用率 (左轴 - Index 0)
+        if (cpuToday[name]) {
+          series.push({
+            name: `${name} 使用率(今)`,
+            type: 'line',
+            yAxisIndex: 0,
+            smooth: true,
+            showSymbol: false,
+            lineStyle: { width: 2 },
+            itemStyle: { color: baseColor },
+            data: cpuToday[name]
+          })
+        }
+
+        // 2. CPU 昨日使用率 (左轴 - Index 0)
+        if (cpuYesterday[name]) {
+          series.push({
+            name: `${name} 使用率(昨)`,
+            type: 'line',
+            yAxisIndex: 0,
+            smooth: true,
+            showSymbol: false,
+            lineStyle: { type: 'dashed', width: 1, opacity: 0.4 },
+            itemStyle: { color: baseColor },
+            data: cpuYesterday[name]
+          })
+        }
+
+        // 3. CPU 限流次数 (右轴 - Index 1)
+        // 使用柱状图更能体现“瞬时爆发”的感觉,且不会与折线混淆
+        if (throttleToday[name]) {
+          series.push({
+            name: `${name} 限流次数(今)`,
+            type: 'bar',
+            yAxisIndex: 1,
+            itemStyle: {
+              color: 'rgba(255, 77, 79, 0.35)', // 统一使用半透明红色作为限流警示
+              borderRadius: [2, 2, 0, 0]
+            },
+            barMaxWidth: 20,
+            data: throttleToday[name],
+            // 在右轴 100 处增加告警标识线
+            markLine: {
+              symbol: 'none',
+              label: { position: 'end', formatter: '告警线(100)' },
+              lineStyle: { color: '#ff4d4f', type: 'dotted', width: 1 },
+              data: [{ yAxis: 100 }]
+            }
+          })
+        }
+      })
+
+      const option = {
+        title: { text: this.title, left: 'center' },
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { type: 'shadow' }, // 配合柱状图使用 shadow 效果更好
+          confine: true
+        },
+        legend: { bottom: 0, type: 'scroll' },
+        grid: { top: 60, left: '3%', right: '4%', bottom: '15%', containLabel: true },
+        xAxis: {
+          type: 'category',
+          boundaryGap: true, // 柱状图建议开启 gap
+          data: this.timeLabels
+        },
+        yAxis: [
+          {
+            type: 'value',
+            name: '使用率',
+            position: 'left',
+            axisLabel: { formatter: '{value} %' },
+            splitLine: { show: true, lineStyle: { type: 'dashed' } }
+          },
+          {
+            type: 'value',
+            name: '限流次数',
+            position: 'right',
+            // 限流次数通常很大,且我们关心是否 > 100,右轴不需要显示背景线
+            splitLine: { show: false },
+            axisLabel: { color: '#ff4d4f' },
+            nameTextStyle: { color: '#ff4d4f' }
+          }
+        ],
+        series: series
+      }
+
+      this.chartInstance.setOption(option, true)
+    },
+
+    handleResize() {
+      this.chartInstance && this.chartInstance.resize()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.chart-wrapper { width: 100%; height: 450px; background: #fff; padding: 15px; }
+.chart-container { width: 100%; height: 100%; }
+</style>

+ 387 - 47
src/views/admin/Dashboard.vue

@@ -1,73 +1,413 @@
 <template>
-  <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 class="monitor-report-container" v-loading="loading">
+    <div class="status-banner" :class="'banner-' + overallStatus">
+      <div class="banner-left">
+        <div class="status-visual">
+          <div class="status-icon-wrapper">
+            <i :class="overallStatus === 'success' ? 'el-icon-circle-check' : 'el-icon-warning-outline'"></i>
+          </div>
+          <div class="pulse-ring"></div>
         </div>
+        <div class="banner-content">
+          <div class="banner-title">
+            {{ reportData.statusSummary || '核心基础设施巡检' }}
+            <el-tag size="mini" :type="overallStatus === 'success' ? 'success' : 'warning'" effect="plain">
+              {{ overallStatus === 'success' ? 'HEALTHY' : 'ACTION REQUIRED' }}
+            </el-tag>
+          </div>
+          <div class="banner-desc">
+            <span>{{ overallDescription }}</span>
+            <span class="update-time"><i class="el-icon-time"></i> 最后更新: {{ reportData.lastUpdateTime || '-' }}</span>
+          </div>
+        </div>
+      </div>
+
+      <div class="banner-stats">
+        <div class="stat-item">
+          <div class="stat-label">巡检范围</div>
+          <div class="stat-value">{{ reportData.totalNodes || 0 }} <small>Nodes</small></div>
+        </div>
+        <div class="stat-divider"></div>
+        <div class="stat-item">
+          <div class="stat-label">检测耗时</div>
+          <div class="stat-value">{{ reportData.checkDuration || '0ms' }}</div>
+        </div>
+        <div class="stat-divider" v-if="hasAnyIssue"></div>
+        <div class="stat-item" v-if="hasAnyIssue">
+          <div class="stat-label">风险指数</div>
+          <div class="stat-value risk-text">{{ reportData.riskLevel || 'High' }}</div>
+        </div>
+      </div>
+
+      <div class="banner-right">
+        <el-button
+          type="primary"
+          size="medium"
+          icon="el-icon-refresh"
+          @click="loadReport"
+          :loading="loading"
+          class="refresh-glow"
+        >
+          重新巡检
+        </el-button>
+      </div>
+    </div>
+
+    <div class="report-grid">
+      <el-row :gutter="20" class="force-row">
 
-        <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>
+        <el-col :span="12" v-for="config in cardConfigs" :key="config.key" class="mb-20">
+          <el-card shadow="hover" class="nice-card" :class="getShadowClass(config)">
+            <div slot="header" class="card-header">
+              <div class="title-wrapper">
+                <i :class="[config.icon, 'icon-accent', config.textColor]"></i>
+                <span class="title-text">{{ config.title }} <small>{{ config.subTitle }}</small></span>
+              </div>
+              <div class="header-ops">
+                <el-tooltip content="排查建议" placement="top">
+                  <el-button
+                    type="text"
+                    icon="el-icon-question"
+                    class="advice-btn"
+                    @click="showAdvice(config)"
+                  ></el-button>
+                </el-tooltip>
+                <el-tag v-if="getData(config.key).length > 0" :type="config.tagType" size="mini" effect="dark" class="status-tag">
+                  {{ getData(config.key).length }}
+                </el-tag>
+              </div>
+            </div>
+
+            <div class="card-body scroll-style">
+              <template v-if="getData(config.key).length > 0">
+                <div v-for="(item, index) in getData(config.key)" :key="index" class="progress-item">
+                  <div class="info">
+                    <span class="name text-ellipsis">{{ item.instance || item.name }}</span>
+                    <span class="val" :class="config.textColor">{{ item.value }}{{ config.unit }}</span>
+                  </div>
+                  <el-progress
+                    :percentage="config.isRatio ? (item.value > 100 ? 100 : item.value) : 100"
+                    :show-text="false"
+                    :color="config.progColor"
+                    :stroke-width="6"
+                  />
+                </div>
+              </template>
+              <div v-else class="status-placeholder">
+                <i class="el-icon-circle-check"></i><p>{{ config.emptyText }}</p>
+              </div>
+            </div>
+          </el-card>
+        </el-col>
+      </el-row>
     </div>
+
+    <el-dialog
+      :title="'💡 排查建议: ' + currentAdvice.title"
+      :visible.sync="dialogVisible"
+      width="500px"
+      custom-class="advice-dialog"
+      append-to-body
+    >
+      <div class="advice-content">
+        <div v-if="currentAdvice.text" class="markdown-body">
+          <i class="el-icon-guideAdvice"></i>
+          <p v-html="formatAdvice(currentAdvice.text)"></p>
+        </div>
+        <el-empty v-else description="暂无排查建议" :image-size="60"></el-empty>
+      </div>
+      <span slot="footer" class="dialog-footer">
+        <el-button type="primary" size="small" @click="dialogVisible = false">知道了</el-button>
+      </span>
+    </el-dialog>
   </div>
 </template>
 
 <script>
-import ChartItem from '@/components/ChartItem.vue'
 import { getDashboard } from '@/api/devops'
 
 export default {
-  components: { ChartItem },
   data() {
     return {
-      timeLabels: [], // 后端返回
-      instances: [] // 后端返回
+      loading: false,
+      dialogVisible: false,
+      currentAdvice: { title: '', text: '' },
+      reportData: {
+        cpuThrottled: [], advice_cpuThrottled: '',
+        memRisk: [], advice_memRisk: '',
+        diskIo: [], advice_diskIo: '',
+        diskUsageRisk: [], advice_diskUsageRisk: '',
+        inodeRisk: [], advice_inodeRisk: '',
+        fdRisk: [], advice_fdRisk: '',
+        netEstMax: [], advice_netEstMax: '',
+        netTwMax: [], advice_netTwMax: '',
+        netOverflows: [], advice_netOverflows: '',
+        netDrops: [], advice_netDrops: '',
+        advices: {}
+      },
+      // 使用配置化方式渲染卡片,减少 HTML 冗余
+      cardConfigs: [
+        { key: 'cpuThrottled', title: 'CPU 性能节流', subTitle: '24H Throttled', icon: 'el-icon-cpu', textColor: 'warning-text', tagType: 'warning', progColor: '#e6a23c', unit: 's', isRatio: false, emptyText: '无节流限制' },
+        { key: 'memRisk', title: '内存水位线', subTitle: '> 85% Usage', icon: 'el-icon-box', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '%', isRatio: true, emptyText: '水位正常' },
+        { key: 'diskIo', title: '磁盘 I/O 响应', subTitle: 'Wait > 50ms', icon: 'el-icon-receiving', textColor: 'warning-text', tagType: 'warning', progColor: '#e6a23c', unit: 'ms', isRatio: false, emptyText: 'IO 响应极快' },
+        { key: 'diskUsageRisk', title: '根分区空间', subTitle: '( / ) > 85%', icon: 'el-icon-pie-chart', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '%', isRatio: true, emptyText: '磁盘空间充沛' },
+        { key: 'inodeRisk', title: 'inode 使用率', subTitle: '> 80% Usage', icon: 'el-icon-files', textColor: 'warning-text', tagType: 'warning', progColor: '#e6a23c', unit: '%', isRatio: true, emptyText: '索引节点充足' },
+        { key: 'fdRisk', title: '文件句柄 (FD)', subTitle: 'Usage Ratio', icon: 'el-icon-link', textColor: 'primary-text', tagType: 'primary', progColor: '#409eff', unit: '%', isRatio: true, emptyText: 'FD 占用正常' },
+        { key: 'netEstMax', title: 'TCP EST 状态数量', subTitle: 'TCP EST', icon: 'el-icon-warning-outline', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '次', isRatio: false, emptyText: '未超过阈值' },
+        { key: 'netTwMax', title: 'TCP TIME_WAIT 状态', subTitle: 'TCP TIME_WAIT', icon: 'el-icon-warning-outline', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '次', isRatio: false, emptyText: '未超过阈值' },
+        { key: 'netOverflows', title: 'TCP 全连接队列溢出', subTitle: 'Listen Overflow', icon: 'el-icon-warning-outline', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '次', isRatio: false, emptyText: '无队列溢出' },
+        { key: 'netDrops', title: 'TCP 数据包丢弃', subTitle: 'TCP Drops', icon: 'el-icon-circle-close', textColor: 'danger-text', tagType: 'danger', progColor: '#f56c6c', unit: '次', isRatio: false, emptyText: '无 TCP 丢弃' }
+      ]
     }
   },
-  mounted() {
-    this.fetchData()
+  computed: {
+    hasAnyIssue() {
+      return this.cardConfigs.some(c => this.reportData[c.key]?.length > 0)
+    },
+    overallStatus() { return this.hasAnyIssue ? 'warning' : 'success' },
+    overallDescription() {
+      return this.hasAnyIssue ? '系统检测到部分节点异常,请点击卡片问号查看排查建议。' : '所有基础设施运行参数健康。'
+    }
   },
+  created() { this.loadReport() },
   methods: {
-    fetchData() {
-      getDashboard().then(resp => {
+    getData(key) { return this.reportData[key] || [] },
+    getShadowClass(config) {
+      const hasData = this.getData(config.key).length > 0
+      if (!hasData) return ''
+      return config.tagType + '-shadow'
+    },
+    async loadReport() {
+      this.loading = true
+      try {
+        const resp = await getDashboard()
         if (resp.code === 0) {
-          this.timeLabels = resp.data.timeLabels
-          this.instances = resp.data.instances
-        } else {
-          this.$message.error(resp.msg)
+          this.reportData = Object.assign({}, this.reportData, resp.data)
         }
-      }).catch(error => {
-        this.$message.error(error.message)
-      })
+      } finally { this.loading = false }
+    },
+    showAdvice(config) {
+      this.currentAdvice = {
+        title: config.title,
+        text: this.reportData.advices[config.key] || '暂无详细排查指引,请联系运维团队。'
+      }
+      this.dialogVisible = true
+    },
+    formatAdvice(text) {
+      // 简单的换行转义,如果后端返回的是带换行的文本
+      return text.replace(/\n/g, '<br>')
     }
   }
 }
 </script>
 
 <style scoped>
-.report-container { padding: 20px; background: #f4f7f9; }
-.node-card { margin-bottom: 25px; }
-.node-title { font-weight: bold; color: #1890ff; }
+/* 保持原样式并添加以下内容 */
+
+.header-ops {
+  display: flex;
+  align-items: center;
+}
+
+.advice-btn {
+  font-size: 18px;
+  color: #94a3b8;
+  margin-right: 8px;
+  padding: 0;
+  transition: color 0.2s;
+}
+
+.advice-btn:hover {
+  color: #409eff;
+}
+
+/* 弹窗样式美化 */
+.advice-dialog >>> .el-dialog__header {
+  border-bottom: 1px solid #f1f5f9;
+  padding: 20px;
+}
+
+.advice-dialog >>> .el-dialog__title {
+  font-size: 16px;
+  font-weight: bold;
+  color: #1e293b;
+}
+
+.advice-content {
+  padding: 10px 5px;
+  line-height: 1.6;
+  color: #475569;
+  font-size: 14px;
+}
+
+.markdown-body {
+  background: #f8fafc;
+  padding: 15px;
+  border-radius: 8px;
+  border-left: 4px solid #409eff;
+}
+
+/* 之前已有的样式保持不变 */
+.monitor-report-container { padding: 24px; background-color: #f6f8fb; min-height: 100vh; }
+.force-row { display: flex !important; flex-wrap: wrap !important; }
+.nice-card { height: 320px; border-radius: 12px; display: flex; flex-direction: column; margin-bottom: 20px; overflow: hidden; }
+.nice-card >>> .el-card__header { padding: 15px 20px; background-color: #fafbfd; border-bottom: 1px solid #f1f5f9; flex-shrink: 0; }
+.nice-card >>> .el-card__body { padding: 0; flex: 1; display: flex; flex-direction: column; overflow: hidden; }
+.card-header { display: flex; justify-content: space-between; align-items: center; }
+.title-wrapper { display: flex; align-items: center; flex: 1; }
+.status-tag { border-radius: 10px; padding: 0 8px; font-weight: bold; }
+.icon-accent { font-size: 18px; margin-right: 10px; }
+.title-text { font-size: 14px; font-weight: 600; color: #334155; }
+.title-text small { font-weight: normal; font-size: 11px; color: #94a3b8; display: block; }
+.card-body { padding: 15px 20px; flex: 1; overflow-y: auto; }
+.progress-item { margin-bottom: 18px; }
+.progress-item .info { display: flex; justify-content: space-between; margin-bottom: 6px; font-size: 12px; }
+.status-placeholder { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #cbd5e1; }
+.warning-shadow { border-top: 3px solid #e6a23c; }
+.danger-shadow { border-top: 3px solid #f56c6c; }
+.primary-shadow { border-top: 3px solid #409eff; }
+.warning-text { color: #e6a23c; }
+.danger-text { color: #f56c6c; }
+.primary-text { color: #409eff; }
+.text-ellipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 180px; }
+.scroll-style::-webkit-scrollbar { width: 4px; }
+.scroll-style::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 10px; }
+
+.status-banner {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24px 32px;
+  background: #ffffff;
+  border-radius: 16px;
+  margin-bottom: 24px;
+  position: relative;
+  overflow: hidden;
+  box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05);
+  border: 1px solid rgba(255, 255, 255, 0.8);
+}
+
+/* 状态颜色背景微调 */
+.banner-success {
+  background: linear-gradient(135deg, #f0fdf4 0%, #ffffff 50%);
+  border-left: 6px solid #22c55e;
+}
+.banner-warning {
+  background: linear-gradient(135deg, #fffbeb 0%, #ffffff 50%);
+  border-left: 6px solid #f59e0b;
+}
+
+/* 左侧状态图标与动画 */
+.banner-left {
+  display: flex;
+  align-items: center;
+  gap: 20px;
+}
+
+.status-visual {
+  position: relative;
+  width: 48px;
+  height: 48px;
+}
+
+.status-icon-wrapper {
+  width: 48px;
+  height: 48px;
+  border-radius: 14px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 24px;
+  z-index: 2;
+  position: relative;
+}
+
+.banner-success .status-icon-wrapper { background: #dcfce7; color: #16a34a; }
+.banner-warning .status-icon-wrapper { background: #fef3c7; color: #d97706; }
+
+.pulse-ring {
+  position: absolute;
+  top: 0; left: 0; width: 100%; height: 100%;
+  border-radius: 14px;
+  animation: pulse 2s infinite;
+}
+.banner-success .pulse-ring { background: rgba(34, 197, 94, 0.2); }
+.banner-warning .pulse-ring { background: rgba(245, 158, 11, 0.2); }
+
+@keyframes pulse {
+  0% { transform: scale(1); opacity: 1; }
+  100% { transform: scale(1.5); opacity: 0; }
+}
+
+/* 内容文本区 */
+.banner-title {
+  font-size: 20px;
+  font-weight: 800;
+  color: #1e293b;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 4px;
+}
+
+.banner-desc {
+  font-size: 14px;
+  color: #64748b;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.update-time {
+  font-size: 12px;
+  color: #94a3b8;
+}
+
+/* 中间指标区 */
+.banner-stats {
+  display: flex;
+  align-items: center;
+  background: rgba(248, 250, 252, 0.8);
+  padding: 12px 24px;
+  border-radius: 12px;
+  gap: 20px;
+}
+
+.stat-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.stat-label {
+  font-size: 11px;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+  color: #94a3b8;
+  margin-bottom: 2px;
+}
+
+.stat-value {
+  font-size: 18px;
+  font-weight: 700;
+  color: #334155;
+}
+.stat-value small { font-size: 12px; font-weight: normal; }
+.risk-text { color: #ef4444; }
+
+.stat-divider {
+  width: 1px;
+  height: 24px;
+  background: #e2e8f0;
+}
+
+/* 按钮发光效果 */
+.refresh-glow {
+  box-shadow: 0 4px 14px 0 rgba(64, 158, 255, 0.3);
+  transition: all 0.3s ease;
+}
+.refresh-glow:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 6px 20px rgba(64, 158, 255, 0.5);
+}
 </style>

+ 125 - 217
src/views/admin/Dashboard1.vue

@@ -1,105 +1,24 @@
 <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 class="monitor-report">
+    <el-card class="header-card" shadow="never">
+      <div slot="header" class="header-content">
+        <span class="title">🌐 基础设施资源监控日报</span>
+        <el-tag :type="reportData.status === '正常' ? 'success' : 'danger'" effect="dark">
+          状态:{{ reportData.statusSummary }}
+        </el-tag>
+      </div>
+      <div class="summary-text">
+        报告日期:{{ reportData.reportDate }} | 统计范围:全量生产集群节点
+      </div>
+    </el-card>
+
+    <el-row :gutter="20" class="chart-grid">
+      <el-col v-for="chart in chartConfigs" :key="chart.id" :span="12">
+        <el-card class="chart-card" shadow="hover">
+          <div slot="header" class="chart-header">
+            <i :class="chart.icon" /> {{ chart.title }}
           </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>
+          <div :id="chart.id" class="chart-container" />
         </el-card>
       </el-col>
     </el-row>
@@ -107,33 +26,60 @@
 </template>
 
 <script>
-import { getDashboard } from '@/api/devops'
-import { getAuthedUser } from '@/utils/auth'
+import * as echarts from 'echarts'
+import {getDashboard} from "@/api/devops";
 
 export default {
   name: 'Dashboard',
+  props: {
+    // 模拟从后端/父组件传入的数据
+    reportData: {
+      type: Object,
+      default: () => ({
+        reportDate: '2023-10-27',
+        statusSummary: '正常',
+        timeLabels: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '23:59'],
+        cpuSeries: { '192.168.1.1': [12, 15, 45, 30, 22, 18, 10], '192.168.1.2': [20, 22, 33, 55, 40, 30, 25] },
+        memSeries: { '192.168.1.1': [60, 62, 65, 70, 68, 65, 63], '192.168.1.2': [40, 42, 45, 48, 50, 52, 50] },
+        diskSeries: { '192.168.1.1': [5, 8, 12, 10, 8, 7, 5], '192.168.1.2': [15, 20, 25, 22, 20, 18, 15] },
+        netSeries: { '192.168.1.1': [2.5, 3.1, 8.5, 12.2, 7.4, 4.2, 3.0] }
+      })
+    }
+  },
   data() {
     return {
-      roles: [],
-      machineStatList: [],
-      sysInfo: null
+      charts: {}, // 存储 echarts 实例
+      chartConfigs: [
+        { id: 'cpuChart', title: '计算 (CPU Usage %)', icon: 'el-icon-cpu', unit: '%', dataKey: 'cpuSeries', warn: 70, crit: 85 },
+        { id: 'memChart', title: '存储 (Memory Usage %)', icon: 'el-icon-monitor', unit: '%', dataKey: 'memSeries', warn: 75, crit: 90 },
+        { id: 'diskChart', title: '磁盘 (Disk I/O Saturation %)', icon: 'el-icon-coin', unit: '%', dataKey: 'diskSeries', warn: 80, crit: 95 },
+        { id: 'netChart', title: '网络 (Network Ingress MB/s)', icon: 'el-icon-connection', unit: 'MB/s', dataKey: 'netSeries', warn: null }
+      ]
     }
   },
-  created() {
-    const { roles } = getAuthedUser()
-    this.roles = roles
-    document.title = 'Dashboard'
-    this.getData()
+  mounted() {
+    this.initAllCharts()
+    window.addEventListener('resize', this.handleResize)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.handleResize)
+    // 销毁实例,防止内存溢出
+    Object.values(this.charts).forEach(chart => chart.dispose())
   },
   methods: {
     getData() {
-      this.getDevopsDashboard()
-    },
-    getDevopsDashboard() {
       getDashboard().then(resp => {
         if (resp.code === 0) {
-          this.sysInfo = resp.data.sysInfo
-          this.machineStatList = resp.data.machineStatList
+          this.reportData = resp.data.reportData
+          this.statusSummary = resp.data.statusSummary
+          this.timeLabels = resp.data.timeLabels
+          this.cpuSeries = resp.data.cpuSeries
+          this.memSeries = resp.data.memSeries
+          this.diskSeries = resp.data.diskSeries
+          this.netSeries = resp.data.netSeries
+
+          // this.sysInfo = resp.data.sysInfo
+          // this.machineStatList = resp.data.machineStatList
         } else {
           this.$message.error(resp.msg)
         }
@@ -141,128 +87,90 @@ export default {
         this.$message.error(error.message)
       })
     },
-    goToRole(data) {
-      this.$message.info('当前操作角色: ' + data)
+    getWatermark(warn, crit) {
+      if (!warn) return {}
+      return {
+        symbol: ['none', 'none'],
+        silent: true,
+        data: [
+          { yAxis: warn, label: { formatter: '告警 {value}%' }, lineStyle: { color: '#E6A23C', type: 'dashed' }},
+          { yAxis: crit, label: { formatter: '临界 {value}%' }, lineStyle: { color: '#F56C6C', type: 'solid' }}
+        ]
+      }
+    },
+    initAllCharts() {
+      this.chartConfigs.forEach(conf => {
+        const chartDom = document.getElementById(conf.id)
+        const myChart = echarts.init(chartDom)
+
+        const seriesData = Object.keys(this.reportData[conf.dataKey]).map((ip, index) => ({
+          name: ip,
+          type: 'line',
+          smooth: true,
+          showSymbol: false,
+          data: this.reportData[conf.dataKey][ip],
+          markLine: index === 0 ? this.getWatermark(conf.warn, conf.crit) : {}
+        }))
+
+        const option = {
+          tooltip: { trigger: 'axis', backgroundColor: 'rgba(255, 255, 255, 0.9)', borderWidth: 1 },
+          legend: { bottom: '5', type: 'scroll', icon: 'circle' },
+          grid: { top: '15%', left: '3%', right: '8%', bottom: '15%', containLabel: true },
+          xAxis: { type: 'category', boundaryGap: false, data: this.reportData.timeLabels },
+          yAxis: { type: 'value', axisLabel: { formatter: `{value}${conf.unit}` }},
+          series: seriesData
+        }
+
+        myChart.setOption(option)
+        this.charts[conf.id] = myChart
+      })
+      this.getData()
     },
-    // 其余逻辑保持不变...
-    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)
+    handleResize() {
+      Object.values(this.charts).forEach(chart => chart.resize())
     }
   }
 }
 </script>
 
 <style scoped>
-.dashboard-wrapper {
-  padding: 10px;
+.monitor-report {
+  padding: 20px;
+  background-color: #f5f7fa;
+  min-height: 100vh;
 }
-
-.stat-row {
+.header-card {
   margin-bottom: 20px;
+  border-radius: 8px;
 }
-
-.role-card {
-  background: #fcfcfc;
-  border-left: 4px solid #409EFF;
-}
-
-.role-container {
+.header-content {
   display: flex;
+  justify-content: space-between;
   align-items: center;
 }
-
-.role-label {
+.title {
+  font-size: 20px;
   font-weight: bold;
-  color: #606266;
-  margin-right: 15px;
+  color: #303133;
 }
-
-.role-tag {
-  margin-right: 10px;
-  cursor: pointer;
-  transition: all 0.3s;
+.summary-text {
+  font-size: 14px;
+  color: #909399;
+  margin-top: 5px;
 }
-
-.role-tag:hover {
-  opacity: 0.8;
-  transform: translateY(-1px);
-}
-
-.card-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  font-weight: bold;
+.chart-grid {
+  margin-top: 10px;
 }
-
-.process-row {
+.chart-card {
   margin-bottom: 20px;
+  border-radius: 8px;
 }
-
-.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 {
+.chart-header {
   font-weight: bold;
+  color: #409EFF;
 }
-
-.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;
+.chart-container {
+  height: 350px;
+  width: 100%;
 }
 </style>

+ 258 - 0
src/views/admin/Dashboard2.vue

@@ -0,0 +1,258 @@
+<template>
+  <div class="monitor-dashboard">
+    <el-card class="header-card" shadow="never">
+      <div class="header-content">
+        <el-row type="flex" justify="space-between" align="middle">
+          <el-col :span="12">
+            <h2 class="title">🌐 基础设施资源监控 (24H)</h2>
+            <div class="summary">
+              数据日期:<el-tag size="small" type="info">{{ reportDate || '加载中...' }}</el-tag>
+              | 状态:<el-tag size="small" :type="statusType" effect="dark">{{ statusSummary }}</el-tag>
+            </div>
+          </el-col>
+          <el-col :span="12" class="header-ops">
+            <el-select v-model="refreshInterval" size="mini" placeholder="自动刷新" style="width: 110px; margin-right: 10px;">
+              <el-option label="不自动刷新" :value="0" />
+              <el-option label="1分钟刷新" :value="60000" />
+              <el-option label="5分钟刷新" :value="300000" />
+            </el-select>
+            <el-button
+              type="primary"
+              size="mini"
+              icon="el-icon-refresh"
+              :loading="loading"
+              @click="fetchData"
+            >
+              手动刷新
+            </el-button>
+          </el-col>
+        </el-row>
+      </div>
+    </el-card>
+
+    <el-row :gutter="20" class="pillar-grid" v-loading="loading">
+      <el-col
+        v-for="chart in chartConfigs"
+        :key="chart.id"
+        :xs="24"
+        :sm="24"
+        :md="12"
+        :lg="12"
+      >
+        <el-card class="chart-card" shadow="hover">
+          <div slot="header" class="card-header">
+            <i :class="chart.icon" style="margin-right: 8px; color: #409EFF;"></i>
+            <span class="header-text">{{ chart.title }}</span>
+          </div>
+          <div :id="chart.id" class="chart-container" />
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+import { getDashboard } from '@/api/devops'
+
+export default {
+  name: 'InfrastructureDashboard',
+  data() {
+    return {
+      loading: false,
+      reportDate: '',
+      statusSummary: '数据加载中...',
+      statusType: 'info',
+      charts: {},
+      refreshInterval: 0, // 默认不自动刷新
+      timer: null, // 定时器实例
+      chartConfigs: [
+        { id: 'cpuChart', title: 'CPU 使用率', icon: 'el-icon-monitor', unit: '%', key: 'cpuSeries', warn: 70, crit: 80 },
+        { id: 'memChart', title: '内存使用率', icon: 'el-icon-set-up', unit: '%', key: 'memSeries', warn: 70, crit: 85 },
+        { id: 'diskChart', title: '磁盘 I/O 饱和度', icon: 'el-icon-receiving', unit: '%', key: 'diskSeries', warn: 80, crit: 90 },
+        { id: 'netChart', title: '网络吞吐量', icon: 'el-icon-connection', unit: 'MB/s', key: 'netSeries', warn: null, crit: null }
+      ]
+    }
+  },
+  watch: {
+    // 监听刷新频率变化
+    refreshInterval(newVal) {
+      this.clearTimer()
+      if (newVal > 0) {
+        this.startTimer()
+      }
+    }
+  },
+  mounted() {
+    this.fetchData()
+    window.addEventListener('resize', this.handleResize)
+  },
+  activated() {
+    this.handleResize()
+    // 切回页面时,如果配置了自动刷新,重新开启
+    if (this.refreshInterval > 0) {
+      this.startTimer()
+    }
+  },
+  deactivated() {
+    // 切出页面(标签切换)时停止请求
+    this.clearTimer()
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.handleResize)
+    this.clearTimer()
+    Object.values(this.charts).forEach(chart => {
+      chart && chart.dispose()
+    })
+  },
+  methods: {
+    async fetchData() {
+      this.loading = true
+      try {
+        const resp = await getDashboard()
+        if (resp.code === 0) {
+          const reportData = resp.data
+          this.reportDate = reportData.reportDate
+          this.statusSummary = '数据已加载'
+          this.statusType = 'success'
+
+          this.$nextTick(() => {
+            this.initAllCharts(reportData)
+          })
+        } else {
+          this.$message.error(resp.msg || '获取数据失败')
+          this.statusType = 'danger'
+        }
+      } catch (error) {
+        console.error('Monitor Fetch Error:', error)
+        this.$message.error('数据刷新异常')
+      } finally {
+        this.loading = false
+      }
+    },
+
+    initAllCharts(data) {
+      this.chartConfigs.forEach(conf => {
+        const chartDom = document.getElementById(conf.id)
+        if (!chartDom) return
+
+        let myChart = echarts.getInstanceByDom(chartDom)
+        if (!myChart) {
+          myChart = echarts.init(chartDom)
+          this.charts[conf.id] = myChart
+        }
+
+        const option = this.getOption(conf, data)
+        // setOption 的第二个参数设为 true,确保新数据完全覆盖旧数据(不合并)
+        myChart.setOption(option, true)
+      })
+      // 渲染后统一进行一次 resize 适配容器
+      this.handleResize()
+    },
+
+    getOption(conf, rawData) {
+      const seriesData = rawData[conf.key] || {}
+      const timeLabels = rawData.timeLabels || []
+
+      const series = Object.keys(seriesData).map((ip, index) => ({
+        name: ip,
+        type: 'line',
+        smooth: true,
+        showSymbol: false,
+        data: seriesData[ip],
+        lineStyle: { width: 2 },
+        markLine: (index === 0 && conf.warn) ? this.getWatermarkLines(conf.warn, conf.crit) : null
+      }))
+
+      return {
+        tooltip: { trigger: 'axis', confine: true },
+        legend: { bottom: 0, type: 'scroll' },
+        grid: { top: 40, left: '3%', right: '5%', bottom: '15%', containLabel: true },
+        xAxis: { type: 'category', boundaryGap: false, data: timeLabels },
+        yAxis: { type: 'value', axisLabel: { formatter: `{value}${conf.unit}` } },
+        series: series
+      }
+    },
+
+    getWatermarkLines(warn, crit) {
+      return {
+        symbol: ['none', 'none'],
+        silent: true,
+        data: [
+          { yAxis: warn, lineStyle: { color: '#fa8c16', type: 'dashed' }, label: { position: 'end', formatter: 'Warn' } },
+          { yAxis: crit, lineStyle: { color: '#ff4d4f', type: 'dashed' }, label: { position: 'end', formatter: 'Crit' } }
+        ]
+      }
+    },
+
+    startTimer() {
+      this.timer = setInterval(() => {
+        // 如果当前正在加载,则跳过本次定时,避免请求堆积
+        if (!this.loading) {
+          this.fetchData()
+        }
+      }, this.refreshInterval)
+    },
+
+    clearTimer() {
+      if (this.timer) {
+        clearInterval(this.timer)
+        this.timer = null
+      }
+    },
+
+    handleResize() {
+      this.$nextTick(() => {
+        Object.values(this.charts).forEach(chart => {
+          chart && chart.resize()
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.monitor-dashboard {
+  padding: 15px;
+  background-color: #f0f2f5;
+  min-height: 100%;
+}
+
+.header-card {
+  margin-bottom: 15px;
+}
+
+.header-ops {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+}
+
+.title {
+  margin: 0 0 5px 0;
+  font-size: 18px;
+  color: #303133;
+}
+
+.summary {
+  color: #606266;
+  font-size: 12px;
+}
+
+.chart-card {
+  margin-bottom: 20px;
+  overflow: hidden;
+}
+
+.card-header {
+  border-left: 4px solid #409eff;
+  padding-left: 10px;
+  font-weight: bold;
+}
+
+.chart-container {
+  width: 100%;
+  height: 320px;
+}
+</style>

+ 80 - 2
src/views/devops/app/AppStat.vue

@@ -83,6 +83,10 @@
           width="280"
         >
           <template slot-scope="scope">
+            <el-button
+              size="mini"
+              @click="handleAppMon(scope.row)"
+            >资源监控</el-button>
             <el-button
               size="mini"
               @click="handleDetail(scope.$index, scope.row)"
@@ -197,15 +201,61 @@
       :visible.sync="showAppDialog"
       width="70%"
       center
+    >
+      <template />
+    </el-dialog>
+    <el-dialog
+      title="应用资源监控"
+      append-to-body
+      :visible.sync="showAppMonDialog"
+      width="100%"
+      center
     >
       <template>
+        <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">
+                  <cpu-chart
+                    title="CPU 同比 (%)"
+                    :time-labels="timeLabels"
+                    :cpu-group="item.cpu"
+                    :throttle-group="item.cpuThrottle"
+                    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>
     </el-dialog>
   </el-container>
 </template>
 
 <script>
+import ChartItem from '@/components/ChartItem.vue'
+import CpuChart from '@/components/CpuChart.vue'
 import {
+  getAppMonData,
   getAppStat,
   getAppStatDetail,
   getAppStatList,
@@ -216,6 +266,10 @@ import {
 
 export default {
   name: 'AppStat',
+  components: {
+    ChartItem,
+    CpuChart
+  },
   data() {
     return {
       envList: [],
@@ -237,7 +291,11 @@ export default {
       appStatList: [],
       statAppId: '',
       // **********************************************************************
-      showAppDialog: false
+      showAppDialog: false,
+      // **********************************************************************
+      showAppMonDialog: false,
+      timeLabels: [],
+      instances: []
     }
   },
   created() {
@@ -300,6 +358,23 @@ export default {
     onRefresh() {
       this.getAppStatDetailWrapper(this.statAppId)
     },
+    handleAppMon(row) {
+      getAppMonData(row.appId).then(resp => {
+        if (resp.code === 0) {
+          this.timeLabels = resp.data.timeLabels
+          this.instances = resp.data.instances
+          if (this.timeLabels.length === 0 || this.instances.length === 0) {
+            this.$message.warning('没有监控数据')
+            return
+          }
+          this.showAppMonDialog = true
+        } else {
+          this.$message.warning(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
     handleDetail(index, row) {
       this.statAppId = row.appId
       this.getAppStatDetailWrapper(this.statAppId)
@@ -371,10 +446,13 @@ export default {
       const queryInfo = {}
       queryInfo.appId = row.appId
       this.showAppDialog = true
-    },
+    }
   }
 }
 </script>
 
 <style>
+.report-container { padding: 20px; background: #f4f7f9; }
+.node-card { margin-bottom: 25px; }
+.node-title { font-weight: bold; color: #1890ff; }
 </style>

+ 74 - 2
src/views/devops/machine/MachineHost.vue

@@ -98,6 +98,11 @@
           width="180"
         >
           <template slot-scope="scope">
+            <el-button
+              size="mini"
+              type="warning"
+              @click="handleMachineMon(scope.row)"
+            >资源监控</el-button>
             <el-button
               size="mini"
               type="warning"
@@ -170,21 +175,67 @@
         </el-table>
       </template>
     </el-dialog>
+    <!-- 机器资源监控对话框 -->
+    <el-dialog
+      title="机器资源监控"
+      append-to-body
+      :visible.sync="showMachineMonDialog"
+      width="100%"
+      center
+    >
+      <template>
+        <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>
+    </el-dialog>
   </el-container>
 </template>
 
 <script>
+import ChartItem from '@/components/ChartItem.vue'
 import {
   deleteMachine,
   deprecateMachine,
   getEnvList,
-  getMachineList,
+  getMachineList, getMachineMon,
   getMachineUsedList,
   updateMachineEnv
 } from '@/api/devops'
 
 export default {
   name: 'MachineHost',
+  components: { ChartItem },
   data() {
     return {
       queryInfo: {
@@ -206,7 +257,11 @@ export default {
       },
       envList: [],
       showUsageDialog: false,
-      machineUsedList: []
+      machineUsedList: [],
+      // **********************************************************************
+      showMachineMonDialog: false,
+      timeLabels: [],
+      instances: []
     }
   },
   created() {
@@ -299,6 +354,23 @@ export default {
       })
       this.getData()
     },
+    handleMachineMon(row) {
+      getMachineMon(row.machineId).then(resp => {
+        if (resp.code === 0) {
+          this.timeLabels = resp.data.timeLabels
+          this.instances = resp.data.instances
+          if (this.timeLabels.length === 0 || this.instances.length === 0) {
+            this.$message.warning('没有监控数据')
+            return
+          }
+          this.showMachineMonDialog = true
+        } else {
+          this.$message.warning(resp.msg)
+        }
+      }).catch(error => {
+        this.$message.error(error.message)
+      })
+    },
     handleDeprecate(index, row) {
       const formData = new FormData()
       formData.append('machineId', row.machineId)