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 = () => ( const ProjectSelector = () => (
<div className="mb-6"> <div className="relative w-64">
<label htmlFor="project-select" className="block text-sm font-medium text-gray-700 mb-2">
</label>
<div className="relative">
<select <select
id="project-select" id="project-select"
value={selectedProject} value={selectedProject}
onChange={(e) => setSelectedProject(e.target.value)} 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) => ( {projects.map((project) => (
<option key={project.id} value={project.id}> <option key={project.id} value={project.id}>
@@ -613,8 +609,7 @@ const Analytics: React.FC = () => {
))} ))}
</select> </select>
<div className="absolute inset-y-0 right-0 flex items-center px-2 pointer-events-none"> <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" /> <ArrowRight className="w-4 h-4 text-gray-400" />
</div>
</div> </div>
</div> </div>
); );
@@ -624,11 +619,7 @@ const Analytics: React.FC = () => {
<div className="p-6"> <div className="p-6">
<h1 className="mb-6 text-2xl font-bold">Analytics Dashboard</h1> <h1 className="mb-6 text-2xl font-bold">Analytics Dashboard</h1>
{/* 添加项目选择器 */} {/* 删除这里的项目选择器 */}
<ProjectSelector />
{/* Add the Influencer Tracking Form at the top */}
<InfluencerTrackingFormComponent />
{loading ? ( {loading ? (
<div className="flex flex-col items-center justify-center h-80"> <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"> <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> <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"> <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"> <div className="flex space-x-2">
<select <select
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" 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 { useSearchParams, useNavigate } from 'react-router-dom';
import { import {
Facebook, Facebook,
MessageSquare, MessageSquare,
Instagram,
Linkedin,
CheckCircle, CheckCircle,
XCircle, XCircle,
MoreHorizontal,
ExternalLink, ExternalLink,
BookOpen,
ThumbsUp, ThumbsUp,
ThumbsDown, ThumbsDown,
Minus, Minus,
AlertTriangle, AlertTriangle,
User,
Award,
Briefcase,
Youtube,
Hash,
Filter, Filter,
ChevronDown, ChevronDown,
ArrowLeft ArrowLeft,
Instagram,
Linkedin,
Youtube
} from 'lucide-react'; } from 'lucide-react';
import CommentPreview from './CommentPreview'; import CommentPreview from './CommentPreview';
import { Spin, Empty } from 'antd';
import { Comment } from '../types';
// 定义后端返回的评论类型 // 定义后端返回的评论类型
interface ApiComment { interface ApiComment {
@@ -39,26 +35,21 @@ interface ApiComment {
full_name: string; full_name: string;
avatar_url: string; avatar_url: string;
}; };
post?: {
title: string;
post_id: string;
platform: string;
post_url: string;
description: string;
published_at: string;
influencer_id: string;
};
} }
// 定义前端使用的评论类型 // 使用导入的Comment类型不再需要FrontendComment接口
interface FrontendComment { // 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;
}
interface CommentListProps { interface CommentListProps {
postId?: string; // 可选的帖子 ID如果提供则只获取该帖子的评论 postId?: string; // 可选的帖子 ID如果提供则只获取该帖子的评论
@@ -70,6 +61,8 @@ interface PostData {
description?: string; description?: string;
platform: string; platform: string;
post_url?: string; post_url?: string;
author?: string;
authorType?: string;
} }
const CommentList: React.FC<CommentListProps> = () => { const CommentList: React.FC<CommentListProps> = () => {
@@ -77,23 +70,34 @@ const CommentList: React.FC<CommentListProps> = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const postId = searchParams.get('post_id'); 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 [post, setPost] = useState<PostData | null>(null); // Store post data
const [loading, setLoading] = useState<boolean>(true); 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 [error, setError] = useState<string | null>(null);
// 过滤和分页状态 // 过滤和分页状态
const [platformFilter, setPlatformFilter] = useState<string>('all'); const [platformFilter, setPlatformFilter] = useState<string>('all');
const [statusFilter, setStatusFilter] = 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 [replyStatusFilter, setReplyStatusFilter] = useState<string>('all');
const [languageFilter, setLanguageFilter] = useState<string>('all'); const [languageFilter, setLanguageFilter] = useState<string>('all');
const [searchQuery, setSearchQuery] = useState<string>(''); const [searchQuery, setSearchQuery] = useState<string>('');
const [currentPage, setCurrentPage] = useState<number>(1); const [offset, setOffset] = useState<number>(0);
const [pageSize, setPageSize] = useState<number>(10); const [pageSize] = useState<number>(20);
const [totalComments, setTotalComments] = useState<number>(0); const [totalComments, setTotalComments] = useState<number>(0);
const [showFilters, setShowFilters] = useState<boolean>(false); 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 // Fetch post details if postId is provided
useEffect(() => { useEffect(() => {
@@ -103,19 +107,95 @@ const CommentList: React.FC<CommentListProps> = () => {
try { try {
setLoading(true); setLoading(true);
// Mock post data // 获取认证token
const mockPost = { const token = await getAuthToken();
id: postId,
title: 'Sample Post Title', if (!token) {
content: 'This is a sample post content for demonstration purposes.', console.error('Authentication token not found');
platform: 'Facebook', setError('Authentication required. Please log in again.');
url: 'https://facebook.com/sample-post' 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); setLoading(false);
} catch (error) { } catch (error) {
console.error('Error fetching post details:', error); console.error('Error fetching post details:', error);
// 如果获取帖子详情失败使用帖子ID作为标题
setPost({
id: postId,
title: `Post ${postId}`,
platform: 'facebook' // 使用有效的平台值
});
setLoading(false); setLoading(false);
} }
}; };
@@ -123,54 +203,394 @@ const CommentList: React.FC<CommentListProps> = () => {
fetchPostDetails(); fetchPostDetails();
}, [postId]); }, [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(() => { useEffect(() => {
const fetchComments = async () => { const fetchComments = async () => {
try { try {
setLoading(true); setLoading(true);
setComments([]);
setOffset(0);
setHasMore(true);
// Mock comments data // 获取认证token
const mockComments = [ const token = await getAuthToken();
{
id: '1', if (!token) {
content: 'Great post! I really enjoyed reading this.', console.error('Authentication token not found');
author: 'John Smith', setError('Authentication required. Please log in again.');
timestamp: '2023-05-15T10:30:00Z', setLoading(false);
platform: 'Facebook', return;
sentiment: 'positive', }
status: 'approved'
}, // 构建查询参数
{ const params = new URLSearchParams();
id: '2',
content: 'This was very helpful, thanks for sharing!', // 如果URL中有post_id参数则只获取该帖子的评论
author: 'Sarah Johnson', if (postId) {
timestamp: '2023-05-14T14:45:00Z', console.log('Fetching comments for specific post:', postId);
platform: 'Twitter', params.append('post_id', postId);
sentiment: 'positive', } else {
status: 'pending' console.log('Fetching all comments (no specific post)');
}, }
{
id: '3', params.append('limit', pageSize.toString());
content: 'I have a question about the third point you mentioned...', params.append('offset', '0');
author: 'Michael Brown',
timestamp: '2023-05-13T09:15:00Z', if (platformFilter !== 'all') {
platform: 'Instagram', params.append('platform', platformFilter);
sentiment: 'neutral', }
status: 'approved'
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); setLoading(false);
} catch (error) { } catch (error) {
console.error('Error fetching comments:', error); console.error('Error fetching comments:', error);
setError('Failed to fetch comments. Please try again later.');
setLoading(false); setLoading(false);
// 如果获取评论失败,使用空数组
setComments([]);
setTotalComments(0);
setHasMore(false);
} }
}; };
console.log('Fetching initial comments data...');
fetchComments(); 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' => { const detectLanguage = (text: string): 'zh-TW' | 'zh-CN' | 'en' => {
@@ -193,6 +613,62 @@ const CommentList: React.FC<CommentListProps> = () => {
navigate('/posts'); 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) { if (loading) {
return ( return (
@@ -235,24 +711,22 @@ const CommentList: React.FC<CommentListProps> = () => {
return ( return (
<div className="flex flex-1 overflow-hidden"> <div className="flex flex-1 overflow-hidden">
<div className="flex flex-col 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"> <div className="flex items-center">
{postId && ( {postId && (
<button <button
onClick={handleBackToPosts} 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> </button>
)} )}
<h2 className="text-lg font-semibold"> <h2 className="text-lg font-semibold">
{post ? `${post.title} 的评论` : '所有评论'} {postId ? (post ? `"${post.title}" 的评论` : '加载帖子中...') : '所有评论'}
</h2> </h2>
{post && (
<span className="ml-2 text-sm text-gray-500"> <span className="ml-2 text-sm text-gray-500">
({totalComments} ) ({totalComments} )
</span> </span>
)}
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
@@ -260,7 +734,7 @@ const CommentList: React.FC<CommentListProps> = () => {
<input <input
type="text" type="text"
placeholder="搜索评论..." 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} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} 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' 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> </button>
</div> </div>
</div> </div>
@@ -342,7 +816,9 @@ const CommentList: React.FC<CommentListProps> = () => {
{/* Mobile comment list */} {/* Mobile comment list */}
<div className="block md:hidden"> <div className="block md:hidden">
<div className="space-y-4"> <div className="space-y-4">
{comments.map((comment) => ( {comments.map((comment) => {
const platformInfo = getPlatformIcon(comment.platform);
return (
<div <div
key={comment.id} key={comment.id}
className="overflow-hidden bg-white rounded-lg shadow cursor-pointer" 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="p-4">
<div className="flex items-start justify-between mb-3"> <div className="flex items-start justify-between mb-3">
<div className="flex items-center"> <div className="flex items-center">
<Facebook className="w-5 h-5 text-blue-600" /> {platformInfo.icon}
<span className="ml-2 text-sm font-medium">Facebook</span> <span className="ml-2 text-sm font-medium">{platformInfo.name}</span>
</div> </div>
</div> </div>
<p className="mb-2 text-sm text-gray-900">{comment.content}</p> <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> </div>
))} );
})}
</div> </div>
</div> </div>
@@ -385,7 +862,9 @@ const CommentList: React.FC<CommentListProps> = () => {
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{comments.map((comment) => ( {comments.map((comment) => {
const platformInfo = getPlatformIcon(comment.platform);
return (
<tr <tr
key={comment.id} key={comment.id}
className="cursor-pointer hover:bg-gray-50" className="cursor-pointer hover:bg-gray-50"
@@ -394,9 +873,9 @@ const CommentList: React.FC<CommentListProps> = () => {
<td className="px-6 py-4 whitespace-nowrap"> <td className="px-6 py-4 whitespace-nowrap">
<div className="flex flex-col"> <div className="flex flex-col">
<div className="flex items-center"> <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"> <span className="ml-2 text-sm text-gray-900">
Facebook {platformInfo.name}
</span> </span>
</div> </div>
</div> </div>
@@ -477,7 +956,8 @@ const CommentList: React.FC<CommentListProps> = () => {
</button> </button>
</td> </td>
</tr> </tr>
))} );
})}
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -489,6 +969,26 @@ const CommentList: React.FC<CommentListProps> = () => {
<CommentPreview comment={selectedComment} onClose={() => setSelectedComment(null)} /> <CommentPreview comment={selectedComment} onClose={() => setSelectedComment(null)} />
</div> </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> </div>
); );
}; };

View File

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

View File

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