feat: 操作日志
This commit is contained in:
parent
066a1a0f97
commit
dcb6300ec7
40
apps/web-antd/src/api/monitor/online/index.ts
Normal file
40
apps/web-antd/src/api/monitor/online/index.ts
Normal 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}`);
|
||||
}
|
10
apps/web-antd/src/api/monitor/online/model.d.ts
vendored
Normal file
10
apps/web-antd/src/api/monitor/online/model.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
export interface OnlineUser {
|
||||
tokenId: string;
|
||||
deptName: string;
|
||||
userName: string;
|
||||
ipaddr: string;
|
||||
loginLocation: string;
|
||||
browser: string;
|
||||
os: string;
|
||||
loginTime: number;
|
||||
}
|
31
apps/web-antd/src/api/monitor/operlog/index.ts
Normal file
31
apps/web-antd/src/api/monitor/operlog/index.ts
Normal 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);
|
||||
}
|
21
apps/web-antd/src/api/monitor/operlog/model.d.ts
vendored
Normal file
21
apps/web-antd/src/api/monitor/operlog/model.d.ts
vendored
Normal 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;
|
||||
}
|
@ -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>
|
@ -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: '操作时间',
|
||||
},
|
||||
];
|
223
apps/web-antd/src/views/monitor/operlog/data.tsx
Normal file
223
apps/web-antd/src/views/monitor/operlog/data.tsx
Normal 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: '操作时间',
|
||||
},
|
||||
];
|
@ -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>
|
||||
|
12
cspell.json
12
cspell.json
@ -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/**",
|
||||
|
Loading…
Reference in New Issue
Block a user