XuqmGroup-Web/tenant-platform/src/views/bug-collect/BugCollectOverview.vue

222 行
7.0 KiB
Vue

<template>
<div>
<h2 style="margin-bottom: 24px">Bug 概览</h2>
<!-- App selector bar -->
<div class="app-selector-bar">
<span class="selector-label">选择应用</span>
<el-select
:model-value="appKey"
placeholder="请选择应用"
style="width:220px"
:loading="loadingApps"
@change="setApp"
>
<el-option v-for="a in apps" :key="a.appKey" :label="a.name" :value="a.appKey" />
</el-select>
</div>
<el-empty v-if="gateStatus === 'no-app'" description="请选择一个应用" style="margin-top:80px" />
<div v-else-if="gateStatus === 'loading'" v-loading="true" style="min-height:200px" />
<div v-else-if="gateStatus === 'not-enabled'" style="margin-top:60px;text-align:center">
<el-empty description="当前应用未开通崩溃收集服务">
<el-button type="primary" @click="applyDialogVisible = true">申请开通</el-button>
</el-empty>
</div>
<template v-else>
<!-- 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>
</template>
<el-dialog v-model="applyDialogVisible" title="申请开通崩溃收集" width="400px" :close-on-click-modal="false">
<el-input v-model="applyReason" type="textarea" placeholder="请说明申请原因(选填)" :rows="3" />
<template #footer>
<el-button @click="applyDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="applyLoading" @click="submitActivation">提交申请</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed, watch } from 'vue'
import { bugCollectApi, type BugCollectOverview } from '@/api/bugcollect'
import { Warning, Plus, User } from '@element-plus/icons-vue'
import { useBugCollectApp } from '@/composables/useBugCollectApp'
const { apps, loadingApps, appKey, setApp, gateStatus, applyDialogVisible, applyReason, applyLoading, submitActivation } = useBugCollectApp()
const EMPTY: BugCollectOverview = {
totalIssues: 0,
todayNewIssues: 0,
affectedUsers: 0,
crashRate: 0,
crashRateTrend: [],
topIssues: [],
}
const overview = ref<BugCollectOverview>({ ...EMPTY })
const maxRate = computed(() => {
const trend = overview.value.crashRateTrend ?? []
const rates = trend.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'
}
async function loadData() {
if (!appKey.value || gateStatus.value !== 'enabled') return
try {
const res = await bugCollectApi.overview(appKey.value)
const d = res.data.data ?? {}
overview.value = {
totalIssues: d.totalIssues ?? 0,
todayNewIssues: d.todayNewIssues ?? 0,
affectedUsers: d.affectedUsers ?? 0,
crashRate: d.crashRate ?? 0,
crashRateTrend: d.crashRateTrend ?? [],
topIssues: d.topIssues ?? [],
}
} catch {
overview.value = { ...EMPTY }
}
}
watch(gateStatus, (s) => { if (s === 'enabled') loadData() })
onMounted(loadData)
</script>
<style scoped>
.app-selector-bar { display:flex; align-items:center; gap:12px; margin-bottom:20px; }
.selector-label { font-size:14px; color:#606266; }
.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>