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:
浅念
2026-09-15 17:50:36 +08:00
parent f11abbff43
commit a2ec4510a7
11 changed files with 320 additions and 1 deletions
+50
View File
@@ -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 } : {} })
+43
View File
@@ -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}` })
+109
View File
@@ -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}`
})
+7
View File
@@ -263,6 +263,13 @@
"role": "Role Manage",
"userCenter": "User Center",
"menu": "Menu Manage"
},
"tracker": {
"title": "Analytics",
"dashboard": "Overview",
"visitor": "Visitors",
"visitorDetail": "Visitor Detail",
"site": "Sites"
}
},
"table": {
+7
View File
@@ -263,6 +263,13 @@
"role": "角色管理",
"userCenter": "个人中心",
"menu": "菜单管理"
},
"tracker": {
"title": "访客统计",
"dashboard": "数据总览",
"visitor": "访客管理",
"visitorDetail": "访客详情",
"site": "站点管理"
}
},
"table": {
+3 -1
View File
@@ -3,6 +3,7 @@ import { dashboardRoutes } from './dashboard'
import { systemRoutes } from './system'
import { resultRoutes } from './result'
import { exceptionRoutes } from './exception'
import { trackerRoutes } from './tracker'
/**
* 导出所有模块化路由
@@ -11,5 +12,6 @@ export const routeModules: AppRouteRecord[] = [
dashboardRoutes,
systemRoutes,
resultRoutes,
exceptionRoutes
exceptionRoutes,
trackerRoutes
]
+61
View File
@@ -0,0 +1,61 @@
import { AppRouteRecord } from '@/types/router'
/**
* 访客统计模块路由
*
* 菜单:数据总览 / 访客管理 / 站点管理;访客详情不进菜单(isHide),
* 由列表页跳转并携带 query: visitor_id,activePath 让"访客管理"保持高亮。
*/
export const trackerRoutes: AppRouteRecord = {
path: '/tracker',
name: 'Tracker',
component: '/index/index',
meta: {
title: 'menus.tracker.title',
icon: 'ri:bar-chart-box-line',
roles: ['R_SUPER', 'R_ADMIN']
},
children: [
{
path: 'dashboard',
name: 'TrackerDashboard',
component: '/tracker/dashboard/index',
meta: {
title: 'menus.tracker.dashboard',
keepAlive: true,
roles: ['R_SUPER', 'R_ADMIN']
}
},
{
path: 'visitor',
name: 'TrackerVisitor',
component: '/tracker/visitor/index',
meta: {
title: 'menus.tracker.visitor',
keepAlive: true,
roles: ['R_SUPER', 'R_ADMIN']
}
},
{
path: 'visitor/detail',
name: 'TrackerVisitorDetail',
component: '/tracker/visitor/detail',
meta: {
title: 'menus.tracker.visitorDetail',
isHide: true,
keepAlive: true,
activePath: '/tracker/visitor'
}
},
{
path: 'site',
name: 'TrackerSite',
component: '/tracker/site/index',
meta: {
title: 'menus.tracker.site',
keepAlive: true,
roles: ['R_SUPER', 'R_ADMIN']
}
}
]
}
@@ -0,0 +1,10 @@
<template>
<div class="p-5">
<h2 class="mb-3 text-lg font-semibold">数据总览</h2>
<div>Task 19 填充</div>
</div>
</template>
<script setup lang="ts">
// 占位页:Task 19 填充真实内容(今日指标 / 趋势 / 来源 / 热门页面)
</script>
+10
View File
@@ -0,0 +1,10 @@
<template>
<div class="p-5">
<h2 class="mb-3 text-lg font-semibold">站点管理</h2>
<div>Task 21 填充</div>
</div>
</template>
<script setup lang="ts">
// 占位页:Task 21 填充真实内容(站点列表 + 创建 / 编辑 / 停用)
</script>
@@ -0,0 +1,10 @@
<template>
<div class="p-5">
<h2 class="mb-3 text-lg font-semibold">访客详情</h2>
<div>Task 20 填充</div>
</div>
</template>
<script setup lang="ts">
// 占位页:Task 20 填充真实内容(档案 + 会话列表 + 跨天时间线;query 参数 visitor_id)
</script>
+10
View File
@@ -0,0 +1,10 @@
<template>
<div class="p-5">
<h2 class="mb-3 text-lg font-semibold">访客管理</h2>
<div>Task 20 填充</div>
</div>
</template>
<script setup lang="ts">
// 占位页:Task 20 填充真实内容(访客列表 + 筛选 + 分页)
</script>