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