浏览代码

feat:优化Redis缓存监控页面

microrain 5 月之前
父节点
当前提交
05c68045e0
共有 1 个文件被更改,包括 673 次插入85 次删除
  1. 673 85
      src/views/system/monitor/cache/index.vue

+ 673 - 85
src/views/system/monitor/cache/index.vue

@@ -1,8 +1,90 @@
 <template>
-  <div class="page">
+  <div class="page redis-monitor">
+    <!-- Redis状态概览 -->
+    <el-row :gutter="15" class="dashboard-row">
+      <el-col :span="6" class="marg-b-15">
+        <el-card shadow="hover" class="status-card memory-card">
+          <div class="status-value">{{ sysInfo.memory ? memorySizeFormat(sysInfo.memory.used_memory) : '加载中...' }}</div>
+          <div class="status-label">内存使用</div>
+          <el-progress 
+            v-if="sysInfo.memory" 
+            :percentage="parseFloat(((sysInfo.memory.used_memory / sysInfo.memory.maxmemory) * 100).toFixed(1))" 
+            :color="memoryColorGetter" 
+            :stroke-width="8" 
+            class="memory-progress"
+          />
+          <div class="status-icon"><i class="el-icon-cpu"></i></div>
+        </el-card>
+      </el-col>
+      <el-col :span="6" class="marg-b-15">
+        <el-card shadow="hover" class="status-card keys-card">
+          <div class="status-value">{{ sysInfo.stats ? sysInfo.stats.total_commands_processed : '加载中...' }}</div>
+          <div class="status-label">已处理命令</div>
+          <div class="status-trend">
+            <div class="trend-value">+{{ commandsProcessedTrend }}</div>
+            <div class="trend-chart">
+              <div ref="commandsTrendRef" class="mini-chart"></div>
+            </div>
+          </div>
+          <div class="status-icon"><i class="el-icon-s-operation"></i></div>
+        </el-card>
+      </el-col>
+      <el-col :span="6" class="marg-b-15">
+        <el-card shadow="hover" class="status-card clients-card">
+          <div class="status-value">{{ sysInfo.clients ? sysInfo.clients.connected_clients : '加载中...' }}</div>
+          <div class="status-label">客户端连接数</div>
+          <div class="clients-info" v-if="sysInfo.clients">
+            <span>阻塞: {{ sysInfo.clients.blocked_clients }}</span>
+          </div>
+          <div class="status-icon"><i class="el-icon-user"></i></div>
+        </el-card>
+      </el-col>
+      <el-col :span="6" class="marg-b-15">
+        <el-card shadow="hover" class="status-card uptime-card">
+          <div class="status-value">{{ sysInfo.server ? timeFormat(sysInfo.server.uptime_in_seconds) : '加载中...' }}</div>
+          <div class="status-label">服务运行时间</div>
+          <div class="uptime-info" v-if="sysInfo.server">
+            <span>版本: {{ sysInfo.server.redis_version }}</span>
+          </div>
+          <div class="status-icon"><i class="el-icon-time"></i></div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 内存使用图表 & 命令处理图表 -->
     <el-row :gutter="15">
       <el-col :span="12" class="marg-b-15">
-        <el-card shadow="nover" class="box-card-height" style="height:auto">
+        <el-card shadow="hover" class="chart-card">
+          <template #header>
+            <div class="card-header">
+              <span>内存使用监控</span>
+              <div class="card-header-right">
+                <el-tag size="small" type="info">实时更新</el-tag>
+              </div>
+            </div>
+          </template>
+          <div ref="memoryChartRef" class="chart-container"></div>
+        </el-card>
+      </el-col>
+      <el-col :span="12" class="marg-b-15">
+        <el-card shadow="hover" class="chart-card">
+          <template #header>
+            <div class="card-header">
+              <span>命令处理监控</span>
+              <div class="card-header-right">
+                <el-tag size="small" type="info">实时更新</el-tag>
+              </div>
+            </div>
+          </template>
+          <div ref="commandsChartRef" class="chart-container"></div>
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 系统信息卡片 -->
+    <el-row :gutter="15">
+      <el-col :span="12" class="marg-b-15">
+        <el-card shadow="hover" class="data-card">
           <template #header>
             <div class="card-header">
               <span>客户端信息</span>
@@ -323,9 +405,11 @@
 </template>
 
 <script lang="ts">
-import { toRefs, reactive, defineComponent } from 'vue';
+import { toRefs, reactive, defineComponent, ref, onMounted, onBeforeUnmount, computed } from 'vue';
 import 'echarts-wordcloud';
 import { getSSEOrigin } from '/@/utils/origin'
+import * as echarts from 'echarts';
+
 let interval: any = null;
 let es: any = null;
 export default defineComponent({
@@ -335,8 +419,414 @@ export default defineComponent({
     const state: any = reactive({
       myCharts: [],
       sysInfo: {},
+      memoryChartData: {
+        times: [] as string[],
+        values: [] as number[]
+      },
+      commandsChartData: {
+        times: [] as string[],
+        values: [] as number[],
+        lastValue: 0,
+        trend: [] as number[]
+      },
+      topCommands: [] as { name: string, value: number }[]
+    });
+
+    const memoryChartRef = ref<HTMLElement | null>(null);
+    const commandsChartRef = ref<HTMLElement | null>(null);
+    const commandsTrendRef = ref<HTMLElement | null>(null);
+    const commandsPieChartRef = ref<HTMLElement | null>(null);
+
+    let memoryChart: echarts.ECharts | null = null;
+    let commandsChart: echarts.ECharts | null = null;
+    let commandsTrendChart: echarts.ECharts | null = null;
+    let commandsPieChart: echarts.ECharts | null = null;
+
+    // 引入memorySizeFormat和timeFormat方法到setup作用域
+    function memorySizeFormat(size: any) {
+      size = parseFloat(size);
+      let rank = 0;
+      let rankchar = 'Bytes';
+      while (size > 1024 && rankchar != 'TB') {
+        size = size / 1024;
+        rank++;
+        if (rank == 1) {
+          rankchar = 'KB';
+        } else if (rank == 2) {
+          rankchar = 'MB';
+        } else if (rank == 3) {
+          rankchar = 'GB';
+        } else if (rank == 4) {
+          rankchar = 'TB';
+        }
+      }
+      return size.toFixed(2) + ' ' + rankchar;
+    }
+
+    function lengthToFixed2(size: any) {
+      size = parseFloat(size);
+      return size.toFixed(2);
+    }
+
+    function timeFormat(second: any) {
+      if (!second) return '-'
+      second = parseFloat(second);
+      let rank = 0;
+      let rankchar = '秒';
+      while ((second > 60 && rankchar != '小时' && rankchar != '天') || (second > 24 && rankchar == '小时')) {
+        if (rankchar == '小时') {
+          second = second / 24;
+        } else {
+          second = second / 60;
+        }
+        rank++;
+        if (rank == 1) {
+          rankchar = '分';
+        } else if (rank == 2) {
+          rankchar = '小时';
+        } else if (rank == 3) {
+          rankchar = '天';
+        }
+      }
+      return second.toFixed(2) + ' ' + rankchar;
+    }
+
+    const commandsProcessedTrend = computed(() => {
+      const trend = state.commandsChartData.trend;
+      if (trend.length === 0) return 0;
+      return trend[trend.length - 1];
+    });
+
+    const memoryColorGetter = computed(() => {
+      if (!state.sysInfo.memory) return '#409EFF';
+      const percentage = (state.sysInfo.memory.used_memory / state.sysInfo.memory.maxmemory) * 100;
+      if (percentage < 50) return '#67C23A';
+      if (percentage < 80) return '#E6A23C';
+      return '#F56C6C';
+    });
+
+    onMounted(() => {
+      initCharts();
+      
+      // 每5秒更新一次图表数据
+      interval = setInterval(() => {
+        updateMemoryChart();
+        updateCommandsChart();
+        updateCommandsPieChart();
+      }, 5000);
     });
 
+    onBeforeUnmount(() => {
+      if (interval) {
+        clearInterval(interval);
+        interval = null;
+      }
+      
+      if (es) {
+        es.close();
+      }
+      
+      destroyCharts();
+    });
+
+    function destroyCharts() {
+      if (memoryChart) {
+        memoryChart.dispose();
+        memoryChart = null;
+      }
+      
+      if (commandsChart) {
+        commandsChart.dispose();
+        commandsChart = null;
+      }
+      
+      if (commandsTrendChart) {
+        commandsTrendChart.dispose();
+        commandsTrendChart = null;
+      }
+      
+      if (commandsPieChart) {
+        commandsPieChart.dispose();
+        commandsPieChart = null;
+      }
+    }
+
+    function initCharts() {
+      if (memoryChartRef.value) {
+        memoryChart = echarts.init(memoryChartRef.value);
+        const option = {
+          grid: {
+            top: 20,
+            right: 20,
+            bottom: 30,
+            left: 50
+          },
+          tooltip: {
+            trigger: 'axis',
+            formatter: function(params: any) {
+              const time = params[0].axisValue;
+              const value = memorySizeFormat(params[0].data);
+              return `${time}<br />内存使用: ${value}`;
+            }
+          },
+          xAxis: {
+            type: 'category',
+            data: [],
+            axisLabel: {
+              rotate: 45
+            }
+          },
+          yAxis: {
+            type: 'value',
+            axisLabel: {
+              formatter: function(value: number) {
+                return memorySizeFormat(value);
+              }
+            }
+          },
+          series: [
+            {
+              data: [],
+              type: 'line',
+              smooth: true,
+              areaStyle: {
+                opacity: 0.3
+              },
+              lineStyle: {
+                width: 3
+              },
+              itemStyle: {
+                color: '#409EFF'
+              }
+            }
+          ]
+        };
+        memoryChart.setOption(option);
+      }
+
+      if (commandsChartRef.value) {
+        commandsChart = echarts.init(commandsChartRef.value);
+        const option = {
+          grid: {
+            top: 20,
+            right: 20,
+            bottom: 30,
+            left: 50
+          },
+          tooltip: {
+            trigger: 'axis'
+          },
+          xAxis: {
+            type: 'category',
+            data: [],
+            axisLabel: {
+              rotate: 45
+            }
+          },
+          yAxis: {
+            type: 'value'
+          },
+          series: [
+            {
+              data: [],
+              type: 'bar',
+              itemStyle: {
+                color: '#67C23A'
+              }
+            }
+          ]
+        };
+        commandsChart.setOption(option);
+      }
+
+      if (commandsTrendRef.value) {
+        commandsTrendChart = echarts.init(commandsTrendRef.value);
+        const option = {
+          grid: {
+            top: 0,
+            right: 0,
+            bottom: 0,
+            left: 0
+          },
+          xAxis: {
+            type: 'category',
+            show: false,
+            data: [1, 2, 3, 4, 5]
+          },
+          yAxis: {
+            type: 'value',
+            show: false
+          },
+          series: [
+            {
+              data: [0, 0, 0, 0, 0],
+              type: 'line',
+              smooth: true,
+              symbol: 'none',
+              lineStyle: {
+                width: 2,
+                color: '#67C23A'
+              }
+            }
+          ]
+        };
+        commandsTrendChart.setOption(option);
+      }
+
+      if (commandsPieChartRef.value) {
+        commandsPieChart = echarts.init(commandsPieChartRef.value);
+        updateCommandsPieChart();
+      }
+    }
+
+    function getCurrentTime() {
+      const now = new Date();
+      const hours = now.getHours().toString().padStart(2, '0');
+      const minutes = now.getMinutes().toString().padStart(2, '0');
+      const seconds = now.getSeconds().toString().padStart(2, '0');
+      return `${hours}:${minutes}:${seconds}`;
+    }
+
+    function updateMemoryChart() {
+      if (!memoryChart || !state.sysInfo.memory) return;
+      
+      const currentTime = getCurrentTime();
+      state.memoryChartData.times.push(currentTime);
+      state.memoryChartData.values.push(state.sysInfo.memory.used_memory);
+      
+      // 保持最多显示20个数据点
+      if (state.memoryChartData.times.length > 20) {
+        state.memoryChartData.times.shift();
+        state.memoryChartData.values.shift();
+      }
+      
+      memoryChart.setOption({
+        xAxis: {
+          data: state.memoryChartData.times
+        },
+        series: [
+          {
+            data: state.memoryChartData.values
+          }
+        ]
+      });
+    }
+
+    function updateCommandsChart() {
+      if (!commandsChart || !state.sysInfo.stats) return;
+      
+      const currentTime = getCurrentTime();
+      const currentValue = state.sysInfo.stats.total_commands_processed;
+      
+      // 计算增量
+      let increment = 0;
+      if (state.commandsChartData.lastValue > 0) {
+        increment = currentValue - state.commandsChartData.lastValue;
+      }
+      
+      state.commandsChartData.lastValue = currentValue;
+      state.commandsChartData.times.push(currentTime);
+      state.commandsChartData.values.push(increment);
+      
+      // 添加到趋势数据
+      state.commandsChartData.trend.push(increment);
+      if (state.commandsChartData.trend.length > 5) {
+        state.commandsChartData.trend.shift();
+      }
+      
+      // 保持最多显示20个数据点
+      if (state.commandsChartData.times.length > 20) {
+        state.commandsChartData.times.shift();
+        state.commandsChartData.values.shift();
+      }
+      
+      commandsChart.setOption({
+        xAxis: {
+          data: state.commandsChartData.times
+        },
+        series: [
+          {
+            data: state.commandsChartData.values
+          }
+        ]
+      });
+      
+      // 更新小趋势图
+      if (commandsTrendChart) {
+        commandsTrendChart.setOption({
+          series: [
+            {
+              data: state.commandsChartData.trend
+            }
+          ]
+        });
+      }
+    }
+
+    function updateCommandsPieChart() {
+      if (!commandsPieChart || !state.sysInfo.stats) return;
+      
+      // 构建命令统计数据
+      if (state.sysInfo.stats.commandstats) {
+        const commandsData = [];
+        for (const cmd in state.sysInfo.stats.commandstats) {
+          if (Object.prototype.hasOwnProperty.call(state.sysInfo.stats.commandstats, cmd)) {
+            const cmdName = cmd.replace('cmdstat_', '').toUpperCase();
+            const calls = state.sysInfo.stats.commandstats[cmd].calls;
+            commandsData.push({
+              name: cmdName,
+              value: calls
+            });
+          }
+        }
+        
+        // 按调用次数排序并取前10个
+        commandsData.sort((a, b) => b.value - a.value);
+        state.topCommands = commandsData.slice(0, 10);
+        
+        commandsPieChart.setOption({
+          tooltip: {
+            trigger: 'item',
+            formatter: '{a} <br/>{b}: {c} ({d}%)'
+          },
+          legend: {
+            orient: 'vertical',
+            right: 10,
+            top: 'center',
+            data: state.topCommands.map((item: { name: string }) => item.name)
+          },
+          series: [
+            {
+              name: '命令调用次数',
+              type: 'pie',
+              radius: ['40%', '70%'],
+              avoidLabelOverlap: false,
+              itemStyle: {
+                borderRadius: 10,
+                borderColor: '#fff',
+                borderWidth: 2
+              },
+              label: {
+                show: false,
+                position: 'center'
+              },
+              emphasis: {
+                label: {
+                  show: true,
+                  fontSize: 16,
+                  fontWeight: 'bold'
+                }
+              },
+              labelLine: {
+                show: false
+              },
+              data: state.topCommands
+            }
+          ]
+        });
+      }
+    }
+
     function startWs() {
       es = new EventSource(getSSEOrigin("/subscribe/redisinfo"));
 
@@ -359,7 +849,8 @@ export default defineComponent({
 
     function memoryInfoMsg(event: { data: any; }) {
       const data = JSON.parse(event.data);
-      state.sysInfo.memory = data
+      state.sysInfo.memory = data;
+      updateMemoryChart();
     }
 
     function serverInfoMsg(event: { data: any; }) {
@@ -375,7 +866,9 @@ export default defineComponent({
 
     function statsInfoMsg(event: { data: any; }) {
       const data = JSON.parse(event.data);
-      state.sysInfo.stats = data
+      state.sysInfo.stats = data;
+      updateCommandsChart();
+      updateCommandsPieChart();
     }
 
     function clientsInfoMsg(event: { data: any; }) {
@@ -385,6 +878,15 @@ export default defineComponent({
 
     return {
       ...toRefs(state),
+      memoryChartRef,
+      commandsChartRef,
+      commandsTrendRef,
+      commandsPieChartRef,
+      commandsProcessedTrend,
+      memoryColorGetter,
+      memorySizeFormat,
+      lengthToFixed2,
+      timeFormat
     };
   },
   unmounted() {
@@ -400,98 +902,184 @@ export default defineComponent({
     return {};
   },
   methods: {
-    memorySizeFormat(size: any) {
-      size = parseFloat(size);
-      let rank = 0;
-      let rankchar = 'Bytes';
-      while (size > 1024 && rankchar != 'TB') {
-        size = size / 1024;
-        rank++;
-        if (rank == 1) {
-          rankchar = 'KB';
-        } else if (rank == 2) {
-          rankchar = 'MB';
-        } else if (rank == 3) {
-          rankchar = 'GB';
-        } else if (rank == 4) {
-          rankchar = 'TB';
-        }
-      }
-      return size.toFixed(2) + ' ' + rankchar;
-    },
-    lengthToFixed2(size: any) {
-      size = parseFloat(size);
-      return size.toFixed(2);
-    },
-    timeFormat(second: any) {
-      if (!second) return '-'
-      second = parseFloat(second);
-      let rank = 0;
-      let rankchar = '秒';
-      while ((second > 60 && rankchar != '小时' && rankchar != '天') || (second > 24 && rankchar == '小时')) {
-        if (rankchar == '小时') {
-          second = second / 24;
-        } else {
-          second = second / 60;
-        }
-        rank++;
-        if (rank == 1) {
-          rankchar = '分';
-        } else if (rank == 2) {
-          rankchar = '小时';
-        } else if (rank == 3) {
-          rankchar = '天';
-        }
-      }
-      return second.toFixed(2) + ' ' + rankchar;
-    },
   },
 });
 
 </script>
 
 <style scoped lang="scss">
-.el-card {
-  height: 300px;
-  overflow-y: auto;
-}
+.redis-monitor {
+  .dashboard-row {
+    margin-bottom: 10px;
+  }
 
-.marg-b-15 {
-  margin-bottom: 15px;
-}
+  .status-card {
+    height: 120px;
+    position: relative;
+    overflow: visible;
+    transition: all 0.3s;
 
-.cell {
-  box-sizing: border-box;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: normal;
-  word-break: break-all;
-  line-height: 36px;
-  padding-left: 10px;
-  padding-right: 10px;
-}
+    &:hover {
+      transform: translateY(-5px);
+      box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
+    }
 
-.cell-card {
-  box-sizing: border-box;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: normal;
-  word-break: break-all;
-  line-height: 1.2;
-  margin: 10px 0;
-}
+    .status-value {
+      font-size: 26px;
+      font-weight: bold;
+      line-height: 1.2;
+      margin-bottom: 5px;
+    }
 
-.box-card {
-  min-height: 380px;
-}
+    .status-label {
+      font-size: 14px;
+      color: #909399;
+      margin-bottom: 10px;
+    }
 
-.box-card-meter {
-  height: 230px;
+    .status-icon {
+      position: absolute;
+      right: 15px;
+      top: 15px;
+      font-size: 24px;
+      opacity: 0.2;
+    }
 
-  min-height: 180px;
-}
+    .status-trend {
+      display: flex;
+      align-items: center;
+      margin-top: 10px;
+      
+      .trend-value {
+        color: #67C23A;
+        font-weight: bold;
+        margin-right: 10px;
+      }
+      
+      .trend-chart {
+        flex: 1;
+      }
+    }
+    
+    .clients-info, .uptime-info {
+      font-size: 12px;
+      color: #909399;
+      margin-top: 15px;
+    }
+
+    .memory-progress {
+      margin-top: 5px;
+    }
+
+    &.memory-card { border-top: 3px solid #409EFF; }
+    &.keys-card { border-top: 3px solid #67C23A; }
+    &.clients-card { border-top: 3px solid #E6A23C; }
+    &.uptime-card { border-top: 3px solid #F56C6C; }
+  }
+
+  .chart-card, .data-card {
+    margin-bottom: 15px;
+    
+    .card-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+
+    .chart-container {
+      height: 300px;
+      width: 100%;
+    }
+
+    .commands-chart-container {
+      height: 350px;
+      width: 100%;
+    }
+  }
+
+  .data-card {
+    .db-card {
+      margin-bottom: 15px;
+      transition: all 0.3s;
+      
+      &:hover {
+        transform: translateY(-3px);
+        box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
+      }
+      
+      .db-title {
+        font-size: 16px;
+        font-weight: bold;
+        margin-bottom: 15px;
+        color: #409EFF;
+      }
+      
+      .db-info {
+        .db-item {
+          margin-bottom: 8px;
+          display: flex;
+          justify-content: space-between;
+          
+          .db-label {
+            color: #606266;
+          }
+          
+          .db-value {
+            font-weight: bold;
+          }
+        }
+      }
+    }
+  }
+
+  .mini-chart {
+    height: 30px;
+    width: 100%;
+  }
+
+  .marg-b-15 {
+    margin-bottom: 15px;
+  }
 
-.el-form-item {
-  margin-bottom: 5px;
+  .cell {
+    box-sizing: border-box;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: normal;
+    word-break: break-all;
+    line-height: 36px;
+    padding-left: 10px;
+    padding-right: 10px;
+  }
+
+  .cell-card {
+    box-sizing: border-box;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: normal;
+    word-break: break-all;
+    line-height: 1.2;
+    margin: 10px 0;
+  }
+
+  table {
+    width: 100%;
+    border-collapse: collapse;
+    
+    td {
+      padding: 8px;
+      &:first-child {
+        width: 40%;
+        color: #606266;
+      }
+      &:last-child {
+        font-weight: 500;
+      }
+    }
+    
+    tr:nth-child(even) {
+      background-color: #f9f9f9;
+    }
+  }
 }
 </style>