From db70602e9fc260e3e2905836dab073786e94a13e Mon Sep 17 00:00:00 2001 From: William Tso Date: Tue, 8 Apr 2025 00:03:13 +0800 Subject: [PATCH] hide filter --- app/analytics/page.tsx | 415 ++++++++++++++++++++---------- app/api/events/summary/route.ts | 4 + app/api/shortlinks/byUrl/route.ts | 139 ++++++++++ app/utils/store.ts | 40 ++- lib/analytics.ts | 2 + lib/clickhouse.ts | 6 +- 6 files changed, 454 insertions(+), 152 deletions(-) create mode 100644 app/api/shortlinks/byUrl/route.ts diff --git a/app/analytics/page.tsx b/app/analytics/page.tsx index 12b7861..886afa2 100644 --- a/app/analytics/page.tsx +++ b/app/analytics/page.tsx @@ -13,6 +13,7 @@ import { ProjectSelector } from '@/app/components/ui/ProjectSelector'; import { TagSelector } from '@/app/components/ui/TagSelector'; import { useSearchParams } from 'next/navigation'; import { useShortUrlStore } from '@/app/utils/store'; +import { useRouter } from 'next/navigation'; // 事件类型定义 interface Event { @@ -121,23 +122,75 @@ export default function AnalyticsPage() { const shorturlParam = searchParams.get('shorturl'); // 使用 Zustand store - const { selectedShortUrl } = useShortUrlStore(); + const { selectedShortUrl, setSelectedShortUrl, clearSelectedShortUrl } = useShortUrlStore(); // 存储 shorturl 参数 const [selectedShortUrlString, setSelectedShortUrlString] = useState(null); - // 当 URL 参数变化时更新状态 + // 从 API 加载短链接数据 useEffect(() => { + // 处理 URL 参数 if (shorturlParam) { + // 保存参数到状态 setSelectedShortUrlString(shorturlParam); - console.log('Selected shorturl from URL:', shorturlParam); - // 已经通过 Zustand store 传递了完整数据 - if (selectedShortUrl) { - console.log('Complete shortUrl data from store:', selectedShortUrl); + // 如果 store 中没有选中的短链接或者 store 中的短链接与 URL 参数不匹配,则从 localStorage 更新 + if (!selectedShortUrl || selectedShortUrl.shortUrl !== shorturlParam) { + // 首先检查 localStorage 是否已有此数据 + const localStorageData = localStorage.getItem('shorturl-storage'); + + if (localStorageData) { + try { + const parsedData = JSON.parse(localStorageData); + if (parsedData.state?.selectedShortUrl && parsedData.state.selectedShortUrl.shortUrl === shorturlParam) { + // 数据已存在于 localStorage 且匹配 URL 参数,无需操作 + return; + } + } catch (e) { + console.error('Error parsing localStorage data:', e); + } + } + + // 如果 localStorage 中没有匹配的数据,则从 API 获取 + const fetchShortUrlData = async () => { + try { + // 构建带有 URL 参数的查询字符串 + const encodedUrl = encodeURIComponent(shorturlParam); + const apiUrl = `/api/shortlinks/byUrl?url=${encodedUrl}`; + + console.log('Fetching shorturl data from:', apiUrl); + + // 使用 API 端点获取短链接数据 + const response = await fetch(apiUrl); + + if (!response.ok) { + console.error('Failed to fetch shorturl data:', response.statusText); + return; + } + + const result = await response.json(); + + // 如果找到匹配的短链接数据 + if (result.success && result.data) { + console.log('Retrieved shortlink data:', result.data); + // 设置到 Zustand store (会自动更新到 localStorage) + setSelectedShortUrl(result.data); + } + } catch (error) { + console.error('Error fetching shorturl data:', error); + } + }; + + fetchShortUrlData(); } + } else { + // 如果 URL 没有参数,清除文本状态 + setSelectedShortUrlString(null); + + // 如果 URL 没有参数但 store 中有数据,我们保持 store 中的数据不变 + // 这样用户在清除 URL 参数后仍能看到之前选择的短链接数据 } - }, [shorturlParam, selectedShortUrl]); + }, [shorturlParam, selectedShortUrl, setSelectedShortUrl]); // 默认日期范围为最近7天 const today = new Date(); @@ -165,6 +218,11 @@ export default function AnalyticsPage() { const [deviceData, setDeviceData] = useState(null); const [events, setEvents] = useState([]); + const router = useRouter(); + + // 添加 Snackbar 状态 + const [isSnackbarOpen, setIsSnackbarOpen] = useState(false); + useEffect(() => { const fetchData = async () => { setLoading(true); @@ -183,6 +241,12 @@ export default function AnalyticsPage() { pageSize: pageSize.toString() }); + // Add linkId parameter if a shorturl is selected + if (selectedShortUrl && selectedShortUrl.id) { + params.append('linkId', selectedShortUrl.id); + console.log('Adding linkId to requests:', selectedShortUrl.id); + } + // 添加团队ID参数 - 支持多个团队 if (selectedTeamIds.length > 0) { selectedTeamIds.forEach(teamId => { @@ -249,7 +313,28 @@ export default function AnalyticsPage() { }; fetchData(); - }, [dateRange, selectedTeamIds, selectedProjectIds, selectedTagNames, currentPage, pageSize]); + }, [dateRange, selectedTeamIds, selectedProjectIds, selectedTagNames, currentPage, pageSize, selectedShortUrl]); + + // Function to clear the shorturl filter + const handleClearShortUrlFilter = () => { + // Clear the shorturl from Zustand store + clearSelectedShortUrl(); + + // Create a new URL object to manipulate the current URL + const currentUrl = new URL(window.location.href); + + // Remove the shorturl parameter + currentUrl.searchParams.delete('shorturl'); + + // Get all other parameters and preserve them + const newUrl = `/analytics${currentUrl.search}`; + + // Navigate to the updated URL + router.push(newUrl); + + // Show a message to the user + setIsSnackbarOpen(true); + }; if (loading) { return ( @@ -269,16 +354,43 @@ export default function AnalyticsPage() { return (
+ {/* Notification Snackbar */} + {isSnackbarOpen && ( +
+ URL filter cleared + +
+ )} +

Analytics Dashboard

{/* 如果有选定的 shorturl,可以显示一个提示,显示更多详细信息 */} {selectedShortUrl && (
-
- {selectedShortUrl.title || 'Untitled'} - - - {selectedShortUrl.shortUrl} +
+
+ {selectedShortUrl.title || 'Untitled'} + - + {selectedShortUrl.shortUrl} +
+ +
+
+ Analytics filtered for this short URL only + {selectedShortUrl.id && (ID: {selectedShortUrl.id})}
{selectedShortUrl.tags && selectedShortUrl.tags.length > 0 && (
@@ -294,51 +406,67 @@ export default function AnalyticsPage() { {/* 如果只有 URL 参数但没有完整数据,则显示简单提示 */} {selectedShortUrlString && !selectedShortUrl && ( -
- Filtered by Short URL: - {selectedShortUrlString} +
+
+ Filtered by Short URL: + {selectedShortUrlString} +
+
)} - { - const newTeamIds = Array.isArray(value) ? value : [value]; - - // Check if team selection has changed - if (JSON.stringify(newTeamIds) !== JSON.stringify(selectedTeamIds)) { - // Clear project selection when team changes - setSelectedProjectIds([]); - - // Update team selection - setSelectedTeamIds(newTeamIds); - } - }} - className="w-[250px]" - multiple={true} - /> - setSelectedProjectIds(Array.isArray(value) ? value : [value])} - className="w-[250px]" - multiple={true} - teamIds={selectedTeamIds.length > 0 ? selectedTeamIds : undefined} - /> - { - // TagSelector返回的是标签名称 - if (Array.isArray(value)) { - setSelectedTagNames(value); - } else { - setSelectedTagNames(value ? [value] : []); - } - // 我们需要将标签名称映射回ID,但由于TagSelector内部已经做了处理 - // 这里不需要额外的映射代码,selectedTagNames存储名称即可 - }} - className="w-[250px]" - multiple={true} - teamIds={selectedTeamIds.length > 0 ? selectedTeamIds : undefined} - /> + + {/* 只在没有选中 shorturl 时显示筛选选择器 */} + {!selectedShortUrl && ( + <> + { + const newTeamIds = Array.isArray(value) ? value : [value]; + + // Check if team selection has changed + if (JSON.stringify(newTeamIds) !== JSON.stringify(selectedTeamIds)) { + // Clear project selection when team changes + setSelectedProjectIds([]); + + // Update team selection + setSelectedTeamIds(newTeamIds); + } + }} + className="w-[250px]" + multiple={true} + /> + setSelectedProjectIds(Array.isArray(value) ? value : [value])} + className="w-[250px]" + multiple={true} + teamIds={selectedTeamIds.length > 0 ? selectedTeamIds : undefined} + /> + { + // TagSelector返回的是标签名称 + if (Array.isArray(value)) { + setSelectedTagNames(value); + } else { + setSelectedTagNames(value ? [value] : []); + } + // 我们需要将标签名称映射回ID,但由于TagSelector内部已经做了处理 + // 这里不需要额外的映射代码,selectedTagNames存储名称即可 + }} + className="w-[250px]" + multiple={true} + teamIds={selectedTeamIds.length > 0 ? selectedTeamIds : undefined} + /> + + )} +
- {/* 显示团队选择信息 */} - {selectedTeamIds.length > 0 && ( -
- - {selectedTeamIds.length === 1 ? 'Team filter:' : 'Teams filter:'} - -
- {selectedTeamIds.map(teamId => ( - - {teamId} - + {/* 仅在未选中 shorturl 且有选择的筛选条件时显示筛选条件标签 */} + {!selectedShortUrl && ( + <> + {/* 显示团队选择信息 */} + {selectedTeamIds.length > 0 && ( +
+ + {selectedTeamIds.length === 1 ? 'Team filter:' : 'Teams filter:'} - ))} - {selectedTeamIds.length > 0 && ( - - )} -
-
- )} +
+ {selectedTeamIds.map(teamId => ( + + {teamId} + + + ))} + {selectedTeamIds.length > 0 && ( + + )} +
+
+ )} - {/* 显示项目选择信息 */} - {selectedProjectIds.length > 0 && ( -
- - {selectedProjectIds.length === 1 ? 'Project filter:' : 'Projects filter:'} - -
- {selectedProjectIds.map(projectId => ( - - {projectId} - + {/* 显示项目选择信息 */} + {selectedProjectIds.length > 0 && ( +
+ + {selectedProjectIds.length === 1 ? 'Project filter:' : 'Projects filter:'} - ))} - {selectedProjectIds.length > 0 && ( - - )} -
-
- )} +
+ {selectedProjectIds.map(projectId => ( + + {projectId} + + + ))} + {selectedProjectIds.length > 0 && ( + + )} +
+
+ )} - {/* 显示标签选择信息 */} - {selectedTagNames.length > 0 && ( -
- - {selectedTagNames.length === 1 ? 'Tag filter:' : 'Tags filter:'} - -
- {selectedTagNames.map(tagName => ( - - {tagName} - + {/* 显示标签选择信息 */} + {selectedTagNames.length > 0 && ( +
+ + {selectedTagNames.length === 1 ? 'Tag filter:' : 'Tags filter:'} - ))} - {selectedTagNames.length > 0 && ( - - )} -
-
+
+ {selectedTagNames.map(tagName => ( + + {tagName} + + + ))} + {selectedTagNames.length > 0 && ( + + )} +
+
+ )} + )} {/* 仪表板内容 - 现在放在事件列表之后 */} diff --git a/app/api/events/summary/route.ts b/app/api/events/summary/route.ts index 6263f53..8430194 100644 --- a/app/api/events/summary/route.ts +++ b/app/api/events/summary/route.ts @@ -11,6 +11,10 @@ export async function GET(request: NextRequest) { const projectIds = searchParams.getAll('projectId'); const tagIds = searchParams.getAll('tagId'); + // Add debug log to check if linkId is being received + const linkId = searchParams.get('linkId'); + console.log('Summary API received linkId:', linkId); + const summary = await getEventsSummary({ startTime: searchParams.get('startTime') || undefined, endTime: searchParams.get('endTime') || undefined, diff --git a/app/api/shortlinks/byUrl/route.ts b/app/api/shortlinks/byUrl/route.ts new file mode 100644 index 0000000..f656ff9 --- /dev/null +++ b/app/api/shortlinks/byUrl/route.ts @@ -0,0 +1,139 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { executeQuery } from '@/lib/clickhouse'; +import type { ApiResponse } from '@/lib/types'; + +export async function GET(request: NextRequest) { + try { + // Get the url from query parameters + const searchParams = request.nextUrl.searchParams; + const url = searchParams.get('url'); + + if (!url) { + return NextResponse.json({ + success: false, + error: 'URL parameter is required' + }, { status: 400 }); + } + + console.log('Fetching shortlink by URL:', url); + + // Query to fetch a single shortlink by shortUrl in attributes + const query = ` + SELECT + id, + external_id, + type, + slug, + original_url, + title, + description, + attributes, + schema_version, + creator_id, + creator_email, + creator_name, + created_at, + updated_at, + deleted_at, + projects, + teams, + tags, + qr_codes AS qr_codes, + channels, + favorites, + expires_at, + click_count, + unique_visitors + FROM shorturl_analytics.shorturl + WHERE JSONHas(attributes, 'shortUrl') + AND JSONExtractString(attributes, 'shortUrl') = '${url}' + AND deleted_at IS NULL + LIMIT 1 + `; + + console.log('Executing query:', query); + + // Execute the query + const result = await executeQuery(query); + + // If no shortlink found with the specified URL + if (!Array.isArray(result) || result.length === 0) { + return NextResponse.json({ + success: false, + error: 'Shortlink not found' + }, { status: 404 }); + } + + // Process the shortlink data + const shortlink = result[0]; + + // Extract shortUrl from attributes + let shortUrl = ''; + try { + if (shortlink.attributes && typeof shortlink.attributes === 'string') { + const attributes = JSON.parse(shortlink.attributes); + shortUrl = attributes.shortUrl || ''; + } + } catch (e) { + console.error('Error parsing shortlink attributes:', e); + } + + // Process teams + let teams = []; + try { + if (shortlink.teams && typeof shortlink.teams === 'string') { + teams = JSON.parse(shortlink.teams); + } + } catch (e) { + console.error('Error parsing teams:', e); + } + + // Process tags + let tags = []; + try { + if (shortlink.tags && typeof shortlink.tags === 'string') { + tags = JSON.parse(shortlink.tags); + } + } catch (e) { + console.error('Error parsing tags:', e); + } + + // Process projects + let projects = []; + try { + if (shortlink.projects && typeof shortlink.projects === 'string') { + projects = JSON.parse(shortlink.projects); + } + } catch (e) { + console.error('Error parsing projects:', e); + } + + // Format the data to match what our store expects + const formattedShortlink = { + id: shortlink.id || '', + slug: shortlink.slug || '', + originalUrl: shortlink.original_url || '', + title: shortlink.title || '', + shortUrl: shortUrl, + teams: teams, + projects: projects, + tags: tags.map((tag) => tag.tag_name || ''), + createdAt: shortlink.created_at, + domain: new URL(shortUrl || 'https://example.com').hostname + }; + + const response: ApiResponse = { + success: true, + data: formattedShortlink + }; + + return NextResponse.json(response); + } catch (error) { + console.error('Error fetching shortlink by URL:', error); + const response: ApiResponse = { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred' + }; + return NextResponse.json(response, { status: 500 }); + } +} \ No newline at end of file diff --git a/app/utils/store.ts b/app/utils/store.ts index 5b714db..9feedea 100644 --- a/app/utils/store.ts +++ b/app/utils/store.ts @@ -1,4 +1,18 @@ import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +// Define interface for team, project and tag objects +interface TeamData { + team_id: string; + team_name: string; + [key: string]: unknown; +} + +interface ProjectData { + project_id: string; + project_name: string; + [key: string]: unknown; +} // 定义 ShortUrl 数据类型 export interface ShortUrlData { @@ -7,9 +21,9 @@ export interface ShortUrlData { originalUrl: string; title?: string; shortUrl: string; - teams?: any[]; - projects?: any[]; - tags?: any[]; + teams?: TeamData[]; + projects?: ProjectData[]; + tags?: string[]; createdAt?: string; domain?: string; } @@ -21,9 +35,17 @@ interface ShortUrlStore { clearSelectedShortUrl: () => void; } -// 创建 store -export const useShortUrlStore = create((set) => ({ - selectedShortUrl: null, - setSelectedShortUrl: (shortUrl) => set({ selectedShortUrl: shortUrl }), - clearSelectedShortUrl: () => set({ selectedShortUrl: null }), -})); +// 创建 store 并使用 persist 中间件保存到 localStorage +export const useShortUrlStore = create()( + persist( + (set) => ({ + selectedShortUrl: null, + setSelectedShortUrl: (shortUrl) => set({ selectedShortUrl: shortUrl }), + clearSelectedShortUrl: () => set({ selectedShortUrl: null }), + }), + { + name: 'shorturl-storage', // localStorage 中的 key 名称 + partialize: (state) => ({ selectedShortUrl: state.selectedShortUrl }), // 只持久化 selectedShortUrl + } + ) +); diff --git a/lib/analytics.ts b/lib/analytics.ts index bc24e9c..ba6e199 100644 --- a/lib/analytics.ts +++ b/lib/analytics.ts @@ -67,7 +67,9 @@ export async function getEventsSummary(params: { projectIds?: string[]; tagIds?: string[]; }): Promise { + console.log('getEventsSummary received params:', params); const filter = buildFilter(params); + console.log('getEventsSummary built filter:', filter); // 获取基本统计数据 const baseQuery = ` diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index d8dec24..88e6dd9 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -26,6 +26,7 @@ function buildDateFilter(startTime?: string, endTime?: string): string { // 构建通用过滤条件 export function buildFilter(params: Partial): string { + console.log('buildFilter received params:', JSON.stringify(params)); const filters = []; // 添加日期过滤条件 @@ -43,6 +44,7 @@ export function buildFilter(params: Partial): string { // 添加链接ID过滤条件 if (params.linkId) { + console.log('Adding link_id filter:', params.linkId); filters.push(`link_id = '${params.linkId}'`); } @@ -100,7 +102,7 @@ export function buildOrderBy(sortBy: string = 'event_time', sortOrder: string = // 执行查询 export async function executeQuery(query: string) { - console.log('执行查询:', query); // 查询日志 + console.log('Executing query:', query); // 查询日志 try { const resultSet = await clickhouse.query({ query, @@ -117,7 +119,7 @@ export async function executeQuery(query: string) { // 执行返回单一结果的查询 export async function executeQuerySingle(query: string) { - console.log('执行单一结果查询:', query); // 查询日志 + console.log('Executing single result query:', query); // 查询日志 try { const resultSet = await clickhouse.query({ query,