496 lines
19 KiB
TypeScript
496 lines
19 KiB
TypeScript
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; |