sentiment analysisi
This commit is contained in:
@@ -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<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概览数据
|
||||
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<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 (
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className="p-6">
|
||||
@@ -1584,6 +1797,22 @@ const Analytics: React.FC = () => {
|
||||
{/* 平台分布 */}
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h3 className="mb-4 text-lg font-medium text-gray-800">平台分布</h3>
|
||||
{platformLoading ? (
|
||||
<div className="flex flex-col items-center justify-center h-64">
|
||||
<div className="w-12 h-12 border-4 border-blue-500 rounded-full border-t-transparent animate-spin"></div>
|
||||
<p className="mt-4 text-gray-600">加载平台分布数据中...</p>
|
||||
</div>
|
||||
) : platformError ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 p-4 rounded-lg bg-red-50">
|
||||
<AlertTriangle className="w-10 h-10 mb-2 text-red-500" />
|
||||
<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}>
|
||||
@@ -1591,23 +1820,30 @@ const Analytics: React.FC = () => {
|
||||
<div className="flex items-center">
|
||||
{getPlatformIcon(item.name)}
|
||||
<span className="ml-2 text-sm font-medium text-gray-700">
|
||||
{item.name === 'xiaohongshu' ? '小红书' : item.name}
|
||||
{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}%</span>
|
||||
<span className="text-sm font-medium text-gray-700">{item.percentage?.toFixed(1)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-2 bg-gray-200 rounded-full">
|
||||
<div
|
||||
className={`${getPlatformColor(item.name)} h-2 rounded-full transition-all duration-500 ease-in-out`}
|
||||
style={{ width: `${item.percentage}%` }}
|
||||
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>
|
||||
|
||||
{/* 审核状态分布 */}
|
||||
@@ -1656,28 +1892,42 @@ const Analytics: React.FC = () => {
|
||||
{/* 情感分析详情 */}
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h3 className="mb-4 text-lg font-medium text-gray-800">情感分析详情</h3>
|
||||
{sentimentLoading ? (
|
||||
<div className="flex flex-col items-center justify-center h-64">
|
||||
<div className="w-12 h-12 border-4 border-blue-500 rounded-full border-t-transparent animate-spin"></div>
|
||||
<p className="mt-4 text-gray-600">加载情感分析数据中...</p>
|
||||
</div>
|
||||
) : sentimentError ? (
|
||||
<div className="flex flex-col items-center justify-center h-64 p-4 rounded-lg bg-red-50">
|
||||
<AlertTriangle className="w-10 h-10 mb-2 text-red-500" />
|
||||
<p className="text-center text-red-600">{sentimentError}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex justify-center mb-6">
|
||||
<div className="relative w-48 h-12 rounded-lg bg-gradient-to-r from-red-500 via-yellow-400 to-green-500">
|
||||
<div
|
||||
className="absolute top-0 w-1 h-full transform -translate-x-1/2 bg-black border-2 border-white rounded-full"
|
||||
style={{ left: `${sentimentData.positive}%` }}
|
||||
style={{ left: `${sentimentScore * 100}%` }}
|
||||
></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}%</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}%</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}%</p>
|
||||
<p className="text-lg font-bold text-green-500">{sentimentData.positive.toFixed(1)}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 热门文章 */}
|
||||
@@ -1706,3 +1956,28 @@ const Analytics: React.FC = () => {
|
||||
};
|
||||
|
||||
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'; // 默认灰色
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user