events filter
This commit is contained in:
@@ -112,9 +112,9 @@ export default function AnalyticsPage() {
|
||||
Tag Analytics ({selectedTagIds.length} selected)
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{selectedTagIds.map((tagId) => (
|
||||
<div key={tagId} className="p-4 border rounded-md">
|
||||
<h3 className="font-medium text-gray-800">Tag ID: {tagId}</h3>
|
||||
{selectedTagIds.map((tagName) => (
|
||||
<div key={tagName} className="p-4 border rounded-md">
|
||||
<h3 className="font-medium text-gray-800">Tag: {tagName}</h3>
|
||||
<p className="text-gray-500 mt-2">Tag analytics will appear here</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,50 +1,70 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import type { ApiResponse, EventsQueryParams, EventType } from '@/lib/types';
|
||||
import {
|
||||
getEvents,
|
||||
getEventsSummary,
|
||||
getTimeSeriesData,
|
||||
getGeoAnalytics,
|
||||
getDeviceAnalytics
|
||||
} from '@/lib/analytics';
|
||||
import { getEvents, EventsQueryParams } from '@/lib/analytics';
|
||||
import { ApiResponse } from '@/lib/types';
|
||||
|
||||
// 获取事件列表
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
// 获取查询参数
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(searchParams.get('pageSize') || '20');
|
||||
const eventType = searchParams.get('eventType') || undefined;
|
||||
const linkId = searchParams.get('linkId') || undefined;
|
||||
const linkSlug = searchParams.get('linkSlug') || undefined;
|
||||
const userId = searchParams.get('userId') || undefined;
|
||||
|
||||
// 获取可能存在的多个团队、项目和标签ID
|
||||
const teamIds = searchParams.getAll('teamId');
|
||||
const projectIds = searchParams.getAll('projectId');
|
||||
const tagIds = searchParams.getAll('tagId');
|
||||
|
||||
const startTime = searchParams.get('startTime') || undefined;
|
||||
const endTime = searchParams.get('endTime') || undefined;
|
||||
const sortBy = searchParams.get('sortBy') || undefined;
|
||||
const sortOrder = (searchParams.get('sortOrder') as 'asc' | 'desc') || undefined;
|
||||
|
||||
console.log("API接收到的tagIds:", tagIds); // 添加日志便于调试
|
||||
|
||||
// 获取事件列表
|
||||
const params: EventsQueryParams = {
|
||||
startTime: searchParams.get('startTime') || undefined,
|
||||
endTime: searchParams.get('endTime') || undefined,
|
||||
eventType: searchParams.get('eventType') as EventType || undefined,
|
||||
linkId: searchParams.get('linkId') || undefined,
|
||||
linkSlug: searchParams.get('linkSlug') || undefined,
|
||||
userId: searchParams.get('userId') || undefined,
|
||||
teamId: searchParams.get('teamId') || undefined,
|
||||
projectId: searchParams.get('projectId') || undefined,
|
||||
page: searchParams.has('page') ? parseInt(searchParams.get('page')!, 10) : 1,
|
||||
pageSize: searchParams.has('pageSize') ? parseInt(searchParams.get('pageSize')!, 10) : 20,
|
||||
sortBy: searchParams.get('sortBy') || undefined,
|
||||
sortOrder: (searchParams.get('sortOrder') as 'asc' | 'desc') || undefined
|
||||
page,
|
||||
pageSize,
|
||||
eventType,
|
||||
linkId,
|
||||
linkSlug,
|
||||
userId,
|
||||
teamId: teamIds.length > 0 ? teamIds[0] : undefined,
|
||||
teamIds: teamIds.length > 1 ? teamIds : undefined,
|
||||
projectId: projectIds.length > 0 ? projectIds[0] : undefined,
|
||||
projectIds: projectIds.length > 1 ? projectIds : undefined,
|
||||
tagIds: tagIds.length > 0 ? tagIds : undefined,
|
||||
startTime,
|
||||
endTime,
|
||||
sortBy,
|
||||
sortOrder
|
||||
};
|
||||
|
||||
const { events, total } = await getEvents(params);
|
||||
|
||||
const response: ApiResponse<typeof events> = {
|
||||
|
||||
const result = await getEvents(params);
|
||||
|
||||
const response: ApiResponse<typeof result.events> = {
|
||||
success: true,
|
||||
data: events,
|
||||
data: result.events,
|
||||
meta: {
|
||||
total,
|
||||
page: params.page,
|
||||
pageSize: params.pageSize
|
||||
total: result.total,
|
||||
page,
|
||||
pageSize
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
console.error('获取事件列表失败:', error);
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
data: null,
|
||||
error: error instanceof Error ? error.message : '获取事件列表失败'
|
||||
};
|
||||
return NextResponse.json(response, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -46,18 +46,47 @@ export function TagSelector({
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const selectorRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Initialize selected tags based on value prop
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
if (Array.isArray(value)) {
|
||||
setSelectedIds(value);
|
||||
} else {
|
||||
setSelectedIds(value ? [value] : []);
|
||||
}
|
||||
} else {
|
||||
setSelectedIds([]);
|
||||
// 标签名称与ID的映射函数
|
||||
const getTagIdByName = (name: string): string | undefined => {
|
||||
const tag = tags.find(t => t.name === name);
|
||||
return tag?.id;
|
||||
};
|
||||
|
||||
const getTagNameById = (id: string): string | undefined => {
|
||||
const tag = tags.find(t => t.id === id);
|
||||
return tag?.name;
|
||||
};
|
||||
|
||||
// 从标签名称转换为标签ID
|
||||
const nameToId = (nameOrNames: string | string[] | undefined): string[] => {
|
||||
if (!nameOrNames) return [];
|
||||
if (Array.isArray(nameOrNames)) {
|
||||
return nameOrNames
|
||||
.map(name => getTagIdByName(name))
|
||||
.filter((id): id is string => !!id);
|
||||
}
|
||||
}, [value]);
|
||||
const id = getTagIdByName(nameOrNames);
|
||||
return id ? [id] : [];
|
||||
};
|
||||
|
||||
// 从标签ID转换为标签名称
|
||||
const idToName = (idOrIds: string | string[] | undefined): string[] => {
|
||||
if (!idOrIds) return [];
|
||||
if (Array.isArray(idOrIds)) {
|
||||
return idOrIds
|
||||
.map(id => getTagNameById(id))
|
||||
.filter((name): name is string => !!name);
|
||||
}
|
||||
const name = getTagNameById(idOrIds);
|
||||
return name ? [name] : [];
|
||||
};
|
||||
|
||||
// 初始化已选择的标签 - 从传入的名称转换为ID
|
||||
useEffect(() => {
|
||||
if (tags.length > 0 && value) {
|
||||
setSelectedIds(nameToId(value));
|
||||
}
|
||||
}, [value, tags]);
|
||||
|
||||
// Add click outside listener to close dropdown
|
||||
useEffect(() => {
|
||||
@@ -167,8 +196,10 @@ export function TagSelector({
|
||||
|
||||
setSelectedIds(newSelected);
|
||||
|
||||
// 传递标签名称而不是ID
|
||||
if (onChange) {
|
||||
onChange(multiple ? newSelected : newSelected[0] || '');
|
||||
const tagNames = idToName(newSelected);
|
||||
onChange(multiple ? tagNames : tagNames[0] || '');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -176,8 +207,11 @@ export function TagSelector({
|
||||
e.stopPropagation();
|
||||
const newSelected = selectedIds.filter(id => id !== tagId);
|
||||
setSelectedIds(newSelected);
|
||||
|
||||
// 传递标签名称而不是ID
|
||||
if (onChange) {
|
||||
onChange(multiple ? newSelected : newSelected[0] || '');
|
||||
const tagNames = idToName(newSelected);
|
||||
onChange(multiple ? tagNames : tagNames[0] || '');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -214,6 +248,7 @@ export function TagSelector({
|
||||
);
|
||||
}
|
||||
|
||||
// 根据已选择的ID筛选出已选择的标签
|
||||
const selectedTags = tags.filter(tag => selectedIds.includes(tag.id));
|
||||
|
||||
return (
|
||||
|
||||
20
app/page.tsx
20
app/page.tsx
@@ -322,11 +322,11 @@ export default function HomePage() {
|
||||
{selectedTagIds.length === 1 ? 'Tag filter:' : 'Tags filter:'}
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedTagIds.map(tagId => (
|
||||
<span key={tagId} className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
|
||||
{tagId}
|
||||
{selectedTagIds.map(tagName => (
|
||||
<span key={tagName} className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
|
||||
{tagName}
|
||||
<button
|
||||
onClick={() => setSelectedTagIds(selectedTagIds.filter(id => id !== tagId))}
|
||||
onClick={() => setSelectedTagIds(selectedTagIds.filter(name => name !== tagName))}
|
||||
className="ml-1 text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
×
|
||||
@@ -350,7 +350,7 @@ export default function HomePage() {
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Recent Events</h2>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
@@ -473,19 +473,19 @@ export default function HomePage() {
|
||||
<p className="text-2xl font-semibold text-gray-900">
|
||||
{typeof summary.totalEvents === 'number' ? summary.totalEvents.toLocaleString() : summary.totalEvents}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h3 className="text-sm font-medium text-gray-500">Unique Visitors</h3>
|
||||
<p className="text-2xl font-semibold text-gray-900">
|
||||
{typeof summary.uniqueVisitors === 'number' ? summary.uniqueVisitors.toLocaleString() : summary.uniqueVisitors}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h3 className="text-sm font-medium text-gray-500">Total Conversions</h3>
|
||||
<p className="text-2xl font-semibold text-gray-900">
|
||||
{typeof summary.totalConversions === 'number' ? summary.totalConversions.toLocaleString() : summary.totalConversions}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h3 className="text-sm font-medium text-gray-500">Avg. Time Spent</h3>
|
||||
<p className="text-2xl font-semibold text-gray-900">
|
||||
@@ -510,8 +510,8 @@ export default function HomePage() {
|
||||
<div className="bg-white rounded-lg shadow p-6 mb-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Geographic Distribution</h2>
|
||||
<GeoAnalytics data={geoData} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user