feat: 来源组合显示——徽章/识别卡拼装落地页·国家·语言(搜索意图线索)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,8 @@ export interface VisitorRow {
|
||||
total_stay_ms: number
|
||||
first_source: string
|
||||
last_source: string
|
||||
/** 首次访问落地页(空串=未采集,如直达首页;列表 tooltip / 详情识别卡用) */
|
||||
first_landing_path: string
|
||||
/** 首触来源归因(first_touch 分类;无分类行/未重算的历史访客为 '',前端回落 first_source 展示) */
|
||||
channel: string
|
||||
platform: string
|
||||
@@ -87,7 +89,6 @@ export interface VisitorDetail extends VisitorRow {
|
||||
/** GenericIPAddressField(null=True),未采集到时为 null */
|
||||
last_ip: string | null
|
||||
first_utm: Record<string, string>
|
||||
first_landing_path: string
|
||||
first_referrer_domain: string
|
||||
/** 来源归因块(first_touch=null 时前端回落 first_source 展示) */
|
||||
attribution: AttributionBlock
|
||||
|
||||
@@ -94,6 +94,22 @@
|
||||
<span class="shrink-0 text-xs text-g-600">搜索词</span>
|
||||
<span class="search-term">{{ firstTouch.search_term }}</span>
|
||||
</div>
|
||||
<!-- 入口页:落地页是搜索意图的直接线索(如搜"挖掘机"落地产品页) -->
|
||||
<div class="search-term-row">
|
||||
<span class="shrink-0 text-xs text-g-600">入口页</span>
|
||||
<template v-if="detail.first_landing_path">
|
||||
<ElTooltip :content="detail.first_landing_path" placement="top">
|
||||
<span class="mono-path landing-path">{{ detail.first_landing_path }}</span>
|
||||
</ElTooltip>
|
||||
<span class="text-xs text-g-500 shrink-0">落地页是搜索意图的直接线索</span>
|
||||
</template>
|
||||
<span v-else>—</span>
|
||||
</div>
|
||||
<!-- 上下文:国家·语言·设备一句话组合(空段省略;只做来源故事线,不重复描述列表) -->
|
||||
<div v-if="sourceContextText" class="search-term-row">
|
||||
<span class="shrink-0 text-xs text-g-600">上下文</span>
|
||||
<span class="text-xs">{{ sourceContextText }}</span>
|
||||
</div>
|
||||
<!-- 冲突时并列展示两路证据(文本插值渲染,禁 v-html) -->
|
||||
<div
|
||||
v-if="firstTouch.resolution === 'conflict' && conflictEvidences.length"
|
||||
@@ -512,6 +528,13 @@
|
||||
() => CHANNEL_LABELS[firstTouch.value?.channel ?? ''] ?? CHANNEL_LABELS.unknown
|
||||
)
|
||||
|
||||
/** 识别卡上下文一句话:国家 · 语言 · 设备类型(空段省略,全空不渲染该行) */
|
||||
const sourceContextText = computed(() => {
|
||||
const d = detail.value
|
||||
if (!d) return ''
|
||||
return [d.country, d.language, d.device_type].filter(Boolean).join(' · ')
|
||||
})
|
||||
|
||||
/** conflict 时并列的两路证据(referrer 观察 vs UTM 标签),文本插值渲染 */
|
||||
const conflictEvidences = computed(() => {
|
||||
const d = firstTouch.value?.detail
|
||||
|
||||
@@ -252,14 +252,28 @@
|
||||
|
||||
const renderSource = (val: string) => h('span', { class: 'whitespace-nowrap' }, val || '—')
|
||||
|
||||
/** 首触来源:有归因走渠道徽章(icon+platform/variant+搜索词+诚实后缀),first_source 原文进 tooltip 兜底 */
|
||||
/** 首触来源 tooltip:意图与上下文组合多行(渠道按 channelBadge 同款数据拼装,搜索词有才显示) */
|
||||
const firstSourceTooltipLines = (row: VisitorRow): string[] => {
|
||||
const meta = CHANNEL_LABELS[row.channel] ?? CHANNEL_LABELS.unknown
|
||||
const platform = [row.platform, row.variant].filter(Boolean).join('·')
|
||||
const lines = [`渠道:${meta.icon} ${[meta.label, platform].filter(Boolean).join(' · ')}`]
|
||||
if (row.search_term) lines.push(`搜索词:${row.search_term}`)
|
||||
lines.push(`落地页:${row.first_landing_path || '—'}`)
|
||||
lines.push(`国家·语言:${[row.country, row.language].filter(Boolean).join(' · ') || '—'}`)
|
||||
return lines
|
||||
}
|
||||
|
||||
/** 首触来源:有归因走渠道徽章(icon+platform/variant+搜索词+诚实后缀),tooltip 展示来源组合信息;
|
||||
* 无归因(历史数据)回落 first_source 原文 */
|
||||
const renderFirstSource = (row: VisitorRow) => {
|
||||
if (!row.channel) return renderSource(row.first_source)
|
||||
return h(
|
||||
ElTooltip,
|
||||
{ content: row.first_source || '—', placement: 'top' },
|
||||
{ placement: 'top' },
|
||||
{
|
||||
default: () => h('span', { class: 'whitespace-nowrap' }, channelBadge(row))
|
||||
default: () => h('span', { class: 'whitespace-nowrap' }, channelBadge(row)),
|
||||
content: () =>
|
||||
firstSourceTooltipLines(row).map((line, i) => h('div', { key: i }, line))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ def _row(visitor, last_session, online_cutoff, ft=None):
|
||||
"first_source": _source(visitor.first_referrer_domain, visitor.first_utm),
|
||||
"last_source": _source(last_session.referrer_domain, last_session.utm)
|
||||
if last_session else "直接访问", # 无会话访客(仅采集异常场景)
|
||||
"first_landing_path": visitor.first_landing_path, # 列表首触来源 tooltip 用(详情本就输出)
|
||||
"channel": ft.channel if ft else "",
|
||||
"platform": ft.platform if ft else "",
|
||||
"variant": ft.variant if ft else "",
|
||||
|
||||
@@ -57,8 +57,8 @@ def test_list_fields_and_default_bot_excluded(client, member_user):
|
||||
site = Site.objects.create(name="s", domains=["a.com"])
|
||||
v1 = _visitor(site, "v1", country="中国", province="上海", city="上海",
|
||||
device_type="desktop", os="Windows", browser="Chrome", language="zh-CN",
|
||||
first_referrer_domain="google.com", visit_count=2, active_days=1,
|
||||
last_seen=timezone.now())
|
||||
first_referrer_domain="google.com", first_landing_path="/from-google",
|
||||
visit_count=2, active_days=1, last_seen=timezone.now())
|
||||
v2 = _visitor(site, "v2", first_utm={"utm_source": "baidu"},
|
||||
last_seen=timezone.now() - timedelta(hours=1))
|
||||
bot = _visitor(site, "bot-v")
|
||||
@@ -77,16 +77,18 @@ def test_list_fields_and_default_bot_excluded(client, member_user):
|
||||
assert set(row) == {"visitor_id", "site_id", "site_name", "country", "province", "city",
|
||||
"device_type", "os", "browser", "language", "first_seen_at",
|
||||
"last_seen_at", "visit_count", "active_days", "session_count",
|
||||
"total_stay_ms", "first_source", "last_source", "online",
|
||||
"channel", "platform", "variant", "search_term", "resolution"}
|
||||
"total_stay_ms", "first_source", "last_source", "first_landing_path",
|
||||
"online", "channel", "platform", "variant", "search_term", "resolution"}
|
||||
assert row["country"] == "中国" and row["os"] == "Windows" and row["browser"] == "Chrome"
|
||||
assert row["session_count"] == 2
|
||||
assert row["total_stay_ms"] == 150000 # 120000+30000,有效停留口径
|
||||
assert row["first_source"] == "google.com"
|
||||
assert row["last_source"] == "bing.com" # 最近会话 s2 的 referrer
|
||||
assert row["first_landing_path"] == "/from-google"
|
||||
assert row["online"] is True
|
||||
row2 = d["list"][1]
|
||||
assert row2["first_source"] == "baidu" # first_referrer 空 → first_utm 兜底
|
||||
assert row2["first_landing_path"] == "" # 未采集 → 空串(前端显示 —)
|
||||
assert row2["last_source"] == "直接访问" # 会话无来源 → 兜底
|
||||
assert row2["total_stay_ms"] == 5000 and row2["online"] is False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user