post comments fix

This commit is contained in:
2025-03-11 15:20:51 +08:00
parent 9d89eb4290
commit b53fe1b6b0
4 changed files with 719 additions and 226 deletions

View File

@@ -595,16 +595,12 @@ const Analytics: React.FC = () => {
// 项目选择器组件
const ProjectSelector = () => (
<div className="mb-6">
<label htmlFor="project-select" className="block text-sm font-medium text-gray-700 mb-2">
</label>
<div className="relative">
<div className="relative w-64">
<select
id="project-select"
value={selectedProject}
onChange={(e) => setSelectedProject(e.target.value)}
className="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
className="block w-full py-2 pl-3 pr-10 text-base border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
>
{projects.map((project) => (
<option key={project.id} value={project.id}>
@@ -613,8 +609,7 @@ const Analytics: React.FC = () => {
))}
</select>
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none">
<ArrowRight className="h-4 w-4 text-gray-400" />
</div>
<ArrowRight className="w-4 h-4 text-gray-400" />
</div>
</div>
);
@@ -624,11 +619,7 @@ const Analytics: React.FC = () => {
<div className="p-6">
<h1 className="mb-6 text-2xl font-bold">Analytics Dashboard</h1>
{/* 添加项目选择器 */}
<ProjectSelector />
{/* Add the Influencer Tracking Form at the top */}
<InfluencerTrackingFormComponent />
{/* 删除这里的项目选择器 */}
{loading ? (
<div className="flex flex-col items-center justify-center h-80">
@@ -653,6 +644,7 @@ const Analytics: React.FC = () => {
<div className="flex flex-col items-start justify-between mb-6 space-y-4 lg:flex-row lg:items-center lg:space-y-0">
<h2 className="text-2xl font-bold text-gray-800"></h2>
<div className="flex flex-col space-y-3 sm:flex-row sm:space-y-0 sm:space-x-4">
<ProjectSelector />
<div className="flex space-x-2">
<select
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"

View File

@@ -1,29 +1,25 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import {
Facebook,
MessageSquare,
Instagram,
Linkedin,
CheckCircle,
XCircle,
MoreHorizontal,
ExternalLink,
BookOpen,
ThumbsUp,
ThumbsDown,
Minus,
AlertTriangle,
User,
Award,
Briefcase,
Youtube,
Hash,
Filter,
ChevronDown,
ArrowLeft
ArrowLeft,
Instagram,
Linkedin,
Youtube
} from 'lucide-react';
import CommentPreview from './CommentPreview';
import { Spin, Empty } from 'antd';
import { Comment } from '../types';
// 定义后端返回的评论类型
interface ApiComment {
@@ -39,26 +35,21 @@ interface ApiComment {
full_name: string;
avatar_url: string;
};
post?: {
title: string;
post_id: string;
platform: string;
post_url: string;
description: string;
published_at: string;
influencer_id: string;
};
}
// 定义前端使用的评论类型
interface FrontendComment {
id: string;
content: string;
author: string;
authorType: 'user' | 'kol' | 'official';
platform: 'facebook' | 'threads' | 'instagram' | 'linkedin' | 'xiaohongshu' | 'youtube';
contentType?: 'post' | 'reel' | 'video' | 'short';
timestamp: string;
sentiment: string;
status: string;
replyStatus?: string;
language?: string;
articleTitle?: string;
postAuthor?: string;
postAuthorType?: string;
url?: string;
}
// 使用导入的Comment类型不再需要FrontendComment接口
// interface FrontendComment {
// ...
// }
interface CommentListProps {
postId?: string; // 可选的帖子 ID如果提供则只获取该帖子的评论
@@ -70,6 +61,8 @@ interface PostData {
description?: string;
platform: string;
post_url?: string;
author?: string;
authorType?: string;
}
const CommentList: React.FC<CommentListProps> = () => {
@@ -77,23 +70,34 @@ const CommentList: React.FC<CommentListProps> = () => {
const navigate = useNavigate();
const postId = searchParams.get('post_id');
const [comments, setComments] = useState<FrontendComment[]>([]);
const [comments, setComments] = useState<Comment[]>([]);
const [post, setPost] = useState<PostData | null>(null); // Store post data
const [loading, setLoading] = useState<boolean>(true);
const [selectedComment, setSelectedComment] = useState<FrontendComment | null>(null);
const [selectedComment, setSelectedComment] = useState<Comment | null>(null);
const [error, setError] = useState<string | null>(null);
// 过滤和分页状态
const [platformFilter, setPlatformFilter] = useState<string>('all');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [sentimentFilter, setSentimentFilter] = useState<string>('all');
const [sentimentFilter] = useState<string>('all');
const [replyStatusFilter, setReplyStatusFilter] = useState<string>('all');
const [languageFilter, setLanguageFilter] = useState<string>('all');
const [searchQuery, setSearchQuery] = useState<string>('');
const [currentPage, setCurrentPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10);
const [offset, setOffset] = useState<number>(0);
const [pageSize] = useState<number>(20);
const [totalComments, setTotalComments] = useState<number>(0);
const [showFilters, setShowFilters] = useState<boolean>(false);
const [hasMore, setHasMore] = useState<boolean>(true);
const [isLoadingMore, setIsLoadingMore] = useState<boolean>(false);
// 无限滚动相关的引用
const observer = useRef<IntersectionObserver | null>(null);
const loadMoreRef = useRef<HTMLDivElement | null>(null);
// 添加日志查看是否正确获取到postId
useEffect(() => {
console.log('CommentList - postId from URL:', postId);
}, [postId]);
// Fetch post details if postId is provided
useEffect(() => {
@@ -103,19 +107,95 @@ const CommentList: React.FC<CommentListProps> = () => {
try {
setLoading(true);
// Mock post data
const mockPost = {
id: postId,
title: 'Sample Post Title',
content: 'This is a sample post content for demonstration purposes.',
platform: 'Facebook',
url: 'https://facebook.com/sample-post'
};
// 获取认证token
const token = await getAuthToken();
if (!token) {
console.error('Authentication token not found');
setError('Authentication required. Please log in again.');
setLoading(false);
return;
}
// 从后端API获取帖子详情
const response = await fetch(`http://localhost:4000/api/posts/${postId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`API request failed with status: ${response.status}`);
}
const data = await response.json();
if (data) {
console.log('Post details from API:', data);
// 将API返回的平台值映射到Comment类型中定义的平台值
let mappedPlatform: Comment['platform'] = 'facebook'; // 默认值
const platformFromApi = data.platform || 'unknown';
// 映射平台值
switch(platformFromApi.toLowerCase()) {
case 'facebook':
mappedPlatform = 'facebook';
break;
case 'instagram':
mappedPlatform = 'instagram';
break;
case 'linkedin':
mappedPlatform = 'linkedin';
break;
case 'youtube':
mappedPlatform = 'youtube';
break;
case 'threads':
mappedPlatform = 'threads';
break;
case 'twitter': // 添加对Twitter的支持
mappedPlatform = 'twitter'; // 直接映射到twitter
break;
case 'xiaohongshu':
case '小红书':
mappedPlatform = 'xiaohongshu';
break;
default:
// 如果无法映射,使用默认值
console.warn(`Unknown platform: ${platformFromApi}, using default: facebook`);
mappedPlatform = 'facebook';
}
setPost({
id: data.post_id,
title: data.title || 'Untitled Post',
description: data.description || '',
platform: mappedPlatform, // 使用映射后的平台值
post_url: data.post_url,
author: data.influencer?.name || 'Unknown Author',
authorType: 'kol'
});
} else {
// 如果找不到帖子使用帖子ID作为标题
setPost({
id: postId,
title: `Post ${postId}`,
platform: 'facebook' // 使用有效的平台值
});
}
setPost(mockPost);
setLoading(false);
} catch (error) {
console.error('Error fetching post details:', error);
// 如果获取帖子详情失败使用帖子ID作为标题
setPost({
id: postId,
title: `Post ${postId}`,
platform: 'facebook' // 使用有效的平台值
});
setLoading(false);
}
};
@@ -123,54 +203,394 @@ const CommentList: React.FC<CommentListProps> = () => {
fetchPostDetails();
}, [postId]);
// Fetch comments
// 加载更多评论的函数
const loadMoreComments = useCallback(async () => {
if (isLoadingMore || !hasMore) return;
console.log('Loading more comments from offset:', offset);
setIsLoadingMore(true);
try {
// 获取认证token
const token = await getAuthToken();
if (!token) {
console.error('Authentication token not found');
setError('Authentication required. Please log in again.');
setIsLoadingMore(false);
return;
}
// 构建查询参数
const params = new URLSearchParams();
// 如果URL中有post_id参数则只获取该帖子的评论
if (postId) {
console.log('Loading more comments for specific post:', postId);
params.append('post_id', postId);
}
params.append('limit', pageSize.toString());
params.append('offset', offset.toString());
if (platformFilter !== 'all') {
params.append('platform', platformFilter);
}
if (statusFilter !== 'all') {
params.append('status', statusFilter);
}
if (sentimentFilter !== 'all') {
params.append('sentiment', sentimentFilter);
}
if (replyStatusFilter !== 'all') {
params.append('reply_status', replyStatusFilter);
}
if (searchQuery) {
params.append('search', searchQuery);
}
console.log('Fetching more comments with params:', params.toString());
// 从后端API获取评论数据
const response = await fetch(`http://localhost:4000/api/comments?${params.toString()}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`API request failed with status: ${response.status}`);
}
const data = await response.json();
console.log('Received more comments:', data?.comments?.length || 0);
if (data && Array.isArray(data.comments)) {
// 转换API返回的数据为前端需要的格式
const newComments: Comment[] = data.comments.map((apiComment: ApiComment) => {
// 计算情感分析结果
let sentiment: 'positive' | 'negative' | 'neutral' | 'mixed' = 'neutral';
if (apiComment.sentiment_score > 0.3) {
sentiment = 'positive';
} else if (apiComment.sentiment_score < -0.3) {
sentiment = 'negative';
}
// 检测语言
const language = detectLanguage(apiComment.content);
// 使用API返回的post数据中的platform如果没有则使用post对象中的platform
const platformFromApi = apiComment.post?.platform || (post?.platform || 'unknown');
console.log('Comment platform from API:', platformFromApi, 'Post platform:', post?.platform);
// 将API返回的平台值映射到Comment类型中定义的平台值
let mappedPlatform: Comment['platform'] = 'facebook'; // 默认值
// 映射平台值
switch(platformFromApi.toLowerCase()) {
case 'facebook':
mappedPlatform = 'facebook';
break;
case 'instagram':
mappedPlatform = 'instagram';
break;
case 'linkedin':
mappedPlatform = 'linkedin';
break;
case 'youtube':
mappedPlatform = 'youtube';
break;
case 'threads':
mappedPlatform = 'threads';
break;
case 'twitter': // 添加对Twitter的支持
mappedPlatform = 'twitter'; // 直接映射到twitter
break;
case 'xiaohongshu':
case '小红书':
mappedPlatform = 'xiaohongshu';
break;
default:
// 如果无法映射,使用默认值
console.warn(`Unknown platform: ${platformFromApi}, using default: facebook`);
mappedPlatform = 'facebook';
}
return {
id: apiComment.comment_id,
content: apiComment.content,
author: apiComment.user_profile?.full_name || 'Anonymous User',
authorType: 'user', // 默认为普通用户
platform: mappedPlatform, // 使用映射后的平台值
timestamp: apiComment.created_at,
sentiment: sentiment,
status: 'approved', // 默认状态
replyStatus: 'none',
language: language as Comment['language'],
articleTitle: apiComment.post?.title || 'Untitled Post',
postAuthor: post?.author || 'Unknown Author',
postAuthorType: 'kol', // 默认为KOL
url: apiComment.post?.post_url || '#'
};
});
// 更新状态
setComments(prevComments => [...prevComments, ...newComments]);
setTotalComments(data.count || totalComments);
setOffset(prevOffset => prevOffset + newComments.length);
// 检查是否还有更多数据
setHasMore(newComments.length === pageSize);
console.log('Updated comments count:', comments.length + newComments.length);
} else {
// 如果没有更多数据
console.log('No more comments available');
setHasMore(false);
}
} catch (err) {
console.error('Error fetching more comments:', err);
setError('Failed to fetch more comments.');
} finally {
setIsLoadingMore(false);
}
}, [offset, platformFilter, statusFilter, sentimentFilter, replyStatusFilter, searchQuery, postId, pageSize, isLoadingMore, hasMore, totalComments, comments.length, post]);
// 设置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 comments...');
loadMoreComments();
}
}, {
rootMargin: '100px', // 提前100px触发加载
threshold: 0.1 // 降低阈值,使其更容易触发
});
if (loadMoreRef.current) {
observer.current.observe(loadMoreRef.current);
}
return () => {
if (observer.current) {
observer.current.disconnect();
}
};
}, [loading, hasMore, isLoadingMore, loadMoreComments]);
// Fetch comments - 修改为初始加载
useEffect(() => {
const fetchComments = async () => {
try {
setLoading(true);
setComments([]);
setOffset(0);
setHasMore(true);
// Mock comments data
const mockComments = [
{
id: '1',
content: 'Great post! I really enjoyed reading this.',
author: 'John Smith',
timestamp: '2023-05-15T10:30:00Z',
platform: 'Facebook',
sentiment: 'positive',
status: 'approved'
},
{
id: '2',
content: 'This was very helpful, thanks for sharing!',
author: 'Sarah Johnson',
timestamp: '2023-05-14T14:45:00Z',
platform: 'Twitter',
sentiment: 'positive',
status: 'pending'
},
{
id: '3',
content: 'I have a question about the third point you mentioned...',
author: 'Michael Brown',
timestamp: '2023-05-13T09:15:00Z',
platform: 'Instagram',
sentiment: 'neutral',
status: 'approved'
// 获取认证token
const token = await getAuthToken();
if (!token) {
console.error('Authentication token not found');
setError('Authentication required. Please log in again.');
setLoading(false);
return;
}
// 构建查询参数
const params = new URLSearchParams();
// 如果URL中有post_id参数则只获取该帖子的评论
if (postId) {
console.log('Fetching comments for specific post:', postId);
params.append('post_id', postId);
} else {
console.log('Fetching all comments (no specific post)');
}
params.append('limit', pageSize.toString());
params.append('offset', '0');
if (platformFilter !== 'all') {
params.append('platform', platformFilter);
}
if (statusFilter !== 'all') {
params.append('status', statusFilter);
}
if (sentimentFilter !== 'all') {
params.append('sentiment', sentimentFilter);
}
if (replyStatusFilter !== 'all') {
params.append('reply_status', replyStatusFilter);
}
if (searchQuery) {
params.append('search', searchQuery);
}
console.log('Fetching initial comments with params:', params.toString());
// 从后端API获取评论数据
const response = await fetch(`http://localhost:4000/api/comments?${params.toString()}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`API request failed with status: ${response.status}`);
}
const data = await response.json();
if (data && Array.isArray(data.comments)) {
// 转换API返回的数据为前端需要的格式
const frontendComments: Comment[] = data.comments.map((apiComment: ApiComment) => {
// 计算情感分析结果
let sentiment: 'positive' | 'negative' | 'neutral' | 'mixed' = 'neutral';
if (apiComment.sentiment_score > 0.3) {
sentiment = 'positive';
} else if (apiComment.sentiment_score < -0.3) {
sentiment = 'negative';
}
// 检测语言
const language = detectLanguage(apiComment.content);
// 使用API返回的post数据中的platform如果没有则使用post对象中的platform
const platformFromApi = apiComment.post?.platform || (post?.platform || 'unknown');
console.log('Comment platform from API:', platformFromApi, 'Post platform:', post?.platform);
// 将API返回的平台值映射到Comment类型中定义的平台值
let mappedPlatform: Comment['platform'] = 'facebook'; // 默认值
// 映射平台值
switch(platformFromApi.toLowerCase()) {
case 'facebook':
mappedPlatform = 'facebook';
break;
case 'instagram':
mappedPlatform = 'instagram';
break;
case 'linkedin':
mappedPlatform = 'linkedin';
break;
case 'youtube':
mappedPlatform = 'youtube';
break;
case 'threads':
mappedPlatform = 'threads';
break;
case 'twitter': // 添加对Twitter的支持
mappedPlatform = 'twitter'; // 直接映射到twitter
break;
case 'xiaohongshu':
case '小红书':
mappedPlatform = 'xiaohongshu';
break;
default:
// 如果无法映射,使用默认值
console.warn(`Unknown platform: ${platformFromApi}, using default: facebook`);
mappedPlatform = 'facebook';
}
return {
id: apiComment.comment_id,
content: apiComment.content,
author: apiComment.user_profile?.full_name || 'Anonymous User',
authorType: 'user', // 默认为普通用户
platform: mappedPlatform, // 使用映射后的平台值
timestamp: apiComment.created_at,
sentiment: sentiment,
status: 'approved', // 默认状态
replyStatus: 'none',
language: language as Comment['language'],
articleTitle: apiComment.post?.title || 'Untitled Post',
postAuthor: post?.author || 'Unknown Author',
postAuthorType: 'kol', // 默认为KOL
url: apiComment.post?.post_url || '#'
};
});
setComments(frontendComments);
setTotalComments(data.count || frontendComments.length);
setOffset(frontendComments.length);
setHasMore(frontendComments.length === pageSize);
console.log('Initial comments loaded:', frontendComments.length);
} else {
// 如果没有评论数据
setComments([]);
setTotalComments(0);
setHasMore(false);
}
];
setComments(mockComments);
setTotalComments(mockComments.length);
setLoading(false);
} catch (error) {
console.error('Error fetching comments:', error);
setError('Failed to fetch comments. Please try again later.');
setLoading(false);
// 如果获取评论失败,使用空数组
setComments([]);
setTotalComments(0);
setHasMore(false);
}
};
console.log('Fetching initial comments data...');
fetchComments();
}, [postId, currentPage, pageSize, statusFilter, platformFilter, sentimentFilter]);
// 组件卸载时清理
return () => {
if (observer.current) {
observer.current.disconnect();
}
};
}, [postId, pageSize, statusFilter, platformFilter, sentimentFilter, replyStatusFilter, searchQuery, post]);
// 获取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;
}
};
// 简单的语言检测
const detectLanguage = (text: string): 'zh-TW' | 'zh-CN' | 'en' => {
@@ -193,6 +613,62 @@ const CommentList: React.FC<CommentListProps> = () => {
navigate('/posts');
};
// 根据平台类型返回相应的图标和名称
const getPlatformIcon = (platform: string) => {
// 确保platform是字符串并转换为小写
const platformLower = (platform || '').toLowerCase();
switch (platformLower) {
case 'facebook':
return {
icon: <Facebook className="w-5 h-5 text-blue-600" />,
name: 'Facebook'
};
case 'instagram':
return {
icon: <Instagram className="w-5 h-5 text-pink-600" />,
name: 'Instagram'
};
case 'linkedin':
return {
icon: <Linkedin className="w-5 h-5 text-blue-800" />,
name: 'LinkedIn'
};
case 'youtube':
return {
icon: <Youtube className="w-5 h-5 text-red-600" />,
name: 'YouTube'
};
case 'threads':
return {
icon: <MessageSquare className="w-5 h-5 text-black" />,
name: 'Threads'
};
case 'twitter': // 添加对Twitter的支持
return {
icon: <MessageSquare className="w-5 h-5 text-blue-400" />,
name: 'Twitter'
};
case 'xiaohongshu':
case '小红书':
return {
icon: <div className="flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 rounded-full"></div>,
name: '小红书'
};
case 'tiktok':
return {
icon: <div className="flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-black rounded-full">T</div>,
name: 'TikTok'
};
default:
console.log('Unknown platform:', platform);
return {
icon: <MessageSquare className="w-5 h-5 text-gray-600" />,
name: platform || 'Unknown'
};
}
};
// 显示加载状态
if (loading) {
return (
@@ -235,24 +711,22 @@ const CommentList: React.FC<CommentListProps> = () => {
return (
<div className="flex flex-1 overflow-hidden">
<div className="flex flex-col flex-1 overflow-hidden">
<div className="bg-white p-4 border-b flex items-center justify-between">
<div className="flex items-center justify-between p-4 bg-white border-b">
<div className="flex items-center">
{postId && (
<button
onClick={handleBackToPosts}
className="mr-4 p-1 hover:bg-gray-100 rounded-full transition-colors duration-200"
className="p-1 mr-4 transition-colors duration-200 rounded-full hover:bg-gray-100"
>
<ArrowLeft className="h-5 w-5 text-gray-500" />
<ArrowLeft className="w-5 h-5 text-gray-500" />
</button>
)}
<h2 className="text-lg font-semibold">
{post ? `${post.title} 的评论` : '所有评论'}
{postId ? (post ? `"${post.title}" 的评论` : '加载帖子中...') : '所有评论'}
</h2>
{post && (
<span className="ml-2 text-sm text-gray-500">
({totalComments} )
</span>
)}
</div>
<div className="flex items-center">
@@ -260,7 +734,7 @@ const CommentList: React.FC<CommentListProps> = () => {
<input
type="text"
placeholder="搜索评论..."
className="px-4 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
className="px-4 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
@@ -271,9 +745,9 @@ const CommentList: React.FC<CommentListProps> = () => {
showFilters ? 'bg-blue-100 text-blue-700' : 'hover:bg-gray-100'
}`}
>
<Filter className="h-4 w-4 mr-1" />
<Filter className="w-4 h-4 mr-1" />
<ChevronDown className="h-4 w-4 ml-1" />
<ChevronDown className="w-4 h-4 ml-1" />
</button>
</div>
</div>
@@ -342,7 +816,9 @@ const CommentList: React.FC<CommentListProps> = () => {
{/* Mobile comment list */}
<div className="block md:hidden">
<div className="space-y-4">
{comments.map((comment) => (
{comments.map((comment) => {
const platformInfo = getPlatformIcon(comment.platform);
return (
<div
key={comment.id}
className="overflow-hidden bg-white rounded-lg shadow cursor-pointer"
@@ -351,8 +827,8 @@ const CommentList: React.FC<CommentListProps> = () => {
<div className="p-4">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center">
<Facebook className="w-5 h-5 text-blue-600" />
<span className="ml-2 text-sm font-medium">Facebook</span>
{platformInfo.icon}
<span className="ml-2 text-sm font-medium">{platformInfo.name}</span>
</div>
</div>
<p className="mb-2 text-sm text-gray-900">{comment.content}</p>
@@ -364,7 +840,8 @@ const CommentList: React.FC<CommentListProps> = () => {
</div>
</div>
</div>
))}
);
})}
</div>
</div>
@@ -385,7 +862,9 @@ const CommentList: React.FC<CommentListProps> = () => {
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{comments.map((comment) => (
{comments.map((comment) => {
const platformInfo = getPlatformIcon(comment.platform);
return (
<tr
key={comment.id}
className="cursor-pointer hover:bg-gray-50"
@@ -394,9 +873,9 @@ const CommentList: React.FC<CommentListProps> = () => {
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex flex-col">
<div className="flex items-center">
<Facebook className="w-5 h-5 text-blue-600" />
{platformInfo.icon}
<span className="ml-2 text-sm text-gray-900">
Facebook
{platformInfo.name}
</span>
</div>
</div>
@@ -477,7 +956,8 @@ const CommentList: React.FC<CommentListProps> = () => {
</button>
</td>
</tr>
))}
);
})}
</tbody>
</table>
</div>
@@ -489,6 +969,26 @@ const CommentList: React.FC<CommentListProps> = () => {
<CommentPreview comment={selectedComment} onClose={() => setSelectedComment(null)} />
</div>
)}
{/* 在评论列表的底部添加无限滚动加载指示器 */}
<div
ref={loadMoreRef}
style={{
textAlign: 'center',
padding: '20px 0',
visibility: loading || !hasMore ? 'hidden' : 'visible'
}}
>
{isLoadingMore && (
<div className="py-4">
<Spin tip="Loading more comments..." />
</div>
)}
{!hasMore && comments.length === 0 && !loading && (
<Empty description="No comments found" />
)}
</div>
</div>
);
};

View File

@@ -294,6 +294,7 @@ const PostList: React.FC<PostListProps> = ({ influencerId, projectId }) => {
// Handle post click to view comments
const handleViewComments = (postId: string) => {
console.log('Navigating to comments for post:', postId);
navigate(`/comments?post_id=${postId}`);
};

View File

@@ -1,6 +1,6 @@
export interface Comment {
id: string;
platform: 'facebook' | 'threads' | 'instagram' | 'linkedin' | 'xiaohongshu' | 'youtube';
platform: 'facebook' | 'threads' | 'instagram' | 'linkedin' | 'xiaohongshu' | 'youtube' | 'twitter';
contentType?: 'post' | 'reel' | 'video' | 'short';
content: string;
author: string;
@@ -59,7 +59,7 @@ export interface ReplyPersona {
export interface ReplyAccount {
id: string;
name: string;
platform: 'facebook' | 'threads' | 'instagram' | 'linkedin' | 'xiaohongshu' | 'youtube';
platform: 'facebook' | 'threads' | 'instagram' | 'linkedin' | 'xiaohongshu' | 'youtube' | 'twitter';
avatar: string;
role: 'admin' | 'moderator' | 'support';
}