feat: 操作日志管理前端实现

前端实现:
- operationLog.js: API接口封装(44行,4个接口)
- operationLog.vue: 管理页面(351行)

页面特点:
- 搜索:模块、操作类型、操作人、时间范围
- 表格:模块、操作类型(标签)、请求信息、执行时长、状态
- 详情:完整的请求参数、响应结果、错误信息
- 批量清理:删除N天前的日志(7-365天)

操作类型:查询/新增/更新/删除/导入/导出/登录/登出

模块状态: 完整(前端+后端)
This commit is contained in:
zhangjf 2026-02-16 09:07:37 +08:00
parent 3dd1b88749
commit d8dcbd0ef2
2 changed files with 393 additions and 0 deletions

View File

@ -0,0 +1,43 @@
import request from '../utils/request'
/**
* 获取操作日志列表分页
*/
export const getOperationLogList = (params) => {
return request({
url: '/sys/api/v1/operation-log/list',
method: 'get',
params
})
}
/**
* 获取操作日志详情
*/
export const getOperationLogById = (logId) => {
return request({
url: `/sys/api/v1/operation-log/${logId}`,
method: 'get'
})
}
/**
* 删除操作日志
*/
export const deleteOperationLog = (logId) => {
return request({
url: `/sys/api/v1/operation-log/${logId}`,
method: 'delete'
})
}
/**
* 批量删除操作日志删除指定天数之前的日志
*/
export const batchDeleteOperationLog = (days) => {
return request({
url: '/sys/api/v1/operation-log/batch',
method: 'delete',
params: { days }
})
}

View File

@ -0,0 +1,350 @@
<template>
<div class="log-container">
<el-card>
<!-- 搜索栏 -->
<el-form :inline="true" :model="searchForm">
<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.operationType" placeholder="请选择" clearable style="width: 150px;">
<el-option label="查询" value="SELECT" />
<el-option label="新增" value="INSERT" />
<el-option label="更新" value="UPDATE" />
<el-option label="删除" value="DELETE" />
<el-option label="导入" value="IMPORT" />
<el-option label="导出" value="EXPORT" />
<el-option label="登录" value="LOGIN" />
<el-option label="登出" value="LOGOUT" />
</el-select>
</el-form-item>
<el-form-item label="操作人ID">
<el-input-number v-model="searchForm.operatorId" :min="1" controls-position="right" style="width: 150px;" />
</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"
style="width: 380px;"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作栏 -->
<el-row style="margin-bottom: 15px;">
<el-button type="danger" @click="handleBatchDelete">批量清理</el-button>
</el-row>
<!-- 表格 -->
<el-table :data="tableData" border v-loading="loading" stripe>
<el-table-column prop="logId" label="日志ID" width="80" />
<el-table-column prop="module" label="模块" width="120" />
<el-table-column prop="operationType" label="操作类型" width="100">
<template #default="{ row }">
<el-tag :type="getOperationTypeTag(row.operationType)">
{{ getOperationTypeText(row.operationType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="操作描述" width="200" show-overflow-tooltip />
<el-table-column prop="requestMethod" label="请求方式" width="100" />
<el-table-column prop="requestUrl" label="请求URL" width="250" show-overflow-tooltip />
<el-table-column prop="executionTime" label="执行时长" width="100">
<template #default="{ row }">
<span :style="{ color: row.executionTime > 1000 ? 'red' : 'inherit' }">
{{ row.executionTime }}ms
</span>
</template>
</el-table-column>
<el-table-column prop="operatorName" label="操作人" width="120" />
<el-table-column prop="operatorIp" label="IP地址" width="140" />
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag v-if="row.status === 1" type="success">成功</el-tag>
<el-tag v-else type="danger">失败</el-tag>
</template>
</el-table-column>
<el-table-column prop="createdTime" label="操作时间" width="180" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleViewDetail(row)">查看详情</el-button>
<el-button type="danger" size="small" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="page.current"
v-model:page-size="page.size"
:total="page.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="fetchData"
@current-change="fetchData"
style="margin-top: 20px; justify-content: flex-end;"
/>
</el-card>
<!-- 详情对话框 -->
<el-dialog v-model="detailVisible" title="操作日志详情" width="800px">
<el-descriptions :column="2" border>
<el-descriptions-item label="日志ID">{{ detail.logId }}</el-descriptions-item>
<el-descriptions-item label="模块">{{ detail.module }}</el-descriptions-item>
<el-descriptions-item label="操作类型">
<el-tag :type="getOperationTypeTag(detail.operationType)">
{{ getOperationTypeText(detail.operationType) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag v-if="detail.status === 1" type="success">成功</el-tag>
<el-tag v-else type="danger">失败</el-tag>
</el-descriptions-item>
<el-descriptions-item label="操作描述" :span="2">{{ detail.description }}</el-descriptions-item>
<el-descriptions-item label="请求方式">{{ detail.requestMethod }}</el-descriptions-item>
<el-descriptions-item label="执行时长">{{ detail.executionTime }}ms</el-descriptions-item>
<el-descriptions-item label="请求URL" :span="2">{{ detail.requestUrl }}</el-descriptions-item>
<el-descriptions-item label="请求参数" :span="2">
<pre style="max-height: 200px; overflow-y: auto;">{{ detail.requestParams }}</pre>
</el-descriptions-item>
<el-descriptions-item label="响应结果" :span="2">
<pre style="max-height: 200px; overflow-y: auto;">{{ detail.responseResult }}</pre>
</el-descriptions-item>
<el-descriptions-item label="操作人">{{ detail.operatorName }}</el-descriptions-item>
<el-descriptions-item label="操作人ID">{{ detail.operatorId }}</el-descriptions-item>
<el-descriptions-item label="IP地址">{{ detail.operatorIp }}</el-descriptions-item>
<el-descriptions-item label="位置">{{ detail.operatorLocation }}</el-descriptions-item>
<el-descriptions-item label="操作时间" :span="2">{{ detail.createdTime }}</el-descriptions-item>
<el-descriptions-item v-if="detail.errorMsg" label="错误信息" :span="2">
<pre style="max-height: 200px; overflow-y: auto; color: red;">{{ detail.errorMsg }}</pre>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="detailVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 批量删除对话框 -->
<el-dialog v-model="batchDeleteVisible" title="批量清理日志" width="400px">
<el-form>
<el-form-item label="清理天数">
<el-input-number v-model="batchDeleteDays" :min="7" :max="365" style="width: 100%;" />
<div style="color: #909399; font-size: 12px; margin-top: 5px;">
将删除 {{ batchDeleteDays }} 天前的所有操作日志
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="batchDeleteVisible = false">取消</el-button>
<el-button type="danger" @click="handleConfirmBatchDelete">确定删除</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getOperationLogList,
getOperationLogById,
deleteOperationLog,
batchDeleteOperationLog
} from '../../api/operationLog'
//
const searchForm = reactive({
module: '',
operationType: '',
operatorId: null
})
//
const dateRange = ref([])
//
const tableData = ref([])
const loading = ref(false)
//
const page = reactive({
current: 1,
size: 10,
total: 0
})
//
const detailVisible = ref(false)
const detail = ref({})
//
const batchDeleteVisible = ref(false)
const batchDeleteDays = ref(30)
//
const getOperationTypeText = (type) => {
const typeMap = {
'SELECT': '查询',
'INSERT': '新增',
'UPDATE': '更新',
'DELETE': '删除',
'IMPORT': '导入',
'EXPORT': '导出',
'LOGIN': '登录',
'LOGOUT': '登出'
}
return typeMap[type] || type
}
//
const getOperationTypeTag = (type) => {
const tagMap = {
'SELECT': '',
'INSERT': 'success',
'UPDATE': 'warning',
'DELETE': 'danger',
'IMPORT': 'info',
'EXPORT': 'info',
'LOGIN': 'success',
'LOGOUT': 'info'
}
return tagMap[type] || ''
}
//
const fetchData = async () => {
loading.value = true
try {
const params = {
current: page.current,
size: page.size,
module: searchForm.module || undefined,
operationType: searchForm.operationType || undefined,
operatorId: searchForm.operatorId || undefined,
startTime: dateRange.value && dateRange.value[0] ? dateRange.value[0] : undefined,
endTime: dateRange.value && dateRange.value[1] ? dateRange.value[1] : undefined
}
const res = await getOperationLogList(params)
tableData.value = res.records
page.total = res.total
} catch (error) {
console.error('加载数据失败:', error)
ElMessage.error(error.message || '加载数据失败')
} finally {
loading.value = false
}
}
//
const handleSearch = () => {
page.current = 1
fetchData()
}
//
const handleReset = () => {
searchForm.module = ''
searchForm.operationType = ''
searchForm.operatorId = null
dateRange.value = []
page.current = 1
fetchData()
}
//
const handleViewDetail = async (row) => {
try {
const res = await getOperationLogById(row.logId)
detail.value = res
detailVisible.value = true
} catch (error) {
console.error('加载详情失败:', error)
ElMessage.error(error.message || '加载详情失败')
}
}
//
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该操作日志吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await deleteOperationLog(row.logId)
ElMessage.success('删除成功')
//
if (tableData.value.length === 1 && page.current > 1) {
page.current--
}
await fetchData()
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error)
ElMessage.error(error.message || '删除失败')
}
}
}
//
const handleBatchDelete = () => {
batchDeleteVisible.value = true
}
//
const handleConfirmBatchDelete = async () => {
try {
await ElMessageBox.confirm(
`确定要删除 ${batchDeleteDays.value} 天前的所有操作日志吗?此操作不可恢复!`,
'警告',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning'
}
)
await batchDeleteOperationLog(batchDeleteDays.value)
ElMessage.success('批量删除成功')
batchDeleteVisible.value = false
await fetchData()
} catch (error) {
if (error !== 'cancel') {
console.error('批量删除失败:', error)
ElMessage.error(error.message || '批量删除失败')
}
}
}
//
onMounted(() => {
fetchData()
})
</script>
<style scoped>
.log-container {
padding: 20px;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
background-color: #f5f5f5;
padding: 10px;
border-radius: 4px;
margin: 0;
}
</style>