'use client'; import { useState, useMemo } from 'react'; import { toast } from 'sonner'; import { Plus, Search, Pencil, Trash2, Send, CheckCircle, XCircle, MoreHorizontal, } from 'lucide-react'; import { useArticles, useCreateArticle, useUpdateArticle, useDeleteArticle, usePublishArticle, useSubmitForReview, useCategories, useCategoryTree, useCreateCategory, useUpdateCategory, useDeleteCategory, useTags, useCreateTag, useUpdateTag, useDeleteTag, useComments, useApproveComment, useRejectComment, useDeleteComment, } from '@/hooks/use-content'; import type { Article, Category, Tag, Comment, CreateArticleRequest, UpdateArticleRequest, CreateCategoryRequest, UpdateCategoryRequest, CreateTagRequest, UpdateTagRequest, } from '@/lib/content-api'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Checkbox } from '@/components/ui/checkbox'; import { Separator } from '@/components/ui/separator'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; const ARTICLE_STATUS_OPTIONS = [ { label: '全部', value: 'ALL' }, { label: '草稿', value: 'draft' }, { label: '待审核', value: 'pending' }, { label: '已发布', value: 'published' }, { label: '已拒绝', value: 'rejected' }, ]; const ARTICLE_STATUS_LABEL: Record = { draft: '草稿', pending: '待审核', published: '已发布', rejected: '已拒绝', }; const ARTICLE_STATUS_CLASS: Record = { draft: 'bg-gray-100 text-gray-800 hover:bg-gray-100 border-gray-200', pending: 'bg-yellow-100 text-yellow-800 hover:bg-yellow-100 border-yellow-200', published: 'bg-green-100 text-green-800 hover:bg-green-100 border-green-200', rejected: 'bg-red-100 text-red-800 hover:bg-red-100 border-red-200', }; const COMMENT_STATUS_LABEL: Record = { pending: '待审核', approved: '已通过', rejected: '已拒绝', }; const COMMENT_STATUS_CLASS: Record = { pending: 'bg-yellow-100 text-yellow-800 hover:bg-yellow-100 border-yellow-200', approved: 'bg-green-100 text-green-800 hover:bg-green-100 border-green-200', rejected: 'bg-red-100 text-red-800 hover:bg-red-100 border-red-200', }; function buildCategoryTree(categories: Category[]): (Category & { children: Category[] })[] { const map = new Map(); const roots: (Category & { children: Category[] })[] = []; categories.forEach((cat) => map.set(cat.id, { ...cat, children: [] })); categories.forEach((cat) => { const node = map.get(cat.id)!; if (cat.parentId && map.has(cat.parentId)) { map.get(cat.parentId)!.children.push(node); } else { roots.push(node); } }); return roots; } function flattenCategoryTree( nodes: (Category & { children: Category[] })[], depth = 0 ): { category: Category; depth: number }[] { const result: { category: Category; depth: number }[] = []; nodes.forEach((node) => { result.push({ category: node, depth }); if (node.children.length > 0) { result.push(...flattenCategoryTree(node.children as (Category & { children: Category[] })[], depth + 1)); } }); return result; } function ArticleTab() { const [page, setPage] = useState(1); const [statusFilter, setStatusFilter] = useState('ALL'); const [searchKeyword, setSearchKeyword] = useState(''); const [createDialogOpen, setCreateDialogOpen] = useState(false); const [editDialogOpen, setEditDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [selectedArticle, setSelectedArticle] = useState
(null); const [formTitle, setFormTitle] = useState(''); const [formContent, setFormContent] = useState(''); const [formCategoryId, setFormCategoryId] = useState(''); const [formTags, setFormTags] = useState([]); const filterStatus = statusFilter === 'ALL' ? undefined : statusFilter; const { data: articlesData, isLoading } = useArticles({ page, limit: 20, status: filterStatus, keyword: searchKeyword || undefined, }); const { data: categoriesData } = useCategories(); const { data: tagsData } = useTags(); const createArticle = useCreateArticle(); const updateArticle = useUpdateArticle(); const deleteArticle = useDeleteArticle(); const publishArticle = usePublishArticle(); const submitForReview = useSubmitForReview(); const articles = (articlesData as any)?.data?.articles || []; const categories = (categoriesData as any)?.data || []; const tags = (tagsData as any)?.data?.tags || []; const resetForm = () => { setFormTitle(''); setFormContent(''); setFormCategoryId(''); setFormTags([]); }; const openCreateDialog = () => { resetForm(); setCreateDialogOpen(true); }; const openEditDialog = (article: Article) => { setSelectedArticle(article); setFormTitle(article.title); setFormContent(article.content); setFormCategoryId(article.categoryId || ''); setFormTags(article.tags || []); setEditDialogOpen(true); }; const openDeleteDialog = (article: Article) => { setSelectedArticle(article); setDeleteDialogOpen(true); }; const handleCreate = async () => { try { const data: CreateArticleRequest = { title: formTitle, slug: formTitle.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9\u4e00-\u9fa5-]/g, ''), content: formContent, categoryId: formCategoryId || undefined, tags: formTags.length > 0 ? formTags : undefined, }; await createArticle.mutateAsync(data); toast.success('文章创建成功'); setCreateDialogOpen(false); resetForm(); } catch { toast.error('创建文章失败'); } }; const handleUpdate = async () => { if (!selectedArticle) return; try { const data: UpdateArticleRequest = { title: formTitle, content: formContent, categoryId: formCategoryId || undefined, tags: formTags.length > 0 ? formTags : undefined, }; await updateArticle.mutateAsync({ id: selectedArticle.id, data }); toast.success('文章更新成功'); setEditDialogOpen(false); resetForm(); } catch { toast.error('更新文章失败'); } }; const handleDelete = async () => { if (!selectedArticle) return; try { await deleteArticle.mutateAsync(selectedArticle.id); toast.success('文章删除成功'); setDeleteDialogOpen(false); setSelectedArticle(null); } catch { toast.error('删除文章失败'); } }; const handlePublish = async (article: Article) => { try { await publishArticle.mutateAsync(article.id); toast.success('文章已发布'); } catch { toast.error('发布失败'); } }; const handleSubmitForReview = async (article: Article) => { try { await submitForReview.mutateAsync(article.id); toast.success('已提交审核'); } catch { toast.error('提交审核失败'); } }; const toggleTag = (tagName: string) => { setFormTags((prev) => prev.includes(tagName) ? prev.filter((t) => t !== tagName) : [...prev, tagName] ); }; return (
{ setSearchKeyword(e.target.value); setPage(1); }} className="pl-9" />
{isLoading ? (
{Array.from({ length: 5 }).map((_, i) => ( ))}
) : articles.length === 0 ? (
暂无文章
) : (
标题 分类 状态 作者 创建时间 操作 {articles.map((article: Article) => ( {article.title} {categories.find((c: Category) => c.id === article.categoryId)?.name || '-'} {ARTICLE_STATUS_LABEL[article.status] || article.status} {article.authorId} {new Date(article.createdAt).toLocaleString('zh-CN')} openEditDialog(article)}> 编辑 {article.status === 'draft' && ( handleSubmitForReview(article)}> 提交审核 )} {article.status === 'pending' && ( handlePublish(article)}> 发布 )} openDeleteDialog(article)} className="text-destructive" > 删除 ))}
)} 新建文章 填写文章信息以创建新文章
setFormTitle(e.target.value)} placeholder="请输入文章标题" />