2025.11.27.17.50

This commit is contained in:
RUI
2025-11-27 17:50:44 +08:00
commit 5dbb30b32c
111 changed files with 18320 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
import ArticleEditor from '@/components/admin/ArticleEditor';
export default function CreateArticlePage() {
return <ArticleEditor mode="create" />;
}

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { useRouter } from 'next/router';
import ArticleEditor from '@/components/admin/ArticleEditor';
import AdminLayout from '@/components/admin/AdminLayout';
export default function EditArticlePage() {
const router = useRouter();
const { id } = router.query;
if (!id) return null;
return (
<AdminLayout>
<ArticleEditor mode="edit" articleId={id as string} />
</AdminLayout>
);
}

View File

@@ -0,0 +1,238 @@
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import Link from 'next/link';
import AdminLayout from '@/components/admin/AdminLayout';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import { Plus, Search, Edit, Trash2, Eye, Loader2 } from 'lucide-react';
interface Article {
_id: string;
文章标题: string;
ID: { _id: string; 分类名称: string } | null;
ID: { _id: string; username: string } | null;
: 'draft' | 'published' | 'offline';
: {
阅读数: number;
点赞数: number;
};
createdAt: string;
}
export default function ArticlesPage() {
const router = useRouter();
const [articles, setArticles] = useState<Article[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [deleteId, setDeleteId] = useState<string | null>(null);
useEffect(() => {
fetchArticles();
}, [page, searchTerm]);
const fetchArticles = async () => {
setLoading(true);
try {
const res = await fetch(`/api/admin/articles?page=${page}&limit=10&search=${searchTerm}`);
const data = await res.json();
if (res.ok) {
setArticles(data.articles);
setTotalPages(data.pagination.pages);
}
} catch (error) {
console.error('Failed to fetch articles', error);
} finally {
setLoading(false);
}
};
const handleDelete = async () => {
if (!deleteId) return;
try {
const res = await fetch(`/api/admin/articles/${deleteId}`, {
method: 'DELETE',
});
if (res.ok) {
fetchArticles();
setDeleteId(null);
} else {
alert('删除失败');
}
} catch (error) {
console.error('Failed to delete article', error);
alert('删除出错');
}
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'published':
return <Badge className="bg-green-500"></Badge>;
case 'draft':
return <Badge variant="secondary">稿</Badge>;
case 'offline':
return <Badge variant="destructive"></Badge>;
default:
return <Badge variant="outline">{status}</Badge>;
}
};
return (
<AdminLayout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold tracking-tight"></h2>
<p className="text-muted-foreground">
</p>
</div>
<Link href="/admin/articles/create">
<Button>
<Plus className="mr-2 h-4 w-4" />
</Button>
</Link>
</div>
<div className="flex items-center space-x-2">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜索文章标题..."
className="pl-8"
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setPage(1); // 重置到第一页
}}
/>
</div>
</div>
<div className="rounded-md border bg-white">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead> (/)</TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center">
<Loader2 className="mx-auto h-6 w-6 animate-spin" />
</TableCell>
</TableRow>
) : articles.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center">
</TableCell>
</TableRow>
) : (
articles.map((article) => (
<TableRow key={article._id}>
<TableCell className="font-medium max-w-[200px] truncate" title={article.}>
{article.}
</TableCell>
<TableCell>{article.ID?. || '-'}</TableCell>
<TableCell>{article.ID?.username || 'Unknown'}</TableCell>
<TableCell>{getStatusBadge(article.)}</TableCell>
<TableCell>
{article..} / {article..}
</TableCell>
<TableCell>{new Date(article.createdAt).toLocaleDateString()}</TableCell>
<TableCell className="text-right space-x-2">
<Link href={`/admin/articles/edit/${article._id}`}>
<Button variant="ghost" size="icon">
<Edit className="h-4 w-4" />
</Button>
</Link>
<Button
variant="ghost"
size="icon"
className="text-red-500 hover:text-red-600 hover:bg-red-50"
onClick={() => setDeleteId(article._id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* Pagination */}
<div className="flex items-center justify-end space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
>
</Button>
<span className="text-sm text-muted-foreground">
{page} / {totalPages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
>
</Button>
</div>
{/* Delete Confirmation Dialog */}
<Dialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDeleteId(null)}>
</Button>
<Button variant="destructive" onClick={handleDelete}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</AdminLayout>
);
}