import { useMemo, useState } from 'react' import { Link, useNavigate } from 'react-router' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { CirclePlus, Eye, EyeOff, Pencil, Pin, Search, Trash2 } from 'lucide-react' import { Button, Card, Input, Modal, Segmented, Table, Tag } from 'antd' import type { TableProps } from 'antd' import { api } from '../lib/api' import type { NoteItem } from '../lib/api' import { invalidatePublic } from '../lib/invalidate' import { useDocumentTitle } from '../features/site' import { formatDateISO } from '../lib/utils' import { AdminTopbar } from '../components/Layout' import { useToast } from '@/hooks/use-toast' type StatusFilter = 'all' | 'public' | 'private' function useAdminNotes() { return useQuery({ queryKey: ['admin', 'notes'], queryFn: () => api<{ items: NoteItem[]; total: number }>('/api/admin/notes'), }) } export default function AdminList() { useDocumentTitle('笔记管理') const { data, isLoading } = useAdminNotes() const qc = useQueryClient() const navigate = useNavigate() const { toast } = useToast() const [confirmNote, setConfirmNote] = useState(null) const [filter, setFilter] = useState('all') const [query, setQuery] = useState('') const trashMutation = useMutation({ mutationFn: (id: number) => api(`/api/admin/notes/${id}`, { method: 'DELETE' }), onSuccess: () => { setConfirmNote(null) toast({ description: '笔记已移至回收站' }) void qc.invalidateQueries({ queryKey: ['admin'] }) invalidatePublic(qc) }, onError: () => toast({ description: '操作失败,请重试', variant: 'error' }), }) const toggleStatus = useMutation({ mutationFn: async (note: NoteItem) => { // 状态切换:读全文 → 翻转 status → PUT const full = await api(`/api/admin/notes/${note.id}`) return api(`/api/admin/notes/${note.id}`, { method: 'PUT', body: JSON.stringify({ title: full.title, slug: full.slug, summary: full.summary, content: full.content, status: full.status === 'public' ? 'private' : 'public', tags: full.tags, pinned: full.pinned, }), }) }, onSuccess: (_d, note) => { toast({ description: note.status === 'public' ? '已转为私有' : '已转为公开' }) void qc.invalidateQueries({ queryKey: ['admin'] }) invalidatePublic(qc) }, onError: () => toast({ description: '操作失败,请重试', variant: 'error' }), }) const items = data?.items ?? [] const publicCount = items.filter((n) => n.status === 'public').length const privateCount = items.length - publicCount const visible = useMemo(() => { const q = query.trim().toLowerCase() return items.filter((n) => { if (filter === 'public' && n.status !== 'public') return false if (filter === 'private' && n.status === 'public') return false if (q) { const hay = `${n.title} ${n.slug} ${n.tags.join(' ')}`.toLowerCase() if (!hay.includes(q)) return false } return true }) }, [items, filter, query]) const columns: TableProps['columns'] = [ { title: '标题', key: 'title', render: (_, n) => ( <> {n.pinned && } {n.title} /{n.slug} ), }, { title: '标签', key: 'tags', render: (_, n) => n.tags.length > 0 ? (
{n.tags.map((t) => {t})}
) : ( — ), }, { title: '日期', key: 'date', render: (_, n) => {formatDateISO(n.updated_at)}, }, { title: '状态', key: 'status', render: (_, n) => { const isPublic = n.status === 'public' return ( {isPublic ? '已发布' : '私有'} ) }, }, { title: '操作', key: 'actions', className: 'pn-al-col-actions', render: (_, n) => { const isPublic = n.status === 'public' return (
) }, }, ] return ( <>
笔记 {data?.total ?? items.length} 全部笔记,含 {privateCount} 篇私有 已发布 {publicCount} 对外公开的笔记 私有 {privateCount} 尚未公开,仅自己可见
setFilter(v as StatusFilter)} className="min-w-0" options={[ { value: 'all', label: <>全部{items.length} }, { value: 'public', label: <>已发布{publicCount} }, { value: 'private', label: <>私有{privateCount} }, ]} /> 共 {visible.length} 篇笔记
{isLoading ? (
加载中…
) : visible.length === 0 ? (
{items.length === 0 ? '还没有笔记,点击「新建笔记」开始' : '没有符合筛选条件的笔记'}
) : ( (n.status === 'public' ? '' : 'pn-al-is-draft')} /> )}
共 {data?.total ?? 0} 篇 · 显示 {visible.length} 篇
setConfirmNote(null)} title="移入回收站?" width={380} centered maskClosable={false} cancelText="取消" okText={trashMutation.isPending ? '删除中…' : '确认删除'} confirmLoading={trashMutation.isPending} onOk={() => { if (confirmNote) trashMutation.mutate(confirmNote.id) }} >

「{confirmNote?.title}」将被软删除,30 天内可在回收站恢复。

{trashMutation.isError &&

操作失败,请重试

}
) }