2026-06-16 18:30:01 +08:00
|
|
|
<template>
|
|
|
|
|
<div v-if="detail">
|
|
|
|
|
<el-page-header @back="$router.back()" :content="detail.title" style="margin-bottom: 24px" />
|
|
|
|
|
|
|
|
|
|
<!-- Basic Info -->
|
|
|
|
|
<el-card style="margin-bottom: 16px">
|
|
|
|
|
<template #header>基本信息</template>
|
|
|
|
|
<el-descriptions :column="3" border>
|
|
|
|
|
<el-descriptions-item label="类型">
|
|
|
|
|
<el-tag size="small" :type="issueTypeTag(detail.type)">{{ detail.type }}</el-tag>
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="平台">{{ detail.platform }}</el-descriptions-item>
|
2026-06-17 10:24:01 +08:00
|
|
|
<el-descriptions-item label="状态">
|
|
|
|
|
<el-tag size="small" :type="detail.isResolved ? 'success' : 'danger'">
|
|
|
|
|
{{ detail.isResolved ? '已解决' : '未解决' }}
|
|
|
|
|
</el-tag>
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="应用版本">{{ detail.appVersion ?? '-' }}</el-descriptions-item>
|
2026-06-16 18:30:01 +08:00
|
|
|
<el-descriptions-item label="总次数">{{ detail.count }}</el-descriptions-item>
|
2026-06-17 10:24:01 +08:00
|
|
|
<el-descriptions-item label="指纹">
|
|
|
|
|
<span style="font-family:monospace;font-size:12px">{{ detail.fingerprint ?? '-' }}</span>
|
|
|
|
|
</el-descriptions-item>
|
2026-06-16 18:30:01 +08:00
|
|
|
<el-descriptions-item label="首次出现">{{ formatTime(detail.firstSeenAt) }}</el-descriptions-item>
|
|
|
|
|
<el-descriptions-item label="最后出现" :span="2">{{ formatTime(detail.lastSeenAt) }}</el-descriptions-item>
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
</el-card>
|
|
|
|
|
|
2026-06-17 10:24:01 +08:00
|
|
|
<!-- Stack Trace (from most recent event) -->
|
|
|
|
|
<el-card v-if="latestStack" style="margin-bottom: 16px">
|
2026-06-16 18:30:01 +08:00
|
|
|
<template #header>Stack Trace</template>
|
2026-06-17 10:24:01 +08:00
|
|
|
<pre class="stack-trace">{{ latestStack }}</pre>
|
2026-06-16 18:30:01 +08:00
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
<!-- Recent Events -->
|
|
|
|
|
<el-card>
|
2026-06-17 10:24:01 +08:00
|
|
|
<template #header>最近崩溃事件</template>
|
|
|
|
|
<el-table :data="detail.events ?? []" border stripe>
|
|
|
|
|
<el-table-column prop="userId" label="用户 ID" width="180" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="platform" label="平台" width="100" />
|
|
|
|
|
<el-table-column prop="appVersion" label="版本" width="110" />
|
|
|
|
|
<el-table-column prop="message" label="错误信息" min-width="240" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="createdAt" label="时间" width="170">
|
|
|
|
|
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
2026-06-16 18:30:01 +08:00
|
|
|
</el-table-column>
|
2026-06-17 10:24:01 +08:00
|
|
|
<el-table-column label="堆栈" width="90" align="center">
|
2026-06-16 18:30:01 +08:00
|
|
|
<template #default="{ row }">
|
2026-06-17 10:24:01 +08:00
|
|
|
<el-popover v-if="row.stack" trigger="click" :width="600">
|
2026-06-16 18:30:01 +08:00
|
|
|
<template #reference>
|
|
|
|
|
<el-button link type="primary" size="small">查看</el-button>
|
|
|
|
|
</template>
|
2026-06-17 10:24:01 +08:00
|
|
|
<pre class="props-json">{{ row.stackSymbolicated || row.stack }}</pre>
|
2026-06-16 18:30:01 +08:00
|
|
|
</el-popover>
|
2026-06-17 10:24:01 +08:00
|
|
|
<span v-else style="color:#ccc">-</span>
|
2026-06-16 18:30:01 +08:00
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</el-card>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-else v-loading="loading" style="min-height: 300px" />
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-06-17 10:24:01 +08:00
|
|
|
import { ref, computed, onMounted } from 'vue'
|
2026-06-16 18:30:01 +08:00
|
|
|
import { useRoute } from 'vue-router'
|
|
|
|
|
import { bugCollectApi, type BugCollectIssueDetail } from '@/api/bugcollect'
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
const detail = ref<BugCollectIssueDetail | null>(null)
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
2026-06-17 10:24:01 +08:00
|
|
|
const latestStack = computed(() => {
|
|
|
|
|
const events = detail.value?.events ?? []
|
|
|
|
|
for (const e of events) {
|
|
|
|
|
const s = e.stackSymbolicated || e.stack
|
|
|
|
|
if (s) return s
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
})
|
|
|
|
|
|
2026-06-16 18:30:01 +08:00
|
|
|
function issueTypeTag(type: string) {
|
|
|
|
|
const map: Record<string, string> = {
|
|
|
|
|
CRASH: 'danger',
|
|
|
|
|
ERROR: 'warning',
|
|
|
|
|
ANR: 'danger',
|
|
|
|
|
WARNING: '',
|
|
|
|
|
}
|
|
|
|
|
return (map[type] ?? '') as '' | 'success' | 'warning' | 'info' | 'danger'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatTime(ts: string) {
|
|
|
|
|
if (!ts) return '-'
|
|
|
|
|
return new Date(ts).toLocaleString('zh-CN')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
const id = route.params.id as string
|
|
|
|
|
loading.value = true
|
|
|
|
|
try {
|
|
|
|
|
const res = await bugCollectApi.issueDetail(id)
|
|
|
|
|
detail.value = res.data.data
|
|
|
|
|
} catch {
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.stack-trace {
|
|
|
|
|
background: #1e1e1e;
|
|
|
|
|
color: #d4d4d4;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
}
|
|
|
|
|
.props-json {
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
max-height: 400px;
|
|
|
|
|
}
|
|
|
|
|
</style>
|