import { Download, Eye, RefreshCcw, Search, X } from 'lucide-react'; import { FormEvent, useEffect, useMemo, useState } from 'react'; import { listAdminTrackingEventKeys, listAdminTrackingEvents, } from '../api/adminApiClient'; import type { AdminTrackingEventEntryPayload, AdminTrackingEventKeyPayload, TrackingScopeKind, } from '../api/adminApiTypes'; import { AdminUserReferenceButton } from '../components/AdminUserReferenceButton'; import { buildAdminTrackingEventKeyOptions, filterAdminTrackingEventDefinitions, filterAdminTrackingEventKeyOptions, 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 [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [limit, setLimit] = useState('200'); const [eventKeys, setEventKeys] = useState( [], ); const [errorMessage, setErrorMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isExportingAll, setIsExportingAll] = useState(false); const [detailEntry, setDetailEntry] = useState(null); useEffect(() => { void refreshTrackingEventKeys(); void refreshTrackingEvents(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [token]); const eventKeyOptions = useMemo( () => buildAdminTrackingEventKeyOptions(eventKeys), [eventKeys], ); const filteredEventDefinitions = useMemo(() => { const dynamicOptions = filterAdminTrackingEventKeyOptions( eventKeyOptions, eventKey, ); if (dynamicOptions.length) { return dynamicOptions; } return filterAdminTrackingEventDefinitions(eventKey); }, [eventKey, eventKeyOptions]); async function refreshTrackingEventKeys() { try { const response = await listAdminTrackingEventKeys(token); setEventKeys(response.eventKeys); } catch { setEventKeys([]); } } async function refreshTrackingEvents() { setIsLoading(true); setErrorMessage(''); try { const response = await listAdminTrackingEvents(token, { eventKey, userId, scopeKind, scopeId, startDate, endDate, 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); } async function handleExportAll() { setIsExportingAll(true); setErrorMessage(''); try { const response = await listAdminTrackingEvents(token, { eventKey, userId, scopeKind, scopeId, startDate, endDate, limit: 100_000, exportAll: true, }); if (!response.entries.length) { setErrorMessage('当前没有可导出的埋点数据'); return; } exportTrackingEventsAsExcel(response.entries); } catch (error: unknown) { handlePageError(error, onUnauthorized, setErrorMessage); } finally { setIsExportingAll(false); } } 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 || '-'} {entry.userId ? ( ) : null}
owner: {entry.ownerUserId || '-'} {entry.ownerUserId ? ( ) : null}
{entry.profileId || '-'} module: {entry.moduleKey || '-'}
                        {formatMetadataJson(entry.metadataJson)}
                      
{formatOccurredAt(entry.occurredAt)}
{isLoading ? '正在加载' : '暂无数据'}
{detailEntry ? ( setDetailEntry(null)} /> ) : null}
); } function TrackingEventDetailPanel({ entry, token, onUnauthorized, onClose, }: { entry: AdminTrackingEventEntryPayload; token: string; onUnauthorized: (message?: string) => void; onClose: () => void; }) { return (

{resolveEventTitle(entry)}

{exportColumns.map((column) => (
{column.label}
{column.key === 'metadataJson' ? (
                    {formatMetadataJson(entry.metadataJson)}
                  
) : column.key === 'userId' && entry.userId ? (
{entry.userId}
) : column.key === 'ownerUserId' && entry.ownerUserId ? (
{entry.ownerUserId}
) : ( 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(''); }