fix: 审计事件详情

This commit is contained in:
luo
2026-08-10 14:44:59 +08:00
parent 9f7f619083
commit 373ad4c2a5
3 changed files with 101 additions and 33 deletions

View File

@@ -3,15 +3,19 @@
<ElButton v-if="eventId" link type="primary" @click="openEvent">事件详情</ElButton> <ElButton v-if="eventId" link type="primary" @click="openEvent">事件详情</ElButton>
<ElButton v-if="actorRef" link type="primary" @click="openActor"> 操作者行为时间线 </ElButton> <ElButton v-if="actorRef" link type="primary" @click="openActor"> 操作者行为时间线 </ElButton>
<ElButton <ElButton
v-show="showResources"
v-for="resource in stableResourceRefs" v-for="resource in stableResourceRefs"
:key="`${resource.resource_type}:${resource.resource_id}`" :key="`${resource.resource_type}:${resource.resource_id}`"
link link
type="primary" type="primary"
:title="resourceTimelineLabel(resource)"
:aria-label="resourceTimelineLabel(resource)"
@click="openResource(resource)" @click="openResource(resource)"
> >
资源审计时间线 {{ resourceTimelineLabel(resource) }}
</ElButton> </ElButton>
<ElButton <ElButton
v-show="showResources"
v-for="resource in searchableResourceRefs" v-for="resource in searchableResourceRefs"
:key="`${resource.resource_type}:${resource.resource_key}`" :key="`${resource.resource_type}:${resource.resource_key}`"
link link
@@ -52,6 +56,7 @@
AuditSearchResourceType AuditSearchResourceType
} from '@/types/api' } from '@/types/api'
import { openAuditInvestigation, openAuditResourceSearch } from './investigationController' import { openAuditInvestigation, openAuditResourceSearch } from './investigationController'
import { auditResourceTypeLabels } from '@/utils/business/audit'
import { resolveInvestigationReference } from '@/utils/business/auditNavigation' import { resolveInvestigationReference } from '@/utils/business/auditNavigation'
const props = withDefaults( const props = withDefaults(
@@ -59,8 +64,9 @@
refs?: AuditInvestigationRefs | null refs?: AuditInvestigationRefs | null
currentEventId?: string currentEventId?: string
showEmpty?: boolean showEmpty?: boolean
showResources?: boolean
}>(), }>(),
{ currentEventId: '', showEmpty: true } { currentEventId: '', showEmpty: true, showResources: true }
) )
const searchableTypes = new Set<AuditSearchResourceType>([ const searchableTypes = new Set<AuditSearchResourceType>([
'iot_card', 'iot_card',
@@ -111,8 +117,8 @@
actorRef.value || actorRef.value ||
requestId.value || requestId.value ||
correlationId.value || correlationId.value ||
stableResourceRefs.value.length || (props.showResources &&
searchableResourceRefs.value.length || (stableResourceRefs.value.length || searchableResourceRefs.value.length)) ||
stableIntegrationRefs.value.length stableIntegrationRefs.value.length
) )
) )
@@ -135,6 +141,10 @@
resourceType: resource.resource_type, resourceType: resource.resource_type,
id: resource.resource_id id: resource.resource_id
}) })
const resourceTimelineLabel = (
resource: AuditInvestigationResourceRef & { resource_id: string }
) =>
`资源审计时间线 · ${auditResourceTypeLabels[resource.resource_type] || resource.resource_type} · ${resource.resource_id}`
const searchResource = ( const searchResource = (
resource: AuditInvestigationResourceRef & { resource: AuditInvestigationResourceRef & {
resource_type: AuditSearchResourceType resource_type: AuditSearchResourceType

View File

@@ -99,6 +99,8 @@ declare module 'vue' {
ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] ElDescriptions: typeof import('element-plus/es')['ElDescriptions']

View File

@@ -99,32 +99,11 @@
><AuditInvestigationLinks ><AuditInvestigationLinks
:refs="detail.investigation_refs" :refs="detail.investigation_refs"
:current-event-id="detail.event_id" :current-event-id="detail.event_id"
:show-resources="false"
/></ElCard> /></ElCard>
<ElCard shadow="never" class="section-card"> <ElCard shadow="never" class="section-card">
<template #header>相关资源与快照</template> <template #header>相关资源与快照</template>
<ElTable :data="detail.resources || []" :row-key="resourceRowKey"> <ElTable :data="detail.resources || []" :row-key="resourceRowKey">
<ElTableColumn type="expand" width="48">
<template #default="{ row }">
<div class="resource-snapshot-grid">
<section>
<h4>身份快照</h4>
<pre>{{ auditJson(row.identity_snapshot) }}</pre>
</section>
<section>
<h4>变更前</h4>
<pre>{{ auditJson(row.before_data) }}</pre>
</section>
<section>
<h4>变更后</h4>
<pre>{{ auditJson(row.after_data) }}</pre>
</section>
<section>
<h4>主体安全数据</h4>
<pre>{{ auditJson(row.subject_data) }}</pre>
</section>
</div>
</template>
</ElTableColumn>
<ElTableColumn <ElTableColumn
prop="display_name" prop="display_name"
label="资源" label="资源"
@@ -141,7 +120,7 @@
auditResourceRelationLabels[row.relation] || row.relation auditResourceRelationLabels[row.relation] || row.relation
}}</template> }}</template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn prop="role" label="业务角色" min-width="150" /> <ElTableColumn prop="role" label="业务角色" min-width="160" show-overflow-tooltip />
<ElTableColumn label="主体可见性" min-width="150"> <ElTableColumn label="主体可见性" min-width="150">
<template #default="{ row }">{{ <template #default="{ row }">{{
auditSubjectVisibilityLabels[row.subject_visibility] || auditSubjectVisibilityLabels[row.subject_visibility] ||
@@ -160,6 +139,19 @@
row.created_at ? formatDateTime(row.created_at) : '-' row.created_at ? formatDateTime(row.created_at) : '-'
}}</template> }}</template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn label="操作" width="220" fixed="right">
<template #default="{ row }">
<ElButton link type="primary" @click="openSnapshotDialog(row)">查看快照</ElButton>
<ElButton
link
type="primary"
:disabled="!row.resource_id"
@click="openResourceTimeline(row)"
>
资源审计时间线
</ElButton>
</template>
</ElTableColumn>
</ElTable> </ElTable>
</ElCard> </ElCard>
<ElCard v-if="userStore.isSuperAdmin" shadow="never"> <ElCard v-if="userStore.isSuperAdmin" shadow="never">
@@ -170,6 +162,37 @@
<ElEmpty v-else-if="!loading" description="未找到审计事件" /> <ElEmpty v-else-if="!loading" description="未找到审计事件" />
</div> </div>
</ElCard> </ElCard>
<ElDialog
v-model="snapshotDialogVisible"
:title="snapshotDialogTitle"
width="min(960px, calc(100% - 32px))"
destroy-on-close
>
<div v-if="selectedResource" class="resource-snapshot-grid">
<ElCollapse class="resource-snapshot-section">
<ElCollapseItem title="身份快照" name="identity">
<pre>{{ auditJson(selectedResource.identity_snapshot) }}</pre>
</ElCollapseItem>
</ElCollapse>
<ElCollapse class="resource-snapshot-section">
<ElCollapseItem title="主体安全数据" name="subject">
<pre>{{ auditJson(selectedResource.subject_data) }}</pre>
</ElCollapseItem>
</ElCollapse>
<section class="resource-snapshot-diff">
<ElCollapse class="resource-snapshot-section">
<ElCollapseItem title="变更前" name="before">
<pre>{{ auditJson(selectedResource.before_data) }}</pre>
</ElCollapseItem>
</ElCollapse>
<ElCollapse class="resource-snapshot-section">
<ElCollapseItem title="变更后" name="after">
<pre>{{ auditJson(selectedResource.after_data) }}</pre>
</ElCollapseItem>
</ElCollapse>
</section>
</div>
</ElDialog>
</div> </div>
</template> </template>
@@ -195,6 +218,7 @@
import { formatDateTime } from '@/utils/business/format' import { formatDateTime } from '@/utils/business/format'
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user'
import AuditInvestigationLinks from '@/components/business/audit/AuditInvestigationLinks.vue' import AuditInvestigationLinks from '@/components/business/audit/AuditInvestigationLinks.vue'
import { openAuditInvestigation } from '@/components/business/audit/investigationController'
defineOptions({ name: 'AuditEventDetail' }) defineOptions({ name: 'AuditEventDetail' })
const route = useRoute() const route = useRoute()
@@ -202,6 +226,14 @@
const userStore = useUserStore() const userStore = useUserStore()
const loading = ref(false) const loading = ref(false)
const detail = ref<AuditEventDetail>() const detail = ref<AuditEventDetail>()
const snapshotDialogVisible = ref(false)
const selectedResource = ref<AuditResourceView>()
const snapshotDialogTitle = computed(() => {
const resource = selectedResource.value
if (!resource) return '资源快照'
const type = auditResourceTypeLabels[resource.resource_type] || resource.resource_type
return `资源快照 · ${resource.display_name || resource.resource_id || type}`
})
const scopeDisplay = computed(() => { const scopeDisplay = computed(() => {
if (!detail.value) return '-' if (!detail.value) return '-'
const type = auditScopeTypeLabels[detail.value.scope_type] || detail.value.scope_type const type = auditScopeTypeLabels[detail.value.scope_type] || detail.value.scope_type
@@ -223,6 +255,18 @@
[row.resource_type, row.resource_id || row.resource_key || row.sort_order || row.display_name] [row.resource_type, row.resource_id || row.resource_key || row.sort_order || row.display_name]
.filter((item) => item !== null && item !== undefined && item !== '') .filter((item) => item !== null && item !== undefined && item !== '')
.join(':') .join(':')
const openSnapshotDialog = (row: AuditResourceView) => {
selectedResource.value = row
snapshotDialogVisible.value = true
}
const openResourceTimeline = (row: AuditResourceView) => {
if (!row.resource_id) return
openAuditInvestigation({
mode: 'resource',
resourceType: row.resource_type,
id: row.resource_id
})
}
onMounted(async () => { onMounted(async () => {
const id = String(route.params.eventId || '') const id = String(route.params.eventId || '')
if (!id) return if (!id) return
@@ -279,14 +323,22 @@
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px; gap: 16px;
padding: 8px 16px 16px; padding: 8px 0;
} }
.resource-snapshot-grid h4 { .resource-snapshot-diff {
margin: 0 0 8px; display: grid;
font-size: 14px; grid-column: 1 / -1;
font-weight: 600; grid-template-columns: repeat(2, minmax(0, 1fr));
color: var(--el-text-color-primary); gap: 16px;
}
.resource-snapshot-section {
min-width: 0;
}
.resource-snapshot-section :deep(.el-collapse-item__content) {
padding-bottom: 0;
} }
.resource-snapshot-grid pre { .resource-snapshot-grid pre {
@@ -319,5 +371,9 @@
.resource-snapshot-grid { .resource-snapshot-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.resource-snapshot-diff {
grid-template-columns: 1fr;
}
} }
</style> </style>