134 lines
5.3 KiB
TypeScript
134 lines
5.3 KiB
TypeScript
import { i18n, SUPPORTED_LOCALES } from '../i18n'
|
|
|
|
type MatomoWindow = Window & { _paq?: unknown[][] }
|
|
type EventCategory = '欢迎页' | '页面导航' | '时间轴' | '系统设置' | '属性面板' | '使用说明'
|
|
type EventAction = '点击按钮' | '点击开关' | '调整数值' | '点击下载'
|
|
type MessageTree = Record<string, unknown>
|
|
|
|
let chineseLabels: Map<string, string> | null = null
|
|
|
|
function flattenMessages(value: unknown, prefix = '', output: Record<string, string> = {}) {
|
|
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<HTMLElement>('[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<HTMLElement>('[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<HTMLButtonElement>('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<HTMLAnchorElement>('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<HTMLElement>('[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<HTMLButtonElement>('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)
|
|
}
|