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(() => { /* 静默失败 */ })
|
|||
|
|
}
|