hide filter
This commit is contained in:
@@ -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<string | null>(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<DeviceAnalyticsType | null>(null);
|
||||
const [events, setEvents] = useState<Event[]>([]);
|
||||
|
||||
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 (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
{/* Notification Snackbar */}
|
||||
{isSnackbarOpen && (
|
||||
<div className="fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 rounded-md shadow-lg z-50 flex items-center">
|
||||
<span>URL filter cleared</span>
|
||||
<button
|
||||
onClick={() => setIsSnackbarOpen(false)}
|
||||
className="ml-3 text-white hover:text-gray-200 p-1"
|
||||
aria-label="Close notification"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Analytics Dashboard</h1>
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center">
|
||||
{/* 如果有选定的 shorturl,可以显示一个提示,显示更多详细信息 */}
|
||||
{selectedShortUrl && (
|
||||
<div className="bg-blue-100 text-blue-800 px-3 py-2 rounded-md text-sm flex flex-col">
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium">{selectedShortUrl.title || 'Untitled'}</span>
|
||||
<span className="mx-2">-</span>
|
||||
<span>{selectedShortUrl.shortUrl}</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<span className="font-medium">{selectedShortUrl.title || 'Untitled'}</span>
|
||||
<span className="mx-2">-</span>
|
||||
<span>{selectedShortUrl.shortUrl}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClearShortUrlFilter}
|
||||
className="ml-3 text-blue-700 hover:text-blue-900 p-1 rounded-full hover:bg-blue-200"
|
||||
aria-label="Clear shorturl filter"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-xs mt-1 text-blue-700">
|
||||
<span>Analytics filtered for this short URL only</span>
|
||||
{selectedShortUrl.id && <span className="ml-2 text-blue-500">(ID: {selectedShortUrl.id})</span>}
|
||||
</div>
|
||||
{selectedShortUrl.tags && selectedShortUrl.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
@@ -294,51 +406,67 @@ export default function AnalyticsPage() {
|
||||
|
||||
{/* 如果只有 URL 参数但没有完整数据,则显示简单提示 */}
|
||||
{selectedShortUrlString && !selectedShortUrl && (
|
||||
<div className="bg-blue-100 text-blue-800 px-3 py-1 rounded-md text-sm flex items-center">
|
||||
<span>Filtered by Short URL:</span>
|
||||
<span className="ml-2 font-medium">{selectedShortUrlString}</span>
|
||||
<div className="bg-blue-100 text-blue-800 px-3 py-1 rounded-md text-sm flex items-center justify-between">
|
||||
<div>
|
||||
<span>Filtered by Short URL:</span>
|
||||
<span className="ml-2 font-medium">{selectedShortUrlString}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClearShortUrlFilter}
|
||||
className="ml-3 text-blue-700 hover:text-blue-900 p-1 rounded-full hover:bg-blue-200"
|
||||
aria-label="Clear shorturl filter"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<TeamSelector
|
||||
value={selectedTeamIds}
|
||||
onChange={(value) => {
|
||||
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}
|
||||
/>
|
||||
<ProjectSelector
|
||||
value={selectedProjectIds}
|
||||
onChange={(value) => setSelectedProjectIds(Array.isArray(value) ? value : [value])}
|
||||
className="w-[250px]"
|
||||
multiple={true}
|
||||
teamIds={selectedTeamIds.length > 0 ? selectedTeamIds : undefined}
|
||||
/>
|
||||
<TagSelector
|
||||
value={selectedTagNames}
|
||||
onChange={(value) => {
|
||||
// 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 && (
|
||||
<>
|
||||
<TeamSelector
|
||||
value={selectedTeamIds}
|
||||
onChange={(value) => {
|
||||
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}
|
||||
/>
|
||||
<ProjectSelector
|
||||
value={selectedProjectIds}
|
||||
onChange={(value) => setSelectedProjectIds(Array.isArray(value) ? value : [value])}
|
||||
className="w-[250px]"
|
||||
multiple={true}
|
||||
teamIds={selectedTeamIds.length > 0 ? selectedTeamIds : undefined}
|
||||
/>
|
||||
<TagSelector
|
||||
value={selectedTagNames}
|
||||
onChange={(value) => {
|
||||
// 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}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DateRangePicker
|
||||
value={dateRange}
|
||||
onChange={setDateRange}
|
||||
@@ -346,97 +474,102 @@ export default function AnalyticsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 显示团队选择信息 */}
|
||||
{selectedTeamIds.length > 0 && (
|
||||
<div className="bg-blue-50 rounded-lg p-3 mb-6 flex items-center">
|
||||
<span className="text-blue-700 font-medium mr-2">
|
||||
{selectedTeamIds.length === 1 ? 'Team filter:' : 'Teams filter:'}
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedTeamIds.map(teamId => (
|
||||
<span key={teamId} className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
|
||||
{teamId}
|
||||
<button
|
||||
onClick={() => setSelectedTeamIds(selectedTeamIds.filter(id => id !== teamId))}
|
||||
className="ml-1 text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{/* 仅在未选中 shorturl 且有选择的筛选条件时显示筛选条件标签 */}
|
||||
{!selectedShortUrl && (
|
||||
<>
|
||||
{/* 显示团队选择信息 */}
|
||||
{selectedTeamIds.length > 0 && (
|
||||
<div className="bg-blue-50 rounded-lg p-3 mb-6 flex items-center">
|
||||
<span className="text-blue-700 font-medium mr-2">
|
||||
{selectedTeamIds.length === 1 ? 'Team filter:' : 'Teams filter:'}
|
||||
</span>
|
||||
))}
|
||||
{selectedTeamIds.length > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedTeamIds([])}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 underline"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedTeamIds.map(teamId => (
|
||||
<span key={teamId} className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
|
||||
{teamId}
|
||||
<button
|
||||
onClick={() => setSelectedTeamIds(selectedTeamIds.filter(id => id !== teamId))}
|
||||
className="ml-1 text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
{selectedTeamIds.length > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedTeamIds([])}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 underline"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 显示项目选择信息 */}
|
||||
{selectedProjectIds.length > 0 && (
|
||||
<div className="bg-blue-50 rounded-lg p-3 mb-6 flex items-center">
|
||||
<span className="text-blue-700 font-medium mr-2">
|
||||
{selectedProjectIds.length === 1 ? 'Project filter:' : 'Projects filter:'}
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedProjectIds.map(projectId => (
|
||||
<span key={projectId} className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
|
||||
{projectId}
|
||||
<button
|
||||
onClick={() => setSelectedProjectIds(selectedProjectIds.filter(id => id !== projectId))}
|
||||
className="ml-1 text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{/* 显示项目选择信息 */}
|
||||
{selectedProjectIds.length > 0 && (
|
||||
<div className="bg-blue-50 rounded-lg p-3 mb-6 flex items-center">
|
||||
<span className="text-blue-700 font-medium mr-2">
|
||||
{selectedProjectIds.length === 1 ? 'Project filter:' : 'Projects filter:'}
|
||||
</span>
|
||||
))}
|
||||
{selectedProjectIds.length > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedProjectIds([])}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 underline"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedProjectIds.map(projectId => (
|
||||
<span key={projectId} className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
|
||||
{projectId}
|
||||
<button
|
||||
onClick={() => setSelectedProjectIds(selectedProjectIds.filter(id => id !== projectId))}
|
||||
className="ml-1 text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
{selectedProjectIds.length > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedProjectIds([])}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 underline"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 显示标签选择信息 */}
|
||||
{selectedTagNames.length > 0 && (
|
||||
<div className="bg-blue-50 rounded-lg p-3 mb-6 flex items-center">
|
||||
<span className="text-blue-700 font-medium mr-2">
|
||||
{selectedTagNames.length === 1 ? 'Tag filter:' : 'Tags filter:'}
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedTagNames.map(tagName => (
|
||||
<span key={tagName} className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
|
||||
{tagName}
|
||||
<button
|
||||
onClick={() => {
|
||||
// 移除对应的标签名称
|
||||
setSelectedTagNames(selectedTagNames.filter(name => name !== tagName));
|
||||
}}
|
||||
className="ml-1 text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{/* 显示标签选择信息 */}
|
||||
{selectedTagNames.length > 0 && (
|
||||
<div className="bg-blue-50 rounded-lg p-3 mb-6 flex items-center">
|
||||
<span className="text-blue-700 font-medium mr-2">
|
||||
{selectedTagNames.length === 1 ? 'Tag filter:' : 'Tags filter:'}
|
||||
</span>
|
||||
))}
|
||||
{selectedTagNames.length > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedTagNames([])}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 underline"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedTagNames.map(tagName => (
|
||||
<span key={tagName} className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
|
||||
{tagName}
|
||||
<button
|
||||
onClick={() => {
|
||||
// 移除对应的标签名称
|
||||
setSelectedTagNames(selectedTagNames.filter(name => name !== tagName));
|
||||
}}
|
||||
className="ml-1 text-blue-600 hover:text-blue-800"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
{selectedTagNames.length > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedTagNames([])}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 underline"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 仪表板内容 - 现在放在事件列表之后 */}
|
||||
|
||||
Reference in New Issue
Block a user