ForcePilot/web/src/components/external-systems/asset-management/adapter-asset/AssetDetailView.vue
Kris d38f325577 feat: 完成外部系统模块多维度优化迭代
本次提交包含以下核心改进:
1.  统一组件命名规范:为所有列表页组件添加defineOptions配置组件名
2.  重构侧边导航系统:支持折叠态、路由name跳转、自动激活匹配
3.  新增通用基础组件:ErrorRetryAlert错误重试提示、PermissionButton权限按钮
4.  优化列表页错误处理:替换全局错误提示为统一ErrorRetryAlert组件
5.  增强仪表盘能力:添加自动重试机制、traceId展示、多维度指标统计
6.  完善页面容器功能:支持详情页标签、面包屑优化、权限控制
7.  新增系统健康状态监控:集成健康检查API,展示系统健康状态
8.  优化页面交互:加载态、空态、错误态统一视觉语言
2026-07-11 05:44:45 +08:00

485 lines
15 KiB
Vue

<template>
<div class="asset-detail-view">
<PageContainer :detail-label="detail?.name || ''">
<template #header>
<div class="asset-detail-view__header">
<div class="asset-detail-view__title-row">
<a-button type="text" size="small" class="asset-detail-view__back" @click="handleBack">
<template #icon><ArrowLeft :size="16" /></template>
</a-button>
<h2 class="asset-detail-view__title">
{{ detail?.name || '资产详情' }}
</h2>
<a-tag v-if="detail?.adapter_type" color="blue">{{ detail.adapter_type }}</a-tag>
<a-tag v-if="detail?.asset_type">{{ detail.asset_type }}</a-tag>
</div>
<div v-if="isAdmin && detail" class="asset-detail-view__actions">
<a-button :loading="validating" @click="handleValidate">
<template #icon><CheckCircle :size="14" /></template>
校验
</a-button>
<a-button @click="handleEdit">
<template #icon><Edit :size="14" /></template>
编辑
</a-button>
<a-button danger :loading="submitting" @click="handleDelete">
<template #icon><Trash2 :size="14" /></template>
删除
</a-button>
</div>
</div>
</template>
<LoadingState v-if="detailLoading && !detail" type="mask" text="加载资产详情..." />
<EmptyState
v-else-if="detailError"
type="default"
title="资产不存在"
:description="detailError.message || '加载资产详情失败'"
>
<template #actions>
<a-button type="primary" @click="handleBack">返回列表</a-button>
</template>
</EmptyState>
<template v-else-if="detail">
<!-- 基础信息区 -->
<section class="asset-detail-view__section">
<h3 class="asset-detail-view__section-title">基础信息</h3>
<a-descriptions :column="2" bordered size="small">
<a-descriptions-item label="资产 ID">
<code class="asset-detail-view__id">{{ detail.id }}</code>
</a-descriptions-item>
<a-descriptions-item label="资产名称">{{ detail.name }}</a-descriptions-item>
<a-descriptions-item label="适配器类型">
<a-tag color="blue">{{ detail.adapter_type }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="资产类型">
<a-tag>{{ detail.asset_type }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="状态">
<StatusTag :status="detail.status" :show-icon="true" />
</a-descriptions-item>
<a-descriptions-item label="大小">{{ formatSize(detail.size) }}</a-descriptions-item>
<a-descriptions-item label="Checksum" :span="2">
<code class="asset-detail-view__checksum">{{ detail.checksum || '-' }}</code>
</a-descriptions-item>
<a-descriptions-item v-if="detail.status_message" label="状态信息" :span="2">
{{ detail.status_message }}
</a-descriptions-item>
<a-descriptions-item v-if="detail.created_by" label="创建人">
{{ detail.created_by }}
</a-descriptions-item>
<a-descriptions-item v-if="detail.created_at" label="创建时间">
{{ formatTime(detail.created_at) }}
</a-descriptions-item>
<a-descriptions-item v-if="detail.updated_at" label="更新时间" :span="2">
{{ formatTime(detail.updated_at) }}
</a-descriptions-item>
</a-descriptions>
</section>
<!-- Tab 区 -->
<section class="asset-detail-view__section">
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange">
<!-- 预览 Tab -->
<a-tab-pane key="preview" tab="预览">
<div class="asset-detail-view__tab-body">
<div class="asset-detail-view__tab-toolbar">
<a-button
v-if="isAdmin"
size="small"
:loading="generatingPreview"
@click="handleGeneratePreview"
>
<template #icon><RefreshCw :size="14" /></template>
重新生成预览
</a-button>
</div>
<div class="asset-detail-view__preview-section">
<div class="asset-detail-view__preview-title">结构化摘要</div>
<LoadingState v-if="previewLoading && !preview" type="mask" text="加载结构化预览..." />
<EmptyState
v-else-if="previewError"
type="default"
title="结构化预览加载失败"
:description="previewError.message"
>
<template #actions>
<a-button type="primary" size="small" @click="fetchPreview(assetId)">重试</a-button>
</template>
</EmptyState>
<CodeBlock
v-else-if="preview"
:code="preview.preview || ''"
:max-height="400"
/>
<EmptyState v-else type="related" title="暂无结构化预览" />
</div>
<div class="asset-detail-view__preview-section">
<div class="asset-detail-view__preview-title">文本预览</div>
<LoadingState v-if="textPreviewLoading && !textPreview" type="mask" text="加载文本预览..." />
<EmptyState
v-else-if="textPreviewError"
type="default"
title="文本预览加载失败"
:description="textPreviewError.message"
>
<template #actions>
<a-button type="primary" size="small" @click="fetchTextPreview(assetId)">重试</a-button>
</template>
</EmptyState>
<CodeBlock
v-else-if="textPreview"
:code="textPreview.preview || ''"
:max-height="400"
/>
<EmptyState v-else type="related" title="暂无文本预览" />
</div>
</div>
</a-tab-pane>
<!-- 引用 Tab -->
<a-tab-pane key="references" tab="引用">
<div class="asset-detail-view__tab-body">
<LoadingState v-if="referencesLoading" type="mask" text="加载引用关系..." />
<EmptyState
v-else-if="referencesError"
type="default"
title="引用加载失败"
:description="referencesError.message"
>
<template #actions>
<a-button type="primary" size="small" @click="fetchReferences(assetId)">重试</a-button>
</template>
</EmptyState>
<template v-else-if="references">
<section class="asset-detail-view__ref-section">
<h4 class="asset-detail-view__ref-title">
关联工具 ({{ referencesTools.length }})
</h4>
<EmptyState v-if="!referencesTools.length" type="related" size="compact" />
<a-list v-else size="small" :data-source="referencesTools" :split="false">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta :title="item.name || item.slug" :description="item.description || ''">
<template #avatar>
<Wrench :size="16" />
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</section>
<section class="asset-detail-view__ref-section">
<h4 class="asset-detail-view__ref-title">
关联系统 ({{ referencesSystems.length }})
</h4>
<EmptyState v-if="!referencesSystems.length" type="related" size="compact" />
<a-list v-else size="small" :data-source="referencesSystems" :split="false">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta :title="item.name || item.slug" :description="item.adapter_type || ''">
<template #avatar>
<Server :size="16" />
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</section>
</template>
<EmptyState v-else type="related" title="暂无引用" description="该资产暂未被任何工具或系统引用" />
</div>
</a-tab-pane>
</a-tabs>
</section>
</template>
</PageContainer>
<!-- 编辑弹窗 -->
<AssetEditModal
v-model:visible="editModalVisible"
:asset="detail"
@success="handleEditSuccess"
/>
<!-- 删除确认 -->
<ConfirmDialog
:visible="deleteConfirmVisible"
level="high"
title="删除资产"
:content="deleteContent"
ok-text="删除"
:loading="submitting"
@confirm="handleDeleteConfirm"
@cancel="deleteConfirmVisible = false"
/>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import {
ArrowLeft,
CheckCircle,
Edit,
Trash2,
RefreshCw,
Wrench,
Server
} from 'lucide-vue-next'
import PageContainer from '@/components/external-systems/layout/PageContainer.vue'
import StatusTag from '@/components/external-systems/common/StatusTag.vue'
import EmptyState from '@/components/external-systems/common/EmptyState.vue'
import LoadingState from '@/components/external-systems/common/LoadingState.vue'
import CodeBlock from '@/components/external-systems/common/CodeBlock.vue'
import ConfirmDialog from '@/components/external-systems/common/ConfirmDialog.vue'
import AssetEditModal from './AssetEditModal.vue'
import { formatSize, formatTime } from '@/composables/external-systems/utils'
import { useExternalAsset } from '@/composables/external-systems/useExternalAsset'
import { useUserStore } from '@/stores/user'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const isAdmin = computed(() => userStore.isAdmin)
const assetId = computed(() => route.params.id)
const {
detail,
detailLoading,
detailError,
preview,
previewLoading,
previewError,
textPreview,
textPreviewLoading,
textPreviewError,
references,
referencesLoading,
referencesError,
submitting,
validating,
generatingPreview,
fetchDetail,
fetchPreview,
fetchTextPreview,
fetchReferences,
validate,
remove
} = useExternalAsset()
const activeTab = ref('preview')
const editModalVisible = ref(false)
const deleteConfirmVisible = ref(false)
const referencesTools = computed(() => references.value?.tools || [])
const referencesSystems = computed(() => references.value?.systems || [])
const deleteContent = computed(() =>
detail.value
? `确认删除资产「${detail.value.name}」?此操作不可恢复。`
: ''
)
function handleBack() {
router.push({ name: 'EsAssets' })
}
async function loadDetail() {
if (!assetId.value) return
await fetchDetail(assetId.value)
// 并行加载结构化预览与文本预览
await Promise.allSettled([
fetchPreview(assetId.value),
fetchTextPreview(assetId.value)
])
}
function handleTabChange(key) {
if (key === 'references' && !references.value && !referencesLoading.value) {
fetchReferences(assetId.value)
} else if (key === 'preview') {
if (!preview.value && !previewLoading.value) {
fetchPreview(assetId.value)
}
if (!textPreview.value && !textPreviewLoading.value) {
fetchTextPreview(assetId.value)
}
}
}
async function handleValidate() {
try {
await validate(assetId.value)
message.success('校验已完成')
await loadDetail()
} catch (err) {
message.error(err?.message || '校验失败')
}
}
async function handleGeneratePreview() {
try {
await fetchPreview(assetId.value)
message.success('预览已重新生成')
} catch (err) {
message.error(err?.message || '生成预览失败')
}
}
function handleEdit() {
editModalVisible.value = true
}
function handleEditSuccess() {
editModalVisible.value = false
loadDetail()
}
function handleDelete() {
deleteConfirmVisible.value = true
}
async function handleDeleteConfirm() {
try {
await remove(assetId.value)
message.success('删除成功')
deleteConfirmVisible.value = false
router.push({ name: 'EsAssets' })
} catch (err) {
message.error(err?.message || '删除失败')
}
}
// ===== 错误处理 =====
watch(detailError, (err) => {
if (err) message.error(err.message || '加载资产详情失败')
})
watch(previewError, (err) => {
if (err) message.error(err.message || '加载资产预览失败')
})
watch(textPreviewError, (err) => {
if (err) message.error(err.message || '加载文本预览失败')
})
watch(referencesError, (err) => {
if (err) message.error(err.message || '加载资产引用失败')
})
// ===== 生命周期 =====
onMounted(loadDetail)
</script>
<style scoped lang="less">
.asset-detail-view {
height: 100%;
&__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
width: 100%;
}
&__title-row {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
&__back {
flex-shrink: 0;
}
&__title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--gray-900);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__actions {
flex-shrink: 0;
}
&__section {
margin-bottom: 24px;
}
&__section-title {
margin: 0 0 12px;
color: var(--gray-800);
font-size: 16px;
font-weight: 600;
}
&__id {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
color: var(--gray-700);
}
&__checksum {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
color: var(--gray-700);
word-break: break-all;
}
&__tab-body {
min-height: 300px;
position: relative;
}
&__tab-toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 12px;
}
&__preview-section {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
&__preview-title {
margin-bottom: 8px;
color: var(--gray-800);
font-size: 14px;
font-weight: 600;
}
&__ref-section {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
}
&__ref-title {
margin: 0 0 8px;
color: var(--gray-800);
font-size: 14px;
font-weight: 600;
}
}
</style>