feat(admin): 访客详情模板重组三栏Tabs——时间线/会话/原始数据合并左栏

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-18 16:40:24 +08:00
parent 47e8f1bd01
commit 550e180318
+98 -97
View File
@@ -1,4 +1,4 @@
<!-- 访客统计 · 访客详情(客迹视觉 V1:头部卡 + 4 统计卡 + 双栏时间线/来源证据 + 会话与原始数据) --> <!-- 访客统计 · 访客详情(客迹视觉 V1:头部卡 + 三栏=时间线/会话/原始数据 Tab + 统计卡/来源证据 + 访问环境) -->
<!-- keepAlive=true 且路由 path 固定:访客切换复用同一实例,刷新依赖 onActivated + query watch --> <!-- keepAlive=true 且路由 path 固定:访客切换复用同一实例,刷新依赖 onActivated + query watch -->
<template> <template>
<div class="visitor-detail-page p-5"> <div class="visitor-detail-page p-5">
@@ -59,31 +59,15 @@
<ElEmpty v-else-if="!profileLoading" description="访客数据加载失败或不存在" /> <ElEmpty v-else-if="!profileLoading" description="访客数据加载失败或不存在" />
</div> </div>
<!-- 4 统计卡:图标块 + 小标签 + 大数字 --> <!-- 主体三栏:左=时间线/会话/原始数据三 Tab,中=统计卡+来源证据,右=访问环境 -->
<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-main"> <div class="vd-main">
<!-- 访问时间线 --> <!-- 左栏:三 Tab(访问时间线 / 会话 / 原始数据) -->
<div class="vd-card vd-timeline-card"> <div class="vd-card vd-tabs-card">
<div class="vd-card-head"> <ElTabs v-model="activeTab" class="detail-tabs">
<div class="vd-card-title"> <!-- Tab 1:访问时间线 -->
<ElIcon :size="16"><ClockIcon /></ElIcon> <ElTabPane label="访问时间线" name="timeline">
<span>访问时间线</span> <!-- 工具行:原卡头工具迁入(标题职能由 tab label 承担) -->
</div> <div class="vd-tl-tools">
<div class="vd-card-tools">
<span class="vd-tool-label">时间范围</span> <span class="vd-tool-label">时间范围</span>
<ElRadioGroup v-model="days" size="small" @change="loadTimeline()"> <ElRadioGroup v-model="days" size="small" @change="loadTimeline()">
<ElRadioButton :value="7">近 7 天</ElRadioButton> <ElRadioButton :value="7">近 7 天</ElRadioButton>
@@ -110,7 +94,6 @@
</button> </button>
</div> </div>
</div> </div>
</div>
<ElAlert <ElAlert
v-if="timeline?.truncated" v-if="timeline?.truncated"
@@ -143,7 +126,9 @@
{{ session.started_at ? formatTime(session.started_at) : '未知会话' }} {{ session.started_at ? formatTime(session.started_at) : '未知会话' }}
</span> </span>
<span class="tl-session-meta">{{ timelineSessionSource(session) }}</span> <span class="tl-session-meta">{{ timelineSessionSource(session) }}</span>
<span class="tl-session-meta">停留 {{ formatStay(sessionStayMs(session)) }}</span> <span class="tl-session-meta">
停留 {{ formatStay(sessionStayMs(session)) }}
</span>
</div> </div>
<EventTimeline :events="session.events" /> <EventTimeline :events="session.events" />
</div> </div>
@@ -162,75 +147,9 @@
:image-size="80" :image-size="80"
/> />
</div> </div>
</div> </ElTabPane>
<!-- 右列:来源证据 + 访问环境 --> <!-- Tab 2:会话列表 -->
<div class="vd-side">
<!-- 来源证据:三口径渠道徽章 + 证据详情子面板 -->
<div class="vd-card vd-evidence-card">
<div class="vd-card-head">
<div class="vd-card-title">
<ElIcon :size="16"><LinkIcon /></ElIcon>
<span>来源证据</span>
</div>
</div>
<div class="vd-ev-list">
<div v-for="row in evidenceRows" :key="row.label" class="vd-ev-row">
<span class="vd-ev-label">{{ row.label }}</span>
<ElTooltip
v-if="badgeOf(row.evidence)"
:content="badgeTooltip(row.evidence)"
placement="top"
>
<span
class="vd-badge"
:style="badgeStyle(row.evidence)"
>{{ badgeText(row.evidence) }}</span>
</ElTooltip>
<span v-else class="vd-ev-empty">—</span>
</div>
</div>
<!-- 证据详情:浅蓝底子面板(标记来源/具体智能体/原始提问) -->
<div class="vd-ev-detail">
<div class="vd-ev-detail-title">证据详情</div>
<div v-for="row in evidenceDetailRows" :key="row.label" class="vd-ev-detail-row">
<span class="vd-ev-detail-label">{{ row.label }}</span>
<span class="vd-ev-detail-value" :class="{ 'is-empty': row.value === '未提供' }">
{{ row.value }}
</span>
</div>
</div>
</div>
<!-- 访问环境:档案字段行式展示 -->
<div class="vd-card vd-env-card">
<div class="vd-card-head">
<div class="vd-card-title">
<ElIcon :size="16"><MonitorIcon /></ElIcon>
<span>访问环境</span>
</div>
</div>
<div class="vd-env-list">
<div v-for="row in envRows" :key="row.label" class="vd-env-row">
<span class="vd-env-icon">
<ElIcon :size="14">
<component :is="row.icon" />
</ElIcon>
</span>
<span class="vd-env-label">{{ row.label }}</span>
<span class="vd-env-value" :class="{ 'is-empty': row.value === '—' }">
{{ row.value }}
</span>
</div>
</div>
</div>
</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"> <ElTabPane :label="`会话(${detail?.sessions.length ?? 0})`" name="sessions">
<div class="flex-c gap-2 mb-3"> <div class="flex-c gap-2 mb-3">
<h3 class="m-0 text-sm font-semibold">会话列表</h3> <h3 class="m-0 text-sm font-semibold">会话列表</h3>
@@ -271,7 +190,7 @@
</ElTable> </ElTable>
</ElTabPane> </ElTabPane>
<!-- Tab 2:原始上报批次(raw_batch,排查"上报了为何没进报表") --> <!-- Tab 3:原始上报批次(raw_batch,排查"上报了为何没进报表") -->
<ElTabPane label="原始数据" name="raw"> <ElTabPane label="原始数据" name="raw">
<div class="flex-c flex-wrap gap-2 mb-3"> <div class="flex-c flex-wrap gap-2 mb-3">
<h3 class="m-0 text-sm font-semibold">原始上报批次</h3> <h3 class="m-0 text-sm font-semibold">原始上报批次</h3>
@@ -327,6 +246,88 @@
</ElTabPane> </ElTabPane>
</ElTabs> </ElTabs>
</div> </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">
<div class="vd-card-title">
<ElIcon :size="16"><LinkIcon /></ElIcon>
<span>来源证据</span>
</div>
</div>
<div class="vd-ev-list">
<div v-for="row in evidenceRows" :key="row.label" class="vd-ev-row">
<span class="vd-ev-label">{{ row.label }}</span>
<ElTooltip
v-if="badgeOf(row.evidence)"
:content="badgeTooltip(row.evidence)"
placement="top"
>
<span
class="vd-badge"
:style="badgeStyle(row.evidence)"
>{{ badgeText(row.evidence) }}</span>
</ElTooltip>
<span v-else class="vd-ev-empty">—</span>
</div>
</div>
<!-- 证据详情:浅蓝底子面板(标记来源/具体智能体/原始提问) -->
<div class="vd-ev-detail">
<div class="vd-ev-detail-title">证据详情</div>
<div v-for="row in evidenceDetailRows" :key="row.label" class="vd-ev-detail-row">
<span class="vd-ev-detail-label">{{ row.label }}</span>
<span class="vd-ev-detail-value" :class="{ 'is-empty': row.value === '未提供' }">
{{ row.value }}
</span>
</div>
</div>
</div>
</div>
<!-- 右栏:访问环境 -->
<div class="vd-side">
<!-- 访问环境:档案字段行式展示 -->
<div class="vd-card vd-env-card">
<div class="vd-card-head">
<div class="vd-card-title">
<ElIcon :size="16"><MonitorIcon /></ElIcon>
<span>访问环境</span>
</div>
</div>
<div class="vd-env-list">
<div v-for="row in envRows" :key="row.label" class="vd-env-row">
<span class="vd-env-icon">
<ElIcon :size="14">
<component :is="row.icon" />
</ElIcon>
</span>
<span class="vd-env-label">{{ row.label }}</span>
<span class="vd-env-value" :class="{ 'is-empty': row.value === '—' }">
{{ row.value }}
</span>
</div>
</div>
</div>
</div>
</div>
</template> </template>
<!-- 原始批次详情抽屉:body 是服务端脱敏后的 JSON 文本,<pre> 文本插值渲染(禁 v-html) --> <!-- 原始批次详情抽屉: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) const days = ref(30)
/** 排序:desc=日期组最新在前+组内事件最新在前;asc=都最早在前 */ /** 排序:desc=日期组最新在前+组内事件最新在前;asc=都最早在前 */
const order = ref<'desc' | 'asc'>('desc') const order = ref<'desc' | 'asc'>('desc')