170 行
4.7 KiB
Vue
170 行
4.7 KiB
Vue
|
|
<template>
|
|||
|
|
<div>
|
|||
|
|
<h2 style="margin-bottom: 24px">Bug 概览</h2>
|
|||
|
|
|
|||
|
|
<!-- Stats Cards -->
|
|||
|
|
<el-row :gutter="16">
|
|||
|
|
<el-col :span="6">
|
|||
|
|
<el-card shadow="hover">
|
|||
|
|
<el-statistic title="总 Issue 数" :value="overview.totalIssues">
|
|||
|
|
<template #prefix><el-icon><Warning /></el-icon></template>
|
|||
|
|
</el-statistic>
|
|||
|
|
</el-card>
|
|||
|
|
</el-col>
|
|||
|
|
<el-col :span="6">
|
|||
|
|
<el-card shadow="hover">
|
|||
|
|
<el-statistic title="今日新增" :value="overview.todayNewIssues">
|
|||
|
|
<template #prefix><el-icon><Plus /></el-icon></template>
|
|||
|
|
</el-statistic>
|
|||
|
|
</el-card>
|
|||
|
|
</el-col>
|
|||
|
|
<el-col :span="6">
|
|||
|
|
<el-card shadow="hover">
|
|||
|
|
<el-statistic title="影响用户数" :value="overview.affectedUsers">
|
|||
|
|
<template #prefix><el-icon><User /></el-icon></template>
|
|||
|
|
</el-statistic>
|
|||
|
|
</el-card>
|
|||
|
|
</el-col>
|
|||
|
|
<el-col :span="6">
|
|||
|
|
<el-card shadow="hover">
|
|||
|
|
<el-statistic title="崩溃率" :value="overview.crashRate" :precision="2" suffix="%" />
|
|||
|
|
</el-card>
|
|||
|
|
</el-col>
|
|||
|
|
</el-row>
|
|||
|
|
|
|||
|
|
<!-- Crash Rate Trend -->
|
|||
|
|
<el-card style="margin-top: 16px">
|
|||
|
|
<template #header>崩溃率趋势(近 7 天)</template>
|
|||
|
|
<div v-if="overview.crashRateTrend.length" class="trend-chart">
|
|||
|
|
<div
|
|||
|
|
v-for="item in overview.crashRateTrend"
|
|||
|
|
:key="item.date"
|
|||
|
|
class="trend-bar-wrap"
|
|||
|
|
>
|
|||
|
|
<div class="trend-bar-track">
|
|||
|
|
<div
|
|||
|
|
class="trend-bar"
|
|||
|
|
:style="{ height: barHeight(item.rate) + '%' }"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
<div class="trend-label">{{ item.date.slice(5) }}</div>
|
|||
|
|
<div class="trend-value">{{ item.rate.toFixed(2) }}%</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<el-empty v-else description="暂无数据" />
|
|||
|
|
</el-card>
|
|||
|
|
|
|||
|
|
<!-- Top 5 Issues -->
|
|||
|
|
<el-card style="margin-top: 16px">
|
|||
|
|
<template #header>
|
|||
|
|
<div class="toolbar toolbar-space-between">
|
|||
|
|
<span>高频错误 Top 5</span>
|
|||
|
|
<el-button link type="primary" @click="$router.push('/bugcollect/rank/freq')">查看全部</el-button>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
<el-table :data="overview.topIssues" border stripe>
|
|||
|
|
<el-table-column prop="title" label="错误标题" min-width="300" show-overflow-tooltip />
|
|||
|
|
<el-table-column prop="type" label="类型" width="120">
|
|||
|
|
<template #default="{ row }">
|
|||
|
|
<el-tag size="small" :type="issueTypeTag(row.type)">{{ row.type }}</el-tag>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
<el-table-column prop="count" label="次数" width="100" sortable />
|
|||
|
|
<el-table-column label="操作" width="100" align="center">
|
|||
|
|
<template #default="{ row }">
|
|||
|
|
<el-button link type="primary" size="small" @click="$router.push(`/bugcollect/issues/${row.id}`)">详情</el-button>
|
|||
|
|
</template>
|
|||
|
|
</el-table-column>
|
|||
|
|
</el-table>
|
|||
|
|
</el-card>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import { ref, onMounted, computed } from 'vue'
|
|||
|
|
import { bugCollectApi, type BugCollectOverview } from '@/api/bugcollect'
|
|||
|
|
import { Warning, Plus, User } from '@element-plus/icons-vue'
|
|||
|
|
|
|||
|
|
const overview = ref<BugCollectOverview>({
|
|||
|
|
totalIssues: 0,
|
|||
|
|
todayNewIssues: 0,
|
|||
|
|
affectedUsers: 0,
|
|||
|
|
crashRate: 0,
|
|||
|
|
crashRateTrend: [],
|
|||
|
|
topIssues: [],
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const maxRate = computed(() => {
|
|||
|
|
const rates = overview.value.crashRateTrend.map((i) => i.rate)
|
|||
|
|
return Math.max(...rates, 1)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
function barHeight(rate: number) {
|
|||
|
|
return Math.min((rate / maxRate.value) * 100, 100)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function issueTypeTag(type: string) {
|
|||
|
|
const map: Record<string, string> = {
|
|||
|
|
CRASH: 'danger',
|
|||
|
|
ERROR: 'warning',
|
|||
|
|
ANR: 'danger',
|
|||
|
|
WARNING: '',
|
|||
|
|
}
|
|||
|
|
return (map[type] ?? '') as '' | 'success' | 'warning' | 'info' | 'danger'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(async () => {
|
|||
|
|
try {
|
|||
|
|
const res = await bugCollectApi.overview()
|
|||
|
|
overview.value = res.data.data
|
|||
|
|
} catch {}
|
|||
|
|
})
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.trend-chart {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-end;
|
|||
|
|
gap: 16px;
|
|||
|
|
height: 200px;
|
|||
|
|
padding: 0 8px;
|
|||
|
|
}
|
|||
|
|
.trend-bar-wrap {
|
|||
|
|
flex: 1;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
height: 100%;
|
|||
|
|
}
|
|||
|
|
.trend-bar-track {
|
|||
|
|
flex: 1;
|
|||
|
|
width: 100%;
|
|||
|
|
max-width: 48px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-end;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
.trend-bar {
|
|||
|
|
width: 100%;
|
|||
|
|
background: linear-gradient(180deg, #409eff 0%, #79bbff 100%);
|
|||
|
|
border-radius: 4px 4px 0 0;
|
|||
|
|
min-height: 4px;
|
|||
|
|
transition: height 0.3s ease;
|
|||
|
|
}
|
|||
|
|
.trend-label {
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #999;
|
|||
|
|
margin-top: 6px;
|
|||
|
|
}
|
|||
|
|
.trend-value {
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #333;
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
.toolbar-space-between {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
</style>
|