Files
fangke/admin/src/views/tracker/visitor/components/EventTimeline.vue
T

160 lines
4.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 访客统计 · 事件时间线(详情页访问时间线与会话抽屉共用) -->
<!-- 事件行三分支:page_view(浏览+停留)/ page_stay 独立条目 / sdk_loaded 及自定义事件 -->
<!-- 视觉:客迹 V1——灰色时间 + 36px 浅蓝图标块 + 粗体标题 + 灰色 path + 停留时长,左侧 2px 连接线贯穿 -->
<template>
<div class="evt-timeline">
<div v-for="(e, i) in events" :key="i" class="evt-row">
<span class="evt-time">{{ formatTime(e.time) }}</span>
<div class="evt-icon">
<ElIcon :size="16">
<component :is="eventMeta(e.event_name).icon" />
</ElIcon>
</div>
<div class="evt-content">
<!-- 标题行:事件名粗体(page_stay 独立条目带时长) -->
<div class="evt-title">
{{ eventMeta(e.event_name).label }}
<span v-if="e.event_name === 'page_stay'" class="evt-stay">
<span class="evt-stay-label">停留</span>
{{ formatStay(e.active_ms ?? 0) }}
</span>
</div>
<div v-if="e.path" class="evt-path">{{ e.path }}</div>
<div v-if="e.title" class="evt-page-title">{{ e.title }}</div>
<!-- page_view 合并停留:下一行「停留」灰字 + 时长深色粗体 -->
<div v-if="e.event_name === 'page_view' && (e.active_ms ?? 0) > 0" class="evt-stay">
<span class="evt-stay-label">停留</span>
{{ formatStay(e.active_ms) }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { markRaw, 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=SDK 加载、其余兜底 */
const EVENT_META: Record<string, { icon: Component; label: string }> = {
page_view: { icon: markRaw(View), label: '浏览页面' },
page_stay: { icon: markRaw(Timer), label: '页面停留' },
sdk_loaded: { icon: markRaw(Cpu), label: 'SDK 加载' }
}
const eventMeta = (name: string): { icon: Component; label: string } =>
EVENT_META[name] ?? { icon: markRaw(InfoFilled), label: name }
</script>
<style lang="scss" scoped>
$time-col: 44px;
$icon-size: 36px;
$row-gap: 12px;
// 连接线 x = 时间列 + 间距 + 图标块中心(2px 宽居中于图标中线)
$line-left: $time-col + $row-gap + $icon-size / 2 - 1px;
.evt-timeline {
position: relative;
// 左侧 2px 连接线贯穿(图标块不透明底盖住穿越段,形成"节点"视觉)
&::before {
position: absolute;
top: 6px;
bottom: 6px;
left: $line-left;
width: 2px;
content: '';
background: #dce3ec;
}
}
.evt-row {
display: flex;
gap: $row-gap;
padding: 8px 0;
}
.evt-time {
width: $time-col;
flex: none;
font-size: 12px;
line-height: $icon-size;
color: #86909c;
text-align: right;
}
.evt-icon {
position: relative;
z-index: 1;
display: flex;
width: $icon-size;
height: $icon-size;
flex: none;
align-items: center;
justify-content: center;
color: #2563eb;
background: #e8f1fe;
border-radius: 10px;
}
.evt-content {
min-width: 0;
padding: 3px 0;
}
.evt-title {
font-size: 14px;
font-weight: 600;
color: #1d2129;
}
.evt-path {
margin-top: 2px;
overflow: hidden;
font-family: var(--el-font-family-mono, monospace);
font-size: 12px;
color: #86909c;
text-overflow: ellipsis;
white-space: nowrap;
}
.evt-page-title {
margin-top: 2px;
font-size: 12px;
color: #86909c;
}
// 停留时长:「停留」灰字 + 时长深色粗体
.evt-stay {
display: block;
margin-top: 2px;
font-size: 12px;
font-weight: 600;
color: #4e5969;
.evt-stay-label {
margin-right: 4px;
font-weight: 400;
color: #86909c;
}
}
// 标题行内的独立 page_stay 时长(不再单独成行)
.evt-title .evt-stay {
display: inline;
margin-top: 0;
margin-left: 8px;
}
</style>