'use client'; import { useEffect, useState } from 'react'; import useSWR from 'swr'; import { Plus, Pencil, Trash2, Tags, Loader2 } from 'lucide-react'; import { AdminHeader } from '@/components/admin/AdminHeader'; import { PaginationTable, type Column } from '@/components/admin/PaginationTable'; import { TableToolbar } from '@/components/admin/TableToolbar'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { ImageUpload } from '@/components/admin/ImageUpload'; import { RichEditor } from '@/components/admin/RichEditor'; import { MarkdownImport } from '@/components/admin/MarkdownImport'; import { adminApi } from '@/lib/admin-services'; import { useAdminStore } from '@/store/adminStore'; import { cn, formatDate, resolveUploadUrl } from '@/lib/utils'; import { isHTMLContent } from '@/lib/content'; import type { News, NewsCategory } from '@/lib/types'; type ContentMode = 'rich' | 'markdown'; interface FormState { categoryId: number; title: string; cover: string; intro: string; content: string; isTop: number; status: number; } export default function NewsAdminPage() { const [hydrated, setHydrated] = useState(false); const [page, setPage] = useState(1); const [categoryId, setCategoryId] = useState(); const [keyword, setKeyword] = useState(''); const [searchKw, setSearchKw] = useState(''); const [open, setOpen] = useState(false); const [editId, setEditId] = useState(null); const [saving, setSaving] = useState(false); const [contentMode, setContentMode] = useState('rich'); const isSuperAdmin = useAdminStore((s) => s.admin?.role) === 'super_admin'; const [catOpen, setCatOpen] = useState(false); const [catName, setCatName] = useState(''); const [catSort, setCatSort] = useState(0); const [form, setForm] = useState({ categoryId: 0, title: '', cover: '', intro: '', content: '', isTop: 0, status: 1, }); useEffect(() => setHydrated(true), []); const { data: categories, mutate: mutateCats } = useSWR( hydrated ? '/admin/news-category' : null, () => adminApi.newsCategoryAll(), ); const { data, isLoading, mutate } = useSWR( hydrated ? ['/admin/news', page, categoryId, searchKw] : null, () => adminApi.newsPaginate({ page, pageSize: 10, categoryId, keyword: searchKw || undefined, }), ); const openCreate = () => { if (!categories || categories.length === 0) { alert('请先创建新闻分类'); setCatOpen(true); return; } setEditId(null); setContentMode('rich'); setForm({ categoryId: categories[0].id, title: '', cover: '', intro: '', content: '', isTop: 0, status: 1, }); setOpen(true); }; const openEdit = async (id: number) => { const detail = await adminApi.newsDetail(id); setEditId(id); setContentMode(isHTMLContent(detail.content) ? 'rich' : 'markdown'); setForm({ categoryId: detail.categoryId, title: detail.title, cover: detail.cover, intro: detail.intro, content: detail.content, isTop: detail.isTop, status: detail.status, }); setOpen(true); }; const onSave = async () => { if (!form.title || !form.content) { alert('请填写标题和正文'); return; } setSaving(true); try { const payload = { ...form, categoryId: Number(form.categoryId) }; if (editId) { await adminApi.newsUpdate(editId, payload); } else { await adminApi.newsCreate(payload); } setOpen(false); await mutate(); } catch (e) { alert((e as Error).message); } finally { setSaving(false); } }; const onDelete = async (id: number) => { if (!confirm('确认删除该新闻?')) return; try { await adminApi.newsDelete(id); await mutate(); } catch (e) { alert((e as Error).message); } }; const onAddCategory = async () => { if (!catName.trim()) { alert('请输入分类名称'); return; } try { await adminApi.newsCategoryCreate({ name: catName, sort: catSort, isShow: 1, }); setCatName(''); setCatSort(0); await mutateCats(); } catch (e) { alert((e as Error).message); } }; const onDeleteCategory = async (id: number) => { if (!confirm('确认删除该分类?')) return; try { await adminApi.newsCategoryDelete(id); await mutateCats(); } catch (e) { alert((e as Error).message); } }; const columns: Column[] = [ { key: 'id', title: 'ID', width: 60 }, { key: 'cover', title: '封面', width: 100, render: (r) => r.cover ? ( // eslint-disable-next-line @next/next/no-img-element {r.title} ) : ( ), }, { key: 'title', title: '标题' }, { key: 'category', title: '分类', width: 100, render: (r) => r.category?.name ?? '-', }, { key: 'createdAt', title: '发布时间', width: 160, render: (r) => formatDate(r.createdAt, 'YYYY-MM-DD HH:mm'), }, { key: 'isTop', title: '置顶', width: 80, render: (r) => r.isTop === 1 ? : '-', }, { key: 'status', title: '状态', width: 80, render: (r) => r.status === 1 ? ( 发布 ) : ( 草稿 ), }, { key: '_op', title: '操作', width: 150, render: (r) => (
{isSuperAdmin && ( )}
), }, ]; return (
setKeyword(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { setSearchKw(keyword); setPage(1); } }} className="max-w-xs" /> } right={ <> } /> columns={columns} rows={data?.list ?? []} total={data?.total ?? 0} page={page} pageSize={10} loading={isLoading} onPageChange={setPage} rowKey={(r) => String(r.id)} /> {editId ? '编辑新闻' : '新增新闻'}
setForm({ ...form, title: e.target.value })} />
setForm({ ...form, cover: url })} />