Files
api-client/src/views/index/components/TrackingCharts.vue

351 lines
11 KiB
Vue
Raw Normal View History

2026-02-20 08:04:46 +08:00
<template>
<div class="tracking-charts">
<el-card class="filter-card" shadow="never">
<el-form :inline="true" :model="queryForm" size="small" @submit.native.prevent>
<el-form-item label="App">
<el-select v-model="queryForm.appId" filterable>
<el-option v-for="(label, value) in appMap" :key="value" :label="label" :value="value">
<span style="float: left">{{ label }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ value }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="事件名称">
<el-select v-model="queryForm.eventName" clearable filterable placeholder="全部事件">
<el-option v-for="(label, value) in eventNameMap" :key="value" :label="label" :value="value">
<span style="float: left">{{ label }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ value }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="事件类型">
<el-select v-model="queryForm.eventType" clearable placeholder="全部类型">
<el-option v-for="(label, value) in eventTypeMap" :key="value" :label="label" :value="value" />
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker
v-model="queryForm.dateRange"
:default-time="['00:00:00', '23:59:59']"
end-placeholder="结束日期"
:picker-options="pickerOptions"
range-separator="至"
start-placeholder="开始日期"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
/>
</el-form-item>
<el-form-item>
<el-button icon="el-icon-search" type="primary" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<div v-loading="loading" class="charts-container">
<el-row :gutter="20">
<el-col :span="24">
<el-card class="chart-card" shadow="never">
<div slot="header"><span>时段趋势</span></div>
<v-chart autoresize class="chart" :option="trendOption" />
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px">
<el-col :span="12">
<el-card class="chart-card" shadow="never">
<div slot="header"><span>事件名称分布</span></div>
<v-chart autoresize class="chart" :option="eventNameOption" />
</el-card>
</el-col>
<el-col :span="12">
<el-card class="chart-card" shadow="never">
<div slot="header"><span>事件类型分布</span></div>
<v-chart autoresize class="chart" :option="eventTypeOption" />
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px">
<el-col :span="24">
<el-card class="chart-card" shadow="never">
<div slot="header"><span>页面访问排行</span></div>
<v-chart autoresize class="chart" :option="pageOption" />
</el-card>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
import { getTrackingLogsList } from '@/api/system'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart, PieChart, BarChart } from 'echarts/charts'
import { GridComponent, TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components'
import VChart from 'vue-echarts'
import dayjs from 'dayjs'
import { groupBy, countBy } from 'lodash'
import { eventNameMap, eventTypeMap, appMap } from '../utils'
use([CanvasRenderer, LineChart, PieChart, BarChart, GridComponent, TooltipComponent, LegendComponent, TitleComponent])
export default {
name: 'TrackingCharts',
components: {
VChart,
},
data() {
return {
eventNameMap,
eventTypeMap,
appMap,
loading: false,
queryForm: {
appId: '69665538a49b8ae3be50fe5d',
eventName: '',
eventType: '',
dateRange: [],
},
pickerOptions: {
shortcuts: [
{
text: '今天',
onClick(picker) {
const end = new Date()
const start = new Date()
start.setHours(0, 0, 0, 0)
end.setHours(23, 59, 59, 999)
picker.$emit('pick', [start, end])
},
},
{
text: '昨天',
onClick(picker) {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24)
start.setHours(0, 0, 0, 0)
end.setTime(end.getTime() - 3600 * 1000 * 24)
end.setHours(23, 59, 59, 999)
picker.$emit('pick', [start, end])
},
},
{
text: '最近一周',
onClick(picker) {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
picker.$emit('pick', [start, end])
},
},
],
},
list: [],
trendOption: {},
eventNameOption: {},
eventTypeOption: {},
pageOption: {},
}
},
created() {
// 默认选中今天
const end = new Date()
const start = new Date()
start.setHours(0, 0, 0, 0)
end.setHours(23, 59, 59, 999)
this.queryForm.dateRange = [dayjs(start).format('YYYY-MM-DD HH:mm:ss'), dayjs(end).format('YYYY-MM-DD HH:mm:ss')]
this.fetchData()
},
methods: {
async fetchData() {
this.loading = true
try {
const params = {
appId: this.queryForm.appId,
eventName: this.queryForm.eventName || undefined,
eventType: this.queryForm.eventType || undefined,
startTime: this.queryForm.dateRange ? this.queryForm.dateRange[0] : undefined,
endTime: this.queryForm.dateRange ? this.queryForm.dateRange[1] : undefined,
pageNo: 1,
pageSize: 10000, // 获取足够多的数据进行前端统计,或者应该依赖后端聚合接口
}
const { data } = await getTrackingLogsList(params)
this.list = data.list || []
this.processData()
} catch (error) {
console.error(error)
this.$baseMessage('获取数据失败', 'error')
} finally {
this.loading = false
}
},
handleQuery() {
this.fetchData()
},
resetQuery() {
this.queryForm.appId = 'wx2d5351b816654a93'
this.queryForm.eventName = ''
this.queryForm.eventType = ''
const end = new Date()
const start = new Date()
start.setHours(0, 0, 0, 0)
end.setHours(23, 59, 59, 999)
this.queryForm.dateRange = [dayjs(start).format('YYYY-MM-DD HH:mm:ss'), dayjs(end).format('YYYY-MM-DD HH:mm:ss')]
this.fetchData()
},
processData() {
// 1. 时段趋势
const logsByHour = groupBy(this.list, (item) => dayjs(item.createdAt).format('HH:00'))
const hours = []
const counts = []
for (let i = 0; i < 24; i++) {
const hour = `${String(i).padStart(2, '0')}:00`
hours.push(hour)
counts.push(logsByHour[hour] ? logsByHour[hour].length : 0)
}
this.trendOption = {
tooltip: {
trigger: 'axis',
},
xAxis: {
type: 'category',
data: hours,
},
yAxis: {
type: 'value',
},
series: [
{
data: counts,
type: 'line',
smooth: true,
areaStyle: {},
},
],
}
// 2. 事件名称分布
const eventNameCounts = countBy(this.list, 'eventName')
const eventNameData = Object.keys(eventNameCounts).map((key) => ({
name: this.eventNameMap[key] || key,
value: eventNameCounts[key],
}))
this.eventNameOption = {
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
left: 'left',
},
series: [
{
name: '事件名称',
type: 'pie',
radius: '50%',
data: eventNameData,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
}
// 3. 事件类型分布
const eventTypeCounts = countBy(this.list, 'eventType')
const eventTypeData = Object.keys(eventTypeCounts).map((key) => ({
name: this.eventTypeMap[key] || key,
value: eventTypeCounts[key],
}))
this.eventTypeOption = {
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
left: 'left',
},
series: [
{
name: '事件类型',
type: 'pie',
radius: '50%',
data: eventTypeData,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
}
// 4. 页面访问排行
const pageCounts = countBy(this.list, 'page')
// 排序并取前10
const sortedPages = Object.keys(pageCounts)
.sort((a, b) => pageCounts[b] - pageCounts[a])
.slice(0, 10)
const pageData = sortedPages.map((key) => pageCounts[key])
this.pageOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'value',
boundaryGap: [0, 0.01],
},
yAxis: {
type: 'category',
data: sortedPages,
},
series: [
{
name: '访问次数',
type: 'bar',
data: pageData,
},
],
}
},
},
}
</script>
<style scoped>
.tracking-charts {
padding: 20px;
}
.filter-card {
margin-bottom: 20px;
}
.chart-card {
height: 400px;
}
.chart {
height: 300px;
}
</style>