XuqmGroup-Web/ops-platform/src/views/log-monitor/LogOverview.vue
XuqmGroup 9bc227f3a3 feat: 日志监控模块 — 8 个页面
- 概览仪表盘(统计卡片 + echarts 趋势图 + Top5)
- 错误列表(分页 + 筛选)
- 错误详情(stack trace + 源码上下文)
- 事件流水(分页 + 筛选)
- 漏斗分析(动态步骤 + 转化率)
- Webhook 配置(CRUD)
- 高频/高危排行

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 16:20:16 +08:00

129 行
4.1 KiB
Vue

<template>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px">
<h2 style="margin: 0">日志监控概览</h2>
</div>
<el-menu mode="horizontal" :default-active="$route.path" router class="log-nav">
<el-menu-item index="/log/overview">概览</el-menu-item>
<el-menu-item index="/log/issues">错误列表</el-menu-item>
<el-menu-item index="/log/events">事件流水</el-menu-item>
<el-menu-item index="/log/funnels">漏斗分析</el-menu-item>
<el-menu-item index="/log/rank/freq">高频排行</el-menu-item>
<el-menu-item index="/log/rank/risk">高危排行</el-menu-item>
<el-menu-item index="/log/webhooks">Webhook</el-menu-item>
</el-menu>
<p style="margin: 16px 0 24px; color: #606266">
汇总错误追踪崩溃率与影响用户快速判断应用健康状态
</p>
<el-row :gutter="16">
<el-col :xs="12" :sm="6" v-for="item in stats" :key="item.label">
<el-card shadow="hover">
<el-statistic :title="item.label" :value="item.value" />
</el-card>
</el-col>
</el-row>
<el-row :gutter="16" style="margin-top: 16px">
<el-col :xs="24" :md="14">
<el-card>
<template #header>近 7 天崩溃率趋势</template>
<div ref="trendChartRef" class="chart-box" />
</el-card>
</el-col>
<el-col :xs="24" :md="10">
<el-card>
<template #header>Top 5 高频错误</template>
<el-table :data="topIssues" size="small" stripe>
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
<el-table-column prop="count" label="次数" width="80" align="right" />
<el-table-column prop="platform" label="平台" width="80" />
</el-table>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { logApi, type LogIssueSummary } from '@/api/log'
import * as echarts from 'echarts'
const stats = ref([
{ label: '总 Issue 数', value: 0 },
{ label: '今日新增', value: 0 },
{ label: '影响用户数', value: 0 },
{ label: '崩溃率', value: '0%' },
])
const topIssues = ref<LogIssueSummary[]>([])
const trendChartRef = ref<HTMLDivElement>()
let trendChart: echarts.ECharts | null = null
function initTrendChart(dailyTrend: Array<{ date: string; crashRate: number }>) {
if (!trendChartRef.value) return
const dates = dailyTrend.map((d) => {
const parts = d.date.split('-')
return `${parseInt(parts[1])}/${parseInt(parts[2])}`
})
const values = dailyTrend.map((d) => d.crashRate)
trendChart = echarts.init(trendChartRef.value)
trendChart.setOption({
tooltip: { trigger: 'axis', formatter: '{b}<br/>崩溃率: {c}%' },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: dates, boundaryGap: false },
yAxis: { type: 'value', axisLabel: { formatter: '{value}%' } },
series: [
{
name: '崩溃率',
type: 'line',
smooth: true,
data: values,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(245,108,108,0.3)' },
{ offset: 1, color: 'rgba(245,108,108,0.05)' },
]),
},
itemStyle: { color: '#f56c6c' },
},
],
})
}
function handleResize() {
trendChart?.resize()
}
onMounted(async () => {
try {
const res = await logApi.getOverview()
const d = res.data.data
stats.value[0].value = d.totalIssues ?? 0
stats.value[1].value = d.todayNew ?? 0
stats.value[2].value = d.affectedUsers ?? 0
stats.value[3].value = `${((d.crashRate ?? 0) * 100).toFixed(2)}%`
topIssues.value = (d.topIssues ?? []).slice(0, 5)
initTrendChart(d.dailyTrend ?? [])
} catch {
initTrendChart([])
}
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
trendChart?.dispose()
})
</script>
<style scoped>
.chart-box {
width: 100%;
height: 320px;
}
</style>