This commit is contained in:
310
src/views/audit/events/index.vue
Normal file
310
src/views/audit/events/index.vue
Normal file
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="audit-events-page" id="table-full-screen">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchFormItems"
|
||||
show-expand
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:column-list="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="load"
|
||||
/>
|
||||
|
||||
<ArtTable
|
||||
row-key="event_id"
|
||||
:loading="loading"
|
||||
:data="items"
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:margin-top="10"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElButton, ElMessage, ElTag } from 'element-plus'
|
||||
import { AuditService } from '@/api/modules'
|
||||
import type { AuditEventQuery, AuditEventView } from '@/types/api'
|
||||
import type { SearchFormItem } from '@/types'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { auditResultMeta, auditRiskMeta } from '@/utils/business/audit'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'AuditEvents' })
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const items = ref<AuditEventView[]>([])
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
const initialSearchState = {
|
||||
dateRange: [] as string[],
|
||||
category: undefined,
|
||||
result: undefined,
|
||||
risk: undefined,
|
||||
source: undefined,
|
||||
actor_kind: undefined,
|
||||
scope_type: undefined
|
||||
}
|
||||
let searchForm = reactive({ ...initialSearchState })
|
||||
const categoryOptions = [
|
||||
{ label: '配置', value: 'configuration' },
|
||||
{ label: '可靠性', value: 'reliability' },
|
||||
{ label: '资产', value: 'asset' },
|
||||
{ label: '安全', value: 'security' },
|
||||
{ label: '身份', value: 'identity' },
|
||||
{ label: '业务', value: 'business' }
|
||||
]
|
||||
const resultOptions = [
|
||||
{ label: '成功', value: 'success' },
|
||||
{ label: '失败', value: 'failed' },
|
||||
{ label: '拒绝', value: 'denied' },
|
||||
{ label: '部分成功', value: 'partial' },
|
||||
{ label: '未知', value: 'unknown' }
|
||||
]
|
||||
const riskOptions = [
|
||||
{ label: '低', value: 'low' },
|
||||
{ label: '普通', value: 'normal' },
|
||||
{ label: '高', value: 'high' },
|
||||
{ label: '严重', value: 'critical' }
|
||||
]
|
||||
const sourceOptions = [
|
||||
{ label: '后台管理 API', value: 'admin_api' },
|
||||
{ label: '个人客户 API', value: 'personal_api' },
|
||||
{ label: '代理 OpenAPI', value: 'openapi' },
|
||||
{ label: '异步 Worker', value: 'worker' },
|
||||
{ label: '计划任务', value: 'scheduler' },
|
||||
{ label: '外部系统回调', value: 'callback' }
|
||||
]
|
||||
const sourceLabels: Record<string, string> = Object.fromEntries(
|
||||
sourceOptions.map((item) => [item.value, item.label])
|
||||
)
|
||||
const actorOptions = [
|
||||
{ label: '人工账号', value: 'account' },
|
||||
{ label: '个人客户', value: 'personal_customer' },
|
||||
{ label: '开放接口账号', value: 'openapi' },
|
||||
{ label: '系统任务', value: 'system_task' },
|
||||
{ label: '计划任务', value: 'scheduled_job' },
|
||||
{ label: '外部系统', value: 'external_system' }
|
||||
]
|
||||
const scopeOptions = [
|
||||
{ label: '平台', value: 'platform' },
|
||||
{ label: '店铺', value: 'shop' },
|
||||
{ label: '个人客户', value: 'personal_customer' }
|
||||
]
|
||||
const searchFormItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '起止时间',
|
||||
prop: 'dateRange',
|
||||
type: 'date',
|
||||
config: {
|
||||
type: 'daterange',
|
||||
rangeSeparator: '至',
|
||||
startPlaceholder: '开始时间',
|
||||
endPlaceholder: '结束时间',
|
||||
valueFormat: 'YYYY-MM-DD'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '类别',
|
||||
prop: 'category',
|
||||
type: 'select',
|
||||
options: categoryOptions,
|
||||
config: { clearable: true }
|
||||
},
|
||||
{
|
||||
label: '结果',
|
||||
prop: 'result',
|
||||
type: 'select',
|
||||
options: resultOptions,
|
||||
config: { clearable: true }
|
||||
},
|
||||
{
|
||||
label: '风险',
|
||||
prop: 'risk',
|
||||
type: 'select',
|
||||
options: riskOptions,
|
||||
config: { clearable: true }
|
||||
},
|
||||
{
|
||||
label: '来源',
|
||||
prop: 'source',
|
||||
type: 'select',
|
||||
options: sourceOptions,
|
||||
config: { clearable: true }
|
||||
},
|
||||
{
|
||||
label: '操作者',
|
||||
prop: 'actor_kind',
|
||||
type: 'select',
|
||||
options: actorOptions,
|
||||
config: { clearable: true }
|
||||
},
|
||||
{
|
||||
label: '业务范围',
|
||||
prop: 'scope_type',
|
||||
type: 'select',
|
||||
options: scopeOptions,
|
||||
config: { clearable: true }
|
||||
}
|
||||
]
|
||||
const columnOptions = [
|
||||
{ label: '发生时间', prop: 'occurred_at' },
|
||||
{ label: '动作', prop: 'action_name' },
|
||||
{ label: '摘要', prop: 'summary' },
|
||||
{ label: '主要资源', prop: 'primary_resource' },
|
||||
{ label: '操作者', prop: 'actor_name' },
|
||||
{ label: '来源', prop: 'source' },
|
||||
{ label: '结果', prop: 'result' },
|
||||
{ label: '风险', prop: 'risk_level' },
|
||||
{ label: '操作', prop: 'actions' }
|
||||
]
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'occurred_at',
|
||||
label: '发生时间',
|
||||
width: 180,
|
||||
formatter: (row: AuditEventView) => formatDateTime(row.occurred_at)
|
||||
},
|
||||
{
|
||||
prop: 'action_name',
|
||||
label: '动作',
|
||||
minWidth: 220,
|
||||
formatter: (row: AuditEventView) => row.action_name || '-'
|
||||
},
|
||||
{ prop: 'summary', label: '摘要', minWidth: 240, showOverflowTooltip: true },
|
||||
{
|
||||
prop: 'primary_resource',
|
||||
label: '主要资源',
|
||||
minWidth: 210,
|
||||
showOverflowTooltip: true,
|
||||
formatter: (row: AuditEventView) => {
|
||||
const resource =
|
||||
row.resources?.find((item) => item.relation === 'primary') || row.resources?.[0]
|
||||
if (!resource) return '-'
|
||||
return resource.display_name || resource.resource_key || resource.resource_id || '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'actor_name',
|
||||
label: '操作者',
|
||||
minWidth: 170,
|
||||
formatter: (row: AuditEventView) => row.actor_name || row.actor_id || '-'
|
||||
},
|
||||
{
|
||||
prop: 'source',
|
||||
label: '来源',
|
||||
showOverflowTooltip: true,
|
||||
width: 130,
|
||||
formatter: (row: AuditEventView) => sourceLabels[row.source] || row.source
|
||||
},
|
||||
{
|
||||
prop: 'result',
|
||||
label: '结果',
|
||||
width: 100,
|
||||
formatter: (row: AuditEventView) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: auditResultMeta[row.result]?.type || 'info' },
|
||||
() => auditResultMeta[row.result]?.label || row.result
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'risk_level',
|
||||
label: '风险',
|
||||
width: 90,
|
||||
formatter: (row: AuditEventView) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: auditRiskMeta[row.risk_level]?.type || 'info', effect: 'plain' },
|
||||
() => auditRiskMeta[row.risk_level]?.label || row.risk_level
|
||||
)
|
||||
},
|
||||
{
|
||||
prop: 'actions',
|
||||
label: '操作',
|
||||
width: 110,
|
||||
fixed: 'right',
|
||||
formatter: (row: AuditEventView) =>
|
||||
h(
|
||||
ElButton,
|
||||
{ type: 'primary', link: true, onClick: () => openDetail(row) },
|
||||
() => '查看详情'
|
||||
)
|
||||
}
|
||||
])
|
||||
|
||||
const toAuditDate = (value?: string, endExclusive = false) => {
|
||||
if (!value) return undefined
|
||||
const [year, month, day] = value.split('-').map(Number)
|
||||
const date = new Date(year, month - 1, day + (endExclusive ? 1 : 0))
|
||||
return date.toISOString()
|
||||
}
|
||||
|
||||
const buildQuery = (): AuditEventQuery => ({
|
||||
page: pagination.page,
|
||||
page_size: pagination.pageSize,
|
||||
created_from: toAuditDate(searchForm.dateRange?.[0]),
|
||||
created_to: toAuditDate(searchForm.dateRange?.[1], true),
|
||||
category: searchForm.category,
|
||||
result: searchForm.result,
|
||||
risk: searchForm.risk,
|
||||
source: searchForm.source,
|
||||
actor_kind: searchForm.actor_kind,
|
||||
scope_type: searchForm.scope_type
|
||||
})
|
||||
const load = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = (await AuditService.getEvents(buildQuery())).data
|
||||
items.value = data.items
|
||||
pagination.total = data.total
|
||||
} catch {
|
||||
ElMessage.error('审计事件加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
load()
|
||||
}
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, { ...initialSearchState, dateRange: [] })
|
||||
pagination.page = 1
|
||||
load()
|
||||
}
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
load()
|
||||
}
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
load()
|
||||
}
|
||||
const openDetail = (row: AuditEventView) =>
|
||||
router.push(`/audit/events/${encodeURIComponent(row.event_id)}`)
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.audit-events-page {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user