feat(admin): 访客详情模板重组三栏Tabs——时间线/会话/原始数据合并左栏
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<!-- 访客统计 · 访客详情(客迹视觉 V1:头部卡 + 4 统计卡 + 双栏时间线/来源证据 + 会话与原始数据) -->
|
||||
<!-- 访客统计 · 访客详情(客迹视觉 V1:头部卡 + 三栏=时间线/会话/原始数据 Tab + 统计卡/来源证据 + 访问环境) -->
|
||||
<!-- keepAlive=true 且路由 path 固定:访客切换复用同一实例,刷新依赖 onActivated + query watch -->
|
||||
<template>
|
||||
<div class="visitor-detail-page p-5">
|
||||
@@ -59,113 +59,211 @@
|
||||
<ElEmpty v-else-if="!profileLoading" description="访客数据加载失败或不存在" />
|
||||
</div>
|
||||
|
||||
<!-- 4 统计卡:图标块 + 小标签 + 大数字 -->
|
||||
<div class="vd-stats">
|
||||
<div v-for="tile in statTiles" :key="tile.label" class="vd-card vd-stat-card">
|
||||
<div class="vd-stat-icon">
|
||||
<ElIcon :size="20">
|
||||
<component :is="tile.icon" />
|
||||
</ElIcon>
|
||||
</div>
|
||||
<div class="vd-stat-body">
|
||||
<span class="vd-stat-label">{{ tile.label }}</span>
|
||||
<span class="vd-stat-value">{{ tile.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体双栏:时间线(左) + 来源证据/访问环境(右) -->
|
||||
<!-- 主体三栏:左=时间线/会话/原始数据三 Tab,中=统计卡+来源证据,右=访问环境 -->
|
||||
<div class="vd-main">
|
||||
<!-- 访问时间线 -->
|
||||
<div class="vd-card vd-timeline-card">
|
||||
<div class="vd-card-head">
|
||||
<div class="vd-card-title">
|
||||
<ElIcon :size="16"><ClockIcon /></ElIcon>
|
||||
<span>访问时间线</span>
|
||||
</div>
|
||||
<div class="vd-card-tools">
|
||||
<span class="vd-tool-label">时间范围</span>
|
||||
<ElRadioGroup v-model="days" size="small" @change="loadTimeline()">
|
||||
<ElRadioButton :value="7">近 7 天</ElRadioButton>
|
||||
<ElRadioButton :value="30">近 30 天</ElRadioButton>
|
||||
<ElRadioButton :value="90">近 90 天</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
<!-- 正序/倒序真实切换:倒序=日期组与组内事件都最新在前;正序=都最早在前 -->
|
||||
<div class="vd-order-toggle" role="group" aria-label="时间线排序">
|
||||
<button
|
||||
type="button"
|
||||
class="vd-order-btn"
|
||||
:class="{ 'is-active': order === 'desc' }"
|
||||
@click="order = 'desc'"
|
||||
>
|
||||
倒序
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="vd-order-btn"
|
||||
:class="{ 'is-active': order === 'asc' }"
|
||||
@click="order = 'asc'"
|
||||
>
|
||||
正序
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElAlert
|
||||
v-if="timeline?.truncated"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mb-3"
|
||||
title="事件较多,仅显示最近 1000 条"
|
||||
/>
|
||||
|
||||
<div v-loading="timelineLoading" class="tl-body">
|
||||
<template v-if="orderedDays.length">
|
||||
<div v-for="day in orderedDays" :key="day.date" class="tl-day">
|
||||
<!-- 日期组头:蓝点 + 09月16日 + 今天 tag -->
|
||||
<div class="tl-day-header">
|
||||
<i class="tl-dot" />
|
||||
<span class="tl-date">{{ formatDayLabel(day.date) }}</span>
|
||||
<span v-if="isToday(day.date)" class="tl-today-tag">今天</span>
|
||||
<span class="tl-day-meta">{{ day.sessions.length }} 次会话</span>
|
||||
<!-- 左栏:三 Tab(访问时间线 / 会话 / 原始数据) -->
|
||||
<div class="vd-card vd-tabs-card">
|
||||
<ElTabs v-model="activeTab" class="detail-tabs">
|
||||
<!-- Tab 1:访问时间线 -->
|
||||
<ElTabPane label="访问时间线" name="timeline">
|
||||
<!-- 工具行:原卡头工具迁入(标题职能由 tab label 承担) -->
|
||||
<div class="vd-tl-tools">
|
||||
<span class="vd-tool-label">时间范围</span>
|
||||
<ElRadioGroup v-model="days" size="small" @change="loadTimeline()">
|
||||
<ElRadioButton :value="7">近 7 天</ElRadioButton>
|
||||
<ElRadioButton :value="30">近 30 天</ElRadioButton>
|
||||
<ElRadioButton :value="90">近 90 天</ElRadioButton>
|
||||
</ElRadioGroup>
|
||||
<!-- 正序/倒序真实切换:倒序=日期组与组内事件都最新在前;正序=都最早在前 -->
|
||||
<div class="vd-order-toggle" role="group" aria-label="时间线排序">
|
||||
<button
|
||||
type="button"
|
||||
class="vd-order-btn"
|
||||
:class="{ 'is-active': order === 'desc' }"
|
||||
@click="order = 'desc'"
|
||||
>
|
||||
倒序
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="vd-order-btn"
|
||||
:class="{ 'is-active': order === 'asc' }"
|
||||
@click="order = 'asc'"
|
||||
>
|
||||
正序
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 会话块:会话头(时间·来源·停留)+ 事件序列 -->
|
||||
<div
|
||||
v-for="(session, si) in day.sessions"
|
||||
:key="session.session_id ?? `orphan-${si}`"
|
||||
class="tl-session"
|
||||
>
|
||||
<div class="tl-session-header">
|
||||
<span class="tl-session-time">
|
||||
{{ session.started_at ? formatTime(session.started_at) : '未知会话' }}
|
||||
</span>
|
||||
<span class="tl-session-meta">{{ timelineSessionSource(session) }}</span>
|
||||
<span class="tl-session-meta">停留 {{ formatStay(sessionStayMs(session)) }}</span>
|
||||
<ElAlert
|
||||
v-if="timeline?.truncated"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mb-3"
|
||||
title="事件较多,仅显示最近 1000 条"
|
||||
/>
|
||||
|
||||
<div v-loading="timelineLoading" class="tl-body">
|
||||
<template v-if="orderedDays.length">
|
||||
<div v-for="day in orderedDays" :key="day.date" class="tl-day">
|
||||
<!-- 日期组头:蓝点 + 09月16日 + 今天 tag -->
|
||||
<div class="tl-day-header">
|
||||
<i class="tl-dot" />
|
||||
<span class="tl-date">{{ formatDayLabel(day.date) }}</span>
|
||||
<span v-if="isToday(day.date)" class="tl-today-tag">今天</span>
|
||||
<span class="tl-day-meta">{{ day.sessions.length }} 次会话</span>
|
||||
</div>
|
||||
|
||||
<!-- 会话块:会话头(时间·来源·停留)+ 事件序列 -->
|
||||
<div
|
||||
v-for="(session, si) in day.sessions"
|
||||
:key="session.session_id ?? `orphan-${si}`"
|
||||
class="tl-session"
|
||||
>
|
||||
<div class="tl-session-header">
|
||||
<span class="tl-session-time">
|
||||
{{ session.started_at ? formatTime(session.started_at) : '未知会话' }}
|
||||
</span>
|
||||
<span class="tl-session-meta">{{ timelineSessionSource(session) }}</span>
|
||||
<span class="tl-session-meta">
|
||||
停留 {{ formatStay(sessionStayMs(session)) }}
|
||||
</span>
|
||||
</div>
|
||||
<EventTimeline :events="session.events" />
|
||||
</div>
|
||||
</div>
|
||||
<EventTimeline :events="session.events" />
|
||||
</div>
|
||||
</template>
|
||||
<!-- 失败:请求已结束但无数据对象(首屏加载中不显示,避免闪现) -->
|
||||
<ElEmpty
|
||||
v-else-if="timeline === null && !timelineLoading"
|
||||
description="时间线加载失败"
|
||||
:image-size="80"
|
||||
/>
|
||||
<!-- 成功但窗口内无事件 -->
|
||||
<ElEmpty
|
||||
v-else-if="!timelineLoading"
|
||||
description="该时间范围内暂无访问轨迹"
|
||||
:image-size="80"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 失败:请求已结束但无数据对象(首屏加载中不显示,避免闪现) -->
|
||||
<ElEmpty
|
||||
v-else-if="timeline === null && !timelineLoading"
|
||||
description="时间线加载失败"
|
||||
:image-size="80"
|
||||
/>
|
||||
<!-- 成功但窗口内无事件 -->
|
||||
<ElEmpty
|
||||
v-else-if="!timelineLoading"
|
||||
description="该时间范围内暂无访问轨迹"
|
||||
:image-size="80"
|
||||
/>
|
||||
</div>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- Tab 2:会话列表 -->
|
||||
<ElTabPane :label="`会话(${detail?.sessions.length ?? 0})`" name="sessions">
|
||||
<div class="flex-c gap-2 mb-3">
|
||||
<h3 class="m-0 text-sm font-semibold">会话列表</h3>
|
||||
<span class="text-xs text-g-500">点击行查看会话事件序列</span>
|
||||
</div>
|
||||
<ElTable
|
||||
:data="detail?.sessions ?? []"
|
||||
v-loading="profileLoading"
|
||||
empty-text="暂无会话数据"
|
||||
row-class-name="session-row"
|
||||
@row-click="openSession"
|
||||
>
|
||||
<ElTableColumn label="会话 ID" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<code class="session-id-code">{{ row.session_id.slice(0, 8) }}</code>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="开始时间" width="150">
|
||||
<template #default="{ row }">{{ formatDateTime(row.started_at) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="结束时间" width="150">
|
||||
<template #default="{ row }">{{ formatDateTime(row.ended_at) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="来源" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="whitespace-nowrap">{{ sessionSourceText(row) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="落地页" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="mono-path">{{ row.landing_path || '—' }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="event_count" label="事件数" width="80" align="right" />
|
||||
<ElTableColumn label="停留" width="130">
|
||||
<template #default="{ row }">{{ formatStay(row.stay_ms) }}</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- Tab 3:原始上报批次(raw_batch,排查"上报了为何没进报表") -->
|
||||
<ElTabPane label="原始数据" name="raw">
|
||||
<div class="flex-c flex-wrap gap-2 mb-3">
|
||||
<h3 class="m-0 text-sm font-semibold">原始上报批次</h3>
|
||||
<span class="text-xs text-g-500">
|
||||
每次接收的原样存档(保留 30 天);敏感字段已脱敏为 [FILTERED],被拒批次超 4KB 截断
|
||||
</span>
|
||||
</div>
|
||||
<ElTable
|
||||
:data="rawBatches?.list ?? []"
|
||||
v-loading="rawLoading"
|
||||
empty-text="暂无原始上报数据"
|
||||
row-class-name="session-row"
|
||||
@row-click="openRawDrawer"
|
||||
>
|
||||
<ElTableColumn label="接收时间" width="170">
|
||||
<template #default="{ row }">{{ formatDateTime(row.received_at) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="批次 ID" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.batch_id" class="flex-c gap-1">
|
||||
<code class="session-id-code">{{ row.batch_id.slice(0, 8) }}</code>
|
||||
<ElButton
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click.stop="copyBatchId(row.batch_id)"
|
||||
>
|
||||
复制
|
||||
</ElButton>
|
||||
</div>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="rawStatus(row.status).type" size="small">
|
||||
{{ rawStatus(row.status).label }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="event_count" label="入库数" width="80" align="right" />
|
||||
<ElTableColumn prop="rejected_count" label="拒绝数" width="80" align="right" />
|
||||
</ElTable>
|
||||
<div class="flex-c justify-end mt-3">
|
||||
<ElPagination
|
||||
:total="rawBatches?.total ?? 0"
|
||||
:page-size="RAW_PAGE_SIZE"
|
||||
:current-page="rawPage"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="onRawPageChange"
|
||||
/>
|
||||
</div>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</div>
|
||||
|
||||
<!-- 右列:来源证据 + 访问环境 -->
|
||||
<!-- 中栏:4 统计卡(2×2)+ 来源证据 -->
|
||||
<div class="vd-side">
|
||||
<!-- 4 统计卡:图标块 + 小标签 + 大数字 -->
|
||||
<div class="vd-stats">
|
||||
<div v-for="tile in statTiles" :key="tile.label" class="vd-card vd-stat-card">
|
||||
<div class="vd-stat-icon">
|
||||
<ElIcon :size="20">
|
||||
<component :is="tile.icon" />
|
||||
</ElIcon>
|
||||
</div>
|
||||
<div class="vd-stat-body">
|
||||
<span class="vd-stat-label">{{ tile.label }}</span>
|
||||
<span class="vd-stat-value">{{ tile.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 来源证据:三口径渠道徽章 + 证据详情子面板 -->
|
||||
<div class="vd-card vd-evidence-card">
|
||||
<div class="vd-card-head">
|
||||
@@ -201,7 +299,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右栏:访问环境 -->
|
||||
<div class="vd-side">
|
||||
<!-- 访问环境:档案字段行式展示 -->
|
||||
<div class="vd-card vd-env-card">
|
||||
<div class="vd-card-head">
|
||||
@@ -227,106 +328,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 会话与原始数据(设计图外保留的既有能力,界面按新色板统一) -->
|
||||
<div class="vd-card vd-bottom-card">
|
||||
<ElTabs v-model="activeTab" class="detail-tabs">
|
||||
<!-- Tab 1:会话列表 -->
|
||||
<ElTabPane :label="`会话(${detail?.sessions.length ?? 0})`" name="sessions">
|
||||
<div class="flex-c gap-2 mb-3">
|
||||
<h3 class="m-0 text-sm font-semibold">会话列表</h3>
|
||||
<span class="text-xs text-g-500">点击行查看会话事件序列</span>
|
||||
</div>
|
||||
<ElTable
|
||||
:data="detail?.sessions ?? []"
|
||||
v-loading="profileLoading"
|
||||
empty-text="暂无会话数据"
|
||||
row-class-name="session-row"
|
||||
@row-click="openSession"
|
||||
>
|
||||
<ElTableColumn label="会话 ID" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<code class="session-id-code">{{ row.session_id.slice(0, 8) }}</code>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="开始时间" width="150">
|
||||
<template #default="{ row }">{{ formatDateTime(row.started_at) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="结束时间" width="150">
|
||||
<template #default="{ row }">{{ formatDateTime(row.ended_at) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="来源" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="whitespace-nowrap">{{ sessionSourceText(row) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="落地页" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="mono-path">{{ row.landing_path || '—' }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="event_count" label="事件数" width="80" align="right" />
|
||||
<ElTableColumn label="停留" width="130">
|
||||
<template #default="{ row }">{{ formatStay(row.stay_ms) }}</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</ElTabPane>
|
||||
|
||||
<!-- Tab 2:原始上报批次(raw_batch,排查"上报了为何没进报表") -->
|
||||
<ElTabPane label="原始数据" name="raw">
|
||||
<div class="flex-c flex-wrap gap-2 mb-3">
|
||||
<h3 class="m-0 text-sm font-semibold">原始上报批次</h3>
|
||||
<span class="text-xs text-g-500">
|
||||
每次接收的原样存档(保留 30 天);敏感字段已脱敏为 [FILTERED],被拒批次超 4KB 截断
|
||||
</span>
|
||||
</div>
|
||||
<ElTable
|
||||
:data="rawBatches?.list ?? []"
|
||||
v-loading="rawLoading"
|
||||
empty-text="暂无原始上报数据"
|
||||
row-class-name="session-row"
|
||||
@row-click="openRawDrawer"
|
||||
>
|
||||
<ElTableColumn label="接收时间" width="170">
|
||||
<template #default="{ row }">{{ formatDateTime(row.received_at) }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="批次 ID" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.batch_id" class="flex-c gap-1">
|
||||
<code class="session-id-code">{{ row.batch_id.slice(0, 8) }}</code>
|
||||
<ElButton
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
@click.stop="copyBatchId(row.batch_id)"
|
||||
>
|
||||
复制
|
||||
</ElButton>
|
||||
</div>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="rawStatus(row.status).type" size="small">
|
||||
{{ rawStatus(row.status).label }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="event_count" label="入库数" width="80" align="right" />
|
||||
<ElTableColumn prop="rejected_count" label="拒绝数" width="80" align="right" />
|
||||
</ElTable>
|
||||
<div class="flex-c justify-end mt-3">
|
||||
<ElPagination
|
||||
:total="rawBatches?.total ?? 0"
|
||||
:page-size="RAW_PAGE_SIZE"
|
||||
:current-page="rawPage"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="onRawPageChange"
|
||||
/>
|
||||
</div>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 原始批次详情抽屉:body 是服务端脱敏后的 JSON 文本,<pre> 文本插值渲染(禁 v-html) -->
|
||||
@@ -692,7 +693,7 @@
|
||||
})
|
||||
|
||||
// ---------- 跨天时间线 ----------
|
||||
const activeTab = ref<'sessions' | 'raw'>('sessions')
|
||||
const activeTab = ref<'timeline' | 'sessions' | 'raw'>('timeline')
|
||||
const days = ref(30)
|
||||
/** 排序:desc=日期组最新在前+组内事件最新在前;asc=都最早在前 */
|
||||
const order = ref<'desc' | 'asc'>('desc')
|
||||
|
||||
Reference in New Issue
Block a user