本章我们将实现博客的文章管理功能。
数据库结构
CREATE TABLE topics (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
category_id INT NOT NULL,
summary VARCHAR(255) NOT NULL,
markdown VARCHAR NOT NULL,
html VARCHAR NOT NULL,
hit INT NOT NULL DEFAULT 0,
dateline TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_del BOOLEAN NOT NULL DEFAULT FALSE,
FOREIGN KEY (category_id) REFERENCES categories (id)
);
字段 | 说明 |
---|---|
id |
主键。唯一标识,自增。 |
title |
文章标题 |
category_id |
外键。分类ID |
summary |
摘要。纯文本 |
markdown |
内容的markdown格式。 |
html |
内容的HTML格式。 |
hit |
文章浏览次数 |
dateline |
文章添加时间 |
is_del |
是否删除 |
提示:PostgreSQL中,没有限定长度的
VARCHAR
等同于TEXT
,并且效率远高于其它数据库的TEXT
,同时可以指定默认值。
数据库视图
CREATE VIEW v_topic_cat_list AS
SELECT t.id, title, summary, hit, dateline,category_id,t.is_del,
c.name AS category_name
FROM
topics AS t
INNER JOIN categories AS c
ON t.category_id=c.id
WHERE c.is_del = false
;
该视图主要用于列表显示文章,为了显示分类名称,关联了分类表。
数据模型
// src/model.rs
#[derive(PostgresMapper, Serialize)]
#[pg_mapper(table="v_topic_cat_list")]
pub struct TopicList {
pub id:i64,
pub title: String,
pub category_id:i32,
pub summary:String,
pub hit:i32,
pub dateline:time::SystemTime,
pub is_del:bool,
pub category_name:String,
}
impl TopicList {
pub fn dateline(&self) ->String {
let ts = self.dateline.clone().duration_since(time::UNIX_EPOCH).unwrap_or(time::Duration::from_secs(0)).as_secs() as i64;
Local.timestamp(ts, 0).format("%Y/%m/%d %H:%M:%S").to_string()
}
}
#[derive(PostgresMapper, Serialize)]
#[pg_mapper(table="topics")]
pub struct TopicID {
pub id:i64,
}
#[derive(PostgresMapper, Serialize)]
#[pg_mapper(table="topics")]
pub struct TopicEditData {
pub id:i64,
pub title: String,
pub category_id: i32,
pub summary: String,
pub markdown: String,
}
TopicList
:对应数据库视图v_topic_cat_list
TopicList::dateline()
:将dateline
字段格式化为年/月/日 时:分:秒
的字符串TopicID
:文章的IDTopicEditData
:用于修改的文章数据
依赖
为了处理文章发表的时间,引入新的依赖:
# Cargo.toml
[dependencies]
# ...
chrono = "0.4"
数据库操作
以下代码均位于
src/db/topic.rs
文件。
增加文章 create()
pub async fn create(client: &Client, frm: &form::CreateTopic) -> Result<TopicID> {
let html = md2html(&frm.markdown);
let dateline = time::SystemTime::now();
super::insert(client, "INSERT INTO topics (title,category_id, summary, markdown, html, hit, dateline, is_del) VALUES ($1, $2, $3, $4, $5, 0, $6, false) RETURNING id", &[&frm.title, &frm.category_id, &frm.summary, &frm.markdown, &html, &dateline ], "添加文章失败").await
}
form::CreateTopic
的定义见下文的“表单类”部分。
本函数将表单提交的Markdown转换成HTML,然后分别保存到数据库中。
分页获取文章列表list()
pub async fn list(client: &Client, page: u32) -> Result<Paginate<Vec<TopicList>>> {
let sql=format!("SELECT id,title,category_id,summary,hit,dateline,is_del,category_name FROM v_topic_cat_list WHERE is_del=false ORDER BY id DESC LIMIT {} OFFSET {}", DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE as u32 * page);
let count_sql = "SELECT COUNT(*) FROM v_topic_cat_list WHERE is_del=false";
super::pagination(client, &sql, count_sql, &[], page).await
}
注意,该函数并没有从原始的topics
数据表中获取数据,而是从视图v_topic_cat_list
中获取。
修改文章update()
pub async fn update(client: &Client, frm: &EditTopic, id: i64) -> Result<bool> {
let html = md2html(&frm.markdown);
let sql =
"UPDATE topics SET title=$1,category_id=$2,summary=$3,markdown=$4,html=$5 WHERE id=$6";
let n = super::execute(
client,
sql,
&[
&frm.title,
&frm.category_id,
&frm.summary,
&frm.markdown,
&html,
&id,
],
)
.await?;
Ok(n > 0)
}
查找用于修改的文章数据find2edit()
pub async fn find2edit(client: &Client, id: i64) -> Result<TopicEditData> {
super::query_row(
client,
"SELECT id,title,category_id,summary,markdown FROM topics WHERE id=$1 LIMIT 1",
&[&id],
)
.await
}
删除或还原文章del_or_restore()
pub async fn del_or_restore(client: &Client, id: i64, is_del: bool) -> Result<bool> {
let n = super::del_or_restore(client, "topics", &id, is_del).await?;
Ok(n > 0)
}
Markdown转HTML md2html()
fn md2html(markdown: &str) -> String {
md::to_html(markdown)
}
该函数调用的md::to_html()
位于md
模块,该模块的说明见下文。
分页 Paginate
为方便模板的分页操作,对Paginate
扩展了几个方法:
// src/db/paginate.rs
impl<T> Paginate<T> {
// ...
pub fn has_prev(&self) ->bool {
self.page > 0
}
pub fn last_page(&self) -> i64 {
self.total_pages-1
}
pub fn has_next(&self)->bool {
(self.page as i64) < self.last_page()
}
pub fn is_active(&self, page :&i64)->bool {
(self.page as i64) == *page
}
}
has_prev()
:是否有上一页last_page()
:最后一页的页码has_next()
:是否有下一页is_active()
:判断指定的页码是否是当前激活页码
md
模块
为了将Markdown转换为HTML,增加该模块:
// src/md.rs
use pulldown_cmark::{html, Options, Parser};
fn get_parser(md: &str) -> Parser {
Parser::new_ext(md, Options::all())
}
pub fn to_html(md: &str) -> String {
let mut out_html = String::new();
html::push_html(&mut out_html, get_parser(md));
out_html
}
get_parser()
:初始化解析器to_html()
:将Markdown转换为HTML
表单类
// src/form.rs
#[derive(Deserialize)]
pub struct CreateTopic {
pub title: String,
pub category_id: i32,
pub summary: String,
pub markdown: String,
}
pub type EditTopic = CreateTopic;
CreateTopic
:创建文章的表单EditTopic
:修改文章的表单
注意,为展示更多的可能性,这里 EditTopic
直接以别名的形式由CreateTopic
定义。这种上一章的EditCategory
是不同的。
字段 | 说明 | 表单元素 |
---|---|---|
title |
文章标题 | 单行文本框(input) |
category_id |
分类ID | 下拉框(select) |
summary |
摘要 | 多行文本框(textarea) |
markdown |
表单输入的Markdown | 多行文本框(textarea) |
模板
为了节约篇幅,本章只挑几个特殊的模板进行说明。完整的模板请通过本章分支的对应目录查看。
分页模板 templates/pagination.html
<nav>
<ul class="pagination">
{% if list.has_prev() %}
<li class="page-item">
<a class="page-link" href="?page={{ page - 1 }}" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
{% endif %}
{%for i in 0..list.total_pages%}
<li class="page-item{% if list.is_active(i) %} active{%endif%}" aria-current="page">
<a class="page-link" href="?page={{i}}">{{ i+1 }}</a>
</li>
{%endfor%}
{% if list.has_next() %}
<li class="page-item">
<a class="page-link" href="?page={{page+1}}" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
{% endif %}
</ul>
</nav>
这里的 list
需要Paginate<Vec<T>>
类型。
文章列表模板 index.html
由于文章列表需要进行分页,所以在该模板的content
块中,引入了分页模板:
{% extends "./../base.html" %}
{% block title%}所有文章{%endblock%}
{% block toolbar %} {% include "./toolbar.html" %} {%endblock%}
{% block msg %} {%include "../msg.index"%} {%endblock%}
{% block content %}
<table class="table table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>标题</th>
<th>分类</th>
<th>摘要</th>
<th>浏览次数</th>
<th>时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in list.data%}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.title }}</td>
<td>{{ item.category_name }}</td>
<td>{{ item.summary }}</td>
<td>{{ item.hit }}</td>
<td class="dateline">{{ item.dateline() }}</td>
<td>
<a href="/admin/topic/edit/{{ item.id }}" class="btn btn-primary btn-sm">修改</a>
<a href="/admin/topic/del/{{ item.id }}" class="btn btn-danger btn-sm" onclick="return confirm('确定删除「{{ item.title }}」');">删除</a>
</td>
</tr>
{%endfor%}
</tbody>
</table>
{% include "../../pagination.html" %}
{%endblock%}
视图类
// src/view/backend/topic.rs
#[derive(Template)]
#[template(path="backend/topic/add.html")]
pub struct Add {
pub cats : Vec<Category>,
}
#[derive(Template)]
#[template(path="backend/topic/index.html")]
pub struct Index {
pub msg:Option<String>,
pub page: u32,
pub list:Paginate<Vec<TopicList>>,
}
#[derive(Template)]
#[template(path="backend/topic/edit.html")]
pub struct Edit {
pub cats : Vec<Category>,
pub item: TopicEditData,
}
Add
:添加文章的视图。由于需要分类列表,所以其中包含了pub cats : Vec<Category>
字段Index
:文章列表视图。 +msg
:显示提示信息 +page
:分页的页码 +list
:带分页信息的文章列表Edit
:视图。除了分类列表之外,还包含了要修改的文章的数据item
handler
文章管理的handler定义在src/handler/backend/topic.rs,由于没有涉及新知识,请自行在源码仓库查看。
路由
文章管理的路由定义在src/handler/backend/mod.rs,由于没有涉及新知识,请自行在源码仓库查看。
标签:category,markdown,axum,list,pub,html,构建,博客,id From: https://www.cnblogs.com/pythonClub/p/17779454.html