90 lines
3.0 KiB
Vue
90 lines
3.0 KiB
Vue
|
|
<!-- 访客统计 · 事件时间线(详情页轨迹 Tab 与会话抽屉共用) -->
|
|||
|
|
<!-- 事件行三分支:page_view(浏览+停留副行+title)/ page_stay 独立条目 / sdk_loaded 及自定义事件 -->
|
|||
|
|
<template>
|
|||
|
|
<ElTimeline>
|
|||
|
|
<ElTimelineItem
|
|||
|
|
v-for="(e, i) in events"
|
|||
|
|
:key="i"
|
|||
|
|
:icon="eventMeta(e.event_name).icon"
|
|||
|
|
:type="eventMeta(e.event_name).type"
|
|||
|
|
:timestamp="formatTime(e.time)"
|
|||
|
|
>
|
|||
|
|
<!-- page_view:浏览 + 停留副行(active_ms>0 才显示)+ title 小字 -->
|
|||
|
|
<template v-if="e.event_name === 'page_view'">
|
|||
|
|
<div>
|
|||
|
|
浏览
|
|||
|
|
<span class="mono-path">{{ e.path || '—' }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="(e.active_ms ?? 0) > 0" class="stay-line"
|
|||
|
|
>停留 {{ formatStay(e.active_ms) }}</div
|
|||
|
|
>
|
|||
|
|
<div v-if="e.title" class="title-line">{{ e.title }}</div>
|
|||
|
|
</template>
|
|||
|
|
<!-- page_stay 独立条目(无匹配 page_view 时服务端保留原事件名) -->
|
|||
|
|
<template v-else-if="e.event_name === 'page_stay'">
|
|||
|
|
<div>
|
|||
|
|
停留
|
|||
|
|
<span class="mono-path">{{ e.path || '—' }}</span>
|
|||
|
|
({{ formatStay(e.active_ms ?? 0) }})
|
|||
|
|
</div>
|
|||
|
|
<div v-if="e.title" class="title-line">{{ e.title }}</div>
|
|||
|
|
</template>
|
|||
|
|
<!-- sdk_loaded / 其他自定义事件 -->
|
|||
|
|
<template v-else>
|
|||
|
|
<div>
|
|||
|
|
{{ eventMeta(e.event_name).label }}
|
|||
|
|
<span v-if="e.path" class="mono-path">{{ e.path }}</span>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
</ElTimelineItem>
|
|||
|
|
</ElTimeline>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import type { Component } from 'vue'
|
|||
|
|
import { Cpu, InfoFilled, Timer, View } from '@element-plus/icons-vue'
|
|||
|
|
import type { TimelineEvent } from '@/api/tracker/visitors'
|
|||
|
|
import { formatStay } from '@/utils/tracker'
|
|||
|
|
|
|||
|
|
defineOptions({ name: 'EventTimeline' })
|
|||
|
|
|
|||
|
|
defineProps<{
|
|||
|
|
/** 已按服务端合并规则处理过的序列(page_stay 已并入 page_view 的 active_ms) */
|
|||
|
|
events: TimelineEvent[]
|
|||
|
|
}>()
|
|||
|
|
|
|||
|
|
/** ISO(后端上海时间)→ HH:mm */
|
|||
|
|
const formatTime = (iso?: string | null): string => (iso ? iso.slice(11, 16) : '—')
|
|||
|
|
|
|||
|
|
/** 事件行图标/节点色:page_view=浏览、page_stay=计时、sdk_loaded=加载、其余兜底 */
|
|||
|
|
const EVENT_META: Record<
|
|||
|
|
string,
|
|||
|
|
{ icon: Component; type: 'primary' | 'warning' | 'info'; label: string }
|
|||
|
|
> = {
|
|||
|
|
page_view: { icon: View, type: 'primary', label: '浏览' },
|
|||
|
|
page_stay: { icon: Timer, type: 'warning', label: '停留' },
|
|||
|
|
sdk_loaded: { icon: Cpu, type: 'info', label: 'SDK 加载' }
|
|||
|
|
}
|
|||
|
|
const eventMeta = (name: string) =>
|
|||
|
|
EVENT_META[name] ?? { icon: InfoFilled, type: 'info', label: name }
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.mono-path {
|
|||
|
|
font-family: var(--el-font-family-mono, monospace);
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stay-line {
|
|||
|
|
margin-top: 2px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: var(--el-color-warning);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.title-line {
|
|||
|
|
margin-top: 2px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: var(--el-text-color-secondary);
|
|||
|
|
}
|
|||
|
|
</style>
|