操作日志和访问日志

This commit is contained in:
2025-11-11 22:32:46 +08:00
parent efc079c0e5
commit 12a0ff8afc
17 changed files with 2300 additions and 4 deletions
+36
View File
@@ -0,0 +1,36 @@
import request from '@/utils/request'
// 获取访问日志列表
export function getAccessLogs(params) {
return request({
url: '/api/access-logs',
method: 'get',
params
})
}
// 根据ID获取访问日志详情
export function getAccessLogById(id) {
return request({
url: `/api/access-logs/${id}`,
method: 'get'
})
}
// 获取用户访问统计
export function getUserAccessStats(params) {
return request({
url: '/api/access-logs/user/stats',
method: 'get',
params
})
}
// 清空旧访问日志
export function clearOldAccessLogs(keepDays = 90) {
return request({
url: '/api/access-logs/clear',
method: 'post',
data: { keep_days: keepDays }
})
}
+45
View File
@@ -0,0 +1,45 @@
import request from '@/utils/request'
// 获取操作日志列表
export function getOperationLogs(params) {
return request({
url: '/api/operation-logs',
method: 'get',
params
})
}
// 根据ID获取操作日志详情
export function getOperationLogById(id) {
return request({
url: `/api/operation-logs/${id}`,
method: 'get'
})
}
// 获取用户操作统计
export function getUserOperationStats(params) {
return request({
url: '/api/operation-logs/user/stats',
method: 'get',
params
})
}
// 获取租户操作统计
export function getTenantOperationStats(params) {
return request({
url: '/api/operation-logs/tenant/stats',
method: 'get',
params
})
}
// 清空旧日志
export function clearOldLogs(keepDays = 90) {
return request({
url: '/api/operation-logs/clear',
method: 'post',
data: { keep_days: keepDays }
})
}
+367
View File
@@ -0,0 +1,367 @@
<template>
<div class="access-log-container">
<!-- 统计面板 -->
<!-- <StatisticsPanel /> -->
<!-- 搜索和操作栏 -->
<el-card shadow="hover" style="margin-bottom: 20px">
<el-form :model="filters" label-width="100px" :inline="true">
<el-form-item label="用户">
<el-input v-model="filters.username" placeholder="搜索用户名" clearable />
</el-form-item>
<el-form-item label="模块">
<el-select v-model="filters.module" placeholder="选择模块" clearable>
<el-option label="全部" value="" />
<el-option
v-for="opt in moduleOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="资源类型">
<el-input v-model="filters.resource_type" placeholder="搜索资源类型" clearable />
</el-form-item>
</el-form>
<el-form :model="dateRange" label-width="100px" :inline="true">
<el-form-item label="访问时间">
<el-date-picker
v-model="dateRange.range"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleDateChange"
/>
</el-form-item>
</el-form>
<div style="margin-top: 15px">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button @click="showClearDialog">清空日志</el-button>
<el-button @click="handleExport">导出</el-button>
</div>
</el-card>
<!-- 日志列表 -->
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>访问日志列表</span>
<span class="log-count"> {{ total }} </span>
</div>
</template>
<el-table
:data="tableData"
stripe
style="width: 100%; margin-bottom: 20px"
v-loading="loading"
@row-click="handleRowClick"
>
<el-table-column prop="username" label="用户" align="center" width="120" />
<el-table-column prop="module_name" label="模块" align="center" width="120">
<template #default="{ row }">
<el-tag>{{ row.module_name }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="resource_type" label="资源类型" align="center" width="120" />
<el-table-column prop="request_url" label="访问路径" align="center" min-width="200">
<template #default="{ row }">
<el-tooltip :content="row.request_url" placement="top">
<span>{{ row.request_url }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="request_method" label="方法" align="center" width="80">
<template #default="{ row }">
<el-tag :type="getMethodTag(row.request_method)">
{{ row.request_method }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="ip_address" label="IP地址" align="center" width="140" />
<el-table-column prop="duration" label="耗时(ms)" align="center" width="100" />
<el-table-column prop="create_time" label="访问时间" align="center" width="180">
<template #default="{ row }">
{{ formatTime(row.create_time) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="100">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="showDetail(row)">
详情
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@change="handlePageChange"
/>
</el-card>
<!-- 详情对话框 -->
<el-dialog v-model="detailDialogVisible" title="访问日志详情" width="70%">
<el-descriptions v-if="currentRecord" :column="2" border>
<el-descriptions-item label="日志ID">
{{ currentRecord.id }}
</el-descriptions-item>
<el-descriptions-item label="用户">
{{ currentRecord.username }}
</el-descriptions-item>
<el-descriptions-item label="租户ID">
{{ currentRecord.tenant_id }}
</el-descriptions-item>
<el-descriptions-item label="用户ID">
{{ currentRecord.user_id }}
</el-descriptions-item>
<el-descriptions-item label="模块">
{{ currentRecord.module_name }}
</el-descriptions-item>
<el-descriptions-item label="资源类型">
{{ currentRecord.resource_type }}
</el-descriptions-item>
<el-descriptions-item label="资源ID">
{{ currentRecord.resource_id || '-' }}
</el-descriptions-item>
<el-descriptions-item label="访问路径">
{{ currentRecord.request_url }}
</el-descriptions-item>
<el-descriptions-item label="请求方法">
{{ currentRecord.request_method }}
</el-descriptions-item>
<el-descriptions-item label="查询字符串">
{{ currentRecord.query_string || '-' }}
</el-descriptions-item>
<el-descriptions-item label="IP地址">
{{ currentRecord.ip_address }}
</el-descriptions-item>
<el-descriptions-item label="User Agent">
<div style="word-break: break-all; font-size: 12px">
{{ currentRecord.user_agent }}
</div>
</el-descriptions-item>
<el-descriptions-item label="耗时(ms)">
{{ currentRecord.duration }}
</el-descriptions-item>
<el-descriptions-item label="访问时间">
{{ formatTime(currentRecord.create_time) }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
<!-- 清空对话框 -->
<el-dialog v-model="clearDialogVisible" title="清空日志" width="400px">
<el-form :model="clearForm" label-width="100px">
<el-form-item label="保留天数">
<el-input-number v-model="clearForm.keepDays" :min="0" :max="365" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="clearDialogVisible = false">取消</el-button>
<el-button type="danger" @click="handleClear">确定清空</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getAccessLogs, clearOldAccessLogs } from '@/api/accessLog'
import { getAllMenus } from '@/api/menu'
const filters = reactive({
username: '',
module: '',
resource_type: ''
})
const dateRange = reactive({
range: null
})
const tableData = ref([])
const loading = ref(false)
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(20)
const detailDialogVisible = ref(false)
const clearDialogVisible = ref(false)
const currentRecord = ref(null)
const clearForm = reactive({
keepDays: 90
})
const moduleOptions = ref([])
// 格式化时间
const formatTime = (time) => {
if (!time) return '-'
const date = new Date(time)
return date.toLocaleString('zh-CN')
}
// 获取HTTP方法的标签类型
const getMethodTag = (method) => {
const tagMap = {
GET: 'success',
POST: 'warning',
PUT: 'info',
DELETE: 'danger'
}
return tagMap[method] || 'info'
}
// 加载菜单数据(用于模块选项)
const loadMenus = async () => {
try {
const res = await getAllMenus()
if (res.data) {
const menus = res.data.list || res.data || []
const options = menus.map((m) => ({
value: m.path ? m.path.split('/').pop() : m.permission,
label: m.name
}))
moduleOptions.value = options
}
} catch (e) {
console.error('Failed to load menus:', e)
}
}
// 加载日志列表
const loadLogs = async () => {
loading.value = true
try {
const params = {
page_num: currentPage.value,
page_size: pageSize.value,
username: filters.username || undefined,
module: filters.module || undefined,
resource_type: filters.resource_type || undefined
}
if (dateRange.range && dateRange.range.length === 2) {
params.start_time = dateRange.range[0].toLocaleString('zh-CN')
params.end_time = dateRange.range[1].toLocaleString('zh-CN')
}
const res = await getAccessLogs(params)
if (res.data) {
tableData.value = res.data
// 补充模块名称(从 moduleOptions 中查找)
tableData.value.forEach((row) => {
const module = moduleOptions.value.find((m) => m.value === row.module)
row.module_name = module ? module.label : row.module
})
total.value = res.total || 0
}
} catch (error) {
ElMessage.error('加载访问日志失败')
console.error(error)
} finally {
loading.value = false
}
}
// 处理搜索
const handleSearch = () => {
currentPage.value = 1
loadLogs()
}
// 处理重置
const handleReset = () => {
filters.username = ''
filters.module = ''
filters.resource_type = ''
dateRange.range = null
currentPage.value = 1
loadLogs()
}
// 处理日期改变
const handleDateChange = () => {
currentPage.value = 1
loadLogs()
}
// 处理分页改变
const handlePageChange = () => {
loadLogs()
}
// 显示详情
const showDetail = (row) => {
currentRecord.value = row
detailDialogVisible.value = true
}
// 显示清空对话框
const showClearDialog = () => {
clearDialogVisible.value = true
}
// 处理清空日志
const handleClear = async () => {
try {
await ElMessageBox.confirm(
`确定要清空 ${clearForm.keepDays} 天前的日志吗?该操作不可撤销。`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
await clearOldAccessLogs(clearForm.keepDays)
ElMessage.success('日志清空成功')
clearDialogVisible.value = false
loadLogs()
} catch (e) {
// 取消操作
}
}
// 处理导出
const handleExport = () => {
// 可以调用后端导出接口或使用前端库导出
ElMessage.info('导出功能暂未实现')
}
// 页面初始化
onMounted(async () => {
await loadMenus()
await loadLogs()
})
</script>
<style scoped>
.access-log-container {
padding: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.log-count {
color: #909399;
font-size: 14px;
}
</style>
@@ -0,0 +1,181 @@
<template>
<el-dialog
v-model="dialogVisible"
title="操作日志详情"
width="80%"
@close="closeDialog"
>
<div v-if="logDetail" class="detail-container">
<!-- 基本信息 -->
<el-descriptions :column="2" border>
<el-descriptions-item label="操作ID">{{ logDetail.id }}</el-descriptions-item>
<el-descriptions-item label="用户">{{ logDetail.username }}</el-descriptions-item>
<el-descriptions-item label="模块">{{ logDetail.module }}</el-descriptions-item>
<el-descriptions-item label="操作类型">
<el-tag :type="getOperationTag(logDetail.operation)">{{ logDetail.operation }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="资源类型">{{ logDetail.resource_type }}</el-descriptions-item>
<el-descriptions-item label="资源ID">{{ logDetail.resource_id }}</el-descriptions-item>
<el-descriptions-item label="操作结果">
<el-tag :type="logDetail.status === 1 ? 'success' : 'danger'">
{{ logDetail.status === 1 ? '成功' : '失败' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="操作耗时">{{ logDetail.duration_ms }}ms</el-descriptions-item>
<el-descriptions-item label="请求方法">{{ logDetail.request_method }}</el-descriptions-item>
<el-descriptions-item label="请求IP">{{ logDetail.ip_address }}</el-descriptions-item>
<el-descriptions-item label="操作时间" :span="2">{{ formatTime(logDetail.create_time) }}</el-descriptions-item>
</el-descriptions>
<!-- 请求信息 -->
<el-divider content-position="left">请求信息</el-divider>
<el-descriptions :column="1" border>
<el-descriptions-item label="请求URL">
<div class="url-text">{{ logDetail.request_url }}</div>
</el-descriptions-item>
<el-descriptions-item label="User Agent">
<div class="user-agent-text">{{ logDetail.user_agent }}</div>
</el-descriptions-item>
</el-descriptions>
<!-- 操作说明 -->
<el-divider content-position="left">操作说明</el-divider>
<div class="description-box">{{ logDetail.description || '-' }}</div>
<!-- 修改前后值 -->
<div v-if="logDetail.old_value || logDetail.new_value">
<el-divider content-position="left">数据变更</el-divider>
<el-row :gutter="20">
<el-col :span="12" v-if="logDetail.old_value">
<h4>修改前</h4>
<pre class="json-box">{{ formatJson(logDetail.old_value) }}</pre>
</el-col>
<el-col :span="12" v-if="logDetail.new_value">
<h4>修改后</h4>
<pre class="json-box">{{ formatJson(logDetail.new_value) }}</pre>
</el-col>
</el-row>
</div>
<!-- 错误信息 -->
<div v-if="logDetail.error_message">
<el-divider content-position="left">错误信息</el-divider>
<el-alert
:title="logDetail.error_message"
type="error"
:closable="false"
show-icon
/>
</div>
</div>
<template #footer>
<el-button @click="closeDialog">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref } from 'vue'
import { getOperationLogById } from '@/api/operationLog'
import { ElMessage } from 'element-plus'
const dialogVisible = ref(false)
const logDetail = ref(null)
const getOperationTag = (operation) => {
const map = {
'CREATE': 'success',
'READ': 'info',
'UPDATE': 'warning',
'DELETE': 'danger',
'LOGIN': 'primary',
'LOGOUT': 'info'
}
return map[operation] || 'info'
}
const formatTime = (time) => {
if (!time) return '-'
return new Date(time).toLocaleString('zh-CN')
}
const formatJson = (jsonStr) => {
try {
if (typeof jsonStr === 'string') {
return JSON.stringify(JSON.parse(jsonStr), null, 2)
}
return JSON.stringify(jsonStr, null, 2)
} catch {
return jsonStr || '-'
}
}
const openDetail = async (id) => {
try {
const res = await getOperationLogById(id)
if (res.success) {
logDetail.value = res.data
dialogVisible.value = true
}
} catch (error) {
ElMessage.error('加载日志详情失败')
}
}
const closeDialog = () => {
dialogVisible.value = false
logDetail.value = null
}
defineExpose({
openDetail
})
</script>
<style scoped>
.detail-container {
padding: 10px 0;
}
.url-text {
word-break: break-all;
font-family: monospace;
font-size: 12px;
background: #f5f7fa;
padding: 8px;
border-radius: 4px;
}
.user-agent-text {
word-break: break-all;
font-family: monospace;
font-size: 12px;
background: #f5f7fa;
padding: 8px;
border-radius: 4px;
}
.description-box {
background: #f5f7fa;
padding: 12px;
border-radius: 4px;
min-height: 60px;
line-height: 1.6;
}
.json-box {
background: #f5f7fa;
padding: 12px;
border-radius: 4px;
font-size: 12px;
overflow-x: auto;
border-left: 3px solid #409eff;
margin: 0;
}
h4 {
margin: 10px 0 5px 0;
color: #303133;
}
</style>
@@ -0,0 +1,111 @@
<template>
<el-container>
<!-- 统计面板 -->
<el-card class="statistics-card" shadow="hover">
<template #header>
<div class="card-header">
<span>操作统计</span>
<el-button type="primary" size="small" @click="refreshStats">刷新</el-button>
</div>
</template>
<el-row :gutter="20" v-if="statsData">
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-item">
<div class="stat-label">总操作数</div>
<div class="stat-value">{{ statsData.total_operations || 0 }}</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-item">
<div class="stat-label">活跃用户</div>
<div class="stat-value">{{ statsData.total_users || 0 }}</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-item">
<div class="stat-label">查询操作</div>
<div class="stat-value" style="color: #409eff">{{ statsData.query_operations || 0 }}</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="stat-item">
<div class="stat-label">修改操作</div>
<div class="stat-value" style="color: #e6a23c">{{ statsData.modify_operations || 0 }}</div>
</div>
</el-col>
</el-row>
<!-- 用户排行 -->
<div v-if="topUsers && topUsers.length > 0" style="margin-top: 20px">
<h4>用户操作排行</h4>
<el-table :data="topUsers" stripe max-height="300">
<el-table-column prop="username" label="用户名" width="150" />
<el-table-column prop="operation_count" label="操作次数" width="100" align="center" />
<el-table-column prop="last_operation_time" label="最后操作" width="180" />
</el-table>
</div>
</el-card>
</el-container>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getTenantOperationStats } from '@/api/operationLog'
import { ElMessage } from 'element-plus'
const statsData = ref(null)
const topUsers = ref([])
const days = ref(7)
const loadStats = async () => {
try {
const res = await getTenantOperationStats({ days: days.value })
if (res.success) {
statsData.value = res.data.statistics
topUsers.value = res.data.top_users || []
}
} catch (error) {
ElMessage.error('加载统计数据失败')
}
}
const refreshStats = () => {
loadStats()
}
onMounted(() => {
loadStats()
})
</script>
<style scoped>
.statistics-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.stat-item {
text-align: center;
padding: 15px;
background: #f0f9ff;
border-radius: 4px;
}
.stat-label {
font-size: 12px;
color: #909399;
margin-bottom: 8px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #303133;
}
</style>
+349
View File
@@ -0,0 +1,349 @@
<template>
<div class="operation-log-container">
<!-- 统计面板 -->
<!-- <StatisticsPanel /> -->
<!-- 搜索和操作栏 -->
<el-card shadow="hover" style="margin-bottom: 20px">
<el-form :model="filters" label-width="100px" :inline="true">
<el-form-item label="用户">
<el-input v-model="filters.username" placeholder="搜索用户名" clearable />
</el-form-item>
<el-form-item label="模块">
<el-select v-model="filters.module" placeholder="选择模块" clearable>
<el-option label="全部" value="" />
<el-option
v-for="opt in moduleOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="操作类型">
<el-select v-model="filters.operation" placeholder="选择操作类型" clearable>
<el-option label="全部" value="" />
<el-option label="新增" value="CREATE" />
<el-option label="查询" value="READ" />
<el-option label="修改" value="UPDATE" />
<el-option label="删除" value="DELETE" />
<el-option label="登录" value="LOGIN" />
</el-select>
</el-form-item>
<el-form-item label="操作结果">
<el-select v-model="filters.status" placeholder="选择操作结果" clearable>
<el-option label="全部" value="" />
<el-option label="成功" :value="1" />
<el-option label="失败" :value="0" />
</el-select>
</el-form-item>
</el-form>
<el-form :model="dateRange" label-width="100px" :inline="true">
<el-form-item label="操作时间">
<el-date-picker
v-model="dateRange.range"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleDateChange"
/>
</el-form-item>
</el-form>
<div style="margin-top: 15px">
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button @click="showClearDialog">清空日志</el-button>
<el-button @click="handleExport">导出</el-button>
</div>
</el-card>
<!-- 日志列表 -->
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>操作日志列表</span>
<span class="log-count"> {{ total }} </span>
</div>
</template>
<el-table
:data="tableData"
stripe
style="width: 100%; margin-bottom: 20px"
v-loading="loading"
@row-click="handleRowClick"
>
<el-table-column prop="username" label="用户" align="center"/>
<el-table-column prop="module_name" label="模块" align="center">
<template #default="{ row }">
<el-tag>{{ row.module_name }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="operation" label="操作类型" align="center">
<template #default="{ row }">
<el-tag :type="getOperationTag(row.operation)">{{ row.operation }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="resource_type" label="资源类型" align="center"/>
<el-table-column prop="status" label="结果" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="ip_address" label="IP地址" align="center"/>
<el-table-column prop="create_time" label="操作时间" align="center">
<template #default="{ row }">
{{ formatTime(row.create_time) }}
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click.stop="handleDetail(row.id)">
查看详情
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="pagination.page_num"
v-model:page-size="pagination.page_size"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@change="loadLogs"
/>
</el-card>
<!-- 详情对话框 -->
<OperationLogDetail ref="detailRef" />
<!-- 清空日志对话框 -->
<el-dialog v-model="clearDialogVisible" title="清空旧日志" width="400px">
<el-form :model="clearForm" label-width="150px">
<el-form-item label="保留天数">
<el-input-number
v-model="clearForm.keep_days"
:min="1"
:max="365"
controls-position="right"
/>
<span style="color: #909399; font-size: 12px; margin-left: 10px">
将删除超过 {{ clearForm.keep_days }} 天的日志
</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="clearDialogVisible = false">取消</el-button>
<el-button type="danger" @click="handleClearLogs">确认清空</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { getOperationLogs, clearOldLogs } from '@/api/operationLog'
import { getAllMenus } from '@/api/menu'
import { ElMessage, ElMessageBox } from 'element-plus'
// import StatisticsPanel from './components/StatisticsPanel.vue'
import OperationLogDetail from './components/OperationLogDetail.vue'
const tableData = ref([])
const loading = ref(false)
const total = ref(0)
const detailRef = ref(null)
const clearDialogVisible = ref(false)
const moduleOptions = ref([])
const pagination = reactive({
page_num: 1,
page_size: 20
})
const filters = reactive({
username: '',
module: '',
operation: '',
status: ''
})
const dateRange = reactive({
range: null
})
const clearForm = reactive({
keep_days: 90
})
const getOperationTag = (operation) => {
const map = {
'CREATE': 'success',
'READ': 'info',
'UPDATE': 'warning',
'DELETE': 'danger',
'LOGIN': 'primary',
'LOGOUT': 'info'
}
return map[operation] || 'info'
}
const formatTime = (time) => {
if (!time) return '-'
return new Date(time).toLocaleString('zh-CN')
}
// 加载系统菜单并构建模块下拉选项
const loadMenus = async () => {
try {
const res = await getAllMenus()
if (res && res.success && Array.isArray(res.data)) {
const map = new Map()
res.data.forEach((m) => {
const name = m.name || m.title || ''
let code = ''
// 优先使用 path 最后一段作为模块标识(例如 /system/dict -> dict),更贴近日志中的 module 字段
if (m.path) {
const parts = m.path.split('/').filter(Boolean)
code = parts.length ? parts[parts.length - 1] : m.path
} else if (m.permission) {
// 次选使用 permission
code = m.permission
} else {
code = String(m.id)
}
if (code && !map.has(code)) {
map.set(code, name)
}
})
moduleOptions.value = Array.from(map.entries()).map(([value, label]) => ({ value, label }))
}
} catch (error) {
// 忽略菜单加载错误,不影响日志页面其他功能
console.error('加载菜单失败', error)
}
}
const loadLogs = async () => {
loading.value = true
try {
const params = {
...pagination,
...filters
}
// 添加日期范围参数
if (dateRange.range && dateRange.range.length === 2) {
params.start_time = new Date(dateRange.range[0]).toISOString().split('T')[0]
params.end_time = new Date(dateRange.range[1]).toISOString().split('T')[0]
}
const res = await getOperationLogs(params)
if (res.success) {
tableData.value = res.data || []
total.value = res.total || 0
}
} catch (error) {
ElMessage.error('加载日志失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.page_num = 1
loadLogs()
}
const handleReset = () => {
filters.username = ''
filters.module = ''
filters.operation = ''
filters.status = ''
dateRange.range = null
pagination.page_num = 1
loadLogs()
}
const handleDateChange = () => {
handleSearch()
}
const handleRowClick = (row) => {
handleDetail(row.id)
}
const handleDetail = (id) => {
detailRef.value?.openDetail(id)
}
const showClearDialog = () => {
clearDialogVisible.value = true
}
const handleClearLogs = async () => {
ElMessageBox.confirm(
`将删除超过 ${clearForm.keep_days} 天的所有操作日志,此操作无法撤销!`,
'警告',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
const res = await clearOldLogs(clearForm.keep_days)
if (res.success) {
ElMessage.success('日志清空成功')
clearDialogVisible.value = false
loadLogs()
}
} catch (error) {
ElMessage.error('清空日志失败')
}
}).catch(() => {
// 用户取消
})
}
const handleExport = () => {
ElMessage.info('导出功能开发中...')
}
onMounted(async () => {
await loadMenus()
loadLogs()
})
</script>
<style scoped>
.operation-log-container {
padding: 10px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.log-count {
color: #909399;
font-size: 14px;
}
:deep(.el-table__row) {
cursor: pointer;
}
:deep(.el-table__row:hover) {
background-color: #f5f7fa;
}
</style>