feat(admin): tracker api 模块与路由
- api/tracker/{visitors,dashboard,sites}.ts:类型逐字段对齐 server/tracker/api 序列化输出
(stay 一律 stay_ms,不引用已移除的 duration_ms;last_ip/时间线孤立会话组的
started_at 按后端可为 null;TrendPoint 含 new_visitors)
- 路由模块 tracker:数据总览/访客管理/站点管理三项菜单,访客详情 isHide 不进菜单
(query: visitor_id,activePath 回指访客管理),zh/en 语言包补 menus.tracker
- 4 个占位视图(Task 19-21 填充真实内容),vue-tsc 构建通过
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 总览 API(server/tracker/api/dashboard.py)
|
||||
*
|
||||
* 今日四指标与 refresh_daily 共享口径(daily_counts:排除爬虫,上海自然日);
|
||||
* site 参数传 site_id,不传 = 聚合全部站点(站点不存在返回 404 site_not_found)。
|
||||
*/
|
||||
|
||||
/** 今日四指标 + 当前在线(5 分钟内有行为的非爬虫访客数) */
|
||||
export interface TodayCounts {
|
||||
pv: number
|
||||
uv: number
|
||||
sessions: number
|
||||
new_visitors: number
|
||||
active_now: number
|
||||
}
|
||||
|
||||
/** 近 30 天趋势点(缺日补零,今天一位用实时值覆盖;每日含全部四指标) */
|
||||
export interface TrendPoint {
|
||||
date: string
|
||||
pv: number
|
||||
uv: number
|
||||
sessions: number
|
||||
new_visitors: number
|
||||
}
|
||||
|
||||
/** 今日来源 TOP8(referrer_domain 为空归并为"直接访问") */
|
||||
export interface SourceItem {
|
||||
source: string
|
||||
sessions: number
|
||||
}
|
||||
|
||||
/** 今日热门页面 TOP8(page_view 按 path 聚合) */
|
||||
export interface PageItem {
|
||||
path: string
|
||||
pv: number
|
||||
}
|
||||
|
||||
/** 总览响应 */
|
||||
export interface OverviewResp {
|
||||
today: TodayCounts
|
||||
trend: TrendPoint[]
|
||||
sources: SourceItem[]
|
||||
top_pages: PageItem[]
|
||||
}
|
||||
|
||||
/** 总览数据(site 可选,传 site_id) */
|
||||
export const getOverview = (site?: string) =>
|
||||
request.get<OverviewResp>({ url: '/admin/dashboard/overview', params: site ? { site } : {} })
|
||||
@@ -0,0 +1,43 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 站点 API(server/tracker/api/sites.py)
|
||||
*
|
||||
* 域名后端归一(去协议/端口/路径、转小写、去空去重);DELETE 为软删(停用),仅 admin。
|
||||
* 模板 http 封装暴露 get/post/put/del/request——无 patch 方法,PATCH 走通用 request 指定 method。
|
||||
*/
|
||||
|
||||
/** 站点行(sites.py `_serialize`) */
|
||||
export interface SiteRow {
|
||||
id: number
|
||||
name: string
|
||||
site_id: string
|
||||
domains: string[]
|
||||
allow_no_origin: boolean
|
||||
is_active: boolean
|
||||
created_at: string
|
||||
}
|
||||
|
||||
/** 创建站点参数(name 必填,domains 归一后为空返回 400 invalid_domains) */
|
||||
export interface CreateSiteData {
|
||||
name: string
|
||||
domains: string[]
|
||||
allow_no_origin?: boolean
|
||||
}
|
||||
|
||||
/** 更新站点参数(Partial,仅传要改的字段) */
|
||||
export type UpdateSiteData = Partial<Pick<SiteRow, 'name' | 'domains' | 'allow_no_origin' | 'is_active'>>
|
||||
|
||||
/** 站点列表(按 created_at 倒序) */
|
||||
export const getSites = () => request.get<SiteRow[]>({ url: '/admin/sites' })
|
||||
|
||||
/** 创建站点 */
|
||||
export const createSite = (data: CreateSiteData) =>
|
||||
request.post<SiteRow>({ url: '/admin/sites', data })
|
||||
|
||||
/** 更新站点(PATCH:模板封装未暴露 patch,走通用 request) */
|
||||
export const updateSite = (id: number, data: UpdateSiteData) =>
|
||||
request.request<SiteRow>({ url: `/admin/sites/${id}`, method: 'PATCH', data })
|
||||
|
||||
/** 停用站点(DELETE 软删:is_active=False,上报侧 collect 即 403) */
|
||||
export const deactivateSite = (id: number) => request.del<SiteRow>({ url: `/admin/sites/${id}` })
|
||||
@@ -0,0 +1,109 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 访客 API(server/tracker/api/visitors.py)
|
||||
*
|
||||
* 字段对照说明:
|
||||
* - stay 相关只有 stay_ms(有效停留口径)——duration_ms 已从 API 移除,前端不得引用
|
||||
* - 空白字符串字段(country/os/browser 等)后端以 '' 序列化,不为 null
|
||||
*/
|
||||
|
||||
/** 访客行(visitors.py `_row`:列表/详情共用行字段) */
|
||||
export interface VisitorRow {
|
||||
visitor_id: string
|
||||
country: string
|
||||
province: string
|
||||
city: string
|
||||
device_type: string
|
||||
os: string
|
||||
browser: string
|
||||
language: string
|
||||
first_seen_at: string
|
||||
last_seen_at: string
|
||||
visit_count: number
|
||||
active_days: number
|
||||
session_count: number
|
||||
/** 会话 stay_ms 累计(无会话时后端补 0) */
|
||||
total_stay_ms: number
|
||||
first_source: string
|
||||
last_source: string
|
||||
online: boolean
|
||||
}
|
||||
|
||||
/** 会话摘要(visitors.py `_session_summary`) */
|
||||
export interface SessionSummary {
|
||||
session_id: string
|
||||
started_at: string
|
||||
ended_at: string
|
||||
referrer_domain: string
|
||||
utm: Record<string, string>
|
||||
landing_path: string
|
||||
event_count: number
|
||||
stay_ms: number
|
||||
}
|
||||
|
||||
/** 访客详情(`_row` + 详情扩展字段 + 全量会话摘要,M1 不分页) */
|
||||
export interface VisitorDetail extends VisitorRow {
|
||||
screen: string
|
||||
timezone_name: string
|
||||
/** GenericIPAddressField(null=True),未采集到时为 null */
|
||||
last_ip: string | null
|
||||
first_utm: Record<string, string>
|
||||
first_landing_path: string
|
||||
first_referrer_domain: string
|
||||
sessions: SessionSummary[]
|
||||
}
|
||||
|
||||
/** 时间线单事件(`_merge_page_stay` 白名单字段,不透出 properties 原文) */
|
||||
export interface TimelineEvent {
|
||||
time: string
|
||||
event_name: string
|
||||
path: string
|
||||
title: string
|
||||
/** page_view 合并后为累计毫秒(初始 0);独立成条的 page_stay 为毫秒;其余事件为 null */
|
||||
active_ms: number | null
|
||||
}
|
||||
|
||||
/** 时间线单会话分组(session 为 NULL 的孤立事件并成一组:session_id/started_at 均为 null) */
|
||||
export interface TimelineSession {
|
||||
session_id: string | null
|
||||
started_at: string | null
|
||||
events: TimelineEvent[]
|
||||
}
|
||||
|
||||
/** 时间线单日(日期组倒序,日内会话/事件正序) */
|
||||
export interface TimelineDay {
|
||||
date: string
|
||||
sessions: TimelineSession[]
|
||||
}
|
||||
|
||||
/** 时间线响应(truncated 区分"没动静"和"被截断") */
|
||||
export interface TimelineResp {
|
||||
days: TimelineDay[]
|
||||
truncated: boolean
|
||||
}
|
||||
|
||||
/** 访客列表响应 */
|
||||
export interface VisitorListResp {
|
||||
list: VisitorRow[]
|
||||
total: number
|
||||
page: number
|
||||
}
|
||||
|
||||
/** 访客列表(筛选参数:search/country/device_type/online/is_new/show_bot/date_from/date_to/page) */
|
||||
export const getVisitors = (params: Record<string, unknown>) =>
|
||||
request.get<VisitorListResp>({ url: '/admin/visitors', params })
|
||||
|
||||
/** 访客详情(404 时后端返回 visitor_not_found) */
|
||||
export const getVisitor = (visitorId: string) =>
|
||||
request.get<VisitorDetail>({ url: `/admin/visitors/${visitorId}` })
|
||||
|
||||
/** 跨天时间线(days 后端夹紧 1-90,默认 30) */
|
||||
export const getTimeline = (visitorId: string, days = 30) =>
|
||||
request.get<TimelineResp>({ url: `/admin/visitors/${visitorId}/timeline`, params: { days } })
|
||||
|
||||
/** 单会话详情(会话摘要 + 事件序列,同一套 page_view/page_stay 合并规则) */
|
||||
export const getSession = (visitorId: string, sessionId: string) =>
|
||||
request.get<SessionSummary & { events: TimelineEvent[] }>({
|
||||
url: `/admin/visitors/${visitorId}/sessions/${sessionId}`
|
||||
})
|
||||
Reference in New Issue
Block a user