本次提交包含以下核心改进: 1. 统一组件命名规范:为所有列表页组件添加defineOptions配置组件名 2. 重构侧边导航系统:支持折叠态、路由name跳转、自动激活匹配 3. 新增通用基础组件:ErrorRetryAlert错误重试提示、PermissionButton权限按钮 4. 优化列表页错误处理:替换全局错误提示为统一ErrorRetryAlert组件 5. 增强仪表盘能力:添加自动重试机制、traceId展示、多维度指标统计 6. 完善页面容器功能:支持详情页标签、面包屑优化、权限控制 7. 新增系统健康状态监控:集成健康检查API,展示系统健康状态 8. 优化页面交互:加载态、空态、错误态统一视觉语言
485 lines
15 KiB
Vue
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>
|