ソースを参照

feat: v4版本 日志管理

qindao 1 年間 前
コミット
776aa085ec

+ 87 - 0
src/api/system/log.ts

@@ -0,0 +1,87 @@
+import { http } from "@/utils/http/axios";
+import { LoginLogPageRequest, LoginLogRespose, OperationLogRespose, OperatinoLogPageRequest, SystemLogRequest, SystemLogRespose, OpretionPageDetail  } from "@/types/log/type";
+
+/**
+  *@description: V4: 获取登录日志列表
+*/
+export function queryLoginLogList(data: LoginLogPageRequest) {
+  return http.request<LoginLogRespose>({
+    url: '/admin/log/queryLoginLogPage',
+    method: 'post',
+    data: data
+  })
+}
+
+/** 
+ * @description: @v4:登录日志导出 admin/log/exportLoginLog
+ */
+export function exportLoginLog(data: LoginLogPageRequest) {
+  return http.request({
+    url: '/admin/log/exportLoginLog',
+    method: 'post',
+    responseType: 'blob',
+    data
+  },
+  { isTransformResponse: false }
+  )
+}
+
+/**
+  *@description: V4: 获取操作日志列表
+*/
+export function queryOperationLogList(data: OperatinoLogPageRequest) {
+  return http.request<OperationLogRespose>({
+    url: '/admin/log/queryLogPage',
+    method: 'post',
+    data: data
+  })
+}
+
+/** 
+ * @description: @v4:操作日志导出 admin/log/exportLog
+ */
+export function exportOperationLog(data: OperatinoLogPageRequest) {
+  return http.request({
+    url: '/admin/log/exportLog',
+    method: 'post',
+    responseType: 'blob',
+    data
+  },
+  { isTransformResponse: false }
+  )
+}
+
+/**
+  *@description: V4: 获取操作日志详情
+*/
+export function queryOperationLogDetail(id: number) {
+  return http.request<OpretionPageDetail>({
+    url: `/admin/log/queryLogDetail?id=${id}`,
+    method: 'get',
+  })
+}
+
+/**
+  *@description: V4: 获取系统日志列表
+*/
+export function querySystemLogList(data: SystemLogRequest) {
+  return http.request<SystemLogRespose>({
+    url: '/admin/log/queryEventLogPage',
+    method: 'post',
+    data: data
+  })
+}
+
+/** 
+ * @description: @v4:系统日志导出 admin/log/exportEventLog
+ */
+export function exportSystemLog(data: SystemLogRequest) {
+  return http.request({
+    url: '/admin/log/exportEventLog',
+    method: 'post',
+    responseType: 'blob',
+    data
+  },
+  { isTransformResponse: false }
+  )
+}

+ 10 - 0
src/router/full-routes.ts

@@ -525,6 +525,16 @@ const fullRoutes: AppRouteRecordRaw[] = [
           title: '字典管理',
         }
       },
+      {
+        // 系统管理
+        path: 'log',
+        name: 'SystemLog',
+        component: '/system/log/log',
+        meta: { 
+          icon: '',
+          title: '日志管理',
+        }
+      },
     ]
   },
 

+ 173 - 0
src/types/log/constants.ts

@@ -0,0 +1,173 @@
+/* 日志管理tab数据 */
+export const logTabsList = [
+  {
+    value: 1,
+    label: '登录日志'
+  },
+  {
+    value: 2,
+    label: '系统运行日志'
+  },
+  {
+    value: 3,
+    label: '操作日志'
+  },
+];
+
+/* 日志管理tab枚举 */
+export enum LogTabEnum {
+  LOGIN = 1,
+  SYSTEM = 2,
+  OPERATION = 3,
+}
+
+/* 设备类型 */
+export const deviceList = [
+  {
+    value: 'PC',
+    label: '电脑'    
+  },
+  {
+    value: 'Mobile',
+    label: '移动端'    
+  },
+]
+
+/* 平台类型 */
+export const platformList = [
+   {
+    value: 'FRONT',
+    label: '平台侧'    
+  },
+  {
+    value: 'ADMIN',
+    label: '管理侧'    
+  },
+]
+
+export interface OptionsProps {
+  value: string | number,
+  label: string
+}
+
+/* 日志管理操作类型 */
+export enum OperationType{
+  QUERY = '查询',
+  INSERT = '新增',
+  UPDATE = '修改',
+  DELETE='删除',
+  GRANT = '授杈',
+  EXPORT = '导出',
+  SWITCH = '开启/关闭',
+  IMPORT = '导入',
+  LOGIN = '登录',
+  LOGOUT = '登出',
+  URGE = '加急'
+}
+
+/* 日志管理操作类型 */
+export const operationList = [
+  {
+    value: 'QUERY',
+    label: '查询'    
+  },
+  {
+    value: 'INSERT',
+    label: '新增'    
+  },
+  {
+    value: 'UPDATE',
+    label: '修改'    
+  },
+  {
+    value: 'DELETE',
+    label: '删除'    
+  },
+  {
+    value: 'GRANT',
+    label: '授杈'    
+  },
+  {
+    value: 'EXPORT',
+    label: '导出'    
+  },
+  {
+    value: 'SWITCH',
+    label: '开启/关闭'    
+  },
+  {
+    value: 'IMPORT',
+    label: '导入'    
+  },
+  {
+    value: 'LOGOUT',
+    label: '登出'    
+  },
+  {
+    value: 'LOGIN',
+    label: '登录'    
+  },
+  {
+    value: 'URGE',
+    label: '加急'    
+  },
+]
+
+// 事件类型 1-相机上线 2-相机下线 3-系统重启'
+export enum EventTypeEnm {
+  CAMERA_ONINE = 1,
+  CAMERA_OFF = 2,
+  SYSTEM_RESET = 3
+}
+
+/* 事件类型 */
+export const eventList = [
+  {
+    value: 1,
+    label: '相机上线'    
+  },
+  {
+    value: 2,
+    label: '相机下线'    
+  },
+  {
+    value: 3,
+    label: '系统重启'    
+  },
+]
+
+export const eventTypeArr = {
+  [EventTypeEnm.CAMERA_ONINE]: '相机上线',
+  [EventTypeEnm.CAMERA_OFF]: '相机下线',
+  [EventTypeEnm.SYSTEM_RESET]: '系统重启',
+}
+
+/* 搜索项下拉选择数据 */
+export const queryTypeSelect = [
+  {
+    value: 'realname',
+    label: '姓名',
+  },
+  {
+    value: 'account',
+    label: '账号',
+  },
+];
+
+/* 操作结果枚举 */
+export enum OperationTypeEnum {
+  LOGIN = 1,
+  LOGOUT = 2
+}
+
+/* 登录状态枚举 */
+export enum LoginStatusEnum {
+  FAIL = 0,
+  SUCCESS = 1
+}
+
+/* 客户端枚举 */
+export enum ClientTypeEnum {
+  PC = 'PC',
+  MOBILE = 'Mobile'
+}

+ 119 - 0
src/types/log/type.ts

@@ -0,0 +1,119 @@
+import { PaginationRequest, PaginationResponse } from "@/types/common/type";
+
+/* 登录日志列表row */
+export interface LoginLogItem {
+  /*主键 */
+  id: number;
+  /*用户名 */
+  userName: string;
+  /*用户姓名(昵称) */
+  realName: string;
+  /*人员组织 */
+  deptName: string;
+  /*设备类型 */
+  device: string;
+  /*类型 1-登录 2-登出 */
+  type: number;
+  /*登录状态:0--失败 1--成功 */
+  loginStatus: number;
+  /*登录IP */
+  loginIp: string;
+  /*登录时间 */
+  createdAt: Record<string, unknown>;
+}
+
+/* 登录日志列表请求参数 */
+export interface LoginLogPageRequest extends PaginationRequest {
+  queryParam: {
+    userName?: LoginLogItem['userName'],
+    realName?: LoginLogItem['realName'],
+    device?: LoginLogItem['device'],
+    /*开始时间 */
+    startTime?: string;
+    /*结束时间 */
+    endTime?: string;
+    /*人员组织id */
+    deptId?: number;
+    date?: string;
+    queryType?: string,
+    queryTypeContent?: string
+  }
+}
+
+/* 登录日志接口返回 */
+export type LoginLogRespose = PaginationResponse<LoginLogItem>;
+
+/* 操作日志列表row */
+export interface OpreationLogItem {
+  /*自增主键 */
+  id: number;
+  /*操作人 */
+  operatorName: string;
+  /*用户姓名(昵称) */
+  realName: string;
+  /*IP地址 */
+  clientIp: string;
+  /*应用侧 */
+  platform: string;
+  /*所属模块 */
+  module: string;
+  /*内容 */
+  content: string;
+  /*操作类型 */
+  operatorType: string;
+  /*登录时间 */
+  createdAt: string;
+  /*操作结果 */
+  isSuccess: number;
+}
+
+/* 操作日志列表请求参数 */
+export interface OperatinoLogPageRequest extends PaginationRequest {
+  queryParam: {
+    operatorName?: OpreationLogItem['operatorName'],
+    realName?: OpreationLogItem['realName'],
+    platform?: OpreationLogItem['platform'],
+    module?: OpreationLogItem['module'],
+    operatorType?: OpreationLogItem['operatorType'],
+    /*开始时间 */
+    startTime?: string;
+    /*结束时间 */
+    endTime?: string;
+    date?: string;
+    queryType?: string,
+    queryTypeContent?: string
+  }
+}
+
+/* 操作日志接口返回 */
+export type OperationLogRespose = PaginationResponse<OpreationLogItem>;
+
+/* 操作日志详情 */
+export type OpretionPageDetail = Pick<OpreationLogItem, 'module' | 'operatorType' | 'operatorName' | 'clientIp' |'isSuccess' | 'createdAt'> & { parameter: string}
+
+/* 系统日志列表row */
+export interface SystemLogItem {
+  /*主键 */
+  id: number;
+  /*事件类型 */
+  eventType: number;
+  /*时间 */
+  createdAt: Record<string, unknown>;
+  /*事件内容 */
+  eventContent: string;
+}
+
+/* 系统日志列表请求参数 */
+export interface SystemLogRequest extends PaginationRequest {
+   queryParam: {
+    eventType?: SystemLogItem['eventType'],
+    /*开始时间 */
+    startTime?: string;
+    /*结束时间 */
+    endTime?: string;
+    date?: string;
+  }
+}
+
+/* 系统日志接口返回 */
+export type SystemLogRespose = PaginationResponse<SystemLogItem>;

+ 77 - 0
src/views/system/log/components/DetailDialog.vue

@@ -0,0 +1,77 @@
+<template>
+  <el-dialog title="操作日志详细" v-model="dialogTableVisible" width="800">
+    <el-form :model="form" label-width="auto">
+      <el-form-item label="操作模块:">
+        <span>{{ form.module + '/' + OperationType[form.operatorType] }}</span>
+      </el-form-item>
+      <el-form-item label="请求地址:">
+        <span>{{ form.clientIp }}</span>
+      </el-form-item>
+      <el-form-item label="登录信息:">
+        <span>{{ form.operatorName }}</span>
+      </el-form-item>
+      <el-form-item label="请求方式:">
+        <span>{{ form.method }}</span>
+      </el-form-item>
+      <el-form-item label="请求参数:">
+        <span style="overflow-y: scroll;">{{ form.params }}</span>
+      </el-form-item>
+      <el-form-item label="返回参数:">
+        <span>{{ form.result }}</span>
+      </el-form-item>
+      <el-form-item label="操作状态:">
+        <span>{{ form.isSuccess === 1 ? '成功' : '失败' }}</span>
+      </el-form-item>
+      <el-form-item label="操作时间:">
+        <span>{{ form.createdAt }}</span>
+      </el-form-item>
+    </el-form>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue';
+import { queryOperationLogDetail } from '@/api/system/log';
+import { OpretionPageDetail } from '@/types/log/type';
+import { OperationType } from '@/types/log/constants';
+
+const dialogTableVisible = ref(false);
+const form = reactive<OpretionPageDetail & { params: string, method: string, result: string }>({
+  operatorType: '',
+  operatorName: '',
+  clientIp: '',
+  parameter: '',
+  isSuccess: 0,
+  createdAt: '',
+  module: '',
+  params: '',
+  method: '',
+  result: ''
+})
+
+/**
+ * 打开 dialog
+ */
+const open = (id?: number) => {
+  if (id) {
+    loadDetailPage(id);
+  }
+  dialogTableVisible.value = true;
+};
+
+const loadDetailPage = async (id: number) => {
+  const result = await queryOperationLogDetail(id);
+  Object.entries(result).forEach(([key, value]) => {
+    form[key] = value;
+  });
+  // 解析 JSON 字符串
+  const parsedData = JSON.parse(result.parameter);
+  // 解析 `params` 数组中的 JSON 字符串
+  parsedData.params = parsedData.params.map(param => JSON.parse(param));
+  form.params = parsedData.params;
+  form.result = parsedData.result;
+  form.method = parsedData.method;
+}
+
+defineExpose({ open });
+</script>

+ 165 - 0
src/views/system/log/components/LoginLog.vue

@@ -0,0 +1,165 @@
+<!--
+ * @since: 2025-02-07
+ * LoginLog.vue
+-->
+<template>
+  <div class="login-log">
+    <el-card class="mb-3 proCard">
+      <el-space >
+        <el-form ref="searchFormRef" :inline="true" :model="requestParams.queryParam" class="form-inline" >
+          <el-form-item prop="userName">
+           <el-select v-model="requestParams.queryParam.queryType" placeholder="选择类型" class="type-select" >
+              <el-option
+                v-for="item in queryTypeSelect"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+            <el-input v-model="requestParams.queryParam.queryTypeContent" clearable placeholder="请输入搜索关键字"
+              @keyup.enter="queryLoginLogPage" :disabled="requestParams.queryParam.queryType === ''" />
+          </el-form-item>
+          <el-form-item label="终端类型" prop="device">
+            <el-select v-model="requestParams.queryParam.device" placeholder="请选择终端类型" clearable>
+              <el-option :label="item.label" :value="item.label" v-for="item in deviceList" :key="item.value" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="登录时间" prop="date">
+            <el-date-picker 
+              v-model="requestParams.queryParam.date" 
+              type="daterange" 
+              placeholder="请选择登录时间" 
+              range-separator="~"
+              start-placeholder="开始时间"
+              end-placeholder="结束时间"
+              clearable />
+          </el-form-item>
+          <el-form-item label="请选择组织:" label-position="left" prop="deptId">
+            <el-tree-select type="daterange" range-separator="To" start-placeholder="开始时间" end-placeholder="结束时间"
+              v-model="requestParams.queryParam.deptId" :data="departmentList" :render-after-expand="false"
+              :default-expand-all="true" check-strictly placeholder="请选择组织" class="protocal-select" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" :icon="Search" @click="queryLoginLogPage">查询</el-button>
+            <el-button :icon="Refresh" @click="hanleResetForm(searchFormRef)">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </el-space>
+    </el-card>
+
+    <el-card>
+      <template #header>
+        <el-button type="primary" @click="handleExport()">导出数据</el-button>
+      </template>
+      <el-table height="calc(100vh - 500px)" :data="loginLogList" v-loading="loading">
+        <el-table-column label="序号" width="100" prop="id" />
+        <el-table-column label="登录账号" prop="userName" />
+        <el-table-column label="姓名" prop="realName" />
+        <el-table-column label="组织"  prop="deptName" />
+        <el-table-column label="客户端" prop="device" >
+          <template #default="{row}">
+            <el-icon :size="24" color="#409eff" v-if="row.device === ClientTypeEnum.PC"><Platform /></el-icon>
+            <el-icon :size="24" color="#409eff" v-else><Iphone /></el-icon>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作类型"  prop="type" >
+        <template #default="{row}">
+          <el-tag
+            :type="row.type === OperationTypeEnum.LOGIN ? 'success' : 'danger'"
+            round
+          >
+            {{  row.type === OperationTypeEnum.LOGIN ? '登录': '登出' }}
+          </el-tag>
+        </template>
+        </el-table-column>
+        <el-table-column label="操作结果" prop="loginStatus" >
+          <template #default="{ row }">
+           <el-icon :size="24" color="#67C23A" v-if="row.loginStatus === LoginStatusEnum.SUCCESS"><SuccessFilled /></el-icon>
+           <el-icon :size="24" color="#F56C6C" v-else><CircleCloseFilled /></el-icon>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作时间"  prop="createdAt" />
+      </el-table>
+
+      <section class="mt-4 flex justify-end">
+        <el-pagination background layout="total, sizes, prev, pager, next" :page-sizes="[10, 30, 50]" :total="total"
+          v-model:page-size="requestParams.pageSize" v-model:current-page="requestParams.pageNumber"
+          @change="queryLoginLogPage" />
+      </section>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import { Search, Refresh, SuccessFilled, CircleCloseFilled, Platform, Iphone } from '@element-plus/icons-vue';
+import { getAllDepartments } from '@/api/auth/dept';
+import type { FormInstance } from 'element-plus'
+import { OptionsProps, deviceList, queryTypeSelect, LoginStatusEnum, OperationTypeEnum, ClientTypeEnum } from '@/types/log/constants';
+import useSceneInfos from '@/hooks/useSceneInfos';
+import useLoginLogQuery from '../hooks/useLoginLogQuery';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { exportLoginLog } from '@/api/system/log';
+import { downloadByData } from '@/utils/file/download';
+
+const sceneInfos = useSceneInfos();
+const { calculateTreeData } = sceneInfos;
+const { requestParams, total, loginLogList, queryLoginLogPage, loading, resetRequestParams } = useLoginLogQuery();
+
+const departmentList = ref<OptionsProps[]>([]);
+
+onMounted(async () => {
+  queryLoginLogPage();
+  getAllDepartments().then((res) => {
+    departmentList.value = calculateTreeData(
+      res,
+      { level: 3, valueKey: 'id', labelKey: 'deptName' },
+      1,
+    );
+  });
+});
+
+/* 导出数据 */
+const handleExport = () => {
+  ElMessageBox.confirm('确定导出所查询数据?', '导出', {
+      confirmButtonText: '确定',
+      showCancelButton: true,
+      type: 'warning',
+    })
+      .then(() => {
+        exportLoginLog(requestParams).then(async (responnse) => {
+          if (!responnse) {
+            throw new Error('下载文件失败');
+          }
+          downloadByData(responnse, '登录日志.xlsx');
+          ElMessage.success('下载文件成功');
+        });
+      })
+      .catch(() => {
+        ElMessage({
+          type: 'info',
+          message: '取消导出',
+        });
+      });
+}
+
+/* 重置 */
+const searchFormRef = ref<FormInstance>()
+const hanleResetForm = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  resetRequestParams();
+  formEl.resetFields();
+  queryLoginLogPage();
+}
+
+</script>
+
+<style scoped lang="scss">
+.form-inline .el-input {
+  --el-input-width: 160px;
+}
+
+.form-inline .el-select {
+  --el-select-width: 160px;
+}
+</style>

+ 168 - 0
src/views/system/log/components/OperationLog.vue

@@ -0,0 +1,168 @@
+<!--
+ * @since: 2025-02-07
+ * OperationLog.vue
+-->
+<template>
+  <div class="login-log">
+    <el-card class="mb-3 proCard">
+      <el-space >
+        <el-form ref="searchFormRef" :inline="true" :model="requestParams.queryParam" class="form-inline">
+          <el-form-item prop="userName">
+              <el-select v-model="requestParams.queryParam.queryType" placeholder="选择类型" class="type-select">
+              <el-option
+                v-for="item in queryTypeSelect"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+            <el-input v-model="requestParams.queryParam.queryTypeContent" clearable placeholder="请输入搜索关键字"
+              @keyup.enter="queryOperationLogPage" :disabled="requestParams.queryParam.queryType ===''"/>
+          </el-form-item>
+          <el-form-item label="应用侧" prop="platform">
+            <el-select v-model="requestParams.queryParam.platform" placeholder="请选择应用侧" clearable>
+              <el-option :label="item.label" :value="item.label" v-for="item in platformList" :key="item.value" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="所属模块" prop="module">
+            <el-input v-model="requestParams.queryParam.module" clearable placeholder="请输入所属模块"
+              @keyup.enter="queryOperationLogPage" />
+           </el-form-item>
+          <el-form-item label="操作类型" prop="operatorType">
+            <el-select v-model="requestParams.queryParam.operatorType" placeholder="请选择操作类型" clearable>
+              <el-option :label="item.label" :value="item.label" v-for="item in operationList" :key="item.value" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="登录时间" prop="date">
+            <el-date-picker 
+              v-model="requestParams.queryParam.date" 
+              type="daterange" 
+              placeholder="请选择登录时间" 
+              range-separator="~"
+              start-placeholder="开始时间"
+              end-placeholder="结束时间"
+              clearable />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" :icon="Search" @click="queryOperationLogPage">查询</el-button>
+            <el-button :icon="Refresh" @click="handleResetForm(searchFormRef)">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </el-space>
+    </el-card>
+
+    <el-card>
+      <template #header>
+        <el-button type="primary" @click="handleExport()">导出数据</el-button>
+      </template>
+      <el-table height="calc(100vh - 340px)" :data="operationLogList" v-loading="loading">
+        <el-table-column label="日志编号" width="100" prop="id" />
+        <el-table-column label="账号" prop="realName" />
+        <el-table-column label="姓名" prop="operatorName" />
+        <el-table-column label="应用侧" prop="platform" >
+          <template #default="{ row }">
+            {{  row.platform === 'ADMIN' ? '管理侧' : '平台侧' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="所属模块" prop="module" />
+        <el-table-column label="操作内容"  prop="content" />
+        <el-table-column label="操作类型"  prop="operatorType" >
+           <template #default="{ row }">
+            {{  OperationType[row.operatorType] }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作结果" prop="isSuccess" >
+          <template #default="{ row }">
+           <el-icon :size="24" color="#67C23A" v-if="row.isSuccess === LoginStatusEnum.SUCCESS"><SuccessFilled /></el-icon>
+           <el-icon :size="24" color="#F56C6C" v-else><CircleCloseFilled /></el-icon>
+          </template>
+        </el-table-column>
+        <el-table-column label="IP" prop="clientIp" />
+        <el-table-column label="操作时间" prop="createdAt" />
+        <el-table-column label="操作" width="160">
+          <template #default="{ row }">
+            <el-space>
+              <el-button type="primary"  text @click="openDrawer(row.id)" >详情</el-button>
+            </el-space>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <section class="mt-4 flex justify-end">
+        <el-pagination background layout="total, sizes, prev, pager, next" :page-sizes="[10, 30, 50]" :total="total"
+          v-model:page-size="requestParams.pageSize" v-model:current-page="requestParams.pageNumber"
+          @change="queryOperationLogPage" />
+      </section>
+    </el-card>
+
+    <DetailDialog ref="drawerInstance" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import { Search, Refresh, Edit ,SuccessFilled, CircleCloseFilled} from '@element-plus/icons-vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { exportLoginLog } from '@/api/system/log';
+import { downloadByData } from '@/utils/file/download';
+import type { FormInstance } from 'element-plus'
+import DetailDialog from './DetailDialog.vue';
+import { platformList, operationList, queryTypeSelect, LoginStatusEnum, OperationType } from '@/types/log/constants';
+import userOperationQuery from '../hooks/userOperationQuery';
+
+const { requestParams, total, operationLogList, loading, queryOperationLogPage, resetRequestParams } = userOperationQuery();
+
+onMounted(async () => {
+  queryOperationLogPage();
+});
+
+/* 导出数据 */
+const handleExport = () => {
+  ElMessageBox.confirm('确定导出所查询数据?', '导出', {
+      confirmButtonText: '确定',
+      showCancelButton: true,
+      type: 'warning',
+    })
+      .then(() => {
+        exportLoginLog(requestParams).then(async (responnse) => {
+          if (!responnse) {
+            throw new Error('下载文件失败');
+          }
+          downloadByData(responnse, '操作日志.xlsx');
+          ElMessage.success('下载文件成功');
+        });
+      })
+      .catch(() => {
+        ElMessage({
+          type: 'info',
+          message: '取消导出',
+        });
+      });
+}
+
+/* 重置 */
+const searchFormRef = ref<FormInstance>()
+const handleResetForm = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  resetRequestParams();
+  formEl.resetFields();
+  queryOperationLogPage();
+}
+
+const drawerInstance = ref<InstanceType<typeof DetailDialog>>();
+const openDrawer = (id?: number) => {
+  drawerInstance.value?.open(id);
+};
+
+
+</script>
+
+<style scoped lang="scss">
+.form-inline .el-input {
+  --el-input-width: 160px;
+}
+
+.form-inline .el-select {
+  --el-select-width: 160px;
+}
+</style>

+ 123 - 0
src/views/system/log/components/SystemLog.vue

@@ -0,0 +1,123 @@
+<!--
+ * @since: 2025-02-07
+ * System.vue
+-->
+<template>
+  <div class="login-log">
+    <el-card class="mb-3 proCard">
+      <el-space >
+        <el-form ref="searchFormRef" :inline="true" :model="requestParams.queryParam" class="form-inline">
+          <el-form-item label="事件类型" prop="eventType">
+            <el-select 
+              v-model="requestParams.queryParam.eventType" 
+              placeholder="请选择事件类型" 
+              clearable>
+                <el-option 
+                :label="item.label" 
+                :value="item.value" 
+                v-for="item in eventList" 
+                :key="item.value" />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="登录时间" prop="date">
+            <el-date-picker 
+              v-model="requestParams.queryParam.date" 
+              type="daterange" 
+              placeholder="请选择登录时间" 
+              range-separator="~"
+              start-placeholder="开始时间"
+              end-placeholder="结束时间"
+              clearable />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" :icon="Search" @click="querySystemLogPage">查询</el-button>
+            <el-button :icon="Refresh" @click="handleResetForm(searchFormRef)">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </el-space>
+    </el-card>
+
+    <el-card>
+      <template #header>
+        <el-button type="primary" @click="handleExport()">导出数据</el-button>
+      </template>
+      <el-table height="calc(100vh - 440px)" :data="systemLogList" v-loading="loading">
+        <el-table-column label="序号" width="100" prop="id" />
+        <el-table-column label="事件类型" prop="realName" >
+          <template #default="{ row }">
+           <span>{{ eventTypeArr[row.eventType] }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="时间" prop="createdAt" />
+        <el-table-column label="事件" width="200" prop="eventContent" />
+      </el-table>
+
+      <section class="mt-4 flex justify-end">
+        <el-pagination background layout="total, sizes, prev, pager, next" :page-sizes="[10, 30, 50]" :total="total"
+          v-model:page-size="requestParams.pageSize" v-model:current-page="requestParams.pageNumber"
+          @change="querySystemLogPage" />
+      </section>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref,onMounted } from 'vue';
+import { Search, Refresh, Edit } from '@element-plus/icons-vue';
+import useSystemQuery from '../hooks/useSystemQuery';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { exportSystemLog } from '@/api/system/log';
+import { downloadByData } from '@/utils/file/download';
+import { eventList, eventTypeArr } from '@/types/log/constants.ts';
+import type { FormInstance } from 'element-plus'
+
+const { requestParams, total, systemLogList, loading, resetRequestParams, querySystemLogPage } = useSystemQuery();
+
+onMounted(async () => {
+  querySystemLogPage();
+});
+
+/* 导出数据 */
+const handleExport = () => {
+  ElMessageBox.confirm('确定导出所查询数据?', '导出', {
+      confirmButtonText: '确定',
+      showCancelButton: true,
+      type: 'warning',
+    })
+    .then(() => {
+      exportSystemLog(requestParams).then(async (responnse) => {
+        if (!responnse) {
+          throw new Error('下载文件失败');
+        }
+        downloadByData(responnse, '系统日志.xlsx');
+        ElMessage.success('下载文件成功');
+      });
+    })
+    .catch(() => {
+      ElMessage({
+        type: 'info',
+        message: '取消导出',
+      });
+    });
+}
+
+/* 重置 */
+const searchFormRef = ref<FormInstance>()
+const handleResetForm = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  resetRequestParams();
+  formEl.resetFields();
+  querySystemLogPage();
+}
+
+</script>
+
+<style scoped lang="scss">
+.form-inline .el-input {
+  --el-input-width: 160px;
+}
+
+.form-inline .el-select {
+  --el-select-width: 160px;
+}
+</style>

+ 73 - 0
src/views/system/log/hooks/useLoginLogQuery.ts

@@ -0,0 +1,73 @@
+
+import { reactive, ref, shallowRef } from "vue"
+import { LoginLogItem, LoginLogPageRequest } from "@/types/log/type"
+import { DEFAULT_PAGE_SIZE } from "@/types/common/constants"
+import { cloneDeep } from "lodash-es"
+import { queryLoginLogList } from "@/api/system/log"
+
+const defaultLoginLogRequest: LoginLogPageRequest = {
+  pageNumber: 1,
+  pageSize: DEFAULT_PAGE_SIZE,
+  queryParam: {
+    userName: '',
+    device: '',
+    startTime: '',
+    endTime: '',
+    deptId: undefined,
+    date: '',
+    queryType: '',
+    queryTypeContent: ''
+  }
+}
+
+export default function useLoginLogRequest () {
+  const requestParams = reactive<LoginLogPageRequest>(cloneDeep(defaultLoginLogRequest));
+  const loginLogList = shallowRef<LoginLogItem[]>([]);
+  const total = ref(0);
+  const loading = ref(false);
+
+  const queryLoginLogPage = async () => {
+    try {
+      loading.value = true;
+      const { date, queryType, queryTypeContent } = requestParams.queryParam;
+      const params = cloneDeep(requestParams);
+       Object.entries(requestParams.queryParam).forEach(([key, value]) => { 
+        if (value) {
+          if (queryType) {
+              params.queryParam[queryType] = queryTypeContent;
+            }
+        }
+      });
+      if (date && date.length > 0) {
+        params.queryParam.startTime = date[0];
+        params.queryParam.endTime = date[1];
+      }
+     
+      delete params.queryParam.date; 
+      delete params.queryParam.queryType; 
+      delete params.queryParam.queryTypeContent; 
+      const data = await queryLoginLogList(params);
+      loginLogList.value = data.records;
+      total.value = data.totalPage;
+    } catch (error) {
+      console.log(error)
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  const resetRequestParams = () => {
+    requestParams.queryParam.startTime = ''
+    requestParams.queryParam.endTime = ''
+    Object.assign(requestParams, defaultLoginLogRequest)
+  }
+
+  return {
+    requestParams,
+    total,
+    loading,
+    loginLogList,
+    queryLoginLogPage,
+    resetRequestParams
+  }
+}

+ 61 - 0
src/views/system/log/hooks/useSystemQuery.ts

@@ -0,0 +1,61 @@
+
+import { reactive, ref, shallowRef } from "vue"
+import { SystemLogRequest, SystemLogItem } from "@/types/log/type"
+import { DEFAULT_PAGE_SIZE } from "@/types/common/constants"
+import { cloneDeep } from "lodash-es"
+import { querySystemLogList } from "@/api/system/log"
+
+const defaultLoginLogRequest: SystemLogRequest = {
+  pageNumber: 1,
+  pageSize: DEFAULT_PAGE_SIZE,
+  queryParam: {
+    eventType: undefined,
+    startTime: '',
+    endTime: '',
+    date: ''
+  }
+}
+
+export default function useSystemLogRequest () {
+  const requestParams = reactive<SystemLogRequest>(cloneDeep(defaultLoginLogRequest));
+  const systemLogList = shallowRef<SystemLogItem[]>([]);
+  const total = ref(0);
+  const loading = ref(false);
+
+  const querySystemLogPage = async () => {
+    try {
+      loading.value = true;
+      const { date } = requestParams.queryParam;
+      const params = cloneDeep(requestParams);
+      
+      if (date && date.length > 0) {
+        params.queryParam.startTime = date[0];
+        params.queryParam.endTime = date[1];
+      }
+     
+      delete params.queryParam.date; 
+      const data = await querySystemLogList(params);
+      systemLogList.value = data.records;
+      total.value = data.totalPage;
+    } catch (error) {
+      console.log(error)
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  const resetRequestParams = () => {
+    requestParams.queryParam.startTime = ''
+    requestParams.queryParam.endTime = ''
+    Object.assign(requestParams, defaultLoginLogRequest)
+  }
+
+  return {
+    requestParams,
+    total,
+    loading,
+    systemLogList,
+    querySystemLogPage,
+    resetRequestParams
+  }
+}

+ 75 - 0
src/views/system/log/hooks/userOperationQuery.ts

@@ -0,0 +1,75 @@
+
+import { reactive, ref, shallowRef } from "vue"
+import { OperatinoLogPageRequest, OpreationLogItem  } from "@/types/log/type"
+import { DEFAULT_PAGE_SIZE } from "@/types/common/constants"
+import { cloneDeep } from "lodash-es"
+import { queryOperationLogList } from "@/api/system/log"
+
+const defaultOperationLogRequest: OperatinoLogPageRequest = {
+  pageNumber: 1,
+  pageSize: DEFAULT_PAGE_SIZE,
+  queryParam: {
+    operatorName: '',
+    realName: '',
+    platform: '',
+    module: '',
+    operatorType: '',
+    startTime: '',
+    endTime: '',
+    date: '',
+    queryType: '',
+    queryTypeContent: ''
+  }
+}
+
+export default function useOperationLogRequest () {
+  const requestParams = reactive<OperatinoLogPageRequest>(cloneDeep(defaultOperationLogRequest));
+  const operationLogList = shallowRef<OpreationLogItem[]>([]);
+  const total = ref(0);
+  const loading = ref(false);
+
+  const queryOperationLogPage = async () => {
+    try {
+      loading.value = true;
+     const { date, queryType, queryTypeContent } = requestParams.queryParam;
+      const params = cloneDeep(requestParams);
+       Object.entries(requestParams.queryParam).forEach(([key, value]) => { 
+        if (value) {
+          if (queryType) {
+              params.queryParam[queryType] = queryTypeContent;
+            }
+        }
+      });
+      if (date && date.length > 0) {
+        params.queryParam.startTime = date[0];
+        params.queryParam.endTime = date[1];
+      }
+     
+      delete params.queryParam.date; 
+      delete params.queryParam.queryType; 
+      delete params.queryParam.queryTypeContent
+      const data = await queryOperationLogList(params);
+      operationLogList.value = data.records;
+      total.value = data.totalPage;
+    } catch (error) {
+      console.log(error)
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  const resetRequestParams = () => {
+    requestParams.queryParam.startTime = ''
+    requestParams.queryParam.endTime = ''
+    Object.assign(requestParams, defaultOperationLogRequest)
+  }
+
+  return {
+    requestParams,
+    total,
+    loading,
+    operationLogList,
+    queryOperationLogPage,
+    resetRequestParams
+  }
+}

+ 73 - 0
src/views/system/log/log.vue

@@ -0,0 +1,73 @@
+<template>
+  <div class="log-content">
+    <div class="flex log-head-tabs" >
+      <div
+        class="flex justify-center items-center tab-item"
+        :class="{ 'tab-item-active': currentTab === item.value }"
+        @click="switchTable(item.value)"
+        v-for="item in logTabsList"
+        :key="item.value"
+      >
+        {{ item.label }}
+      </div>
+    </div>
+    <component :is="currentComponent" />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { computed, ref } from 'vue';
+  import { LogTabEnum, logTabsList } from '@/types/log/constants';
+  import LoginLog from './components/LoginLog.vue';
+  import SystemLog from './components/SystemLog.vue';
+  import OperationLog from './components/OperationLog.vue';
+
+  const currentTab = ref(1);
+    const switchTable = (value) => {
+      currentTab.value = value;
+    };
+
+  // 根据tab展示不同组件
+  const ComponentEnum = {
+    [LogTabEnum.LOGIN]: LoginLog,
+    [LogTabEnum.OPERATION]: OperationLog,
+    [LogTabEnum.SYSTEM]: SystemLog,
+  }
+  const currentComponent = computed(()=> ComponentEnum[currentTab.value])
+</script>
+
+<style scoped lang="scss">
+  .log-content {
+    height: calc(100vh - 64px - 18px);
+    background-color: rgba(255, 255, 255, 1);
+    padding: 21px;
+  }
+  .log-head {
+    height: 56px;
+
+    &-tabs {
+      margin: 18px 0;
+      .tab-item {
+        width: 188px;
+        height: 38px;
+        background: #fafafa;
+        border: 1px solid #d9d9d9;
+        cursor: pointer;
+
+        &-active {
+          color: rgba(22, 119, 255, 1);
+          background: #e2eefe;
+          border: 1px solid #1890ff;
+        }
+      }
+
+      :first-child {
+        border-radius: 8px 0px 0px 8px;
+      }
+
+      :last-child {
+        border-radius: 0px 8px 8px 0px;
+      }
+    }
+  }
+</style>