潘志宝
2025-02-26 b3a43e63d2c2fa854d676676d3f8072c0d943d13
Merge branch 'master' of http://dlindusit.com:53929/r/iailab-plat-ui-vue3
已修改1个文件
已删除8个文件
已添加10个文件
1663 ■■■■ 文件已修改
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/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/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/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="登录日期">