post perfermance fornt
This commit is contained in:
@@ -64,6 +64,8 @@ interface EngagementData {
|
|||||||
likes: number;
|
likes: number;
|
||||||
comments: number;
|
comments: number;
|
||||||
shares: number;
|
shares: number;
|
||||||
|
sentiment?: string;
|
||||||
|
sentimentScore?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新KOL数据接口,与API接口结构匹配
|
// 更新KOL数据接口,与API接口结构匹配
|
||||||
@@ -136,6 +138,7 @@ const Analytics: React.FC = () => {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [kolError, setKolError] = useState<string | null>(null); // 新增KOL数据错误状态
|
const [kolError, setKolError] = useState<string | null>(null); // 新增KOL数据错误状态
|
||||||
const [filteredEngagementData, setFilteredEngagementData] = useState<EngagementData[]>([]);
|
const [filteredEngagementData, setFilteredEngagementData] = useState<EngagementData[]>([]);
|
||||||
|
const [postDataLoading, setPostDataLoading] = useState(true); // 添加贴文数据加载状态
|
||||||
|
|
||||||
// 添加项目相关状态
|
// 添加项目相关状态
|
||||||
const [projects, setProjects] = useState<Project[]>([
|
const [projects, setProjects] = useState<Project[]>([
|
||||||
@@ -239,6 +242,9 @@ const Analytics: React.FC = () => {
|
|||||||
// 获取KOL概览数据
|
// 获取KOL概览数据
|
||||||
fetchKolOverviewData();
|
fetchKolOverviewData();
|
||||||
|
|
||||||
|
// 获取贴文表现数据
|
||||||
|
fetchPostPerformanceData();
|
||||||
|
|
||||||
const fetchAnalyticsData = async () => {
|
const fetchAnalyticsData = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -385,14 +391,98 @@ const Analytics: React.FC = () => {
|
|||||||
// 不需要额外排序,API已经排序
|
// 不需要额外排序,API已经排序
|
||||||
const sortedKOLs = filteredKOLs;
|
const sortedKOLs = filteredKOLs;
|
||||||
|
|
||||||
|
// 定义函数获取贴文表现数据 - 将函数移到组件级别便于多处调用
|
||||||
|
const fetchPostPerformanceData = async () => {
|
||||||
|
try {
|
||||||
|
setPostDataLoading(true); // 设置加载状态
|
||||||
|
setFilteredEngagementData([]); // 重置现有数据
|
||||||
|
|
||||||
|
// 构建API URL - 不添加任何查询参数,因为默认返回已经按照engagement排序
|
||||||
|
let url = `http://localhost:4000/api/analytics/post-performance`;
|
||||||
|
|
||||||
|
console.log('请求贴文表现数据URL:', url);
|
||||||
|
|
||||||
|
// 添加认证头
|
||||||
|
const authToken = 'eyJhbGciOiJIUzI1NiIsImtpZCI6Inl3blNGYnRBOGtBUnl4UmUiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3h0cWhsdXpvcm5hemxta29udWNyLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiI1YjQzMThiZi0yMWE4LTQ3YWMtOGJmYS0yYThmOGVmOWMwZmIiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzQxNjI3ODkyLCJpYXQiOjE3NDE2MjQyOTIsImVtYWlsIjoidml0YWxpdHltYWlsZ0BnbWFpbC5jb20iLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7ImVtYWlsX3ZlcmlmaWVkIjp0cnVlfSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc0MTYyNDI5Mn1dLCJzZXNzaW9uX2lkIjoiODlmYjg0YzktZmEzYy00YmVlLTk0MDQtNjI1MjE0OGIyMzVlIiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.VuUX2yhqN-FZseKL8fQG91i1cohfRqW2m1Z8CIWhZuk';
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'accept': 'application/json',
|
||||||
|
'Authorization': `Bearer ${authToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('成功获取贴文表现数据:', result);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 检查API响应中是否有数据
|
||||||
|
if (result.data && Array.isArray(result.data)) {
|
||||||
|
// 将API返回的数据映射到EngagementData结构 - 根据实际API响应格式调整
|
||||||
|
const mappedData = result.data.map((post: any) => ({
|
||||||
|
id: post.post_id || post.id || `post-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
title: post.title || '无标题贴文',
|
||||||
|
thumbnail: post.thumbnail || post.image_url || `https://via.placeholder.com/150?text=${post.platform}`,
|
||||||
|
platform: post.platform,
|
||||||
|
kolId: post.kol_id,
|
||||||
|
date: post.publish_date?.split(' ')[0] || new Date().toISOString().split('T')[0],
|
||||||
|
views: post.metrics?.views || 0,
|
||||||
|
likes: post.metrics?.likes || 0,
|
||||||
|
comments: post.metrics?.comments || 0,
|
||||||
|
shares: post.metrics?.shares || 0,
|
||||||
|
sentiment: post.sentiment || (post.sentiment_score > 0.6 ? 'positive' : post.sentiment_score > 0.4 ? 'neutral' : 'negative'),
|
||||||
|
sentimentScore: post.sentiment_score ? Math.round(post.sentiment_score * 100) : 50
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 根据KOL和平台过滤数据
|
||||||
|
let filteredData = mappedData;
|
||||||
|
|
||||||
|
// 如果选择了特定KOL,过滤数据
|
||||||
|
if (selectedKOL !== 'all') {
|
||||||
|
filteredData = filteredData.filter(post => post.kolId === selectedKOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果选择了特定平台,过滤数据
|
||||||
|
if (selectedPlatform !== 'all') {
|
||||||
|
filteredData = filteredData.filter(post =>
|
||||||
|
post.platform.toLowerCase() === selectedPlatform.toLowerCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredEngagementData(filteredData);
|
||||||
|
|
||||||
|
// 如果API返回了模拟数据标志
|
||||||
|
if (result.is_mock_data) {
|
||||||
|
console.info('注意: 使用的是模拟贴文表现数据');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('API返回的数据格式不正确:', result);
|
||||||
|
setFilteredEngagementData([]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('API调用失败:', result.error || '未知错误');
|
||||||
|
setFilteredEngagementData([]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('获取贴文表现数据失败,HTTP状态:', response.status);
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('错误详情:', errorText);
|
||||||
|
setFilteredEngagementData([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取贴文表现数据时发生错误:', error);
|
||||||
|
setFilteredEngagementData([]);
|
||||||
|
} finally {
|
||||||
|
setPostDataLoading(false); // 无论成功失败都结束加载状态
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Update filtered engagement data when KOL selection changes
|
// Update filtered engagement data when KOL selection changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedKOL === 'all') {
|
// 当KOL、平台或项目选择变化时,重新获取贴文数据
|
||||||
setFilteredEngagementData([]);
|
fetchPostPerformanceData();
|
||||||
} else {
|
}, [selectedKOL, selectedPlatform, selectedProject, timeRange]);
|
||||||
setFilteredEngagementData([]);
|
|
||||||
}
|
|
||||||
}, [selectedKOL]);
|
|
||||||
|
|
||||||
const getPlatformIcon = (platform: string) => {
|
const getPlatformIcon = (platform: string) => {
|
||||||
const platformLower = platform.toLowerCase();
|
const platformLower = platform.toLowerCase();
|
||||||
@@ -1025,117 +1115,132 @@ const Analytics: React.FC = () => {
|
|||||||
{/* KOL 贴文表现 */}
|
{/* KOL 贴文表现 */}
|
||||||
<div className="p-6 mb-8 bg-white rounded-lg shadow">
|
<div className="p-6 mb-8 bg-white rounded-lg shadow">
|
||||||
<h3 className="mb-6 text-lg font-medium text-gray-800">KOL 贴文表现</h3>
|
<h3 className="mb-6 text-lg font-medium text-gray-800">KOL 贴文表现</h3>
|
||||||
<div className="overflow-x-auto">
|
{postDataLoading ? (
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<div className="flex flex-col items-center justify-center h-64">
|
||||||
<thead className="bg-gray-50">
|
<div className="w-12 h-12 border-4 border-blue-500 rounded-full border-t-transparent animate-spin"></div>
|
||||||
<tr>
|
<p className="mt-4 text-gray-600">加载贴文数据中...</p>
|
||||||
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
</div>
|
||||||
贴文
|
) : filteredEngagementData.length === 0 ? (
|
||||||
</th>
|
<div className="p-6 text-center border border-gray-200 rounded-lg">
|
||||||
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
<div className="mb-4 text-gray-400">
|
||||||
KOL
|
<Eye className="inline-block w-12 h-12" />
|
||||||
</th>
|
</div>
|
||||||
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
<h3 className="mb-2 text-lg font-medium text-gray-700">没有找到贴文数据</h3>
|
||||||
平台
|
<p className="text-gray-500">请尝试更改筛选条件或检查所选项目是否有贴文数据。</p>
|
||||||
</th>
|
</div>
|
||||||
<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">
|
||||||
</th>
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
<thead className="bg-gray-50">
|
||||||
观看数
|
<tr>
|
||||||
</th>
|
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
||||||
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
贴文
|
||||||
赞数
|
</th>
|
||||||
</th>
|
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
||||||
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
KOL
|
||||||
留言数
|
</th>
|
||||||
</th>
|
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
||||||
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
平台
|
||||||
分享数
|
</th>
|
||||||
</th>
|
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
||||||
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
发布日期
|
||||||
情绪指标
|
</th>
|
||||||
</th>
|
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
||||||
</tr>
|
观看数
|
||||||
</thead>
|
</th>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
||||||
{filteredEngagementData.map((post, index) => (
|
赞数
|
||||||
<tr key={post.id} className="hover:bg-gray-50">
|
</th>
|
||||||
<td className="px-6 py-4">
|
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
||||||
<div className="flex items-center">
|
留言数
|
||||||
<div className="flex-shrink-0 w-12 h-12 mr-3 overflow-hidden rounded">
|
</th>
|
||||||
<img src={post.thumbnail} alt={post.title} className="object-cover w-full h-full" />
|
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
||||||
</div>
|
分享数
|
||||||
<div className="max-w-xs text-sm text-gray-900 truncate">{post.title}</div>
|
</th>
|
||||||
</div>
|
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase">
|
||||||
</td>
|
情绪指标
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
</th>
|
||||||
<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">
|
|
||||||
{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">
|
|
||||||
{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">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
</table>
|
{filteredEngagementData.map((post, index) => (
|
||||||
</div>
|
<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>
|
||||||
|
</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">
|
||||||
|
{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">
|
||||||
|
{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">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 概览卡片 */}
|
{/* 概览卡片 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user