From a2ec4510a73bac71c8c46c12cbe31d2d485bb400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 15 Sep 2026 17:50:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20tracker=20api=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E4=B8=8E=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- admin/src/api/tracker/dashboard.ts | 50 +++++++++ admin/src/api/tracker/sites.ts | 43 ++++++++ admin/src/api/tracker/visitors.ts | 109 ++++++++++++++++++++ admin/src/locales/langs/en.json | 7 ++ admin/src/locales/langs/zh.json | 7 ++ admin/src/router/modules/index.ts | 4 +- admin/src/router/modules/tracker.ts | 61 +++++++++++ admin/src/views/tracker/dashboard/index.vue | 10 ++ admin/src/views/tracker/site/index.vue | 10 ++ admin/src/views/tracker/visitor/detail.vue | 10 ++ admin/src/views/tracker/visitor/index.vue | 10 ++ 11 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 admin/src/api/tracker/dashboard.ts create mode 100644 admin/src/api/tracker/sites.ts create mode 100644 admin/src/api/tracker/visitors.ts create mode 100644 admin/src/router/modules/tracker.ts create mode 100644 admin/src/views/tracker/dashboard/index.vue create mode 100644 admin/src/views/tracker/site/index.vue create mode 100644 admin/src/views/tracker/visitor/detail.vue create mode 100644 admin/src/views/tracker/visitor/index.vue diff --git a/admin/src/api/tracker/dashboard.ts b/admin/src/api/tracker/dashboard.ts new file mode 100644 index 0000000..5c0da12 --- /dev/null +++ b/admin/src/api/tracker/dashboard.ts @@ -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({ url: '/admin/dashboard/overview', params: site ? { site } : {} }) diff --git a/admin/src/api/tracker/sites.ts b/admin/src/api/tracker/sites.ts new file mode 100644 index 0000000..66f6573 --- /dev/null +++ b/admin/src/api/tracker/sites.ts @@ -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> + +/** 站点列表(按 created_at 倒序) */ +export const getSites = () => request.get({ url: '/admin/sites' }) + +/** 创建站点 */ +export const createSite = (data: CreateSiteData) => + request.post({ url: '/admin/sites', data }) + +/** 更新站点(PATCH:模板封装未暴露 patch,走通用 request) */ +export const updateSite = (id: number, data: UpdateSiteData) => + request.request({ url: `/admin/sites/${id}`, method: 'PATCH', data }) + +/** 停用站点(DELETE 软删:is_active=False,上报侧 collect 即 403) */ +export const deactivateSite = (id: number) => request.del({ url: `/admin/sites/${id}` }) diff --git a/admin/src/api/tracker/visitors.ts b/admin/src/api/tracker/visitors.ts new file mode 100644 index 0000000..c4e6fd3 --- /dev/null +++ b/admin/src/api/tracker/visitors.ts @@ -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 + 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 + 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) => + request.get({ url: '/admin/visitors', params }) + +/** 访客详情(404 时后端返回 visitor_not_found) */ +export const getVisitor = (visitorId: string) => + request.get({ url: `/admin/visitors/${visitorId}` }) + +/** 跨天时间线(days 后端夹紧 1-90,默认 30) */ +export const getTimeline = (visitorId: string, days = 30) => + request.get({ url: `/admin/visitors/${visitorId}/timeline`, params: { days } }) + +/** 单会话详情(会话摘要 + 事件序列,同一套 page_view/page_stay 合并规则) */ +export const getSession = (visitorId: string, sessionId: string) => + request.get({ + url: `/admin/visitors/${visitorId}/sessions/${sessionId}` + }) diff --git a/admin/src/locales/langs/en.json b/admin/src/locales/langs/en.json index 51e196b..02e2649 100644 --- a/admin/src/locales/langs/en.json +++ b/admin/src/locales/langs/en.json @@ -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": { diff --git a/admin/src/locales/langs/zh.json b/admin/src/locales/langs/zh.json index 77a23a5..387e57c 100644 --- a/admin/src/locales/langs/zh.json +++ b/admin/src/locales/langs/zh.json @@ -263,6 +263,13 @@ "role": "角色管理", "userCenter": "个人中心", "menu": "菜单管理" + }, + "tracker": { + "title": "访客统计", + "dashboard": "数据总览", + "visitor": "访客管理", + "visitorDetail": "访客详情", + "site": "站点管理" } }, "table": { diff --git a/admin/src/router/modules/index.ts b/admin/src/router/modules/index.ts index deff162..38b1998 100644 --- a/admin/src/router/modules/index.ts +++ b/admin/src/router/modules/index.ts @@ -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 ] diff --git a/admin/src/router/modules/tracker.ts b/admin/src/router/modules/tracker.ts new file mode 100644 index 0000000..dabaf0e --- /dev/null +++ b/admin/src/router/modules/tracker.ts @@ -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'] + } + } + ] +} diff --git a/admin/src/views/tracker/dashboard/index.vue b/admin/src/views/tracker/dashboard/index.vue new file mode 100644 index 0000000..4107d09 --- /dev/null +++ b/admin/src/views/tracker/dashboard/index.vue @@ -0,0 +1,10 @@ + + + diff --git a/admin/src/views/tracker/site/index.vue b/admin/src/views/tracker/site/index.vue new file mode 100644 index 0000000..b2fd849 --- /dev/null +++ b/admin/src/views/tracker/site/index.vue @@ -0,0 +1,10 @@ + + + diff --git a/admin/src/views/tracker/visitor/detail.vue b/admin/src/views/tracker/visitor/detail.vue new file mode 100644 index 0000000..fc96909 --- /dev/null +++ b/admin/src/views/tracker/visitor/detail.vue @@ -0,0 +1,10 @@ + + + diff --git a/admin/src/views/tracker/visitor/index.vue b/admin/src/views/tracker/visitor/index.vue new file mode 100644 index 0000000..05995c5 --- /dev/null +++ b/admin/src/views/tracker/visitor/index.vue @@ -0,0 +1,10 @@ + + +