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>
|
2026-02-20 16:02:54 +08:00
|
|
|
|
<el-form-item label="User ID">
|
|
|
|
|
|
<el-select v-model="queryForm.userId" allow-create clearable default-first-option filterable placeholder="请输入或选择User ID">
|
|
|
|
|
|
<el-option v-for="item in userOptions" :key="item.value" :label="item.label" :value="item.value">
|
|
|
|
|
|
<span style="float: left">{{ item.label }}</span>
|
|
|
|
|
|
<span v-if="item.value !== item.label" style="float: right; color: #8492a6; font-size: 13px; margin-left: 20px">
|
|
|
|
|
|
{{ item.value }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</el-option>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
2026-02-20 08:04:46 +08:00
|
|
|
|
<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>
|
2026-02-20 15:45:23 +08:00
|
|
|
|
<v-chart autoresize class="chart" :option="eventNameOption" @click="handleEventNameClick" />
|
2026-02-20 08:04:46 +08:00
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="12">
|
|
|
|
|
|
<el-card class="chart-card" shadow="never">
|
|
|
|
|
|
<div slot="header"><span>事件类型分布</span></div>
|
2026-02-20 15:45:23 +08:00
|
|
|
|
<v-chart autoresize class="chart" :option="eventTypeOption" @click="handleEventTypeClick" />
|
2026-02-20 08:04:46 +08:00
|
|
|
|
</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>
|
2026-02-20 15:45:23 +08:00
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="20" style="margin-top: 20px">
|
|
|
|
|
|
<el-col :span="24">
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<div slot="header" style="display: flex; justify-content: space-between; align-items: center">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
详细日志列表
|
|
|
|
|
|
<span v-if="filterText" style="font-size: 12px; color: #666">({{ filterText }})</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<el-button v-if="isFiltered" size="mini" type="text" @click="clearFilter">清除筛选</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-table :data="paginatedList" style="width: 100%">
|
|
|
|
|
|
<el-table-column label="时间" prop="createdAt" width="160">
|
|
|
|
|
|
<template slot-scope="{ row }">
|
|
|
|
|
|
{{ formatTime(row.createdAt) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="事件名称" prop="eventName" show-overflow-tooltip>
|
|
|
|
|
|
<template slot-scope="{ row }">
|
|
|
|
|
|
{{ eventNameMap[row.eventName] || row.eventName }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="事件类型" prop="eventType" show-overflow-tooltip>
|
|
|
|
|
|
<template slot-scope="{ row }">
|
|
|
|
|
|
{{ eventTypeMap[row.eventType] || row.eventType }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="页面" prop="page" show-overflow-tooltip />
|
|
|
|
|
|
<el-table-column label="元素ID" prop="elementId" show-overflow-tooltip />
|
|
|
|
|
|
<el-table-column label="元素内容" prop="elementContent" show-overflow-tooltip />
|
|
|
|
|
|
<el-table-column label="User ID" prop="userId" show-overflow-tooltip />
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
background
|
|
|
|
|
|
:current-page="currentPage"
|
|
|
|
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
|
:page-size="pageSize"
|
|
|
|
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
|
|
|
|
style="margin-top: 20px; text-align: right"
|
|
|
|
|
|
:total="total"
|
|
|
|
|
|
@current-change="handleCurrentChange"
|
|
|
|
|
|
@size-change="handleSizeChange"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
2026-02-20 08:04:46 +08:00
|
|
|
|
</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'
|
2026-02-20 16:02:54 +08:00
|
|
|
|
import { eventNameMap, eventTypeMap, appMap, userMap } from '../utils'
|
2026-02-20 08:04:46 +08:00
|
|
|
|
|
|
|
|
|
|
use([CanvasRenderer, LineChart, PieChart, BarChart, GridComponent, TooltipComponent, LegendComponent, TitleComponent])
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'TrackingCharts',
|
|
|
|
|
|
components: {
|
|
|
|
|
|
VChart,
|
|
|
|
|
|
},
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
eventNameMap,
|
|
|
|
|
|
eventTypeMap,
|
|
|
|
|
|
appMap,
|
2026-02-20 16:02:54 +08:00
|
|
|
|
userMap,
|
2026-02-20 08:04:46 +08:00
|
|
|
|
loading: false,
|
|
|
|
|
|
queryForm: {
|
|
|
|
|
|
appId: '69665538a49b8ae3be50fe5d',
|
2026-02-20 16:02:54 +08:00
|
|
|
|
userId: '',
|
2026-02-20 08:04:46 +08:00
|
|
|
|
eventName: '',
|
|
|
|
|
|
eventType: '',
|
|
|
|
|
|
dateRange: [],
|
|
|
|
|
|
},
|
2026-02-20 16:02:54 +08:00
|
|
|
|
userOptions: [],
|
2026-02-20 08:04:46 +08:00
|
|
|
|
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: [],
|
2026-02-20 15:45:23 +08:00
|
|
|
|
filteredList: [],
|
|
|
|
|
|
currentPage: 1,
|
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
|
isFiltered: false,
|
|
|
|
|
|
filterText: '',
|
2026-02-20 08:04:46 +08:00
|
|
|
|
trendOption: {},
|
|
|
|
|
|
eventNameOption: {},
|
|
|
|
|
|
eventTypeOption: {},
|
|
|
|
|
|
pageOption: {},
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-02-20 15:45:23 +08:00
|
|
|
|
computed: {
|
|
|
|
|
|
total() {
|
|
|
|
|
|
return this.filteredList.length
|
|
|
|
|
|
},
|
|
|
|
|
|
paginatedList() {
|
|
|
|
|
|
const start = (this.currentPage - 1) * this.pageSize
|
|
|
|
|
|
const end = start + this.pageSize
|
|
|
|
|
|
return this.filteredList.slice(start, end)
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-02-20 08:04:46 +08:00
|
|
|
|
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: {
|
2026-02-20 15:45:23 +08:00
|
|
|
|
formatTime(time) {
|
|
|
|
|
|
return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
|
},
|
2026-02-20 08:04:46 +08:00
|
|
|
|
async fetchData() {
|
|
|
|
|
|
this.loading = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const params = {
|
|
|
|
|
|
appId: this.queryForm.appId,
|
2026-02-20 16:02:54 +08:00
|
|
|
|
userId: this.queryForm.userId || undefined,
|
2026-02-20 08:04:46 +08:00
|
|
|
|
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 || []
|
2026-02-20 16:02:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果没有筛选用户,则更新用户选项列表
|
|
|
|
|
|
if (!this.queryForm.userId) {
|
|
|
|
|
|
const userIds = this.list.map((item) => item.userId).filter(Boolean)
|
|
|
|
|
|
const uniqueUsers = [...new Set(userIds)]
|
|
|
|
|
|
this.userOptions = uniqueUsers.map((id) => ({ value: id, label: this.userMap[id] || id }))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-20 15:45:23 +08:00
|
|
|
|
this.filteredList = [...this.list]
|
|
|
|
|
|
this.isFiltered = false
|
|
|
|
|
|
this.filterText = ''
|
|
|
|
|
|
this.currentPage = 1
|
2026-02-20 08:04:46 +08:00
|
|
|
|
this.processData()
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(error)
|
|
|
|
|
|
this.$baseMessage('获取数据失败', 'error')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.loading = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
handleQuery() {
|
|
|
|
|
|
this.fetchData()
|
|
|
|
|
|
},
|
|
|
|
|
|
resetQuery() {
|
2026-02-20 16:02:54 +08:00
|
|
|
|
this.queryForm.appId = '69665538a49b8ae3be50fe5d'
|
|
|
|
|
|
this.queryForm.userId = ''
|
2026-02-20 08:04:46 +08:00
|
|
|
|
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()
|
|
|
|
|
|
},
|
2026-02-20 15:45:23 +08:00
|
|
|
|
handleSizeChange(val) {
|
|
|
|
|
|
this.pageSize = val
|
|
|
|
|
|
this.currentPage = 1
|
|
|
|
|
|
},
|
|
|
|
|
|
handleCurrentChange(val) {
|
|
|
|
|
|
this.currentPage = val
|
|
|
|
|
|
},
|
|
|
|
|
|
clearFilter() {
|
|
|
|
|
|
this.filteredList = [...this.list]
|
|
|
|
|
|
this.isFiltered = false
|
|
|
|
|
|
this.filterText = ''
|
|
|
|
|
|
this.currentPage = 1
|
|
|
|
|
|
},
|
|
|
|
|
|
handleEventNameClick(params) {
|
|
|
|
|
|
const name = params.name
|
|
|
|
|
|
// name 可能是中文映射名,需要反向查找 key,或者在 processData 时把 key 存入
|
|
|
|
|
|
// 简单起见,processData 中 name 已经是中文名或者 key 了。
|
|
|
|
|
|
// 如果我们用中文名展示,过滤时比较麻烦。
|
|
|
|
|
|
// 最好在 processData 中让 echarts data item 包含原始 key。
|
|
|
|
|
|
|
|
|
|
|
|
// 重新检查 processData
|
|
|
|
|
|
// const eventNameData = Object.keys(eventNameCounts).map((key) => ({
|
|
|
|
|
|
// name: this.eventNameMap[key] || key,
|
|
|
|
|
|
// value: eventNameCounts[key],
|
|
|
|
|
|
// key: key // 添加原始 key
|
|
|
|
|
|
// }))
|
|
|
|
|
|
|
|
|
|
|
|
const key = params.data.key || params.name // 如果没有 key 属性,回退到 name
|
|
|
|
|
|
this.filteredList = this.list.filter((item) => {
|
|
|
|
|
|
const itemKey = item.eventName
|
|
|
|
|
|
const itemName = this.eventNameMap[itemKey] || itemKey
|
|
|
|
|
|
// 如果 params.data.key 存在,则精确匹配 key
|
|
|
|
|
|
if (params.data.key) {
|
|
|
|
|
|
return item.eventName === params.data.key
|
|
|
|
|
|
}
|
|
|
|
|
|
return itemName === name
|
|
|
|
|
|
})
|
|
|
|
|
|
this.isFiltered = true
|
|
|
|
|
|
this.filterText = `筛选事件: ${name}`
|
|
|
|
|
|
this.currentPage = 1
|
|
|
|
|
|
},
|
|
|
|
|
|
handleEventTypeClick(params) {
|
|
|
|
|
|
const name = params.name
|
|
|
|
|
|
const key = params.data.key || params.name
|
|
|
|
|
|
this.filteredList = this.list.filter((item) => {
|
|
|
|
|
|
if (params.data.key) {
|
|
|
|
|
|
return item.eventType === params.data.key
|
|
|
|
|
|
}
|
|
|
|
|
|
const itemKey = item.eventType
|
|
|
|
|
|
const itemName = this.eventTypeMap[itemKey] || itemKey
|
|
|
|
|
|
return itemName === name
|
|
|
|
|
|
})
|
|
|
|
|
|
this.isFiltered = true
|
|
|
|
|
|
this.filterText = `筛选类型: ${name}`
|
|
|
|
|
|
this.currentPage = 1
|
|
|
|
|
|
},
|
2026-02-20 08:04:46 +08:00
|
|
|
|
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],
|
2026-02-20 15:45:23 +08:00
|
|
|
|
key: key, // 保存原始key
|
2026-02-20 08:04:46 +08:00
|
|
|
|
}))
|
|
|
|
|
|
this.eventNameOption = {
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
trigger: 'item',
|
2026-02-20 15:45:23 +08:00
|
|
|
|
formatter: '{b}: {c} ({d}%)',
|
2026-02-20 08:04:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
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],
|
2026-02-20 15:45:23 +08:00
|
|
|
|
key: key, // 保存原始key
|
2026-02-20 08:04:46 +08:00
|
|
|
|
}))
|
|
|
|
|
|
this.eventTypeOption = {
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
trigger: 'item',
|
2026-02-20 15:45:23 +08:00
|
|
|
|
formatter: '{b}: {c} ({d}%)',
|
2026-02-20 08:04:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
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>
|