From 48d5bdafa43e38402cad4a9abfcbcf4d1e6d6874 Mon Sep 17 00:00:00 2001 From: William Tso Date: Thu, 10 Apr 2025 17:19:40 +0800 Subject: [PATCH] click subpath --- app/analytics/page.tsx | 41 +++++++++++++++++++++- app/api/events/devices/route.ts | 4 ++- app/api/events/geo/route.ts | 4 ++- app/api/events/summary/route.ts | 5 ++- app/api/events/time-series/route.ts | 4 ++- app/api/events/utm/route.ts | 7 ++++ app/components/analytics/PathAnalytics.tsx | 20 +++++++++-- app/components/analytics/UtmAnalytics.tsx | 6 ++-- lib/analytics.ts | 5 +++ lib/clickhouse.ts | 9 ++++- 10 files changed, 95 insertions(+), 10 deletions(-) diff --git a/app/analytics/page.tsx b/app/analytics/page.tsx index 350120f..b8f90fe 100644 --- a/app/analytics/page.tsx +++ b/app/analytics/page.tsx @@ -292,6 +292,8 @@ function AnalyticsContent() { const [selectedProjectIds, setSelectedProjectIds] = useState([]); // 添加标签名称状态 - 用于在UI中显示和API请求 const [selectedTagNames, setSelectedTagNames] = useState([]); + // 添加子路径筛选状态 + const [selectedSubpath, setSelectedSubpath] = useState(''); // 添加分页状态 const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(10); @@ -377,6 +379,11 @@ function AnalyticsContent() { params.append('tagName', tagName); }); } + + // 添加子路径筛选参数 + if (selectedSubpath) { + params.append('subpath', selectedSubpath); + } // 记录构建的 URL,以确保参数正确包含 const summaryUrl = `${baseUrl}/summary?${params.toString()}`; @@ -446,7 +453,7 @@ function AnalyticsContent() { }; fetchData(); - }, [dateRange, selectedTeamIds, selectedProjectIds, selectedTagNames, currentPage, pageSize, selectedShortUrl, shouldFetchData]); + }, [dateRange, selectedTeamIds, selectedProjectIds, selectedTagNames, selectedSubpath, currentPage, pageSize, selectedShortUrl, shouldFetchData]); // Function to clear the shorturl filter const handleClearShortUrlFilter = () => { @@ -467,6 +474,19 @@ function AnalyticsContent() { window.location.href = newUrl.toString(); }; + // 清除子路径筛选 + const handleClearSubpathFilter = () => { + setSelectedSubpath(''); + }; + + // 处理子路径点击 + const handlePathClick = (path: string) => { + console.log('Path clicked:', path); + setSelectedSubpath(path); + // 重置到第一页 + setCurrentPage(1); + }; + if (loading) { return (
@@ -553,6 +573,23 @@ function AnalyticsContent() {
)} + {/* 如果有选定的 subpath,显示提示 */} + {selectedSubpath && ( +
+
+ Filtered by Channel: + {selectedSubpath} +
+ +
+ )} + {/* 只在没有选中 shorturl 时显示筛选选择器 */} {!selectedShortUrl && ( <> @@ -670,6 +707,7 @@ function AnalyticsContent() { projectIds={selectedProjectIds} tagIds={selectedTagNames} linkId={selectedShortUrl?.externalId} + subpath={selectedSubpath} /> @@ -681,6 +719,7 @@ function AnalyticsContent() { startTime={format(dateRange.from, "yyyy-MM-dd'T'HH:mm:ss'Z'")} endTime={format(dateRange.to, "yyyy-MM-dd'T'HH:mm:ss'Z'")} linkId={selectedShortUrl.externalId} + onPathClick={handlePathClick} /> )} diff --git a/app/api/events/devices/route.ts b/app/api/events/devices/route.ts index 3d4d2b2..fe2a847 100644 --- a/app/api/events/devices/route.ts +++ b/app/api/events/devices/route.ts @@ -18,7 +18,9 @@ export async function GET(request: NextRequest) { // 添加团队、项目和标签筛选 teamIds: teamIds.length > 0 ? teamIds : undefined, projectIds: projectIds.length > 0 ? projectIds : undefined, - tagIds: tagIds.length > 0 ? tagIds : undefined + tagIds: tagIds.length > 0 ? tagIds : undefined, + // 添加子路径筛选 + subpath: searchParams.get('subpath') || undefined }); const response: ApiResponse = { diff --git a/app/api/events/geo/route.ts b/app/api/events/geo/route.ts index e9bafdf..b0fc3ad 100644 --- a/app/api/events/geo/route.ts +++ b/app/api/events/geo/route.ts @@ -22,7 +22,9 @@ export async function GET(request: NextRequest) { // 添加团队、项目和标签筛选 teamIds: teamIds.length > 0 ? teamIds : undefined, projectIds: projectIds.length > 0 ? projectIds : undefined, - tagIds: tagIds.length > 0 ? tagIds : undefined + tagIds: tagIds.length > 0 ? tagIds : undefined, + // 添加子路径筛选 + subpath: searchParams.get('subpath') || undefined }); const response: ApiResponse = { diff --git a/app/api/events/summary/route.ts b/app/api/events/summary/route.ts index 3404bff..5f91a24 100644 --- a/app/api/events/summary/route.ts +++ b/app/api/events/summary/route.ts @@ -13,7 +13,9 @@ export async function GET(request: NextRequest) { // Add debug log to check if linkId is being received const linkId = searchParams.get('linkId'); + const subpath = searchParams.get('subpath'); console.log('Summary API received linkId:', linkId); + console.log('Summary API received subpath:', subpath); console.log('Summary API full parameters:', Object.fromEntries(searchParams.entries())); console.log('Summary API URL:', request.url); @@ -23,7 +25,8 @@ export async function GET(request: NextRequest) { linkId: searchParams.get('linkId') || undefined, teamIds: teamIds.length > 0 ? teamIds : undefined, projectIds: projectIds.length > 0 ? projectIds : undefined, - tagIds: tagIds.length > 0 ? tagIds : undefined + tagIds: tagIds.length > 0 ? tagIds : undefined, + subpath: searchParams.get('subpath') || undefined }); const response: ApiResponse = { diff --git a/app/api/events/time-series/route.ts b/app/api/events/time-series/route.ts index 6f0a0c5..2d1a1c8 100644 --- a/app/api/events/time-series/route.ts +++ b/app/api/events/time-series/route.ts @@ -28,7 +28,9 @@ export async function GET(request: NextRequest) { // 添加团队、项目和标签筛选 teamIds: teamIds.length > 0 ? teamIds : undefined, projectIds: projectIds.length > 0 ? projectIds : undefined, - tagIds: tagIds.length > 0 ? tagIds : undefined + tagIds: tagIds.length > 0 ? tagIds : undefined, + // 添加子路径筛选 + subpath: searchParams.get('subpath') || undefined }); const response: ApiResponse = { diff --git a/app/api/events/utm/route.ts b/app/api/events/utm/route.ts index a9b1f4f..daa0faf 100644 --- a/app/api/events/utm/route.ts +++ b/app/api/events/utm/route.ts @@ -24,6 +24,7 @@ export async function GET(request: NextRequest) { const startTime = searchParams.get('startTime'); const endTime = searchParams.get('endTime'); const linkId = searchParams.get('linkId'); + const subpath = searchParams.get('subpath'); // 获取团队、项目和标签筛选参数 const teamIds = searchParams.getAll('teamId'); @@ -39,6 +40,7 @@ export async function GET(request: NextRequest) { startTime, endTime, linkId, + subpath, teamIds, projectIds, tagIds, @@ -63,6 +65,11 @@ export async function GET(request: NextRequest) { conditions.push(`link_id = '${linkId}'`); } + // 添加子路径筛选 + if (subpath) { + conditions.push(`positionCaseInsensitive(url, '/${subpath}') > 0`); + } + // 添加团队筛选 if (teamIds && teamIds.length > 0) { // 如果只有一个团队ID diff --git a/app/components/analytics/PathAnalytics.tsx b/app/components/analytics/PathAnalytics.tsx index 829513d..379fc38 100644 --- a/app/components/analytics/PathAnalytics.tsx +++ b/app/components/analytics/PathAnalytics.tsx @@ -4,6 +4,7 @@ interface PathAnalyticsProps { startTime: string; endTime: string; linkId?: string; + onPathClick?: (path: string) => void; } interface PathData { @@ -12,7 +13,7 @@ interface PathData { percentage: number; } -const PathAnalytics: React.FC = ({ startTime, endTime, linkId }) => { +const PathAnalytics: React.FC = ({ startTime, endTime, linkId, onPathClick }) => { const [loading, setLoading] = useState(true); const [pathData, setPathData] = useState([]); const [error, setError] = useState(null); @@ -83,6 +84,13 @@ const PathAnalytics: React.FC = ({ startTime, endTime, linkI fetchPathData(); }, [startTime, endTime, linkId]); + const handlePathClick = (path: string, e: React.MouseEvent) => { + e.preventDefault(); + if (onPathClick) { + onPathClick(path); + } + }; + if (loading) { return
@@ -118,7 +126,15 @@ const PathAnalytics: React.FC = ({ startTime, endTime, linkI {pathData.map((item, index) => ( - {item.path} + + handlePathClick(item.path, e)} + > + {item.path} + + {item.count}
diff --git a/app/components/analytics/UtmAnalytics.tsx b/app/components/analytics/UtmAnalytics.tsx index 7b05a1e..fd60f71 100644 --- a/app/components/analytics/UtmAnalytics.tsx +++ b/app/components/analytics/UtmAnalytics.tsx @@ -18,9 +18,10 @@ interface UtmAnalyticsProps { teamIds?: string[]; projectIds?: string[]; tagIds?: string[]; + subpath?: string; } -export default function UtmAnalytics({ startTime, endTime, linkId, teamIds, projectIds, tagIds }: UtmAnalyticsProps) { +export default function UtmAnalytics({ startTime, endTime, linkId, teamIds, projectIds, tagIds, subpath }: UtmAnalyticsProps) { const [activeTab, setActiveTab] = useState('source'); const [utmData, setUtmData] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -38,6 +39,7 @@ export default function UtmAnalytics({ startTime, endTime, linkId, teamIds, proj if (startTime) params.append('startTime', startTime); if (endTime) params.append('endTime', endTime); if (linkId) params.append('linkId', linkId); + if (subpath) params.append('subpath', subpath); params.append('utmType', activeTab); // 添加团队ID参数 @@ -78,7 +80,7 @@ export default function UtmAnalytics({ startTime, endTime, linkId, teamIds, proj }; fetchUtmData(); - }, [activeTab, startTime, endTime, linkId, teamIds, projectIds, tagIds]); + }, [activeTab, startTime, endTime, linkId, teamIds, projectIds, tagIds, subpath]); // 安全地格式化数字 const formatNumber = (value: number | undefined | null): string => { diff --git a/lib/analytics.ts b/lib/analytics.ts index ba6e199..78e5d8d 100644 --- a/lib/analytics.ts +++ b/lib/analytics.ts @@ -22,6 +22,7 @@ export interface EventsQueryParams { teamIds?: string[]; projectIds?: string[]; tagIds?: string[]; + subpath?: string; page?: number; pageSize?: number; sortBy?: string; @@ -66,6 +67,7 @@ export async function getEventsSummary(params: { teamIds?: string[]; projectIds?: string[]; tagIds?: string[]; + subpath?: string; }): Promise { console.log('getEventsSummary received params:', params); const filter = buildFilter(params); @@ -186,6 +188,7 @@ export async function getTimeSeriesData(params: { teamIds?: string[]; projectIds?: string[]; tagIds?: string[]; + subpath?: string; }): Promise { const filter = buildFilter(params); @@ -221,6 +224,7 @@ export async function getGeoAnalytics(params: { teamIds?: string[]; projectIds?: string[]; tagIds?: string[]; + subpath?: string; }): Promise { const filter = buildFilter(params); @@ -257,6 +261,7 @@ export async function getDeviceAnalytics(params: { teamIds?: string[]; projectIds?: string[]; tagIds?: string[]; + subpath?: string; }): Promise { const filter = buildFilter(params); diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index 88e6dd9..c057d08 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -1,5 +1,5 @@ import { createClient } from '@clickhouse/client'; -import type { EventsQueryParams } from './types'; +import { EventsQueryParams } from './analytics'; // ClickHouse 客户端配置 const clickhouse = createClient({ @@ -58,6 +58,13 @@ export function buildFilter(params: Partial): string { filters.push(`user_id = '${params.userId}'`); } + // 添加子路径过滤条件 + if (params.subpath) { + console.log('Adding subpath filter:', params.subpath); + // 使用 url 字段和字符串函数替代不存在的 path 字段 + filters.push(`positionCaseInsensitive(url, '/${params.subpath}') > 0`); + } + // 添加团队ID过滤条件 if (params.teamId) { filters.push(`team_id = '${params.teamId}'`);