50faa73cf9
Co-Authored-By: Claude Code <noreply@anthropic.com>
160 lines
4.5 KiB
Vue
160 lines
4.5 KiB
Vue
<!-- 访客统计 · 事件时间线(详情页访问时间线与会话抽屉共用) -->
|
||
<!-- 事件行三分支: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>
|