sentiment analysisi

This commit is contained in:
2025-03-14 16:00:10 +08:00
parent a2f5261523
commit a345bf9cac

View File

@@ -138,6 +138,43 @@ interface DashboardCardsResponse {
error?: string; 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响应接口 // 添加留言趋势API响应接口
interface CommentTrendResponse { interface CommentTrendResponse {
success: boolean; success: boolean;
@@ -220,6 +257,12 @@ const Analytics: React.FC = () => {
const [cardsLoading, setCardsLoading] = useState(true); const [cardsLoading, setCardsLoading] = useState(true);
const [cardsError, setCardsError] = useState<string | null>(null); const [cardsError, setCardsError] = useState<string | null>(null);
const [platformLoading, setPlatformLoading] = useState(true);
const [platformError, setPlatformError] = useState<string | null>(null);
const [sentimentLoading, setSentimentLoading] = useState(true);
const [sentimentError, setSentimentError] = useState<string | null>(null);
const [sentimentScore, setSentimentScore] = useState(0);
// 获取KOL概览数据 // 获取KOL概览数据
const fetchKolOverviewData = async () => { const fetchKolOverviewData = async () => {
setKolLoading(true); setKolLoading(true);
@@ -301,21 +344,17 @@ const Analytics: React.FC = () => {
// 获取留言趋势数据 // 获取留言趋势数据
fetchCommentTrend(); fetchCommentTrend();
// 获取平台分布数据
fetchPlatformDistribution();
// 获取情感分析数据
fetchSentimentAnalysis();
const fetchAnalyticsData = async () => { const fetchAnalyticsData = async () => {
try { try {
setLoading(true); setLoading(true);
// Set default platform distribution data // 删除平台分布的硬编码数据使用API数据替代
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 });
// Set mock status data // Set mock status data
setStatusData([ 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<string, { count: number; percentage: number; color: string }>();
// 为不同平台分配颜色
const platformColors: Record<string, string> = {
'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 ( return (
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<div className="p-6"> <div className="p-6">
@@ -1584,30 +1797,53 @@ const Analytics: React.FC = () => {
{/* 平台分布 */} {/* 平台分布 */}
<div className="p-6 bg-white rounded-lg shadow"> <div className="p-6 bg-white rounded-lg shadow">
<h3 className="mb-4 text-lg font-medium text-gray-800"></h3> <h3 className="mb-4 text-lg font-medium text-gray-800"></h3>
<div className="space-y-4"> {platformLoading ? (
{platformData.map((item, index) => ( <div className="flex flex-col items-center justify-center h-64">
<div key={index}> <div className="w-12 h-12 border-4 border-blue-500 rounded-full border-t-transparent animate-spin"></div>
<div className="flex items-center justify-between mb-1"> <p className="mt-4 text-gray-600">...</p>
<div className="flex items-center"> </div>
{getPlatformIcon(item.name)} ) : platformError ? (
<span className="ml-2 text-sm font-medium text-gray-700"> <div className="flex flex-col items-center justify-center h-64 p-4 rounded-lg bg-red-50">
{item.name === 'xiaohongshu' ? '小红书' : item.name} <AlertTriangle className="w-10 h-10 mb-2 text-red-500" />
</span> <p className="text-center text-red-600">{platformError}</p>
</div>
) : platformData.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 p-4 rounded-lg bg-gray-50">
<PieChart className="w-10 h-10 mb-2 text-gray-400" />
<p className="text-center text-gray-600"></p>
</div>
) : (
<div className="space-y-4">
{platformData.map((item, index) => (
<div key={index}>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center">
{getPlatformIcon(item.name)}
<span className="ml-2 text-sm font-medium text-gray-700">
{item.name === 'xiaohongshu' ? '小红书' :
item.name === 'youtube' ? 'YouTube' :
item.name === 'tiktok' ? 'TikTok' :
item.name.charAt(0).toUpperCase() + item.name.slice(1)}
</span>
</div>
<div className="flex items-center">
<span className="mr-2 text-sm text-gray-500">{item.value} </span>
<span className="text-sm font-medium text-gray-700">{item.percentage?.toFixed(1)}%</span>
</div>
</div> </div>
<div className="flex items-center"> <div className="w-full h-2 bg-gray-200 rounded-full">
<span className="mr-2 text-sm text-gray-500">{item.value} </span> <div
<span className="text-sm font-medium text-gray-700">{item.percentage}%</span> className={`h-2 rounded-full transition-all duration-500 ease-in-out`}
style={{
width: `${item.percentage}%`,
backgroundColor: item.color || getPlatformColorHex(item.name)
}}
></div>
</div> </div>
</div> </div>
<div className="w-full h-2 bg-gray-200 rounded-full"> ))}
<div </div>
className={`${getPlatformColor(item.name)} h-2 rounded-full transition-all duration-500 ease-in-out`} )}
style={{ width: `${item.percentage}%` }}
></div>
</div>
</div>
))}
</div>
</div> </div>
{/* 审核状态分布 */} {/* 审核状态分布 */}
@@ -1656,28 +1892,42 @@ const Analytics: React.FC = () => {
{/* 情感分析详情 */} {/* 情感分析详情 */}
<div className="p-6 bg-white rounded-lg shadow"> <div className="p-6 bg-white rounded-lg shadow">
<h3 className="mb-4 text-lg font-medium text-gray-800"></h3> <h3 className="mb-4 text-lg font-medium text-gray-800"></h3>
<div className="flex justify-center mb-6"> {sentimentLoading ? (
<div className="relative w-48 h-12 rounded-lg bg-gradient-to-r from-red-500 via-yellow-400 to-green-500"> <div className="flex flex-col items-center justify-center h-64">
<div <div className="w-12 h-12 border-4 border-blue-500 rounded-full border-t-transparent animate-spin"></div>
className="absolute top-0 w-1 h-full transform -translate-x-1/2 bg-black border-2 border-white rounded-full" <p className="mt-4 text-gray-600">...</p>
style={{ left: `${sentimentData.positive}%` }}
></div>
</div> </div>
</div> ) : sentimentError ? (
<div className="grid grid-cols-3 gap-4 text-center"> <div className="flex flex-col items-center justify-center h-64 p-4 rounded-lg bg-red-50">
<div> <AlertTriangle className="w-10 h-10 mb-2 text-red-500" />
<p className="text-sm font-medium text-gray-500"></p> <p className="text-center text-red-600">{sentimentError}</p>
<p className="text-lg font-bold text-red-500">{sentimentData.negative}%</p>
</div> </div>
<div> ) : (
<p className="text-sm font-medium text-gray-500"></p> <>
<p className="text-lg font-bold text-yellow-500">{sentimentData.neutral}%</p> <div className="flex justify-center mb-6">
</div> <div className="relative w-48 h-12 rounded-lg bg-gradient-to-r from-red-500 via-yellow-400 to-green-500">
<div> <div
<p className="text-sm font-medium text-gray-500"></p> className="absolute top-0 w-1 h-full transform -translate-x-1/2 bg-black border-2 border-white rounded-full"
<p className="text-lg font-bold text-green-500">{sentimentData.positive}%</p> style={{ left: `${sentimentScore * 100}%` }}
</div> ></div>
</div> </div>
</div>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p className="text-lg font-bold text-red-500">{sentimentData.negative.toFixed(1)}%</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p className="text-lg font-bold text-yellow-500">{sentimentData.neutral.toFixed(1)}%</p>
</div>
<div>
<p className="text-sm font-medium text-gray-500"></p>
<p className="text-lg font-bold text-green-500">{sentimentData.positive.toFixed(1)}%</p>
</div>
</div>
</>
)}
</div> </div>
{/* 热门文章 */} {/* 热门文章 */}
@@ -1706,3 +1956,28 @@ const Analytics: React.FC = () => {
}; };
export default Analytics; 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'; // 默认灰色
}
};