post use api

This commit is contained in:
2025-03-11 12:27:27 +08:00
parent db2aedf0a6
commit 609dfec44e
3 changed files with 189 additions and 866 deletions

View File

@@ -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

View File

@@ -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
`

View File

@@ -92,49 +92,83 @@ const PostList: React.FC<PostListProps> = ({ 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<PostListProps> = ({ 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 <div>{error}</div>;
}