morte change

This commit is contained in:
2025-03-14 21:10:50 +08:00
parent 5a03323c69
commit 942fb592b5
6 changed files with 802 additions and 271 deletions

View File

@@ -193,6 +193,25 @@ interface CommentTrendResponse {
error?: string;
}
// 添加审核状态分布API响应接口
interface ModerationStatusResponse {
success: boolean;
data: {
statuses: {
approved: number;
pending: number;
rejected: number;
};
percentages: {
approved: number;
pending: number;
rejected: number;
};
total: number;
};
error?: string;
}
// 添加热门文章API响应接口
interface PopularPostsResponse {
success: boolean;
@@ -238,6 +257,10 @@ const Analytics: React.FC = () => {
const [trendError, setTrendError] = useState<string | null>(null);
const [maxTimelineCount, setMaxTimelineCount] = useState(1); // 设置默认值为1避免除以零
// 添加审核状态分布相关状态
const [moderationLoading, setModerationLoading] = useState(true);
const [moderationError, setModerationError] = useState<string | null>(null);
// 添加项目相关状态
const [projects, setProjects] = useState<Project[]>([
{ id: '1', name: '项目 1', description: '示例项目 1' },
@@ -290,6 +313,102 @@ const Analytics: React.FC = () => {
const [postsLoading, setPostsLoading] = useState(true);
const [postsError, setPostsError] = useState<string | null>(null);
// 添加获取审核状态分布数据的函数
const fetchModerationStatus = async () => {
try {
setModerationLoading(true);
setModerationError(null);
// 构建API URL
const url = `http://localhost:4000/api/analytics/moderation-status?timeRange=${timeRange}`;
// 添加项目过滤参数(如果选择了特定项目)
const urlWithFilters = selectedProject !== 'all'
? `${url}&projectId=${selectedProject}`
: url;
// 添加内容类型
const finalUrl = `${urlWithFilters}&contentType=all`;
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 ModerationStatusResponse;
console.log('成功获取审核状态分布数据:', result);
if (result.success) {
// 将API返回的数据映射到AnalyticsData结构
const mappedData: AnalyticsData[] = [
{
name: 'approved',
value: result.data.statuses.approved,
color: '#10B981',
percentage: result.data.percentages.approved
},
{
name: 'pending',
value: result.data.statuses.pending,
color: '#F59E0B',
percentage: result.data.percentages.pending
},
{
name: 'rejected',
value: result.data.statuses.rejected,
color: '#EF4444',
percentage: result.data.percentages.rejected
},
];
// 更新状态
setStatusData(mappedData);
} else {
setModerationError(result.error || '获取审核状态分布数据失败');
console.error('API调用失败:', result.error || '未知错误');
// 设置默认数据
setStatusData([
{ name: 'approved', value: 45, color: '#10B981', percentage: 45 },
{ name: 'pending', value: 30, color: '#F59E0B', percentage: 30 },
{ name: 'rejected', value: 25, color: '#EF4444', percentage: 25 }
]);
}
} else {
const errorText = await response.text();
setModerationError(`获取失败 (${response.status}): ${errorText}`);
console.error('获取审核状态分布数据失败HTTP状态:', response.status, errorText);
// 设置默认数据
setStatusData([
{ name: 'approved', value: 45, color: '#10B981', percentage: 45 },
{ name: 'pending', value: 30, color: '#F59E0B', percentage: 30 },
{ name: 'rejected', value: 25, color: '#EF4444', percentage: 25 }
]);
}
} catch (error) {
setModerationError(`获取审核状态分布数据时发生错误: ${error instanceof Error ? error.message : String(error)}`);
console.error('获取审核状态分布数据时发生错误:', error);
// 设置默认数据
setStatusData([
{ name: 'approved', value: 45, color: '#10B981', percentage: 45 },
{ name: 'pending', value: 30, color: '#F59E0B', percentage: 30 },
{ name: 'rejected', value: 25, color: '#EF4444', percentage: 25 }
]);
} finally {
setModerationLoading(false);
}
};
// 获取KOL概览数据
const fetchKolOverviewData = async () => {
setKolLoading(true);
@@ -380,17 +499,15 @@ const Analytics: React.FC = () => {
// 获取热门文章数据
fetchPopularPosts();
// 获取审核状态分布数据
fetchModerationStatus();
const fetchAnalyticsData = async () => {
try {
setLoading(true);
// Set mock status data
setStatusData([
{ name: 'approved', value: 45, color: '#10B981' },
{ name: 'pending', value: 30, color: '#F59E0B' },
{ name: 'rejected', value: 25, color: '#EF4444' }
]);
// 不再使用模拟审核状态数据通过API获取
// 已移至 fetchModerationStatus 函数中
// 从API获取漏斗数据
try {
@@ -436,15 +553,15 @@ const Analytics: React.FC = () => {
// 如果API返回了模拟数据标志
if (result.is_mock_data) {
console.info('注意: 使用的是模拟漏斗数据');
}
} else {
console.error('API返回的数据格式不正确:', result);
// 使用模拟数据作为后备
setFallbackFunnelData();
}
} else {
console.error('API调用失败:', result.error || '未知错误');
console.error('API返回的数据格式不正确:', result);
// 使用模拟数据作为后备
setFallbackFunnelData();
}
} else {
console.error('API调用失败:', result.error || '未知错误');
// 使用模拟数据作为后备
setFallbackFunnelData();
}
} else {
@@ -469,14 +586,14 @@ const Analytics: React.FC = () => {
// 添加辅助函数,设置后备漏斗数据
const setFallbackFunnelData = () => {
setFunnelData([
{ stage: 'Awareness', count: 10000, rate: 100 },
{ stage: 'Interest', count: 7500, rate: 75 },
{ stage: 'Consideration', count: 5000, rate: 50 },
{ stage: 'Intent', count: 3000, rate: 30 },
{ stage: 'Evaluation', count: 2000, rate: 20 },
{ stage: 'Purchase', count: 1000, rate: 10 }
]);
setFunnelData([
{ stage: 'Awareness', count: 10000, rate: 100 },
{ stage: 'Interest', count: 7500, rate: 75 },
{ stage: 'Consideration', count: 5000, rate: 50 },
{ stage: 'Intent', count: 3000, rate: 30 },
{ stage: 'Evaluation', count: 2000, rate: 20 },
{ stage: 'Purchase', count: 1000, rate: 10 }
]);
};
fetchAnalyticsData();
@@ -562,12 +679,12 @@ const Analytics: React.FC = () => {
}
} else {
console.error('API返回的数据格式不正确:', result);
setFilteredEngagementData([]);
setFilteredEngagementData([]);
}
} else {
} else {
console.error('API调用失败:', result.error || '未知错误');
setFilteredEngagementData([]);
}
setFilteredEngagementData([]);
}
} else {
console.error('获取贴文表现数据失败HTTP状态:', response.status);
const errorText = await response.text();
@@ -888,7 +1005,7 @@ const Analytics: React.FC = () => {
<select
id="project-select"
value={selectedProject}
onChange={(e) => setSelectedProject(e.target.value)}
onChange={(e) => handleProjectChange(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"
>
<option value="all"></option>
@@ -1323,6 +1440,41 @@ const Analytics: React.FC = () => {
}
};
const handleTimeRangeChange = (range: string) => {
setTimeRange(range);
// 后续刷新数据
fetchDashboardCards();
fetchCommentTrend();
fetchPlatformDistribution();
fetchSentimentAnalysis();
fetchPopularPosts();
fetchModerationStatus(); // 添加刷新审核状态数据
};
// 项目选择变化处理函数
const handleProjectChange = (projectId: string) => {
setSelectedProject(projectId);
// 刷新数据
fetchDashboardCards();
fetchCommentTrend();
fetchPlatformDistribution();
fetchSentimentAnalysis();
fetchPopularPosts();
fetchModerationStatus(); // 添加刷新审核状态数据
};
// 平台选择变化处理函数
const handlePlatformChange = (platform: string) => {
setSelectedPlatform(platform);
// 刷新数据
fetchDashboardCards();
fetchCommentTrend();
fetchPlatformDistribution();
fetchSentimentAnalysis();
fetchPopularPosts();
fetchModerationStatus(); // 添加刷新审核状态数据
};
return (
<div className="flex-1 overflow-auto">
<div className="p-6">
@@ -1356,7 +1508,7 @@ const Analytics: React.FC = () => {
<select
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
onChange={(e) => handleTimeRangeChange(e.target.value)}
>
<option value="7"> 7 </option>
<option value="30"> 30 </option>
@@ -1377,7 +1529,7 @@ const Analytics: React.FC = () => {
<select
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={selectedPlatform}
onChange={(e) => setSelectedPlatform(e.target.value)}
onChange={(e) => handlePlatformChange(e.target.value)}
>
<option value="all"></option>
<option value="facebook">Facebook</option>
@@ -1425,7 +1577,7 @@ const Analytics: React.FC = () => {
<div className="flex items-center text-sm text-blue-600 cursor-pointer">
<span className="mr-1"></span>
<ArrowRight className="w-4 h-4" />
<ArrowRight className="w-4 h-4" />
</div>
</div>
</div>
@@ -1434,12 +1586,12 @@ const Analytics: React.FC = () => {
<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">KOL数据中...</p>
</div>
</div>
) : kolError ? (
<div className="p-6 text-center border border-red-200 rounded-lg bg-red-50">
<div className="mb-4 text-red-500">
<AlertTriangle className="inline-block w-12 h-12" />
</div>
</div>
<h3 className="mb-2 text-lg font-medium text-red-700">KOL数据</h3>
<p className="text-red-600">{kolError}</p>
<button
@@ -1482,9 +1634,9 @@ const Analytics: React.FC = () => {
{getPlatformIcon(kol.platform)}
<span className="ml-1">{kol.platform}</span>
</div>
</div>
</div>
</div>
</div>
{/* KOL指标 */}
<div className="space-y-3">
{/* 粉丝增长 */}
@@ -1650,117 +1802,117 @@ const Analytics: React.FC = () => {
<p className="text-gray-500"></p>
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
KOL
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
KOL
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredEngagementData.map((post, index) => (
<tr key={post.id} className="hover:bg-gray-50">
<td className="px-6 py-4">
<div className="flex items-center">
<div className="flex-shrink-0 w-12 h-12 mr-3 overflow-hidden rounded">
<img src={post.thumbnail} alt={post.title} className="object-cover w-full h-full" />
</div>
<div className="max-w-xs text-sm text-gray-900 truncate">{post.title}</div>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredEngagementData.map((post, index) => (
<tr key={post.id} className="hover:bg-gray-50">
<td className="px-6 py-4">
<div className="flex items-center">
<div className="flex-shrink-0 w-12 h-12 mr-3 overflow-hidden rounded">
<img src={post.thumbnail} alt={post.title} className="object-cover w-full h-full" />
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="w-8 h-8 mr-2 overflow-hidden rounded-full">
<img
<div className="max-w-xs text-sm text-gray-900 truncate">{post.title}</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="w-8 h-8 mr-2 overflow-hidden rounded-full">
<img
src={kolData.find(k => k.influencer_id === post.kolId)?.avatar || ''}
alt={kolData.find(k => k.influencer_id === post.kolId)?.name || ''}
className="object-cover w-full h-full"
/>
</div>
<div className="text-sm text-gray-900">
className="object-cover w-full h-full"
/>
</div>
<div className="text-sm text-gray-900">
{kolData.find(k => k.influencer_id === post.kolId)?.name || ''}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
{getPlatformIcon(post.platform)}
<span className="ml-2 text-sm text-gray-900">
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
{getPlatformIcon(post.platform)}
<span className="ml-2 text-sm text-gray-900">
{post.platform === 'xiaohongshu' ? '小红书' : post.platform}
</span>
</div>
</td>
<td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
{post.date}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-900">
<Eye className="w-4 h-4 mr-1 text-gray-500" />
{post.views.toLocaleString()}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-900">
<Heart className="w-4 h-4 mr-1 text-red-500" />
{post.likes.toLocaleString()}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-900">
<MessageSquare className="w-4 h-4 mr-1 text-blue-500" />
{post.comments.toLocaleString()}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-900">
<Share2 className="w-4 h-4 mr-1 text-green-500" />
{post.shares.toLocaleString()}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
</span>
</div>
</td>
<td className="px-6 py-4 text-sm text-gray-500 whitespace-nowrap">
{post.date}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-900">
<Eye className="w-4 h-4 mr-1 text-gray-500" />
{post.views.toLocaleString()}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-900">
<Heart className="w-4 h-4 mr-1 text-red-500" />
{post.likes.toLocaleString()}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-900">
<MessageSquare className="w-4 h-4 mr-1 text-blue-500" />
{post.comments.toLocaleString()}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center text-sm text-gray-900">
<Share2 className="w-4 h-4 mr-1 text-green-500" />
{post.shares.toLocaleString()}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div
className="w-16 h-2 overflow-hidden bg-gray-200 rounded-full"
>
<div
className="w-16 h-2 overflow-hidden bg-gray-200 rounded-full"
>
<div
className={getSentimentColor(post.sentiment)}
style={{ width: `${post.sentimentScore}%` }}
></div>
</div>
<span className="ml-2 text-sm text-gray-900">{post.sentimentScore}%</span>
className={getSentimentColor(post.sentiment)}
style={{ width: `${post.sentimentScore}%` }}
></div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<span className="ml-2 text-sm text-gray-900">{post.sentimentScore}%</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
@@ -1780,10 +1932,10 @@ const Analytics: React.FC = () => {
) : (
<>
<p className="mb-2 text-3xl font-bold text-gray-900">{dashboardCards.commentsCount.current.toLocaleString()}</p>
<div className="flex items-center text-sm">
<div className="flex items-center text-sm">
{dashboardCards.commentsCount.changePercentage > 0 ? (
<>
<TrendingUp className="w-4 h-4 mr-1 text-green-500" />
<TrendingUp className="w-4 h-4 mr-1 text-green-500" />
<span className="text-green-500"> {dashboardCards.commentsCount.changePercentage.toFixed(1)}% </span>
</>
) : (
@@ -1792,7 +1944,7 @@ const Analytics: React.FC = () => {
<span className="text-red-500"> {Math.abs(dashboardCards.commentsCount.changePercentage).toFixed(1)}% </span>
</>
)}
</div>
</div>
</>
)}
</div>
@@ -1811,10 +1963,10 @@ const Analytics: React.FC = () => {
) : (
<>
<p className="mb-2 text-3xl font-bold text-gray-900">{(dashboardCards.engagementRate.current / 100).toFixed(1)}%</p>
<div className="flex items-center text-sm">
<div className="flex items-center text-sm">
{dashboardCards.engagementRate.changePercentage > 0 ? (
<>
<TrendingUp className="w-4 h-4 mr-1 text-green-500" />
<TrendingUp className="w-4 h-4 mr-1 text-green-500" />
<span className="text-green-500"> {dashboardCards.engagementRate.changePercentage.toFixed(1)}% </span>
</>
) : (
@@ -1823,7 +1975,7 @@ const Analytics: React.FC = () => {
<span className="text-red-500"> {Math.abs(dashboardCards.engagementRate.changePercentage).toFixed(1)}% </span>
</>
)}
</div>
</div>
</>
)}
</div>
@@ -1841,11 +1993,11 @@ const Analytics: React.FC = () => {
<div className="text-sm text-red-500">{cardsError}</div>
) : (
<>
<p className="mb-2 text-3xl font-bold text-gray-900">{sentimentData.positive}% </p>
<div className="flex items-center text-sm">
<p className="mb-2 text-3xl font-bold text-gray-900">{sentimentData.positive}% </p>
<div className="flex items-center text-sm">
{dashboardCards.sentimentScore.changePercentage > 0 ? (
<>
<TrendingUp className="w-4 h-4 mr-1 text-green-500" />
<TrendingUp className="w-4 h-4 mr-1 text-green-500" />
<span className="text-green-500"> {dashboardCards.sentimentScore.changePercentage.toFixed(1)}% </span>
</>
) : (
@@ -1854,7 +2006,7 @@ const Analytics: React.FC = () => {
<span className="text-red-500"> {Math.abs(dashboardCards.sentimentScore.changePercentage).toFixed(1)}% </span>
</>
)}
</div>
</div>
</>
)}
</div>
@@ -1889,7 +2041,7 @@ const Analytics: React.FC = () => {
<div className="flex flex-col h-64">
{/* 完全重构的柱状图实现 */}
<div className="flex items-end justify-between w-full h-48">
{timelineData.map((item, index) => (
{timelineData.map((item, index) => (
<div key={index} className="flex flex-col items-center w-full">
{/* 留言数标签 */}
<div className="mb-1 text-xs text-gray-500">{item.comments}</div>
@@ -1897,28 +2049,28 @@ const Analytics: React.FC = () => {
{/* 柱状图 - 使用绝对高度值而非百分比 */}
<div
className="relative w-4/5 transition-all duration-300 bg-blue-500 hover:bg-blue-600 rounded-t-md"
style={{
style={{
height: item.comments === 0 ? '4px' : `${Math.max(8, Math.min(180, item.comments / 5))}px`
}}
>
}}
>
{/* 悬停提示 */}
<div className="absolute hidden px-2 py-1 mb-1 text-xs text-white transform -translate-x-1/2 bg-gray-800 rounded group-hover:block bottom-full left-1/2 whitespace-nowrap">
{item.comments}
</div>
</div>
</div>
{/* 日期标签 */}
<div className="mt-2 text-xs text-center text-gray-500">
{new Date(item.date).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })}
</div>
</div>
))}
</div>
</div>
))}
</div>
{/* 图表说明 */}
<div className="mt-2 text-xs text-center text-gray-400">
</div>
</div>
</div>
)}
</div>
@@ -1943,84 +2095,103 @@ const Analytics: React.FC = () => {
<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">
<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>
</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 className="w-full h-2 bg-gray-200 rounded-full">
<div
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="p-6 bg-white rounded-lg shadow">
<h3 className="mb-4 text-lg font-medium text-gray-800"></h3>
{moderationLoading ? (
<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>
) : moderationError ? (
<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">{moderationError}</p>
</div>
) : statusData.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 p-4 rounded-lg bg-gray-50">
<AlertTriangle className="w-10 h-10 mb-2 text-gray-400" />
<p className="text-center text-gray-600"></p>
</div>
) : (
<>
<div className="flex justify-center mb-6">
<div className="relative w-48 h-48 rounded-full">
{statusData.map((item, index) => {
// 计算每个扇形的起始角度和结束角度
const startAngle = index === 0 ? 0 : statusData.slice(0, index).reduce((sum, i) => sum + (i.percentage || 0), 0) * 3.6;
const endAngle = startAngle + (item.percentage || 0) * 3.6;
return (
<div
key={index}
className="absolute inset-0"
style={{
background: `conic-gradient(transparent ${startAngle}deg, ${getStatusColor(item.name)} ${startAngle}deg, ${getStatusColor(item.name)} ${endAngle}deg, transparent ${endAngle}deg)`,
borderRadius: '50%'
}}
></div>
);
})}
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-32 h-32 bg-white rounded-full"></div>
</div>
</div>
</div>
<div className="space-y-2">
{statusData.map((item, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center">
{getStatusIcon(item.name)}
<span className="ml-2 text-sm font-medium text-gray-700">{getStatusName(item.name)}</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 className="w-full h-2 bg-gray-200 rounded-full">
<div
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>
{/* 审核状态分布 */}
{/* <div className="p-6 bg-white rounded-lg shadow">
<h3 className="mb-4 text-lg font-medium text-gray-800">审核状态分布</h3>
<div className="flex justify-center mb-6">
<div className="relative w-48 h-48 rounded-full">
{statusData.map((item, index) => {
// 计算每个扇形的起始角度和结束角度
const startAngle = index === 0 ? 0 : statusData.slice(0, index).reduce((sum, i) => sum + i.percentage, 0) * 3.6;
const endAngle = startAngle + item.percentage * 3.6;
return (
<div
key={index}
className="absolute inset-0"
style={{
background: `conic-gradient(transparent ${startAngle}deg, ${getStatusColor(item.name)} ${startAngle}deg, ${getStatusColor(item.name)} ${endAngle}deg, transparent ${endAngle}deg)`,
borderRadius: '50%'
}}
></div>
);
})}
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-32 h-32 bg-white rounded-full"></div>
</div>
</div>
</div>
<div className="space-y-2">
{statusData.map((item, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex items-center">
{getStatusIcon(item.name)}
<span className="ml-2 text-sm font-medium text-gray-700">{getStatusName(item.name)}</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>
</div>
</div>
))}
</div>
</div> */}
</div>
{/* 情感分析详情 */}
<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>
{sentimentLoading ? (
<div className="flex flex-col items-center justify-center h-64">
@@ -2034,34 +2205,34 @@ const Analytics: React.FC = () => {
</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"
<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: `${sentimentScore * 100}%` }}
></div>
</div>
></div>
</div>
<div className="grid grid-cols-3 gap-4 text-center">
<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>
<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>
{/* 热门文章 */}
<div className="p-6 mt-8 bg-white rounded-lg shadow">
<div className="p-6 mt-8 bg-white rounded-lg shadow" style={{marginTop: '20px'}}>
<h3 className="mb-4 text-lg font-medium text-gray-800"></h3>
{postsLoading ? (
<div className="flex flex-col items-center justify-center h-64">
@@ -2090,25 +2261,25 @@ const Analytics: React.FC = () => {
</span>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center">
<div className="flex items-center">
<Eye className="w-3 h-3 mr-1 text-gray-400" />
<span>{article.views.toLocaleString()} </span>
</div>
</div>
<div className="flex items-center">
<MessageSquare className="w-3 h-3 mr-1 text-gray-400" />
<span>{article.engagement.toLocaleString()} </span>
</div>
</div>
{article.isHighEngagement && (
<div className="flex items-center">
<div className="w-2 h-2 mr-1 bg-green-500 rounded-full"></div>
<span></span>
</div>
)}
</div>
</div>
</div>
))}
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
</>