增加报表

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

+1 -1
View File
@@ -191,7 +191,7 @@ function onTimelineTogglePointerDown(e: PointerEvent) {
window.removeEventListener('pointerup', finish)
window.removeEventListener('pointercancel', finish)
if (!dragging) return
trackMatomoEvent('timeline.resize')
trackMatomoEvent('时间轴', '调整数值', `时间轴高度=${timelineH.value}`)
if (timelineH.value > 0) saveTimelineHeight()
// 拖拽后的 click 与本次手势属于同一操作,不能再次切换折叠。
ignoreTimelineToggleClick = true
+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)
}
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div class="spine-editor">
<div class="spine-editor" data-matomo-section="spine">
<div class="editor-title">{{ $t('spine.title') }}</div>
<label class="field wide"><span>{{ $t('spine.name') }}</span><input v-model="spineObject.name" /></label>
<div class="grid three">
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div class="gradient-editor">
<div class="gradient-editor" data-matomo-section="gradient">
<div
ref="bar"
class="gradient-bar"
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div class="ce">
<div class="ce" data-matomo-section="curve">
<div class="ce-editor">
<svg ref="svg" class="ce-svg" @dblclick="onDbl">
<!-- 网格线(0.25/0.5/0.75) -->
+3 -3
View File
@@ -1,6 +1,6 @@
<template>
<div v-if="show" class="help-overlay" role="presentation" @click.self="close">
<section class="help-dialog" role="dialog" aria-modal="true" aria-labelledby="help-title">
<section class="help-dialog" data-matomo-section="help" role="dialog" aria-modal="true" aria-labelledby="help-title">
<header class="help-head">
<div>
<h2 id="help-title">{{ $t('help.title') }}</h2>
@@ -172,8 +172,8 @@
<dl class="help-list">
<div><dt>{{ $t('help.prerequisite') }}</dt><dd>{{ $t('help.prerequisiteDesc') }}</dd></div>
<div><dt>{{ $t('help.download') }}</dt><dd>
<a class="dl-link" href="/downloads/connector/SpineParticleConnector-windows-x64.exe" download>Windows<br><small>SpineParticleConnector-windows-x64.exe</small></a>
<a class="dl-link" href="/downloads/connector/SpineParticleConnector-macos-universal.zip" download>macOS (Universal)<br><small>SpineParticleConnector-macos-universal.zip</small></a>
<a class="dl-link" href="/downloads/connector/SpineParticleConnector-windows-x64.exe" data-matomo-category="使用说明" data-matomo-action="点击下载" data-matomo-name="下载 Windows 连接器" download>Windows<br><small>SpineParticleConnector-windows-x64.exe</small></a>
<a class="dl-link" href="/downloads/connector/SpineParticleConnector-macos-universal.zip" data-matomo-category="使用说明" data-matomo-action="点击下载" data-matomo-name="下载 macOS 连接器" download>macOS (Universal)<br><small>SpineParticleConnector-macos-universal.zip</small></a>
</dd></div>
<div><dt>{{ $t('help.webSide') }}</dt><dd>{{ $t('help.webSideDesc') }}</dd></div>
<div><dt>{{ $t('help.authorization') }}</dt><dd>{{ $t('help.authorizationDesc') }}</dd></div>
+2 -2
View File
@@ -1,5 +1,5 @@
<template>
<div class="image-card">
<div class="image-card" data-matomo-section="resources">
<div class="imgrow">
<button class="img-thumb" data-matomo-event="resources.pick-image" :title="supportsSequence && resource.imageMode === 'sequence' ? $t('image.chooseSequence') : $t('image.chooseLocal')" @click="pickImage">
<img v-if="displayPreviewUrl" :src="displayPreviewUrl" :alt="resource.textureName" />
@@ -82,7 +82,7 @@
<Teleport to="body">
<div v-if="pivotFullscreen" class="pivot-modal-backdrop" @pointerdown.self="closePivotFullscreen(false)">
<div class="pivot-modal" role="dialog" aria-modal="true" :aria-label="$t('image.pivotModal')">
<div class="pivot-modal" data-matomo-section="resources" role="dialog" aria-modal="true" :aria-label="$t('image.pivotModal')">
<div class="pivot-modal-head">
<div class="pivot-modal-icon">◎</div>
<div class="pivot-modal-info">
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div class="pac">
<div class="pac" data-matomo-section="attribute-control">
<div class="pac-head">
<span class="pac-label">{{ label }}</span>
<div v-if="showModes" class="pac-modes">
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div class="pivot-editor" :class="{ 'pivot-large': large }">
<div class="pivot-editor" data-matomo-section="pivot" :class="{ 'pivot-large': large }">
<div class="pivot-toolbar">
<div class="pivot-hint">
<span class="pivot-hint-icon">⊕</span>
+1 -1
View File
@@ -39,7 +39,7 @@ function normalizeBoneDebugSizes() {
</script>
<template>
<div v-if="show" ref="panelRef" class="settings-panel" tabindex="-1" @focusout="onFocusOut">
<div v-if="show" ref="panelRef" class="settings-panel" data-matomo-section="settings" tabindex="-1" @focusout="onFocusOut">
<div class="sp-head">{{ t('settings.title') }} <button class="sp-close" @click="emit('close')">✕</button></div>
<div class="sp-section-title">{{ t('settings.layout') }}</div>
<label class="sp-switch-row">
+1 -1
View File
@@ -68,7 +68,7 @@ function changeLanguage(event: Event) {
</script>
<template>
<div class="top-operation-stack">
<div class="top-operation-stack" data-matomo-section="toolbar">
<div class="topbar">
<button class="settings-btn" @click="toggleSettings" :title="t('toolbar.settings')">{{ t('toolbar.settings') }}</button>
<button class="undo-btn" :disabled="editorHistoryState.undoCount === 0" @click="undo" :title="t('toolbar.undo')">↶</button>
+2 -2
View File
@@ -1,8 +1,8 @@
<template>
<div class="timeline">
<div class="timeline" data-matomo-section="timeline">
<!-- 控制按钮 -->
<Teleport defer to="#timeline-controls-host">
<div class="tl-controls">
<div class="tl-controls" data-matomo-section="timeline">
<button class="tl-btn" data-matomo-event="timeline.play-toggle" :class="{ on: store.timeline.playing }" @click="store.togglePlay()" :title="store.timeline.playing ? $t('timeline.pause') : $t('timeline.play')">
{{ store.timeline.playing ? '❚❚' : '▶' }}
</button>
+2 -2
View File
@@ -1,6 +1,6 @@
<template>
<!-- 2. 发射模式 -->
<div class="group" v-show="sectionVisible('emitMode')">
<div class="group" data-matomo-section="emission" v-show="sectionVisible('emitMode')">
<div class="group-head" @click="toggle('emitMode')">
<span class="caret">{{ collapsed.emitMode ? '▸' : '▾' }}</span><span>{{ $t('emission.title') }}</span>
<button class="group-reset" data-matomo-event="emission.reset" :title="$t('emission.reset')" @click.stop="resetModule('emitMode')">{{ $t('common.reset') }}</button>
@@ -62,7 +62,7 @@
</div>
<!-- 3. 发射器形状 -->
<div class="group" v-show="sectionVisible('shape')">
<div class="group" data-matomo-section="shape" v-show="sectionVisible('shape')">
<div class="group-head" @click="toggle('shape')">
<span class="caret">{{ collapsed.shape ? '▸' : '▾' }}</span><span>{{ $t('emission.shapeTitle') }}</span>
<button class="group-reset" data-matomo-event="shape.reset" :title="$t('emission.shapeReset')" @click.stop="resetModule('shape')">{{ $t('common.reset') }}</button>
@@ -1,6 +1,6 @@
<template>
<!-- 7. 导出资源:导出参数与资源输出操作。 -->
<div class="group">
<div class="group" data-matomo-section="export">
<div class="group-head" @click="toggle('export')">
<span class="caret">{{ collapsed.export ? '▸' : '▾' }}</span><span>{{ $t('export.title') }}</span>
<button class="group-reset" data-matomo-event="export.reset" :title="$t('export.reset')" @click.stop="resetModule('export')">{{ $t('common.reset') }}</button>
@@ -1,6 +1,6 @@
<template>
<!-- 5. 外观与资源 -->
<div class="group">
<div class="group" data-matomo-section="appearance">
<div class="group-head" @click="toggle('look')">
<span class="caret">{{ collapsed.look ? '▸' : '▾' }}</span><span>{{ $t('appearance.title') }}</span>
<button class="group-reset" data-matomo-event="appearance.reset" :title="$t('appearance.reset')" @click.stop="resetModule('look')">{{ $t('common.reset') }}</button>
@@ -1,6 +1,6 @@
<template>
<!-- 4. 粒子属性 -->
<div class="group">
<div class="group" data-matomo-section="attributes">
<div class="group-head" @click="toggle('attr')">
<span class="caret">{{ collapsed.attr ? '▸' : '▾' }}</span><span>{{ $t('attributes.title') }}</span>
<button class="group-reset" data-matomo-event="attributes.reset" :title="$t('attributes.reset')" @click.stop="resetModule('attr')">{{ $t('common.reset') }}</button>
@@ -1,6 +1,6 @@
<template>
<!-- 6. 修改器 -->
<div class="group">
<div class="group" data-matomo-section="modifiers">
<div class="group-head" @click="toggle('mods')">
<span class="caret">{{ collapsed.mods ? '▸' : '▾' }}</span><span>{{ $t('modifiers.title') }}</span>
<button class="group-reset" data-matomo-event="modifiers.reset" :title="$t('modifiers.reset')" @click.stop="resetModule('mods')">{{ $t('common.reset') }}</button>
@@ -83,7 +83,7 @@
</select>
</label>
<div class="modifier-hint">{{ $t('modifiers.forceHint') }}</div>
<label class="switch-row"><input type="checkbox" v-model="sys.config.showForceFieldShape" /><span class="switch-ui"></span><span>{{ $t('modifiers.showShape') }}</span></label>
<label class="switch-row" data-matomo-name="显示力场形状"><input type="checkbox" v-model="sys.config.showForceFieldShape" /><span class="switch-ui"></span><span>{{ $t('modifiers.showShape') }}</span></label>
</div>
</div>
<div class="modifier-item">
@@ -104,7 +104,7 @@
</select>
</label>
<div class="modifier-hint">{{ $t('modifiers.vortexHint') }}</div>
<label class="switch-row"><input type="checkbox" v-model="sys.config.showVortexShape" /><span class="switch-ui"></span><span>{{ $t('modifiers.showShape') }}</span></label>
<label class="switch-row" data-matomo-name="显示漩涡形状"><input type="checkbox" v-model="sys.config.showVortexShape" /><span class="switch-ui"></span><span>{{ $t('modifiers.showShape') }}</span></label>
</div>
</div>
<div class="modifier-item">
@@ -131,7 +131,7 @@
</select>
</label>
<div class="modifier-hint">{{ $t('modifiers.attractionHint') }}</div>
<label class="switch-row"><input type="checkbox" v-model="sys.config.showAttractionShape" /><span class="switch-ui"></span><span>{{ $t('modifiers.showShape') }}</span></label>
<label class="switch-row" data-matomo-name="显示吸附范围"><input type="checkbox" v-model="sys.config.showAttractionShape" /><span class="switch-ui"></span><span>{{ $t('modifiers.showShape') }}</span></label>
</div>
</div>
<div class="modifier-item">
@@ -1,5 +1,5 @@
<template>
<div class="group config-management">
<div class="group config-management" data-matomo-section="config">
<div class="group-head" @click="toggle('config')">
<span class="caret">{{ collapsed.config ? '▸' : '▾' }}</span><span>{{ $t('config.title') }}</span>
<button type="button" class="group-download" data-matomo-event="config.download-current" @click.stop="downloadCurrentProject">{{ $t('config.local.downloadCurrent') }}</button>
@@ -55,7 +55,7 @@
</div>
<Teleport to="body">
<div v-if="projectSaveOpen" class="project-save-backdrop" @pointerdown.self="closeProjectSaveDialog">
<form class="project-save-dialog" @submit.prevent="confirmSaveConfig" @keydown.esc.prevent="closeProjectSaveDialog">
<form class="project-save-dialog" data-matomo-section="config" @submit.prevent="confirmSaveConfig" @keydown.esc.prevent="closeProjectSaveDialog">
<div class="project-save-head">
<span>{{ $t('config.local.saveTitle') }}</span>
<button type="button" :disabled="projectStorageBusy" @click="closeProjectSaveDialog">✕</button>
@@ -1,6 +1,6 @@
<template>
<!-- 1. 场景对象:创建入口始终可用,即使当前没有粒子系统。 -->
<div class="group">
<div class="group" data-matomo-section="scene">
<div class="group-head scene-head" @click="toggle('scene')">
<span class="caret">{{ collapsed.scene ? '▸' : '▾' }}</span><span>{{ $t('scene.title') }}</span>
<div class="scene-actions">