From c9967b6d707e30e2ac62939843e21fc617516541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Sat, 19 Sep 2026 00:12:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E6=97=B6=E9=97=B4=E7=BA=BF?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E4=BA=8B=E4=BB=B6=E5=BE=BD=E7=AB=A0+?= =?UTF-8?q?=E8=AF=A2=E7=9B=98=E6=BC=8F=E6=96=97=E5=8D=A1+=E4=BB=98?= =?UTF-8?q?=E8=B4=B9=E7=A4=BE=E4=BA=A4=E6=B8=A0=E9=81=93=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=EF=BC=88=E5=90=ABserver=E6=97=B6=E9=97=B4=E7=BA=BF=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E5=AD=97=E6=AE=B5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- admin/src/api/tracker/dashboard.ts | 18 +++ admin/src/api/tracker/visitors.ts | 23 +++- admin/src/utils/tracker/index.ts | 1 + admin/src/views/tracker/dashboard/index.vue | 126 +++++++++++++++++- .../visitor/components/EventTimeline.vue | 75 ++++++++++- server/tracker/api/visitors.py | 35 ++++- server/tracker/tests/test_visitors.py | 61 ++++++++- 7 files changed, 324 insertions(+), 15 deletions(-) diff --git a/admin/src/api/tracker/dashboard.ts b/admin/src/api/tracker/dashboard.ts index fde8246..285d5d9 100644 --- a/admin/src/api/tracker/dashboard.ts +++ b/admin/src/api/tracker/dashboard.ts @@ -57,3 +57,21 @@ export interface OverviewResp { /** 总览数据(site 传 site_id 可选;range 默认 7d) */ export const getOverview = (params: { site?: string; range?: OverviewRange } = {}) => request.get({ url: '/admin/dashboard/overview', params }) + +/** 漏斗级行(label 为服务端基词;末级「邮件已发」前端追加「(未验证)」限定语——T3 审查备忘) */ +export interface FunnelLevel { + /** 级别事件名:page_view/contact_click/form_start/form_submit/form_submit_result */ + key: string + label: string + /** 窗口内发生过该事件的 distinct 会话数(五级逐级独立口径,非嵌套) */ + count: number +} + +/** 询盘漏斗响应(五级,无百分比——前端只做计数条渲染) */ +export interface FunnelSummaryResp { + levels: FunnelLevel[] +} + +/** 询盘漏斗(days=1 当日 / 7 近 7 天,上海自然日窗口;排除爬虫;site 可选) */ +export const getFunnelSummary = (days: number, site?: string) => + request.get({ url: '/admin/funnel-summary', params: { days, site } }) diff --git a/admin/src/api/tracker/visitors.ts b/admin/src/api/tracker/visitors.ts index 6be6154..402039f 100644 --- a/admin/src/api/tracker/visitors.ts +++ b/admin/src/api/tracker/visitors.ts @@ -59,7 +59,7 @@ export interface SessionSummary { /** 单条来源识别(visitors.py `_classification`:classify() 结果 + 规则版本/计算时间) */ export interface AttributionInfo { - /** CHANNELS 枚举:natural_search/paid_search/ai_referral/social/in_app/referral/direct/unknown */ + /** CHANNELS 枚举:natural_search/paid_search/paid_social/ai_referral/social/in_app/referral/direct/unknown */ channel: string /** 平台显示名(Google/ChatGPT/微信…;referral 渠道为来源域名) */ platform: string @@ -116,7 +116,8 @@ export interface VisitorDetail extends VisitorRow { sessions: SessionSummary[] } -/** 时间线单事件(`_merge_page_stay` 白名单字段,不透出 properties 原文) */ +/** 时间线单事件(`_merge_page_stay` 白名单字段,不透出 properties 原文; + * 业务事件额外携带 `_safe_fields` 安全字段——均为可选,旧数据/无该字段时不出现) */ export interface TimelineEvent { time: string event_name: string @@ -124,6 +125,22 @@ export interface TimelineEvent { title: string /** page_view 合并后为累计毫秒(初始 0);独立成条的 page_stay 为毫秒;其余事件为 null */ active_ms: number | null + /** contact_click 咨询渠道:whatsapp / phone / email */ + channel?: string + /** form_start / form_submit 表单标识 */ + form_id?: string + /** form_submit 提交批次号前 8 位(服务端截断,非全量 ID) */ + submission_id?: string + /** form_error 错误码(如 validation_failed) */ + error_code?: string + /** form_error 未通过的表单字段数 */ + field_count?: number + /** form_submit_result 投递结果:mail_sent / mail_failed */ + result_code?: string + /** product_view 产品标识 */ + product_id?: string + /** product_view 是否 URL 推导(非埋点显式上报) */ + inferred?: boolean } /** 时间线单会话分组(session 为 NULL 的孤立事件并成一组:session_id/started_at 均为 null) */ @@ -191,7 +208,7 @@ export interface VisitorListParams { show_bot?: '1' /** visitor_id / last_ip 前缀模糊匹配 */ search?: string - /** 渠道筛选(CHANNELS 枚举:natural_search/paid_search/ai_referral/social/in_app/referral/direct/unknown, + /** 渠道筛选(CHANNELS 枚举:natural_search/paid_search/paid_social/ai_referral/social/in_app/referral/direct/unknown, * 按访客 first_touch 分类过滤;无分类行的历史访客不属于任何渠道桶) */ channel?: string /** YYYY-MM-DD(非法值后端宽松忽略) */ diff --git a/admin/src/utils/tracker/index.ts b/admin/src/utils/tracker/index.ts index 398303f..ba84393 100644 --- a/admin/src/utils/tracker/index.ts +++ b/admin/src/utils/tracker/index.ts @@ -43,6 +43,7 @@ export const countryFlag = (country?: string | null): string => export const CHANNEL_LABELS: Record = { natural_search: { label: '自然搜索', icon: '🔍', color: '#409EFF' }, paid_search: { label: '付费广告', icon: '💰', color: '#F56C6C' }, + paid_social: { label: '付费社交', icon: '📣', color: '#F759AB' }, // 归因 v2:fbclid / fb 家族+付费 medium ai_referral: { label: 'AI 引用', icon: '🤖', color: '#9B59B6' }, social: { label: '社交媒体', icon: '💬', color: '#E6A23C' }, in_app: { label: '应用内', icon: '📱', color: '#67C23A' }, diff --git a/admin/src/views/tracker/dashboard/index.vue b/admin/src/views/tracker/dashboard/index.vue index de45cae..e36fc09 100644 --- a/admin/src/views/tracker/dashboard/index.vue +++ b/admin/src/views/tracker/dashboard/index.vue @@ -86,6 +86,30 @@ + +
+
+
+ + 询盘漏斗 +
+ + + + +
+
+
+ {{ lv.label }} +
+
+
+ {{ fmtInt(lv.count) }} +
+
暂无数据
+
+
+
@@ -139,6 +163,7 @@ import { markRaw } from 'vue' import { Clock as ClockIcon, + Filter as FilterIcon, Histogram as HistogramIcon, Timer as TimerIcon, TrendCharts as TrendChartsIcon, @@ -150,7 +175,9 @@ import { useChart } from '@/hooks/core/useChart' import { getSites, type SiteRow } from '@/api/tracker/sites' import { + getFunnelSummary, getOverview, + type FunnelSummaryResp, type OverviewRange, type OverviewResp } from '@/api/tracker/dashboard' @@ -209,9 +236,10 @@ } } - // 站点切换:总览与最近访问都按站点过滤,需同时重拉 + // 站点切换:总览/漏斗与最近访问都按站点过滤,需同时重拉 const onSiteChange = () => { loadOverview() + loadFunnel() loadRecentVisitors() } @@ -383,6 +411,41 @@ const channelLabel = (channel: string): string => CHANNEL_LABELS[channel]?.label ?? channel + // ---------- 询盘漏斗(五级独立 distinct 会话计数,无百分比;沿周期卡切换模式) ---------- + const funnelDays = ref(7) // 1=当日 / 7=近 7 天(上海自然日窗口,与总览同口径) + const funnel = ref(null) + const funnelLoading = ref(false) + let funnelSeq = 0 + + const loadFunnel = async () => { + const seq = ++funnelSeq + funnelLoading.value = true + try { + const resp = await getFunnelSummary( + funnelDays.value, + selectedSiteId.value === ALL_SITES ? undefined : selectedSiteId.value + ) + if (seq === funnelSeq) funnel.value = resp + } catch { + // 失败已由请求层统一 ElMessage 提示;funnel 置空走空态 + if (seq === funnelSeq) funnel.value = null + } finally { + if (seq === funnelSeq) funnelLoading.value = false + } + } + + /** 漏斗级行:末级追加「(未验证)」限定语——服务端基词只到「邮件已发」, + * CF7 投递结果无回执验证,前端如实标注(T3 审查备忘) */ + const funnelLevels = computed(() => + (funnel.value?.levels ?? []).map((l) => ({ + ...l, + label: l.key === 'form_submit_result' ? `${l.label}(未验证)` : l.label + })) + ) + + /** 条长基准:窗口内最大级计数(全零取 1 防除零——条全空,计数照常显示 0) */ + const funnelMax = computed(() => Math.max(1, ...funnelLevels.value.map((l) => l.count))) + // 数据到位后重绘两张图(nextTick 等容器就绪;卸载销毁由 useChart 的 onUnmounted 负责) watch(overview, () => { nextTick(() => { @@ -432,6 +495,7 @@ booted = true loadSites() loadOverview() + loadFunnel() loadRecentVisitors() } @@ -442,6 +506,7 @@ return } loadOverview() + loadFunnel() loadRecentVisitors() }) @@ -648,6 +713,65 @@ padding: 12px 16px; } + // 询盘漏斗卡:五级横向计数条(条长按各级计数相对最大级;无百分比文案) + .db-funnel-card { + margin-bottom: 16px; + } + + .db-funnel-body { + display: flex; + flex-direction: column; + padding: 18px 20px; + gap: 14px; + } + + .db-funnel-row { + display: flex; + align-items: center; + gap: 12px; + } + + .db-funnel-label { + width: 128px; + flex: none; + font-size: 13px; + color: $vd-text-2; + text-align: right; + } + + .db-funnel-track { + flex: 1; + min-width: 0; + height: 14px; + overflow: hidden; + background: var(--el-fill-color-light); + border-radius: 7px; + } + + .db-funnel-bar { + height: 100%; + background: $vd-primary; + border-radius: 7px; + transition: width 0.3s ease; + } + + .db-funnel-count { + width: 48px; + flex: none; + font-size: 13px; + font-weight: 700; + color: $vd-text; + text-align: right; + font-variant-numeric: tabular-nums; + } + + .db-funnel-empty { + padding: 20px 0; + font-size: 13px; + color: $vd-text-3; + text-align: center; + } + // 最近访问表卡 .db-table-card { padding-bottom: 12px; diff --git a/admin/src/views/tracker/visitor/components/EventTimeline.vue b/admin/src/views/tracker/visitor/components/EventTimeline.vue index d0c4ccb..7aa2172 100644 --- a/admin/src/views/tracker/visitor/components/EventTimeline.vue +++ b/admin/src/views/tracker/visitor/components/EventTimeline.vue @@ -1,5 +1,6 @@ - + +