From 6964eb75cc5d88106b523ab16d84ff9d2e58f2e7 Mon Sep 17 00:00:00 2001 From: William Tso Date: Fri, 14 Mar 2025 12:40:57 +0800 Subject: [PATCH] kol overview frontend --- Requirements.md | 2 +- web/src/components/Analytics.tsx | 685 +++++++++++++++++++------------ 2 files changed, 427 insertions(+), 260 deletions(-) diff --git a/Requirements.md b/Requirements.md index a9a918b..69c3033 100644 --- a/Requirements.md +++ b/Requirements.md @@ -56,7 +56,7 @@ - 条形长度直观反映各平台占比 - 帮助团队了解哪些平台效果更好 -# 审核状态分布 +# 审核状态分布 [先不做] - 环形图展示内容审核状态的分布情况 - 包括三种状态:已核准、待审核、已拒绝 diff --git a/web/src/components/Analytics.tsx b/web/src/components/Analytics.tsx index 2e1301c..835f870 100644 --- a/web/src/components/Analytics.tsx +++ b/web/src/components/Analytics.tsx @@ -1,6 +1,7 @@ import React, { useState, useEffect } from 'react'; import { TrendingUp, + TrendingDown, PieChart, MessageSquare, Facebook, @@ -21,7 +22,8 @@ import { Download, AlertTriangle, Save, - Eye + Eye, + Twitter } from 'lucide-react'; // Define interfaces for analytics data @@ -64,23 +66,20 @@ interface EngagementData { shares: number; } +// 更新KOL数据接口,与API接口结构匹配 interface KOLData { - id: string; + influencer_id: string; name: string; platform: string; - followers: number; - engagement: number; - posts: number; - sentiment: SentimentData; - avatar?: string; - platforms?: string[]; - postCount?: number; - likeCount?: number; - commentCount?: number; - engagementRate?: number; - engagementTrend?: number; - sentimentScore?: number; - officialInteractions?: number; + profile_url: string; + followers_count: number; + followers_change: number; + followers_change_percentage: number | null | string; + likes_change: number; + likes_change_percentage: number | null | string; + follows_change: number; + follows_change_percentage: number | null | string; + avatar?: string; // 兼容旧代码 } interface FunnelData { @@ -105,8 +104,20 @@ interface Project { description?: string; } +// 添加API响应接口 +interface KOLOverviewResponse { + success: boolean; + data: KOLData[]; + pagination: { + limit: number; + offset: number; + total: number; + }; + error?: string; +} + const Analytics: React.FC = () => { - const [timeRange, setTimeRange] = useState('7days'); + const [timeRange, setTimeRange] = useState('30'); // 修改默认值为'30'与API匹配 const [selectedKOL, setSelectedKOL] = useState('all'); const [selectedPlatform, setSelectedPlatform] = useState('all'); const [platformData, setPlatformData] = useState([]); @@ -121,7 +132,9 @@ const Analytics: React.FC = () => { const [kolData, setKolData] = useState([]); const [funnelData, setFunnelData] = useState([]); const [loading, setLoading] = useState(true); + const [kolLoading, setKolLoading] = useState(true); // 新增KOL数据加载状态 const [error, setError] = useState(null); + const [kolError, setKolError] = useState(null); // 新增KOL数据错误状态 const [filteredEngagementData, setFilteredEngagementData] = useState([]); // 添加项目相关状态 @@ -131,7 +144,16 @@ const Analytics: React.FC = () => { { id: '3', name: '项目 3', description: '示例项目 3' }, { id: '550e8400-e29b-41d4-a716-446655440000', name: 'UUID格式项目', description: 'UUID格式的项目ID示例' } ]); - const [selectedProject, setSelectedProject] = useState('1'); + const [selectedProject, setSelectedProject] = useState('all'); + + // 添加分页状态 + const [kolPage, setKolPage] = useState(0); + const [kolPageSize, setKolPageSize] = useState(20); + const [kolTotal, setKolTotal] = useState(0); + + // 添加排序状态 + const [kolSortBy, setKolSortBy] = useState('followers_change'); + const [kolSortOrder, setKolSortOrder] = useState('desc'); // Add new state for influencer tracking const [showTrackingForm, setShowTrackingForm] = useState(false); @@ -145,7 +167,78 @@ const Analytics: React.FC = () => { const [trackingSuccess, setTrackingSuccess] = useState(null); const [trackingError, setTrackingError] = useState(null); + // 获取KOL概览数据 + const fetchKolOverviewData = async () => { + setKolLoading(true); + setKolError(null); + + try { + // 构建API URL,包含所有查询参数 + let url = `http://localhost:4000/api/analytics/kol-overview?timeRange=${timeRange}&sortBy=${kolSortBy}&sortOrder=${kolSortOrder}&limit=${kolPageSize}&offset=${kolPage * kolPageSize}`; + + if (selectedProject !== 'all') { + url += `&projectId=${selectedProject}`; + } + + // 获取认证令牌 (实际项目中应从身份验证上下文中获取) + const authToken = 'eyJhbGciOiJIUzI1NiIsImtpZCI6Inl3blNGYnRBOGtBUnl4UmUiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3h0cWhsdXpvcm5hemxta29udWNyLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiI1YjQzMThiZi0yMWE4LTQ3YWMtOGJmYS0yYThmOGVmOWMwZmIiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzQxNjI3ODkyLCJpYXQiOjE3NDE2MjQyOTIsImVtYWlsIjoidml0YWxpdHltYWlsZ0BnbWFpbC5jb20iLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7ImVtYWlsX3ZlcmlmaWVkIjp0cnVlfSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc0MTYyNDI5Mn1dLCJzZXNzaW9uX2lkIjoiODlmYjg0YzktZmEzYy00YmVlLTk0MDQtNjI1MjE0OGIyMzVlIiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.VuUX2yhqN-FZseKL8fQG91i1cohfRqW2m1Z8CIWhZuk'; + + // 发送请求 + const response = await fetch(url, { + headers: { + 'accept': 'application/json', + 'Authorization': `Bearer ${authToken}` + } + }); + + // 处理响应 + if (response.ok) { + const result: KOLOverviewResponse = await response.json(); + + if (result.success) { + console.log('成功获取KOL概览数据:', result); + + // 更新状态 + setKolData(result.data); + setKolTotal(result.pagination.total); + + // 处理模拟数据标志 + if ('is_mock_data' in result && result.is_mock_data) { + console.info('注意: 使用的是模拟数据'); + } + } else { + // API返回了成功状态码但标记为不成功 + console.error('API返回错误:', result.error); + setKolError(result.error || '无法获取KOL数据'); + + // 保留现有数据或清空 + // setKolData([]); + } + } else { + // HTTP错误 + console.error('获取KOL数据失败,HTTP状态:', response.status); + const errorText = await response.text(); + setKolError(`获取失败 (${response.status}): ${errorText}`); + + // 保留现有数据或清空 + // setKolData([]); + } + } catch (error) { + // 网络或其他错误 + console.error('获取KOL数据时发生错误:', error); + setKolError(`获取KOL数据时发生错误: ${error instanceof Error ? error.message : String(error)}`); + + // 保留现有数据或清空 + // setKolData([]); + } finally { + setKolLoading(false); + } + }; + useEffect(() => { + // 获取KOL概览数据 + fetchKolOverviewData(); + const fetchAnalyticsData = async () => { try { setLoading(true); @@ -175,9 +268,9 @@ const Analytics: React.FC = () => { // Set mock status data setStatusData([ - { name: 'Approved', value: 45, color: '#10B981' }, - { name: 'Pending', value: 30, color: '#F59E0B' }, - { name: 'Rejected', value: 25, color: '#EF4444' } + { name: 'approved', value: 45, color: '#10B981' }, + { name: 'pending', value: 30, color: '#F59E0B' }, + { name: 'rejected', value: 25, color: '#EF4444' } ]); // Set mock popular articles @@ -189,64 +282,6 @@ const Analytics: React.FC = () => { { id: '5', title: 'Content Creation Tips', views: 620, engagement: 57, platform: 'YouTube' } ]); - // Set mock KOL data - setKolData([ - { - id: '1', - name: 'John Smith', - platform: 'Instagram', - followers: 120000, - engagement: 3.5, - posts: 450, - sentiment: { positive: 75, neutral: 15, negative: 10 }, - avatar: 'https://randomuser.me/api/portraits/men/1.jpg', - platforms: ['Instagram', 'TikTok'], - postCount: 120, - likeCount: 45000, - commentCount: 2800, - engagementRate: 3.8, - engagementTrend: 0.5, - sentimentScore: 4.2, - officialInteractions: 12 - }, - { - id: '2', - name: 'Sarah Johnson', - platform: 'YouTube', - followers: 250000, - engagement: 4.2, - posts: 320, - sentiment: { positive: 82, neutral: 10, negative: 8 }, - avatar: 'https://randomuser.me/api/portraits/women/2.jpg', - platforms: ['YouTube', 'Instagram'], - postCount: 85, - likeCount: 120000, - commentCount: 8500, - engagementRate: 4.5, - engagementTrend: 1.2, - sentimentScore: 4.5, - officialInteractions: 8 - }, - { - id: '3', - name: 'David Lee', - platform: 'Twitter', - followers: 180000, - engagement: 2.8, - posts: 1200, - sentiment: { positive: 68, neutral: 22, negative: 10 }, - avatar: 'https://randomuser.me/api/portraits/men/3.jpg', - platforms: ['Twitter', 'LinkedIn'], - postCount: 350, - likeCount: 28000, - commentCount: 4200, - engagementRate: 2.5, - engagementTrend: -0.3, - sentimentScore: 3.8, - officialInteractions: 5 - } - ]); - // 尝试从API获取漏斗数据 try { // 使用选中的项目ID @@ -318,15 +353,20 @@ const Analytics: React.FC = () => { }; fetchAnalyticsData(); - }, [timeRange, selectedProject]); // 添加selectedProject作为依赖项 + }, [timeRange, selectedProject, kolSortBy, kolSortOrder, kolPage, kolPageSize]); // 更新依赖项 + + // 当排序或筛选条件变化时,重置页码 + useEffect(() => { + setKolPage(0); + }, [timeRange, selectedProject, kolSortBy, kolSortOrder]); // Filter KOLs based on selected platform const filteredKOLs = selectedPlatform === 'all' ? kolData - : kolData.filter(kol => kol.platform === selectedPlatform); + : kolData.filter(kol => kol.platform.toLowerCase() === selectedPlatform.toLowerCase()); - // Sort KOLs by followers count - const sortedKOLs = [...filteredKOLs].sort((a, b) => b.followers - a.followers); + // 不需要额外排序,API已经排序 + const sortedKOLs = filteredKOLs; // Update filtered engagement data when KOL selection changes useEffect(() => { @@ -338,11 +378,14 @@ const Analytics: React.FC = () => { }, [selectedKOL]); const getPlatformIcon = (platform: string) => { - switch (platform) { + const platformLower = platform.toLowerCase(); + switch (platformLower) { case 'facebook': return ; case 'threads': return ; + case 'twitter': + return ; case 'instagram': return ; case 'linkedin': @@ -355,6 +398,43 @@ const Analytics: React.FC = () => { return null; } }; + + // 帮助函数:获取表示变化趋势的图标和样式 + const getTrendingIcon = (change: number | null | string) => { + // 如果是字符串(可能是"待计算")或者null,返回中性图标 + if (typeof change !== 'number') { + return { + icon:
, + colorClass: 'text-gray-500' + }; + } + + if (change > 0) { + return { + icon: , + colorClass: 'text-green-500' + }; + } else if (change < 0) { + return { + icon: , + colorClass: 'text-red-500' + }; + } else { + return { + icon:
, + colorClass: 'text-gray-500' + }; + } + }; + + // 帮助函数:格式化百分比变化 + const formatPercentageChange = (change: number | null | string) => { + if (change === null || typeof change === 'string') { + return '0%'; + } + + return `${change > 0 ? '+' : ''}${change.toFixed(1)}%`; + }; const getStatusIcon = (status: string) => { switch (status) { @@ -602,6 +682,7 @@ const Analytics: React.FC = () => { onChange={(e) => setSelectedProject(e.target.value)} className="block w-full py-2 pl-3 pr-10 text-base border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" > + {projects.map((project) => (