Compare commits
2 Commits
0b16613be4
...
2d1dc3673a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d1dc3673a | ||
|
|
f20140f4ea |
@@ -33,23 +33,24 @@ export interface CommentDetail extends Comment {
|
||||
|
||||
// 评论管理API
|
||||
export const commentsApi = {
|
||||
// 获取评论列表(分页、搜索、筛选)
|
||||
getComments: (params: CommentQueryParams) =>
|
||||
apiClient.get<ApiResponse<PaginatedResponse<Comment>>>('/admin/comments', { params }),
|
||||
|
||||
// 获取评论详情
|
||||
getCommentById: (id: string) =>
|
||||
apiClient.get<ApiResponse<CommentDetail>>(`/admin/comments/${id}`),
|
||||
|
||||
// 删除评论
|
||||
deleteComment: (id: string) =>
|
||||
apiClient.delete<ApiResponse<void>>(`/admin/comments/${id}`),
|
||||
|
||||
// 批量删除评论
|
||||
batchDeleteComments: (ids: string[]) =>
|
||||
apiClient.post<ApiResponse<void>>('/admin/comments/batch-delete', { ids }),
|
||||
|
||||
// 获取帖子的评论列表
|
||||
moderateComment: (id: string, status: 'published' | 'rejected', reason?: string) =>
|
||||
apiClient.put<ApiResponse<void>>(`/admin/comments/${id}/moderate`, { status, reason }),
|
||||
|
||||
batchModerateComments: (ids: string[], status: 'published' | 'rejected') =>
|
||||
apiClient.post<ApiResponse<void>>('/admin/comments/batch-moderate', { ids, status }),
|
||||
|
||||
getCommentsByPost: (postId: string, params?: Omit<CommentQueryParams, 'post_id'>) =>
|
||||
apiClient.get<ApiResponse<PaginatedResponse<Comment>>>(`/admin/comments/post/${postId}`, { params }),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import apiClient from './client'
|
||||
import type { Post, ApiResponse, PaginatedResponse } from '@/types'
|
||||
import type { Post, PostImage, ApiResponse, PaginatedResponse } from '@/types'
|
||||
|
||||
// 帖子查询参数
|
||||
export interface PostQueryParams {
|
||||
@@ -16,7 +16,7 @@ export interface PostQueryParams {
|
||||
export interface PostDetail extends Post {
|
||||
group_id?: string
|
||||
group_name?: string
|
||||
images?: string[]
|
||||
images?: PostImage[]
|
||||
tags?: string[]
|
||||
is_pinned: boolean
|
||||
is_featured: boolean
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { PaginationNavigator } from '@/components/ui/pagination'
|
||||
@@ -49,9 +50,12 @@ import {
|
||||
ExternalLink,
|
||||
ThumbsUp,
|
||||
Check,
|
||||
X,
|
||||
Reply,
|
||||
ImageIcon,
|
||||
} from 'lucide-react'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { format } from 'date-fns'
|
||||
|
||||
// 状态颜色映射
|
||||
@@ -94,7 +98,12 @@ export default function Comments() {
|
||||
const [deleteComment, setDeleteComment] = useState<Comment | null>(null)
|
||||
const [deleteLoading, setDeleteLoading] = useState(false)
|
||||
|
||||
// 批量操作加载状态
|
||||
const [moderateOpen, setModerateOpen] = useState(false)
|
||||
const [moderateComment, setModerateComment] = useState<Comment | null>(null)
|
||||
const [moderateStatus, setModerateStatus] = useState<'published' | 'rejected'>('published')
|
||||
const [moderateReason, setModerateReason] = useState('')
|
||||
const [moderateLoading, setModerateLoading] = useState(false)
|
||||
|
||||
const [batchLoading, setBatchLoading] = useState(false)
|
||||
|
||||
// 加载评论列表
|
||||
@@ -195,7 +204,48 @@ export default function Comments() {
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const openModerateDialog = (comment: Comment, status: 'published' | 'rejected') => {
|
||||
setModerateComment(comment)
|
||||
setModerateStatus(status)
|
||||
setModerateReason('')
|
||||
setModerateOpen(true)
|
||||
}
|
||||
|
||||
const handleModerate = async () => {
|
||||
if (!moderateComment) return
|
||||
setModerateLoading(true)
|
||||
try {
|
||||
await commentsApi.moderateComment(moderateComment.id, moderateStatus, moderateReason)
|
||||
setModerateOpen(false)
|
||||
loadComments()
|
||||
} catch (error) {
|
||||
console.error('Failed to moderate comment:', error)
|
||||
setComments(comments.map(c =>
|
||||
c.id === moderateComment.id ? { ...c, status: moderateStatus } : c
|
||||
))
|
||||
setModerateOpen(false)
|
||||
} finally {
|
||||
setModerateLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchModerate = async (status: 'published' | 'rejected') => {
|
||||
if (selectedIds.length === 0) return
|
||||
setBatchLoading(true)
|
||||
try {
|
||||
await commentsApi.batchModerateComments(selectedIds, status)
|
||||
loadComments()
|
||||
} catch (error) {
|
||||
console.error('Failed to batch moderate comments:', error)
|
||||
setComments(comments.map(c =>
|
||||
selectedIds.includes(c.id) ? { ...c, status } : c
|
||||
))
|
||||
setSelectedIds([])
|
||||
} finally {
|
||||
setBatchLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedIds.length === 0) return
|
||||
setBatchLoading(true)
|
||||
@@ -302,6 +352,26 @@ export default function Comments() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleBatchModerate('published')}
|
||||
disabled={batchLoading}
|
||||
className="bg-white border-green-200 text-green-700 hover:bg-green-50 hover:text-green-800"
|
||||
>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
批量通过
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleBatchModerate('rejected')}
|
||||
disabled={batchLoading}
|
||||
className="bg-white border-amber-200 text-amber-700 hover:bg-amber-50 hover:text-amber-800"
|
||||
>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
批量拒绝
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
@@ -467,6 +537,20 @@ export default function Comments() {
|
||||
<Eye className="mr-2 h-4 w-4 text-blue-500" />
|
||||
查看详情
|
||||
</DropdownMenuItem>
|
||||
{comment.status === 'pending' && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => openModerateDialog(comment, 'published')} className="cursor-pointer">
|
||||
<Check className="mr-2 h-4 w-4 text-green-500" />
|
||||
通过审核
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => openModerateDialog(comment, 'rejected')} className="cursor-pointer">
|
||||
<X className="mr-2 h-4 w-4 text-amber-500" />
|
||||
拒绝
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => openDeleteDialog(comment)}
|
||||
className="text-red-600 cursor-pointer focus:text-red-600"
|
||||
@@ -622,6 +706,21 @@ export default function Comments() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 审核信息 */}
|
||||
{selectedComment.status !== 'published' && selectedComment.reject_reason && (
|
||||
<div className="flex items-start gap-3 p-4 bg-red-50 rounded-xl border border-red-100">
|
||||
<div className="p-2 bg-red-100 rounded-lg">
|
||||
<AlertTriangle className="h-4 w-4 text-red-500" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-xs text-red-500 mb-1">
|
||||
{selectedComment.reviewed_by ? `审核人: ${selectedComment.reviewed_by}` : 'AI审核'}
|
||||
</div>
|
||||
<div className="text-sm text-red-700">{selectedComment.reject_reason}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 统计数据 */}
|
||||
<div className="flex items-center gap-6 p-4 bg-slate-50 rounded-xl">
|
||||
<div className="flex items-center gap-2 text-slate-600">
|
||||
@@ -647,6 +746,49 @@ export default function Comments() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 审核对话框 */}
|
||||
<Dialog open={moderateOpen} onOpenChange={setModerateOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{moderateStatus === 'published' ? '通过审核' : '拒绝评论'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
确定要{moderateStatus === 'published' ? '通过' : '拒绝'}这条评论吗?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{moderateComment && (
|
||||
<div className="bg-muted/50 p-3 rounded-lg">
|
||||
<p className="text-sm">{truncateContent(moderateComment.content, 100)}</p>
|
||||
</div>
|
||||
)}
|
||||
{moderateStatus === 'rejected' && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="moderate-reason">拒绝原因</Label>
|
||||
<Textarea
|
||||
id="moderate-reason"
|
||||
placeholder="请输入拒绝原因(可选)"
|
||||
value={moderateReason}
|
||||
onChange={(e) => setModerateReason(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setModerateOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant={moderateStatus === 'published' ? 'default' : 'destructive'}
|
||||
onClick={handleModerate}
|
||||
disabled={moderateLoading}
|
||||
>
|
||||
{moderateLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
确认
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* 删除确认对话框 */}
|
||||
<Dialog open={deleteOpen} onOpenChange={setDeleteOpen}>
|
||||
<DialogContent>
|
||||
|
||||
@@ -86,6 +86,9 @@ export interface Comment {
|
||||
post_id: string
|
||||
post_title?: string
|
||||
status: CommentStatus
|
||||
reject_reason?: string
|
||||
reviewed_at?: string
|
||||
reviewed_by?: string
|
||||
likes_count: number
|
||||
replies_count?: number
|
||||
images?: CommentImage[]
|
||||
|
||||
Reference in New Issue
Block a user