dongyukun
2025-05-29 be664d7c011a473002c1b413bac8303f7905d160
src/views/infra/monitor/components/MonitorDisk.vue
@@ -8,41 +8,25 @@
      :inline="true"
      label-width="68px"
    >
      <!--      <el-form-item label="主机名称" prop="hostName">-->
      <!--        <el-input-->
      <!--          v-model="queryParams.hostName"-->
      <!--          placeholder="请输入主机名称"-->
      <!--          clearable-->
      <!--          @keyup.enter="handleQuery"-->
      <!--          class="!w-240px"-->
      <!--        />-->
      <!--      </el-form-item>-->
      <el-form-item label="服务器IP" prop="hostIp">
        <el-input
          v-model="queryParams.hostIp"
          placeholder="请输入服务器IP"
          clearable
          @keyup.enter="handleQuery"
          class="!w-120px"
        />
      <el-form-item label="主机名称" prop="hostName">
        <el-select v-model="queryParams.hostName" clearable placeholder="请选择" class="!w-180px" @change="getDataList">
          <el-option
            v-for="(host, index) in hosts"
            :key="index"
            :label="host"
            :value="host"
          />
        </el-select>
      </el-form-item>
      <!--      <el-form-item label="盘符" prop="disk">-->
      <!--        <el-input-->
      <!--          v-model="queryParams.disk"-->
      <!--          placeholder="请输入盘符"-->
      <!--          clearable-->
      <!--          @keyup.enter="handleQuery"-->
      <!--          class="!w-240px"-->
      <!--        />-->
      <!--      </el-form-item>-->
      <el-form-item label="磁盘名" prop="diskName">
        <el-input
          v-model="queryParams.diskName"
          placeholder="请输入磁盘名"
          clearable
          @keyup.enter="handleQuery"
          class="!w-120px"
        />
      <el-form-item label="服务器IP" prop="hostIp">
        <el-select v-model="queryParams.hostIp" clearable placeholder="请选择" class="!w-160px" @change="getDataList">
          <el-option
            v-for="(ip, index) in ips"
            :key="index"
            :label="ip"
            :value="ip"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="创建时间" prop="createTime">
        <el-date-picker
@@ -109,9 +93,8 @@
  <ContentWrap v-if="showType == 'chart'">
    <!-- 磁盘使用率折线图 -->
    <el-skeleton :loading="echartsLoading" animated>
      <Echart :height="320" :options="diskChartOptions"/>
    </el-skeleton>
    <div id="chartArea"></div>
    <!-- 磁盘使用率饼图 -->
    <h3 style="margin-top: 20px; margin-bottom: 10px">主机磁盘使用率</h3>
    <div v-for="host in hostList" :key="host.name" class="host">
@@ -131,19 +114,6 @@
        </el-skeleton>
      </div>
    </div>
<!--    <div v-for="(host, hostIndex) in hostList" :key="hostIndex">-->
<!--      <div style="margin-top: 10px">-->
<!--        <el-skeleton :loading="echartsLoading" animated>-->
<!--          {{ hostIndex }} &nbsp;&nbsp;&nbsp;&nbsp;主机名: {{ host.name }}&nbsp;&nbsp;&nbsp;&nbsp;-->
<!--          服务器IP:{{ host.ip }}-->
<!--          <div v-for="(disk, diskIndex) in host.disks" :key="diskIndex">-->
<!--            <h3>{{ disk.name }}</h3>-->
<!--            <div :ref="el => chartRefs[hostIndex][diskIndex] = el"-->
<!--                 :style="{ width: '300px', height: '300px' }"></div>-->
<!--          </div>-->
<!--        </el-skeleton>-->
<!--      </div>-->
<!--    </div>-->
  </ContentWrap>
  <!-- 列表 -->
@@ -217,6 +187,8 @@
const loading = ref(true) // 列表的加载中
const list = ref<MonitorDiskVO[]>([]) // 列表的数据
const hosts = ref<String[]>([]) // 主机列表
const ips = ref<String[]>([]) // ip列表
const total = ref(0) // 列表的总页数
const queryParams = reactive({
  pageNo: 1,
@@ -236,26 +208,197 @@
const echartsLoading = ref(true) // 图表加载中
const showType = ref() //展示类型(chart-图例,data-数据)
const hostList = ref([
  {
    name: 'Thinkpad-E14',
    ip: '172.16.216.133',
    disks: [
      {disk: '磁盘C', used: 70, total: 200},
      {disk: '磁盘D', used: 40, total: 60}
    ]
  },
  {
    name: 'Thinkpad-E16',
    ip: '172.16.216.133',
    disks: [
      {disk: '磁盘C', used: 80, total: 500},
      {disk: '磁盘D', used: 20, total: 500}
    ]
  }
]);
const hostList = ref([]);
const chartRefs = ref([]);
// 颜色集合(10个区分度较好的颜色)
const colors = [
  '#5470C6', '#91CC75', '#FAC858', '#EE6666', '#73C0DE',
  '#3BA272', '#FC8452', '#9A60B4', '#EA7CCC', '#19DCDC'
]
const chartRefs = ref<HTMLElement[]>([])
// 图表实例和容器管理
const chartInstances = ref<{ instance: echarts.ECharts; container: HTMLDivElement }[]>([])
// 清理所有图表资源
const cleanCharts = () => {
  // 1. 销毁所有图表实例
  chartInstances.value.forEach(({ instance }) => {
    instance.dispose()
  })
  // 2. 移除所有容器元素
  chartInstances.value.forEach(({ container }) => {
    container.remove()
  })
  // 3. 清空实例记录
  chartInstances.value = []
}
// 处理接口数据
const processServerData = (apiData: any[]) => {
  return apiData.flatMap(serverObj => {
    return Object.entries(serverObj).map(([serverName, dataPoints]) => ({
      serverName,
      data: (dataPoints as any[]).map(item => ({
        createTime: item.createTime,
        ...Object.fromEntries(
          Object.entries(item)
            .filter(([key]) => key !== 'createTime')
            .map(([key, value]) => [key.replace(/[()/]/g, '_'), value]) // 清理特殊字符
        )
      })).sort((a, b) => a.createTime - b.createTime) // 按时间排序
    }))
  })
}
// 初始化图表
const initCharts = async (apiData: any[]) => {
  cleanCharts()
  await nextTick()
  const servers = processServerData(apiData)
  const chartArea = document.querySelector('#chartArea')
  servers.forEach((server, index) => {
    // 创建容器
    const container = document.createElement('div')
    container.style.width = '100%'
    container.style.height = '300px'
    container.classList.add('chart-container')
    chartArea?.appendChild(container)
    // 初始化图表实例
    const chart = echarts.init(container)
    const dimensions = Object.keys(server.data[0]).filter(k => k !== 'createTime')
    // 图表配置
    const option: echarts.EChartsOption = {
      title: {
        text: `${server.serverName} 磁盘使用率趋势`,
        left: 'center',
        textStyle: {
          fontSize: 16,
          fontWeight: 'bold'
        }
      },
      tooltip: {
        trigger: 'axis',
        valueFormatter: (value) => `${value}%`,
        axisPointer: {
          type: 'cross',
          label: {
            backgroundColor: '#6a7985'
          }
        }
      },
      legend: {
        type: 'scroll',
        top: 30,
        pageIconColor: '#2c3e50',
        pageTextStyle: {
          color: '#666'
        }
      },
      grid: {
        top: 80,
        left: 50,
        right: 30,
        bottom: 30,
        containLabel: true
      },
      xAxis: {
        type: 'time',
        axisLabel: {
          formatter: (value: number) => {
            const date = new Date(value)
            return `${date.getMonth() + 1}/${date.getDate()}`
          },
          interval: 0,
          length: 6,
          rotate: 45,
          fontSize: 12
        },
        min: (value) => value.min - 86400000,
        max: (value) => value.max + 86400000
      },
      yAxis: {
        type: 'value',
        min: 0,
        max: 100,
        axisLabel: {
          formatter: '{value}%',
          fontSize: 12
        },
        splitLine: {
          show: true,
          lineStyle: {
            type: 'dashed'
          }
        }
      },
      toolbox: {
        feature: {
          dataZoom: {
            type: 'inside',
            filterMode: 'none', // 禁用数据过滤
            yAxisIndex: false,
            title: { zoom: '缩放', back: '还原' }
          },
          brush: {
            type: ['lineX', 'clear'],
            title: { lineX: '选择', clear: '清除' }
          },
          saveAsImage: {
            name: '磁盘使用率图表',
            title: '保存',
            pixelRatio: 2
          }
        }
      },
      color: colors,
      series: dimensions.map((dim, dimIndex) => ({
        name: dim.replace(/_/g, ' '), // 还原清理的特殊字符
        type: 'line',
        // 关键配置项
        progressive: 0,          // 禁用分片渲染
        large: false,            // 禁用大数据模式
        showAllSymbol: true,     // 显示所有数据点
        sampling: 'none',        // 禁用采样
        // 数据维度声明
        dimensions: ['createTime', 'value'],
        smooth: true,
        symbol: 'none',
        areaStyle: {
          opacity: 0.3,
          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
            { offset: 0, color: colors[dimIndex % colors.length] },
            { offset: 1, color: 'rgba(255,255,255,0)' }
          ])
        },
        lineStyle: {
          width: 2,
          color: colors[dimIndex % colors.length]
        },
        data: server.data.map(d => [d.createTime, d[dim]])
      })),
      dataZoom: [{
        type: 'inside',
        filterMode: 'none' // 禁用数据过滤
        // type: 'inside',
        // start: 0,
        // end: 100,
        // minValueSpan: 86400000 * 1 // 最小缩放范围为1天
      }]
    }
    chart.setOption(option)
    chartInstances.value.push({ instance: chart, container })
  })
}
/** 查询列表 */
const getList = async () => {
@@ -268,6 +411,19 @@
    loading.value = false
  }
}
/** 查询所有主机名 */
const getAllHost = async () => {
  const data = await MonitorDiskApi.getAllHost()
  hosts.value = data
}
/** 查询所有ip */
const getAllIp = async () => {
  const data = await MonitorDiskApi.getAllIp()
  ips.value = data
}
/** 搜索按钮操作 */
const handleQuery = () => {
@@ -321,98 +477,26 @@
  }
}
/** 堆叠面积图配置 */
const diskChartOptions = reactive<EChartsOption>({
  title: {
    text: '磁盘空间折线图'
  },
  dataset: {
    dimensions: [],
    source: []
  },
  grid: {
    left: 30,
    right: 20,
    bottom: 10,
    top: 70,
    containLabel: true
  },
  legend: {
    top: 0
  },
  series: [
    {
      name: 'disk', type: 'line',
      emphasis: {
        focus: 'series'
      }, smooth: false
    }
  ],
  toolbox: {
    feature: {
      // 数据区域缩放
      dataZoom: {
        yAxisIndex: false // Y轴不缩放
      },
      brush: {
        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
      },
      saveAsImage: {show: true, name: '物理内存日志图片'} // 保存为图片
    }
  },
  tooltip: {
    trigger: 'axis',
    axisPointer: {
      type: 'cross',
      label: {
        backgroundColor: '#6a7985'
      }
    },
    padding: [5, 10]
  },
  xAxis: {
    type: 'category',
    boundaryGap: false,
    axisTick: {
      show: false
    }
  },
  yAxis: {
    name: "单位(百分比)",
    nameTextStyle: {
      color: "#aaa",
      nameLocation: "start",
    },
  },
}) as EChartsOption
/** 查询统计数据列表 */
const getMonitorDiskDataList = async () => {
  const list = await MonitorDiskApi.getMonitorDiskList(queryParams)
  if (list != null && list != undefined && list.length > 0) {
    diskChartOptions.dataset['dimensions'] = Object.keys(list[0])
    diskChartOptions.series = diskChartOptions.dataset['dimensions'].map(item => ({
      name: item.name,
      type: 'line',
      emphasis: {
        focus: 'series'
      },
      smooth: false
    }));
    diskChartOptions.series.splice(0, 1)
    for (let item of list) {
      item.createTime = formatTime(item.createTime, 'yyyy-MM-dd HH:mm:ss')
    }
  }
  // 更新 Echarts 数据
  diskChartOptions.dataset['source'] = list
  echartsLoading.value = false
  await initCharts(list)
}
const usedDiskInstance = async () => {
  const list = await MonitorDiskApi.getMonitorDiskInfo(queryParams)
  hostList.value = list
  // 仪表盘详情,用于显示数据。
}
/** 封装接口调用 */
const getDataList = async () => {
  if(showType.value == 'data') {
    await getList()
  } else {
    await getMonitorDiskDataList()
    await usedDiskInstance()
  }
}
/** 切换展示方式 */
@@ -430,19 +514,16 @@
/** 初始化 **/
onMounted(() => {
  showType.value = 'data';
  const currentDate = new Date();
  const previousDate = new Date(currentDate);
  previousDate.setDate(currentDate.getDate() - 1);
  queryParams.createTime[0] = formatDate(previousDate, 'YYYY-MM-DD HH:mm:ss');
  queryParams.createTime[1] = formatDate(currentDate, 'YYYY-MM-DD HH:mm:ss');
  getAllHost();
  getAllIp();
  getDataList()
  intervalId = setInterval(() => {
    if (showType.value == 'data') {
      getList()
    } else {
      getMonitorDiskDataList()
      usedDiskInstance()
    }
  }, 30000);
    getDataList()
  }, 300000);
})
onBeforeUnmount(() => {
  cleanCharts()
})
onUnmounted(() => {
@@ -484,4 +565,5 @@
    font-size: 0.9em;
    color: #666;
  }
</style>