311 lines
9.0 KiB
Vue
311 lines
9.0 KiB
Vue
<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>
|