"use client"; import { useEffect, useState } from 'react'; import { getSupabaseClient } from '../utils/supabase'; import { AuthChangeEvent } from '@supabase/supabase-js'; import { Loader2, ExternalLink, Search } from 'lucide-react'; import { TeamSelector } from '@/app/components/ui/TeamSelector'; import { useRouter } from 'next/navigation'; import { useShortUrlStore, ShortUrlData } from '@/app/utils/store'; import ProtectedRoute from '@/app/components/ProtectedRoute'; // Define attribute type to avoid using 'any' interface LinkAttributes { title?: string; name?: string; slug?: string; original_url?: string; originalUrl?: string; visits?: number; click_count?: number; team_id?: string; team_name?: string; tags?: string[]; [key: string]: unknown; } // 更新 ShortLink 类型定义以匹配 ClickHouse 数据结构 interface ShortLink { id: string; external_id?: string; type?: string; slug?: string; original_url?: string; title?: string; description?: string; attributes: string | Record; schema_version?: number; creator_id?: string; creator_email?: string; creator_name?: string; created_at: string; updated_at?: string; deleted_at?: string | null; projects?: string | Record[]; teams?: string | Record[]; tags?: string | Record[]; qr_codes?: string | Record[]; channels?: string | Record[]; favorites?: string | Record[]; expires_at?: string | null; click_count?: number; unique_visitors?: number; domain?: string; } // Define ClickHouse shorturl type interface ClickHouseShortUrl { id: string; external_id: string; type: string; slug: string; original_url: string; title: string; description: string; attributes: string; // JSON string schema_version: number; creator_id: string; creator_email: string; creator_name: string; created_at: string; updated_at: string; deleted_at: string | null; projects: string; // JSON string teams: string; // JSON string tags: string; // JSON string qr_codes: string; // JSON string channels: string; // JSON string favorites: string; // JSON string expires_at: string | null; click_count: number; unique_visitors: number; domain?: string; // 添加domain字段 link_attributes?: string; // Optional JSON string containing link-specific attributes } // 示例团队数据 - 实际应用中应从API获取 const teams = [ { id: 'marketing', name: 'Marketing' }, { id: 'sales', name: 'Sales' }, { id: 'product', name: 'Product' }, { id: 'engineering', name: 'Engineering' } ]; // 将 ClickHouse 数据转换为 ShortLink 格式 const convertClickHouseToShortLink = (data: Record): ShortLink => { return { ...data as any, // 使用类型断言处理泛型记录转换 // 确保关键字段存在 id: data.id as string || '', created_at: data.created_at as string || new Date().toISOString(), attributes: data.attributes || '{}' }; }; export default function LinksPage() { return (

短链接管理

); } function LinksPageContent() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [links, setLinks] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [teamFilter, setTeamFilter] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(10); const [totalLinks, setTotalLinks] = useState(0); const [totalPages, setTotalPages] = useState(0); const [searchDebounce, setSearchDebounce] = useState(null); const router = useRouter(); // 使用 Zustand store const { setSelectedShortUrl } = useShortUrlStore(); // 处理点击链接行 const handleRowClick = (link: any) => { // 解析 attributes 字符串为对象 let attributes: Record = {}; try { if (link.attributes && typeof link.attributes === 'string') { attributes = JSON.parse(link.attributes || '{}'); } } catch (e) { console.error('Error parsing link attributes:', e); } // 解析 teams 字符串为数组 let teams: any[] = []; try { if (link.teams && typeof link.teams === 'string') { teams = JSON.parse(link.teams || '[]'); } } catch (e) { console.error('Error parsing teams:', e); } // 解析 projects 字符串为数组 let projects: any[] = []; try { if (link.projects && typeof link.projects === 'string') { projects = JSON.parse(link.projects || '[]'); } } catch (e) { console.error('Error parsing projects:', e); } // 解析 tags 字符串为数组 let tags: string[] = []; try { if (link.tags && typeof link.tags === 'string') { const parsedTags = JSON.parse(link.tags); if (Array.isArray(parsedTags)) { tags = parsedTags.map((tag: { tag_name?: string }) => tag.tag_name || ''); } } } catch (e) { console.error('Error parsing tags:', e); } // 确保 shortUrl 存在 const shortUrlValue = attributes.shortUrl || ''; // 提取用于显示的字段 const shortUrlData = { id: link.id, externalId: link.external_id, // 明确添加 externalId 字段 slug: link.slug, originalUrl: link.original_url, title: link.title, shortUrl: shortUrlValue, teams: teams, projects: projects, tags: tags, createdAt: link.created_at, domain: link.domain || (shortUrlValue ? new URL(shortUrlValue).hostname : '') }; // 打印完整数据,确保 externalId 被包含 console.log('Setting shortURL data in store with externalId:', link.external_id); // 将数据保存到 Zustand store setSelectedShortUrl(shortUrlData); // 导航到分析页面,并在 URL 中包含 shortUrl 参数 router.push(`/analytics?shorturl=${encodeURIComponent(shortUrlValue)}`); }; // Extract link metadata from attributes const getLinkMetadata = (link: ShortLink) => { try { // Parse attributes if it's a string const attributes = typeof link.attributes === 'string' ? JSON.parse(link.attributes) : link.attributes || {}; // Parse attributes to get domain if available let domain = ''; try { // 首先尝试使用link.domain字段 if (link.domain) { domain = link.domain; } // 如果没有domain字段,从shortUrl中提取 else { // Extract domain from shortUrl in attributes if available const attributesObj = typeof link.attributes === 'string' ? JSON.parse(link.attributes) : link.attributes || {}; if (attributesObj.shortUrl) { try { const urlObj = new URL(attributesObj.shortUrl); domain = urlObj.hostname; } catch (e) { console.error('Error parsing shortUrl:', e); } } } } catch (e) { console.error('Error parsing attributes:', e); } // Get team names const teamNames: string[] = []; try { if (link.teams) { const teams = typeof link.teams === 'string' ? JSON.parse(link.teams) : link.teams || []; if (Array.isArray(teams)) { teams.forEach(team => { if (team.team_name) { teamNames.push(team.team_name); } }); } } } catch (e) { console.error('Error parsing teams:', e); } // Get project names const projectNames: string[] = []; try { if (link.projects) { const projects = typeof link.projects === 'string' ? JSON.parse(link.projects) : link.projects || []; if (Array.isArray(projects)) { projects.forEach(project => { if (project.project_name) { projectNames.push(project.project_name); } }); } } } catch (e) { console.error('Error parsing projects:', e); } // Get tag names const tagNames: string[] = []; try { if (link.tags) { const tags = typeof link.tags === 'string' ? JSON.parse(link.tags) : link.tags || []; if (Array.isArray(tags)) { tags.forEach(tag => { if (tag.tag_name) { tagNames.push(tag.tag_name); } }); } } } catch (e) { console.error('Error parsing tags:', e); } return { title: link.title || attributes.title || 'Untitled', slug: link.slug || attributes.slug || '', domain: domain, originalUrl: link.original_url || attributes.originalUrl || attributes.original_url || '', teamNames: teamNames, projectNames: projectNames, tagNames: tagNames, teamName: teamNames[0] || '', // Keep for backward compatibility createdAt: new Date(link.created_at).toLocaleDateString(), visits: link.click_count || 0 }; } catch (error) { console.error('Error parsing link metadata:', error); return { title: 'Error parsing data', slug: '', domain: 'shorturl.example.com', originalUrl: '', teamNames: [], projectNames: [], tagNames: [], teamName: '', createdAt: '', visits: 0 }; } }; useEffect(() => { let isMounted = true; const fetchLinks = async () => { if (!isMounted) return; setLoading(true); setError(null); try { // Fetch data from ClickHouse API with pagination parameters const response = await fetch(`/api/shortlinks?page=${currentPage}&page_size=${pageSize}${searchQuery ? `&search=${encodeURIComponent(searchQuery)}` : ''}${teamFilter ? `&team=${encodeURIComponent(teamFilter)}` : ''}`); if (!response.ok) { throw new Error(`Failed to fetch links: ${response.status} ${response.statusText}`); } const data = await response.json(); if (!data || !data.links || data.links.length === 0) { if (isMounted) { setLinks([]); setTotalLinks(0); setTotalPages(0); } return; } // Convert ClickHouse data format to ShortLink format const convertedLinks = data.links.map(convertClickHouseToShortLink); if (isMounted) { setLinks(convertedLinks); setTotalLinks(data.total || convertedLinks.length); setTotalPages(data.total_pages || Math.ceil(data.total / pageSize) || 1); } } catch (err) { if (isMounted) { setError(err instanceof Error ? err.message : 'Failed to load short URLs'); console.error("Error fetching links:", err); } } finally { if (isMounted) { setLoading(false); } } }; // Subscribe to user auth state const supabase = getSupabaseClient(); const { data: { subscription } } = supabase.auth.onAuthStateChange( (event: AuthChangeEvent) => { if (event === 'SIGNED_IN' || event === 'USER_UPDATED') { fetchLinks(); } if (event === 'SIGNED_OUT') { setLinks([]); } } ); fetchLinks(); return () => { isMounted = false; subscription.unsubscribe(); }; }, [currentPage, pageSize, searchQuery, teamFilter]); // Handle search input with debounce const handleSearchChange = (e: React.ChangeEvent) => { const value = e.target.value; // Clear any existing timeout if (searchDebounce) { clearTimeout(searchDebounce); } // Set the input value immediately for UI feedback setSearchQuery(value); // Set a timeout to actually perform the search setSearchDebounce(setTimeout(() => { setCurrentPage(1); // Reset to page 1 when searching }, 500)); // 500ms debounce }; if (loading && links.length === 0) { return (
); } if (error) { return (

Error loading shortcuts: {error}

); } return (

Short URL Links

{/* Search and filters */}
{ if (e.key === 'Enter') { setCurrentPage(1); // Reset to page 1 when searching } }} className="w-full rounded-md border border-gray-300 py-2 pl-10 pr-4 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" />
{ // 如果是多选模式,值将是数组。对于空数组,设置为 null if (Array.isArray(value)) { setTeamFilter(value.length > 0 ? value[0] : null); } else { setTeamFilter(value || null); } setCurrentPage(1); // Reset to page 1 when filtering }} className="w-64" multiple={true} />
{/* Links table */}
{links.map(link => { const metadata = getLinkMetadata(link); const shortUrl = `https://${metadata.domain}/${metadata.slug}`; return ( handleRowClick(link)}> ); })}
Link Original URL Team Created
{metadata.title} {shortUrl} {/* Tags */} {metadata.tagNames.length > 0 && (
{metadata.tagNames.map((tag, index) => ( {tag} ))}
)}
{metadata.originalUrl}
{/* Teams */} {metadata.teamNames.length > 0 ? (
{metadata.teamNames.map((team, index) => ( {team} ))}
) : ( - )} {/* Projects */} {metadata.projectNames.length > 0 && (
{metadata.projectNames.map((project, index) => ( {project} ))}
)}
{metadata.createdAt}
{/* Pagination */} {totalPages > 0 && (
Showing {((currentPage - 1) * pageSize) + 1} to {Math.min(currentPage * pageSize, totalLinks)} of {totalLinks} results
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => { // Create a window of 5 pages around current page let pageNumber; if (totalPages <= 5) { pageNumber = i + 1; } else { const start = Math.max(1, currentPage - 2); const end = Math.min(totalPages, start + 4); pageNumber = start + i; if (pageNumber > end) return null; } return ( ); })} {/* Page input */}
Go to: { // Allow input to be cleared for typing if (e.target.value === '') { e.target.value = ''; } }} onBlur={(e) => { // Ensure a valid value on blur const value = parseInt(e.target.value, 10); if (isNaN(value) || value < 1) { setCurrentPage(1); } else if (value > totalPages) { setCurrentPage(totalPages); } else { setCurrentPage(value); } }} onKeyDown={(e) => { if (e.key === 'Enter') { const value = parseInt(e.currentTarget.value, 10); if (!isNaN(value) && value >= 1 && value <= totalPages) { setCurrentPage(value); } else if (!isNaN(value) && value < 1) { setCurrentPage(1); } else if (!isNaN(value) && value > totalPages) { setCurrentPage(totalPages); } } }} className="w-16 rounded-md border border-gray-300 px-2 py-1 text-sm text-center" /> of {totalPages}
)} {links.length === 0 && (
No links match your search criteria
)}
); }