jiaxing.zhou

feat(log): 新增系统日志管理页面

- 实现日志列表展示与分页功能- 添加日志类型、用户名、操作模块等多条件搜索
- 支持查看日志详情信息
- 提供单条及批量删除日志功能
- 实现过期日志清理功能
- 添加日志导出功能入口
- 集成Element Plus组件库实现UI界面
- 使用Vue 3 Composition API编写组件逻辑
<template>
<div class="log-management">
<!-- 页面标题 -->
<div class="page-header">
<h2>日志管理</h2>
<p>系统操作日志查询与管理</p>
</div>
<!-- 搜索表单 -->
<el-card class="search-card" shadow="never">
<el-form :model="searchForm" :inline="true" class="search-form">
<el-form-item label="日志类型">
<el-select v-model="searchForm.logType" placeholder="请选择日志类型" clearable style="width: 200px">
<el-option label="操作日志" value="0" />
<el-option label="异常日志" value="1" />
<el-option label="登录日志" value="2" />
</el-select>
</el-form-item>
<el-form-item label="用户名">
<el-input
v-model="searchForm.username"
placeholder="请输入用户名"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="操作模块">
<el-input
v-model="searchForm.module"
placeholder="请输入操作模块"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="操作结果">
<el-select v-model="searchForm.status" placeholder="请选择操作结果" clearable style="width: 200px">
<el-option label="成功" :value="1" />
<el-option label="失败" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="dateRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
@change="handleDateRangeChange"
style="width: 350px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch" :loading="loading">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 操作按钮 -->
<el-card class="toolbar-card" shadow="never">
<div class="toolbar">
<div class="toolbar-left">
<el-button
type="danger"
:disabled="selectedLogs.length === 0"
@click="handleBatchDelete"
>
<el-icon><Delete /></el-icon>
批量删除
</el-button>
<el-button type="warning" @click="handleCleanExpired">
<el-icon><Delete /></el-icon>
清理过期日志
</el-button>
<el-button type="info" @click="handleExport">
<el-icon><Download /></el-icon>
导出日志
</el-button>
</div>
<div class="toolbar-right">
<el-button @click="handleRefresh" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</div>
</el-card>
<!-- 日志列表 -->
<el-card class="table-card" shadow="never">
<el-table
v-loading="loading"
:data="logList"
@selection-change="handleSelectionChange"
stripe
border
style="width: 100%"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column prop="logId" label="日志ID" width="80" align="center" />
<el-table-column prop="logTypeText" label="日志类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getLogTypeTagType(row.logType)" size="small">
{{ row.logTypeText }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="username" label="用户名" width="100" align="center" />
<el-table-column prop="module" label="操作模块" width="120" align="center" />
<el-table-column prop="operation" label="操作内容" min-width="180" show-overflow-tooltip />
<el-table-column prop="ip" label="IP地址" width="120" align="center" />
<el-table-column prop="logTime" label="操作时间" width="160" align="center">
<template #default="{ row }">
{{ formatDate(row.logTime) }}
</template>
</el-table-column>
<el-table-column prop="statusText" label="操作结果" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
{{ row.statusText }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="140" fixed="right" align="center">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="handleViewDetail(row)"
link
>
详情
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(row)"
link
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.pageNum"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 日志详情对话框 -->
<el-dialog
v-model="detailDialogVisible"
title="日志详情"
width="800px"
:close-on-click-modal="false"
>
<div v-if="logDetail" class="log-detail">
<el-descriptions :column="2" border>
<el-descriptions-item label="日志ID">
{{ logDetail.logId }}
</el-descriptions-item>
<el-descriptions-item label="日志类型">
<el-tag :type="getLogTypeTagType(logDetail.logType)">
{{ logDetail.logTypeText }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="用户名">
{{ logDetail.username }}
</el-descriptions-item>
<el-descriptions-item label="用户ID">
{{ logDetail.userId }}
</el-descriptions-item>
<el-descriptions-item label="操作模块">
{{ logDetail.module }}
</el-descriptions-item>
<el-descriptions-item label="操作内容">
{{ logDetail.operation }}
</el-descriptions-item>
<el-descriptions-item label="IP地址">
{{ logDetail.ip }}
</el-descriptions-item>
<el-descriptions-item label="操作时间">
{{ formatDate(logDetail.logTime) }}
</el-descriptions-item>
<el-descriptions-item label="操作结果">
<el-tag :type="logDetail.status === 1 ? 'success' : 'danger'">
{{ logDetail.statusText }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="错误信息" v-if="logDetail.errorMsg">
{{ logDetail.errorMsg }}
</el-descriptions-item>
</el-descriptions>
<div v-if="logDetail.requestParam" class="request-param">
<h4>请求参数</h4>
<el-input
v-model="logDetail.requestParam"
type="textarea"
:rows="6"
readonly
placeholder="无请求参数"
/>
</div>
</div>
<template #footer>
<el-button @click="detailDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 清理过期日志对话框 -->
<el-dialog
v-model="cleanDialogVisible"
title="清理过期日志"
width="400px"
:close-on-click-modal="false"
>
<el-form :model="cleanForm" label-width="100px">
<el-form-item label="过期天数">
<el-input-number
v-model="cleanForm.expireDays"
:min="1"
:max="365"
placeholder="请输入过期天数"
style="width: 100%"
/>
</el-form-item>
<el-form-item>
<el-text type="warning">
将删除指定天数之前的所有日志记录,此操作不可恢复!
</el-text>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="cleanDialogVisible = false">取消</el-button>
<el-button type="danger" @click="handleConfirmClean" :loading="cleanLoading">
确认清理
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Delete, Download } from '@element-plus/icons-vue'
import { logApi, type Log, type LogDetail, type LogSearchParams } from '@/api/log'
import { formatDate } from '@/utils/index'
// 响应式数据
const loading = ref(false)
const logList = ref<Log[]>([])
const selectedLogs = ref<Log[]>([])
const detailDialogVisible = ref(false)
const logDetail = ref<LogDetail | null>(null)
const cleanDialogVisible = ref(false)
const cleanLoading = ref(false)
const dateRange = ref<[string, string] | null>(null)
// 搜索表单
const searchForm = reactive<LogSearchParams>({
pageNum: 1,
pageSize: 10,
logType: '',
username: '',
module: '',
status: undefined,
startTime: '',
endTime: ''
})
// 分页信息
const pagination = reactive({
pageNum: 1,
pageSize: 10,
total: 0
})
// 清理表单
const cleanForm = reactive({
expireDays: 30
})
// 获取日志类型标签类型
const getLogTypeTagType = (logType: string) => {
switch (logType) {
case '0':
return 'primary'
case '1':
return 'danger'
case '2':
return 'success'
default:
return 'info'
}
}
// 导出日志
const handleExport = () => {
ElMessage.info('导出功能开发中...')
}
// 获取日志列表
const getLogList = async () => {
try {
loading.value = true
const params = {
...searchForm,
pageNum: pagination.pageNum,
pageSize: pagination.pageSize
}
const response = await logApi.getLogList(params)
if (response.code === 200) {
logList.value = response.data.records
pagination.total = response.data.total
} else {
ElMessage.error(response.message || '获取日志列表失败')
}
} catch (error) {
console.error('获取日志列表失败:', error)
ElMessage.error('获取日志列表失败')
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
pagination.pageNum = 1
getLogList()
}
// 重置
const handleReset = () => {
Object.assign(searchForm, {
pageNum: 1,
pageSize: 10,
logType: '',
username: '',
module: '',
status: undefined,
startTime: '',
endTime: ''
})
dateRange.value = null
pagination.pageNum = 1
getLogList()
}
// 刷新
const handleRefresh = () => {
getLogList()
}
// 时间范围变化
const handleDateRangeChange = (value: [string, string] | null) => {
if (value) {
searchForm.startTime = value[0]
searchForm.endTime = value[1]
} else {
searchForm.startTime = ''
searchForm.endTime = ''
}
}
// 分页大小变化
const handleSizeChange = (size: number) => {
pagination.pageSize = size
pagination.pageNum = 1
getLogList()
}
// 当前页变化
const handleCurrentChange = (page: number) => {
pagination.pageNum = page
getLogList()
}
// 选择变化
const handleSelectionChange = (selection: Log[]) => {
selectedLogs.value = selection
}
// 查看详情
const handleViewDetail = async (row: Log) => {
try {
const response = await logApi.getLogDetail(row.logId)
if (response.code === 200) {
logDetail.value = response.data
detailDialogVisible.value = true
} else {
ElMessage.error(response.message || '获取日志详情失败')
}
} catch (error) {
console.error('获取日志详情失败:', error)
ElMessage.error('获取日志详情失败')
}
}
// 删除日志
const handleDelete = async (row: Log) => {
try {
await ElMessageBox.confirm(
`确定要删除日志ID为 ${row.logId} 的记录吗?`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
const response = await logApi.deleteLog(row.logId)
if (response.code === 200) {
ElMessage.success('删除成功')
getLogList()
} else {
ElMessage.error(response.message || '删除失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('删除日志失败:', error)
ElMessage.error('删除失败')
}
}
}
// 批量删除
const handleBatchDelete = async () => {
if (selectedLogs.value.length === 0) {
ElMessage.warning('请选择要删除的日志')
return
}
try {
await ElMessageBox.confirm(
`确定要删除选中的 ${selectedLogs.value.length} 条日志记录吗?`,
'确认批量删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
const logIds = selectedLogs.value.map(log => log.logId)
const response = await logApi.deleteLogs(logIds)
if (response.code === 200) {
ElMessage.success('批量删除成功')
getLogList()
} else {
ElMessage.error(response.message || '批量删除失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('批量删除日志失败:', error)
ElMessage.error('批量删除失败')
}
}
}
// 清理过期日志
const handleCleanExpired = () => {
cleanDialogVisible.value = true
}
// 确认清理
const handleConfirmClean = async () => {
if (!cleanForm.expireDays || cleanForm.expireDays <= 0) {
ElMessage.warning('请输入有效的过期天数')
return
}
try {
await ElMessageBox.confirm(
`确定要清理 ${cleanForm.expireDays} 天前的所有日志记录吗?此操作不可恢复!`,
'确认清理',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
cleanLoading.value = true
const response = await logApi.cleanExpiredLogs(cleanForm.expireDays)
if (response.code === 200) {
ElMessage.success(response.data)
cleanDialogVisible.value = false
getLogList()
} else {
ElMessage.error(response.message || '清理失败')
}
} catch (error) {
if (error !== 'cancel') {
console.error('清理过期日志失败:', error)
ElMessage.error('清理失败')
}
} finally {
cleanLoading.value = false
}
}
// 组件挂载时获取数据
onMounted(() => {
getLogList()
})
</script>
<style scoped>
.log-management {
padding: 20px;
}
.page-header {
margin-bottom: 20px;
}
.page-header h2 {
margin: 0 0 8px 0;
color: #303133;
font-size: 20px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: #909399;
font-size: 14px;
}
.search-card {
margin-bottom: 16px;
}
.search-form {
margin-bottom: 0;
}
.toolbar-card {
margin-bottom: 16px;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
}
.toolbar-left,
.toolbar-right {
display: flex;
gap: 8px;
}
.table-card {
margin-bottom: 16px;
}
.pagination-container {
display: flex;
justify-content: center;
margin-top: 20px;
}
.log-detail {
max-height: 600px;
overflow-y: auto;
}
.request-param {
margin-top: 20px;
}
.request-param h4 {
margin: 0 0 10px 0;
color: #303133;
font-size: 14px;
font-weight: 600;
}
/* 表格优化 */
.el-table {
font-size: 14px;
}
.el-table .cell {
padding: 8px 12px;
line-height: 1.5;
}
.el-table th.el-table__cell {
background: #fafafa;
color: #606266;
font-weight: 600;
}
.el-table td.el-table__cell {
padding: 12px 0;
}
.el-table .el-button + .el-button {
margin-left: 8px;
}
</style>