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