Files
backend/frontend/src/composables/useAnalytics.ts
T

147 lines
4.3 KiB
TypeScript
Raw Normal View History

/**
* 用户行为追踪 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(() => { /* 静默失败 */ })
}