From 61a980a37d42fb6d3df12c454e43825eea87af7a Mon Sep 17 00:00:00 2001 From: valarchie <343928303@qq.com> Date: Sat, 15 Jul 2023 17:50:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/optimize.ts | 1 + package.json | 3 +- pnpm-lock.yaml | 89 ++++++ src/api/system/log.ts | 52 ++++ src/main.ts | 6 +- src/utils/common.ts | 66 ++++- src/utils/http/index.ts | 2 + .../system/log/operationLog/description.vue | 93 +++++++ src/views/system/log/operationLog/index.vue | 226 ++++++++++++++++ .../system/log/operationLog/utils/hook.tsx | 254 ++++++++++++++++++ src/views/system/notice/utils/hook.tsx | 2 +- 11 files changed, 788 insertions(+), 6 deletions(-) create mode 100644 src/api/system/log.ts create mode 100644 src/views/system/log/operationLog/description.vue create mode 100644 src/views/system/log/operationLog/index.vue create mode 100644 src/views/system/log/operationLog/utils/hook.tsx diff --git a/build/optimize.ts b/build/optimize.ts index f069907..cb6c1d7 100644 --- a/build/optimize.ts +++ b/build/optimize.ts @@ -7,6 +7,7 @@ const include = [ "qs", "mitt", + "xlsx", "dayjs", "axios", "pinia", diff --git a/package.json b/package.json index 756321e..62e91d1 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "typeit": "^8.7.1", "vue": "^3.3.4", "vue-router": "^4.2.2", - "vue-types": "^5.1.0" + "vue-types": "^5.1.0", + "xlsx": "^0.18.5" }, "devDependencies": { "@commitlint/cli": "^17.6.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73f0f21..bbbdae6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,6 +88,7 @@ specifiers: vue-router: ^4.2.2 vue-tsc: ^1.8.1 vue-types: ^5.1.0 + xlsx: ^0.18.5 dependencies: "@pureadmin/descriptions": 1.1.1_element-plus@2.3.6 @@ -117,6 +118,7 @@ dependencies: vue: 3.3.4 vue-router: 4.2.2_vue@3.3.4 vue-types: 5.1.0_vue@3.3.4 + xlsx: 0.18.5 devDependencies: "@commitlint/cli": 17.6.6 @@ -2310,6 +2312,14 @@ packages: engines: { node: ">=0.4.0" } hasBin: true + /adler-32/1.3.1: + resolution: + { + integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A== + } + engines: { node: ">=0.8" } + dev: false + /agent-base/6.0.2: resolution: { @@ -2732,6 +2742,17 @@ packages: integrity: sha512-sdQZOJdmt3GJs1UMNpCCCyeuS2IEGLXnHyAo9yIO5JJDjbjoVRij4M1qep6P6gFpptD1PqIYgzM+gwJbOi92mw== } + /cfb/1.2.2: + resolution: + { + integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA== + } + engines: { node: ">=0.8" } + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + dev: false + /chalk/2.4.2: resolution: { @@ -2870,6 +2891,14 @@ packages: hasBin: true dev: true + /codepage/1.15.0: + resolution: + { + integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA== + } + engines: { node: ">=0.8" } + dev: false + /color-convert/1.9.3: resolution: { @@ -3105,6 +3134,15 @@ packages: path-type: 4.0.0 dev: true + /crc-32/1.2.2: + resolution: + { + integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + } + engines: { node: ">=0.8" } + hasBin: true + dev: false + /create-require/1.1.1: resolution: { @@ -4343,6 +4381,14 @@ packages: mime-types: 2.1.35 dev: false + /frac/1.1.2: + resolution: + { + integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA== + } + engines: { node: ">=0.8" } + dev: false + /fraction.js/4.2.0: resolution: { @@ -7965,6 +8011,16 @@ packages: readable-stream: 3.6.2 dev: true + /ssf/0.11.2: + resolution: + { + integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g== + } + engines: { node: ">=0.8" } + dependencies: + frac: 1.1.2 + dev: false + /stable/0.1.8: resolution: { @@ -9210,6 +9266,14 @@ packages: isexe: 2.0.0 dev: true + /wmf/1.0.2: + resolution: + { + integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw== + } + engines: { node: ">=0.8" } + dev: false + /word-wrap/1.2.3: resolution: { @@ -9218,6 +9282,14 @@ packages: engines: { node: ">=0.10.0" } dev: true + /word/0.3.0: + resolution: + { + integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA== + } + engines: { node: ">=0.8" } + dev: false + /wrap-ansi/6.2.0: resolution: { @@ -9271,6 +9343,23 @@ packages: signal-exit: 4.0.2 dev: true + /xlsx/0.18.5: + resolution: + { + integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ== + } + engines: { node: ">=0.8" } + hasBin: true + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + dev: false + /xml-name-validator/4.0.0: resolution: { diff --git a/src/api/system/log.ts b/src/api/system/log.ts new file mode 100644 index 0000000..0f6e4d6 --- /dev/null +++ b/src/api/system/log.ts @@ -0,0 +1,52 @@ +import { http } from "@/utils/http"; + +export interface OperationLogsQuery extends BasePageQuery { + businessType?: string; + requestModule?: string; + status?: string; + username?: string; +} + +export interface OperationLogDTO { + businessType?: number; + businessTypeStr?: string; + calledMethod?: string; + deptId?: number; + deptName?: string; + errorStack?: string; + operationId?: number; + operationParam?: string; + operationResult?: string; + operationTime?: Date; + operatorIp?: string; + operatorLocation?: string; + operatorType?: number; + operatorTypeStr?: string; + requestMethod?: string; + requestModule?: string; + requestUrl?: string; + status?: number; + statusStr?: string; + userId?: number; + username?: string; +} + +/** 获取操作日志列表 */ +export const getOperationLogListApi = (params?: OperationLogsQuery) => { + return http.request>>( + "get", + "/logs/operationLogs", + { + params + } + ); +}; + +export const deleteOperationLogApi = (data: Array) => { + return http.request>("delete", "/logs/operationLogs", { + params: { + // 需要将数组转换为字符串 否则Axios会将参数变成 noticeIds[0]:1 noticeIds[1]:2 这种格式,后端接收参数不成功 + operationIds: data.toString() + } + }); +}; diff --git a/src/main.ts b/src/main.ts index c2a6149..f16cbaf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,7 +9,7 @@ import { MotionPlugin } from "@vueuse/motion"; import { injectResponsiveStorage } from "@/utils/responsive"; import Table from "@pureadmin/table"; -// import PureDescriptions from "@pureadmin/descriptions"; +import PureDescriptions from "@pureadmin/descriptions"; // 引入重置样式 import "./style/reset.scss"; @@ -53,7 +53,7 @@ getServerConfig(app).then(async config => { .use(MotionPlugin) .use(ElementPlus) // .use(useEcharts); - .use(Table); - // .use(PureDescriptions); + .use(Table) + .use(PureDescriptions); app.mount("#app"); }); diff --git a/src/utils/common.ts b/src/utils/common.ts index a90d719..29630db 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,5 +1,7 @@ -import { PaginationProps } from "@pureadmin/table"; +import { PaginationProps, TableColumn } from "@pureadmin/table"; import { Sort } from "element-plus"; +import { utils, writeFile } from "xlsx"; +import { message } from "./message"; export class CommonUtils { static getBeginTimeSafely(timeRange: string[]): string { @@ -50,6 +52,68 @@ export class CommonUtils { baseQuery.orderDirection = sort.order; } + /** 适用于BaseQuery中固定的时间参数 beginTime和endTime参数 */ + static fillTimeRangeParams(baseQuery: any, timeRange: string[]) { + if (timeRange == null || timeRange.length == 0 || timeRange === undefined) { + baseQuery["beginTime"] = undefined; + baseQuery["endTime"] = undefined; + return; + } + + if (baseQuery == null || baseQuery === undefined) { + return; + } + + baseQuery["beginTime"] = CommonUtils.getBeginTimeSafely(timeRange); + baseQuery["endTime"] = CommonUtils.getEndTimeSafely(timeRange); + } + + static exportExcel( + columns: TableColumnList, + originalDataList: any[], + excelName: string + ) { + if ( + !Array.isArray(columns) || + !Array.isArray(originalDataList) || + typeof excelName !== "string" + ) { + message("参数异常,导出失败", { type: "error" }); + return; + } + + // columns和dataList为空的话 弹出提示 不执行导出 + if (columns.length === 0 || originalDataList.length === 0) { + message("无法导出空列表", { type: "warning" }); + return; + } + + const titleList: string[] = []; + const dataKeyList: string[] = []; + // 把columns里面的label取出来作为excel的列标题,把prop取出来等下从dataList里面根据作为key取对象中的值 + columns.forEach((column: TableColumn) => { + if (column.label && column.prop) { + titleList.push(column.label); + dataKeyList.push(column.prop as string); + } + }); + + const excelDataList: string[][] = originalDataList.map(item => { + const arr = []; + dataKeyList.forEach(dataKey => { + arr.push(item[dataKey]); + }); + return arr; + }); + + excelDataList.unshift(titleList); + + const workSheet = utils.aoa_to_sheet(excelDataList); + const workBook = utils.book_new(); + utils.book_append_sheet(workBook, workSheet, excelName); + writeFile(workBook, `${excelName}.xlsx`); + } + // 私有构造函数,防止类被实例化 private constructor() {} } diff --git a/src/utils/http/index.ts b/src/utils/http/index.ts index 92d6811..a06a49e 100644 --- a/src/utils/http/index.ts +++ b/src/utils/http/index.ts @@ -125,6 +125,8 @@ class PureHttp { .catch(() => { message("取消重新登录", { type: "info" }); }); + NProgress.done(); + return Promise.reject(response.data.msg); } else { // 其余情况弹出错误提示框 message(response.data.msg, { type: "error" }); diff --git a/src/views/system/log/operationLog/description.vue b/src/views/system/log/operationLog/description.vue new file mode 100644 index 0000000..74da416 --- /dev/null +++ b/src/views/system/log/operationLog/description.vue @@ -0,0 +1,93 @@ + + + + diff --git a/src/views/system/log/operationLog/index.vue b/src/views/system/log/operationLog/index.vue new file mode 100644 index 0000000..40a2729 --- /dev/null +++ b/src/views/system/log/operationLog/index.vue @@ -0,0 +1,226 @@ + + + + + diff --git a/src/views/system/log/operationLog/utils/hook.tsx b/src/views/system/log/operationLog/utils/hook.tsx new file mode 100644 index 0000000..28cfbe4 --- /dev/null +++ b/src/views/system/log/operationLog/utils/hook.tsx @@ -0,0 +1,254 @@ +import dayjs from "dayjs"; +import descriptionForm from "../description.vue"; +import { message } from "@/utils/message"; +import { addDialog, closeDialog } from "@/components/ReDialog"; +import { ElMessageBox, Sort } from "element-plus"; +import { OperationLogsQuery, getOperationLogListApi } from "@/api/system/log"; +import { reactive, ref, onMounted, h, toRaw } from "vue"; +import { useUserStoreHook } from "@/store/modules/user"; +import { deleteOperationLogApi } from "@/api/system/log"; +import { CommonUtils } from "@/utils/common"; + +const operationLogStatusMap = + useUserStoreHook().dictionaryMap["sysOperationLog.status"]; +const businessTypeMap = + useUserStoreHook().dictionaryMap["sysOperationLog.businessType"]; + +export function useOperationLogHook() { + const defaultSort: Sort = { + prop: "operationTime", + order: "descending" + }; + + const pagination: PaginationProps = { + total: 0, + pageSize: 10, + currentPage: 1, + background: true + }; + + const timeRange = ref([]); + + const searchFormParams = reactive({ + beginTime: undefined, + endTime: undefined, + businessType: undefined, + requestModule: undefined, + status: undefined, + username: undefined, + timeRangeColumn: defaultSort.prop + }); + + const dataList = ref([]); + const pageLoading = ref(true); + const multipleSelection = ref([]); + + const columns: TableColumnList = [ + { + type: "selection", + align: "left" + }, + { + label: "操作编号", + prop: "operationId", + minWidth: 100 + }, + { + label: "业务模块", + prop: "requestModule", + minWidth: 120 + }, + { + label: "操作类型", + prop: "businessType", + minWidth: 120, + cellRenderer: ({ row, props }) => ( + + {businessTypeMap[row.businessType].label} + + ) + }, + { + label: "请求方式", + prop: "requestMethod", + minWidth: 120 + }, + { + label: "操作人员", + prop: "username", + minWidth: 120 + }, + { + label: "登录地址", + prop: "operatorIp", + minWidth: 120 + }, + { + label: "状态", + prop: "status", + minWidth: 120, + cellRenderer: ({ row, props }) => ( + + {operationLogStatusMap[row.status].label} + + ) + }, + { + label: "状态名", + prop: "statusStr", + minWidth: 120, + hide: true + }, + { + label: "操作时间", + minWidth: 160, + prop: "operationTime", + sortable: "custom", + formatter: ({ operationTime }) => + dayjs(operationTime).format("YYYY-MM-DD HH:mm:ss") + }, + { + label: "操作", + fixed: "right", + width: 140, + slot: "operation" + } + ]; + + async function onSearch() { + // 点击搜索的时候 需要重置分页 + pagination.currentPage = 1; + getOperationLogList(); + } + + function resetForm(formEl, tableRef) { + if (!formEl) return; + // 清空查询参数 + formEl.resetFields(); + // 清空排序 + searchFormParams.orderColumn = undefined; + searchFormParams.orderDirection = undefined; + // 清空时间查询 TODO 这块有点繁琐 有可以优化的地方吗? + // Form组件的resetFields方法无法清除datepicker里面的数据。 + timeRange.value = []; + searchFormParams.beginTime = undefined; + searchFormParams.endTime = undefined; + tableRef.getTableRef().clearSort(); + // 重置分页并查询 + onSearch(); + } + + async function getOperationLogList(sort: Sort = defaultSort) { + pageLoading.value = true; + if (sort != null) { + CommonUtils.fillSortParams(searchFormParams, sort); + } + CommonUtils.fillPaginationParams(searchFormParams, pagination); + CommonUtils.fillTimeRangeParams(searchFormParams, timeRange.value); + + const { data } = await getOperationLogListApi(toRaw(searchFormParams)); + dataList.value = data.rows; + pagination.total = data.total; + + setTimeout(() => { + pageLoading.value = false; + }, 500); + } + + async function handleDelete(row) { + await deleteOperationLogApi([row.operationId]).then(() => { + message(`您删除了操作编号为${row.operationId}的这条数据`, { + type: "success" + }); + // 刷新列表 + getOperationLogList(); + }); + } + + async function handleBulkDelete(tableRef) { + if (multipleSelection.value.length === 0) { + message("请选择需要删除的数据", { type: "warning" }); + return; + } + + ElMessageBox.confirm( + `确认要删除编号为[ ${multipleSelection.value} ]的日志吗?`, + "系统提示", + { + confirmButtonText: "确定", + cancelButtonText: "取消", + type: "warning", + dangerouslyUseHTMLString: true, + draggable: true + } + ) + .then(async () => { + await deleteOperationLogApi(multipleSelection.value).then(() => { + message(`您删除了日志编号为[ ${multipleSelection.value} ]的数据`, { + type: "success" + }); + // 刷新列表 + getOperationLogList(); + }); + }) + .catch(() => { + message("取消删除", { + type: "info" + }); + // 清空checkbox选择的数据 + tableRef.getTableRef().clearSelection(); + }); + } + + function openDialog(row) { + addDialog({ + title: "日志详情", + width: "60%", + draggable: true, + fullscreenIcon: false, + closeOnClickModal: true, + contentRenderer: () => h(descriptionForm, toRaw(row)), + footerButtons: [ + { + label: "关闭", + text: true, + size: "large", + bg: true, + btnClick: ({ dialog: { options, index } }) => { + closeDialog(options, index); + } + } + ] + }); + } + + onMounted(() => { + getOperationLogList(); + }); + + return { + searchFormParams, + pageLoading, + columns, + dataList, + pagination, + defaultSort, + timeRange, + multipleSelection, + onSearch, + // exportExcel, + getOperationLogList, + resetForm, + openDialog, + handleDelete, + handleBulkDelete + }; +} diff --git a/src/views/system/notice/utils/hook.tsx b/src/views/system/notice/utils/hook.tsx index 0d7adcb..39a97aa 100644 --- a/src/views/system/notice/utils/hook.tsx +++ b/src/views/system/notice/utils/hook.tsx @@ -190,7 +190,7 @@ export function useNoticeHook() { ) .then(async () => { await deleteSystemNoticeApi(multipleSelection.value).then(() => { - message(`您删除了通知编号为[ ${multipleSelection.value} ]的条数据`, { + message(`您删除了通知编号为[ ${multipleSelection.value} ]的数据`, { type: "success" }); // 刷新列表