Files
promote/web/src/components/CommentList.tsx
2025-03-10 23:43:21 +08:00

496 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } 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
} from 'lucide-react';
import CommentPreview from './CommentPreview';
// 定义后端返回的评论类型
interface ApiComment {
comment_id: string;
content: string;
sentiment_score: number;
created_at: string;
updated_at: string;
post_id: string;
user_id: string;
user_profile?: {
id: string;
full_name: string;
avatar_url: 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;
}
interface CommentListProps {
postId?: string; // 可选的帖子 ID如果提供则只获取该帖子的评论
}
interface PostData {
id: string;
title: string;
description?: string;
platform: string;
post_url?: string;
}
const CommentList: React.FC<CommentListProps> = () => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const postId = searchParams.get('post_id');
const [comments, setComments] = useState<FrontendComment[]>([]);
const [post, setPost] = useState<PostData | null>(null); // Store post data
const [loading, setLoading] = useState<boolean>(true);
const [selectedComment, setSelectedComment] = useState<FrontendComment | 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 [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 [totalComments, setTotalComments] = useState<number>(0);
const [showFilters, setShowFilters] = useState<boolean>(false);
// Fetch post details if postId is provided
useEffect(() => {
const fetchPostDetails = async () => {
if (!postId) return;
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'
};
setPost(mockPost);
setLoading(false);
} catch (error) {
console.error('Error fetching post details:', error);
setLoading(false);
}
};
fetchPostDetails();
}, [postId]);
// Fetch comments
useEffect(() => {
const fetchComments = async () => {
try {
setLoading(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'
}
];
setComments(mockComments);
setTotalComments(mockComments.length);
setLoading(false);
} catch (error) {
console.error('Error fetching comments:', error);
setLoading(false);
}
};
fetchComments();
}, [postId, currentPage, pageSize, statusFilter, platformFilter, sentimentFilter]);
// 简单的语言检测
const detectLanguage = (text: string): 'zh-TW' | 'zh-CN' | 'en' => {
const traditionalChineseRegex = /[一-龥]/;
const simplifiedChineseRegex = /[一-龥]/;
const englishRegex = /[a-zA-Z]/;
if (englishRegex.test(text) && !traditionalChineseRegex.test(text) && !simplifiedChineseRegex.test(text)) {
return 'en';
} else if (traditionalChineseRegex.test(text)) {
// 这里简化了繁体/简体的判断,实际实现应该更复杂
return 'zh-TW';
} else {
return 'zh-CN';
}
};
// Function to go back to posts list
const handleBackToPosts = () => {
navigate('/posts');
};
// 显示加载状态
if (loading) {
return (
<div className="flex flex-col flex-1 overflow-hidden md:flex-row">
<div className="flex-1 overflow-auto">
<div className="p-4 sm:p-6">
<div className="flex items-center justify-center h-64">
<div className="w-12 h-12 border-t-2 border-b-2 border-blue-500 rounded-full animate-spin"></div>
</div>
</div>
</div>
</div>
);
}
// 显示错误信息
if (error) {
return (
<div className="flex flex-col flex-1 overflow-hidden md:flex-row">
<div className="flex-1 overflow-auto">
<div className="p-4 sm:p-6">
<div className="flex items-center justify-center h-64">
<div className="text-center text-red-500">
<AlertTriangle className="w-12 h-12 mx-auto mb-4" />
<p>{error}</p>
<button
className="px-4 py-2 mt-4 text-white bg-blue-500 rounded-md hover:bg-blue-600"
onClick={() => window.location.reload()}
>
</button>
</div>
</div>
</div>
</div>
</div>
);
}
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">
{postId && (
<button
onClick={handleBackToPosts}
className="mr-4 p-1 hover:bg-gray-100 rounded-full transition-colors duration-200"
>
<ArrowLeft className="h-5 w-5 text-gray-500" />
</button>
)}
<h2 className="text-lg font-semibold">
{post ? `${post.title} 的评论` : '所有评论'}
</h2>
{post && (
<span className="ml-2 text-sm text-gray-500">
({totalComments} )
</span>
)}
</div>
<div className="flex items-center">
<div className="relative mr-2">
<input
type="text"
placeholder="搜索评论..."
className="px-4 py-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<button
onClick={() => setShowFilters(!showFilters)}
className={`flex items-center px-3 py-2 rounded-lg text-sm ${
showFilters ? 'bg-blue-100 text-blue-700' : 'hover:bg-gray-100'
}`}
>
<Filter className="h-4 w-4 mr-1" />
<ChevronDown className="h-4 w-4 ml-1" />
</button>
</div>
</div>
{/* Mobile filters panel */}
{showFilters && (
<div className="p-4 mb-4 space-y-3 bg-white rounded-lg shadow-md sm:hidden">
<div>
<label className="block mb-1 text-sm font-medium text-gray-700"></label>
<select
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="all"></option>
<option value="pending"></option>
<option value="approved"></option>
<option value="rejected"></option>
</select>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700"></label>
<select
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={platformFilter}
onChange={(e) => setPlatformFilter(e.target.value)}
>
<option value="all"></option>
<option value="facebook">Facebook</option>
<option value="threads">Threads</option>
<option value="instagram">Instagram</option>
<option value="linkedin">LinkedIn</option>
<option value="xiaohongshu"></option>
<option value="youtube">YouTube</option>
</select>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700"></label>
<select
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={replyStatusFilter}
onChange={(e) => setReplyStatusFilter(e.target.value)}
>
<option value="all"></option>
<option value="sent"></option>
<option value="draft">稿</option>
<option value="none"></option>
</select>
</div>
<div>
<label className="block mb-1 text-sm font-medium text-gray-700"></label>
<select
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
value={languageFilter}
onChange={(e) => setLanguageFilter(e.target.value)}
>
<option value="all"></option>
<option value="zh-TW"></option>
<option value="zh-CN"></option>
<option value="en">English</option>
</select>
</div>
</div>
)}
{/* Mobile comment list */}
<div className="block md:hidden">
<div className="space-y-4">
{comments.map((comment) => (
<div
key={comment.id}
className="overflow-hidden bg-white rounded-lg shadow cursor-pointer"
onClick={() => setSelectedComment(comment)}
>
<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>
</div>
</div>
<p className="mb-2 text-sm text-gray-900">{comment.content}</p>
<div className="flex items-center justify-between">
<div className="flex items-center">
<span className="mr-2 text-xs font-medium text-gray-700">{comment.author}</span>
</div>
<span className="text-xs text-gray-500">{comment.timestamp}</span>
</div>
</div>
</div>
))}
</div>
</div>
{/* Desktop table */}
<div className="hidden overflow-hidden bg-white rounded-lg shadow md:block">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase"></th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase"></th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase"></th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase"></th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase"></th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase"></th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase"></th>
<th scope="col" className="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase"></th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{comments.map((comment) => (
<tr
key={comment.id}
className="cursor-pointer hover:bg-gray-50"
onClick={() => setSelectedComment(comment)}
>
<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" />
<span className="ml-2 text-sm text-gray-900">
Facebook
</span>
</div>
</div>
</td>
<td className="px-6 py-4">
<div className="max-w-md text-sm text-gray-900 truncate">
{comment.content}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="mr-2 text-sm text-gray-900">{comment.author}</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-500">{comment.timestamp}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{comment.language === 'zh-TW' && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
</span>
)}
{comment.language === 'zh-CN' && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
</span>
)}
{comment.language === 'en' && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
EN
</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{comment.sentiment === 'positive' && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<ThumbsUp className="w-3 h-3 mr-1" />
</span>
)}
{comment.sentiment === 'negative' && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
<ThumbsDown className="w-3 h-3 mr-1" />
</span>
)}
{comment.sentiment === 'neutral' && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
<Minus className="w-3 h-3 mr-1" />
</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{comment.replyStatus === 'sent' && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<CheckCircle className="w-3 h-3 mr-1" />
</span>
)}
{comment.replyStatus === 'draft' && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<MessageSquare className="w-3 h-3 mr-1" />
稿
</span>
)}
{comment.replyStatus === 'none' && (
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
<XCircle className="w-3 h-3 mr-1" />
</span>
)}
</td>
<td className="px-6 py-4 text-sm font-medium text-right whitespace-nowrap">
<button className="mr-3 text-blue-600 hover:text-blue-900">
<ExternalLink className="w-4 h-4" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
{selectedComment && (
<div className="overflow-auto bg-white border-t border-gray-200 md:w-96 md:border-t-0 md:border-l">
<CommentPreview comment={selectedComment} onClose={() => setSelectedComment(null)} />
</div>
)}
</div>
);
};
export default CommentList;