diff --git a/web/src/components/Analytics.tsx b/web/src/components/Analytics.tsx index 2159eb2..915d9b7 100644 --- a/web/src/components/Analytics.tsx +++ b/web/src/components/Analytics.tsx @@ -138,6 +138,43 @@ interface DashboardCardsResponse { error?: string; } +// 添加平台分布API响应接口 +interface PlatformDistributionResponse { + success: boolean; + data: { + platform: string; + count: number; + percentage: number; + }[]; + metadata: { + total: number; + event_type: string; + }; + error?: string; +} + +// 添加情感分析API响应接口 +interface SentimentAnalysisResponse { + success: boolean; + data: { + positive: { + count: number; + percentage: number; + }; + neutral: { + count: number; + percentage: number; + }; + negative: { + count: number; + percentage: number; + }; + total: number; + average_score: number; + }; + error?: string; +} + // 添加留言趋势API响应接口 interface CommentTrendResponse { success: boolean; @@ -220,6 +257,12 @@ const Analytics: React.FC = () => { const [cardsLoading, setCardsLoading] = useState(true); const [cardsError, setCardsError] = useState(null); + const [platformLoading, setPlatformLoading] = useState(true); + const [platformError, setPlatformError] = useState(null); + const [sentimentLoading, setSentimentLoading] = useState(true); + const [sentimentError, setSentimentError] = useState(null); + const [sentimentScore, setSentimentScore] = useState(0); + // 获取KOL概览数据 const fetchKolOverviewData = async () => { setKolLoading(true); @@ -301,21 +344,17 @@ const Analytics: React.FC = () => { // 获取留言趋势数据 fetchCommentTrend(); + // 获取平台分布数据 + fetchPlatformDistribution(); + + // 获取情感分析数据 + fetchSentimentAnalysis(); + const fetchAnalyticsData = async () => { try { setLoading(true); - // Set default platform distribution data - setPlatformData([ - { name: 'Facebook', value: 35, color: '#1877F2' }, - { name: 'Twitter', value: 25, color: '#1DA1F2' }, - { name: 'Instagram', value: 20, color: '#E4405F' }, - { name: 'LinkedIn', value: 15, color: '#0A66C2' }, - { name: 'YouTube', value: 5, color: '#FF0000' } - ]); - - // Set mock sentiment data - setSentimentData({ positive: 65, neutral: 20, negative: 15 }); + // 删除平台分布的硬编码数据,使用API数据替代 // Set mock status data setStatusData([ @@ -999,6 +1038,180 @@ const Analytics: React.FC = () => { } }; + // 获取平台分布数据 + const fetchPlatformDistribution = async () => { + try { + setPlatformLoading(true); + setPlatformError(null); + + // 构建平台分布API URL + const url = `http://localhost:4000/api/analytics/platform-distribution?timeRange=${timeRange}`; + + // 添加项目过滤参数(如果选择了特定项目) + const urlWithFilters = selectedProject !== 'all' + ? `${url}&projectId=${selectedProject}` + : url; + + // 添加平台过滤参数(如果选择了特定平台) + const finalUrl = selectedPlatform !== 'all' + ? `${urlWithFilters}&platform=${selectedPlatform}` + : urlWithFilters; + + console.log('请求平台分布数据URL:', finalUrl); + + // 添加认证头 + const authToken = 'eyJhbGciOiJIUzI1NiIsImtpZCI6Inl3blNGYnRBOGtBUnl4UmUiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3h0cWhsdXpvcm5hemxta29udWNyLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiI1YjQzMThiZi0yMWE4LTQ3YWMtOGJmYS0yYThmOGVmOWMwZmIiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzQxNjI3ODkyLCJpYXQiOjE3NDE2MjQyOTIsImVtYWlsIjoidml0YWxpdHltYWlsZ0BnbWFpbC5jb20iLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7ImVtYWlsX3ZlcmlmaWVkIjp0cnVlfSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc0MTYyNDI5Mn1dLCJzZXNzaW9uX2lkIjoiODlmYjg0YzktZmEzYy00YmVlLTk0MDQtNjI1MjE0OGIyMzVlIiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.VuUX2yhqN-FZseKL8fQG91i1cohfRqW2m1Z8CIWhZuk'; + + const response = await fetch(finalUrl, { + headers: { + 'accept': 'application/json', + 'Authorization': `Bearer ${authToken}` + } + }); + + if (response.ok) { + const result = await response.json() as PlatformDistributionResponse; + console.log('成功获取平台分布数据:', result); + + if (result.success) { + // 平台名称规范化并合并相同平台(大小写不同) + const platformMap = new Map(); + + // 为不同平台分配颜色 + const platformColors: Record = { + 'facebook': '#1877F2', + 'twitter': '#1DA1F2', + 'instagram': '#E4405F', + 'linkedin': '#0A66C2', + 'youtube': '#FF0000', + 'tiktok': '#000000', + 'xiaohongshu': '#FF0000' + }; + + // 处理并合并平台数据 + result.data.forEach((item) => { + const platformLower = item.platform.toLowerCase(); + const existingData = platformMap.get(platformLower); + + if (existingData) { + // 合并相同平台(大小写不同)的数据 + platformMap.set(platformLower, { + count: existingData.count + item.count, + percentage: existingData.percentage + item.percentage, + color: existingData.color + }); + } else { + // 添加新平台数据 + platformMap.set(platformLower, { + count: item.count, + percentage: item.percentage, + color: platformColors[platformLower] || '#808080' // 默认为灰色 + }); + } + }); + + // 将处理后的数据转换为AnalyticsData数组 + const mappedData: AnalyticsData[] = Array.from(platformMap.entries()).map(([name, data]) => ({ + name: name, + value: data.count, + percentage: data.percentage, + color: data.color + })); + + // 按百分比降序排序 + mappedData.sort((a, b) => (b.percentage || 0) - (a.percentage || 0)); + + // 更新状态 + setPlatformData(mappedData); + + // 如果API返回了模拟数据标志 + if ('is_mock_data' in result && result.is_mock_data) { + console.info('注意: 使用的是模拟平台分布数据'); + } + } else { + setPlatformError(result.error || '获取平台分布数据失败'); + console.error('API调用失败:', result.error || '未知错误'); + } + } else { + const errorText = await response.text(); + setPlatformError(`获取失败 (${response.status}): ${errorText}`); + console.error('获取平台分布数据失败,HTTP状态:', response.status, errorText); + } + } catch (error) { + setPlatformError(`获取平台分布数据时发生错误: ${error instanceof Error ? error.message : String(error)}`); + console.error('获取平台分布数据时发生错误:', error); + } finally { + setPlatformLoading(false); + } + }; + + // 获取情感分析数据 + const fetchSentimentAnalysis = async () => { + try { + setSentimentLoading(true); + setSentimentError(null); + + // 构建情感分析API URL + const url = `http://localhost:4000/api/analytics/sentiment-analysis?timeRange=${timeRange}`; + + // 添加项目过滤参数(如果选择了特定项目) + const urlWithFilters = selectedProject !== 'all' + ? `${url}&projectId=${selectedProject}` + : url; + + // 添加平台过滤参数(如果选择了特定平台) + const finalUrl = selectedPlatform !== 'all' + ? `${urlWithFilters}&platform=${selectedPlatform}` + : urlWithFilters; + + console.log('请求情感分析数据URL:', finalUrl); + + // 添加认证头 + const authToken = 'eyJhbGciOiJIUzI1NiIsImtpZCI6Inl3blNGYnRBOGtBUnl4UmUiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3h0cWhsdXpvcm5hemxta29udWNyLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiI1YjQzMThiZi0yMWE4LTQ3YWMtOGJmYS0yYThmOGVmOWMwZmIiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzQxNjI3ODkyLCJpYXQiOjE3NDE2MjQyOTIsImVtYWlsIjoidml0YWxpdHltYWlsZ0BnbWFpbC5jb20iLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7ImVtYWlsX3ZlcmlmaWVkIjp0cnVlfSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc0MTYyNDI5Mn1dLCJzZXNzaW9uX2lkIjoiODlmYjg0YzktZmEzYy00YmVlLTk0MDQtNjI1MjE0OGIyMzVlIiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.VuUX2yhqN-FZseKL8fQG91i1cohfRqW2m1Z8CIWhZuk'; + + const response = await fetch(finalUrl, { + headers: { + 'accept': 'application/json', + 'Authorization': `Bearer ${authToken}` + } + }); + + if (response.ok) { + const result = await response.json() as SentimentAnalysisResponse; + console.log('成功获取情感分析数据:', result); + + if (result.success) { + // 更新状态 + setSentimentData({ + positive: result.data.positive.percentage, + neutral: result.data.neutral.percentage, + negative: result.data.negative.percentage + }); + + // 设置平均情感分数 + setSentimentScore(result.data.average_score); + + // 如果API返回了模拟数据标志 + if ('is_mock_data' in result && result.is_mock_data) { + console.info('注意: 使用的是模拟情感分析数据'); + } + } else { + setSentimentError(result.error || '获取情感分析数据失败'); + console.error('API调用失败:', result.error || '未知错误'); + } + } else { + const errorText = await response.text(); + setSentimentError(`获取失败 (${response.status}): ${errorText}`); + console.error('获取情感分析数据失败,HTTP状态:', response.status, errorText); + } + } catch (error) { + setSentimentError(`获取情感分析数据时发生错误: ${error instanceof Error ? error.message : String(error)}`); + console.error('获取情感分析数据时发生错误:', error); + } finally { + setSentimentLoading(false); + } + }; + return (
@@ -1584,30 +1797,53 @@ const Analytics: React.FC = () => { {/* 平台分布 */}

平台分布

-
- {platformData.map((item, index) => ( -
-
-
- {getPlatformIcon(item.name)} - - {item.name === 'xiaohongshu' ? '小红书' : item.name} - + {platformLoading ? ( +
+
+

加载平台分布数据中...

+
+ ) : platformError ? ( +
+ +

{platformError}

+
+ ) : platformData.length === 0 ? ( +
+ +

没有找到平台分布数据

+
+ ) : ( +
+ {platformData.map((item, index) => ( +
+
+
+ {getPlatformIcon(item.name)} + + {item.name === 'xiaohongshu' ? '小红书' : + item.name === 'youtube' ? 'YouTube' : + item.name === 'tiktok' ? 'TikTok' : + item.name.charAt(0).toUpperCase() + item.name.slice(1)} + +
+
+ {item.value} 则留言 + {item.percentage?.toFixed(1)}% +
-
- {item.value} 则留言 - {item.percentage}% +
+
-
-
-
-
- ))} -
+ ))} +
+ )}
{/* 审核状态分布 */} @@ -1656,28 +1892,42 @@ const Analytics: React.FC = () => { {/* 情感分析详情 */}

情感分析详情

-
-
-
+ {sentimentLoading ? ( +
+
+

加载情感分析数据中...

-
-
-
-

负面

-

{sentimentData.negative}%

+ ) : sentimentError ? ( +
+ +

{sentimentError}

-
-

中性

-

{sentimentData.neutral}%

-
-
-

正面

-

{sentimentData.positive}%

-
-
+ ) : ( + <> +
+
+
+
+
+
+
+

负面

+

{sentimentData.negative.toFixed(1)}%

+
+
+

中性

+

{sentimentData.neutral.toFixed(1)}%

+
+
+

正面

+

{sentimentData.positive.toFixed(1)}%

+
+
+ + )}
{/* 热门文章 */} @@ -1705,4 +1955,29 @@ const Analytics: React.FC = () => { ); }; -export default Analytics; \ No newline at end of file +export default Analytics; + +// 辅助函数:获取平台的十六进制颜色代码 +const getPlatformColorHex = (platform: string): string => { + const platformLower = platform.toLowerCase(); + switch (platformLower) { + case 'facebook': + return '#1877F2'; + case 'threads': + return '#000000'; + case 'twitter': + return '#1DA1F2'; + case 'instagram': + return '#E4405F'; + case 'linkedin': + return '#0A66C2'; + case 'xiaohongshu': + return '#FF0000'; + case 'youtube': + return '#FF0000'; + case 'tiktok': + return '#000000'; + default: + return '#808080'; // 默认灰色 + } +}; \ No newline at end of file