feat: 操作日志

This commit is contained in:
dap 2024-09-26 08:40:16 +08:00
parent 066a1a0f97
commit dcb6300ec7
9 changed files with 401 additions and 65 deletions

View File

@ -0,0 +1,40 @@
import type { OnlineUser } from './model';
import type { PageQuery } from '#/api/common';
import { requestClient } from '#/api/request';
enum Api {
onlineList = '/monitor/online/list',
root = '/monitor/online',
}
/**
* 线 使
* @returns OnlineUser[]
*/
export function onlineDeviceList() {
return requestClient.get<OnlineUser[]>(Api.root);
}
export function onlineList(params?: PageQuery) {
return requestClient.get<OnlineUser[]>(Api.onlineList, { params });
}
/**
* 线
* @param tokenId token
* @returns void
*/
export function forceLogout(tokenId: string) {
return requestClient.delete<void>(`${Api.root}/${tokenId}`);
}
/**
* Post
* @param tokenId token
* @returns void
*/
export function forceLogout2(tokenId: string) {
return requestClient.post<void>(`${Api.root}/${tokenId}`);
}

View File

@ -0,0 +1,10 @@
export interface OnlineUser {
tokenId: string;
deptName: string;
userName: string;
ipaddr: string;
loginLocation: string;
browser: string;
os: string;
loginTime: number;
}

View File

@ -0,0 +1,31 @@
import type { OperationLog } from './model';
import type { IDS, PageQuery, PageResult } from '#/api/common';
import { commonExport } from '#/api/helper';
import { requestClient } from '#/api/request';
enum Api {
operLogClean = '/monitor/operlog/clean',
operLogExport = '/monitor/operlog/export',
operLogList = '/monitor/operlog/list',
root = '/monitor/operlog',
}
export function operLogList(params?: PageQuery) {
return requestClient.get<PageResult<OperationLog>>(Api.operLogList, {
params,
});
}
export function operLogDelete(operIds: IDS) {
return requestClient.deleteWithMsg<void>(`${Api.root}/${operIds}`);
}
export function operLogClean() {
return requestClient.deleteWithMsg<void>(Api.operLogClean);
}
export function operLogExport(data: any) {
return commonExport(Api.operLogExport, data);
}

View File

@ -0,0 +1,21 @@
export interface OperationLog {
operId: string;
tenantId: string;
title: string;
businessType: number;
businessTypes?: any;
method: string;
requestMethod: string;
operatorType: number;
operName: string;
deptName: string;
operUrl: string;
operIp: string;
operLocation: string;
operParam: string;
jsonResult: string;
status: number;
errorMsg: string;
operTime: string;
costTime: number;
}

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import type { Recordable } from '@vben/types';
import { useVbenDrawer } from '@vben/common-ui';
import { Description, useDescription } from '#/components/description';
import { descSchema } from './data';
const [BasicDrawer, drawerApi] = useVbenDrawer({
onOpenChange: handleOpenChange,
});
const [registerDescription, { setDescProps }] = useDescription({
column: 1,
schema: descSchema,
});
function handleOpenChange(open: boolean) {
if (!open) {
return null;
}
const { record } = drawerApi.getData() as { record: Recordable<any> };
setDescProps({ data: record }, true);
}
</script>
<template>
<BasicDrawer :footer="false" class="w-[700px]" title="查看日志">
<Description @register="registerDescription" />
</BasicDrawer>
</template>

View File

@ -1,55 +0,0 @@
import type { FormSchemaGetter } from '#/adapter';
import { DictEnum } from '@vben/constants';
import { getDictOptions } from '#/utils/dict';
export const querySchema: FormSchemaGetter = () => [
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'title',
label: '系统模块',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'operName',
label: '操作人员',
},
{
component: 'Select',
componentProps: {
options: getDictOptions(DictEnum.SYS_OPER_TYPE),
placeholder: '请选择',
},
fieldName: 'businessType',
label: '操作类型',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'operIp',
label: '操作IP',
},
{
component: 'Select',
componentProps: {
options: getDictOptions(DictEnum.SYS_COMMON_STATUS),
placeholder: '请选择',
},
fieldName: 'status',
label: '状态',
},
{
component: 'RangePicker',
fieldName: 'createTime',
label: '操作时间',
},
];

View File

@ -0,0 +1,223 @@
import type { ColumnType } from 'ant-design-vue/es/table';
import type { FormSchemaGetter } from '#/adapter';
import type { DescItem } from '#/components/description';
import { DictEnum } from '@vben/constants';
import { Tag } from 'ant-design-vue';
import { getDictOptions } from '#/utils/dict';
import {
renderDict,
renderHttpMethodTag,
renderJsonPreview,
} from '#/utils/render';
export const querySchema: FormSchemaGetter = () => [
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'title',
label: '系统模块',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'operName',
label: '操作人员',
},
{
component: 'Select',
componentProps: {
options: getDictOptions(DictEnum.SYS_OPER_TYPE),
placeholder: '请选择',
},
fieldName: 'businessType',
label: '操作类型',
},
{
component: 'Input',
componentProps: {
placeholder: '请输入',
},
fieldName: 'operIp',
label: '操作IP',
},
{
component: 'Select',
componentProps: {
options: getDictOptions(DictEnum.SYS_COMMON_STATUS),
placeholder: '请选择',
},
fieldName: 'status',
label: '状态',
},
{
component: 'RangePicker',
fieldName: 'createTime',
label: '操作时间',
},
];
export const columns: ColumnType[] = [
{
align: 'center',
dataIndex: 'title',
title: '系统模块',
},
{
align: 'center',
customRender({ value }) {
return renderDict(value, DictEnum.SYS_OPER_TYPE);
},
dataIndex: 'businessType',
title: '操作类型',
},
{
align: 'center',
dataIndex: 'operName',
title: '操作人员',
},
{
align: 'center',
dataIndex: 'operIp',
title: 'IP地址',
},
{
align: 'center',
dataIndex: 'operLocation',
title: 'IP信息',
},
{
align: 'center',
customRender({ value }) {
return renderDict(value, DictEnum.SYS_COMMON_STATUS);
},
dataIndex: 'status',
title: '操作状态',
},
{
align: 'center',
dataIndex: 'operTime',
sorter: true,
title: '操作日期',
},
{
align: 'center',
customRender({ text }) {
return `${text} ms`;
},
dataIndex: 'costTime',
sorter: true,
title: '操作耗时',
},
{
align: 'center',
dataIndex: 'action',
fixed: 'right',
title: '操作',
},
];
export const descSchema: DescItem[] = [
{
field: 'operId',
label: '日志编号',
},
{
field: 'status',
label: '操作结果',
render(value) {
return renderDict(value, DictEnum.SYS_COMMON_STATUS);
},
},
{
field: 'title',
label: '操作模块',
labelMinWidth: 80,
render(value, { businessType }) {
const operType = renderDict(businessType, DictEnum.SYS_OPER_TYPE);
return (
<div class="flex items-center">
<Tag>{value}</Tag>
{operType}
</div>
);
},
},
{
field: 'operIp',
label: '操作信息',
render(_, data) {
return `账号: ${data.operName} / ${data.deptName} / ${data.operIp} / ${data.operLocation}`;
},
},
{
field: 'operUrl',
label: '请求信息',
render(_, data) {
const { operUrl, requestMethod } = data;
const methodTag = renderHttpMethodTag(requestMethod);
return (
<span>
{methodTag} {operUrl}
</span>
);
},
},
{
field: 'errorMsg',
label: '异常信息',
render(value) {
return <span class="font-bold text-red-600">{value}</span>;
},
show: (data) => {
return data && data.errorMsg !== '';
},
},
{
field: 'method',
label: '方法',
},
{
field: 'operParam',
label: '请求参数',
render(value) {
return (
<div class="max-h-[300px] overflow-y-auto">
{renderJsonPreview(value)}
</div>
);
},
},
{
field: 'jsonResult',
label: '响应参数',
render(value) {
return (
<div class="max-h-[300px] overflow-y-auto">
{renderJsonPreview(value)}
</div>
);
},
show(data) {
return data && data.jsonResult;
},
},
{
field: 'costTime',
label: '耗时',
render(value) {
return `${value} ms`;
},
},
{
field: 'operTime',
label: '操作时间',
},
];

View File

@ -1,11 +1,20 @@
<script setup lang="ts">
import { Page } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { Card } from 'ant-design-vue';
import type { OperationLog } from '#/api/monitor/operlog/model';
import { onMounted, ref } from 'vue';
import { Page, useVbenDrawer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { Card, Table } from 'ant-design-vue';
import { useVbenForm } from '#/adapter';
import { operLogList } from '#/api/monitor/operlog';
import { querySchema } from './data';
import { columns, querySchema } from './data';
import operationPreviewDrawer from './OperationPreviewDrawer.vue';
const [QueryForm] = useVbenForm({
//
@ -25,6 +34,22 @@ const [QueryForm] = useVbenForm({
},
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
});
const dataSource = ref<OperationLog[]>([]);
onMounted(async () => {
const resp = await operLogList({ pageNum: 1, pageSize: 30 });
dataSource.value = resp.rows;
});
const [OperationPreviewDrawer, drawerApi] = useVbenDrawer({
connectedComponent: operationPreviewDrawer,
});
function handlePreview(record: Recordable<any>) {
drawerApi.setData({ record });
drawerApi.open();
}
</script>
<template>
@ -32,5 +57,16 @@ const [QueryForm] = useVbenForm({
<Card>
<QueryForm />
</Card>
<div class="bg-background"></div>
<Table :columns="columns" :data-source="dataSource">
<template #bodyCell="{ record, column }">
<template v-if="column.dataIndex === 'action'">
<a-button size="small" type="link" @click="handlePreview(record)">
{{ $t('pages.common.preview') }}
</a-button>
</template>
</template>
</Table>
<OperationPreviewDrawer />
</Page>
</template>

View File

@ -19,6 +19,7 @@
"execa",
"Gitee",
"iconify",
"iconoir",
"intlify",
"ipaddr",
"lockb",
@ -26,12 +27,14 @@
"lucide",
"mkdist",
"mockjs",
"naiveui",
"nocheck",
"noopener",
"noreferrer",
"nprogress",
"nuxt",
"oper",
"operlog",
"pinia",
"prefixs",
"publint",
@ -43,6 +46,7 @@
"styl",
"taze",
"ui-kit",
"uicons",
"unplugin",
"unref",
"vben",
@ -51,13 +55,7 @@
"vitepress",
"vnode",
"vueuse",
"yxxx",
"echarts",
"sortablejs",
"etag",
"naiveui",
"uicons",
"iconoir"
"yxxx"
],
"ignorePaths": [
"**/node_modules/**",