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}`
|
||||||
|
})
|
||||||
@@ -263,6 +263,13 @@
|
|||||||
"role": "Role Manage",
|
"role": "Role Manage",
|
||||||
"userCenter": "User Center",
|
"userCenter": "User Center",
|
||||||
"menu": "Menu Manage"
|
"menu": "Menu Manage"
|
||||||
|
},
|
||||||
|
"tracker": {
|
||||||
|
"title": "Analytics",
|
||||||
|
"dashboard": "Overview",
|
||||||
|
"visitor": "Visitors",
|
||||||
|
"visitorDetail": "Visitor Detail",
|
||||||
|
"site": "Sites"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
|||||||
@@ -263,6 +263,13 @@
|
|||||||
"role": "角色管理",
|
"role": "角色管理",
|
||||||
"userCenter": "个人中心",
|
"userCenter": "个人中心",
|
||||||
"menu": "菜单管理"
|
"menu": "菜单管理"
|
||||||
|
},
|
||||||
|
"tracker": {
|
||||||
|
"title": "访客统计",
|
||||||
|
"dashboard": "数据总览",
|
||||||
|
"visitor": "访客管理",
|
||||||
|
"visitorDetail": "访客详情",
|
||||||
|
"site": "站点管理"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { dashboardRoutes } from './dashboard'
|
|||||||
import { systemRoutes } from './system'
|
import { systemRoutes } from './system'
|
||||||
import { resultRoutes } from './result'
|
import { resultRoutes } from './result'
|
||||||
import { exceptionRoutes } from './exception'
|
import { exceptionRoutes } from './exception'
|
||||||
|
import { trackerRoutes } from './tracker'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出所有模块化路由
|
* 导出所有模块化路由
|
||||||
@@ -11,5 +12,6 @@ export const routeModules: AppRouteRecord[] = [
|
|||||||
dashboardRoutes,
|
dashboardRoutes,
|
||||||
systemRoutes,
|
systemRoutes,
|
||||||
resultRoutes,
|
resultRoutes,
|
||||||
exceptionRoutes
|
exceptionRoutes,
|
||||||
|
trackerRoutes
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user