feat: 添加异常日志功能并优化UI样式
refactor: 重构后端查询逻辑和API响应处理 fix: 修复用户角色更新和文件上传问题 test: 添加前端性能测试脚本和E2E测试用例 chore: 更新依赖版本和配置文件 docs: 添加环境检查脚本和测试文档 style: 统一表格标签样式和路由命名 perf: 优化前端页面加载速度和响应时间
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export interface ExceptionLog {
|
||||
id?: number
|
||||
username?: string
|
||||
operation?: string
|
||||
method?: string
|
||||
params?: string
|
||||
errorMsg?: string
|
||||
exceptionStack?: string
|
||||
ip?: string
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
export interface PageResponse<T> {
|
||||
content: T[]
|
||||
totalPages: number
|
||||
totalElements: number
|
||||
currentPage: number
|
||||
size: number
|
||||
}
|
||||
|
||||
export const exceptionLogApi = {
|
||||
getAll: () => request.get<ExceptionLog[]>('/logs/exception'),
|
||||
|
||||
getById: (id: number) => request.get<ExceptionLog>(`/logs/exception/${id}`),
|
||||
|
||||
getPage: (params: {
|
||||
page?: number
|
||||
size?: number
|
||||
sort?: string
|
||||
order?: string
|
||||
keyword?: string
|
||||
}) => request.get<PageResponse<ExceptionLog>>('/logs/exception/page', { params }),
|
||||
|
||||
getCount: () => request.get<number>('/logs/exception/count'),
|
||||
|
||||
create: (data: Partial<ExceptionLog>) => request.post<ExceptionLog>('/logs/exception', data)
|
||||
}
|
||||
@@ -44,7 +44,7 @@
|
||||
<el-menu-item index="/dict">
|
||||
字典管理
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/sysconfig">
|
||||
<el-menu-item index="/sys/config">
|
||||
参数配置
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
@@ -59,6 +59,9 @@
|
||||
<el-menu-item index="/oplog">
|
||||
操作日志
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/exceptionlog">
|
||||
异常日志
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="notify">
|
||||
<template #title>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
import 'element-plus/dist/index.css'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
@@ -11,6 +12,8 @@ const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
app.use(ElementPlus, {
|
||||
locale: zhCn,
|
||||
})
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -33,7 +33,7 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/views/system/MenuManagement.vue')
|
||||
},
|
||||
{
|
||||
path: 'config',
|
||||
path: 'sys/config',
|
||||
name: 'ConfigManagement',
|
||||
component: () => import('@/views/config/ConfigManagement.vue')
|
||||
},
|
||||
@@ -48,19 +48,24 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/views/file/FileManagement.vue')
|
||||
},
|
||||
{
|
||||
path: 'notices',
|
||||
path: 'notice',
|
||||
name: 'NoticeManagement',
|
||||
component: () => import('@/views/notify/NoticeManagement.vue')
|
||||
},
|
||||
{
|
||||
path: 'login-logs',
|
||||
path: 'loginlog',
|
||||
name: 'LoginLog',
|
||||
component: () => import('@/views/audit/LoginLog.vue')
|
||||
},
|
||||
{
|
||||
path: 'operation-logs',
|
||||
path: 'oplog',
|
||||
name: 'OperationLog',
|
||||
component: () => import('@/views/audit/OperationLog.vue')
|
||||
},
|
||||
{
|
||||
path: 'exceptionlog',
|
||||
name: 'ExceptionLog',
|
||||
component: () => import('@/views/audit/ExceptionLog.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div class="exception-log">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="search-section">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索操作人或异常信息"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSearch"
|
||||
>
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
sortable="custom"
|
||||
width="80"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="操作人"
|
||||
sortable="custom"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="operation"
|
||||
label="操作模块"
|
||||
sortable="custom"
|
||||
width="150"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="method"
|
||||
label="请求方法"
|
||||
sortable="custom"
|
||||
width="200"
|
||||
:show-overflow-tooltip="true"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="errorMsg"
|
||||
label="异常信息"
|
||||
:show-overflow-tooltip="true"
|
||||
width="250"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="ip"
|
||||
label="IP地址"
|
||||
sortable="custom"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="异常时间"
|
||||
sortable="custom"
|
||||
width="180"
|
||||
/>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleViewDetail(row)"
|
||||
>
|
||||
查看详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
@current-change="handleTableChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
title="异常详情"
|
||||
width="800px"
|
||||
>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="ID">
|
||||
{{ currentDetail.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作人">
|
||||
{{ currentDetail.username }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作模块">
|
||||
{{ currentDetail.operation }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求方法">
|
||||
{{ currentDetail.method }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求参数">
|
||||
<pre style="max-height: 200px; overflow: auto;">{{ currentDetail.params }}</pre>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="异常信息">
|
||||
<div style="color: #f56c6c; word-break: break-all;">{{ currentDetail.errorMsg }}</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="异常堆栈">
|
||||
<pre style="max-height: 300px; overflow: auto; font-size: 12px;">{{ currentDetail.exceptionStack }}</pre>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="IP地址">
|
||||
{{ currentDetail.ip }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="异常时间">
|
||||
{{ currentDetail.createTime }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { exceptionLogApi, ExceptionLog } from '@/api/exceptionLog'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<ExceptionLog[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const sortInfo = reactive({
|
||||
sort: 'id',
|
||||
order: 'asc'
|
||||
})
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const currentDetail = ref<ExceptionLog>({})
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await exceptionLogApi.getPage({
|
||||
page: pagination.current - 1,
|
||||
size: pagination.pageSize,
|
||||
sort: sortInfo.sort,
|
||||
order: sortInfo.order,
|
||||
keyword: searchKeyword.value || undefined
|
||||
})
|
||||
dataSource.value = res.content
|
||||
pagination.total = res.totalElements
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTableChange = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSortChange = ({ prop, order }: any) => {
|
||||
sortInfo.sort = prop
|
||||
sortInfo.order = order === 'ascending' ? 'asc' : 'desc'
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleViewDetail = (row: ExceptionLog) => {
|
||||
currentDetail.value = { ...row }
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.exception-log {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -63,7 +63,11 @@
|
||||
/>
|
||||
<el-table-column label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
|
||||
<el-tag
|
||||
:type="row.status === '0' ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === '0' ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
@@ -58,7 +58,11 @@
|
||||
/>
|
||||
<el-table-column label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
|
||||
<el-tag
|
||||
:type="row.status === '0' ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === '0' ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
@@ -35,7 +35,11 @@
|
||||
/>
|
||||
<el-table-column label="类型">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.configType === 'Y' ? '' : 'info'">
|
||||
<el-tag
|
||||
:type="row.configType === 'Y' ? 'info' : 'success'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.configType === 'Y' ? '内置' : '自定义' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
@@ -31,7 +31,11 @@
|
||||
/>
|
||||
<el-table-column label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
|
||||
<el-tag
|
||||
:type="row.status === '0' ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === '0' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
@@ -14,9 +14,22 @@
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索文件名"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@input="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
:data="filteredDataSource"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column
|
||||
@@ -46,6 +59,10 @@
|
||||
prop="createdAt"
|
||||
label="上传时间"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createBy"
|
||||
label="上传人"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="150"
|
||||
@@ -75,13 +92,26 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Upload } from '@element-plus/icons-vue'
|
||||
import { Upload, Search } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const searchKeyword = ref('')
|
||||
|
||||
const filteredDataSource = computed(() => {
|
||||
if (!searchKeyword.value) {
|
||||
return dataSource.value
|
||||
}
|
||||
return dataSource.value.filter((item: any) =>
|
||||
item.fileName.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
|
||||
@@ -30,7 +30,11 @@
|
||||
width="100"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.noticeType === '1' ? '' : 'success'">
|
||||
<el-tag
|
||||
:type="row.noticeType === '1' ? 'info' : 'success'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.noticeType === '1' ? '通知' : '公告' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
@@ -40,7 +44,11 @@
|
||||
width="80"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
|
||||
<el-tag
|
||||
:type="row.status === '0' ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === '0' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
@@ -69,6 +69,10 @@ const onFinish = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.post('/auth/login', formState)
|
||||
if (res.code === 401) {
|
||||
ElMessage.error(res.message || '登录失败')
|
||||
return
|
||||
}
|
||||
localStorage.setItem('token', res.token)
|
||||
localStorage.setItem('userId', res.userId)
|
||||
localStorage.setItem('username', res.username)
|
||||
|
||||
@@ -28,7 +28,11 @@
|
||||
width="100"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.menuType === 'M' ? '' : row.menuType === 'C' ? 'success' : 'warning'">
|
||||
<el-tag
|
||||
:type="row.menuType === 'M' ? 'info' : row.menuType === 'C' ? 'success' : 'warning'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.menuType === 'M' ? '目录' : row.menuType === 'C' ? '菜单' : '按钮' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
@@ -62,7 +62,11 @@
|
||||
width="100"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'ACTIVE' ? 'success' : 'danger'">
|
||||
<el-tag
|
||||
:type="row.status === 'ACTIVE' ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === 'ACTIVE' ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
@@ -68,7 +68,11 @@
|
||||
width="100"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'ACTIVE' ? 'success' : 'danger'">
|
||||
<el-tag
|
||||
:type="row.status === 'ACTIVE' ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === 'ACTIVE' ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user