feat: 新增系统日志管理(登录日志、操作日志)

This commit is contained in:
2024-04-09 21:34:56 +08:00
parent d0d1181c49
commit 20184c6e01
9 changed files with 487 additions and 3 deletions

View 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>

View 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>

View 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>

View 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>