import {Download, Eye, RefreshCcw, Search, X} from 'lucide-react'; import {FormEvent, useEffect, useMemo, useState} from 'react'; import {listAdminTrackingEvents} from '../api/adminApiClient'; import type { AdminTrackingEventEntryPayload, TrackingScopeKind, } from '../api/adminApiTypes'; import { filterAdminTrackingEventDefinitions, findAdminTrackingEventDefinition, } from '../config/trackingEventDefinitions'; import {handlePageError} from './pageUtils'; interface AdminTrackingEventsPageProps { token: string; onUnauthorized: (message?: string) => void; } const scopeKindOptions: Array<{value: TrackingScopeKind | ''; label: string}> = [ {value: '', label: '全部'}, {value: 'site', label: 'site'}, {value: 'work', label: 'work'}, {value: 'module', label: 'module'}, {value: 'user', label: 'user'}, ]; const exportColumns: Array<{ key: keyof AdminTrackingEventEntryPayload; label: string; }> = [ {key: 'eventId', label: '事件 ID'}, {key: 'eventKey', label: 'Event Key'}, {key: 'eventTitle', label: '事件名称'}, {key: 'scopeKind', label: 'Scope Kind'}, {key: 'scopeId', label: 'Scope ID'}, {key: 'dayKey', label: 'Day Key'}, {key: 'userId', label: 'User ID'}, {key: 'ownerUserId', label: 'Owner User ID'}, {key: 'profileId', label: 'Profile ID'}, {key: 'moduleKey', label: 'Module Key'}, {key: 'metadataJson', label: 'Metadata JSON'}, {key: 'occurredAt', label: '发生时间'}, ]; export function AdminTrackingEventsPage({ token, onUnauthorized, }: AdminTrackingEventsPageProps) { const [entries, setEntries] = useState([]); const [eventKey, setEventKey] = useState(''); const [userId, setUserId] = useState(''); const [scopeKind, setScopeKind] = useState(''); const [scopeId, setScopeId] = useState(''); const [limit, setLimit] = useState('200'); const [errorMessage, setErrorMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); const [detailEntry, setDetailEntry] = useState(null); useEffect(() => { void refreshTrackingEvents(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [token]); const filteredEventDefinitions = useMemo( () => filterAdminTrackingEventDefinitions(eventKey), [eventKey], ); async function refreshTrackingEvents() { setIsLoading(true); setErrorMessage(''); try { const response = await listAdminTrackingEvents(token, { eventKey, userId, scopeKind, scopeId, limit: parseLimit(limit), }); setEntries(response.entries); } catch (error: unknown) { handlePageError(error, onUnauthorized, setErrorMessage); } finally { setIsLoading(false); } } function handleSearch(event: FormEvent) { event.preventDefault(); void refreshTrackingEvents(); } function handleExport() { if (!entries.length) { setErrorMessage('当前没有可导出的埋点数据'); return; } exportTrackingEventsAsExcel(entries); } return (

埋点数据

原始事件明细

{errorMessage ? (
{errorMessage}
) : null}

事件明细

{entries.length} 条
{entries.length ? ( entries.map((entry) => ( )) ) : ( )}
事件 Scope 用户 归属 Metadata 时间 详情
{resolveEventTitle(entry)} {entry.eventKey} {entry.eventId} {entry.scopeKind} {entry.scopeId || '-'} dayKey: {entry.dayKey} {entry.userId || '-'} owner: {entry.ownerUserId || '-'} {entry.profileId || '-'} module: {entry.moduleKey || '-'}
                        {formatMetadataJson(entry.metadataJson)}
                      
{formatOccurredAt(entry.occurredAt)}
{isLoading ? '正在加载' : '暂无数据'}
{detailEntry ? ( setDetailEntry(null)} /> ) : null}
); } function TrackingEventDetailPanel({ entry, onClose, }: { entry: AdminTrackingEventEntryPayload; onClose: () => void; }) { return (

{resolveEventTitle(entry)}

{exportColumns.map((column) => (
{column.label}
{column.key === 'metadataJson' ? (
                    {formatMetadataJson(entry.metadataJson)}
                  
) : ( formatExportCell(entry[column.key], column.key) || '-' )}
))}
); } function parseLimit(value: string) { const parsed = Number.parseInt(value.trim(), 10); if (!Number.isFinite(parsed)) { return 200; } return Math.min(Math.max(parsed, 1), 1000); } function resolveEventTitle(entry: AdminTrackingEventEntryPayload) { return ( findAdminTrackingEventDefinition(entry.eventKey)?.title || entry.eventTitle || entry.eventKey ); } function formatMetadataJson(value: string) { const trimmed = value.trim(); if (!trimmed) { return '-'; } try { return JSON.stringify(JSON.parse(trimmed), null, 2); } catch { return trimmed; } } function exportTrackingEventsAsExcel(entries: AdminTrackingEventEntryPayload[]) { const tableRows = [ exportColumns.map((column) => `${escapeHtml(column.label)}`).join(''), ...entries.map((entry) => exportColumns .map( (column) => `${escapeHtml(formatExportCell(entry[column.key], column.key))}`, ) .join(''), ), ]; const html = `\uFEFF${tableRows .map((row) => `${row}`) .join('')}
`; const blob = new Blob([html], {type: 'application/vnd.ms-excel;charset=utf-8'}); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `tracking-events-${buildTimestamp()}.xls`; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); } function formatExportCell(value: unknown, key?: keyof AdminTrackingEventEntryPayload) { if (value === null || typeof value === 'undefined') { return ''; } if (key === 'occurredAt') { return formatOccurredAt(String(value)); } return String(value); } function formatOccurredAt(value: string) { const trimmed = value.trim(); if (!trimmed) { return '-'; } if (/^\d+$/.test(trimmed)) { const micros = Number(trimmed); if (Number.isSafeInteger(micros)) { return formatDateTime(new Date(Math.floor(micros / 1000))); } } const parsed = new Date(trimmed); if (!Number.isNaN(parsed.getTime())) { return formatDateTime(parsed); } return trimmed; } function formatDateTime(date: Date) { const pad = (value: number, size = 2) => String(value).padStart(size, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad( date.getHours(), )}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; } function escapeHtml(value: string) { return value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function buildTimestamp() { const now = new Date(); const pad = (value: number) => String(value).padStart(2, '0'); return [ now.getFullYear(), pad(now.getMonth() + 1), pad(now.getDate()), '-', pad(now.getHours()), pad(now.getMinutes()), pad(now.getSeconds()), ].join(''); }