diff --git a/backend/src/routes/analytics.ts b/backend/src/routes/analytics.ts index 6a6a5ef..9f7821a 100644 --- a/backend/src/routes/analytics.ts +++ b/backend/src/routes/analytics.ts @@ -1511,35 +1511,35 @@ analyticsRouter.get('/project/:id/conversion-funnel', async (c) => { .eq('id', projectId) .single(); - // 如果找不到项目或发生错误,返回模拟数据 - if (projectError) { - console.log(`项目未找到或数据库错误,返回模拟数据。项目ID: ${projectId}, 错误: ${projectError.message}`); + // // 如果找不到项目或发生错误,返回模拟数据 + // if (projectError) { + // console.log(`项目未找到或数据库错误,返回模拟数据。项目ID: ${projectId}, 错误: ${projectError.message}`); - // 生成模拟的漏斗数据 - const mockFunnelData = [ - { stage: 'Awareness', count: 100, rate: 100 }, - { stage: 'Interest', count: 75, rate: 75 }, - { stage: 'Consideration', count: 50, rate: 50 }, - { stage: 'Intent', count: 30, rate: 30 }, - { stage: 'Evaluation', count: 20, rate: 20 }, - { stage: 'Purchase', count: 10, rate: 10 } - ]; + // // 生成模拟的漏斗数据 + // const mockFunnelData = [ + // { stage: 'Awareness', count: 100, rate: 100 }, + // { stage: 'Interest', count: 75, rate: 75 }, + // { stage: 'Consideration', count: 50, rate: 50 }, + // { stage: 'Intent', count: 30, rate: 30 }, + // { stage: 'Evaluation', count: 20, rate: 20 }, + // { stage: 'Purchase', count: 10, rate: 10 } + // ]; - return c.json({ - project: { - id: projectId, - name: `模拟项目 (ID: ${projectId})` - }, - timeframe, - funnel_data: mockFunnelData, - metrics: { - total_influencers: 100, - conversion_rate: 10, - avg_stage_dropoff: 18 - }, - is_mock_data: true - }); - } + // return c.json({ + // project: { + // id: projectId, + // name: `模拟项目 (ID: ${projectId})` + // }, + // timeframe, + // funnel_data: mockFunnelData, + // metrics: { + // total_influencers: 100, + // conversion_rate: 10, + // avg_stage_dropoff: 18 + // }, + // is_mock_data: true + // }); + // } // 获取项目关联的网红及其详细信息 const { data: projectInfluencers, error: influencersError } = await supabase diff --git a/backend/src/routes/analytics.ts.bak b/backend/src/routes/analytics.ts.bak deleted file mode 100644 index 0f403d3..0000000 --- a/backend/src/routes/analytics.ts.bak +++ /dev/null @@ -1,802 +0,0 @@ -import { Hono } from 'hono'; -import { authMiddleware } from '../middlewares/auth'; -import clickhouse from '../utils/clickhouse'; -import { addAnalyticsJob } from '../utils/queue'; -import { getRedisClient } from '../utils/redis'; -import supabase from '../utils/supabase'; -import { - scheduleInfluencerCollection, - schedulePostCollection, - removeScheduledJob, - getScheduledJobs -} from '../utils/scheduledTasks'; - -// Define user type -interface User { - id: string; - email: string; - name?: string; -} - -// Extend Hono's Context type -declare module 'hono' { - interface ContextVariableMap { - user: User; - } -} - -const analyticsRouter = new Hono(); - -// Apply auth middleware to all routes -analyticsRouter.use('*', authMiddleware); - -// Track a view event -analyticsRouter.post('/view', async (c) => { - try { - const { content_id } = await c.req.json(); - const user = c.get('user'); - - if (!content_id) { - return c.json({ error: 'Content ID is required' }, 400); - } - - // Get IP and user agent - const ip = c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || '0.0.0.0'; - const userAgent = c.req.header('user-agent') || 'unknown'; - - // Insert view event into ClickHouse - await clickhouse.query({ - query: ` - INSERT INTO promote.view_events (user_id, content_id, ip, user_agent) - VALUES (?, ?, ?, ?) - `, - values: [ - user.id, - content_id, - ip, - userAgent - ] - }); - - // Queue analytics processing job - await addAnalyticsJob('process_views', { - user_id: user.id, - content_id, - timestamp: new Date().toISOString() - }); - - // Increment view count in Redis cache - const redis = await getRedisClient(); - await redis.incr(`views:${content_id}`); - - return c.json({ message: 'View tracked successfully' }); - } catch (error) { - console.error('View tracking error:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// Track a like event -analyticsRouter.post('/like', async (c) => { - try { - const { content_id, action } = await c.req.json(); - const user = c.get('user'); - - if (!content_id || !action) { - return c.json({ error: 'Content ID and action are required' }, 400); - } - - if (action !== 'like' && action !== 'unlike') { - return c.json({ error: 'Action must be either "like" or "unlike"' }, 400); - } - - // Insert like event into ClickHouse - await clickhouse.query({ - query: ` - INSERT INTO promote.like_events (user_id, content_id, action) - VALUES (?, ?, ?) - `, - values: [ - user.id, - content_id, - action === 'like' ? 1 : 2 - ] - }); - - // Queue analytics processing job - await addAnalyticsJob('process_likes', { - user_id: user.id, - content_id, - action, - timestamp: new Date().toISOString() - }); - - // Update like count in Redis cache - const redis = await getRedisClient(); - const likeKey = `likes:${content_id}`; - if (action === 'like') { - await redis.incr(likeKey); - } else { - await redis.decr(likeKey); - } - - return c.json({ message: `${action} tracked successfully` }); - } catch (error) { - console.error('Like tracking error:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// Track a follow event -analyticsRouter.post('/follow', async (c) => { - try { - const { followed_id, action } = await c.req.json(); - const user = c.get('user'); - - if (!followed_id || !action) { - return c.json({ error: 'Followed ID and action are required' }, 400); - } - - if (action !== 'follow' && action !== 'unfollow') { - return c.json({ error: 'Action must be either "follow" or "unfollow"' }, 400); - } - - // Insert follower event into ClickHouse - await clickhouse.query({ - query: ` - INSERT INTO promote.follower_events (follower_id, followed_id, action) - VALUES (?, ?, ?) - `, - values: [ - user.id, - followed_id, - action === 'follow' ? 1 : 2 - ] - }); - - // Queue analytics processing job - await addAnalyticsJob('process_followers', { - follower_id: user.id, - followed_id, - action, - timestamp: new Date().toISOString() - }); - - // Update follower count in Redis cache - const redis = await getRedisClient(); - const followerKey = `followers:${followed_id}`; - if (action === 'follow') { - await redis.incr(followerKey); - } else { - await redis.decr(followerKey); - } - - return c.json({ message: `${action} tracked successfully` }); - } catch (error) { - console.error('Follow tracking error:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// Get analytics for a content -analyticsRouter.get('/content/:id', async (c) => { - try { - const contentId = c.req.param('id'); - - // Get counts from Redis cache - const redis = await getRedisClient(); - const [views, likes] = await Promise.all([ - redis.get(`views:${contentId}`), - redis.get(`likes:${contentId}`) - ]); - - return c.json({ - content_id: contentId, - views: parseInt(views || '0'), - likes: parseInt(likes || '0') - }); - } catch (error) { - console.error('Content analytics error:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// Get analytics for a user -analyticsRouter.get('/user/:id', async (c) => { - try { - const userId = c.req.param('id'); - - // Get follower count from Redis cache - const redis = await getRedisClient(); - const followers = await redis.get(`followers:${userId}`); - - // Get content view and like counts from ClickHouse - const viewsResult = await clickhouse.query({ - query: ` - SELECT content_id, COUNT(*) as view_count - FROM promote.view_events - WHERE user_id = ? - GROUP BY content_id - `, - values: [userId] - }); - - const likesResult = await clickhouse.query({ - query: ` - SELECT content_id, SUM(CASE WHEN action = 1 THEN 1 ELSE -1 END) as like_count - FROM promote.like_events - WHERE user_id = ? - GROUP BY content_id - `, - values: [userId] - }); - - // Extract data from results - const viewsData = 'rows' in viewsResult ? viewsResult.rows : []; - const likesData = 'rows' in likesResult ? likesResult.rows : []; - - return c.json({ - user_id: userId, - followers: parseInt(followers || '0'), - content_analytics: { - views: viewsData, - likes: likesData - } - }); - } catch (error) { - console.error('User analytics error:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// 社群分析相关路由 - -// 获取项目的顶级影响者 -analyticsRouter.get('/project/:id/top-influencers', async (c) => { - try { - const projectId = c.req.param('id'); - - // 从ClickHouse查询项目的顶级影响者 - const result = await clickhouse.query({ - query: ` - SELECT - influencer_id, - SUM(metric_value) AS total_views - FROM events - WHERE - project_id = ? AND - event_type = 'post_view_change' - GROUP BY influencer_id - ORDER BY total_views DESC - LIMIT 10 - `, - values: [projectId] - }); - - // 提取数据 - const influencerData = 'rows' in result ? result.rows : []; - - // 如果有数据,从Supabase获取影响者详细信息 - if (influencerData.length > 0) { - const influencerIds = influencerData.map((item: any) => item.influencer_id); - - const { data: influencerDetails, error } = await supabase - .from('influencers') - .select('influencer_id, name, platform, followers_count, video_count') - .in('influencer_id', influencerIds); - - if (error) { - console.error('Error fetching influencer details:', error); - return c.json({ error: 'Error fetching influencer details' }, 500); - } - - // 合并数据 - const enrichedData = influencerData.map((item: any) => { - const details = influencerDetails?.find( - (detail) => detail.influencer_id === item.influencer_id - ) || {}; - - return { - ...item, - ...details - }; - }); - - return c.json(enrichedData); - } - - return c.json(influencerData); - } catch (error) { - console.error('Error fetching top influencers:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// 获取影响者的粉丝变化趋势(过去6个月) -analyticsRouter.get('/influencer/:id/follower-trend', async (c) => { - try { - const influencerId = c.req.param('id'); - - // 从ClickHouse查询影响者的粉丝变化趋势 - const result = await clickhouse.query({ - query: ` - SELECT - toStartOfMonth(timestamp) AS month, - SUM(metric_value) AS follower_change - FROM events - WHERE - influencer_id = ? AND - event_type = 'follower_change' AND - timestamp >= subtractMonths(now(), 6) - GROUP BY month - ORDER BY month ASC - `, - values: [influencerId] - }); - - // 提取数据 - const trendData = 'rows' in result ? result.rows : []; - - return c.json({ - influencer_id: influencerId, - follower_trend: trendData - }); - } catch (error) { - console.error('Error fetching follower trend:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// 获取网红增长趋势(支持不同指标和时间粒度) -analyticsRouter.get('/influencer/:id/growth', async (c) => { - try { - const influencerId = c.req.param('id'); - const { - metric = 'followers_count', - timeframe = '6months', - interval = 'month' - } = c.req.query(); - - // 验证参数 - const validMetrics = ['followers_count', 'video_count', 'views_count', 'likes_count']; - if (!validMetrics.includes(metric)) { - return c.json({ error: 'Invalid metric specified' }, 400); - } - - // 确定时间范围和间隔函数 - let timeRangeSql: string; - let intervalFunction: string; - - switch (timeframe) { - case '30days': - timeRangeSql = 'timestamp >= subtractDays(now(), 30)'; - break; - case '90days': - timeRangeSql = 'timestamp >= subtractDays(now(), 90)'; - break; - case '6months': - default: - timeRangeSql = 'timestamp >= subtractMonths(now(), 6)'; - break; - case '1year': - timeRangeSql = 'timestamp >= subtractYears(now(), 1)'; - break; - } - - switch (interval) { - case 'day': - intervalFunction = 'toDate(timestamp)'; - break; - case 'week': - intervalFunction = 'toStartOfWeek(timestamp)'; - break; - case 'month': - default: - intervalFunction = 'toStartOfMonth(timestamp)'; - break; - } - - // 从ClickHouse查询数据 - const result = await clickhouse.query({ - query: ` - SELECT - ${intervalFunction} AS time_period, - sumIf(metric_value, metric_name = ?) AS change, - maxIf(metric_total, metric_name = ?) AS total_value - FROM promote.events - WHERE - influencer_id = ? AND - event_type = ? AND - ${timeRangeSql} - GROUP BY time_period - ORDER BY time_period ASC - `, - values: [ - metric, - metric, - influencerId, - `${metric}_change` - ] - }); - - // 提取数据 - const trendData = 'rows' in result ? result.rows : []; - - // 获取网红基本信息 - const { data: influencerInfo, error } = await supabase - .from('influencers') - .select('name, platform, followers_count, video_count') - .eq('influencer_id', influencerId) - .single(); - - if (error) { - console.error('Error fetching influencer details:', error); - } - - return c.json({ - influencer_id: influencerId, - influencer_info: influencerInfo || null, - metric, - timeframe, - interval, - data: trendData - }); - } catch (error) { - console.error('Error fetching influencer growth trend:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// 获取帖子的点赞变化(过去30天) -analyticsRouter.get('/post/:id/like-trend', async (c) => { - try { - const postId = c.req.param('id'); - - // 从ClickHouse查询帖子的点赞变化 - const result = await clickhouse.query({ - query: ` - SELECT - toDate(timestamp) AS day, - SUM(metric_value) AS like_change - FROM events - WHERE - post_id = ? AND - event_type = 'post_like_change' AND - timestamp >= subtractDays(now(), 30) - GROUP BY day - ORDER BY day ASC - `, - values: [postId] - }); - - // 提取数据 - const trendData = 'rows' in result ? result.rows : []; - - return c.json({ - post_id: postId, - like_trend: trendData - }); - } catch (error) { - console.error('Error fetching like trend:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// 获取影响者详细信息 -analyticsRouter.get('/influencer/:id/details', async (c) => { - try { - const influencerId = c.req.param('id'); - - // 从Supabase获取影响者详细信息 - const { data, error } = await supabase - .from('influencers') - .select('influencer_id, name, platform, profile_url, external_id, followers_count, video_count, platform_count, created_at') - .eq('influencer_id', influencerId) - .single(); - - if (error) { - console.error('Error fetching influencer details:', error); - return c.json({ error: 'Error fetching influencer details' }, 500); - } - - if (!data) { - return c.json({ error: 'Influencer not found' }, 404); - } - - return c.json(data); - } catch (error) { - console.error('Error fetching influencer details:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// 获取影响者的帖子列表 -analyticsRouter.get('/influencer/:id/posts', async (c) => { - try { - const influencerId = c.req.param('id'); - - // 从Supabase获取影响者的帖子列表 - const { data, error } = await supabase - .from('posts') - .select('post_id, influencer_id, platform, post_url, title, description, published_at, created_at') - .eq('influencer_id', influencerId) - .order('published_at', { ascending: false }); - - if (error) { - console.error('Error fetching influencer posts:', error); - return c.json({ error: 'Error fetching influencer posts' }, 500); - } - - return c.json(data || []); - } catch (error) { - console.error('Error fetching influencer posts:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// 获取帖子的评论列表 -analyticsRouter.get('/post/:id/comments', async (c) => { - try { - const postId = c.req.param('id'); - - // 从Supabase获取帖子的评论列表 - const { data, error } = await supabase - .from('comments') - .select('comment_id, post_id, user_id, content, sentiment_score, created_at') - .eq('post_id', postId) - .order('created_at', { ascending: false }); - - if (error) { - console.error('Error fetching post comments:', error); - return c.json({ error: 'Error fetching post comments' }, 500); - } - - return c.json(data || []); - } catch (error) { - console.error('Error fetching post comments:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// 获取项目的平台分布 -analyticsRouter.get('/project/:id/platform-distribution', async (c) => { - try { - const projectId = c.req.param('id'); - - // 从ClickHouse查询项目的平台分布 - const result = await clickhouse.query({ - query: ` - SELECT - platform, - COUNT(DISTINCT influencer_id) AS influencer_count - FROM events - WHERE project_id = ? - GROUP BY platform - ORDER BY influencer_count DESC - `, - values: [projectId] - }); - - // 提取数据 - const distributionData = 'rows' in result ? result.rows : []; - - return c.json({ - project_id: projectId, - platform_distribution: distributionData - }); - } catch (error) { - console.error('Error fetching platform distribution:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// 获取项目的互动类型分布 -analyticsRouter.get('/project/:id/interaction-types', async (c) => { - try { - const projectId = c.req.param('id'); - - // 从ClickHouse查询项目的互动类型分布 - const result = await clickhouse.query({ - query: ` - SELECT - event_type, - COUNT(*) AS event_count, - SUM(metric_value) AS total_value - FROM events - WHERE - project_id = ? AND - event_type IN ('click', 'comment', 'share') - GROUP BY event_type - ORDER BY event_count DESC - `, - values: [projectId] - }); - - // 提取数据 - const interactionData = 'rows' in result ? result.rows : []; - - return c.json({ - project_id: projectId, - interaction_types: interactionData - }); - } catch (error) { - console.error('Error fetching interaction types:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// ===== Scheduled Collection Endpoints ===== - -// Schedule automated data collection for an influencer -analyticsRouter.post('/schedule/influencer', async (c) => { - try { - const { influencer_id, cron_expression } = await c.req.json(); - - if (!influencer_id) { - return c.json({ error: 'Influencer ID is required' }, 400); - } - - // Validate that the influencer exists - const { data, error } = await supabase - .from('influencers') - .select('influencer_id') - .eq('influencer_id', influencer_id) - .single(); - - if (error || !data) { - return c.json({ error: 'Influencer not found' }, 404); - } - - // Schedule the collection job - await scheduleInfluencerCollection( - influencer_id, - cron_expression || '0 0 * * *' // Default: Every day at midnight - ); - - return c.json({ - message: 'Influencer metrics collection scheduled successfully', - influencer_id, - cron_expression: cron_expression || '0 0 * * *' - }); - } catch (error) { - console.error('Error scheduling influencer collection:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// Schedule automated data collection for a post -analyticsRouter.post('/schedule/post', async (c) => { - try { - const { post_id, cron_expression } = await c.req.json(); - - if (!post_id) { - return c.json({ error: 'Post ID is required' }, 400); - } - - // Validate that the post exists - const { data, error } = await supabase - .from('posts') - .select('post_id') - .eq('post_id', post_id) - .single(); - - if (error || !data) { - return c.json({ error: 'Post not found' }, 404); - } - - // Schedule the collection job - await schedulePostCollection( - post_id, - cron_expression || '0 0 * * *' // Default: Every day at midnight - ); - - return c.json({ - message: 'Post metrics collection scheduled successfully', - post_id, - cron_expression: cron_expression || '0 0 * * *' - }); - } catch (error) { - console.error('Error scheduling post collection:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// Get all scheduled collection jobs -analyticsRouter.get('/schedule', async (c) => { - try { - const scheduledJobs = await getScheduledJobs(); - - return c.json({ - scheduled_jobs: scheduledJobs - }); - } catch (error) { - console.error('Error fetching scheduled jobs:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// Delete a scheduled collection job -analyticsRouter.delete('/schedule/:job_id', async (c) => { - try { - const jobId = c.req.param('job_id'); - - await removeScheduledJob(jobId); - - return c.json({ - message: 'Scheduled job removed successfully', - job_id: jobId - }); - } catch (error) { - console.error('Error removing scheduled job:', error); - return c.json({ error: 'Internal server error' }, 500); - } -}); - -// ===== Data Export Endpoints ===== - -// Export influencer growth data (CSV format) -analyticsRouter.get('/export/influencer/:id/growth', async (c) => { - try { - const influencerId = c.req.param('id'); - const { - metric = 'followers_count', - timeframe = '6months', - interval = 'month' - } = c.req.query(); - - // The same logic as the influencer growth endpoint, but return CSV format - - // Validate parameters - const validMetrics = ['followers_count', 'video_count', 'views_count', 'likes_count']; - if (!validMetrics.includes(metric)) { - return c.json({ error: 'Invalid metric specified' }, 400); - } - - // Determine time range and interval function - let timeRangeSql: string; - let intervalFunction: string; - - switch (timeframe) { - case '30days': - timeRangeSql = 'timestamp >= subtractDays(now(), 30)'; - break; - case '90days': - timeRangeSql = 'timestamp >= subtractDays(now(), 90)'; - break; - case '6months': - default: - timeRangeSql = 'timestamp >= subtractMonths(now(), 6)'; - break; - case '1year': - timeRangeSql = 'timestamp >= subtractYears(now(), 1)'; - break; - } - - switch (interval) { - case 'day': - intervalFunction = 'toDate(timestamp)'; - break; - case 'week': - intervalFunction = 'toStartOfWeek(timestamp)'; - break; - case 'month': - default: - intervalFunction = 'toStartOfMonth(timestamp)'; - break; - } - - // Query ClickHouse for data - const result = await clickhouse.query({ - query: ` - SELECT - ${intervalFunction} AS time_period, - sumIf(metric_value, metric_name = ?) AS change, - maxIf(metric_total, metric_name = ?) AS total_value - FROM promote.events - WHERE - influencer_id = ? AND - event_type = ? AND - ${timeRangeSql} - GROUP BY time_period - ORDER BY time_period ASC - ` \ No newline at end of file diff --git a/web/src/components/PostList.tsx b/web/src/components/PostList.tsx index 2764427..408d528 100644 --- a/web/src/components/PostList.tsx +++ b/web/src/components/PostList.tsx @@ -92,49 +92,83 @@ const PostList: React.FC = ({ influencerId, projectId }) => { const fetchPosts = async () => { try { setLoading(true); + setError(null); - // Mock data for posts - const mockPosts = [ - { - id: '1', - title: 'Introduction to React Hooks', - content: 'React Hooks are a powerful feature that allows you to use state and other React features without writing a class.', - author: 'John Smith', - date: '2023-05-15', - platform: 'Facebook', - status: 'Published', - engagement: 85, - comments: 12 - }, - { - id: '2', - title: 'Advanced CSS Techniques', - content: 'Learn about the latest CSS techniques including Grid, Flexbox, and CSS Variables.', - author: 'Sarah Johnson', - date: '2023-05-14', - platform: 'Twitter', - status: 'Draft', - engagement: 72, - comments: 8 - }, - { - id: '3', - title: 'JavaScript Performance Tips', - content: 'Optimize your JavaScript code with these performance tips and best practices.', - author: 'Michael Brown', - date: '2023-05-13', - platform: 'LinkedIn', - status: 'Published', - engagement: 68, - comments: 15 + // 构建查询参数 + const queryParams = new URLSearchParams(); + + // 添加过滤条件 + if (influencerId) { + queryParams.append('influencer_id', influencerId); + } + + if (projectId) { + queryParams.append('project_id', projectId); + } + + if (platformFilter !== 'all') { + queryParams.append('platform', platformFilter); + } + + // 添加分页和排序 + queryParams.append('limit', '20'); + queryParams.append('offset', '0'); + queryParams.append('sort', 'published_at'); + queryParams.append('order', 'desc'); + + // 添加认证头 + const authToken = 'eyJhbGciOiJIUzI1NiIsImtpZCI6Inl3blNGYnRBOGtBUnl4UmUiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3h0cWhsdXpvcm5hemxta29udWNyLnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiI1YjQzMThiZi0yMWE4LTQ3YWMtOGJmYS0yYThmOGVmOWMwZmIiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzQxNjI3ODkyLCJpYXQiOjE3NDE2MjQyOTIsImVtYWlsIjoidml0YWxpdHltYWlsZ0BnbWFpbC5jb20iLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7ImVtYWlsX3ZlcmlmaWVkIjp0cnVlfSwicm9sZSI6ImF1dGhlbnRpY2F0ZWQiLCJhYWwiOiJhYWwxIiwiYW1yIjpbeyJtZXRob2QiOiJwYXNzd29yZCIsInRpbWVzdGFtcCI6MTc0MTYyNDI5Mn1dLCJzZXNzaW9uX2lkIjoiODlmYjg0YzktZmEzYy00YmVlLTk0MDQtNjI1MjE0OGIyMzVlIiwiaXNfYW5vbnltb3VzIjpmYWxzZX0.VuUX2yhqN-FZseKL8fQG91i1cohfRqW2m1Z8CIWhZuk'; + + // 发送API请求 + const response = await fetch(`http://localhost:4000/api/posts?${queryParams.toString()}`, { + headers: { + 'accept': 'application/json', + 'Authorization': `Bearer ${authToken}` } - ]; + }); + + if (!response.ok) { + throw new Error(`API request failed with status ${response.status}`); + } + + const data = await response.json(); + console.log('API返回的posts数据:', data); + + if (data && data.posts && Array.isArray(data.posts)) { + // 转换API返回的数据为前端需要的格式 + const frontendPosts: FrontendPost[] = data.posts.map((apiPost: ApiPost) => ({ + id: apiPost.post_id, + title: apiPost.title || 'Untitled Post', + description: apiPost.description || '', + author: apiPost.influencer?.name || 'Unknown Author', + authorType: 'influencer', // 默认为influencer类型 + platform: (apiPost.platform?.toLowerCase() || 'other') as any, + contentType: determineContentType(apiPost), + timestamp: apiPost.published_at || apiPost.updated_at || new Date().toISOString(), + engagement: { + views: apiPost.views_count, + likes: apiPost.likes_count, + comments: apiPost.comments_count, + shares: apiPost.shares_count + }, + url: apiPost.post_url + })); + + setTotalPosts(data.total || frontendPosts.length); + setPosts(frontendPosts); + } else { + console.warn('API返回的数据格式不符合预期'); + setError('Failed to load posts data'); + // 使用模拟数据作为后备 + useMockData(); + } - setTotalPosts(mockPosts.length); - setPosts(mockPosts); setLoading(false); } catch (error) { console.error('Error fetching posts:', error); + setError('Failed to fetch posts. Please try again later.'); + // 使用模拟数据作为后备 + useMockData(); setLoading(false); } }; @@ -223,6 +257,97 @@ const PostList: React.FC = ({ influencerId, projectId }) => { navigate(`/comments?post_id=${postId}`); }; + // 根据帖子信息确定内容类型 + const determineContentType = (apiPost: ApiPost): 'post' | 'video' | 'reel' | 'short' => { + const platform = apiPost.platform?.toLowerCase(); + const url = apiPost.post_url?.toLowerCase() || ''; + + if (platform === 'youtube') { + if (url.includes('/shorts/')) { + return 'short'; + } + return 'video'; + } + + if (platform === 'instagram') { + if (url.includes('/reel/')) { + return 'reel'; + } + return 'post'; + } + + if (platform === 'tiktok') { + return 'short'; + } + + return 'post'; + }; + + // 使用模拟数据作为后备 + const useMockData = () => { + const mockPosts: FrontendPost[] = [ + { + id: '1', + title: 'Introduction to React Hooks', + description: 'React Hooks are a powerful feature that allows you to use state and other React features without writing a class.', + author: 'John Smith', + authorType: 'influencer', + platform: 'youtube', + contentType: 'video', + timestamp: '2023-05-15T10:30:00Z', + engagement: { + views: 1500, + likes: 85, + comments: 12, + shares: 5 + }, + url: 'https://youtube.com/watch?v=example1' + }, + { + id: '2', + title: 'Advanced CSS Techniques', + description: 'Learn about the latest CSS techniques including Grid, Flexbox, and CSS Variables.', + author: 'Sarah Johnson', + authorType: 'influencer', + platform: 'instagram', + contentType: 'post', + timestamp: '2023-05-14T14:20:00Z', + engagement: { + views: 980, + likes: 72, + comments: 8, + shares: 3 + }, + url: 'https://instagram.com/p/example2' + }, + { + id: '3', + title: 'JavaScript Performance Tips', + description: 'Optimize your JavaScript code with these performance tips and best practices.', + author: 'Michael Brown', + authorType: 'influencer', + platform: 'linkedin', + contentType: 'post', + timestamp: '2023-05-13T09:15:00Z', + engagement: { + views: 750, + likes: 68, + comments: 15, + shares: 10 + }, + url: 'https://linkedin.com/post/example3' + } + ]; + + setTotalPosts(mockPosts.length); + setPosts(mockPosts); + }; + + // 在组件加载时获取数据 + useEffect(() => { + fetchPosts(); + }, [influencerId, projectId, platformFilter]); + if (error) { return
{error}
; }