feat: 操作日志管理前端实现
前端实现:
- operationLog.js: API接口封装(44行,4个接口)
- operationLog.vue: 管理页面(351行)
页面特点:
- 搜索:模块、操作类型、操作人、时间范围
- 表格:模块、操作类型(标签)、请求信息、执行时长、状态
- 详情:完整的请求参数、响应结果、错误信息
- 批量清理:删除N天前的日志(7-365天)
操作类型:查询/新增/更新/删除/导入/导出/登录/登出
模块状态:✅ 完整(前端+后端)
This commit is contained in:
parent
3dd1b88749
commit
d8dcbd0ef2
43
fund-admin/src/api/operationLog.js
Normal file
43
fund-admin/src/api/operationLog.js
Normal 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 }
|
||||
})
|
||||
}
|
||||
350
fund-admin/src/views/system/operationLog.vue
Normal file
350
fund-admin/src/views/system/operationLog.vue
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user