Files
one-pipe-system/src/views/audit/integrations/detail.vue
luo 9f7f619083
Some checks failed
构建并部署前端到测试环境 / build-and-deploy (push) Failing after 18s
feat: 审计链路
2026-08-07 18:38:28 +08:00

412 lines
15 KiB
Vue

<template>
<div class="integration-detail-page">
<ElCard shadow="never">
<div class="detail-header">
<ElButton @click="router.back()">
<template #icon>
<ElIcon><ArrowLeft /></ElIcon>
</template>
返回
</ElButton>
<h2 class="detail-title">外部集成交互详情</h2>
<ElTag
v-if="detail"
class="result-tag"
:type="integrationCategoryMeta[detail.result.category]?.type || 'info'"
>
{{ auditResultDisplay(detail.result.code, detail.result.name) }}
</ElTag>
</div>
<div v-loading="loading">
<template v-if="detail">
<ElCard shadow="never" class="section-card">
<template #header>交互概览</template>
<ElDescriptions :column="3" border>
<ElDescriptionsItem label="提供方">
{{ detail.identity.provider_name || detail.identity.provider }}
</ElDescriptionsItem>
<ElDescriptionsItem label="方向">
{{ detail.identity.direction_name || detail.identity.direction }}
</ElDescriptionsItem>
<ElDescriptionsItem label="操作">
{{ detail.identity.operation_name || detail.identity.operation }}
</ElDescriptionsItem>
<ElDescriptionsItem label="结果">
<ElTag :type="integrationCategoryMeta[detail.result.category]?.type || 'info'">
{{ auditResultDisplay(detail.result.code, detail.result.name) }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="结果类别">
{{
integrationCategoryMeta[detail.result.category]?.label || detail.result.category
}}
</ElDescriptionsItem>
<ElDescriptionsItem label="耗时"
>{{ detail.result.duration_ms }} ms</ElDescriptionsItem
>
<ElDescriptionsItem label="HTTP 状态">
{{ detail.result.http_status ?? '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="外部结果码">
{{ detail.result.provider_code || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="状态变更">
{{ booleanText(detail.result.state_changed) }}
</ElDescriptionsItem>
<ElDescriptionsItem v-if="detail.identity.external_id" label="外部业务标识" :span="3">
{{ detail.identity.external_id }}
</ElDescriptionsItem>
<ElDescriptionsItem
v-if="detail.result.provider_message"
label="外部结果摘要"
:span="3"
>
{{ detail.result.provider_message }}
</ElDescriptionsItem>
<ElDescriptionsItem
v-if="detail.result.recovery_strategy"
label="恢复策略说明"
:span="3"
>
{{ detail.result.recovery_strategy }}
</ElDescriptionsItem>
</ElDescriptions>
</ElCard>
<div class="detail-grid section-card">
<ElCard v-if="hasResource" shadow="never">
<template #header>关联资源</template>
<ElDescriptions :column="1">
<ElDescriptionsItem label="资源类型">
{{
auditResourceTypeLabels[detail.resource?.type || ''] ||
detail.resource?.type ||
'-'
}}
</ElDescriptionsItem>
<ElDescriptionsItem label="业务标识">
{{ detail.resource?.key || '-' }}
</ElDescriptionsItem>
</ElDescriptions>
</ElCard>
<ElCard shadow="never">
<template #header>调查入口</template>
<div v-if="hasAssociations" class="link-actions">
<ElButton
v-if="requestReference"
type="primary"
plain
@click="openAuditInvestigation({ mode: 'request', id: requestReference })"
>
请求链路
</ElButton>
<ElButton
v-if="correlationReference"
type="primary"
plain
@click="
openAuditInvestigation({
mode: 'correlation',
id: correlationReference
})
"
>
业务关联链路
</ElButton>
<ElButton
v-if="detail.resource?.type && resourceReference"
type="primary"
plain
@click="openResourceTimeline"
>
资源审计时间线
</ElButton>
</div>
<span v-else class="muted">无可用调查引用</span>
<ElAlert
v-if="hasUnavailableFidelity"
class="fidelity-alert"
type="warning"
:closable="false"
title="部分关联缺少可靠标识,已隐藏相应入口。"
/>
</ElCard>
</div>
<div class="detail-grid section-card">
<ElCard shadow="never">
<template #header>触发信息</template>
<ElDescriptions :column="1">
<ElDescriptionsItem label="触发来源">
{{ detail.trigger.source || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="业务场景">
{{ detail.trigger.scene || '-' }}
</ElDescriptionsItem>
<ElDescriptionsItem label="尝试序号">
{{ detail.trigger.attempt }}
</ElDescriptionsItem>
</ElDescriptions>
</ElCard>
<ElCard shadow="never">
<template #header>时间信息</template>
<ElDescriptions :column="1">
<ElDescriptionsItem label="创建时间">
{{ formatDateTime(detail.timestamps.created_at) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="计划时间">
{{ formatOptionalDate(detail.timestamps.scheduled_at) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="开始时间">
{{ formatOptionalDate(detail.timestamps.started_at) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="更新时间">
{{ formatDateTime(detail.timestamps.updated_at) }}
</ElDescriptionsItem>
</ElDescriptions>
</ElCard>
</div>
<ElCard shadow="never" class="section-card">
<template #header>关联可靠性</template>
<ElDescriptions :column="3" border>
<ElDescriptionsItem label="尝试序列可靠">
{{ booleanText(detail.fidelity.attempt_sequence_reliable) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="业务链路可用">
{{ booleanText(detail.fidelity.correlation_available) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="资源引用可用">
{{ booleanText(detail.fidelity.resource_id_available) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="重试序列可用">
{{ booleanText(detail.fidelity.trigger_series_available) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="外部消息保真度" :span="2">
{{ detail.fidelity.provider_message_fidelity || '-' }}
</ElDescriptionsItem>
</ElDescriptions>
</ElCard>
<div v-if="hasContentSummary" class="content-grid section-card">
<ElCard v-if="hasJsonContent(detail.content.request_summary)" shadow="never">
<template #header>请求摘要</template>
<pre>{{ auditJson(detail.content.request_summary) }}</pre>
</ElCard>
<ElCard v-if="hasJsonContent(detail.content.response_summary)" shadow="never">
<template #header>响应摘要</template>
<pre>{{ auditJson(detail.content.response_summary) }}</pre>
</ElCard>
<ElCard v-if="hasJsonContent(detail.content.metadata)" shadow="never">
<template #header>扩展信息</template>
<pre>{{ auditJson(detail.content.metadata) }}</pre>
</ElCard>
</div>
<ElCard v-if="detail.attempts?.length" shadow="never">
<template #header>尝试记录</template>
<ArtTable :data="detail.attempts" :pagination="false" :margin-top="0">
<template #default>
<ElTableColumn prop="attempt" label="序号" width="80" />
<ElTableColumn label="创建时间" width="180">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</ElTableColumn>
<ElTableColumn label="操作" min-width="180" show-overflow-tooltip>
<template #default="{ row }">{{ row.operation_name || row.operation }}</template>
</ElTableColumn>
<ElTableColumn label="结果" width="110">
<template #default="{ row }">
<ElTag :type="integrationCategoryType(row.result_category)">
{{ auditResultDisplay(row.result, row.result_name) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="耗时" width="100">
<template #default="{ row }">{{ row.duration_ms }} ms</template>
</ElTableColumn>
<ElTableColumn label="已发送" width="90">
<template #default="{ row }">{{ booleanText(row.sent) }}</template>
</ElTableColumn>
<ElTableColumn label="状态变更" width="100">
<template #default="{ row }">{{ booleanText(row.state_changed) }}</template>
</ElTableColumn>
</template>
</ArtTable>
</ElCard>
</template>
<ElEmpty v-else-if="!loading" description="未找到外部集成交互记录" />
</div>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { AuditService } from '@/api/modules'
import type { IntegrationDetailResponse } from '@/types/api'
import {
auditJson,
auditResourceTypeLabels,
auditResultDisplay,
integrationCategoryMeta
} from '@/utils/business/audit'
import { formatDateTime } from '@/utils/business/format'
import { openAuditInvestigation } from '@/components/business/audit/investigationController'
import { resolveInvestigationReference } from '@/utils/business/auditNavigation'
defineOptions({ name: 'AuditIntegrationDetail' })
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const detail = ref<IntegrationDetailResponse>()
const hasUnavailableFidelity = computed(() =>
Object.values(detail.value?.fidelity || {}).some((value) => value === false)
)
const hasResource = computed(() =>
Boolean(detail.value?.resource?.type || detail.value?.resource?.key)
)
const requestReference = computed(() =>
resolveInvestigationReference(detail.value?.linkage.request_id)
)
const correlationReference = computed(() =>
resolveInvestigationReference(detail.value?.linkage.correlation_id, {
fidelity: detail.value?.fidelity.correlation_available
})
)
const resourceReference = computed(() =>
resolveInvestigationReference(detail.value?.resource?.id, {
fidelity: detail.value?.fidelity.resource_id_available
})
)
const hasAssociations = computed(() =>
Boolean(
requestReference.value ||
correlationReference.value ||
(detail.value?.resource?.type && resourceReference.value)
)
)
const hasJsonContent = (value?: Record<string, unknown> | null) =>
Boolean(value && Object.keys(value).length)
const hasContentSummary = computed(() =>
Boolean(
hasJsonContent(detail.value?.content.request_summary) ||
hasJsonContent(detail.value?.content.response_summary) ||
hasJsonContent(detail.value?.content.metadata)
)
)
const booleanText = (value: boolean) => (value ? '是' : '否')
const integrationCategoryType = (category: unknown) =>
integrationCategoryMeta[category as keyof typeof integrationCategoryMeta]?.type || 'info'
const formatOptionalDate = (value?: string | null) => (value ? formatDateTime(value) : '-')
const openResourceTimeline = () => {
if (!detail.value?.resource?.type || !resourceReference.value) return
openAuditInvestigation({
mode: 'resource',
resourceType: detail.value.resource.type,
id: resourceReference.value
})
}
onMounted(async () => {
const id = String(route.params.integrationId || '')
if (!id) return
loading.value = true
try {
detail.value = (await AuditService.getIntegrationDetail(id)).data
} catch {
ElMessage.error('外部集成交互详情加载失败')
} finally {
loading.value = false
}
})
</script>
<style scoped lang="scss">
.integration-detail-page {
padding: 20px;
}
.detail-header {
display: flex;
gap: 16px;
align-items: center;
padding-bottom: 16px;
}
.detail-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.result-tag {
margin-left: auto;
}
.section-card {
margin-bottom: 16px;
}
.detail-grid,
.content-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.content-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.link-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.muted {
color: var(--el-text-color-secondary);
}
.fidelity-alert {
margin-top: 12px;
}
pre {
max-height: 360px;
padding: 12px;
margin: 0;
overflow: auto;
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
line-height: 1.6;
overflow-wrap: anywhere;
white-space: pre-wrap;
background: var(--el-fill-color-light);
border-radius: 4px;
}
@media (width <= 992px) {
.content-grid {
grid-template-columns: 1fr;
}
}
@media (width <= 768px) {
.integration-detail-page {
padding: 12px;
}
.detail-grid {
grid-template-columns: 1fr;
}
}
</style>