首页 > 其他分享 >使用axum构建博客系统 - 文章管理

使用axum构建博客系统 - 文章管理

时间:2023-10-21 20:36:07浏览次数:38  
标签:category markdown axum list pub html 构建 博客 id

本章我们将实现博客的文章管理功能。

数据库结构

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:文章的ID
  • TopicEditData:用于修改的文章数据

依赖

为了处理文章发表的时间,引入新的依赖:

# 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">&laquo;</span>
            </a>
        </li>
        {% else %}
        <li class="page-item disabled">
            <a class="page-link"  aria-label="Previous">
                <span aria-hidden="true">&laquo;</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">&raquo;</span>
            </a>
        </li>
        {% else %}
        <li class="page-item disabled">
            <a class="page-link"  aria-label="Next">
                <span aria-hidden="true">&raquo;</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

相关文章

  • 使用axum构建博客系统 - 后台管理菜单及首页模板
    目前,后台管理功能基本完成,但还有两个工作没做:清理后台管理的导航菜单以及后台管理首页的模板。后台管理菜单<!--templates/backend/base.html--><!--...--><divclass="container-fluid"><divclass="row"><navid="sidebarMenu"c......
  • 使用axum构建博客系统 - 鉴权与登录
    本章实现后台管理的鉴权,以及管理员的登录、注销功能。涉及的知识点有:cookie及中间件等。数据库结构CREATETABLEadmins(idSERIALPRIMARYKEY,emailVARCHAR(255)NOTNULL,passwordVARCHAR(255)NOTNULL,is_delBOOLEANNOTNULLDEFAULTFALSE);字段说......
  • 使用axum构建博客系统 - 网站首页
    后台管理完成后,我们开始进入前台功能的开发。本章我们将完成博客首页的开发。母模板templates/frontend/base.html是时候对前台母模板进行数据填充和块的定义了:<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="utf-8"/><metaname="viewport"c......
  • 使用axum构建博客系统 - 分类文章列表
    本章将实现博客的分类文章列表功能。模板请参见代码仓库的templates/frontend/topic_list.html视图类请参见代码仓库的src/view/frontend/topic.rshandler//src/handler/frontend/topic.rspubasyncfnlist(Extension(state):Extension<Arc<AppState>>,Path(......
  • 使用axum构建博客系统 - 文章详情
    本章将实现博客文章的详情显示功能。数据库视图CREATEVIEWv_topic_cat_detailASSELECTt.id,title,html,hit,dateline,category_id,t.is_del,c.nameAScategory_nameFROMtopicsAStINNERJOINcategoriesAScONt.cate......
  • 使用axum构建博客系统 - 存档文章列表
    本章将实现存档文章列表功能。注意,本章涉及较多PostgreSQL知识,如果你对相关知识不熟悉,可以先让代码跑起来,再去了解相关知识。模板本功能模板文件是templates/frontend/topic_arch.html。视图类本功能视图类定义在src/view/frontend/topic.rs文件。handler//src/handler/fro......
  • Linux中如何构建内核源码树
    Linux中如何构建内核源码树,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。要编写Linux的驱动,必须要构建内核源码树。内核源码树的作用是构造可以在Linux内核中装载和卸载的模块,因此若是要为目标板构造模块,必须保证......
  • 1、构建目录必须和源文件为同级目录
    复制工程后路径发生变化,重新构建运行时会出现“QT构建目录必须和源目录为同级目录”提示,解决办法如下:方法一:点击project(项目)->然后,看看Buitdirectory(构建目录),看看这里是不是文字变成了红色,如果是红色的路径,说明路径是错误的,改一个你自己的路径即可(但要和工程目录平级)。注......
  • 每日博客
    1.Hive是由Facebook公司开发的一个构建在Hadoop之上的数据仓库工具,在某种程度上可以看作是用户编程接口,其本身并不存储和处理数据2.Hive一般依赖于分布式文件系统HDFS,而传统数据库则依赖于本地文件系统,Hive和传统关系数据库都支持分区,传统关系数据库很难实现横向扩展,Hive具......
  • 博客园主题美化
    一、首页主题预览二、主题部署1.开通js权限2.css代码禁用css模板点击查看代码#loading{bottom:0;left:0;position:fixed;right:0;top:0;z-index:9999;background-color:#f4f5f5;pointer-events:none;}.loader-inner{will-change:transform;width:40px;height:40px;positi......