412 lines
15 KiB
Vue
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>
|