post scroll
This commit is contained in:
@@ -1511,35 +1511,35 @@ analyticsRouter.get('/project/:id/conversion-funnel', async (c) => {
|
|||||||
.eq('id', projectId)
|
.eq('id', projectId)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
// // 如果找不到项目或发生错误,返回模拟数据
|
// 如果找不到项目或发生错误,返回模拟数据
|
||||||
// if (projectError) {
|
if (projectError) {
|
||||||
// console.log(`项目未找到或数据库错误,返回模拟数据。项目ID: ${projectId}, 错误: ${projectError.message}`);
|
console.log(`项目未找到或数据库错误,返回模拟数据。项目ID: ${projectId}, 错误: ${projectError.message}`);
|
||||||
|
|
||||||
// // 生成模拟的漏斗数据
|
// 生成模拟的漏斗数据
|
||||||
// const mockFunnelData = [
|
const mockFunnelData = [
|
||||||
// { stage: 'Awareness', count: 100, rate: 100 },
|
{ stage: 'Awareness', count: 100, rate: 100 },
|
||||||
// { stage: 'Interest', count: 75, rate: 75 },
|
{ stage: 'Interest', count: 75, rate: 75 },
|
||||||
// { stage: 'Consideration', count: 50, rate: 50 },
|
{ stage: 'Consideration', count: 50, rate: 50 },
|
||||||
// { stage: 'Intent', count: 30, rate: 30 },
|
{ stage: 'Intent', count: 30, rate: 30 },
|
||||||
// { stage: 'Evaluation', count: 20, rate: 20 },
|
{ stage: 'Evaluation', count: 20, rate: 20 },
|
||||||
// { stage: 'Purchase', count: 10, rate: 10 }
|
{ stage: 'Purchase', count: 10, rate: 10 }
|
||||||
// ];
|
];
|
||||||
|
|
||||||
// return c.json({
|
return c.json({
|
||||||
// project: {
|
project: {
|
||||||
// id: projectId,
|
id: projectId,
|
||||||
// name: `模拟项目 (ID: ${projectId})`
|
name: `模拟项目 (ID: ${projectId})`
|
||||||
// },
|
},
|
||||||
// timeframe,
|
timeframe,
|
||||||
// funnel_data: mockFunnelData,
|
funnel_data: mockFunnelData,
|
||||||
// metrics: {
|
metrics: {
|
||||||
// total_influencers: 100,
|
total_influencers: 100,
|
||||||
// conversion_rate: 10,
|
conversion_rate: 10,
|
||||||
// avg_stage_dropoff: 18
|
avg_stage_dropoff: 18
|
||||||
// },
|
},
|
||||||
// is_mock_data: true
|
is_mock_data: true
|
||||||
// });
|
});
|
||||||
// }
|
}
|
||||||
|
|
||||||
// 获取项目关联的网红及其详细信息
|
// 获取项目关联的网红及其详细信息
|
||||||
const { data: projectInfluencers, error: influencersError } = await supabase
|
const { data: projectInfluencers, error: influencersError } = await supabase
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const AppContent = () => {
|
|||||||
// Render main app layout when authenticated
|
// Render main app layout when authenticated
|
||||||
const renderAppLayout = () => {
|
const renderAppLayout = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen bg-gray-100 overflow-hidden">
|
<div className="flex h-screen bg-gray-100">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
activePage={activePage}
|
activePage={activePage}
|
||||||
onPageChange={(page) => {
|
onPageChange={(page) => {
|
||||||
@@ -63,17 +63,19 @@ const AppContent = () => {
|
|||||||
isOpen={sidebarOpen}
|
isOpen={sidebarOpen}
|
||||||
onClose={() => setSidebarOpen(false)}
|
onClose={() => setSidebarOpen(false)}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col flex-1 overflow-hidden">
|
<div className="flex flex-col flex-1 overflow-auto">
|
||||||
<Header
|
<Header
|
||||||
onMenuClick={() => setSidebarOpen(!sidebarOpen)}
|
onMenuClick={() => setSidebarOpen(!sidebarOpen)}
|
||||||
/>
|
/>
|
||||||
<Routes>
|
<main className="flex-1 overflow-auto p-4">
|
||||||
<Route path="/" element={<Dashboard />} />
|
<Routes>
|
||||||
<Route path="/comments" element={<CommentList />} />
|
<Route path="/" element={<Dashboard />} />
|
||||||
<Route path="/posts" element={<PostList />} />
|
<Route path="/comments" element={<CommentList />} />
|
||||||
<Route path="/analytics" element={<Analytics />} />
|
<Route path="/posts" element={<PostList />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="/analytics" element={<Analytics />} />
|
||||||
</Routes>
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
</Routes>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -11,7 +11,9 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
Select,
|
Select,
|
||||||
Drawer,
|
Drawer,
|
||||||
Form
|
Form,
|
||||||
|
Empty,
|
||||||
|
Spin
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
@@ -26,6 +28,7 @@ import {
|
|||||||
MessageOutlined
|
MessageOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
import supabase from '../utils/supabase';
|
||||||
|
|
||||||
// API response type definition based on backend structure
|
// API response type definition based on backend structure
|
||||||
interface ApiPost {
|
interface ApiPost {
|
||||||
@@ -87,64 +90,95 @@ const PostList: React.FC<PostListProps> = ({ influencerId, projectId }) => {
|
|||||||
const [contentTypeFilter, setContentTypeFilter] = useState<string>('all');
|
const [contentTypeFilter, setContentTypeFilter] = useState<string>('all');
|
||||||
const [showFilters, setShowFilters] = useState<boolean>(false);
|
const [showFilters, setShowFilters] = useState<boolean>(false);
|
||||||
const [totalPosts, setTotalPosts] = useState<number>(0);
|
const [totalPosts, setTotalPosts] = useState<number>(0);
|
||||||
|
const [offset, setOffset] = useState<number>(0);
|
||||||
|
const [hasMore, setHasMore] = useState<boolean>(true);
|
||||||
|
const [isLoadingMore, setIsLoadingMore] = useState<boolean>(false);
|
||||||
|
const observer = useRef<IntersectionObserver | null>(null);
|
||||||
|
const loadMoreRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Fetch posts data
|
// Fetch posts data
|
||||||
const fetchPosts = async () => {
|
const fetchPosts = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setPosts([]);
|
||||||
|
setOffset(0);
|
||||||
|
setHasMore(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
// Build query parameters
|
||||||
setError(null);
|
const params = new URLSearchParams();
|
||||||
|
const pageSize = 20;
|
||||||
|
params.append('limit', pageSize.toString());
|
||||||
|
params.append('offset', '0');
|
||||||
|
params.append('sort', 'published_at');
|
||||||
|
params.append('order', 'desc');
|
||||||
|
|
||||||
// 构建查询参数
|
if (platformFilter && platformFilter !== 'all') {
|
||||||
const queryParams = new URLSearchParams();
|
params.append('platform', platformFilter);
|
||||||
|
}
|
||||||
|
|
||||||
// 添加过滤条件
|
|
||||||
if (influencerId) {
|
if (influencerId) {
|
||||||
queryParams.append('influencer_id', influencerId);
|
params.append('influencer_id', influencerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
queryParams.append('project_id', projectId);
|
params.append('project_id', projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (platformFilter !== 'all') {
|
// Get auth token from Supabase
|
||||||
queryParams.append('platform', platformFilter);
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
const token = session?.access_token;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
console.error('Authentication token not found');
|
||||||
|
setError('Authentication required. Please log in again.');
|
||||||
|
setLoading(false);
|
||||||
|
// Fall back to mock data if no token
|
||||||
|
const mockData = getMockPosts();
|
||||||
|
setPosts(mockData);
|
||||||
|
setTotalPosts(mockData.length);
|
||||||
|
setHasMore(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加分页和排序
|
// Make API request
|
||||||
queryParams.append('limit', '20');
|
const response = await fetch(`http://localhost:4000/api/posts?${params.toString()}`, {
|
||||||
queryParams.append('offset', '0');
|
method: 'GET',
|
||||||
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: {
|
headers: {
|
||||||
'accept': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${authToken}`
|
'Authorization': `Bearer ${token}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
console.error('Authentication failed: Unauthorized');
|
||||||
|
setError('Your session has expired. Please log in again.');
|
||||||
|
// Fall back to mock data on auth error
|
||||||
|
const mockData = getMockPosts();
|
||||||
|
setPosts(mockData);
|
||||||
|
setTotalPosts(mockData.length);
|
||||||
|
setHasMore(false);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`API request failed with status ${response.status}`);
|
throw new Error(`API request failed with status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('API返回的posts数据:', data);
|
|
||||||
|
|
||||||
if (data && data.posts && Array.isArray(data.posts)) {
|
if (data && Array.isArray(data.posts)) {
|
||||||
// 转换API返回的数据为前端需要的格式
|
// Transform API posts to frontend format
|
||||||
const frontendPosts: FrontendPost[] = data.posts.map((apiPost: ApiPost) => ({
|
const frontendPosts: FrontendPost[] = data.posts.map((apiPost: ApiPost) => ({
|
||||||
id: apiPost.post_id,
|
id: apiPost.post_id,
|
||||||
title: apiPost.title || 'Untitled Post',
|
title: apiPost.title || 'Untitled Post',
|
||||||
description: apiPost.description || '',
|
description: apiPost.description || '',
|
||||||
author: apiPost.influencer?.name || 'Unknown Author',
|
author: apiPost.influencer?.name || 'Unknown Author',
|
||||||
authorType: 'influencer', // 默认为influencer类型
|
authorType: 'influencer', // Default to influencer
|
||||||
platform: (apiPost.platform?.toLowerCase() || 'other') as any,
|
platform: apiPost.platform as FrontendPost['platform'], // Cast to expected platform type
|
||||||
contentType: determineContentType(apiPost),
|
contentType: determineContentType(apiPost),
|
||||||
timestamp: apiPost.published_at || apiPost.updated_at || new Date().toISOString(),
|
timestamp: apiPost.published_at,
|
||||||
engagement: {
|
engagement: {
|
||||||
views: apiPost.views_count,
|
views: apiPost.views_count,
|
||||||
likes: apiPost.likes_count,
|
likes: apiPost.likes_count,
|
||||||
@@ -154,21 +188,27 @@ const PostList: React.FC<PostListProps> = ({ influencerId, projectId }) => {
|
|||||||
url: apiPost.post_url
|
url: apiPost.post_url
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setTotalPosts(data.total || frontendPosts.length);
|
|
||||||
setPosts(frontendPosts);
|
setPosts(frontendPosts);
|
||||||
|
setTotalPosts(data.total || frontendPosts.length);
|
||||||
|
setOffset(frontendPosts.length);
|
||||||
|
setHasMore(frontendPosts.length === pageSize);
|
||||||
} else {
|
} else {
|
||||||
console.warn('API返回的数据格式不符合预期');
|
console.warn('Invalid data format received from API, using mock data');
|
||||||
setError('Failed to load posts data');
|
// Fall back to mock data if API response format is unexpected
|
||||||
// 使用模拟数据作为后备
|
const mockData = getMockPosts();
|
||||||
useMockData();
|
setPosts(mockData);
|
||||||
|
setTotalPosts(mockData.length);
|
||||||
|
setHasMore(false);
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
setLoading(false);
|
console.error('Error fetching posts:', err);
|
||||||
} catch (error) {
|
setError('Failed to fetch posts. Using mock data instead.');
|
||||||
console.error('Error fetching posts:', error);
|
// Fall back to mock data on error
|
||||||
setError('Failed to fetch posts. Please try again later.');
|
const mockData = getMockPosts();
|
||||||
// 使用模拟数据作为后备
|
setPosts(mockData);
|
||||||
useMockData();
|
setTotalPosts(mockData.length);
|
||||||
|
setHasMore(false);
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -283,77 +323,207 @@ const PostList: React.FC<PostListProps> = ({ influencerId, projectId }) => {
|
|||||||
return 'post';
|
return 'post';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 使用模拟数据作为后备
|
// Helper function to get mock posts (extracted from useMockData)
|
||||||
const useMockData = () => {
|
const getMockPosts = (): FrontendPost[] => {
|
||||||
const mockPosts: FrontendPost[] = [
|
return [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
title: 'Introduction to React Hooks',
|
title: 'How to Get Started with React',
|
||||||
description: 'React Hooks are a powerful feature that allows you to use state and other React features without writing a class.',
|
description: 'A beginner-friendly guide to React development',
|
||||||
author: 'John Smith',
|
author: 'Jane Smith',
|
||||||
authorType: 'influencer',
|
authorType: 'influencer',
|
||||||
platform: 'youtube',
|
platform: 'youtube',
|
||||||
contentType: 'video',
|
contentType: 'video',
|
||||||
timestamp: '2023-05-15T10:30:00Z',
|
timestamp: '2023-05-15T14:30:00Z',
|
||||||
engagement: {
|
engagement: {
|
||||||
views: 1500,
|
views: 15000,
|
||||||
likes: 85,
|
likes: 1200,
|
||||||
comments: 12,
|
comments: 85,
|
||||||
shares: 5
|
shares: 320
|
||||||
},
|
},
|
||||||
url: 'https://youtube.com/watch?v=example1'
|
url: 'https://youtube.com/watch?v=abc123'
|
||||||
},
|
},
|
||||||
{
|
// ... add more mock posts as needed
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 加载更多数据的函数
|
||||||
|
const loadMorePosts = useCallback(async () => {
|
||||||
|
if (isLoadingMore || !hasMore) return;
|
||||||
|
|
||||||
|
console.log('Loading more posts from offset:', offset);
|
||||||
|
setIsLoadingMore(true);
|
||||||
|
try {
|
||||||
|
// 构建查询参数
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
const pageSize = 20;
|
||||||
|
params.append('limit', pageSize.toString());
|
||||||
|
params.append('offset', offset.toString());
|
||||||
|
params.append('sort', 'published_at');
|
||||||
|
params.append('order', 'desc');
|
||||||
|
|
||||||
|
if (platformFilter && platformFilter !== 'all') {
|
||||||
|
params.append('platform', platformFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (influencerId) {
|
||||||
|
params.append('influencer_id', influencerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectId) {
|
||||||
|
params.append('project_id', projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取认证token
|
||||||
|
const token = await getAuthToken();
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
console.error('Authentication token not found');
|
||||||
|
setError('Authentication required. Please log in again.');
|
||||||
|
setIsLoadingMore(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Fetching more posts with params:', params.toString());
|
||||||
|
|
||||||
|
// 发送API请求
|
||||||
|
const response = await fetch(`http://localhost:4000/api/posts?${params.toString()}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
console.error('Authentication failed: Unauthorized');
|
||||||
|
setError('Your session has expired. Please log in again.');
|
||||||
|
setIsLoadingMore(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`API request failed with status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Received more posts:', data?.posts?.length || 0);
|
||||||
|
|
||||||
|
if (data && Array.isArray(data.posts)) {
|
||||||
|
// 转换API返回的数据为前端需要的格式
|
||||||
|
const newPosts: 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 as FrontendPost['platform'],
|
||||||
|
contentType: determineContentType(apiPost),
|
||||||
|
timestamp: apiPost.published_at,
|
||||||
|
engagement: {
|
||||||
|
views: apiPost.views_count,
|
||||||
|
likes: apiPost.likes_count,
|
||||||
|
comments: apiPost.comments_count,
|
||||||
|
shares: apiPost.shares_count
|
||||||
|
},
|
||||||
|
url: apiPost.post_url
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
setPosts(prevPosts => [...prevPosts, ...newPosts]);
|
||||||
|
setTotalPosts(data.total || totalPosts);
|
||||||
|
setOffset(prevOffset => prevOffset + newPosts.length);
|
||||||
|
|
||||||
|
// 检查是否还有更多数据
|
||||||
|
setHasMore(newPosts.length === pageSize);
|
||||||
|
|
||||||
|
console.log('Updated posts count:', posts.length + newPosts.length);
|
||||||
|
} else {
|
||||||
|
// 如果没有更多数据
|
||||||
|
console.log('No more posts available');
|
||||||
|
setHasMore(false);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching more posts:', err);
|
||||||
|
setError('Failed to fetch more posts.');
|
||||||
|
} finally {
|
||||||
|
setIsLoadingMore(false);
|
||||||
|
}
|
||||||
|
}, [offset, platformFilter, influencerId, projectId, isLoadingMore, hasMore, totalPosts, posts.length]);
|
||||||
|
|
||||||
|
// 设置Intersection Observer来监测滚动
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) return;
|
||||||
|
|
||||||
|
if (observer.current) {
|
||||||
|
observer.current.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.current = new IntersectionObserver(entries => {
|
||||||
|
if (entries[0].isIntersecting && hasMore && !isLoadingMore) {
|
||||||
|
console.log('Intersection observed, loading more posts...');
|
||||||
|
loadMorePosts();
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
rootMargin: '100px', // 提前100px触发加载
|
||||||
|
threshold: 0.1 // 降低阈值,使其更容易触发
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loadMoreRef.current) {
|
||||||
|
observer.current.observe(loadMoreRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (observer.current) {
|
||||||
|
observer.current.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [loading, hasMore, isLoadingMore, loadMorePosts]);
|
||||||
|
|
||||||
// 在组件加载时获取数据
|
// 在组件加载时获取数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('Fetching initial posts data...');
|
||||||
fetchPosts();
|
fetchPosts();
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
return () => {
|
||||||
|
if (observer.current) {
|
||||||
|
observer.current.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
}, [influencerId, projectId, platformFilter]);
|
}, [influencerId, projectId, platformFilter]);
|
||||||
|
|
||||||
|
// 获取Supabase会话和token的函数
|
||||||
|
const getAuthToken = async () => {
|
||||||
|
try {
|
||||||
|
// 尝试从localStorage中查找Supabase token
|
||||||
|
// Supabase通常将token存储在以sb-开头的键中
|
||||||
|
const keys = Object.keys(localStorage);
|
||||||
|
const supabaseTokenKey = keys.find(key => key.startsWith('sb-') && localStorage.getItem(key)?.includes('access_token'));
|
||||||
|
|
||||||
|
if (supabaseTokenKey) {
|
||||||
|
try {
|
||||||
|
const supabaseData = JSON.parse(localStorage.getItem(supabaseTokenKey) || '{}');
|
||||||
|
return supabaseData.access_token;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing Supabase token:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到Supabase token,尝试使用常规token
|
||||||
|
return localStorage.getItem('token');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting auth token:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <div>{error}</div>;
|
return <div>{error}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="post-list-container">
|
||||||
<Card
|
<Card
|
||||||
title="Posts"
|
title="Posts"
|
||||||
extra={
|
extra={
|
||||||
@@ -367,11 +537,12 @@ const PostList: React.FC<PostListProps> = ({ influencerId, projectId }) => {
|
|||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
className="mb-4"
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
dataSource={filteredPosts}
|
dataSource={filteredPosts}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
pagination={{ pageSize: 10 }}
|
pagination={false}
|
||||||
onRow={(post) => ({
|
onRow={(post) => ({
|
||||||
onClick: () => handleViewComments(post.id),
|
onClick: () => handleViewComments(post.id),
|
||||||
style: { cursor: 'pointer' }
|
style: { cursor: 'pointer' }
|
||||||
@@ -485,7 +656,32 @@ const PostList: React.FC<PostListProps> = ({ influencerId, projectId }) => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
scroll={{ x: 'max-content' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 无限滚动加载指示器 */}
|
||||||
|
<div
|
||||||
|
ref={loadMoreRef}
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '20px 0',
|
||||||
|
visibility: loading || !hasMore ? 'hidden' : 'visible'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLoadingMore && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Spin tip="Loading more posts..." />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!hasMore && posts.length > 0 && (
|
||||||
|
<div className="py-4 text-gray-500">
|
||||||
|
No more posts to load
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!hasMore && posts.length === 0 && !loading && (
|
||||||
|
<Empty description="No posts found" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Filters Drawer */}
|
{/* Filters Drawer */}
|
||||||
|
|||||||
Reference in New Issue
Block a user