mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2025-09-09 08:57:14 +08:00
feat: 新增系统日志管理(登录日志、操作日志)
This commit is contained in:
@@ -2,9 +2,11 @@ export * from './area'
|
||||
export * from './auth'
|
||||
export * from './common'
|
||||
export * from './monitor'
|
||||
export * from './system'
|
||||
|
||||
export * from './area/type'
|
||||
export * from './auth/type'
|
||||
export * from './common/type'
|
||||
export * from './monitor/type'
|
||||
export * from './system/type'
|
||||
|
||||
|
1
src/apis/system/index.ts
Normal file
1
src/apis/system/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './log'
|
14
src/apis/system/log.ts
Normal file
14
src/apis/system/log.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import http from '@/utils/http'
|
||||
import type * as System from './type'
|
||||
|
||||
const BASE_URL = '/system/log'
|
||||
|
||||
/** @desc 查询日志列表 */
|
||||
export function listLog(query: System.LogQuery) {
|
||||
return http.get<PageRes<System.LogResp[]>>(`${BASE_URL}`, query)
|
||||
}
|
||||
|
||||
/** @desc 查询日志详情 */
|
||||
export function getLog(id: string) {
|
||||
return http.get<System.LogDetailResp>(`${BASE_URL}/${id}`)
|
||||
}
|
33
src/apis/system/type.ts
Normal file
33
src/apis/system/type.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/** 系统日志类型 */
|
||||
export interface LogResp {
|
||||
id: string
|
||||
description: string
|
||||
module: string
|
||||
timeTaken: number
|
||||
ip: string
|
||||
address: string
|
||||
browser: string
|
||||
os: string
|
||||
status: number
|
||||
errorMsg: string
|
||||
createUserString: string
|
||||
createTime: string
|
||||
}
|
||||
export interface LogDetailResp extends LogResp {
|
||||
traceId: string
|
||||
requestUrl: string
|
||||
requestMethod: string
|
||||
requestHeaders: string
|
||||
requestBody: string
|
||||
statusCode: number
|
||||
responseHeaders: string
|
||||
responseBody: string
|
||||
}
|
||||
export interface LogQuery extends PageQuery {
|
||||
description?: string
|
||||
module?: string
|
||||
ip?: string
|
||||
createUserString?: string
|
||||
createTime?: string
|
||||
status?: number
|
||||
}
|
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios'
|
||||
import qs from 'query-string'
|
||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { useUserStore } from '@/stores'
|
||||
import { getToken } from '@/utils/auth'
|
||||
@@ -8,7 +9,6 @@ import notificationErrorWrapper from '@/utils/notification-error-wrapper'
|
||||
import NProgress from 'nprogress'
|
||||
import 'nprogress/nprogress.css'
|
||||
import router from '@/router'
|
||||
import qs from 'query-string'
|
||||
|
||||
NProgress.configure({ showSpinner: false }) // NProgress Configuration
|
||||
|
||||
@@ -114,7 +114,15 @@ const request = <T = unknown>(config: AxiosRequestConfig): Promise<ApiRes<T>> =>
|
||||
http
|
||||
.request<T>(config)
|
||||
.then((res: AxiosResponse) => resolve(res.data))
|
||||
.catch((err: { message: string }) => reject(err))
|
||||
.catch((err: { msg: string }) => reject(err))
|
||||
})
|
||||
}
|
||||
|
||||
const requestNative = <T = unknown>(config: AxiosRequestConfig): Promise<AxiosResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http
|
||||
.request<T>(config)
|
||||
.catch((err: { msg: string }) => reject(err))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -166,4 +174,4 @@ const del = <T = any>(url: string, params?: object, config?: AxiosRequestConfig)
|
||||
})
|
||||
}
|
||||
|
||||
export default { get, post, put, patch, del }
|
||||
export default { get, post, put, patch, del, request, requestNative }
|
||||
|
115
src/views/system/log/LoginLog.vue
Normal file
115
src/views/system/log/LoginLog.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<GiTable
|
||||
row-key="id"
|
||||
:data="dataList"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
|
||||
:pagination="pagination"
|
||||
:disabledTools="['setting']"
|
||||
@refresh="search"
|
||||
>
|
||||
<template #custom-left>
|
||||
<a-input v-model="queryForm.createUserString" placeholder="请输入登录用户" allow-clear @change="search">
|
||||
<template #prefix><icon-search /></template>
|
||||
</a-input>
|
||||
<a-input v-model="queryForm.ip" placeholder="请输入登录 IP 或地点" allow-clear @change="search">
|
||||
<template #prefix><icon-search /></template>
|
||||
</a-input>
|
||||
<DateRangePicker v-model="queryForm.createTime" @change="search" />
|
||||
<a-button @click="reset">重置</a-button>
|
||||
</template>
|
||||
<template #custom-right>
|
||||
<a-tooltip content="导出">
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<icon-download />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-tag v-if="record.status === 1" color="green">
|
||||
<GiDot type="success" style="width: 5px; height: 5px" />
|
||||
<span style="margin-left: 5px">成功</span>
|
||||
</a-tag>
|
||||
<a-tooltip v-else :content="record.errorMsg">
|
||||
<a-tag color="red" style="cursor: pointer">
|
||||
<GiDot type="danger" style="width: 5px; height: 5px" />
|
||||
<span style="margin-left: 5px">失败</span>
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</GiTable>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { listLog } from '@/apis'
|
||||
import type { TableInstance } from '@arco-design/web-vue'
|
||||
import DateRangePicker from '@/components/DateRangePicker/index.vue'
|
||||
import { useTable } from '@/hooks'
|
||||
|
||||
defineOptions({ name: 'LoginLog' })
|
||||
|
||||
const columns: TableInstance['columns'] = [
|
||||
{
|
||||
title: '序号',
|
||||
width: 66,
|
||||
align: 'center',
|
||||
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize)
|
||||
},
|
||||
{ title: '登录时间', dataIndex: 'createTime', width: 180 },
|
||||
{ title: '用户昵称', dataIndex: 'createUserString', ellipsis: true, tooltip: true },
|
||||
{ title: '登录行为', dataIndex: 'description' },
|
||||
{
|
||||
title: '状态',
|
||||
slotName: 'status',
|
||||
align: 'center',
|
||||
filterable: {
|
||||
filters: [
|
||||
{
|
||||
text: '成功',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
text: '失败',
|
||||
value: 2
|
||||
}
|
||||
],
|
||||
filter: (value, record) => record.status == value,
|
||||
alignLeft: true
|
||||
}
|
||||
},
|
||||
{ title: '登录 IP', dataIndex: 'ip', ellipsis: true, tooltip: true },
|
||||
{ title: '登录地点', dataIndex: 'address', ellipsis: true, tooltip: true },
|
||||
{ title: '浏览器', dataIndex: 'browser', ellipsis: true, tooltip: true },
|
||||
{ title: '终端系统', dataIndex: 'os', ellipsis: true, tooltip: true }
|
||||
]
|
||||
|
||||
const queryForm = reactive({
|
||||
module: '登录',
|
||||
ip: undefined,
|
||||
createUserString: undefined,
|
||||
createTime: undefined,
|
||||
status: undefined,
|
||||
sort: ['createTime,desc']
|
||||
})
|
||||
|
||||
const {
|
||||
tableData: dataList,
|
||||
loading,
|
||||
pagination,
|
||||
search
|
||||
} = useTable((p) => listLog({ ...queryForm, page: p.page, size: p.size }), { immediate: true })
|
||||
|
||||
// 重置
|
||||
const reset = () => {
|
||||
queryForm.ip = undefined
|
||||
queryForm.createUserString = undefined
|
||||
queryForm.createTime = undefined
|
||||
queryForm.status = undefined
|
||||
search()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
136
src/views/system/log/OperationLog.vue
Normal file
136
src/views/system/log/OperationLog.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<GiTable
|
||||
row-key="id"
|
||||
:data="dataList"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
|
||||
:pagination="pagination"
|
||||
column-resizable
|
||||
:disabledTools="['setting']"
|
||||
@refresh="search"
|
||||
>
|
||||
<template #custom-left>
|
||||
<a-input v-model="queryForm.createUserString" placeholder="请输入操作人" allow-clear @change="search">
|
||||
<template #prefix><icon-search /></template>
|
||||
</a-input>
|
||||
<a-input v-model="queryForm.ip" placeholder="请输入操作 IP 或地点" allow-clear @change="search">
|
||||
<template #prefix><icon-search /></template>
|
||||
</a-input>
|
||||
<DateRangePicker v-model="queryForm.createTime" @change="search" />
|
||||
<a-button @click="reset">重置</a-button>
|
||||
</template>
|
||||
<template #custom-right>
|
||||
<a-tooltip content="导出">
|
||||
<a-button>
|
||||
<template #icon>
|
||||
<icon-download />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template #createTime="{ record }">
|
||||
<a-link @click="openDetail(record)">{{ record.createTime }}</a-link>
|
||||
</template>
|
||||
<template #status="{ record }">
|
||||
<a-tag v-if="record.status === 1" color="green">
|
||||
<GiDot type="success" style="width: 5px; height: 5px"></GiDot>
|
||||
<span style="margin-left: 5px">成功</span>
|
||||
</a-tag>
|
||||
<a-tooltip v-else :content="record.errorMsg">
|
||||
<a-tag color="red" style="cursor: pointer">
|
||||
<GiDot type="danger" style="width: 5px; height: 5px"></GiDot>
|
||||
<span style="margin-left: 5px">失败</span>
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template #timeTaken="{ record }">
|
||||
<a-tag v-if="record.timeTaken > 500" color="red">{{ record.timeTaken }}ms</a-tag>
|
||||
<a-tag v-else-if="record.timeTaken > 200" color="orange">{{ record.timeTaken }}ms</a-tag>
|
||||
<a-tag v-else color="green">{{ record.timeTaken }} ms</a-tag>
|
||||
</template>
|
||||
</GiTable>
|
||||
|
||||
<OperationLogDetailDrawer ref="OperationLogDetailDrawerRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { listLog, type LogResp } from '@/apis'
|
||||
import type { TableInstance } from '@arco-design/web-vue'
|
||||
import DateRangePicker from '@/components/DateRangePicker/index.vue'
|
||||
import OperationLogDetailDrawer from './OperationLogDetailDrawer.vue'
|
||||
import { useTable } from '@/hooks'
|
||||
|
||||
defineOptions({ name: 'OperationLog' })
|
||||
|
||||
const columns: TableInstance['columns'] = [
|
||||
{
|
||||
title: '序号',
|
||||
width: 66,
|
||||
align: 'center',
|
||||
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize)
|
||||
},
|
||||
{ title: '操作时间', slotName: 'createTime', width: 180 },
|
||||
{ title: '操作人', dataIndex: 'createUserString', ellipsis: true, tooltip: true },
|
||||
{ title: '操作内容', dataIndex: 'description', ellipsis: true, tooltip: true },
|
||||
{ title: '所属模块', dataIndex: 'module', align: 'center', ellipsis: true, tooltip: true },
|
||||
{
|
||||
title: '状态',
|
||||
slotName: 'status',
|
||||
align: 'center',
|
||||
filterable: {
|
||||
filters: [
|
||||
{
|
||||
text: '成功',
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
text: '失败',
|
||||
value: 2
|
||||
}
|
||||
],
|
||||
filter: (value, record) => record.status == value,
|
||||
alignLeft: true
|
||||
}
|
||||
},
|
||||
{ title: '操作 IP', dataIndex: 'ip', ellipsis: true, tooltip: true },
|
||||
{ title: '操作地点', dataIndex: 'address', ellipsis: true, tooltip: true },
|
||||
{ title: '耗时', slotName: 'timeTaken', align: 'center' },
|
||||
{ title: '浏览器', dataIndex: 'browser', ellipsis: true, tooltip: true },
|
||||
{ title: '终端系统', dataIndex: 'os', ellipsis: true, tooltip: true }
|
||||
]
|
||||
|
||||
const queryForm = reactive({
|
||||
description: undefined,
|
||||
ip: undefined,
|
||||
createUserString: undefined,
|
||||
createTime: undefined,
|
||||
status: undefined,
|
||||
sort: ['createTime,desc']
|
||||
})
|
||||
|
||||
const {
|
||||
loading,
|
||||
tableData: dataList,
|
||||
pagination,
|
||||
search
|
||||
} = useTable((p) => listLog({ ...queryForm, page: p.page, size: p.size }), { immediate: true })
|
||||
|
||||
// 重置查询
|
||||
const reset = () => {
|
||||
queryForm.description = undefined
|
||||
queryForm.ip = undefined
|
||||
queryForm.createUserString = undefined
|
||||
queryForm.createTime = undefined
|
||||
queryForm.status = undefined
|
||||
search()
|
||||
}
|
||||
|
||||
const OperationLogDetailDrawerRef = ref<InstanceType<typeof OperationLogDetailDrawer>>()
|
||||
// 查询详情
|
||||
const openDetail = (item: LogResp) => {
|
||||
OperationLogDetailDrawerRef.value?.open(item.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
127
src/views/system/log/OperationLogDetailDrawer.vue
Normal file
127
src/views/system/log/OperationLogDetailDrawer.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible" title="日志详情" :width="720" :footer="false">
|
||||
<a-descriptions title="基本信息" :column="2" size="large" class="general-description">
|
||||
<a-descriptions-item label="日志 ID">{{ operationLog?.id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="Trace ID">{{ operationLog?.traceId }}</a-descriptions-item>
|
||||
<a-descriptions-item label="操作人">{{ operationLog?.createUserString }}</a-descriptions-item>
|
||||
<a-descriptions-item label="操作时间">{{ operationLog?.createTime }}</a-descriptions-item>
|
||||
<a-descriptions-item label="操作内容">{{ operationLog?.description }}</a-descriptions-item>
|
||||
<a-descriptions-item label="所属模块">{{ operationLog?.module }}</a-descriptions-item>
|
||||
<a-descriptions-item label="操作 IP">{{ operationLog?.ip }}</a-descriptions-item>
|
||||
<a-descriptions-item label="操作地点">{{ operationLog?.address }}</a-descriptions-item>
|
||||
<a-descriptions-item label="浏览器">{{ operationLog?.browser }}</a-descriptions-item>
|
||||
<a-descriptions-item label="终端系统">{{ operationLog?.os }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag v-if="operationLog?.status === 1" color="green">成功</a-tag>
|
||||
<a-tag v-else color="red">失败</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="耗时">
|
||||
<a-tag v-if="operationLog?.timeTaken > 500" color="red">
|
||||
{{ operationLog?.timeTaken }}ms
|
||||
</a-tag>
|
||||
<a-tag v-else-if="operationLog?.timeTaken > 200" color="orange">
|
||||
{{ operationLog?.timeTaken }}ms
|
||||
</a-tag>
|
||||
<a-tag v-else color="green">{{ operationLog?.timeTaken }} ms</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="请求 URI" :span="2">
|
||||
{{ operationLog?.requestUrl }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-descriptions
|
||||
title="响应信息"
|
||||
:column="2"
|
||||
size="large"
|
||||
class="general-description http"
|
||||
style="margin-top: 20px; position: relative"
|
||||
>
|
||||
<a-descriptions-item :span="2">
|
||||
<a-tabs type="card">
|
||||
<a-tab-pane key="1" title="响应头">
|
||||
<VueJsonPretty
|
||||
v-if="operationLog?.responseHeaders"
|
||||
:path="'res'"
|
||||
:data="JSON.parse(operationLog?.responseHeaders)"
|
||||
:show-length="true"
|
||||
/>
|
||||
<span v-else>无</span>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" title="响应体">
|
||||
<VueJsonPretty
|
||||
v-if="operationLog?.responseBody"
|
||||
:path="'res'"
|
||||
:data="JSON.parse(operationLog?.responseBody)"
|
||||
:show-length="true"
|
||||
/>
|
||||
<span v-else>无</span>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<a-descriptions
|
||||
title="请求信息"
|
||||
:column="2"
|
||||
size="large"
|
||||
class="general-description http"
|
||||
style="margin-top: 20px; position: relative"
|
||||
>
|
||||
<a-descriptions-item :span="2">
|
||||
<a-tabs type="card">
|
||||
<a-tab-pane key="1" title="请求头">
|
||||
<VueJsonPretty
|
||||
v-if="operationLog?.requestHeaders"
|
||||
:path="'res'"
|
||||
:data="JSON.parse(operationLog?.requestHeaders)"
|
||||
:show-length="true"
|
||||
/>
|
||||
<span v-else>无</span>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" title="请求体">
|
||||
<VueJsonPretty
|
||||
v-if="operationLog?.requestBody"
|
||||
:path="'res'"
|
||||
:data="JSON.parse(operationLog?.requestBody)"
|
||||
:show-length="true"
|
||||
/>
|
||||
<span v-else>无</span>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getLog, type LogDetailResp } from '@/apis'
|
||||
import VueJsonPretty from 'vue-json-pretty'
|
||||
import 'vue-json-pretty/lib/styles.css'
|
||||
|
||||
const logId = ref('')
|
||||
const operationLog = ref<LogDetailResp | null>()
|
||||
// 查询详情
|
||||
const getOperationLogDetail = async () => {
|
||||
const res = await getLog(logId.value)
|
||||
operationLog.value = res.data
|
||||
}
|
||||
|
||||
const visible = ref(false)
|
||||
// 打开详情
|
||||
const open = async (id: string) => {
|
||||
logId.value = id
|
||||
await getOperationLogDetail()
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.http :deep(.arco-descriptions-item-label-block) {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-content) {
|
||||
padding-top: 5px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
</style>
|
48
src/views/system/log/index.vue
Normal file
48
src/views/system/log/index.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="gi_page">
|
||||
<a-card title="系统日志" class="general-card">
|
||||
<a-tabs type="card-gutter" size="large">
|
||||
<a-tab-pane key="1" title="登录日志">
|
||||
<LoginLog />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" title="操作日志">
|
||||
<OperationLog />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LoginLog from './LoginLog.vue'
|
||||
import OperationLog from './OperationLog.vue'
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.arco-tabs .arco-tabs-nav-type-card-gutter .arco-tabs-tab-active) {
|
||||
box-shadow: inset 0 2px 0 rgb(var(--primary-6)), inset -1px 0 0 var(--color-border-2),
|
||||
inset 1px 0 0 var(--color-border-2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav-type-card-gutter .arco-tabs-tab) {
|
||||
border-radius: var(--border-radius-medium) var(--border-radius-medium) 0 0;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-type-card-gutter > .arco-tabs-content) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav::before) {
|
||||
left: -20px;
|
||||
right: -20px;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav) {
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user