增加报表

This commit is contained in:
tianmo committed 2026-10-28 16:13:18 +08:00
1 parent ca719ba1d4
commit b8652acce0
25 files changed
+136 -44

No files matched your search

+103 -11
View File
@@ -1,15 +1,81 @@
type MatomoWindow = Window & { _paq?: unknown[][] }
import { i18n, SUPPORTED_LOCALES } from '../i18n'
export function trackMatomoEvent(eventName: string) {
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 === '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 separator = eventName.indexOf('.')
if (separator <= 0 || separator === eventName.length - 1) return
const target = window as MatomoWindow
;(target._paq ||= []).push([
'trackEvent',
eventName.slice(0, separator),
eventName.slice(separator + 1),
])
;(target._paq ||= []).push(['trackEvent', category, action, name])
}
export function installMatomoEventTracking() {
@@ -17,7 +83,33 @@ export function installMatomoEventTracking() {
document.addEventListener('click', (event) => {
const target = event.target
if (!(target instanceof Element)) return
const tracked = target.closest<HTMLElement>('[data-matomo-event]')
if (tracked?.dataset.matomoEvent) trackMatomoEvent(tracked.dataset.matomoEvent)
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 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)
}