补齐后台埋点导出和任务事件选择
后台埋点查询支持日期筛选和导出全部模式 新增后台真实 event key 列表接口 任务配置页改用真实 user 埋点 key 候选 更新后台埋点查询文档和定向测试
This commit is contained in:
@@ -15,6 +15,7 @@ import type {
|
||||
AdminMeResponse,
|
||||
AdminOverviewResponse,
|
||||
AdminTrackingEventListQuery,
|
||||
AdminTrackingEventKeyListResponse,
|
||||
AdminTrackingEventListResponse,
|
||||
AdminUpdateWorkVisibilityRequest,
|
||||
AdminUpdateWorkVisibilityResponse,
|
||||
@@ -192,6 +193,13 @@ export function listAdminTrackingEvents(
|
||||
);
|
||||
}
|
||||
|
||||
export function listAdminTrackingEventKeys(token: string) {
|
||||
return request<AdminTrackingEventKeyListResponse>(
|
||||
'/admin/api/tracking/event-keys',
|
||||
{ token },
|
||||
);
|
||||
}
|
||||
|
||||
export function getAdminCreationEntryConfig(token: string) {
|
||||
return request<AdminCreationEntryConfigResponse>(
|
||||
'/admin/api/creation-entry/config',
|
||||
@@ -430,9 +438,14 @@ function buildQueryString(query: AdminTrackingEventListQuery) {
|
||||
appendQueryParam(params, 'userId', query.userId);
|
||||
appendQueryParam(params, 'scopeKind', query.scopeKind);
|
||||
appendQueryParam(params, 'scopeId', query.scopeId);
|
||||
appendQueryParam(params, 'startDate', query.startDate);
|
||||
appendQueryParam(params, 'endDate', query.endDate);
|
||||
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
|
||||
params.set('limit', String(query.limit));
|
||||
}
|
||||
if (query.exportAll) {
|
||||
params.set('exportAll', 'true');
|
||||
}
|
||||
const queryString = params.toString();
|
||||
return queryString ? `?${queryString}` : '';
|
||||
}
|
||||
|
||||
@@ -206,7 +206,10 @@ export interface AdminTrackingEventListQuery {
|
||||
userId?: string;
|
||||
scopeKind?: TrackingScopeKind | '';
|
||||
scopeId?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
limit?: number;
|
||||
exportAll?: boolean;
|
||||
}
|
||||
|
||||
/** 后台创作入口配置响应,同时包含模板入口和独立公告配置。 */
|
||||
@@ -503,3 +506,13 @@ export interface AdminTrackingEventEntryPayload {
|
||||
export interface AdminTrackingEventListResponse {
|
||||
entries: AdminTrackingEventEntryPayload[];
|
||||
}
|
||||
|
||||
export interface AdminTrackingEventKeyPayload {
|
||||
eventKey: string;
|
||||
eventTitle: string;
|
||||
scopeKinds: string[];
|
||||
}
|
||||
|
||||
export interface AdminTrackingEventKeyListResponse {
|
||||
eventKeys: AdminTrackingEventKeyPayload[];
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import {describe, expect, test} from 'vitest';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
adminProfileTaskTrackingEventDefinitions,
|
||||
adminTrackingEventDefinitions,
|
||||
buildAdminTrackingEventKeyOptions,
|
||||
filterAdminProfileTaskTrackingEventDefinitions,
|
||||
filterAdminTrackingEventKeyOptions,
|
||||
filterAdminTrackingEventDefinitions,
|
||||
findAdminTrackingEventDefinition,
|
||||
} from './trackingEventDefinitions';
|
||||
|
||||
describe('admin tracking event definitions', () => {
|
||||
test('后台埋点筛选候选包含后端通用埋点清单', () => {
|
||||
const keys = adminTrackingEventDefinitions.map((definition) => definition.key);
|
||||
const keys = adminTrackingEventDefinitions.map(
|
||||
(definition) => definition.key,
|
||||
);
|
||||
|
||||
expect(keys.length).toBeGreaterThan(40);
|
||||
expect(keys).toContain('daily_login');
|
||||
@@ -22,21 +26,48 @@ describe('admin tracking event definitions', () => {
|
||||
});
|
||||
|
||||
test('任务配置候选只开放适合个人任务的事件', () => {
|
||||
expect(adminProfileTaskTrackingEventDefinitions.map(({key}) => key)).toEqual([
|
||||
'daily_login',
|
||||
]);
|
||||
expect(filterAdminProfileTaskTrackingEventDefinitions('').map(({key}) => key)).toEqual([
|
||||
'daily_login',
|
||||
expect(
|
||||
adminProfileTaskTrackingEventDefinitions.map(({ key }) => key),
|
||||
).toEqual(['daily_login']);
|
||||
expect(
|
||||
filterAdminProfileTaskTrackingEventDefinitions('').map(({ key }) => key),
|
||||
).toEqual(['daily_login']);
|
||||
});
|
||||
|
||||
test('后台埋点 key 候选从真实库响应合并静态标题', () => {
|
||||
const options = buildAdminTrackingEventKeyOptions([
|
||||
{
|
||||
eventKey: 'work_play_start',
|
||||
eventTitle: 'work_play_start',
|
||||
scopeKinds: ['work', 'user'],
|
||||
},
|
||||
{
|
||||
eventKey: 'unknown_event',
|
||||
eventTitle: '未知事件',
|
||||
scopeKinds: ['user'],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(options.find(({ key }) => key === 'work_play_start')?.title).toBe(
|
||||
'作品开始游玩',
|
||||
);
|
||||
expect(options.find(({ key }) => key === 'unknown_event')?.title).toBe(
|
||||
'未知事件',
|
||||
);
|
||||
expect(
|
||||
filterAdminTrackingEventKeyOptions(options, '作品').map(({ key }) => key),
|
||||
).toEqual(['work_play_start']);
|
||||
});
|
||||
|
||||
test('后台埋点筛选支持按中文名称和 key 搜索', () => {
|
||||
expect(filterAdminTrackingEventDefinitions('上传票据').map(({key}) => key)).toEqual([
|
||||
'asset_upload_ticket_create',
|
||||
]);
|
||||
expect(filterAdminTrackingEventDefinitions('work_play').map(({key}) => key)).toEqual([
|
||||
'work_play_start',
|
||||
]);
|
||||
expect(findAdminTrackingEventDefinition(' daily_login ')?.title).toBe('每日登录');
|
||||
expect(
|
||||
filterAdminTrackingEventDefinitions('上传票据').map(({ key }) => key),
|
||||
).toEqual(['asset_upload_ticket_create']);
|
||||
expect(
|
||||
filterAdminTrackingEventDefinitions('work_play').map(({ key }) => key),
|
||||
).toEqual(['work_play_start']);
|
||||
expect(findAdminTrackingEventDefinition(' daily_login ')?.title).toBe(
|
||||
'每日登录',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type {TrackingScopeKind} from '../api/adminApiTypes';
|
||||
import type {
|
||||
AdminTrackingEventKeyPayload,
|
||||
TrackingScopeKind,
|
||||
} from '../api/adminApiTypes';
|
||||
|
||||
export interface AdminTrackingEventDefinition {
|
||||
key: string;
|
||||
@@ -8,6 +11,14 @@ export interface AdminTrackingEventDefinition {
|
||||
taskConfigEligible?: boolean;
|
||||
}
|
||||
|
||||
export interface AdminTrackingEventKeyOption {
|
||||
key: string;
|
||||
title: string;
|
||||
scopeKind: TrackingScopeKind | string;
|
||||
scopeKinds: string[];
|
||||
remark: string;
|
||||
}
|
||||
|
||||
export const adminTrackingEventDefinitions: AdminTrackingEventDefinition[] = [
|
||||
{
|
||||
key: 'auth_login_options_view',
|
||||
@@ -25,7 +36,8 @@ export const adminTrackingEventDefinitions: AdminTrackingEventDefinition[] = [
|
||||
key: 'daily_login',
|
||||
title: '每日登录',
|
||||
scopeKind: 'user',
|
||||
remark: '认证成功或 refresh 续期后由后端幂等记录,用于每日登录任务进度校验。',
|
||||
remark:
|
||||
'认证成功或 refresh 续期后由后端幂等记录,用于每日登录任务进度校验。',
|
||||
taskConfigEligible: true,
|
||||
},
|
||||
{
|
||||
@@ -368,7 +380,8 @@ export const adminTrackingEventDefinitions: AdminTrackingEventDefinition[] = [
|
||||
key: 'match3d_route_success',
|
||||
title: '抓大鹅路由成功',
|
||||
scopeKind: 'user',
|
||||
remark: '抓大鹅创作或运行接口成功响应后兜底记录;GET 入口可能按 site 统计。',
|
||||
remark:
|
||||
'抓大鹅创作或运行接口成功响应后兜底记录;GET 入口可能按 site 统计。',
|
||||
},
|
||||
{
|
||||
key: 'square_hole_route_success',
|
||||
@@ -392,12 +405,15 @@ export const adminTrackingEventDefinitions: AdminTrackingEventDefinition[] = [
|
||||
key: 'work_play_start',
|
||||
title: '作品开始游玩',
|
||||
scopeKind: 'work',
|
||||
remark: '拼图、抓大鹅、方洞、自定义世界、大鱼吃小鱼、Visual Novel 正式开始游玩时记录。',
|
||||
remark:
|
||||
'拼图、抓大鹅、方洞、自定义世界、大鱼吃小鱼、Visual Novel 正式开始游玩时记录。',
|
||||
},
|
||||
];
|
||||
|
||||
export const adminProfileTaskTrackingEventDefinitions =
|
||||
adminTrackingEventDefinitions.filter((definition) => definition.taskConfigEligible);
|
||||
adminTrackingEventDefinitions.filter(
|
||||
(definition) => definition.taskConfigEligible,
|
||||
);
|
||||
|
||||
export function findAdminTrackingEventDefinition(eventKey: string) {
|
||||
const normalizedEventKey = eventKey.trim();
|
||||
@@ -412,6 +428,41 @@ export function filterAdminTrackingEventDefinitions(query: string) {
|
||||
return filterTrackingEventDefinitions(adminTrackingEventDefinitions, query);
|
||||
}
|
||||
|
||||
export function buildAdminTrackingEventKeyOptions(
|
||||
eventKeys: AdminTrackingEventKeyPayload[],
|
||||
): AdminTrackingEventKeyOption[] {
|
||||
const options = new Map<string, AdminTrackingEventKeyOption>();
|
||||
for (const payload of eventKeys) {
|
||||
const key = payload.eventKey.trim();
|
||||
if (!key || options.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const definition = findAdminTrackingEventDefinition(key);
|
||||
const scopeKinds = payload.scopeKinds.length
|
||||
? payload.scopeKinds
|
||||
: definition
|
||||
? [definition.scopeKind]
|
||||
: [];
|
||||
options.set(key, {
|
||||
key,
|
||||
title: definition?.title ?? (payload.eventTitle || key),
|
||||
scopeKind: definition?.scopeKind ?? scopeKinds[0] ?? '',
|
||||
scopeKinds,
|
||||
remark: definition?.remark ?? '真实埋点库中已出现的 event key。',
|
||||
});
|
||||
}
|
||||
return [...options.values()].sort((left, right) =>
|
||||
left.key.localeCompare(right.key),
|
||||
);
|
||||
}
|
||||
|
||||
export function filterAdminTrackingEventKeyOptions(
|
||||
options: AdminTrackingEventKeyOption[],
|
||||
query: string,
|
||||
) {
|
||||
return filterTrackingEventDefinitions(options, query);
|
||||
}
|
||||
|
||||
export function filterAdminProfileTaskTrackingEventDefinitions(query: string) {
|
||||
return filterTrackingEventDefinitions(
|
||||
adminProfileTaskTrackingEventDefinitions,
|
||||
@@ -420,7 +471,9 @@ export function filterAdminProfileTaskTrackingEventDefinitions(query: string) {
|
||||
}
|
||||
|
||||
function filterTrackingEventDefinitions(
|
||||
definitions: AdminTrackingEventDefinition[],
|
||||
definitions: Array<
|
||||
AdminTrackingEventDefinition | AdminTrackingEventKeyOption
|
||||
>,
|
||||
query: string,
|
||||
) {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import {ChevronDown, PowerOff, RefreshCcw, Save} from 'lucide-react';
|
||||
import {FormEvent, useEffect, useMemo, useState} from 'react';
|
||||
import { ChevronDown, PowerOff, RefreshCcw, Save } from 'lucide-react';
|
||||
import { FormEvent, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
disableProfileTaskConfig,
|
||||
listAdminTrackingEventKeys,
|
||||
listProfileTaskConfigs,
|
||||
upsertProfileTaskConfig,
|
||||
} from '../api/adminApiClient';
|
||||
import type {
|
||||
AdminTrackingEventKeyPayload,
|
||||
ProfileTaskConfigAdminResponse,
|
||||
ProfileTaskCycle,
|
||||
TrackingScopeKind,
|
||||
} from '../api/adminApiTypes';
|
||||
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
|
||||
import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm';
|
||||
import {
|
||||
filterAdminProfileTaskTrackingEventDefinitions,
|
||||
buildAdminTrackingEventKeyOptions,
|
||||
filterAdminTrackingEventKeyOptions,
|
||||
findAdminTrackingEventDefinition,
|
||||
} from '../config/trackingEventDefinitions';
|
||||
import {handlePageError} from './pageUtils';
|
||||
import { handlePageError } from './pageUtils';
|
||||
|
||||
interface AdminTaskConfigPageProps {
|
||||
token: string;
|
||||
@@ -25,8 +28,8 @@ interface AdminTaskConfigPageProps {
|
||||
onResultChange: (result: ProfileTaskConfigAdminResponse) => void;
|
||||
}
|
||||
|
||||
const taskCycles: Array<{value: ProfileTaskCycle; label: string}> = [
|
||||
{value: 'daily', label: '每日'},
|
||||
const taskCycles: Array<{ value: ProfileTaskCycle; label: string }> = [
|
||||
{ value: 'daily', label: '每日' },
|
||||
];
|
||||
|
||||
const profileTaskScopeKind = 'user' satisfies TrackingScopeKind;
|
||||
@@ -43,6 +46,9 @@ export function AdminTaskConfigPage({
|
||||
const [description, setDescription] = useState('');
|
||||
const [eventKey, setEventKey] = useState('daily_login');
|
||||
const [eventKeySearch, setEventKeySearch] = useState('每日登录');
|
||||
const [eventKeys, setEventKeys] = useState<AdminTrackingEventKeyPayload[]>(
|
||||
[],
|
||||
);
|
||||
const [isEventKeyPickerOpen, setIsEventKeyPickerOpen] = useState(false);
|
||||
const [cycle, setCycle] = useState<ProfileTaskCycle>('daily');
|
||||
const [threshold, setThreshold] = useState('1');
|
||||
@@ -56,22 +62,53 @@ export function AdminTaskConfigPage({
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isDisabling, setIsDisabling] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
|
||||
const { confirmWrite, confirmDialog } = useAdminWriteConfirm();
|
||||
|
||||
useEffect(() => {
|
||||
void refreshTaskConfigs();
|
||||
void refreshTrackingEventKeys();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
const selectedEventDefinition = useMemo(
|
||||
() => findAdminTrackingEventDefinition(eventKey),
|
||||
[eventKey],
|
||||
const eventKeyOptions = useMemo(
|
||||
() =>
|
||||
buildAdminTrackingEventKeyOptions(eventKeys).filter((option) =>
|
||||
option.scopeKinds.includes(profileTaskScopeKind),
|
||||
),
|
||||
[eventKeys],
|
||||
);
|
||||
const selectedEventOption = useMemo(
|
||||
() =>
|
||||
eventKeyOptions.find((option) => option.key === eventKey) ??
|
||||
findAdminTrackingEventDefinition(eventKey),
|
||||
[eventKey, eventKeyOptions],
|
||||
);
|
||||
const filteredEventDefinitions = useMemo(
|
||||
() => filterAdminProfileTaskTrackingEventDefinitions(eventKeySearch),
|
||||
[eventKeySearch],
|
||||
() => filterAdminTrackingEventKeyOptions(eventKeyOptions, eventKeySearch),
|
||||
[eventKeyOptions, eventKeySearch],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (eventKeySearch !== eventKey) {
|
||||
return;
|
||||
}
|
||||
const nextOption =
|
||||
eventKeyOptions.find((option) => option.key === eventKey) ??
|
||||
findAdminTrackingEventDefinition(eventKey);
|
||||
if (nextOption) {
|
||||
setEventKeySearch(nextOption.title);
|
||||
}
|
||||
}, [eventKey, eventKeyOptions, eventKeySearch]);
|
||||
|
||||
async function refreshTrackingEventKeys() {
|
||||
try {
|
||||
const response = await listAdminTrackingEventKeys(token);
|
||||
setEventKeys(response.eventKeys);
|
||||
} catch {
|
||||
setEventKeys([]);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTaskConfigs() {
|
||||
setIsLoading(true);
|
||||
setListErrorMessage('');
|
||||
@@ -182,16 +219,20 @@ export function AdminTaskConfigPage({
|
||||
setSortOrder(String(entry.sortOrder));
|
||||
setEnabled(entry.enabled);
|
||||
setDisableTaskId(entry.taskId);
|
||||
const nextDefinition = findAdminTrackingEventDefinition(entry.eventKey);
|
||||
setEventKeySearch(nextDefinition?.title ?? entry.eventKey);
|
||||
const nextOption =
|
||||
eventKeyOptions.find((option) => option.key === entry.eventKey) ??
|
||||
findAdminTrackingEventDefinition(entry.eventKey);
|
||||
setEventKeySearch(nextOption?.title ?? entry.eventKey);
|
||||
setIsEventKeyPickerOpen(false);
|
||||
}
|
||||
|
||||
function selectEventKey(nextEventKey: string) {
|
||||
const nextDefinition = findAdminTrackingEventDefinition(nextEventKey);
|
||||
const nextOption =
|
||||
eventKeyOptions.find((option) => option.key === nextEventKey) ??
|
||||
findAdminTrackingEventDefinition(nextEventKey);
|
||||
setEventKey(nextEventKey);
|
||||
if (nextDefinition) {
|
||||
setEventKeySearch(nextDefinition.title);
|
||||
if (nextOption) {
|
||||
setEventKeySearch(nextOption.title);
|
||||
} else {
|
||||
setEventKeySearch(nextEventKey);
|
||||
}
|
||||
@@ -322,9 +363,9 @@ export function AdminTaskConfigPage({
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{selectedEventDefinition ? (
|
||||
{selectedEventOption ? (
|
||||
<small className="admin-field-note">
|
||||
{selectedEventDefinition.remark}
|
||||
{selectedEventOption.remark}
|
||||
</small>
|
||||
) : (
|
||||
<small className="admin-field-note">
|
||||
|
||||
@@ -1,46 +1,55 @@
|
||||
import {Download, Eye, RefreshCcw, Search, X} from 'lucide-react';
|
||||
import {FormEvent, useEffect, useMemo, useState} from 'react';
|
||||
import { Download, Eye, RefreshCcw, Search, X } from 'lucide-react';
|
||||
import { FormEvent, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {listAdminTrackingEvents} from '../api/adminApiClient';
|
||||
import {
|
||||
listAdminTrackingEventKeys,
|
||||
listAdminTrackingEvents,
|
||||
} from '../api/adminApiClient';
|
||||
import type {
|
||||
AdminTrackingEventEntryPayload,
|
||||
AdminTrackingEventKeyPayload,
|
||||
TrackingScopeKind,
|
||||
} from '../api/adminApiTypes';
|
||||
import {
|
||||
buildAdminTrackingEventKeyOptions,
|
||||
filterAdminTrackingEventKeyOptions,
|
||||
filterAdminTrackingEventDefinitions,
|
||||
findAdminTrackingEventDefinition,
|
||||
} from '../config/trackingEventDefinitions';
|
||||
import {handlePageError} from './pageUtils';
|
||||
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 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: '发生时间'},
|
||||
{ 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({
|
||||
@@ -52,21 +61,47 @@ export function AdminTrackingEventsPage({
|
||||
const [userId, setUserId] = useState('');
|
||||
const [scopeKind, setScopeKind] = useState<TrackingScopeKind | ''>('');
|
||||
const [scopeId, setScopeId] = useState('');
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [limit, setLimit] = useState('200');
|
||||
const [eventKeys, setEventKeys] = useState<AdminTrackingEventKeyPayload[]>(
|
||||
[],
|
||||
);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isExportingAll, setIsExportingAll] = useState(false);
|
||||
const [detailEntry, setDetailEntry] =
|
||||
useState<AdminTrackingEventEntryPayload | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshTrackingEventKeys();
|
||||
void refreshTrackingEvents();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [token]);
|
||||
|
||||
const filteredEventDefinitions = useMemo(
|
||||
() => filterAdminTrackingEventDefinitions(eventKey),
|
||||
[eventKey],
|
||||
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);
|
||||
@@ -77,6 +112,8 @@ export function AdminTrackingEventsPage({
|
||||
userId,
|
||||
scopeKind,
|
||||
scopeId,
|
||||
startDate,
|
||||
endDate,
|
||||
limit: parseLimit(limit),
|
||||
});
|
||||
setEntries(response.entries);
|
||||
@@ -100,6 +137,32 @@ export function AdminTrackingEventsPage({
|
||||
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 (
|
||||
<section className="admin-page admin-page-wide">
|
||||
<div className="admin-page-heading">
|
||||
@@ -110,7 +173,7 @@ export function AdminTrackingEventsPage({
|
||||
<div className="admin-action-row">
|
||||
<button
|
||||
className="admin-secondary-button"
|
||||
disabled={isLoading}
|
||||
disabled={isLoading || isExportingAll}
|
||||
type="button"
|
||||
onClick={refreshTrackingEvents}
|
||||
>
|
||||
@@ -126,6 +189,15 @@ export function AdminTrackingEventsPage({
|
||||
<Download size={17} aria-hidden="true" />
|
||||
<span>导出 Excel</span>
|
||||
</button>
|
||||
<button
|
||||
className="admin-secondary-button"
|
||||
disabled={isExportingAll}
|
||||
type="button"
|
||||
onClick={handleExportAll}
|
||||
>
|
||||
<Download size={17} aria-hidden="true" />
|
||||
<span>{isExportingAll ? '导出中' : '导出全部'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -186,7 +258,27 @@ export function AdminTrackingEventsPage({
|
||||
onChange={(event) => setLimit(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<button className="admin-secondary-button" disabled={isLoading} type="submit">
|
||||
<label className="admin-field">
|
||||
<span>开始日期</span>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(event) => setStartDate(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label className="admin-field">
|
||||
<span>结束日期</span>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(event) => setEndDate(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
className="admin-secondary-button"
|
||||
disabled={isLoading}
|
||||
type="submit"
|
||||
>
|
||||
<Search size={17} aria-hidden="true" />
|
||||
<span>{isLoading ? '查询中' : '查询'}</span>
|
||||
</button>
|
||||
@@ -353,9 +445,13 @@ function formatMetadataJson(value: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function exportTrackingEventsAsExcel(entries: AdminTrackingEventEntryPayload[]) {
|
||||
function exportTrackingEventsAsExcel(
|
||||
entries: AdminTrackingEventEntryPayload[],
|
||||
) {
|
||||
const tableRows = [
|
||||
exportColumns.map((column) => `<th>${escapeHtml(column.label)}</th>`).join(''),
|
||||
exportColumns
|
||||
.map((column) => `<th>${escapeHtml(column.label)}</th>`)
|
||||
.join(''),
|
||||
...entries.map((entry) =>
|
||||
exportColumns
|
||||
.map(
|
||||
@@ -368,7 +464,9 @@ function exportTrackingEventsAsExcel(entries: AdminTrackingEventEntryPayload[])
|
||||
const html = `\uFEFF<html><head><meta charset="UTF-8" /></head><body><table>${tableRows
|
||||
.map((row) => `<tr>${row}</tr>`)
|
||||
.join('')}</table></body></html>`;
|
||||
const blob = new Blob([html], {type: 'application/vnd.ms-excel;charset=utf-8'});
|
||||
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;
|
||||
@@ -379,7 +477,10 @@ function exportTrackingEventsAsExcel(entries: AdminTrackingEventEntryPayload[])
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function formatExportCell(value: unknown, key?: keyof AdminTrackingEventEntryPayload) {
|
||||
function formatExportCell(
|
||||
value: unknown,
|
||||
key?: keyof AdminTrackingEventEntryPayload,
|
||||
) {
|
||||
if (value === null || typeof value === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user