Files
backend/frontend/src/composables/useAnalytics.ts
T
34047007@qq.com a6cd99a4ca
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
feat: initial commit - oncology literature search platform
OncoLit: a multi-tenant oncology literature search, feed, and
collaboration platform. Built with FastAPI + Vue 3 + PostgreSQL.
Includes PubMed pipeline, drug approvals, AI summaries, and
systematic review tools.
2026-07-27 07:59:18 +08:00

147 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 用户行为追踪 composable
* - 自动页面访问追踪(在 router.afterEach 中调用)
* - 手动行为埋点(在关键交互处调用 trackAction
*/
import type { RouteLocationNormalized } from 'vue-router'
import { api } from '../api/client'
import { useAuthStore } from '../stores/auth'
function generateUUID(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID()
}
// HTTP 环境 fallback
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
})
}
const _sessionId = generateUUID()
let _lastPath = ''
let _pageEnterTime = Date.now()
const ANONYMOUS_ID_KEY = 'scilit_anonymous_id'
function getOrCreateAnonymousId(): string {
let id = localStorage.getItem(ANONYMOUS_ID_KEY)
if (!id) {
id = generateUUID()
localStorage.setItem(ANONYMOUS_ID_KEY, id)
}
return id
}
/** 路由名称 → 中文页面名称映射 */
const ROUTE_TITLE: Record<string, string> = {
// 公开路由
home: '首页',
'public-detail': '文献详情',
journals: '期刊导航',
cancers: '癌种浏览',
drugs: '药品审批',
guidelines: '临床指南',
'guideline-compare': '指南对比',
'treatment-plans': '治疗方案',
pricing: '定价',
about: '关于',
help: '帮助',
// 认证路由
login: '登录',
'admin-login': '管理员登录',
register: '注册',
'accept-invite': '接受邀请',
'forgot-password': '忘记密码',
'reset-password': '重置密码',
// 应用路由
feed: '我的文献推送',
profile: '个人主页',
search: '文献搜索',
library: '我的文库',
detail: '文献详情',
team: '团队管理',
settings: '设置',
interests: '关注领域',
domains: '领域中心',
'journal-club': '期刊俱乐部',
mdt: 'MDT 多学科会诊',
activity: '浏览历史',
'api-management': 'API 管理',
notifications: '通知中心',
notes: '我的笔记',
reviews: '文献评阅',
'review-detail': '评阅详情',
'journal-detail': '期刊详情',
// 管理后台
'admin-dashboard': '管理后台',
'admin-tenants': '租户管理',
'admin-users': '用户管理',
'admin-pipeline': '数据管道',
'admin-tags': '标签管理',
'admin-journals': '期刊管理',
'admin-drug-approvals': '药品审批管理',
'admin-guidelines': '指南管理',
'admin-notifications': '通知管理',
'admin-page-views': '访问记录',
'admin-feedback': '用户反馈',
'admin-analytics': '数据分析',
'admin-config': '系统配置',
'admin-roles': '平台角色',
}
/** 记录页面访问(供 router.afterEach 调用,不阻塞导航) */
export function trackPageView(to: RouteLocationNormalized) {
if (to.path === _lastPath) return
const duration = Math.floor((Date.now() - _pageEnterTime) / 1000)
const dur = duration > 0 ? duration : undefined
const auth = useAuthStore()
if (auth.isAuthenticated) {
api.post('/analytics/track', {
activity_type: 'page_view',
target_type: 'route',
target_id: (to.name as string) || to.path,
detail: { path: to.path, title: ROUTE_TITLE[(to.name as string)] || document.title },
session_id: _sessionId,
duration_seconds: dur,
}).catch(() => { /* 静默失败 */ })
} else {
api.post('/public/page-view', {
anonymous_id: getOrCreateAnonymousId(),
path: to.path,
page_title: ROUTE_TITLE[(to.name as string)] || document.title,
duration_seconds: dur,
}).catch(() => { /* 静默失败 */ })
}
_lastPath = to.path
_pageEnterTime = Date.now()
}
/**
* 手动记录用户行为
*
* @param type activity_type, 如 'search' / 'view_literature' / 'save_literature'
* @param targetType target_type, 如 'literature' / 'tag' / 'journal'
* @param targetId target_id, 如 PMID / 标签ID
* @param detail 附加信息(如搜索关键词)
*/
export function trackAction(
type: string,
targetType?: string,
targetId?: string,
detail?: Record<string, unknown>,
) {
const auth = useAuthStore()
if (!auth.isAuthenticated) return
api.post('/analytics/track', {
activity_type: type,
target_type: targetType,
target_id: targetId,
detail,
session_id: _sessionId,
}).catch(() => { /* 静默失败 */ })
}