添加文章管理和文章分组页面
This commit is contained in:
135
src/app/(root)/articles/[id]/article-editor.tsx
Normal file
135
src/app/(root)/articles/[id]/article-editor.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
22
src/app/(root)/articles/[id]/page.tsx
Normal file
22
src/app/(root)/articles/[id]/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
105
src/app/(root)/articles/page.tsx
Normal file
105
src/app/(root)/articles/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user