dengzedong
2025-02-27 c1f166d492e9e1ebbd1be11ee7a46fc125df8464
Merge remote-tracking branch 'origin/master'

# Conflicts:
# src/utils/dict.ts
已修改2个文件
已删除8个文件
已添加15个文件
2209 ■■■■■ 文件已修改
src/api/data/arc/data.ts 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/data/arc/index.ts 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/infra/monitordisk/index.ts 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/infra/monitormem/index.ts 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/member_balance.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/member_expenditure_balance.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/member_level.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/member_point.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/member_recharge_balance.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/money.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/svgs/shopping.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/MonitorDiskPie/PieChart.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/mall/kefu.ts 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/dict.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/data/arc/ArcData.vue 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/data/arc/ArcSettingForm.vue 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/data/arc/index.vue 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/monitor/components/MonitorDisk.vue 487 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/monitor/components/MonitorDiskForm.vue 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/monitor/components/MonitorMem.vue 478 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/monitor/components/MonitorMemForm.vue 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/monitor/components/index.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/monitor/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/infra/storage/index_rec.vue 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/loginlog/LoginLogDetail.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/data/arc/data.ts
对比新文件
@@ -0,0 +1,20 @@
import request from '@/config/axios'
export interface ArcDataVO {
  id: string
  arcId: string,
  value: string,
  arcTime: string,
  createTime: string
}
export interface ArcDataPageReqVO extends PageParam {
  arcId?:string,
  startTime?: Date,
  endTime?: Date,
}
// 查询ArcSetting列表
export const getPage = (params: ArcDataPageReqVO) => {
  return request.get({ url: '/data/da/arc/dataPage', params })
}
src/api/data/arc/index.ts
对比新文件
@@ -0,0 +1,40 @@
import request from '@/config/axios'
export interface ArcSettingVO {
  id: string
  name: string,
  type: string,
  point: string,
  calculate: string,
  isEnable: string
}
export interface ArcSettingPageReqVO extends PageParam {
  name?: string,
  type?: string
}
// 查询ArcSetting列表
export const getArcSettingPage = (params: ArcSettingPageReqVO) => {
  return request.get({ url: '/data/da/arc/page', params })
}
// 查询ArcSetting详情
export const getArcSetting = (id: number) => {
  return request.get({ url: `/data/da/arc/info/${id}`})
}
// 新增ArcSetting
export const createArcSetting = (data: ArcSettingVO) => {
  return request.post({ url: '/data/da/arc/create', data })
}
// 修改ArcSetting
export const updateArcSetting = (data: ArcSettingVO) => {
  return request.put({ url: '/data/da/arc/update', data })
}
// 删除ArcSetting
export const deleteArcSetting = (id: number) => {
  return request.delete({ url: '/data/da/arc/delete?id=' + id })
}
src/api/infra/monitordisk/index.ts
对比新文件
@@ -0,0 +1,57 @@
import request from '@/config/axios'
// 磁盘监控日志 VO
export interface MonitorDiskVO {
  id: number // 访问ID
  hostName: string // 主机名称
  hostIp: string // 服务器ip
  disk: string // 盘符
  diskName: string // 磁盘名
  spaceTotal: number // 总空间
  spaceUsed: number // 已用空间
  spaceUsable: number // 可用空间
  spaceRatio: number // 空间使用比例
}
// 磁盘监控日志 API
export const MonitorDiskApi = {
  // 查询磁盘监控日志分页
  getMonitorDiskPage: async (params: any) => {
    return await request.get({ url: `/infra/monitor-disk/page`, params })
  },
  // 查询磁盘监控日志列表
  getMonitorDiskList: async (params: any) => {
    return await request.get({ url: `/infra/monitor-disk/getMonitorDiskList`, params })
  },
  // 查询磁盘监控日志信息
  getMonitorDiskInfo: async (params: any) => {
    return await request.get({ url: `/infra/monitor-disk/getMonitorDiskInfo`, params })
  },
  // 查询磁盘监控日志详情
  getMonitorDisk: async (id: number) => {
    return await request.get({ url: `/infra/monitor-disk/get?id=` + id })
  },
  // 新增磁盘监控日志
  createMonitorDisk: async (data: MonitorDiskVO) => {
    return await request.post({ url: `/infra/monitor-disk/create`, data })
  },
  // 修改磁盘监控日志
  updateMonitorDisk: async (data: MonitorDiskVO) => {
    return await request.put({ url: `/infra/monitor-disk/update`, data })
  },
  // 删除磁盘监控日志
  deleteMonitorDisk: async (id: number) => {
    return await request.delete({ url: `/infra/monitor-disk/delete?id=` + id })
  },
  // 导出磁盘监控日志 Excel
  exportMonitorDisk: async (params) => {
    return await request.download({ url: `/infra/monitor-disk/export-excel`, params })
  },
}
src/api/infra/monitormem/index.ts
对比新文件
@@ -0,0 +1,56 @@
import request from '@/config/axios'
// 内存监控日志 VO
export interface MonitorMemVO {
  id: number // 访问ID
  hostName: string // 主机名称
  hostIp: string // 服务器ip
  serverName: string // 服务名
  physicalTotal: number // 总物理内存
  physicalUsed: number // 已用物理内存
  physicalFree: number // 剩余物理内存
  physicalUsage: number // 物理内存使用率
  runtimeTotal: number // jvm运行总内存
  runtimeMax: number // jvm最大内存
  runtimeUsed: number // jvm已用内存
  runtimeFree: number // jvm空闲内存
  runtimeUsage: number // jvm内存使用率
}
// 内存监控日志 API
export const MonitorMemApi = {
  // 查询内存监控日志分页
  getMonitorMemPage: async (params: any) => {
    return await request.get({ url: `/infra/monitor-mem/page`, params })
  },
  // 查询统计数据列表
  getMonitorMemList: async (params: any) => {
    return await request.get({ url: `/infra/monitor-mem/getMonitorMemList`, params })
  },
  // 查询内存监控日志详情
  getMonitorMem: async (id: number) => {
    return await request.get({ url: `/infra/monitor-mem/get?id=` + id })
  },
  // 新增内存监控日志
  createMonitorMem: async (data: MonitorMemVO) => {
    return await request.post({ url: `/infra/monitor-mem/create`, data })
  },
  // 修改内存监控日志
  updateMonitorMem: async (data: MonitorMemVO) => {
    return await request.put({ url: `/infra/monitor-mem/update`, data })
  },
  // 删除内存监控日志
  deleteMonitorMem: async (id: number) => {
    return await request.delete({ url: `/infra/monitor-mem/delete?id=` + id })
  },
  // 导出内存监控日志 Excel
  exportMonitorMem: async (params) => {
    return await request.download({ url: `/infra/monitor-mem/export-excel`, params })
  },
}
src/assets/svgs/member_balance.svg
文件已删除
src/assets/svgs/member_expenditure_balance.svg
文件已删除
src/assets/svgs/member_level.svg
文件已删除
src/assets/svgs/member_point.svg
文件已删除
src/assets/svgs/member_recharge_balance.svg
文件已删除
src/assets/svgs/money.svg
文件已删除
src/assets/svgs/shopping.svg
文件已删除
src/components/MonitorDiskPie/PieChart.vue
对比新文件
@@ -0,0 +1,35 @@
<template>
  <svg :width="size" :height="size" viewBox="0 0 100 100">
    <!-- 背景圆 -->
    <circle cx="50" cy="50" r="50" fill="#eee"/>
    <!-- 使用率扇形 -->
    <path :d="arcPath" fill="#1C134B"/>
  </svg>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
  used: { type: Number, required: true },
  total: { type: Number, required: true },
  size: { type: Number, default: 150 }
});
const percentage = computed(() => {
  if (props.total === 0) return 0;
  return (props.used / props.total) * 100;
});
const arcPath = computed(() => {
  if (percentage.value >= 100) return '';
  const angle = (percentage.value * 360) / 100;
  const radians = (angle - 90) * Math.PI / 180;
  const x = 50 + 50 * Math.cos(radians);
  const y = 50 + 50 * Math.sin(radians);
  const largeArc = angle > 180 ? 1 : 0;
  return `M 50 50 L 50 0 A 50 50 0 ${largeArc} 1 ${x} ${y} L 50 50 Z`;
});
</script>
src/store/modules/mall/kefu.ts
对比新文件
@@ -0,0 +1,81 @@
import { store } from '@/store'
import { defineStore } from 'pinia'
import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
import { isEmpty } from '@/utils/is'
interface MallKefuInfoVO {
  conversationList: KeFuConversationRespVO[] // 会话列表
  conversationMessageList: Map<number, KeFuMessageRespVO[]> // 会话消息
}
export const useMallKefuStore = defineStore('mall-kefu', {
  state: (): MallKefuInfoVO => ({
    conversationList: [],
    conversationMessageList: new Map<number, KeFuMessageRespVO[]>() // key 会话,value 会话消息列表
  }),
  getters: {
    getConversationList(): KeFuConversationRespVO[] {
      return this.conversationList
    },
    getConversationMessageList(): (conversationId: number) => KeFuMessageRespVO[] | undefined {
      return (conversationId: number) => this.conversationMessageList.get(conversationId)
    }
  },
  actions: {
    // ======================= 会话消息相关 =======================
    /** 缓存历史消息 */
    saveMessageList(conversationId: number, messageList: KeFuMessageRespVO[]) {
      this.conversationMessageList.set(conversationId, messageList)
    },
    // ======================= 会话相关 =======================
    /** 加载会话缓存列表 */
    async setConversationList() {
      this.conversationList = await KeFuConversationApi.getConversationList()
      this.conversationSort()
    },
    /** 更新会话缓存已读 */
    async updateConversationStatus(conversationId: number) {
      if (isEmpty(this.conversationList)) {
        return
      }
      const conversation = this.conversationList.find((item) => item.id === conversationId)
      conversation && (conversation.adminUnreadMessageCount = 0)
    },
    /** 更新会话缓存 */
    async updateConversation(conversationId: number) {
      if (isEmpty(this.conversationList)) {
        return
      }
      const conversation = await KeFuConversationApi.getConversation(conversationId)
      this.deleteConversation(conversationId)
      conversation && this.conversationList.push(conversation)
      this.conversationSort()
    },
    /** 删除会话缓存 */
    deleteConversation(conversationId: number) {
      const index = this.conversationList.findIndex((item) => item.id === conversationId)
      // 存在则删除
      if (index > -1) {
        this.conversationList.splice(index, 1)
      }
    },
    conversationSort() {
      // 按置顶属性和最后消息时间排序
      this.conversationList.sort((a, b) => {
        // 按照置顶排序,置顶的会在前面
        if (a.adminPinned !== b.adminPinned) {
          return a.adminPinned ? -1 : 1
        }
        // 按照最后消息时间排序,最近的会在前面
        return (b.lastMessageTime as unknown as number) - (a.lastMessageTime as unknown as number)
      })
    }
  }
})
export const useMallKefuStoreWithOut = () => {
  return useMallKefuStore(store)
}
src/utils/dict.ts
@@ -188,5 +188,7 @@
  CAPTURE_TYPE = 'capture_type',
  MODEL_RESULT_TYPE = 'model_result_type',
  DATA_QUALITY = 'data_quality',
  ARC_TYPE = 'arc_type',
  ARC_CALCULATE_TYPE = 'arc_calculate_type',
  SOLIDIFY_FLAG = 'ind_solidify_flag'
}
src/views/data/arc/ArcData.vue
对比新文件
@@ -0,0 +1,148 @@
<template>
  <el-drawer
    v-model="drawer"
    size="60%"
    title="归档数据"
    :direction="direction"
    :before-close="handleClose"
  >
    <!-- 搜索 -->
    <ContentWrap>
      <el-form
        class="-mb-15px"
        :model="queryParams"
        ref="queryFormRef"
        :inline="true"
        label-width="68px"
      >
        <el-form-item label="开始时间">
          <el-date-picker
            v-model="queryParams.startTime"
            format="YYYY-MM-DD HH:mm:00"
            value-format="YYYY-MM-DD HH:mm:00"
            type="datetime"
            :clearable="false"
            placeholder="选择日期时间"/>
        </el-form-item>
        <el-form-item label="结束时间">
          <el-date-picker
            v-model="queryParams.endTime"
            format="YYYY-MM-DD HH:mm:00"
            value-format="YYYY-MM-DD HH:mm:00"
            type="datetime"
            :clearable="false"
            placeholder="选择日期时间"/>
        </el-form-item>
        <el-form-item>
          <el-button @click="getList()">查询</el-button>
        </el-form-item>
      </el-form>
    </ContentWrap>
    <!-- 列表 -->
    <ContentWrap>
      <el-table v-loading="loading" :data="list">
        <el-table-column
          prop="value"
          label="数据值"
          header-align="center"
          align="center"
          min-width="100"
        />
        <el-table-column
          prop="arcTime"
          label="归档时间"
          header-align="center"
          align="center"
          min-width="150"
        />
        <el-table-column
          prop="createTime"
          label="创建时间"
          header-align="center"
          align="center"
        />
      </el-table>
      <!-- 分页 -->
      <Pagination
        :total="total"
        v-model:page="queryParams.pageNo"
        v-model:limit="queryParams.pageSize"
        @pagination="getList"
      />
    </ContentWrap>
  </el-drawer>
</template>
<script lang="ts" setup>
  import type {DrawerProps} from 'element-plus'
  import * as ArcDataApi from "@/api/data/arc/data";
  import {ref} from "vue";
  import {getYMDHM0} from "@/utils/dateUtil";
  defineOptions({name: 'ArcData'})
  const message = useMessage() // 消息弹窗
  const {t} = useI18n() // 国际化
  const drawer = ref(false)
  const direction = ref<DrawerProps['direction']>('rtl')
  const loading = ref(true) // 列表的加载中
  const total = ref(0) // 列表的总页数
  const list = ref([]) // 列表的数据
  const queryParams = reactive({
    pageNo: 1,
    pageSize: 10,
    arcId:undefined,
    startTime: undefined,
    endTime: getYMDHM0(new Date()),
  })
  const queryFormRef = ref() // 搜索的表单
  const exportLoading = ref(false) // 导出的加载中
  /** 查询列表 */
  const getList = async () => {
    loading.value = true
    try {
      const page = await ArcDataApi.getPage(queryParams)
      list.value = page.list
      total.value = page.total
    } finally {
      loading.value = false
    }
  }
  /** 搜索按钮操作 */
  const handleQuery = () => {
    queryParams.pageNo = 1
    getList()
  }
  /** 重置按钮操作 */
  const resetQuery = () => {
    queryFormRef.value.resetFields()
    handleQuery()
  }
  /** 打开弹窗 */
  const open = async (arcId?: string) => {
    resetForm()
    drawer.value = true
    queryParams.arcId = arcId
    if (arcId) {
      getList()
    }
  }
  defineExpose({open}) // 提供 open 方法,用于打开弹窗
  /** 重置表单 */
  const resetForm = () => {
    queryParams.pageNo = 1
    queryParams.pageSize = 10
    queryParams.arcId = ''
    queryParams.startTime = ''
    queryParams.endTime = getYMDHM0(new Date())
  }
  const handleClose = (done: () => void) => {
    drawer.value = false
  }
</script>
src/views/data/arc/ArcSettingForm.vue
对比新文件
@@ -0,0 +1,169 @@
<template>
  <Dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
    <el-form
      ref="formRef"
      v-loading="formLoading"
      :model="formData"
      :rules="formRules"
      label-width="120px"
    >
      <el-form-item label="名称" prop="name">
        <el-input v-model="formData.name" placeholder="请输入归档名称" />
      </el-form-item>
      <el-form-item label="归档周期" prop="type">
        <el-select
          v-model="formData.type"
          clearable
          placeholder="请选择归档周期"
        >
          <el-option
            v-for="dict in getDictOptions(DICT_TYPE.ARC_TYPE)"
            :key="dict.value"
            :label="dict.label"
            :value="dict.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="归档点位" prop="point">
            <el-select
              v-model="formData.point"
              filterable
              placeholder="请选择归档点位">
              <el-option
                v-for="(item, index) in pointList"
                :key="index"
                :label="item.pointName"
                :value="item.pointNo"/>
            </el-select>
      </el-form-item>
      <el-form-item label="计算方法" prop="calculate">
        <el-select
          v-model="formData.calculate"
          clearable
          placeholder="请选择计算方法"
        >
          <el-option
            v-for="dict in getDictOptions(DICT_TYPE.ARC_CALCULATE_TYPE)"
            :key="dict.value"
            :label="dict.label"
            :value="dict.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="是否启用" prop="isEnable">
        <el-select
          v-model="formData.isEnable"
          clearable
          placeholder="请选择是否启用"
        >
          <el-option
            v-for="dict in getIntDictOptions(DICT_TYPE.COM_IS_INT)"
            :key="dict.value"
            :label="dict.label"
            :value="dict.value"
          />
        </el-select>
      </el-form-item>
    </el-form>
    <template #footer>
      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
      <el-button @click="dialogVisible = false">取 消</el-button>
    </template>
  </Dialog>
</template>
<script lang="ts" setup>
  import {DICT_TYPE, getDictOptions, getIntDictOptions} from "@/utils/dict";
  import * as ArcDataApi from '@/api/data/arc'
  import { CommonStatusEnum } from '@/utils/constants'
  import * as DaPoint from "@/api/data/da/point";
  defineOptions({ name: 'ArcSettingForm' })
  const { t } = useI18n() // 国际化
  const message = useMessage() // 消息弹窗
  const dialogVisible = ref(false) // 弹窗的是否展示
  const dialogTitle = ref('') // 弹窗的标题
  const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
  const formType = ref('') // 表单的类型:create - 新增;update - 修改
  const formData = ref({
    id: undefined,
    name: undefined,
    type: undefined,
    point: undefined,
    calculate: undefined,
    isEnable: 1
  })
  const formRules = reactive({
    name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
    type: [{ required: true, message: '归档周期不能为空', trigger: 'blur' }],
    point: [{ required: true, message: '归档点位不能为空', trigger: 'blur' }],
    calculate: [{ required: true, message: '计算方法不能为空', trigger: 'blur' }]
  })
  const formRef = ref() // 表单 Ref
  const pointList = ref([{
    pointName: '',
    pointNo: ''
  }])
  const queryParams = reactive({
    pointTypes: "MEASURE,CONSTANT",
  })
  /** 打开弹窗 */
  const open = async (type: string, id?: number) => {
    dialogVisible.value = true
    dialogTitle.value = t('action.' + type)
    formType.value = type
    resetForm()
    getPointList()
    // 修改时,设置数据
    if (id) {
      formLoading.value = true
      try {
        formData.value = await ArcDataApi.getArcSetting(id)
      } finally {
        formLoading.value = false
      }
    }
  }
  defineExpose({ open }) // 提供 open 方法,用于打开弹窗
  /** 提交表单 */
  const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
  const submitForm = async () => {
    // 校验表单
    if (!formRef) return
    const valid = await formRef.value.validate()
    if (!valid) return
    // 提交请求
    formLoading.value = true
    try {
      const data = formData.value as unknown as ArcDataApi.ArcSettingVO
      if (formType.value === 'create') {
        await ArcDataApi.createArcSetting(data)
        message.success(t('common.createSuccess'))
      } else {
        await ArcDataApi.updateArcSetting(data)
        message.success(t('common.updateSuccess'))
      }
      dialogVisible.value = false
      // 发送操作成功的事件
      emit('success')
    } finally {
      formLoading.value = false
    }
  }
  const getPointList = async () => {
    pointList.value = await DaPoint.getPointSimpleList(queryParams)
  }
  /** 重置表单 */
  const resetForm = () => {
    formData.value = {
      id: undefined,
      name: undefined,
      type: undefined,
      point: undefined,
      calculate: undefined,
      isEnable: 1
    }
    formRef.value?.resetFields()
  }
</script>
src/views/data/arc/index.vue
对比新文件
@@ -0,0 +1,167 @@
<template>
    <!-- 搜索 -->
    <ContentWrap>
        <el-form
                class="-mb-15px"
                :model="queryParams"
                ref="queryFormRef"
                :inline="true"
                label-width="68px"
        >
            <el-form-item label="名称" prop="name">
                <el-input
                        v-model="queryParams.name"
                        placeholder="请输入名称"
                        clearable
                        @keyup.enter="handleQuery"
                        class="!w-240px"
                />
            </el-form-item>
            <el-form-item>
                <el-button @click="handleQuery">
                    <Icon icon="ep:search" class="mr-5px"/>
                    搜索
                </el-button>
                <el-button @click="resetQuery">
                    <Icon icon="ep:refresh" class="mr-5px"/>
                    重置
                </el-button>
                <el-button
                        type="primary"
                        plain
                        @click="openForm('create')"
                >
                    <Icon icon="ep:plus" class="mr-5px"/>
                    新增
                </el-button>
            </el-form-item>
        </el-form>
    </ContentWrap>
    <!-- 列表 -->
    <ContentWrap>
        <el-table v-loading="loading" :data="list">
            <el-table-column label="名称" align="center" prop="name"/>
            <el-table-column label="归档周期" align="center" prop="type"/>
            <el-table-column label="归档点位" align="center" prop="point"/>
            <el-table-column label="计算方法" align="center" prop="calculate"/>
            <el-table-column label="是否启用" align="center" prop="isEnable"/>
            <el-table-column label="操作" align="center" min-width="110" fixed="right">
                <template #default="scope">
                    <el-button
                            link
                            type="primary"
                            @click="openForm('update', scope.row.id)"
                    >
                        编辑
                    </el-button>
                    <el-button
                            link
                            type="primary"
                            @click="openArcData(scope.row.id)"
                    >
                        历史值
                    </el-button>
                    <el-button
                            link
                            type="danger"
                            @click="handleDelete(scope.row.id)"
                    >
                        删除
                    </el-button>
                </template>
            </el-table-column>
        </el-table>
        <!-- 分页 -->
        <Pagination
                :total="total"
                v-model:page="queryParams.pageNo"
                v-model:limit="queryParams.pageSize"
                @pagination="getList"
        />
    </ContentWrap>
    <!-- 表单弹窗:添加/修改 -->
    <ArcSettingForm ref="formRef" @success="getList"/>
    <!-- 历史值弹窗 -->
    <ArcData ref="dataRef"/>
</template>
<script lang="ts" setup>
    import * as ArcSetting from '@/api/data/arc/index'
    import ArcSettingForm from './ArcSettingForm.vue'
    import ArcData from './ArcData.vue'
    defineOptions({name: 'DataArc'})
    const message = useMessage() // 消息弹窗
    const {t} = useI18n() // 国际化
    const loading = ref(true) // 列表的加载中
    const total = ref(0) // 列表的总页数
    const list = ref([]) // 列表的数据
    const queryParams = reactive({
        pageNo: 1,
        pageSize: 10,
        name: undefined,
        type: undefined
    })
    const queryFormRef = ref() // 搜索的表单
    const exportLoading = ref(false) // 导出的加载中
    /** 查询列表 */
    const getList = async () => {
        loading.value = true
        try {
            const page = await ArcSetting.getArcSettingPage(queryParams)
            list.value = page.list
            total.value = page.total
        } finally {
            loading.value = false
        }
    }
    /** 搜索按钮操作 */
    const handleQuery = () => {
        queryParams.pageNo = 1
        getList()
    }
    /** 重置按钮操作 */
    const resetQuery = () => {
        queryFormRef.value.resetFields()
        handleQuery()
    }
    /** 添加/修改操作 */
    const formRef = ref()
    const openForm = (type: string, id?: number) => {
        formRef.value.open(type, id)
    }
    /** 历史操作 */
    const dataRef = ref()
    const openArcData = (id?: string) => {
      dataRef.value.open(id)
    }
    /** 删除按钮操作 */
    const handleDelete = async (id: number) => {
        try {
            // 删除的二次确认
            await message.delConfirm()
            // 发起删除
            await ArcSetting.deleteArcSetting(id)
            message.success(t('common.delSuccess'))
            // 刷新列表
            await getList()
        } catch {
        }
    }
    /** 初始化 **/
    onMounted(async () => {
        await getList()
    })
</script>
src/views/infra/monitor/components/MonitorDisk.vue
对比新文件
@@ -0,0 +1,487 @@
<template>
  <ContentWrap>
    <!-- 搜索工作栏 -->
    <el-form
      class="-mb-15px"
      :model="queryParams"
      ref="queryFormRef"
      :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>
      <!--      <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>
      <el-form-item label="创建时间" prop="createTime">
        <el-date-picker
          v-model="queryParams.createTime"
          value-format="YYYY-MM-DD HH:mm:ss"
          type="datetimerange"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
          class="!w-360px"
        />
      </el-form-item>
      <el-form-item>
        <el-button @click="handleQuery">
          <Icon icon="ep:search" class="mr-5px"/>
          搜索
        </el-button>
        <el-button @click="resetQuery">
          <Icon icon="ep:refresh" class="mr-5px"/>
          重置
        </el-button>
        <el-button
          type="primary"
          plain
          @click="openForm('create')"
          v-hasPermi="['infra:monitor-disk:create']"
        >
          <Icon icon="ep:plus" class="mr-5px"/>
          新增
        </el-button>
        <el-button
          type="success"
          plain
          @click="handleExport"
          :loading="exportLoading"
          v-hasPermi="['infra:monitor-disk:export']"
        >
          <Icon icon="ep:download" class="mr-5px"/>
          导出
        </el-button>
      </el-form-item>
      <el-form-item style="float: right">
        <el-button
          v-if="showType == 'chart'"
          type="warning"
          style="font-weight: bold"
          plain
          @click="switchShow('data')">
          <Icon icon="fa-solid:th-list" class="mr-5px"/>
          列表展示
        </el-button>
        <el-button
          v-if="showType == 'data'"
          type="danger"
          style="font-weight: bold"
          plain
          @click="switchShow('chart')">
          <Icon icon="fa-solid:chart-pie" class="mr-5px"/>
          图例展示
        </el-button>
      </el-form-item>
    </el-form>
  </ContentWrap>
  <ContentWrap v-if="showType == 'chart'">
    <!-- 磁盘使用率折线图 -->
    <el-skeleton :loading="echartsLoading" animated>
      <Echart :height="320" :options="diskChartOptions"/>
    </el-skeleton>
    <!-- 磁盘使用率饼图 -->
    <h3 style="margin-top: 20px; margin-bottom: 10px">主机磁盘使用率</h3>
    <div v-for="host in hostList" :key="host.name" class="host">
      <div class="host-child">
        <h4>主机名:{{ host.name }}&nbsp;&nbsp;&nbsp;&nbsp;主机IP:{{ host.ip }}</h4>
        <el-skeleton :loading="echartsLoading" animated>
          <div class="disks">
            <div v-for="disk in host.disks" :key="disk.name" class="disk">
              <h4 id="diskTitle">{{ disk.disk }}</h4>
              <PieChart :used="disk.used" :total="disk.total" />
              <div class="disk-info">
                <div style="margin-bottom: 6px; font-size: 16px"><span style="color: #b9292b ;font-weight: bolder">{{ disk.total != 0 ? ((disk.used / disk.total) * 100).toFixed(1) : 0.0 }}% </span>已使用</div>
                <div style="font-weight: bolder">{{ disk.used }}GB / {{ disk.total }}GB</div>
              </div>
            </div>
          </div>
        </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>
  <!-- 列表 -->
  <ContentWrap v-if="showType == 'data'">
    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
      <el-table-column label="主机名称" align="center" prop="hostName"/>
      <el-table-column label="服务器ip" align="center" prop="hostIp"/>
      <el-table-column label="盘符" align="center" prop="disk"/>
      <el-table-column label="磁盘名" align="center" prop="diskName"/>
      <el-table-column label="总空间" align="center" prop="spaceTotal"/>
      <el-table-column label="已用空间" align="center" prop="spaceUsed"/>
      <el-table-column label="可用空间" align="center" prop="spaceUsable"/>
      <el-table-column label="空间使用比例" align="center" prop="spaceRatio"/>
      <el-table-column
        label="创建时间"
        align="center"
        prop="createTime"
        :formatter="dateFormatter"
        width="180px"
      />
      <el-table-column label="操作" align="center">
        <template #default="scope">
          <el-button
            link
            type="primary"
            @click="openForm('update', scope.row.id)"
            v-hasPermi="['infra:monitor-disk:query']"
          >
            详情
          </el-button>
          <el-button
            link
            type="danger"
            @click="handleDelete(scope.row.id)"
            v-hasPermi="['infra:monitor-disk:delete']"
          >
            删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- 分页 -->
    <Pagination
      :total="total"
      v-model:page="queryParams.pageNo"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />
  </ContentWrap>
  <!-- 表单弹窗:添加/修改 -->
  <MonitorDiskForm ref="formRef" @success="getList"/>
</template>
<script setup lang="ts">
import {dateFormatter} from '@/utils/formatTime'
import download from '@/utils/download'
import {MonitorDiskApi, MonitorDiskVO} from '@/api/infra/monitordisk'
import MonitorDiskForm from './MonitorDiskForm.vue'
import {EChartsOption} from "echarts";
import * as echarts from 'echarts';
import {formatTime} from "@/utils";
import {formatDate} from "@vueuse/core";
import PieChart from '@/components/MonitorDiskPie/PieChart.vue';
/** 磁盘监控日志 列表 */
defineOptions({name: 'MonitorDisk'})
const message = useMessage() // 消息弹窗
const {t} = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<MonitorDiskVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
  pageNo: 1,
  pageSize: 10,
  hostName: undefined,
  hostIp: undefined,
  disk: undefined,
  diskName: undefined,
  spaceTotal: undefined,
  spaceUsed: undefined,
  spaceUsable: undefined,
  spaceRatio: undefined,
  createTime: [],
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
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 chartRefs = ref([]);
/** 查询列表 */
const getList = async () => {
  loading.value = true
  try {
    const data = await MonitorDiskApi.getMonitorDiskPage(queryParams)
    list.value = data.list
    total.value = data.total
  } finally {
    loading.value = false
  }
}
/** 搜索按钮操作 */
const handleQuery = () => {
  queryParams.pageNo = 1
  if (showType.value == 'data') {
    getList()
  } else {
    getMonitorDiskDataList()
    usedDiskInstance()
  }
}
/** 重置按钮操作 */
const resetQuery = () => {
  queryFormRef.value.resetFields()
  handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
  formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
  try {
    // 删除的二次确认
    await message.delConfirm()
    // 发起删除
    await MonitorDiskApi.deleteMonitorDisk(id)
    message.success(t('common.delSuccess'))
    // 刷新列表
    await getList()
  } catch {
  }
}
/** 导出按钮操作 */
const handleExport = async () => {
  try {
    // 导出的二次确认
    await message.exportConfirm()
    // 发起导出
    exportLoading.value = true
    const data = await MonitorDiskApi.exportMonitorDisk(queryParams)
    download.excel(data, '磁盘监控日志.xls')
  } catch {
  } finally {
    exportLoading.value = false
  }
}
/** 堆叠面积图配置 */
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
}
const usedDiskInstance = async () => {
  const list = await MonitorDiskApi.getMonitorDiskInfo(queryParams)
  hostList.value = list
  // 仪表盘详情,用于显示数据。
}
/** 切换展示方式 */
const switchShow = (type: String) => {
  showType.value = type
  if (showType.value == 'data') {
    getList()
  } else {
    getMonitorDiskDataList()
    usedDiskInstance()
  }
}
let intervalId;
/** 初始化 **/
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');
  intervalId = setInterval(() => {
    if (showType.value == 'data') {
      getList()
    } else {
      getMonitorDiskDataList()
      usedDiskInstance()
    }
  }, 30000);
})
onUnmounted(() => {
  clearInterval(intervalId);
});
</script>
<style>
  .host {
    margin-bottom: 20px;
    margin-right: 20px;
    border-radius: 8px;
  }
  .host-child {
    background: rgba(200, 200, 200, 0.3);
    border-radius: 8px;
    padding: 10px;
  }
  .disks {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
    gap: 20px;
    margin-top: 20px;
  }
  .disk {
    width: 250px;
    background: rgba(100, 100, 150, 0.2);
    padding: 15px;
    border-radius: 16px;
    text-align: center;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  }
  #diskTitle {
    margin-bottom: 10px
  }
  .disk-info {
    margin-top: 10px;
    font-size: 0.9em;
    color: #666;
  }
</style>
src/views/infra/monitor/components/MonitorDiskForm.vue
对比新文件
@@ -0,0 +1,128 @@
<template>
  <Dialog :title="dialogTitle" v-model="dialogVisible">
    <el-form
      ref="formRef"
      :model="formData"
      :rules="formRules"
      label-width="100px"
      v-loading="formLoading"
    >
      <el-form-item label="主机名称" prop="hostName">
        <el-input v-model="formData.hostName" placeholder="请输入主机名称" />
      </el-form-item>
      <el-form-item label="服务器ip" prop="hostIp">
        <el-input v-model="formData.hostIp" placeholder="请输入服务器ip" />
      </el-form-item>
      <el-form-item label="盘符" prop="disk">
        <el-input v-model="formData.disk" placeholder="请输入盘符" />
      </el-form-item>
      <el-form-item label="磁盘名" prop="diskName">
        <el-input v-model="formData.diskName" placeholder="请输入磁盘名" />
      </el-form-item>
      <el-form-item label="总空间" prop="spaceTotal">
        <el-input v-model="formData.spaceTotal" placeholder="请输入总空间" />
      </el-form-item>
      <el-form-item label="已用空间" prop="spaceUsed">
        <el-input v-model="formData.spaceUsed" placeholder="请输入已用空间" />
      </el-form-item>
      <el-form-item label="可用空间" prop="spaceUsable">
        <el-input v-model="formData.spaceUsable" placeholder="请输入可用空间" />
      </el-form-item>
      <el-form-item label="空间使用比例" prop="spaceRatio">
        <el-input v-model="formData.spaceRatio" placeholder="请输入空间使用比例" />
      </el-form-item>
    </el-form>
    <template #footer>
      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
      <el-button @click="dialogVisible = false">取 消</el-button>
    </template>
  </Dialog>
</template>
<script setup lang="ts">
import { MonitorDiskApi, MonitorDiskVO } from '@/api/infra/monitordisk'
/** 磁盘监控日志 表单 */
defineOptions({ name: 'MonitorDiskForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
  id: undefined,
  hostName: undefined,
  hostIp: undefined,
  disk: undefined,
  diskName: undefined,
  spaceTotal: undefined,
  spaceUsed: undefined,
  spaceUsable: undefined,
  spaceRatio: undefined,
})
const formRules = reactive({
  hostName: [{ required: true, message: '主机名称不能为空', trigger: 'blur' }],
  hostIp: [{ required: true, message: '服务器ip不能为空', trigger: 'blur' }],
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
  dialogVisible.value = true
  dialogTitle.value = t('action.' + type)
  formType.value = type
  resetForm()
  // 修改时,设置数据
  if (id) {
    formLoading.value = true
    try {
      formData.value = await MonitorDiskApi.getMonitorDisk(id)
    } finally {
      formLoading.value = false
    }
  }
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
  // 校验表单
  await formRef.value.validate()
  // 提交请求
  formLoading.value = true
  try {
    const data = formData.value as unknown as MonitorDiskVO
    if (formType.value === 'create') {
      await MonitorDiskApi.createMonitorDisk(data)
      message.success(t('common.createSuccess'))
    } else {
      await MonitorDiskApi.updateMonitorDisk(data)
      message.success(t('common.updateSuccess'))
    }
    dialogVisible.value = false
    // 发送操作成功的事件
    emit('success')
  } finally {
    formLoading.value = false
  }
}
/** 重置表单 */
const resetForm = () => {
  formData.value = {
    id: undefined,
    hostName: undefined,
    hostIp: undefined,
    disk: undefined,
    diskName: undefined,
    spaceTotal: undefined,
    spaceUsed: undefined,
    spaceUsable: undefined,
    spaceRatio: undefined,
  }
  formRef.value?.resetFields()
}
</script>
src/views/infra/monitor/components/MonitorMem.vue
对比新文件
@@ -0,0 +1,478 @@
<template>
  <ContentWrap>
    <!-- 搜索工作栏 -->
    <el-form
      class="-mb-15px"
      :model="queryParams"
      ref="queryFormRef"
      :inline="true"
      label-width="68px"
    >
<!--      <el-form-item label="主机名称" prop="hostName">-->
<!--        <el-input-->
<!--          v-model="queryParams.hostName"-->
<!--          placeholder="请输入主机名称"-->
<!--          clearable-->
<!--          @keyup.enter="handleQuery"-->
<!--          class="!w-120px"-->
<!--        />-->
<!--      </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>
      <el-form-item label="服务名" prop="serverName">
        <el-input
          v-model="queryParams.serverName"
          placeholder="请输入服务名"
          clearable
          @keyup.enter="handleQuery"
          class="!w-120px"
        />
      </el-form-item>
      <el-form-item label="创建时间" prop="createTime">
        <el-date-picker
          v-model="queryParams.createTime"
          value-format="YYYY-MM-DD HH:mm:ss"
          type="datetimerange"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
          class="!w-360px"
        />
      </el-form-item>
      <el-form-item>
        <el-button @click="handleQuery">
          <Icon icon="ep:search" class="mr-5px"/>
          搜索
        </el-button>
        <el-button @click="resetQuery">
          <Icon icon="ep:refresh" class="mr-5px"/>
          重置
        </el-button>
        <el-button
          type="primary"
          plain
          @click="openForm('create')"
          v-hasPermi="['infra:monitor-mem:create']"
        >
          <Icon icon="ep:plus" class="mr-5px"/>
          新增
        </el-button>
        <el-button
          type="success"
          plain
          @click="handleExport"
          :loading="exportLoading"
          v-hasPermi="['infra:monitor-mem:export']"
        >
          <Icon icon="ep:download" class="mr-5px"/>
          导出
        </el-button>
      </el-form-item>
      <el-form-item style="float: right">
        <el-button
          v-if="showType == 'chart'"
          type="warning"
          style="font-weight: bold"
          plain
          @click="switchShow('data')">
          <Icon icon="fa-solid:th-list" class="mr-5px"/>
          列表展示
        </el-button>
        <el-button
          v-if="showType == 'data'"
          type="danger"
          style="font-weight: bold"
          plain
          @click="switchShow('chart')">
          <Icon icon="fa-solid:chart-pie" class="mr-5px"/>
          图例展示
        </el-button>
      </el-form-item>
    </el-form>
  </ContentWrap>
  <ContentWrap v-if="showType == 'chart'">
    <!-- 物理内存折线图 -->
    <el-skeleton :loading="echartsLoading" animated>
      <Echart :height="320" :options="physicalChartOptions"/>
    </el-skeleton>
    <!-- JVM内存折线图 -->
    <el-skeleton :loading="echartsLoading" animated>
      <Echart style="margin-top: 20px" :height="320" :options="JVMChartOptions"/>
    </el-skeleton>
  </ContentWrap>
  <!-- 列表 -->
  <ContentWrap v-if="showType == 'data'">
    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
      <el-table-column label="主机名称" align="center" prop="hostName"/>
      <el-table-column label="服务器ip" align="center" prop="hostIp"/>
      <el-table-column label="服务名" align="center" prop="serverName" width="120"/>
      <el-table-column label="总内存" align="center" prop="physicalTotal"/>
      <el-table-column label="已用内存" align="center" prop="physicalUsed"/>
      <el-table-column label="空闲内存" align="center" prop="physicalFree"/>
      <el-table-column label="内存使用率" align="center" prop="physicalUsage" width="100"/>
      <el-table-column label="JVM占用内存" align="center" prop="runtimeTotal"/>
      <el-table-column label="JVM最大内存" align="center" prop="runtimeMax"/>
      <el-table-column label="JVM可用内存" align="center" prop="runtimeUsed"/>
      <el-table-column label="JVM空闲内存" align="center" prop="runtimeFree"/>
      <el-table-column label="JVM内存使用率" align="center" prop="runtimeUsage"/>
      <el-table-column
        label="创建时间"
        align="center"
        prop="createTime"
        :formatter="dateFormatter"
        width="180px"
      />
      <el-table-column label="操作" align="center">
        <template #default="scope">
          <el-button
            link
            type="primary"
            @click="openForm('update', scope.row.id)"
            v-hasPermi="['infra:monitor-mem:query']"
          >
            详情
          </el-button>
          <el-button
            link
            type="danger"
            @click="handleDelete(scope.row.id)"
            v-hasPermi="['infra:monitor-mem:delete']"
          >
            删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <!-- 分页 -->
    <Pagination
      :total="total"
      v-model:page="queryParams.pageNo"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />
  </ContentWrap>
  <!-- 表单弹窗:添加/修改 -->
  <MonitorMemForm ref="formRef" @success="getList"/>
</template>
<script setup lang="ts">
import {dateFormatter} from '@/utils/formatTime'
import download from '@/utils/download'
import {MonitorMemApi, MonitorMemVO} from '@/api/infra/monitormem'
import MonitorMemForm from './MonitorMemForm.vue'
import {EChartsOption} from "echarts";
import {formatTime} from "@/utils";
import {formatDate} from "@vueuse/core";
/** 内存监控日志 列表 */
defineOptions({name: 'MonitorMem'})
const message = useMessage() // 消息弹窗
const {t} = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<MonitorMemVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
  pageNo: 1,
  pageSize: 10,
  hostName: undefined,
  hostIp: undefined,
  serverName: undefined,
  physicalTotal: undefined,
  physicalUsed: undefined,
  physicalFree: undefined,
  physicalUsage: undefined,
  runtimeTotal: undefined,
  runtimeMax: undefined,
  runtimeUsed: undefined,
  runtimeFree: undefined,
  runtimeUsage: undefined,
  createTime: [],
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
const echartsLoading = ref(true) // 图表加载中
const showType = ref() //展示类型(chart-图例,data-数据)
/** 查询列表 */
const getList = async () => {
  loading.value = true
  try {
    const data = await MonitorMemApi.getMonitorMemPage(queryParams)
    list.value = data.list
    total.value = data.total
  } finally {
    loading.value = false
  }
}
/** 搜索按钮操作 */
const handleQuery = () => {
  queryParams.pageNo = 1
  if(showType.value == 'data') {
    getList()
  } else {
    getMonitorMemDataList()
  }
}
/** 重置按钮操作 */
const resetQuery = () => {
  queryFormRef.value.resetFields()
  handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
  formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
  try {
    // 删除的二次确认
    await message.delConfirm()
    // 发起删除
    await MonitorMemApi.deleteMonitorMem(id)
    message.success(t('common.delSuccess'))
    // 刷新列表
    await getList()
  } catch {
  }
}
/** 堆叠面积图配置 */
const physicalChartOptions = reactive<EChartsOption>({
  title: {
    text: '物理内存折线图'
  },
  dataset: {
    dimensions: ['createTime', 'physicalTotal', 'physicalUsed', 'physicalFree'],
    source: []
  },
  grid: {
    left: 20,
    right: 20,
    bottom: 10,
    top: 70,
    containLabel: true
  },
  legend: {
    top: 0
  },
  series: [
    {
      name: '总物理内存', type: 'line',
      emphasis: {
        focus: 'series'
      }, smooth: false
    },
    {
      name: '已用物理内存', type: 'line', areaStyle: {},
      emphasis: {
        focus: 'series'
      }, smooth: false
    },
    {
      name: '剩余物理内存', type: 'line', areaStyle: {},
      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: "单位(MB)",
    nameTextStyle: {
      color: "#aaa",
      nameLocation: "start",
    },
  },
}) as EChartsOption
/** 堆叠面积图配置 */
const JVMChartOptions = reactive<EChartsOption>({
  title: {
    text: 'JVM内存折线图'
  },
  dataset: {
    dimensions: ['createTime', 'runtimeMax', 'runtimeTotal', 'runtimeUsed', 'runtimeFree'],
    source: []
  },
  grid: {
    left: 20,
    right: 20,
    bottom: 0,
    top: 70,
    containLabel: true
  },
  legend: {
    top: 0
  },
  series: [
    {
      name: 'JVM最大内存', type: 'line',
      emphasis: {
        focus: 'series'
      }, smooth: false
    },
    {
      name: 'JVM占用内存', type: 'line',
      emphasis: {
        focus: 'series'
      }, smooth: false
    },
    {
      name: 'JVM可用内存', type: 'line', areaStyle: {},
      emphasis: {
        focus: 'series'
      }, smooth: false
    },
    {
      name: 'JVM空闲内存', type: 'line', areaStyle: {},
      emphasis: {
        focus: 'series'
      }, smooth: false
    }
  ],
  toolbox: {
    feature: {
      // 数据区域缩放
      dataZoom: {
        yAxisIndex: false // Y轴不缩放
      },
      brush: {
        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
      },
      saveAsImage: {show: true, name: 'JVM内存日志图片'} // 保存为图片
    }
  },
  tooltip: {
    trigger: 'axis',
    axisPointer: {
      type: 'cross',
      label: {
        backgroundColor: '#6a7985'
      }
    },
    padding: [5, 10]
  },
  xAxis: {
    type: 'category',
    boundaryGap: false,
    axisTick: {
      show: false
    }
  },
  yAxis: {
    name: "单位(MB)",
    nameTextStyle: {
      color: "#aaa",
      nameLocation: "start",
    },
  },
}) as EChartsOption
/** 查询统计数据列表 */
const getMonitorMemDataList = async () => {
  const list = await MonitorMemApi.getMonitorMemList(queryParams)
  for (let item of list) {
    item.createTime = formatTime(item.createTime, 'yyyy-MM-dd HH:mm:ss')
  }
  // 更新 Echarts 数据
  if (physicalChartOptions.dataset && physicalChartOptions.dataset['source']) {
    physicalChartOptions.dataset['source'] = list
  }
  if (JVMChartOptions.dataset && JVMChartOptions.dataset['source']) {
    JVMChartOptions.dataset['source'] = list
  }
  echartsLoading.value = false
}
/** 切换展示方式 */
const switchShow = (type: String) => {
  showType.value = type
  if(showType.value == 'data') {
    getList()
  } else {
    getMonitorMemDataList()
  }
}
/** 导出按钮操作 */
const handleExport = async () => {
  try {
    // 导出的二次确认
    await message.exportConfirm()
    // 发起导出
    exportLoading.value = true
    const data = await MonitorMemApi.exportMonitorMem(queryParams)
    download.excel(data, '内存监控日志.xls')
  } catch {
  } finally {
    exportLoading.value = false
  }
}
let intervalId;
/** 初始化 **/
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');
  intervalId = setInterval(() => {
    if(showType.value == 'data') {
      getList()
    } else {
      getMonitorMemDataList()
    }
  }, 60000);
})
onUnmounted(() => {
  clearInterval(intervalId);
});
</script>
src/views/infra/monitor/components/MonitorMemForm.vue
对比新文件
@@ -0,0 +1,148 @@
<template>
  <Dialog :title="dialogTitle" v-model="dialogVisible">
    <el-form
      ref="formRef"
      :model="formData"
      :rules="formRules"
      label-width="100px"
      v-loading="formLoading"
    >
      <el-form-item label="主机名称" prop="hostName">
        <el-input v-model="formData.hostName" placeholder="请输入主机名称" />
      </el-form-item>
      <el-form-item label="服务器ip" prop="hostIp">
        <el-input v-model="formData.hostIp" placeholder="请输入服务器ip" />
      </el-form-item>
      <el-form-item label="服务名" prop="serverName">
        <el-input v-model="formData.serverName" placeholder="请输入服务名" />
      </el-form-item>
      <el-form-item label="总内存" prop="physicalTotal">
        <el-input v-model="formData.physicalTotal" placeholder="请输入总物理内存" />
      </el-form-item>
      <el-form-item label="已用内存" prop="physicalUsed">
        <el-input v-model="formData.physicalUsed" placeholder="请输入已用物理内存" />
      </el-form-item>
      <el-form-item label="空闲内存" prop="physicalFree">
        <el-input v-model="formData.physicalFree" placeholder="请输入空闲内存" />
      </el-form-item>
      <el-form-item label="内存使用率" prop="physicalUsage">
        <el-input v-model="formData.physicalUsage" placeholder="请输入物理内存使用率" />
      </el-form-item>
      <el-form-item label="jvm运行总内存" prop="runtimeTotal">
        <el-input v-model="formData.runtimeTotal" placeholder="请输入jvm运行总内存" />
      </el-form-item>
      <el-form-item label="jvm最大内存" prop="runtimeMax">
        <el-input v-model="formData.runtimeMax" placeholder="请输入jvm最大内存" />
      </el-form-item>
      <el-form-item label="jvm已用内存" prop="runtimeUsed">
        <el-input v-model="formData.runtimeUsed" placeholder="请输入jvm已用内存" />
      </el-form-item>
      <el-form-item label="jvm空闲内存" prop="runtimeFree">
        <el-input v-model="formData.runtimeFree" placeholder="请输入jvm空闲内存" />
      </el-form-item>
      <el-form-item label="jvm内存使用率" prop="runtimeUsage">
        <el-input v-model="formData.runtimeUsage" placeholder="请输入jvm内存使用率" />
      </el-form-item>
    </el-form>
    <template #footer>
      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
      <el-button @click="dialogVisible = false">取 消</el-button>
    </template>
  </Dialog>
</template>
<script setup lang="ts">
import { MonitorMemApi, MonitorMemVO } from '@/api/infra/monitormem'
/** 内存监控日志 表单 */
defineOptions({ name: 'MonitorMemForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
  id: undefined,
  hostName: undefined,
  hostIp: undefined,
  serverName: undefined,
  physicalTotal: undefined,
  physicalUsed: undefined,
  physicalFree: undefined,
  physicalUsage: undefined,
  runtimeTotal: undefined,
  runtimeMax: undefined,
  runtimeUsed: undefined,
  runtimeFree: undefined,
  runtimeUsage: undefined,
})
const formRules = reactive({
  hostName: [{ required: true, message: '主机名称不能为空', trigger: 'blur' }],
  hostIp: [{ required: true, message: '服务器ip不能为空', trigger: 'blur' }],
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
  dialogVisible.value = true
  dialogTitle.value = t('action.' + type)
  formType.value = type
  resetForm()
  // 修改时,设置数据
  if (id) {
    formLoading.value = true
    try {
      formData.value = await MonitorMemApi.getMonitorMem(id)
    } finally {
      formLoading.value = false
    }
  }
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
  // 校验表单
  await formRef.value.validate()
  // 提交请求
  formLoading.value = true
  try {
    const data = formData.value as unknown as MonitorMemVO
    if (formType.value === 'create') {
      await MonitorMemApi.createMonitorMem(data)
      message.success(t('common.createSuccess'))
    } else {
      await MonitorMemApi.updateMonitorMem(data)
      message.success(t('common.updateSuccess'))
    }
    dialogVisible.value = false
    // 发送操作成功的事件
    emit('success')
  } finally {
    formLoading.value = false
  }
}
/** 重置表单 */
const resetForm = () => {
  formData.value = {
    id: undefined,
    hostName: undefined,
    hostIp: undefined,
    serverName: undefined,
    physicalTotal: undefined,
    physicalUsed: undefined,
    physicalFree: undefined,
    physicalUsage: undefined,
    runtimeTotal: undefined,
    runtimeMax: undefined,
    runtimeUsed: undefined,
    runtimeFree: undefined,
    runtimeUsage: undefined,
  }
  formRef.value?.resetFields()
}
</script>
src/views/infra/monitor/components/index.ts
对比新文件
@@ -0,0 +1,3 @@
import MonitorMem from './MonitorMem.vue'
import MonitorDisk from './MonitorDisk.vue'
export { MonitorMem, MonitorDisk }
src/views/infra/monitor/index.vue
对比新文件
@@ -0,0 +1,20 @@
<template>
  <ContentWrap>
    <el-tabs v-model="activeName">
      <el-tab-pane label="内存监控日志" name="monitorMem">
        <monitor-mem ref="memInfoRef" />
      </el-tab-pane>
      <el-tab-pane label="硬盘监控日志" name="colum">
        <monitor-disk ref="diskInfoRef" />
      </el-tab-pane>
    </el-tabs>
  </ContentWrap>
</template>
<script lang="ts" setup>
import { MonitorMem, MonitorDisk } from './components'
defineOptions({ name: 'InfraMonitor' })
const activeName = ref('monitorMem') // Tag 激活的窗口
</script>
src/views/infra/storage/index_rec.vue
文件已删除
src/views/system/loginlog/LoginLogDetail.vue
@@ -16,7 +16,7 @@
      <el-descriptions-item label="浏览器">
        {{ detailData.userAgent }}
      </el-descriptions-item>
      <el-descriptions-item label="登陆结果">
      <el-descriptions-item label="登录结果">
        <dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_RESULT" :value="detailData.result" />
      </el-descriptions-item>
      <el-descriptions-item label="登录日期">