文章管理
在本文中,我们将展示如何使用 Tio Boot Admin 和 React 配合 WangEditor 来实现文章管理功能,包括文章的分类管理、创建、编辑、列表展示以及预览。以下是完整的实现步骤和代码示例。
数据表
为实现文章管理功能,需要设计两张数据表:文章分类表和文章表。 文章分类表
drop table if exists tio_boot_admin_article_category;
create table tio_boot_admin_article_category(
"id" BIGINT PRIMARY KEY,
"orders" int,
name varchar,
remark VARCHAR(256),
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted SMALLINT DEFAULT 0,
tenant_id BIGINT NOT NULL DEFAULT 0
);
文章表
drop table if exists tio_boot_admin_article;
CREATE TABLE tio_boot_admin_article (
"id" BIGINT PRIMARY KEY,
category_id int default 0,
title varchar,
content TEXT NOT NULL,
visibility VARCHAR(10) NOT NULL DEFAULT 'public', -- 'public' 或 'private'
views int,
files jsonb,
tags varchar,
"embedding" VECTOR,
"search_vector" TSVECTOR,
remark VARCHAR(256),
creator VARCHAR(64) DEFAULT '',
create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) DEFAULT '',
update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted SMALLINT DEFAULT 0,
tenant_id BIGINT NOT NULL DEFAULT 0
);
CREATE OR REPLACE FUNCTION update_tio_boot_admin_article_search_vector() RETURNS trigger AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('english', coalesce(NEW.title, '')), 'A') ||
setweight(to_tsvector('english', coalesce(NEW.content, '')), 'B');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_tio_boot_admin_article_search_vector
BEFORE INSERT OR UPDATE ON tio_boot_admin_article
FOR EACH ROW EXECUTE FUNCTION update_tio_boot_admin_article_search_vector();
CREATE INDEX idx_tio_boot_admin_article_search_vector ON tio_boot_admin_article USING GIN (search_vector);
分类
文章分类模块用于管理分类信息,如排序、名称、备注等。
articleCategoryColumn.tsx
import { ProColumns } from "@ant-design/pro-components";
export const articleCategoryColumns = (): ProColumns<any>[] => [
{ title: "orders", dataIndex: "orders", valueType: "text" },
{ title: "name", dataIndex: "name", valueType: "text" },
{ title: "Remark", dataIndex: "remark", valueType: "text" },
{ title: "Creator", dataIndex: "creator", valueType: "text", hideInForm: true },
{ title: "Update Time", dataIndex: "update_time", valueType: "dateTime", hideInSearch: true, hideInForm: true },
];
articleCategoryService.ts 定义分页、创建等服务的预处理逻辑
export const articleCategoryBeforePageRequest = (params: any, isRecoveryMode?: boolean, containsUpload?: boolean) => {
params.idType = "long";
params.remarkOp = "ct";
params.orderBy = "orders";
params.update_time_type = "string[]";
params.update_time_op = "bt";
params.update_time_to_type = "ISO8601";
if (containsUpload) {
params.json_fields = ["files"];
}
if (isRecoveryMode) {
params.deleted = 1;
} else {
params.deleted = 0;
}
params.nameOp = "ct";
return params;
};
export const articleCategoryCreatePageRequest = (params: any, containsUpload?: boolean) => {
params.ordersType = "int";
if (containsUpload) {
params.json_fields = ["files"];
}
return params;
};
分类页面 articleCategoryIndex.tsx
使用 ApiTableLong 组件实现分类展示和管理:
import React from "react";
import {
articleCategoryBeforePageRequest,
articleCategoryCreatePageRequest,
} from "@/pages/article/articleCategory/articleCategoryService";
import ApiTableLong from "@/components/common/ApiTableLong";
import { articleCategoryColumns } from "@/pages/article/articleCategory/articleCategoryColumn";
export default () => {
const from = "tio_boot_admin_article_category";
return (
<ApiTableLong
from={from}
columns={articleCategoryColumns()}
beforePageRequest={articleCategoryBeforePageRequest}
beforeCreateRequest={articleCategoryCreatePageRequest}
/>
);
};
显示效果
文章
文章模块用于管理文章内容的创建、编辑、列表展示和预览。
创建文章
articleService.ts 实现文章的创建和查询:
import { createRequest, getRequest } from "@/utils/apiTable";
const tableName = "tio_boot_admin_article";
export async function createSystemArticle(data: any) {
return createRequest(data, tableName);
}
export async function getArticleById(id: any) {
return getRequest(id, { idType: "long" }, tableName);
}
createArticle.tsx 基于 ProForm 和 WangEditor 实现文章创建表单
import React from "react";
import CommonEditor from "@/components/common/CommonEditor";
import {ProForm, ProFormText} from "@ant-design/pro-components";
import {createSystemArticle, getArticleById} from "@/pages/article/article/articleService";
import {Form, message} from "antd";
// @ts-ignore
import {useParams} from 'umi';
const CreateArticle: React.FC = () => {
// 使用泛型确保类型正确
const {id} = useParams<{ id: string }>();
// 编辑器内容
const [html, setHtml] = React.useState('')
const [messageApi, contextHolder] = message.useMessage();
const [form] = Form.useForm();
// 使用 useEffect 来在组件挂载时获取文章内容
React.useEffect(() => {
if (id && id !== ':id') {
getArticleById(id).then(response => {
console.log("response:{}", response)
form.setFieldsValue({id: response.data.id, title: response.data.title});
setHtml(response.data.content);
}).catch(err => message.error('Failed to fetch article: ' + err));
}
}, [id]);
const handleSubmit = async () => {
const formValues = await form.validateFields();
formValues.content = html;
const hide = messageApi.loading("submitting...")
try {
const response = await createSystemArticle(formValues);
if (response.ok) {
messageApi.success("create article successful")
setHtml("")
form.resetFields();
} else {
messageApi.error("failed to crate article")
}
hide()
} catch (error) {
messageApi.error("" + error)
hide()
}
}
return (
<>
{contextHolder}
<ProForm layout="horizontal" onFinish={handleSubmit} form={form}>
<ProFormText name="id" label="id" hidden/>
<ProFormText name="title" label="Title"
rules={[{required: true}]}/>
<CommonEditor value={html} onChange={editor => setHtml(editor.getHtml())}/>
</ProForm>
</>
)
}
export default CreateArticle;
文章列表
articleColumn.tsx 定义文章列表的表格列:
import { ProColumns } from "@ant-design/pro-components";
export const systemArticleListColumns = (): ProColumns<any>[] => [
{
title: "Title",
dataIndex: "title",
formItemProps(form) {
return {
rules: [
{
required: true,
},
],
};
},
},
{
title: "content",
dataIndex: "content",
valueType: "textarea",
hideInForm: true,
ellipsis: true,
formItemProps(form) {
return {
rules: [
{
required: true,
},
],
};
},
},
{
title: "category_id",
dataIndex: "category_id",
},
{
title: "views",
dataIndex: "views",
},
{
title: "visibility",
dataIndex: "visibility",
},
{
title: "Remark",
dataIndex: "remark",
},
{
title: "update_time",
dataIndex: "update_time",
valueType: "dateTime",
hideInSearch: true,
hideInForm: true,
},
{
key: "update_time",
title: "update_time",
dataIndex: "update_time_range",
valueType: "dateTimeRange",
hideInTable: true,
hideInForm: true,
},
];
articleIndex.tsx 实现文章的列表展示和操作:
import React from "react";
import { systemArticleListColumns } from "@/pages/article/article/articleColumn";
// @ts-ignore
import { useNavigate } from "umi";
import ApiTableLong from "@/components/common/ApiTableLong";
export default () => {
const from = "tio_boot_admin_article";
let navigate = useNavigate();
const beforePageRequest = (params: any) => {
params.idType = "long";
params.titleOp = "ct";
params.contentOp = "ct";
params.remarkOp = "ct";
params.deleted = 0;
params.orderBy = "update_time";
params.update_time_type = "string[]";
params.update_time_op = "bt";
params.isAsc = "false";
return params;
};
const beforeCreateRequest = (formValues: any) => {
return {
...formValues,
idType: "long",
};
};
const editContentAndPreview = {
title: "Edit",
valueType: "option",
width: 200,
render: (text: any, row: any) => [
<a key="editContent" onClick={() => navigate("/article/create/" + row.id, { replace: true })}>
Edit Content
</a>,
<a key="preView" onClick={() => navigate("/article/" + row.id, { replace: true })}>
Preview
</a>,
],
};
const columns = [...systemArticleListColumns(), editContentAndPreview];
return (
<ApiTableLong
from={from}
columns={columns}
beforePageRequest={beforePageRequest}
beforeCreateRequest={beforeCreateRequest}
/>
);
};
显示效果
预览文章
为文章预览提供样式支持: previewArticel.css
.editor-content-view {
/*border: 3px solid #ccc;*/
border-radius: 5px;
padding: 0 10px;
margin-top: 20px;
overflow-x: auto;
}
.editor-content-view p,
.editor-content-view li {
white-space: pre-wrap; /* 保留空格 */
}
.editor-content-view blockquote {
border-left: 8px solid #d0e5f2;
padding: 10px 10px;
margin: 10px 0;
background-color: #f1f1f1;
}
.editor-content-view code {
font-family: monospace;
background-color: #eee;
padding: 3px;
border-radius: 3px;
}
.editor-content-view pre > code {
display: block;
padding: 10px;
}
.editor-content-view table {
border-collapse: collapse;
}
.editor-content-view td,
.editor-content-view th {
border: 1px solid #ccc;
min-width: 50px;
height: 20px;
}
.editor-content-view th {
background-color: #f1f1f1;
}
.editor-content-view ul,
.editor-content-view ol {
padding-left: 20px;
}
.editor-content-view input[type="checkbox"] {
margin-right: 5px;
}
previewArticle.tsx 加载文章并渲染 HTML 内容:
import React from "react";
import { getArticleById } from "@/pages/article/article/articleService";
import { message } from "antd";
// @ts-ignore
import { useParams } from "umi";
import "./previewArticel.css";
import "@wangeditor/editor/dist/css/style.css";
const PreviewArticle: React.FC = () => {
const { id } = useParams<{ id: string }>();
// 编辑器内容
const [html, setHtml] = React.useState("");
const [title, setTitle] = React.useState("");
const [messageApi, contextHolder] = message.useMessage();
// 使用 useEffect 来在组件挂载时获取文章内容
React.useEffect(() => {
if (id && id !== ":id") {
getArticleById(id)
.then((response) => {
if (response.ok) {
setTitle(response.data.title);
setHtml(response.data.content);
} else {
messageApi.error("Failed to fetch article: " + id);
}
})
.catch((err) => messageApi.error("Failed to fetch article: " + err));
}
}, [id]);
return (
<>
{contextHolder}
<h1 style={{ textAlign: "center" }}>{title}</h1>
<div id="editor-content-view" className="editor-content-view" dangerouslySetInnerHTML={{ __html: html }} />
</>
);
};
export default PreviewArticle;
菜单
{
path: '/article',
name: 'article',
icon: 'FileWordOutlined',
routes: [
{
path: 'articleCategory',
name: 'Article Category',
component: './article/articleCategory/articleCategoryIndex',
},
{
path: '/article/create/:id',
name: 'Create Article',
hideInMenu: false,
component: './article/article/createArticle',
},
{
path: '/article/article',
name: 'Article',
hideInMenu: false,
component: './article/article/articleIndex',
},
],
},
{
path: '/article/:id',
hideInMenu: true,
layout: false,
name: 'PreviewArticle',
component: './article/article/previewArticle',
},
总结
通过 Tio Boot Admin 与 React 的结合,配合 WangEditor,我们可以高效实现文章管理功能,包括分类、创建、展示和预览。这种方式结构清晰,易于扩展,适用于中小型 CMS 系统的开发需求。