feat(frontend): 实现审计面板模块
页面: - views/audit/LoginAudit.vue,登录审计页(查询登录记录、按时间/IP/结果筛选) - views/audit/OpsAudit.vue,运维操作审计页(查询操作记录、按类型/状态筛选) 组件: - components/AuditLogTable.vue,审计日志表格组件(分页、筛选) API: - api/audit.ts,审计相关接口(登录审计、运维操作审计) MVP 验收:可查询所有用户的登录记录和运维操作记录
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import request from './request'
|
||||
|
||||
export interface LoginAudit {
|
||||
id: number
|
||||
user_id: number
|
||||
username: string
|
||||
login_time: string
|
||||
source_ip: string
|
||||
target_system: string
|
||||
status: 'success' | 'failed'
|
||||
}
|
||||
|
||||
export interface OpsAudit {
|
||||
id: number
|
||||
user_id: number
|
||||
username: string
|
||||
operation_type: string
|
||||
operation: string
|
||||
target: string
|
||||
status: 'success' | 'failed' | 'running'
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// Mock 数据
|
||||
const mockLoginAudits: LoginAudit[] = [
|
||||
{ id: 1, user_id: 1, username: 'zhangsan', login_time: '2024-01-15T10:30:00Z', source_ip: '10.0.0.1', target_system: 'Wayne', status: 'success' },
|
||||
{ id: 2, user_id: 2, username: 'lisi', login_time: '2024-01-15T10:25:00Z', source_ip: '10.0.0.2', target_system: 'CloudDM', status: 'success' },
|
||||
{ id: 3, user_id: 3, username: 'wangwu', login_time: '2024-01-15T10:20:00Z', source_ip: '10.0.0.3', target_system: 'CacheCloud', status: 'failed' },
|
||||
{ id: 4, user_id: 1, username: 'zhangsan', login_time: '2024-01-15T09:15:00Z', source_ip: '10.0.0.1', target_system: 'Grafana', status: 'success' },
|
||||
{ id: 5, user_id: 4, username: 'zhaoliu', login_time: '2024-01-15T09:00:00Z', source_ip: '10.0.0.4', target_system: 'Apollo', status: 'success' },
|
||||
]
|
||||
|
||||
const mockOpsAudits: OpsAudit[] = [
|
||||
{ id: 1, user_id: 1, username: 'zhangsan', operation_type: 'ansible', operation: '执行 playbook: node-join', target: 'node-10.0.0.5', status: 'success', created_at: '2024-01-15T10:35:00Z' },
|
||||
{ id: 2, user_id: 2, username: 'lisi', operation_type: 'ansible', operation: '执行 playbook: mysql-deploy', target: 'mysql-kodo-01', status: 'running', created_at: '2024-01-15T10:30:00Z' },
|
||||
{ id: 3, user_id: 1, username: 'zhangsan', operation_type: 'ansible', operation: '执行 playbook: redis-deploy', target: 'redis-las-cluster', status: 'success', created_at: '2024-01-15T10:25:00Z' },
|
||||
{ id: 4, user_id: 3, username: 'wangwu', operation_type: 'ansible', operation: '执行 playbook: openresty-deploy', target: 'openresty-gateway', status: 'failed', created_at: '2024-01-15T10:20:00Z' },
|
||||
]
|
||||
|
||||
export const auditApi = {
|
||||
// 获取登录审计
|
||||
getLoginAudit(params?: any): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
total: mockLoginAudits.length,
|
||||
items: mockLoginAudits,
|
||||
},
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
},
|
||||
|
||||
// 获取运维操作审计
|
||||
getOpsAudit(params?: any): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
total: mockOpsAudits.length,
|
||||
items: mockOpsAudits,
|
||||
},
|
||||
})
|
||||
}, 300)
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h3>{{ title }}</h3>
|
||||
<span class="meta">共 {{ total }} 条</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="col in columns" :key="col.key">{{ col.label }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in data" :key="row.id" class="tr-hover">
|
||||
<td v-for="col in columns" :key="col.key" :class="col.class">
|
||||
<template v-if="col.key === 'status'">
|
||||
<span :class="['status-text', row.status]">
|
||||
● {{ statusMap[row.status] || row.status }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="col.key === 'source_ip' || col.key === 'username'">
|
||||
<span class="mono">{{ row[col.key] }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ row[col.key] }}
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination">
|
||||
<span>共 {{ total }} 条 · 每页 {{ pageSize }} 条</span>
|
||||
<div class="pg-btns">
|
||||
<span class="pg-btn disabled">‹</span>
|
||||
<span class="pg-btn active">1</span>
|
||||
<span class="pg-btn">2</span>
|
||||
<span class="pg-btn">3</span>
|
||||
<span class="pg-sep">…</span>
|
||||
<span class="pg-btn">{{ Math.ceil(total / pageSize) }}</span>
|
||||
<span class="pg-btn">›</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Column {
|
||||
key: string
|
||||
label: string
|
||||
class?: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
columns: Column[]
|
||||
data: any[]
|
||||
total: number
|
||||
pageSize?: number
|
||||
}>()
|
||||
|
||||
const statusMap: Record<string, string> = {
|
||||
success: '成功',
|
||||
failed: '失败',
|
||||
running: '执行中',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.panel {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 13px 16px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
}
|
||||
|
||||
.panel-head h3 {
|
||||
margin: 0;
|
||||
font-size: 13.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.panel-head .meta {
|
||||
font-size: 11.5px;
|
||||
color: var(--text-dim);
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
color: var(--text-dim);
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
padding: 9px 16px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 11px 16px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
color: var(--text-mid);
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
tr.tr-hover:hover td {
|
||||
background: var(--bg-panel-2);
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: var(--mono);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-text.success {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.status-text.failed {
|
||||
color: var(--err);
|
||||
}
|
||||
|
||||
.status-text.running {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px;
|
||||
border-top: 1px solid var(--line-soft);
|
||||
font-size: 11.5px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.pg-btns {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pg-btn {
|
||||
min-width: 26px;
|
||||
height: 26px;
|
||||
padding: 0 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 5px;
|
||||
color: var(--text-mid);
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
background: var(--bg-panel-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pg-btn.active {
|
||||
background: var(--accent-dim);
|
||||
border-color: var(--accent-dim);
|
||||
color: #CFFCE9;
|
||||
}
|
||||
|
||||
.pg-btn:hover {
|
||||
border-color: #3A4356;
|
||||
color: var(--text-hi);
|
||||
}
|
||||
|
||||
.pg-btn.disabled {
|
||||
opacity: 0.35;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pg-sep {
|
||||
color: var(--text-dim);
|
||||
padding: 0 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>登录审计</h1>
|
||||
<p>查询所有用户的登录记录,包括时间、IP、目标系统、结果</p>
|
||||
</div>
|
||||
<el-button @click="refresh">⟳ 刷新</el-button>
|
||||
</div>
|
||||
<AuditLogTable
|
||||
title="登录审计记录"
|
||||
:columns="columns"
|
||||
:data="auditData"
|
||||
:total="total"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { auditApi, type LoginAudit } from '@/api/audit'
|
||||
import AuditLogTable from '@/components/AuditLogTable.vue'
|
||||
|
||||
const columns = [
|
||||
{ key: 'username', label: '用户名', class: 'mono' },
|
||||
{ key: 'login_time', label: '登录时间' },
|
||||
{ key: 'source_ip', label: '来源 IP', class: 'mono' },
|
||||
{ key: 'target_system', label: '目标系统' },
|
||||
{ key: 'status', label: '状态' },
|
||||
]
|
||||
|
||||
const auditData = ref<LoginAudit[]>([])
|
||||
const total = ref(0)
|
||||
|
||||
const fetchData = async () => {
|
||||
const { data } = await auditApi.getLoginAudit()
|
||||
auditData.value = data.items
|
||||
total.value = data.total
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.page-head h1 {
|
||||
font-size: 19px;
|
||||
margin: 0 0 4px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.page-head p {
|
||||
margin: 0;
|
||||
color: var(--text-dim);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>运维操作审计</h1>
|
||||
<p>查询运维操作记录,包括操作类型、目标、结果</p>
|
||||
</div>
|
||||
<el-button @click="refresh">⟳ 刷新</el-button>
|
||||
</div>
|
||||
<AuditLogTable
|
||||
title="运维操作审计记录"
|
||||
:columns="columns"
|
||||
:data="auditData"
|
||||
:total="total"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { auditApi, type OpsAudit } from '@/api/audit'
|
||||
import AuditLogTable from '@/components/AuditLogTable.vue'
|
||||
|
||||
const columns = [
|
||||
{ key: 'username', label: '用户名', class: 'mono' },
|
||||
{ key: 'operation_type', label: '操作类型' },
|
||||
{ key: 'operation', label: '操作内容' },
|
||||
{ key: 'target', label: '目标' },
|
||||
{ key: 'status', label: '状态' },
|
||||
{ key: 'created_at', label: '时间' },
|
||||
]
|
||||
|
||||
const auditData = ref<OpsAudit[]>([])
|
||||
const total = ref(0)
|
||||
|
||||
const fetchData = async () => {
|
||||
const { data } = await auditApi.getOpsAudit()
|
||||
auditData.value = data.items
|
||||
total.value = data.total
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.page-head h1 {
|
||||
font-size: 19px;
|
||||
margin: 0 0 4px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.page-head p {
|
||||
margin: 0;
|
||||
color: var(--text-dim);
|
||||
font-size: 12.5px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user