diff --git a/app/(app)/analytics/page.tsx b/app/(app)/analytics/page.tsx deleted file mode 100644 index d6dc856..0000000 --- a/app/(app)/analytics/page.tsx +++ /dev/null @@ -1,126 +0,0 @@ -"use client"; - -import { useState } from 'react'; -import { subDays } from 'date-fns'; -import { DateRangePicker } from '@/app/components/ui/DateRangePicker'; -import { TeamSelector } from '@/app/components/ui/TeamSelector'; -import { ProjectSelector } from '@/app/components/ui/ProjectSelector'; -import { TagSelector } from '@/app/components/ui/TagSelector'; - -export default function AnalyticsPage() { - // 默认日期范围为最近7天 - const today = new Date(); - const [dateRange, setDateRange] = useState({ - from: subDays(today, 7), // 7天前 - to: today // 今天 - }); - - // 添加团队选择状态 - 使用数组支持多选 - const [selectedTeamIds, setSelectedTeamIds] = useState([]); - - // 添加项目选择状态 - 使用数组支持多选 - const [selectedProjectIds, setSelectedProjectIds] = useState([]); - - // 添加标签选择状态 - 使用数组支持多选 - const [selectedTagIds, setSelectedTagIds] = useState([]); - - // 分析是否有任何选择 - const hasNoSelection = selectedTeamIds.length === 0 && - selectedProjectIds.length === 0 && - selectedTagIds.length === 0; - - return ( -
-
-

Analytics

- -
- setSelectedTeamIds(Array.isArray(value) ? value : [value])} - className="w-[250px]" - multiple={true} - /> - setSelectedProjectIds(Array.isArray(value) ? value : [value])} - className="w-[250px]" - multiple={true} - teamId={selectedTeamIds.length === 1 ? selectedTeamIds[0] : undefined} - /> - setSelectedTagIds(Array.isArray(value) ? value : [value])} - className="w-[250px]" - multiple={true} - teamId={selectedTeamIds.length === 1 ? selectedTeamIds[0] : undefined} - /> - -
-
- - {/* 如果没有选择任何项,显示提示信息 */} - {hasNoSelection && ( -
-

- Please select teams, projects, or tags to view analytics -

-
- )} - - {/* 显示团队相关的分析数据 */} - {selectedTeamIds.length > 0 && ( -
-

- Team Analytics ({selectedTeamIds.length} selected) -

-
- {selectedTeamIds.map((teamId) => ( -
-

Team ID: {teamId}

-

Team analytics will appear here

-
- ))} -
-
- )} - - {/* 显示项目相关的分析数据 */} - {selectedProjectIds.length > 0 && ( -
-

- Project Analytics ({selectedProjectIds.length} selected) -

-
- {selectedProjectIds.map((projectId) => ( -
-

Project ID: {projectId}

-

Project analytics will appear here

-
- ))} -
-
- )} - - {/* 显示标签相关的分析数据 */} - {selectedTagIds.length > 0 && ( -
-

- Tag Analytics ({selectedTagIds.length} selected) -

-
- {selectedTagIds.map((tagName) => ( -
-

Tag: {tagName}

-

Tag analytics will appear here

-
- ))} -
-
- )} -
- ); -} \ No newline at end of file diff --git a/app/components/ui/ProjectSelector.tsx b/app/components/ui/ProjectSelector.tsx index af53ba5..2d7b6f0 100644 --- a/app/components/ui/ProjectSelector.tsx +++ b/app/components/ui/ProjectSelector.tsx @@ -18,6 +18,14 @@ interface Project { deleted_at?: string | null; schema_version?: number | null; creator_id?: string | null; + team_name?: string; +} + +// 添加需要的类型定义 +interface ProjectWithTeam { + project_id: string; + projects: Project; + teams?: { name: string }; } // ProjectSelector component with multi-select support @@ -85,14 +93,14 @@ export function ProjectSelector({ let projectsQuery; if (teamId) { - // If a teamId is provided, fetch projects for that team + // 如果提供了teamId,获取该团队的项目 projectsQuery = supabase .from('team_projects') - .select('project_id, projects:project_id(*)') + .select('project_id, projects:project_id(*), teams:team_id(name)') .eq('team_id', teamId) .is('projects.deleted_at', null); } else { - // Otherwise, fetch projects the user is a member of + // 否则,获取用户所属的所有项目及其所属团队 projectsQuery = supabase .from('user_projects') .select('project_id, projects:project_id(*)') @@ -109,17 +117,59 @@ export function ProjectSelector({ return; } - // Extract the project data from the query results - if (isMounted && projectsData && projectsData.length > 0) { - const projectList: Project[] = []; + // 如果没有提供teamId,需要单独获取每个项目对应的团队信息 + if (!teamId && projectsData.length > 0) { + const projectIds = projectsData.map(item => item.project_id); - for (const item of projectsData) { - if (item.projects && typeof item.projects === 'object' && 'id' in item.projects && 'name' in item.projects) { - projectList.push(item.projects as Project); - } + // 获取项目所属的团队信息 + const { data: teamProjectsData, error: teamProjectsError } = await supabase + .from('team_projects') + .select('project_id, teams:team_id(name)') + .in('project_id', projectIds); + + if (teamProjectsError) throw teamProjectsError; + + // 创建项目ID到团队名称的映射 + const projectTeamMap: Record = {}; + if (teamProjectsData) { + teamProjectsData.forEach(item => { + if (item.teams && typeof item.teams === 'object' && 'name' in item.teams) { + projectTeamMap[item.project_id] = (item.teams as { name: string }).name; + } + }); } - setProjects(projectList); + // 提取项目数据,并添加团队名称 + if (isMounted && projectsData) { + const projectList: Project[] = []; + + for (const item of projectsData) { + if (item.projects && typeof item.projects === 'object' && 'id' in item.projects && 'name' in item.projects) { + const project = item.projects as Project; + project.team_name = projectTeamMap[project.id]; + projectList.push(project); + } + } + + setProjects(projectList); + } + } else { + // 如果提供了teamId,直接从查询结果中提取项目和团队信息 + if (isMounted && projectsData) { + const projectList: Project[] = []; + + for (const item of projectsData as ProjectWithTeam[]) { + if (item.projects && typeof item.projects === 'object' && 'id' in item.projects && 'name' in item.projects) { + const project = item.projects as Project; + if (item.teams && 'name' in item.teams) { + project.team_name = item.teams.name; + } + projectList.push(project); + } + } + + setProjects(projectList); + } } } catch (err) { if (isMounted) { @@ -274,6 +324,11 @@ export function ProjectSelector({ > {project.name} + {project.team_name && ( + + {project.team_name} + + )} {project.description && ( {project.description} diff --git a/app/page.tsx b/app/page.tsx index 9c69539..c343440 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -36,6 +36,7 @@ interface Event { link_id?: string; link_slug?: string; link_tags?: string; + ip_address?: string; } // 格式化日期函数 @@ -98,6 +99,7 @@ const extractEventInfo = (event: Event) => { eventType: event.event_type || '-', visitorId: event.visitor_id?.substring(0, 8) || '-', referrer: eventAttrs?.referrer || '-', + ipAddress: event.ip_address || '-', location: event.country ? (event.city ? `${event.city}, ${event.country}` : event.country) : '-', device: event.device_type || '-', browser: event.browser || '-', @@ -124,6 +126,11 @@ export default function HomePage() { // 添加标签选择状态 - 使用数组支持多选 const [selectedTagIds, setSelectedTagIds] = useState([]); + // 添加分页状态 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [totalEvents, setTotalEvents] = useState(0); + const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [summary, setSummary] = useState(null); @@ -145,7 +152,9 @@ export default function HomePage() { const baseUrl = '/api/events'; const params = new URLSearchParams({ startTime, - endTime + endTime, + page: currentPage.toString(), + pageSize: pageSize.toString() }); // 添加团队ID参数 - 支持多个团队 @@ -197,6 +206,15 @@ export default function HomePage() { setGeoData(geoData.data); setDeviceData(deviceData.data); setEvents(eventsData.data || []); + + // 设置总事件数量用于分页 + if (eventsData.meta) { + // 确保将total转换为数字,无论它是字符串还是数字 + const totalCount = parseInt(String(eventsData.meta.total), 10); + if (!isNaN(totalCount)) { + setTotalEvents(totalCount); + } + } } catch (err) { setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); } finally { @@ -205,7 +223,7 @@ export default function HomePage() { }; fetchData(); - }, [dateRange, selectedTeamIds, selectedProjectIds, selectedTagIds]); + }, [dateRange, selectedTeamIds, selectedProjectIds, selectedTagIds, currentPage, pageSize]); if (loading) { return ( @@ -345,8 +363,38 @@ export default function HomePage() { )} - {/* 事件列表部分 - 现在放在最上面 */} -
+ {/* 仪表板内容 - 现在放在事件列表之后 */} + <> + {summary && ( +
+
+

Total Events

+

+ {typeof summary.totalEvents === 'number' ? summary.totalEvents.toLocaleString() : summary.totalEvents} +

+
+
+

Unique Visitors

+

+ {typeof summary.uniqueVisitors === 'number' ? summary.uniqueVisitors.toLocaleString() : summary.uniqueVisitors} +

+
+
+

Total Conversions

+

+ {typeof summary.totalConversions === 'number' ? summary.totalConversions.toLocaleString() : summary.totalConversions} +

+
+
+

Avg. Time Spent

+

+ {summary.averageTimeSpent?.toFixed(1) || '0'}s +

+
+
+ )} + +

Recent Events

@@ -376,6 +424,9 @@ export default function HomePage() { Team/Project + + IP/Location + Device Info @@ -433,6 +484,18 @@ export default function HomePage() {
{info.teamName}
{info.projectName}
+ +
+ + IP: + {info.ipAddress} + + + Location: + {info.location} + +
+
@@ -462,38 +525,217 @@ export default function HomePage() { No events found
)} -
- - {/* 仪表板内容 - 现在放在事件列表之后 */} - <> - {summary && ( -
-
-

Total Events

-

- {typeof summary.totalEvents === 'number' ? summary.totalEvents.toLocaleString() : summary.totalEvents} -

-
-
-

Unique Visitors

-

- {typeof summary.uniqueVisitors === 'number' ? summary.uniqueVisitors.toLocaleString() : summary.uniqueVisitors} -

-
-
-

Total Conversions

-

- {typeof summary.totalConversions === 'number' ? summary.totalConversions.toLocaleString() : summary.totalConversions} -

+ + {/* 分页控件 - 删除totalEvents > 0条件,改为events.length > 0 */} + {!loading && events.length > 0 && ( +
+
+ + +
+
+
+

+ Showing {events.length > 0 ? ((currentPage - 1) * pageSize) + 1 : 0} to {events.length > 0 ? ((currentPage - 1) * pageSize) + events.length : 0} of{' '} + {totalEvents} results +

+
+
+
+ +
+ + {/* 添加直接跳转到指定页的输入框 */} +
+ Go to: + { + const page = parseInt(e.target.value); + if (!isNaN(page) && page >= 1 && page <= Math.ceil(totalEvents / pageSize)) { + setCurrentPage(page); + } + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + const input = e.target as HTMLInputElement; + const page = parseInt(input.value); + if (!isNaN(page) && page >= 1 && page <= Math.ceil(totalEvents / pageSize)) { + setCurrentPage(page); + } + } + }} + className="w-16 px-3 py-1 border border-gray-300 rounded-md text-sm" + /> + + of {Math.max(1, Math.ceil(totalEvents / pageSize))} + +
+ +
-
-

Avg. Time Spent

-

- {summary.averageTimeSpent?.toFixed(1) || '0'}s -

)} +

Event Trends

diff --git a/lib/analytics.ts b/lib/analytics.ts index 4f0f2e8..b0ac662 100644 --- a/lib/analytics.ts +++ b/lib/analytics.ts @@ -73,7 +73,7 @@ export async function getEventsSummary(params: { const baseQuery = ` SELECT count() as totalEvents, - uniq(visitor_id) as uniqueVisitors, + uniq(ip_address) as uniqueVisitors, countIf(event_type = 'conversion') as totalConversions, avg(time_spent_sec) as averageTimeSpent, @@ -196,7 +196,7 @@ export async function getTimeSeriesData(params: { SELECT toStartOfInterval(event_time, INTERVAL ${interval}) as timestamp, count() as events, - uniq(visitor_id) as visitors, + uniq(ip_address) as visitors, countIf(event_type = 'conversion') as conversions FROM events ${filter} @@ -221,7 +221,7 @@ export async function getGeoAnalytics(params: { SELECT ${groupByField} as location, count() as visits, - uniq(visitor_id) as visitors, + uniq(ip_address) as visitors, count() * 100.0 / sum(count()) OVER () as percentage FROM events ${filter}