import { i18n, SUPPORTED_LOCALES } from '../i18n' type MatomoWindow = Window & { _paq?: unknown[][] } type EventCategory = '欢迎页' | '页面导航' | '时间轴' | '系统设置' | '属性面板' | '使用说明' type EventAction = '点击按钮' | '点击开关' | '调整数值' | '点击下载' type MessageTree = Record let chineseLabels: Map | null = null function flattenMessages(value: unknown, prefix = '', output: Record = {}) { if (!value || typeof value !== 'object') return output for (const [key, child] of Object.entries(value as MessageTree)) { const path = prefix ? `${prefix}.${key}` : key if (typeof child === 'string') output[path] = child else flattenMessages(child, path, output) } return output } function localizedToChinese(value: string | null | undefined) { const label = value?.replace(/\s+/g, ' ').trim() if (!label) return '' if (!chineseLabels) { chineseLabels = new Map() const chinese = flattenMessages(i18n.global.getLocaleMessage('zh-CN')) for (const locale of SUPPORTED_LOCALES) { const messages = flattenMessages(i18n.global.getLocaleMessage(locale.code)) for (const [key, text] of Object.entries(messages)) { const translated = chinese[key] if (translated) chineseLabels.set(text.replace(/\s+/g, ' ').trim(), translated) } } } return chineseLabels.get(label) || '' } function categoryFor(element: Element): EventCategory { const section = element.closest('[data-matomo-section]')?.dataset.matomoSection if (section === 'welcome') return '欢迎页' if (section === 'timeline') return '时间轴' if (section === 'settings') return '系统设置' return '属性面板' } function controlName(control: Element, fallback: string) { const candidates = [ (control as HTMLElement).dataset.matomoName, control.closest('[data-matomo-name]')?.dataset.matomoName, control.getAttribute('aria-label'), control.getAttribute('title'), control.textContent, control.closest('label')?.textContent, control.parentElement?.querySelector('.ns-label, .pac-label')?.textContent, ] for (const candidate of candidates) { const chinese = localizedToChinese(candidate) if (chinese) return chinese } return fallback } function buttonName(button: HTMLButtonElement) { const section = button.closest('[data-matomo-section]') || document const className = [...button.classList] .filter((name) => !['active', 'collapsed', 'danger', 'ghost', 'off', 'on', 'primary', 'selected'].includes(name)) .sort() .join('-') || button.type || 'button' const peers = [...section.querySelectorAll('button')] .filter((peer) => ([...peer.classList] .filter((name) => !['active', 'collapsed', 'danger', 'ghost', 'off', 'on', 'primary', 'selected'].includes(name)) .sort() .join('-') || peer.type || 'button') === className) return controlName(button, `按钮${Math.max(1, peers.indexOf(button) + 1)}`) } export function trackMatomoEvent(category: EventCategory, action: EventAction, name: string) { if (!import.meta.env.PROD) return const target = window as MatomoWindow ;(target._paq ||= []).push(['trackEvent', category, action, name]) } export function trackMatomoPageView() { if (!import.meta.env.PROD) return const target = window as MatomoWindow ;(target._paq ||= []).push( ['setCustomUrl', window.location.href], ['setDocumentTitle', document.title], ['trackPageView'], ) } export function installMatomoEventTracking() { if (!import.meta.env.PROD) return document.addEventListener('click', (event) => { const target = event.target if (!(target instanceof Element)) return const download = target.closest('a[data-matomo-category][data-matomo-action][data-matomo-name]') if (download) { trackMatomoEvent( download.dataset.matomoCategory as EventCategory, download.dataset.matomoAction as EventAction, download.dataset.matomoName || '', ) return } const tracked = target.closest('[data-matomo-event]') if (tracked) { const category = tracked.dataset.matomoCategory as EventCategory | undefined const action = tracked.dataset.matomoAction as EventAction | undefined trackMatomoEvent(category || categoryFor(tracked), action || '点击按钮', tracked.dataset.matomoEvent || '') return } const button = target.closest('button') if (!button) return trackMatomoEvent(categoryFor(button), '点击按钮', buttonName(button)) }, true) document.addEventListener('change', (event) => { const target = event.target if (!(target instanceof HTMLInputElement)) return if (target.type === 'checkbox') { const name = controlName(target, '开关') trackMatomoEvent(categoryFor(target), '点击开关', `${name}=${target.checked ? '开启' : '关闭'}`) return } if (target.type !== 'range' && target.type !== 'number') return const pairedRange = target.type === 'range' || !!target.parentElement?.querySelector('input[type="range"]') if (!pairedRange) return const name = controlName(target, '数值') trackMatomoEvent(categoryFor(target), '调整数值', `${name}=${target.value}`) }, true) }