From 35b94cc8758738a3d68ef8650b5ba8293cedf7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9A=93=E6=9C=88=E5=BD=92=E5=B0=98?= Date: Wed, 12 Feb 2025 00:15:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en.yaml | 1 + locales/zh-CN.yaml | 1 + package.json | 3 +- pnpm-lock.yaml | 13 + src/api/monitor.ts | 20 +- .../monitor/operation/components/form.vue | 134 ++++++++ src/views/monitor/operation/hook.tsx | 308 +++++++++++++++++ src/views/monitor/operation/index.vue | 320 ++++++++++++++++++ 8 files changed, 798 insertions(+), 2 deletions(-) create mode 100644 src/views/monitor/operation/components/form.vue create mode 100644 src/views/monitor/operation/hook.tsx create mode 100644 src/views/monitor/operation/index.vue diff --git a/locales/en.yaml b/locales/en.yaml index defdf2f..6bd0580 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -38,6 +38,7 @@ buttons:ResetPassword: Reset Password buttons:RoleAllocation: Role Allocation buttons:PermissionDetails: Permission Details buttons:ForceToExit: Force Exit +buttons:Details: Details search:Total: Total search:History: History search:Collect: Collect diff --git a/locales/zh-CN.yaml b/locales/zh-CN.yaml index 43e56dd..02b8947 100644 --- a/locales/zh-CN.yaml +++ b/locales/zh-CN.yaml @@ -38,6 +38,7 @@ buttons:ResetPassword: 重置密码 buttons:RoleAllocation: 角色分配 buttons:PermissionDetails: 权限详情 buttons:ForceToExit: 强制退出 +buttons:Details: 详情 search:Total: 共 search:History: 搜索历史 search:Collect: 收藏 diff --git a/package.json b/package.json index ca2e1c3..b8e31f6 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,8 @@ "@zxcvbn-ts/core": "^3.0.4", "animate.css": "^4.1.1", "axios": "^1.7.9", - "dayjs": "^1.11.13", "cropperjs": "^1.6.2", + "dayjs": "^1.11.13", "echarts": "^5.5.1", "element-plus": "^2.9.0", "js-cookie": "^3.0.5", @@ -74,6 +74,7 @@ "typeit": "^8.8.7", "vue": "^3.5.13", "vue-i18n": "^10.0.5", + "vue-json-pretty": "^2.4.0", "vue-router": "^4.5.0", "vue-tippy": "^6.5.0", "vue-types": "^5.1.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 50cab2b..2b7ffcb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ importers: vue-i18n: specifier: ^10.0.5 version: 10.0.5(vue@3.5.13(typescript@5.6.3)) + vue-json-pretty: + specifier: ^2.4.0 + version: 2.4.0(vue@3.5.13(typescript@5.6.3)) vue-router: specifier: ^4.5.0 version: 4.5.0(vue@3.5.13(typescript@5.6.3)) @@ -3603,6 +3606,12 @@ packages: peerDependencies: vue: ^3.0.0 + vue-json-pretty@2.4.0: + resolution: {integrity: sha512-e9bP41DYYIc2tWaB6KuwqFJq5odZ8/GkE6vHQuGcbPn37kGk4a3n1RNw3ZYeDrl66NWXgTlOfS+M6NKkowmkWw==, tarball: https://registry.npmmirror.com/vue-json-pretty/-/vue-json-pretty-2.4.0.tgz} + engines: {node: '>= 10.0.0', npm: '>= 5.0.0'} + peerDependencies: + vue: '>=3.0.0' + vue-router@4.5.0: resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==} peerDependencies: @@ -7258,6 +7267,10 @@ snapshots: '@vue/devtools-api': 6.6.4 vue: 3.5.13(typescript@5.6.3) + vue-json-pretty@2.4.0(vue@3.5.13(typescript@5.6.3)): + dependencies: + vue: 3.5.13(typescript@5.6.3) + vue-router@4.5.0(vue@3.5.13(typescript@5.6.3)): dependencies: '@vue/devtools-api': 6.6.4 diff --git a/src/api/monitor.ts b/src/api/monitor.ts index a8fd85f..5e54ec0 100644 --- a/src/api/monitor.ts +++ b/src/api/monitor.ts @@ -1,5 +1,5 @@ import { http } from "@/utils/http"; -import type { UserLoginLogInfo } from "types/monitor"; +import type { OperationLogInfo, UserLoginLogInfo } from "types/monitor"; import { filterEmptyObject } from "./utils"; // --------------------------登录日志相关-------------------------------------- @@ -28,3 +28,21 @@ export const getUserLoginLogAPI = (params: { export const deleteUserOnlineAPI = (id: string) => { return http.request("delete", `/api/log/logout/${id}`); }; + +// ------------------------操作日志相关---------------------------------------- + +/** + * 获取用户操作日志 + */ +export const getUserOperationsAPI = (params: { + page: number; + pageSize: number; +}) => { + return http.request>( + "GET", + "/api/log/operation", + { + params + } + ); +}; diff --git a/src/views/monitor/operation/components/form.vue b/src/views/monitor/operation/components/form.vue new file mode 100644 index 0000000..61cb24f --- /dev/null +++ b/src/views/monitor/operation/components/form.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/src/views/monitor/operation/hook.tsx b/src/views/monitor/operation/hook.tsx new file mode 100644 index 0000000..9237056 --- /dev/null +++ b/src/views/monitor/operation/hook.tsx @@ -0,0 +1,308 @@ +import dayjs from "dayjs"; +import Detail from "./components/form.vue"; +import { message } from "@/utils/message"; +import { addDialog } from "@/components/ReDialog"; +import { getKeyList } from "@pureadmin/utils"; +import { getUserOperationsAPI } from "@/api/monitor"; +import { usePublicHooks } from "@/views/system/hooks"; +import type { PaginationProps } from "@pureadmin/table"; +import { type Ref, reactive, ref, onMounted, h } from "vue"; +import type { OperationLogInfo } from "types/monitor"; + +export function useOperation(tableRef: Ref) { + const form = reactive({ + module: "", + status: "", + operatingTime: "" + }); + const dataList = ref([]); + const loading = ref(true); + const selectedNum = ref(0); + const { tagStyle } = usePublicHooks(); + + const pagination = reactive({ + total: 0, + pageSize: 10, + currentPage: 1, + background: true + }); + + const getOperationType = (type: number) => { + switch (type) { + case 1: + return "primary"; + case 2: + return "success"; + case 3: + return "warning"; + case 4: + return "danger"; + case 5: + return "success"; + case 6: + return "primary"; + case 7: + return "success"; + case 8: + return "danger"; + default: + return "info"; + } + }; + const getOperationName = (type: number) => { + switch (type) { + case 1: + return "查询"; + case 2: + return "新增"; + case 3: + return "修改"; + case 4: + return "删除"; + case 5: + return "授权"; + case 6: + return "导出"; + case 7: + return "导入"; + case 8: + return "强退"; + default: + return "其他"; + } + }; + const getRequestType = (method: string) => { + switch (method) { + case "GET": + return "primary"; + case "POST": + return "success"; + case "PUT": + return "warning"; + case "DELETE": + return "danger"; + case "PATCH": + return "info"; + default: + return "info"; + } + }; + const columns: TableColumnList = [ + { + label: "勾选列", // 如果需要表格多选,此处label必须设置 + type: "selection", + fixed: "left", + reserveSelection: true // 数据刷新后保留选项 + }, + { + label: "操作人员账号", + prop: "operator_name", + minWidth: 100 + }, + { + label: "操作人员昵称", + prop: "operator_nickname", + minWidth: 100 + }, + { + label: "操作名称", + prop: "operation_name", + minWidth: 100 + }, + { + label: "操作类型", + prop: "operation_type", + cellRenderer: ({ row, props }) => ( + + {getOperationName(row.operation_type)} + + ) + }, + { + label: "操作IP", + prop: "host" + }, + { + label: "操作地点", + prop: "location" + }, + { + label: "请求方法", + prop: "request_method", + cellRenderer: ({ row, props }) => ( + + {row.request_method} + + ) + }, + { + label: "请求路径", + prop: "request_path" + }, + { + label: "请求耗时", + prop: "cost_time", + cellRenderer: ({ row, props }) => ( + + {row.cost_time.toFixed(2)} ms + + ) + }, + { + label: "操作系统", + prop: "os", + hide: true + }, + { + label: "浏览器类型", + prop: "browser", + hide: true + }, + { + label: "操作状态", + prop: "status", + cellRenderer: ({ row, props }) => ( + + {row.status === 1 ? "成功" : "失败"} + + ) + }, + { + label: "操作时间", + prop: "operation_time", + minWidth: 180, + formatter: ({ operation_time }) => + dayjs(operation_time).format("YYYY-MM-DD HH:mm:ss") + }, + { + label: "操作", + fixed: "right", + width: 100, + slot: "operation" + } + ]; + + const handleSizeChange = async (val: number) => { + const res = await getUserOperationsAPI({ + page: pagination.currentPage, + pageSize: val + }); + if (res.success) { + dataList.value = res.data.result; + pagination.total = res.data.total; + pagination.currentPage = res.data.page; + } + }; + + const handleCurrentChange = async (val: number) => { + const res = await getUserOperationsAPI({ + page: val, + pageSize: pagination.pageSize + }); + if (res.success) { + dataList.value = res.data.result; + pagination.total = res.data.total; + pagination.currentPage = res.data.page; + } + }; + + /** 当CheckBox选择项发生变化时会触发该事件 */ + const handleSelectionChange = val => { + selectedNum.value = val.length; + // 重置表格高度 + tableRef.value.setAdaptive(); + }; + + /** 取消选择 */ + const onSelectionCancel = () => { + selectedNum.value = 0; + // 用于多选表格,清空用户的选择 + tableRef.value.getTableRef().clearSelection(); + }; + + /** 批量删除 */ + const onbatchDel = () => { + // 返回当前选中的行 + const curSelected = tableRef.value.getTableRef().getSelectionRows(); + // 接下来根据实际业务,通过选中行的某项数据,比如下面的id,调用接口进行批量删除 + message(`已删除序号为 ${getKeyList(curSelected, "id")} 的数据`, { + type: "success" + }); + tableRef.value.getTableRef().clearSelection(); + onSearch(); + }; + + /** 清空日志 */ + const clearAll = async () => { + // 根据实际业务,调用接口删除所有日志数据 + message("已删除所有日志数据", { + type: "success" + }); + await onSearch(); + }; + + const onSearch = async () => { + loading.value = true; + const res = await getUserOperationsAPI({ + page: pagination.currentPage, + pageSize: pagination.pageSize + }); + if (res.success) { + dataList.value = res.data.result; + pagination.total = res.data.total; + pagination.currentPage = res.data.page; + } + + setTimeout(() => { + loading.value = false; + }, 500); + }; + + const resetForm = formEl => { + if (!formEl) return; + formEl.resetFields(); + onSearch(); + }; + + const onDetailHandle = () => { + addDialog({ + title: `详情`, + props: {}, + width: "40%", + draggable: true, + fullscreenIcon: true, + closeOnClickModal: false, + contentRenderer: () => h(Detail, {}), + beforeSure: (done, {}) => { + done(); + } + }); + }; + + onMounted(async () => { + await onSearch(); + }); + + return { + form, + loading, + columns, + dataList, + pagination, + selectedNum, + onSearch, + clearAll, + resetForm, + onbatchDel, + getOperationName, + onDetailHandle, + handleSizeChange, + onSelectionCancel, + handleCurrentChange, + handleSelectionChange + }; +} diff --git a/src/views/monitor/operation/index.vue b/src/views/monitor/operation/index.vue new file mode 100644 index 0000000..4673718 --- /dev/null +++ b/src/views/monitor/operation/index.vue @@ -0,0 +1,320 @@ + + + + +