refactor: 系统管理/系统日志 => 系统监控/系统日志

This commit is contained in:
2024-04-14 22:51:33 +08:00
parent d2af75fbf9
commit e0378d8a7e
12 changed files with 241 additions and 221 deletions

View File

@@ -1 +1,2 @@
export * from './online'
export * from './log'

24
src/apis/monitor/log.ts Normal file
View File

@@ -0,0 +1,24 @@
import http from '@/utils/http'
import type * as Monitor from './type'
const BASE_URL = '/system/log'
/** @desc 查询日志列表 */
export function listLog(query: Monitor.LogPageQuery) {
return http.get<PageRes<Monitor.LogResp[]>>(`${BASE_URL}`, query)
}
/** @desc 查询日志详情 */
export function getLog(id: string) {
return http.get<Monitor.LogDetailResp>(`${BASE_URL}/${id}`)
}
/** @desc 导出登录日志 */
export function exportLoginLog(query: Monitor.LogQuery) {
return http.download<any>(`${BASE_URL}/export/login`, query)
}
/** @desc 导出操作日志 */
export function exportOperationLog(query: Monitor.LogQuery) {
return http.download<any>(`${BASE_URL}/export/operation`, query)
}

View File

@@ -13,8 +13,43 @@ export interface OnlineUserResp {
createUserString: string
createTime: string
}
export interface OnlineUserQuery extends PageQuery {
nickname?: string
loginTime?: string
}
/** 系统日志类型 */
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{
description?: string
module?: string
ip?: string
createUserString?: string
createTime?: string
status?: number
sort: Array<string>
}
export interface LogPageQuery extends PageQuery, LogQuery {}

View File

@@ -2,7 +2,7 @@ export * from './user'
export * from './role'
export * from './menu'
export * from './dept'
export * from './log'
export * from '../monitor/log'
export * from './dict'
export * from './file'
export * from './storage'

View File

@@ -1,22 +0,0 @@
import http from '@/utils/http'
import type * as System from './type'
const BASE_URL = '/system/log'
/** @desc 查询日志列表 */
export function listLog(query: System.PageLogQuery) {
return http.get<PageRes<System.LogResp[]>>(`${BASE_URL}`, query)
}
/** @desc 查询日志详情 */
export function getLog(id: string) {
return http.get<System.LogDetailResp>(`${BASE_URL}/${id}`)
}
/** @desc 导出日志列表 */
export function exportLog(query: System.LogQuery) {
return http.download<any>(`${BASE_URL}/export/login`, query)
}
/**@desc 导出操作日志 */
export function exportOperateLog(query: System.LogQuery) {
return http.download<any>(`${BASE_URL}/export/operation`, query)
}

View File

@@ -119,43 +119,6 @@ export interface DeptQuery {
sort: Array<string>
}
/** 系统日志类型 */
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 PageLogQuery extends PageQuery,LogQuery{}
// 系统日志查询条件
export interface LogQuery{
description?: string
module?: string
ip?: string
createUserString?: string
createTime?: string
status?: number
}
/** 系统字典类型 */
export interface DictResp {
id: string

View File

@@ -1,29 +1,35 @@
<template>
<div class="json_prettt_container">
<div class="json_pretty_container">
<vue-json-pretty
:path="'res'"
:data="JSONObject"
:show-length="true"
/>
<icon-copy class="copy_icon" @click="onCopy(JSONObject)"/>
:path="'res'"
:data="JSONObject"
:show-length="true"
/>
<icon-copy class="copy_icon" @click="onCopy(JSONObject)" />
</div>
</template>
<script setup lang="ts">
import VueJsonPretty from 'vue-json-pretty'
import 'vue-json-pretty/lib/styles.css'
import {copyText} from '@/utils'
import { copyText } from '@/utils'
defineOptions({ name: 'JsonPretty', inheritAttrs: false })
const props = defineProps<{
josn: string
json: string
}>()
const JSONObject = computed(()=>JSON.parse(props?.josn))
const onCopy =(data:object)=>{
const JSONObject = computed(() => JSON.parse(props?.json))
// 拷贝
const onCopy = (data: object) => {
copyText(JSON.stringify(data))
console.log('copyObject',data)
}
</script>
<style lang="scss" scoped>
.json_prettt_container{
.json_pretty_container{
width: 100%;
height: 100%;
overflow: auto;

View File

@@ -6,8 +6,8 @@
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="pagination"
@filterChange="filterChange"
:disabledTools="['setting']"
@filterChange="filterChange"
@refresh="search"
>
<template #custom-left>
@@ -22,7 +22,7 @@
</template>
<template #custom-right>
<a-tooltip content="导出">
<a-button @click="onExportFile">
<a-button @click="onExport">
<template #icon>
<icon-download />
</template>
@@ -45,22 +45,13 @@
</template>
<script setup lang="ts">
import { listLog,exportLog } from '@/apis'
import { exportLoginLog, listLog } from '@/apis'
import type { TableInstance } from '@arco-design/web-vue'
import DateRangePicker from '@/components/DateRangePicker/index.vue'
import { useTable } from '@/hooks'
import {useDownload} from '@/hooks'
import { useTable, useDownload } from '@/hooks'
defineOptions({ name: 'LoginLog' })
const filterChange = (values,record)=>{
try {
const slotName = columns[values.split('_').pop()].slotName as string
const value = record.join(',')
queryForm[slotName] = value
search()
} catch (error) {
search()
}
}
const columns: TableInstance['columns'] = [
{
title: '序号',
@@ -95,10 +86,7 @@ const columns: TableInstance['columns'] = [
{ title: '浏览器', dataIndex: 'browser', ellipsis: true, tooltip: true },
{ title: '终端系统', dataIndex: 'os', ellipsis: true, tooltip: true }
]
//
const onExportFile = ()=>{
useDownload(()=>exportLog(queryForm))
}
const queryForm = reactive({
module: '登录',
ip: undefined,
@@ -123,6 +111,22 @@ const reset = () => {
queryForm.status = undefined
search()
}
//
const onExport = () => {
useDownload(() => exportLoginLog(queryForm))
}
//
const filterChange = (dataIndex, filteredValues) => {
try {
const slotName = columns[dataIndex.split('_').pop()].slotName as string
queryForm[slotName] = filteredValues.join(',')
search()
} catch (error) {
search()
}
}
</script>
<style lang="scss" scoped></style>

View File

@@ -7,8 +7,8 @@
:scroll="{ x: '100%', y: '100%', minWidth: 1000 }"
:pagination="pagination"
column-resizable
@filterChange="filterChange"
:disabledTools="['setting']"
@filterChange="filterChange"
@refresh="search"
>
<template #custom-left>
@@ -22,8 +22,8 @@
<a-button @click="reset">重置</a-button>
</template>
<template #custom-right>
<a-tooltip content="导出" @click="onExportFile">
<a-button>
<a-tooltip content="导出">
<a-button @click="onExport">
<template #icon>
<icon-download />
</template>
@@ -56,23 +56,14 @@
</template>
<script setup lang="ts">
import { listLog, type LogResp,exportOperateLog } from '@/apis'
import { listLog, exportOperationLog, 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 { useDownload, useTable } from '@/hooks'
import { useTable, useDownload } from '@/hooks'
defineOptions({ name: 'OperationLog' })
const filterChange = (values,record)=>{
try {
const slotName = columns[values.split('_').pop()].slotName as string
const value = record.join(',')
queryForm[slotName] = value
search()
} catch (error) {
search()
}
}
const columns: TableInstance['columns'] = [
{
title: '序号',
@@ -109,10 +100,7 @@ const columns: TableInstance['columns'] = [
{ title: '浏览器', dataIndex: 'browser', ellipsis: true, tooltip: true },
{ title: '终端系统', dataIndex: 'os', ellipsis: true, tooltip: true }
]
//
const onExportFile = ()=>{
useDownload(()=>exportOperateLog(queryForm))
}
const queryForm = reactive({
description: undefined,
ip: undefined,
@@ -139,6 +127,22 @@ const reset = () => {
search()
}
//
const onExport = () => {
useDownload(() => exportOperationLog(queryForm))
}
//
const filterChange = (dataIndex, filteredValues) => {
try {
const slotName = columns[dataIndex.split('_').pop()].slotName as string
queryForm[slotName] = filteredValues.join(',')
search()
} catch (error) {
search()
}
}
const OperationLogDetailDrawerRef = ref<InstanceType<typeof OperationLogDetailDrawer>>()
//
const openDetail = (item: LogResp) => {

View File

@@ -0,0 +1,106 @@
<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">{{ dataDetail?.id }}</a-descriptions-item>
<a-descriptions-item label="Trace ID" >{{ dataDetail?.traceId }}<TextCopy :value="dataDetail?.traceId" /></a-descriptions-item>
<a-descriptions-item label="操作人">{{ dataDetail?.createUserString }}</a-descriptions-item>
<a-descriptions-item label="操作时间">{{ dataDetail?.createTime }}</a-descriptions-item>
<a-descriptions-item label="操作内容">{{ dataDetail?.description }}</a-descriptions-item>
<a-descriptions-item label="所属模块">{{ dataDetail?.module }}</a-descriptions-item>
<a-descriptions-item label="操作 IP">{{ dataDetail?.ip }}</a-descriptions-item>
<a-descriptions-item label="操作地点">{{ dataDetail?.address }}</a-descriptions-item>
<a-descriptions-item label="浏览器">{{ dataDetail?.browser }}</a-descriptions-item>
<a-descriptions-item label="终端系统">{{ dataDetail?.os }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag v-if="dataDetail?.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="dataDetail?.timeTaken > 500" color="red">
{{ dataDetail?.timeTaken }}ms
</a-tag>
<a-tag v-else-if="dataDetail?.timeTaken > 200" color="orange">
{{ dataDetail?.timeTaken }}ms
</a-tag>
<a-tag v-else color="green">{{ dataDetail?.timeTaken }} ms</a-tag>
</a-descriptions-item>
<a-descriptions-item label="请求 URI" :span="2">
{{ dataDetail?.requestUrl }}<TextCopy :value="dataDetail?.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="响应头">
<JsonPretty v-if="dataDetail?.responseHeaders" :json="dataDetail?.responseHeaders" />
<span v-else></span>
</a-tab-pane>
<a-tab-pane key="2" title="响应体">
<JsonPretty v-if="dataDetail?.responseBody" :json="dataDetail?.responseBody" />
<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="请求头">
<JsonPretty v-if="dataDetail?.requestHeaders" :json="dataDetail?.requestHeaders" />
<span v-else></span>
</a-tab-pane>
<a-tab-pane key="2" title="请求体">
<JsonPretty v-if="dataDetail?.requestBody" :json="dataDetail?.requestBody" />
<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'
const dataId = ref('')
const dataDetail = ref<LogDetailResp>()
// 查询详情
const getDataDetail = async () => {
const res = await getLog(dataId.value)
dataDetail.value = res.data
}
const visible = ref(false)
// 打开详情
const open = async (id: string) => {
dataId.value = id
await getDataDetail()
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

@@ -1,12 +1,12 @@
<template>
<div class="gi_page">
<a-card title="系统日志" class="general-card">
<a-tabs type="card-gutter" size="large" :active-key="activeKey" @change="change">
<a-tab-pane key="1" title="登录日志"/>
<a-tab-pane key="2" title="操作日志"/>
<a-tabs type="card-gutter" size="large" :active-key="activeKey" @change="change">
<a-tab-pane key="1" title="登录日志" />
<a-tab-pane key="2" title="操作日志" />
</a-tabs>
<keep-alive>
<component :is="PaneMap[activeKey]"></component>
<component :is="PaneMap[activeKey]" />
</keep-alive>
</a-card>
</div>
@@ -15,12 +15,15 @@
<script setup lang="ts">
import LoginLog from './LoginLog.vue'
import OperationLog from './OperationLog.vue'
const route = useRoute()
const router = useRouter()
const PaneMap:Record<string,Component> = {
const PaneMap: Record<string, Component> = {
'1': LoginLog,
'2': OperationLog
}
const activeKey = ref('1')
watch(
() => route.query,
@@ -31,6 +34,7 @@ watch(
},
{ immediate: true }
)
const change = (key: string | number) => {
activeKey.value = key as string
router.replace({ path: route.path, query: { tabKey: key } })

View File

@@ -1,105 +0,0 @@
<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 }}<TextCopy :value="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 }}<TextCopy :value="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="响应头">
<JsonPretty v-if="operationLog?.responseHeaders" :josn="operationLog?.responseHeaders"/>
<span v-else></span>
</a-tab-pane>
<a-tab-pane key="2" title="响应体">
<JsonPretty v-if="operationLog?.responseBody" :josn="operationLog?.responseBody"/>
<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="请求头">
<JsonPretty v-if="operationLog?.requestHeaders" :josn="operationLog?.requestHeaders"/>
<span v-else></span>
</a-tab-pane>
<a-tab-pane key="2" title="请求体">
<JsonPretty v-if="operationLog?.requestBody" :josn="operationLog?.requestBody"/>
<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 JsonPretty from '@/components/JsonPretty/index.vue'
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>