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",
|
||||
"userCenter": "User Center",
|
||||
"menu": "Menu Manage"
|
||||
},
|
||||
"tracker": {
|
||||
"title": "Analytics",
|
||||
"dashboard": "Overview",
|
||||
"visitor": "Visitors",
|
||||
"visitorDetail": "Visitor Detail",
|
||||
"site": "Sites"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -263,6 +263,13 @@
|
||||
"role": "角色管理",
|
||||
"userCenter": "个人中心",
|
||||
"menu": "菜单管理"
|
||||
},
|
||||
"tracker": {
|
||||
"title": "访客统计",
|
||||
"dashboard": "数据总览",
|
||||
"visitor": "访客管理",
|
||||
"visitorDetail": "访客详情",
|
||||
"site": "站点管理"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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