|
|
@@ -0,0 +1,341 @@
|
|
|
+<template>
|
|
|
+ <div class="audit-container">
|
|
|
+ <!-- 搜索区域 -->
|
|
|
+ <el-card class="search-card" shadow="hover">
|
|
|
+ <el-form :model="queryParams" inline>
|
|
|
+ <el-form-item label="操作类型">
|
|
|
+ <el-select v-model="queryParams.action" placeholder="全部" clearable style="width: 120px">
|
|
|
+ <el-option label="创建" value="create" />
|
|
|
+ <el-option label="更新" value="update" />
|
|
|
+ <el-option label="删除" value="delete" />
|
|
|
+ <el-option label="登录" value="login" />
|
|
|
+ <el-option label="登出" value="logout" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="资源类型">
|
|
|
+ <el-select v-model="queryParams.resource" placeholder="全部" clearable style="width: 120px">
|
|
|
+ <el-option label="用户" value="user" />
|
|
|
+ <el-option label="摄像头" value="camera" />
|
|
|
+ <el-option label="视频" value="video" />
|
|
|
+ <el-option label="直播" value="session" />
|
|
|
+ <el-option label="认证" value="auth" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="时间范围">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="dateRange"
|
|
|
+ type="daterange"
|
|
|
+ range-separator="至"
|
|
|
+ start-placeholder="开始日期"
|
|
|
+ end-placeholder="结束日期"
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
+ style="width: 240px"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
|
|
|
+ <el-button :icon="Refresh" @click="resetQuery">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 统计卡片 -->
|
|
|
+ <el-row :gutter="20" class="stat-row">
|
|
|
+ <el-col :xs="12" :sm="6">
|
|
|
+ <el-card shadow="hover" class="stat-card">
|
|
|
+ <el-statistic title="总操作数" :value="total" />
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="12" :sm="6">
|
|
|
+ <el-card shadow="hover" class="stat-card">
|
|
|
+ <el-statistic title="创建操作" :value="getActionCount('create')" />
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="12" :sm="6">
|
|
|
+ <el-card shadow="hover" class="stat-card">
|
|
|
+ <el-statistic title="更新操作" :value="getActionCount('update')" />
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ <el-col :xs="12" :sm="6">
|
|
|
+ <el-card shadow="hover" class="stat-card">
|
|
|
+ <el-statistic title="删除操作" :value="getActionCount('delete')" />
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 数据表格 -->
|
|
|
+ <el-card shadow="hover">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span>审计日志列表</span>
|
|
|
+ <el-button type="success" :icon="Refresh" link @click="getList">刷新</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-table v-loading="loading" :data="auditList" border stripe>
|
|
|
+ <el-table-column prop="created_at" label="时间" width="170" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ formatTime(row.created_at) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="username" label="操作用户" width="120" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.username || '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="action" label="操作类型" width="100" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="getActionTagType(row.action)" size="small">
|
|
|
+ {{ getActionLabel(row.action) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="resource" label="资源类型" width="100" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag type="info" size="small">{{ getResourceLabel(row.resource) }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="resource_id" label="资源 ID" width="120" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="ip_address" label="IP 地址" width="130" show-overflow-tooltip />
|
|
|
+ <el-table-column label="详情" min-width="200">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span v-if="row.parsedDetails" class="details-text">
|
|
|
+ {{ formatDetails(row.parsedDetails) }}
|
|
|
+ </span>
|
|
|
+ <span v-else class="details-text">-</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="80" align="center" fixed="right">
|
|
|
+ <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="queryParams.page"
|
|
|
+ v-model:page-size="queryParams.pageSize"
|
|
|
+ :page-sizes="[20, 50, 100]"
|
|
|
+ :total="total"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ class="pagination"
|
|
|
+ @size-change="getList"
|
|
|
+ @current-change="getList"
|
|
|
+ />
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 详情弹窗 -->
|
|
|
+ <el-dialog v-model="detailVisible" title="审计日志详情" width="600px">
|
|
|
+ <el-descriptions v-if="currentLog" :column="1" border>
|
|
|
+ <el-descriptions-item label="日志 ID">{{ currentLog.id }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="操作时间">{{ formatTime(currentLog.created_at) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="操作用户">{{ currentLog.username || currentLog.user_id || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="操作类型">
|
|
|
+ <el-tag :type="getActionTagType(currentLog.action)">{{ getActionLabel(currentLog.action) }}</el-tag>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="资源类型">{{ getResourceLabel(currentLog.resource) }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="资源 ID">{{ currentLog.resource_id || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="IP 地址">{{ currentLog.ip_address || '-' }}</el-descriptions-item>
|
|
|
+ <el-descriptions-item label="User Agent">
|
|
|
+ <span class="user-agent-text">{{ currentLog.user_agent || '-' }}</span>
|
|
|
+ </el-descriptions-item>
|
|
|
+ <el-descriptions-item label="详细信息">
|
|
|
+ <pre v-if="currentLog.parsedDetails" class="details-json">{{ JSON.stringify(currentLog.parsedDetails, null, 2) }}</pre>
|
|
|
+ <span v-else>-</span>
|
|
|
+ </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 { ElMessage } from 'element-plus'
|
|
|
+import { Search, Refresh } from '@element-plus/icons-vue'
|
|
|
+import { getAuditLogs, type AuditLog, type AuditLogQueryParams } from '@/api/audit'
|
|
|
+
|
|
|
+const loading = ref(false)
|
|
|
+const auditList = ref<AuditLog[]>([])
|
|
|
+const total = ref(0)
|
|
|
+const detailVisible = ref(false)
|
|
|
+const currentLog = ref<AuditLog | null>(null)
|
|
|
+const dateRange = ref<[string, string] | null>(null)
|
|
|
+
|
|
|
+const queryParams = reactive<AuditLogQueryParams & { page: number; pageSize: number }>({
|
|
|
+ action: '',
|
|
|
+ resource: '',
|
|
|
+ user_id: '',
|
|
|
+ start_date: '',
|
|
|
+ end_date: '',
|
|
|
+ page: 1,
|
|
|
+ pageSize: 20
|
|
|
+})
|
|
|
+
|
|
|
+// 简单的操作计数 (基于当前页数据)
|
|
|
+function getActionCount(action: string): number {
|
|
|
+ return auditList.value.filter(log => log.action === action).length
|
|
|
+}
|
|
|
+
|
|
|
+function getActionLabel(action: string): string {
|
|
|
+ const map: Record<string, string> = {
|
|
|
+ create: '创建',
|
|
|
+ update: '更新',
|
|
|
+ delete: '删除',
|
|
|
+ login: '登录',
|
|
|
+ logout: '登出',
|
|
|
+ view: '查看',
|
|
|
+ }
|
|
|
+ return map[action] || action
|
|
|
+}
|
|
|
+
|
|
|
+function getActionTagType(action: string): '' | 'success' | 'warning' | 'danger' | 'info' {
|
|
|
+ const map: Record<string, '' | 'success' | 'warning' | 'danger' | 'info'> = {
|
|
|
+ create: 'success',
|
|
|
+ update: 'warning',
|
|
|
+ delete: 'danger',
|
|
|
+ login: '',
|
|
|
+ logout: 'info',
|
|
|
+ view: 'info',
|
|
|
+ }
|
|
|
+ return map[action] || 'info'
|
|
|
+}
|
|
|
+
|
|
|
+function getResourceLabel(resource: string): string {
|
|
|
+ const map: Record<string, string> = {
|
|
|
+ user: '用户',
|
|
|
+ camera: '摄像头',
|
|
|
+ video: '视频',
|
|
|
+ session: '直播',
|
|
|
+ live_input: '直播源',
|
|
|
+ auth: '认证',
|
|
|
+ }
|
|
|
+ return map[resource] || resource
|
|
|
+}
|
|
|
+
|
|
|
+function formatTime(timestamp: number): string {
|
|
|
+ if (!timestamp) return '-'
|
|
|
+ return new Date(timestamp * 1000).toLocaleString('zh-CN')
|
|
|
+}
|
|
|
+
|
|
|
+function formatDetails(details: Record<string, any>): string {
|
|
|
+ if (!details) return '-'
|
|
|
+ const parts: string[] = []
|
|
|
+ if (details.message) parts.push(details.message)
|
|
|
+ if (details.title) parts.push(`标题: ${details.title}`)
|
|
|
+ if (details.name) parts.push(`名称: ${details.name}`)
|
|
|
+ if (details.username) parts.push(`用户: ${details.username}`)
|
|
|
+ return parts.length > 0 ? parts.join(', ') : JSON.stringify(details).substring(0, 50) + '...'
|
|
|
+}
|
|
|
+
|
|
|
+function showDetail(row: AuditLog) {
|
|
|
+ currentLog.value = row
|
|
|
+ detailVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+async function getList() {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const params: any = {
|
|
|
+ page: queryParams.page,
|
|
|
+ pageSize: queryParams.pageSize
|
|
|
+ }
|
|
|
+ if (queryParams.action) params.action = queryParams.action
|
|
|
+ if (queryParams.resource) params.resource = queryParams.resource
|
|
|
+ if (queryParams.user_id) params.user_id = queryParams.user_id
|
|
|
+ if (dateRange.value) {
|
|
|
+ params.start_date = dateRange.value[0]
|
|
|
+ params.end_date = dateRange.value[1]
|
|
|
+ }
|
|
|
+
|
|
|
+ const res = await getAuditLogs(params)
|
|
|
+ if (res.code === 200) {
|
|
|
+ auditList.value = res.data?.rows || []
|
|
|
+ total.value = res.data?.total || 0
|
|
|
+ } else {
|
|
|
+ ElMessage.error(res.msg || '获取审计日志失败')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Failed to load audit logs:', error)
|
|
|
+ ElMessage.error('获取审计日志失败')
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleQuery() {
|
|
|
+ queryParams.page = 1
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+function resetQuery() {
|
|
|
+ queryParams.action = ''
|
|
|
+ queryParams.resource = ''
|
|
|
+ queryParams.user_id = ''
|
|
|
+ dateRange.value = null
|
|
|
+ queryParams.page = 1
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ getList()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.audit-container {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.stat-row {
|
|
|
+ margin-bottom: 20px;
|
|
|
+
|
|
|
+ .el-col {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stat-card {
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.pagination {
|
|
|
+ margin-top: 20px;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+.details-text {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+.user-agent-text {
|
|
|
+ font-size: 12px;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+
|
|
|
+.details-json {
|
|
|
+ font-size: 12px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 4px;
|
|
|
+ max-height: 200px;
|
|
|
+ overflow: auto;
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+</style>
|