click subpath

This commit is contained in:
2025-04-10 17:19:40 +08:00
parent ace231b93f
commit 48d5bdafa4
10 changed files with 95 additions and 10 deletions

View File

@@ -292,6 +292,8 @@ function AnalyticsContent() {
const [selectedProjectIds, setSelectedProjectIds] = useState<string[]>([]); const [selectedProjectIds, setSelectedProjectIds] = useState<string[]>([]);
// 添加标签名称状态 - 用于在UI中显示和API请求 // 添加标签名称状态 - 用于在UI中显示和API请求
const [selectedTagNames, setSelectedTagNames] = useState<string[]>([]); const [selectedTagNames, setSelectedTagNames] = useState<string[]>([]);
// 添加子路径筛选状态
const [selectedSubpath, setSelectedSubpath] = useState<string>('');
// 添加分页状态 // 添加分页状态
const [currentPage, setCurrentPage] = useState<number>(1); const [currentPage, setCurrentPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10); const [pageSize, setPageSize] = useState<number>(10);
@@ -377,6 +379,11 @@ function AnalyticsContent() {
params.append('tagName', tagName); params.append('tagName', tagName);
}); });
} }
// 添加子路径筛选参数
if (selectedSubpath) {
params.append('subpath', selectedSubpath);
}
// 记录构建的 URL以确保参数正确包含 // 记录构建的 URL以确保参数正确包含
const summaryUrl = `${baseUrl}/summary?${params.toString()}`; const summaryUrl = `${baseUrl}/summary?${params.toString()}`;
@@ -446,7 +453,7 @@ function AnalyticsContent() {
}; };
fetchData(); fetchData();
}, [dateRange, selectedTeamIds, selectedProjectIds, selectedTagNames, currentPage, pageSize, selectedShortUrl, shouldFetchData]); }, [dateRange, selectedTeamIds, selectedProjectIds, selectedTagNames, selectedSubpath, currentPage, pageSize, selectedShortUrl, shouldFetchData]);
// Function to clear the shorturl filter // Function to clear the shorturl filter
const handleClearShortUrlFilter = () => { const handleClearShortUrlFilter = () => {
@@ -467,6 +474,19 @@ function AnalyticsContent() {
window.location.href = newUrl.toString(); window.location.href = newUrl.toString();
}; };
// 清除子路径筛选
const handleClearSubpathFilter = () => {
setSelectedSubpath('');
};
// 处理子路径点击
const handlePathClick = (path: string) => {
console.log('Path clicked:', path);
setSelectedSubpath(path);
// 重置到第一页
setCurrentPage(1);
};
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
@@ -553,6 +573,23 @@ function AnalyticsContent() {
</div> </div>
)} )}
{/* 如果有选定的 subpath显示提示 */}
{selectedSubpath && (
<div className="bg-blue-100 text-blue-800 px-3 py-2 rounded-md text-sm flex items-center justify-between">
<div>
<span>Filtered by Channel:</span>
<span className="ml-2 font-medium">{selectedSubpath}</span>
</div>
<button
onClick={handleClearSubpathFilter}
className="ml-3 text-blue-700 hover:text-blue-900 p-1 rounded-full hover:bg-blue-200"
aria-label="Clear subpath filter"
>
×
</button>
</div>
)}
{/* 只在没有选中 shorturl 时显示筛选选择器 */} {/* 只在没有选中 shorturl 时显示筛选选择器 */}
{!selectedShortUrl && ( {!selectedShortUrl && (
<> <>
@@ -670,6 +707,7 @@ function AnalyticsContent() {
projectIds={selectedProjectIds} projectIds={selectedProjectIds}
tagIds={selectedTagNames} tagIds={selectedTagNames}
linkId={selectedShortUrl?.externalId} linkId={selectedShortUrl?.externalId}
subpath={selectedSubpath}
/> />
</div> </div>
@@ -681,6 +719,7 @@ function AnalyticsContent() {
startTime={format(dateRange.from, "yyyy-MM-dd'T'HH:mm:ss'Z'")} startTime={format(dateRange.from, "yyyy-MM-dd'T'HH:mm:ss'Z'")}
endTime={format(dateRange.to, "yyyy-MM-dd'T'HH:mm:ss'Z'")} endTime={format(dateRange.to, "yyyy-MM-dd'T'HH:mm:ss'Z'")}
linkId={selectedShortUrl.externalId} linkId={selectedShortUrl.externalId}
onPathClick={handlePathClick}
/> />
</div> </div>
)} )}

View File

@@ -18,7 +18,9 @@ export async function GET(request: NextRequest) {
// 添加团队、项目和标签筛选 // 添加团队、项目和标签筛选
teamIds: teamIds.length > 0 ? teamIds : undefined, teamIds: teamIds.length > 0 ? teamIds : undefined,
projectIds: projectIds.length > 0 ? projectIds : 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<typeof data> = { const response: ApiResponse<typeof data> = {

View File

@@ -22,7 +22,9 @@ export async function GET(request: NextRequest) {
// 添加团队、项目和标签筛选 // 添加团队、项目和标签筛选
teamIds: teamIds.length > 0 ? teamIds : undefined, teamIds: teamIds.length > 0 ? teamIds : undefined,
projectIds: projectIds.length > 0 ? projectIds : 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<typeof data> = { const response: ApiResponse<typeof data> = {

View File

@@ -13,7 +13,9 @@ export async function GET(request: NextRequest) {
// Add debug log to check if linkId is being received // Add debug log to check if linkId is being received
const linkId = searchParams.get('linkId'); const linkId = searchParams.get('linkId');
const subpath = searchParams.get('subpath');
console.log('Summary API received linkId:', linkId); 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 full parameters:', Object.fromEntries(searchParams.entries()));
console.log('Summary API URL:', request.url); console.log('Summary API URL:', request.url);
@@ -23,7 +25,8 @@ export async function GET(request: NextRequest) {
linkId: searchParams.get('linkId') || undefined, linkId: searchParams.get('linkId') || undefined,
teamIds: teamIds.length > 0 ? teamIds : undefined, teamIds: teamIds.length > 0 ? teamIds : undefined,
projectIds: projectIds.length > 0 ? projectIds : 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<typeof summary> = { const response: ApiResponse<typeof summary> = {

View File

@@ -28,7 +28,9 @@ export async function GET(request: NextRequest) {
// 添加团队、项目和标签筛选 // 添加团队、项目和标签筛选
teamIds: teamIds.length > 0 ? teamIds : undefined, teamIds: teamIds.length > 0 ? teamIds : undefined,
projectIds: projectIds.length > 0 ? projectIds : 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<typeof data> = { const response: ApiResponse<typeof data> = {

View File

@@ -24,6 +24,7 @@ export async function GET(request: NextRequest) {
const startTime = searchParams.get('startTime'); const startTime = searchParams.get('startTime');
const endTime = searchParams.get('endTime'); const endTime = searchParams.get('endTime');
const linkId = searchParams.get('linkId'); const linkId = searchParams.get('linkId');
const subpath = searchParams.get('subpath');
// 获取团队、项目和标签筛选参数 // 获取团队、项目和标签筛选参数
const teamIds = searchParams.getAll('teamId'); const teamIds = searchParams.getAll('teamId');
@@ -39,6 +40,7 @@ export async function GET(request: NextRequest) {
startTime, startTime,
endTime, endTime,
linkId, linkId,
subpath,
teamIds, teamIds,
projectIds, projectIds,
tagIds, tagIds,
@@ -63,6 +65,11 @@ export async function GET(request: NextRequest) {
conditions.push(`link_id = '${linkId}'`); conditions.push(`link_id = '${linkId}'`);
} }
// 添加子路径筛选
if (subpath) {
conditions.push(`positionCaseInsensitive(url, '/${subpath}') > 0`);
}
// 添加团队筛选 // 添加团队筛选
if (teamIds && teamIds.length > 0) { if (teamIds && teamIds.length > 0) {
// 如果只有一个团队ID // 如果只有一个团队ID

View File

@@ -4,6 +4,7 @@ interface PathAnalyticsProps {
startTime: string; startTime: string;
endTime: string; endTime: string;
linkId?: string; linkId?: string;
onPathClick?: (path: string) => void;
} }
interface PathData { interface PathData {
@@ -12,7 +13,7 @@ interface PathData {
percentage: number; percentage: number;
} }
const PathAnalytics: React.FC<PathAnalyticsProps> = ({ startTime, endTime, linkId }) => { const PathAnalytics: React.FC<PathAnalyticsProps> = ({ startTime, endTime, linkId, onPathClick }) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [pathData, setPathData] = useState<PathData[]>([]); const [pathData, setPathData] = useState<PathData[]>([]);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -83,6 +84,13 @@ const PathAnalytics: React.FC<PathAnalyticsProps> = ({ startTime, endTime, linkI
fetchPathData(); fetchPathData();
}, [startTime, endTime, linkId]); }, [startTime, endTime, linkId]);
const handlePathClick = (path: string, e: React.MouseEvent) => {
e.preventDefault();
if (onPathClick) {
onPathClick(path);
}
};
if (loading) { if (loading) {
return <div className="py-8 flex justify-center"> return <div className="py-8 flex justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500" /> <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500" />
@@ -118,7 +126,15 @@ const PathAnalytics: React.FC<PathAnalyticsProps> = ({ startTime, endTime, linkI
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{pathData.map((item, index) => ( {pathData.map((item, index) => (
<tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}> <tr key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{item.path}</td> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
<a
href="#"
className="hover:text-blue-600 hover:underline cursor-pointer"
onClick={(e) => handlePathClick(item.path, e)}
>
{item.path}
</a>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right">{item.count}</td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-right">{item.count}</td>
<td className="px-6 py-4 whitespace-nowrap text-right"> <td className="px-6 py-4 whitespace-nowrap text-right">
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">

View File

@@ -18,9 +18,10 @@ interface UtmAnalyticsProps {
teamIds?: string[]; teamIds?: string[];
projectIds?: string[]; projectIds?: string[];
tagIds?: 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<string>('source'); const [activeTab, setActiveTab] = useState<string>('source');
const [utmData, setUtmData] = useState<UtmData[]>([]); const [utmData, setUtmData] = useState<UtmData[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
@@ -38,6 +39,7 @@ export default function UtmAnalytics({ startTime, endTime, linkId, teamIds, proj
if (startTime) params.append('startTime', startTime); if (startTime) params.append('startTime', startTime);
if (endTime) params.append('endTime', endTime); if (endTime) params.append('endTime', endTime);
if (linkId) params.append('linkId', linkId); if (linkId) params.append('linkId', linkId);
if (subpath) params.append('subpath', subpath);
params.append('utmType', activeTab); params.append('utmType', activeTab);
// 添加团队ID参数 // 添加团队ID参数
@@ -78,7 +80,7 @@ export default function UtmAnalytics({ startTime, endTime, linkId, teamIds, proj
}; };
fetchUtmData(); fetchUtmData();
}, [activeTab, startTime, endTime, linkId, teamIds, projectIds, tagIds]); }, [activeTab, startTime, endTime, linkId, teamIds, projectIds, tagIds, subpath]);
// 安全地格式化数字 // 安全地格式化数字
const formatNumber = (value: number | undefined | null): string => { const formatNumber = (value: number | undefined | null): string => {

View File

@@ -22,6 +22,7 @@ export interface EventsQueryParams {
teamIds?: string[]; teamIds?: string[];
projectIds?: string[]; projectIds?: string[];
tagIds?: string[]; tagIds?: string[];
subpath?: string;
page?: number; page?: number;
pageSize?: number; pageSize?: number;
sortBy?: string; sortBy?: string;
@@ -66,6 +67,7 @@ export async function getEventsSummary(params: {
teamIds?: string[]; teamIds?: string[];
projectIds?: string[]; projectIds?: string[];
tagIds?: string[]; tagIds?: string[];
subpath?: string;
}): Promise<EventsSummary> { }): Promise<EventsSummary> {
console.log('getEventsSummary received params:', params); console.log('getEventsSummary received params:', params);
const filter = buildFilter(params); const filter = buildFilter(params);
@@ -186,6 +188,7 @@ export async function getTimeSeriesData(params: {
teamIds?: string[]; teamIds?: string[];
projectIds?: string[]; projectIds?: string[];
tagIds?: string[]; tagIds?: string[];
subpath?: string;
}): Promise<TimeSeriesData[]> { }): Promise<TimeSeriesData[]> {
const filter = buildFilter(params); const filter = buildFilter(params);
@@ -221,6 +224,7 @@ export async function getGeoAnalytics(params: {
teamIds?: string[]; teamIds?: string[];
projectIds?: string[]; projectIds?: string[];
tagIds?: string[]; tagIds?: string[];
subpath?: string;
}): Promise<GeoData[]> { }): Promise<GeoData[]> {
const filter = buildFilter(params); const filter = buildFilter(params);
@@ -257,6 +261,7 @@ export async function getDeviceAnalytics(params: {
teamIds?: string[]; teamIds?: string[];
projectIds?: string[]; projectIds?: string[];
tagIds?: string[]; tagIds?: string[];
subpath?: string;
}): Promise<DeviceAnalytics> { }): Promise<DeviceAnalytics> {
const filter = buildFilter(params); const filter = buildFilter(params);

View File

@@ -1,5 +1,5 @@
import { createClient } from '@clickhouse/client'; import { createClient } from '@clickhouse/client';
import type { EventsQueryParams } from './types'; import { EventsQueryParams } from './analytics';
// ClickHouse 客户端配置 // ClickHouse 客户端配置
const clickhouse = createClient({ const clickhouse = createClient({
@@ -58,6 +58,13 @@ export function buildFilter(params: Partial<EventsQueryParams>): string {
filters.push(`user_id = '${params.userId}'`); 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过滤条件 // 添加团队ID过滤条件
if (params.teamId) { if (params.teamId) {
filters.push(`team_id = '${params.teamId}'`); filters.push(`team_id = '${params.teamId}'`);