添加文章管理和文章分组页面

This commit is contained in:
Eamon
2026-06-09 16:37:40 +08:00
parent 9f74483345
commit d2c7846a91
24 changed files with 1908 additions and 32 deletions

View File

@@ -0,0 +1,135 @@
"use client"
import { Loader2 } from "lucide-react"
import dynamic from "next/dynamic"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { useCallback, useEffect, useState } from "react"
import { toast } from "sonner"
import { createArticle, getArticle, updateArticle } from "@/actions/article"
import type { ArticleGroup } from "@/models/article-group"
const Editor = dynamic(() => import("@/components/editor/richTextEditor"), {
ssr: false,
loading: () => (
<div className="flex items-center justify-center h-64 text-muted-foreground">
...
</div>
),
})
interface ArticleEditorProps {
articleId: string
groups: ArticleGroup[]
}
export default function ArticleEditor({
articleId,
groups,
}: ArticleEditorProps) {
const router = useRouter()
const isNew = articleId === "new"
const [loading, setLoading] = useState(!isNew)
const [title, setTitle] = useState("")
const [initialContent, setInitialContent] = useState("")
const [groupId, setGroupId] = useState<string>(
groups.length > 0 ? String(groups[0].id) : "",
)
useEffect(() => {
if (!isNew) {
const id = Number(articleId)
if (Number.isNaN(id)) {
toast.error("无效的文章 ID")
router.push("/articles")
return
}
getArticle(id).then(resp => {
if (resp.success && resp.data) {
setTitle(resp.data.title || "")
setInitialContent(resp.data.content || "")
if (resp.data.group_id) {
setGroupId(String(resp.data.group_id))
}
} else {
toast.error(
resp.success ? "文章数据为空" : resp.message || "文章不存在",
)
router.push("/articles")
}
setLoading(false)
})
}
}, [articleId, isNew, router])
const handleSave = useCallback(
async (data: { title: string; content: string }) => {
if (!groupId) {
toast.error("请选择文章分组")
return
}
if (isNew) {
const resp = await createArticle({
title: data.title,
content: data.content,
group_id: Number(groupId),
status: 1,
})
if (resp.success) {
toast.success("文章创建成功")
router.push("/articles")
} else {
toast.error(resp.message || "创建失败")
}
} else {
const id = Number(articleId)
const resp = await updateArticle({
id,
title: data.title,
content: data.content,
group_id: Number(groupId),
status: 1,
})
if (resp.success) {
toast.success("文章保存成功")
router.push("/articles")
} else {
toast.error(resp.message || "保存失败")
}
}
},
[isNew, articleId, groupId, router],
)
if (loading) {
return (
<div className="flex items-center justify-center h-full">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
)
}
if (groups.length === 0) {
return (
<div className="flex flex-col items-center justify-center h-full gap-4 text-muted-foreground">
<p></p>
<Link href="/article-groups">
<span className="text-primary hover:underline"></span>
</Link>
</div>
)
}
return (
<div className="flex-1 h-full min-h-0">
<Editor
content={initialContent}
title={title}
groups={groups}
groupId={groupId}
onGroupChange={setGroupId}
onSave={handleSave}
/>
</div>
)
}

View File

@@ -0,0 +1,22 @@
import { getArticleGroupList } from "@/actions/article-group"
import { Page } from "@/components/page"
import ArticleEditor from "./article-editor"
export default async function ArticleEditPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const groupsResp = await getArticleGroupList()
console.log(groupsResp, "groupsResp")
const groups = groupsResp.success ? groupsResp.data : []
console.log(groups, "groups")
return (
<Page>
<ArticleEditor articleId={id} groups={groups} />
</Page>
)
}

View File

@@ -0,0 +1,105 @@
"use client"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { Suspense, useCallback } from "react"
import { toast } from "sonner"
import { getArticlePage, removeArticle } from "@/actions/article"
import { DataTable, useDataTable } from "@/components/data-table"
import { Page } from "@/components/page"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import type { Article } from "@/models/article"
import { formatDate } from "@/models/formatDate"
export default function ArticleListPage() {
const router = useRouter()
const fetchFn = useCallback(
(page: number, size: number) => getArticlePage({ page, size }),
[],
)
const table = useDataTable(fetchFn)
console.log(table, "tabletabletable")
const handleDelete = useCallback(
async (id: number, title: string) => {
if (!confirm(`确定要删除文章《${title}》吗?此操作不可恢复!`)) return
try {
const resp = await removeArticle(id)
console.log(resp, "resprespresp")
if (resp.success) {
toast.success("文章已删除")
table.refresh()
router.refresh()
} else {
toast.error(resp.message || "删除失败")
}
} catch (error) {
toast.error("删除失败")
console.error(error)
}
},
[table, router],
)
return (
<Page>
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"></h1>
<Link href="/articles/new">
<Button></Button>
</Link>
</div>
<Suspense>
<DataTable<Article>
{...table}
columns={[
{ header: "标题", accessorKey: "title" },
{
header: "状态",
accessorKey: "status",
cell: ({ row }) => (
<Badge
variant={row.original.status === 1 ? "default" : "secondary"}
>
{row.original.status === 1 ? "已发布" : "草稿"}
</Badge>
),
},
{
header: "更新时间",
accessorKey: "updated_at",
cell: ({ row }) => formatDate(row.original.updated_at),
},
{
header: "操作",
id: "actions",
cell: ({ row }) => (
<div className="flex gap-2">
<Link
href={`/articles/${row.original.id}`}
className="text-primary hover:underline"
>
</Link>
<button
onClick={() =>
handleDelete(row.original.id, row.original.title)
}
className="text-destructive hover:underline"
>
</button>
</div>
),
},
]}
/>
</Suspense>
</Page>
)
}