Преглед изворни кода

Merge branch 'all-v4-qindao' into 'all-v4'

feat: v4版本 日志管理

See merge request skyeye/skyeye_frontend/skyeye-admin!293
Fei Liu пре 1 година
родитељ
комит
9d79779ba5

+ 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 - 32
src/router/full-routes.ts

@@ -416,38 +416,6 @@ const fullRoutes: AppRouteRecordRaw[] = [
           title: '违规问题'
         }
       },
-      {
-        // 系统日志
-        path: 'logs',
-        name: 'DataLogs',
-        component: 'ParentLayout',
-        meta: { 
-          icon: '',
-          title: '系统日志',
-        },
-        children: [
-          {
-            // 操作日志
-            path: 'operation',
-            name: 'DataLogsOperation',
-            component: '/system/logs/operlog',
-            meta: { 
-              icon: '',
-              title: '操作日志',
-            },
-          },
-          {
-            // 登录日志
-            path: 'login',
-            name: 'DataLogsLogin',
-            component: '/system/logs/logininfor',
-            meta: { 
-              icon: '',
-              title: '登录日志',
-            }
-          }
-        ]
-      }
     ]
   },
 
@@ -525,6 +493,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>

+ 156 - 172
src/views/system/user/CreateAdminDrawer.vue

@@ -1,51 +1,23 @@
 <template>
   <el-drawer v-model="isDrawer" :size="width" :title="props.title" @close="handleReset">
-    <el-form
-      :model="formParams"
-      :rules="rules"
-      ref="formRef"
-      label-placement="left"
-      :label-width="80"
-    >
+    <el-form :model="formParams" :rules="rules" ref="formRef" label-placement="left" :label-width="80">
       <el-form-item label="登录账号" prop="username">
-        <el-input
-          placeholder="请输入登录账号"
-          v-model="formParams.username"
-          :disabled="!isAddUser"
-        />
+        <el-input placeholder="请输入登录账号" v-model="formParams.username" :disabled="!isAddUser" />
       </el-form-item>
       <el-form-item label="姓名" prop="realname">
         <el-input placeholder="请输入姓名" v-model="formParams.realname" />
       </el-form-item>
       <el-form-item label="角色" prop="roleIdList">
-        <el-select clearable v-model="formParams.roleIdList" multiple>
-          <el-option
-            v-for="item in roleData"
-            :key="item.value"
-            :label="item.label"
-            :value="item.value"
-          />
+        <el-select clearable v-model="formParams.roleIdList" multiple v-if="isCanAddTenant && isSysTenant">
+          <el-option v-for="item in roleData" :key="item.value" :label="item.label" :value="item.value" />
         </el-select>
+        <el-input v-else placeholder="租户管理员默认拥有租全部角色权限" disabled />
       </el-form-item>
       <el-form-item v-if="isAddUser" label="密码" prop="password">
-        <el-input
-          type="password"
-          show-password-on="mousedown"
-          placeholder="请输入密码"
-          v-model="formParams.password"
-        />
+        <el-input type="password" show-password-on="mousedown" placeholder="请输入密码" v-model="formParams.password" />
       </el-form-item>
-      <el-form-item
-        v-if="isAddUser"
-        label="确认密码"
-        :prop="!formParams.username ? 'passwordRe' : ''"
-      >
-        <el-input
-          type="password"
-          show-password-on="mousedown"
-          placeholder="请输入密码"
-          v-model="formParams.passwordRe"
-        />
+      <el-form-item v-if="isAddUser" label="确认密码" :prop="!formParams.username ? 'passwordRe' : ''">
+        <el-input type="password" show-password-on="mousedown" placeholder="请输入密码" v-model="formParams.passwordRe" />
       </el-form-item>
       <el-form-item label="是否启用" prop="isDisabled">
         <el-switch v-model="formParams.isDisabled" />
@@ -61,133 +33,115 @@
 </template>
 
 <script lang="ts" setup>
-  import { ref, computed } from 'vue';
-  import { FormRules, ElMessage } from 'element-plus';
-  import { userInfo } from '@/api/system/user';
-  import { cloneDeep } from 'lodash-es';
-  import {
-    addTenantAdmin,
-    AddTenantAdminProps,
-    updateUser,
-    addSingleUser,
-  } from '@/api/system/user-operate';
-  import { EditType, OptionsProps, DisabledEnum } from './types';
-  import { useTargetTenantIdSetting } from '@/utils/useTargetTenantIdSetting';
-  import { ResultEnum } from '@/enums/httpEnum';
+import { ref, computed, watch } from 'vue';
+import { FormRules, ElMessage } from 'element-plus';
+import { userInfo } from '@/api/system/user';
+import { cloneDeep } from 'lodash-es';
+import {
+  addTenantAdmin,
+  AddTenantAdminProps,
+  updateUser,
+  addSingleUser,
+} from '@/api/system/user-operate';
+import { EditType, OptionsProps, DisabledEnum } from './types';
+import { useTargetTenantIdSetting } from '@/utils/useTargetTenantIdSetting';
+import { ResultEnum } from '@/enums/httpEnum';
 
-  const { isSysTenant } = useTargetTenantIdSetting();
-  const rules: FormRules = {
-    username: {
-      required: true,
-      message: '登录账号不能为空',
-      trigger: 'blur',
-    },
-    realname: {
-      required: true,
-      message: '姓名不能为空',
-      trigger: 'blur',
-    },
-    roleIdList: {
-      required: true,
-      message: '请选择角色',
-      trigger: 'change',
-    },
-    password: {
-      required: true,
-      message: '密码不能为空',
-      trigger: 'blur',
-    },
-    passwordRe: {
-      required: true,
-      message: '密码不能为空',
-      trigger: 'blur',
-    },
-  };
+const { isSysTenant } = useTargetTenantIdSetting();
+let rules: FormRules = {
+  username: {
+    required: true,
+    message: '登录账号不能为空',
+    trigger: 'blur',
+  },
+  realname: {
+    required: true,
+    message: '姓名不能为空',
+    trigger: 'blur',
+  },
+  roleIdList: {
+    required: true,
+    message: '请选择角色',
+    trigger: 'change',
+  },
+  password: {
+    required: true,
+    message: '密码不能为空',
+    trigger: 'blur',
+  },
+  passwordRe: {
+    required: true,
+    message: '密码不能为空',
+    trigger: 'blur',
+  },
+};
 
-  const emit = defineEmits(['change']);
-  const props = withDefaults(
-    defineProps<{ title: string; width: number; editType: EditType; roleData: OptionsProps[] }>(),
-    {
-      title: '添加用户',
-      width: 450,
-    },
-  );
+const emit = defineEmits(['change']);
+const props = withDefaults(
+  defineProps<{ title: string; width: number; editType: EditType; roleData: OptionsProps[], isCanAddTenant: boolean, isSysTenant: boolean }>(),
+  {
+    title: '添加用户',
+    width: 450,
+  },
+);
 
-  const defaultValueRef = () => ({
-    username: '',
-    isDisabled: true,
-    realname: '',
-    roleIdList: [],
-    password: '',
-    passwordRe: '',
-  });
+const defaultValueRef = () => ({
+  username: '',
+  isDisabled: true,
+  realname: '',
+  roleIdList: [],
+  password: '',
+  passwordRe: '',
+});
 
-  const message = ElMessage;
-  const formRef: any = ref(null);
-  const isDrawer = ref(false);
-  const subLoading = ref(false);
-  const formParams = ref<AddTenantAdminProps>(defaultValueRef());
+const message = ElMessage;
+const formRef: any = ref(null);
+const isDrawer = ref(false);
+const subLoading = ref(false);
+const formParams = ref<AddTenantAdminProps>(defaultValueRef());
 
-  async function openDrawer(userId?) {
-    if (userId) {
-      formParams.value.userId = userId;
-      getInfo();
-      return;
-    }
-    isDrawer.value = true;
+async function openDrawer(userId?) {
+  if (userId) {
+    formParams.value.userId = userId;
+    getInfo();
+    return;
   }
+  isDrawer.value = true;
+}
 
-  const isAddUser = computed(() => {
-    return props.editType === EditType.create;
-  });
+const isAddUser = computed(() => {
+  return props.editType === EditType.create;
+});
 
-  function closeDrawer() {
-    isDrawer.value = false;
-  }
+function closeDrawer() {
+  isDrawer.value = false;
+}
 
-  function formSubmit() {
-    formRef.value.validate((valid) => {
-      if (!valid) {
-        message.error('请填写完整信息');
-        return;
-      }
+function formSubmit() {
+  formRef.value.validate((valid) => {
+    if (!valid) {
+      message.error('请填写完整信息');
+      return;
+    }
 
-      // 克隆
-      const params = cloneDeep(formParams.value);
-      if (params.password != params.passwordRe) {
-        return message.error('两次密码不一致');
-      }
-      if (isAddUser.value) {
-        formParams.value.isDisabled
-          ? (params.isDisabled = DisabledEnum.NO)
-          : (params.isDisabled = DisabledEnum.YES);
-        if (!isSysTenant) {
-          addTenantAdmin(params).then((res) => {
-            message.success('添加管理员账户成功');
-            emit('change');
-            handleReset();
-            closeDrawer();
-          });
-        } else {
-          addSingleUser(params)
-            .then((res) => {
-              if (res.code !== ResultEnum.SUCCESS) {
-                return Promise.reject(res.msg);
-              }
-              emit('change');
-              handleReset();
-              closeDrawer();
-              ElMessage.success('添加成功');
-            })
-            .catch((error) => {
-              ElMessage.error(error);
-            });
-        }
+    // 克隆
+    const params = cloneDeep(formParams.value);
+    if (params.password != params.passwordRe) {
+      return message.error('两次密码不一致');
+    }
+    if (isAddUser.value) {
+      formParams.value.isDisabled
+        ? (params.isDisabled = DisabledEnum.NO)
+        : (params.isDisabled = DisabledEnum.YES);
+      if (!isSysTenant) {
+        addTenantAdmin(params).then((res) => {
+          message.success('添加管理员账户成功');
+          emit('change');
+          handleReset();
+          closeDrawer();
+        });
       } else {
-        formParams.value.isDisabled
-          ? (params.isDisabled = DisabledEnum.NO)
-          : (params.isDisabled = DisabledEnum.YES);
-        updateUser(params)
+        addSingleUser(params)
           .then((res) => {
             if (res.code !== ResultEnum.SUCCESS) {
               return Promise.reject(res.msg);
@@ -195,34 +149,64 @@
             emit('change');
             handleReset();
             closeDrawer();
-            ElMessage.success('修改成功');
+            ElMessage.success('添加成功');
           })
           .catch((error) => {
             ElMessage.error(error);
           });
       }
-    });
-  }
+    } else {
+      formParams.value.isDisabled
+        ? (params.isDisabled = DisabledEnum.NO)
+        : (params.isDisabled = DisabledEnum.YES);
+      updateUser(params)
+        .then((res) => {
+          if (res.code !== ResultEnum.SUCCESS) {
+            return Promise.reject(res.msg);
+          }
+          emit('change');
+          handleReset();
+          closeDrawer();
+          ElMessage.success('修改成功');
+        })
+        .catch((error) => {
+          ElMessage.error(error);
+        });
+    }
+  });
+}
 
-  function handleReset() {
-    formRef.value.resetFields();
-    formParams.value = Object.assign(formParams.value, defaultValueRef());
-  }
+function handleReset() {
+  formRef.value.resetFields();
+  formParams.value = Object.assign(formParams.value, defaultValueRef());
+}
 
-  function getInfo() {
-    userInfo({ userId: Number(formParams.value.userId) }).then((res) => {
-      Object.entries(res).forEach(([key, value]) => {
-        formParams.value[key] = value;
-      });
-      formParams.value.roleIdList = res.roleIds;
-      formParams.value.passwordRe = res.password;
-      formParams.value.isDisabled = res.isDisabled === DisabledEnum.NO ? true : false;
-      isDrawer.value = true;
+function getInfo() {
+  userInfo({ userId: Number(formParams.value.userId) }).then((res) => {
+    Object.entries(res).forEach(([key, value]) => {
+      formParams.value[key] = value;
     });
+    formParams.value.roleIdList = res.roleIds;
+    formParams.value.passwordRe = res.password;
+    formParams.value.isDisabled = res.isDisabled === DisabledEnum.NO ? true : false;
+    isDrawer.value = true;
+  });
+}
+
+watch(
+  () => props,
+  () => {
+    if (props.isCanAddTenant && !props.isSysTenant) {
+      rules!.roleIdList!.required = false
+    }
+  },
+  {
+    immediate: true
   }
+)
 
-  defineExpose({
-    openDrawer,
-    closeDrawer,
-  });
+defineExpose({
+  openDrawer,
+  closeDrawer,
+});
 </script>

+ 2 - 2
src/views/system/user/user.vue

@@ -54,7 +54,7 @@
              <el-tooltip
               class="box-item"
               effect="dark"
-              content="该账号为租户管理员账号,自动拥有全部相机的查看权限,其他账号无此功能"
+              content="租户管理员账号,自动拥有全部相机查看权限及功能权限"
               placement="top-start"
               v-if="scope.row.roleType === RoleTypeEnum.TENANT_ADMIN"
             >
@@ -119,7 +119,7 @@
       :roleData="roleData" :width="450" />
     <ResetCodeDrawer ref="ResetCodeDrawerRef" :title="drawerTitle" @change="reloadTable" />
     <CreateAdminDrawer ref="createAdminDrawerRef" :title="createAdminTitle" @change="reloadTable"
-      :editType="createAdminType" :roleData="roleData" :width="450" />
+      :editType="createAdminType" :roleData="roleData" :width="450" :isCanAddTenant="isCanAddTenant" :isSysTenant="isSysTenant"/>
     <AddUser v-if="showAddPopover" :colseAddUser="setShowAddPopoverFalse" v-model="showAddPopover" class="add-popover"
       @change="reloadTable" />
   </div>