This commit is contained in:
胡天 2024-02-04 16:33:41 +08:00
commit 84458b3d3e
27 changed files with 1233 additions and 0 deletions

5
.env Normal file
View File

@ -0,0 +1,5 @@
# 日志级别
RUST_LOG=INFO
# 运行模式
RUN_MODE=dev

52
app.sql Normal file
View File

@ -0,0 +1,52 @@
CREATE TABLE categoies
( -- 分类
id int unsigned PRIMARY KEY AUTO_INCREMENT, -- 自增主键
name VARCHAR(20) NOT NULL UNIQUE, -- 分类名称
is_del BOOLEAN NOT NULL DEFAULT FALSE -- 是否删除
);
CREATE TABLE articles
( -- 文章
id int unsigned PRIMARY KEY AUTO_INCREMENT, -- 自增主键
category_id INT unsigned NOT NULL REFERENCES categoies (id), -- 文章所属分类的ID外键
title VARCHAR(255) NOT NULL, -- 文章标题
content TEXT NOT NULL, -- 文章内容
dateline timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 添加时间
is_del BOOLEAN NOT NULL DEFAULT FALSE -- 是否删除
);
-- 插入示例数据
INSERT INTO categoies (id, name)
VALUES (1, 'Rust'),
(2, 'Go'),
(3, 'Javascript');
INSERT INTO categoies (name)
VALUES ('U3A0CsWdiy'),
('SWACTQFa0Y'),
('GYqfhaKJ6J'),
('0sjsXVArdZ'),
('MiN8lR1g9B'),
('oBorPeyIvH'),
('cqS4jGnmxG'),
('dc0qqvbDNP'),
('jq8K6LgUFy'),
('K1tKtlvzgf'),
('Z5kEYZYEdp'),
('y3K6ryqRMF'),
('hwPu60bq1u'),
('2Idzt9CmAV'),
('vbLGfMJNHz'),
('6tTPkRtpWB'),
('sWBfrpOAIB'),
('zgmXGcYsGt'),
('WH2EBpojIS'),
('m1rsNTknqS');
INSERT INTO articles (category_id, title, content)
VALUES (1, '标题-GLKUSroPOR', '内容-GLKUSroPOR'),
(1, '标题-hFQRulHJAk', '内容-hFQRulHJAk'),
(2, '标题-pM0TURxhwC', '内容-pM0TURxhwC'),
(1, '标题-svNJmWaqRo', '内容-svNJmWaqRo'),
(3, '标题-8XWiTUSfhB', '内容-8XWiTUSfhB'),
(2, '标题-yvwE32TLkg', '内容-yvwE32TLkg');

6
config/default.toml Normal file
View File

@ -0,0 +1,6 @@
[web]
addr = "0.0.0.0:9527"
[mysql]
max_cons = 5
dsn = "mysql://root:mysql123!%40%23@127.0.0.1:3306/study"

6
config/dev.toml Normal file
View File

@ -0,0 +1,6 @@
[web]
addr = "0.0.0.0:9527"
[mysql]
max_cons = 5
dns = "mysql://root:mysql123!%40%23@127.0.0.1:3306/study"

46
src/config.rs Normal file
View File

@ -0,0 +1,46 @@
use std::env;
use config::File;
use serde::Deserialize;
use crate::err::AppError;
use crate::Result;
#[derive(Deserialize)]
pub struct MysqlConfig {
pub dsn: String,
pub max_cons: u32,
}
#[derive(Deserialize)]
pub struct WebConfig {
pub addr: String,
}
#[derive(Deserialize)]
pub struct AppConfig {
pub mysql: MysqlConfig,
pub web: WebConfig,
}
impl AppConfig {
pub fn new() -> Result<Self> {
let run_mode = env::var("RUN_MODE").unwrap_or_else(|_| "dev".into());
// https://github.com/mehcode/config-rs/blob/master/examples/hierarchical-env/settings.rs
config::Config::builder()
// Start off by merging in the "default" configuration file
.add_source(File::with_name("config/default"))
// Add in the current environment file
// Default to 'dev' env
// Note that this file is _optional_
.add_source(
File::with_name(&format!("config/{}", run_mode)).required(false),
)
.build()
.map_err(AppError::from)?
.try_deserialize()
.map_err(AppError::from)
}
}

49
src/entity/article.rs Normal file
View File

@ -0,0 +1,49 @@
use serde::{Deserialize, Serialize};
use sea_orm::entity::prelude::*;
// Model是只读的只能用来SELECT
#[derive(Debug, Clone, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "articles")]
pub struct Model {
#[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub id: u32,
pub category_id: u32,
pub title: String,
pub content: String,
pub dateline: chrono::DateTime<chrono::Local>,
pub is_del: bool,
}
#[derive(Debug, Clone, Copy, EnumIter)]
pub enum Relation {
// 给文章实体定义了包含Category的关系
Category,
}
impl RelationTrait for Relation {
// 定义了 Relation 并没实际意义,毕竟它只是一个枚举值而已。我们需要为这个关系实现业务逻辑:
fn def(&self) -> RelationDef {
return match self {
// 声明当前模型属于category模型(多对一)从category_id关联对应表的id字段
Relation::Category => {
Entity::belongs_to(super::category::Entity)
.from(Column::CategoryId)
.to(super::category::Column::Id)
.into()
}
}
}
}
// 定义好了关系,我们还需要实现 Relate告诉 SeaORM ,我们定义的 Relation::Category关系需要如何去建立
impl Related<super::category::Entity> for Entity {
fn to() -> RelationDef {
Relation::Category.def()
}
}
// INSERT/UPDATE/DELETE 等属于写操作,需使用 ActiveModel。
/// [DeriveEntityModel] 这个 derive 根据我们定义的[Model],生成了包括 [ActiveModel]在内的多个数据结构
impl ActiveModelBehavior for ActiveModel {}

36
src/entity/category.rs Normal file
View File

@ -0,0 +1,36 @@
use serde::{Deserialize, Serialize};
use sea_orm::entity::prelude::*;
// Model是只读的只能用来SELECT
#[derive(Debug, Clone, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "categoies")]
pub struct Model {
#[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub id: u32,
pub name: String,
pub is_del: bool,
}
#[derive(Debug, Clone, Copy, EnumIter)]
pub enum Relation {
Articles,
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
Entity::has_many(super::article::Entity).into()
}
}
impl Related<super::article::Entity> for Entity {
fn to() -> RelationDef {
Relation::Articles.def()
}
}
// INSERT/UPDATE/DELETE 等属于写操作,需使用 ActiveModel。
/// [DeriveEntityModel] 这个 derive 根据我们定义的[Model],生成了包括 [ActiveModel]在内的多个数据结构
impl ActiveModelBehavior for ActiveModel {}

2
src/entity/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod category;
pub mod article;

73
src/err.rs Normal file
View File

@ -0,0 +1,73 @@
#[derive(Debug)]
pub enum AppErrorType {
Config,
Database,
Notfound,
Template,
}
type Cause = Box<dyn std::error::Error>;
#[derive(Debug)]
pub enum AppErrorItem {
Message(String),
Cause(Cause),
}
#[derive(Debug)]
pub struct AppError {
pub types: AppErrorType,
pub error: AppErrorItem,
}
impl AppError {
pub fn new(types: AppErrorType, error: AppErrorItem) -> Self {
Self { types, error }
}
pub fn from_err(cause: Cause, types: AppErrorType) -> Self {
Self::new(types, AppErrorItem::Cause(cause))
}
pub fn from_msg(msg: &str, types: AppErrorType) -> Self {
Self::new(types, AppErrorItem::Message(msg.to_string()))
}
pub fn notfound() -> Self {
Self::from_msg("不存在的记录", AppErrorType::Notfound)
}
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl std::error::Error for AppError {}
impl From<config::ConfigError> for AppError {
fn from(err: config::ConfigError) -> Self {
Self::from_err(Box::new(err), AppErrorType::Config)
}
}
impl From<askama::Error> for AppError {
fn from(err: askama::Error) -> Self {
Self::from_err(Box::new(err), AppErrorType::Template)
}
}
impl From<sea_orm::DbErr> for AppError {
fn from(err: sea_orm::DbErr) -> Self {
Self::from_err(Box::new(err), AppErrorType::Database)
}
}
impl axum::response::IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let msg = match self.error {
AppErrorItem::Cause(err) => err.to_string(),
AppErrorItem::Message(msg) => msg.to_string(),
};
msg.into_response()
}
}

12
src/form.rs Normal file
View File

@ -0,0 +1,12 @@
use serde::Deserialize;
#[derive(Deserialize)]
pub struct CategoryForm {
pub name: String,
}
#[derive(Deserialize)]
pub struct ArticleForm {
pub title: String,
pub category_id: u32,
pub content: String,
}

52
src/handler/article.rs Normal file
View File

@ -0,0 +1,52 @@
use std::sync::Arc;
use axum::extract::{Query, State};
use sea_orm::{ColumnTrait, Condition, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect};
use crate::entity::{article, category};
use crate::err::AppError;
use crate::handler::{get_conn, HtmlResponse, log_error, render};
use crate::param::ArticleParams;
use crate::Result;
use crate::state::AppState;
use crate::view;
pub async fn index(
State(state): State<Arc<AppState>>,
Query(params): Query<ArticleParams>,
) -> Result<HtmlResponse> {
let handler_name = "article/index";
let conn = get_conn(&state);
// 构建条件: 所有未被删除的文章
let condition = Condition::all().add(article::Column::IsDel.eq(false));
let select = article::Entity::find().filter(condition);
let record_total = select
.clone()
.count(conn)
.await
.map_err(AppError::from)
.map_err(log_error(handler_name))?;
let page_size = 15u64;
let page = 0u64;
let page_total = f64::ceil(record_total as f64 / page_size as f64) as u64;
let offset = page_size * page;
let list = select.find_also_related(category::Entity)
.order_by_desc(article::Column::Id)
.limit(page_size)
.offset(offset)
.all(conn)
.await
.map_err(AppError::from)
.map_err(log_error(handler_name))?;
let tpl = view::ArticlesTemplate {
list,
page_total,
params,
};
render(tpl, handler_name)
}

233
src/handler/category.rs Normal file
View File

@ -0,0 +1,233 @@
use std::sync::Arc;
use axum::extract::{Path, Query, State};
use axum::Form;
use sea_orm::{ActiveModelTrait, ColumnTrait, Condition, EntityTrait, IntoActiveModel, ModelTrait, PaginatorTrait, QueryFilter, QueryOrder};
use sea_orm::ActiveValue::Set;
use crate::{form, Result, view};
use crate::entity::{article, category};
use crate::err::AppError;
use crate::handler::{get_conn, HtmlResponse, log_error, redirect, RedirectResponse, render};
use crate::param::{CategoryParams, DelParams};
use crate::state::AppState;
pub async fn index(
State(state): State<Arc<AppState>>,
Query(params): Query<CategoryParams>,
) -> Result<HtmlResponse> {
let handler_name = "category/index";
let conn = get_conn(&state);
let page = params.page(); // 当前页码
let page_size = params.page_size(); // 每页条数默认15
let mut select = category::Entity::find()
.filter(
Condition::all()
.add_option(
params.keyword_opt().map(|n| category::Column::Name.contains(&n))
)
.add_option(params.is_del_opt().map(|n| category::Column::IsDel.eq(n)))
);
if let Some(ord) = params.order() {
select = select.order_by(category::Column::Id, ord);
}
let paginator = select.paginate(conn, page_size);
let page_total = paginator.num_pages().await.map_err(AppError::from)?;
if page_total == 0 {
let tpl = view::CategoryTemplate {
categories: vec![],
params,
page_total,
};
return render(tpl, handler_name);
}
let categories = paginator
.fetch_page(page)
.await
.map_err(AppError::from)
.map_err(log_error(handler_name))?;
let tpl = view::CategoryTemplate {
categories,
params,
page_total,
};
render(tpl, handler_name)
}
pub async fn find(
State(state): State<Arc<AppState>>,
Path(id): Path<i32>,
) -> Result<String> {
let id = u32::try_from(id).or(Err(AppError::notfound()))?;
let handler_name = "category/find";
let conn = get_conn(&state);
let cate = category::Entity::find_by_id(id)
.one(conn)
.await
.map_err(AppError::from)
.map_err(log_error(handler_name))?;
match cate {
None => Err(AppError::notfound()),
Some(cate) => Ok(format!("id: {}, 名称: {}", cate.id, cate.name)),
}
}
pub async fn add_ui() -> Result<HtmlResponse> {
let handler_name = "category/add_ui";
let tpl = view::CategoryAddTemplate;
render(tpl, handler_name)
}
pub async fn add(
State(state): State<Arc<AppState>>,
Form(frm): Form<form::CategoryForm>,
) -> Result<RedirectResponse> {
let handler_name = "category/add";
let conn = get_conn(&state);
let am = category::ActiveModel {
name: Set(frm.name),
// all other attributes are `NotSet`
..Default::default()
};
let added_category = am.insert(conn)
.await
.map_err(AppError::from)
.map_err(log_error(handler_name))?;
let url = format!("/category?msg=分类添加成功ID是{}", added_category.id);
redirect(url.as_str())
}
pub async fn edit_ui(
State(state): State<Arc<AppState>>,
Path(id): Path<i32>,
) -> Result<HtmlResponse> {
let id = u32::try_from(id).or(Err(AppError::notfound()))?;
let handler_name = "category/edit_ui";
let conn = get_conn(&state);
let cate = category::Entity::find_by_id(id)
.one(conn)
.await
.map_err(AppError::from)
.map_err(log_error(handler_name))?
.ok_or(AppError::notfound())?;
let tpl = view::CategoryEditTemplate { category: cate };
render(tpl, handler_name)
}
pub async fn edit(
State(state): State<Arc<AppState>>,
Path(id): Path<u32>,
Form(frm): Form<form::CategoryForm>,
) -> Result<RedirectResponse> {
let id = u32::try_from(id).or(Err(AppError::notfound()))?;
let handler_name = "category/edit";
let conn = get_conn(&state);
let cate = category::Entity::find_by_id(id)
.one(conn)
.await
.map_err(AppError::from)
.map_err(log_error(handler_name))?
.ok_or(AppError::notfound())?;
let mut am = cate.into_active_model();
am.name = Set(frm.name);
am.update(conn)
.await
.map_err(AppError::from)
.map_err(log_error(handler_name))?;
redirect("/category?msg=分类修改成功")
}
pub async fn del(
State(state): State<Arc<AppState>>,
Path(params): Path<DelParams>,
) -> Result<RedirectResponse> {
let handler_name = "category/del";
let conn = get_conn(&state);
let real = params.real.unwrap_or(false);
let cate = category::Entity::find_by_id(params.id)
.one(conn)
.await
.map_err(AppError::from)
.map_err(log_error(handler_name))?
.ok_or(AppError::notfound())?;
if real {
cate.delete(conn)
.await
.map_err(AppError::from)
.map_err(log_error(handler_name))?;
} else {
let mut am = cate.into_active_model();
am.is_del = Set(true);
am.save(conn)
.await
.map_err(AppError::from)
.map_err(log_error(handler_name))?;
}
redirect("/category?msg=分类删除成功")
}
pub async fn articles(
State(state): State<Arc<AppState>>,
Path(id): Path<u32>,
Query(params): Query<CategoryParams>,
) -> Result<HtmlResponse> {
let handler_name = "category/articles";
let conn = get_conn(&state);
let cate = category::Entity::find_by_id(id)
.one(conn)
.await
.map_err(AppError::from)
.map_err(log_error(handler_name))?
.ok_or(AppError::notfound())
.map_err(log_error(handler_name))?;
let paginator = cate.find_related(article::Entity)
.paginate(conn, params.page_size());
let articles = paginator.fetch_page(params.page())
.await
.map_err(AppError::from)
.map_err(log_error(handler_name))?;
let page_total = paginator
.num_pages()
.await
.map_err(AppError::from)
.map_err(log_error(handler_name))?;
let tpl = view::CategoryArticlesTemplate {
params,
page_total,
category: cate,
articles,
};
render(tpl, handler_name)
}

8
src/handler/index.rs Normal file
View File

@ -0,0 +1,8 @@
use crate::handler::{HtmlResponse, render};
use crate::{Result, view};
pub async fn index() -> Result<HtmlResponse> {
let handler_name = "index";
let tpl = view::IndexTemplate;
render(tpl, handler_name)
}

45
src/handler/mod.rs Normal file
View File

@ -0,0 +1,45 @@
use askama::Template;
use axum::http::{header, HeaderMap, StatusCode};
use axum::response::Html;
use sea_orm::DatabaseConnection;
pub use index::index;
use crate::err::AppError;
use crate::Result;
use crate::state::AppState;
pub mod category;
mod index;
pub mod article;
fn get_conn(state: &AppState) -> &DatabaseConnection {
&state.conn
}
type HtmlResponse = Html<String>;
type RedirectResponse = (StatusCode, HeaderMap, ());
/// 渲染模板
fn render<T: Template>(tpl: T, handler_name: &str) -> Result<HtmlResponse> {
let out = tpl
.render()
.map_err(AppError::from)
.map_err(log_error(handler_name))?;
Ok(Html(out))
}
/// 记录错误
fn log_error(handler_name: &str) -> Box<dyn Fn(AppError) -> AppError> {
let handler_name = handler_name.to_string();
Box::new(move |err| {
tracing::error!("{}: {:?}", handler_name, err);
err
})
}
fn redirect(url: &str) -> Result<RedirectResponse> {
let mut header = HeaderMap::new();
header.insert(header::LOCATION, url.parse().unwrap());
Ok((StatusCode::FOUND, header, ()))
}

11
src/lib.rs Normal file
View File

@ -0,0 +1,11 @@
pub mod entity;
pub mod state;
pub mod config;
pub mod handler;
pub mod view;
pub mod err;
pub mod router;
pub mod param;
pub mod form;
pub type Result<T> = std::result::Result<T, err::AppError>;

103
src/param.rs Normal file
View File

@ -0,0 +1,103 @@
use serde::Deserialize;
const DEFAULT_PAGE_SIZE: u64 = 15;
#[derive(Debug, Clone, Deserialize)]
pub struct CategoryParams {
// 用于搜索的关键字
pub keyword: Option<String>,
// 是否删除
pub is_del: Option<i32>,
// 排序
pub sort: Option<String>,
// 每页记录条数
pub page_size: Option<u64>,
// 当前页码
pub page: Option<u64>,
pub msg: Option<String>,
}
impl Default for CategoryParams {
fn default() -> Self {
Self {
keyword: None,
is_del: None,
sort: None,
page_size: None,
page: None,
msg: None,
}
}
}
impl CategoryParams {
pub fn keyword(&self) -> String {
self.keyword.clone().unwrap_or("".to_string())
}
pub fn keyword_opt(&self) -> Option<String> {
match &self.keyword {
Some(s) => {
if s.is_empty() {
None
} else {
Some(s.to_string())
}
}
_ => None,
}
}
pub fn is_del_opt(&self) -> Option<bool> {
match self.is_del {
Some(n) => match n {
0 => Some(false),
1 => Some(true),
_ => None,
},
_ => None,
}
}
pub fn is_del(&self) -> i32 {
match self.is_del_opt() {
Some(b) => {
if b {
1
} else {
0
}
}
_ => -1,
}
}
pub fn sort(&self) -> String {
self.sort.clone().unwrap_or("".to_string())
}
pub fn order(&self) -> Option<sea_orm::Order> {
match self.sort().as_str() {
"asc" => Some(sea_orm::Order::Asc),
"desc" => Some(sea_orm::Order::Desc),
_ => None,
}
}
pub fn page_size(&self) -> u64 {
let ps = self.page_size.unwrap_or(0);
if ps <= 0 {
return DEFAULT_PAGE_SIZE;
}
ps
}
pub fn page(&self) -> u64 {
self.page.unwrap_or(0)
}
}
#[derive(Deserialize, Debug)]
pub struct DelParams {
pub id: u32,
pub real: Option<bool>,
}
pub type ArticleParams = CategoryParams;

22
src/router.rs Normal file
View File

@ -0,0 +1,22 @@
use std::sync::Arc;
use axum::routing::get;
use crate::handler;
use crate::state::AppState;
pub fn init() -> axum::Router<Arc<AppState>> {
axum::Router::new()
.route("/", get(handler::index))
.route("/category", get(handler::category::index))
.route(
"/category/add",
get(handler::category::add_ui).post(handler::category::add),
)
.route(
"/category/edit/:id",
get(handler::category::edit_ui).post(handler::category::edit),
)
.route("/category/del/:id", get(handler::category::del))
.route("/category/del/:id/:real", get(handler::category::del))
.route("/category/articles/:id", get(handler::category::articles))
.route("/article", get(handler::article::index))
}

6
src/state.rs Normal file
View File

@ -0,0 +1,6 @@
use sea_orm::DatabaseConnection;
pub struct AppState {
pub conn: DatabaseConnection,
}

47
src/view.rs Normal file
View File

@ -0,0 +1,47 @@
use askama::Template;
use crate::{entity, param};
#[derive(Template)]
#[template(path = "index.html")]
pub struct IndexTemplate;
#[derive(Template)]
#[template(path = "category.html")]
pub struct CategoryTemplate {
pub params: param::CategoryParams,
pub categories: Vec<entity::category::Model>,
pub page_total: u64,
}
#[derive(Template)]
#[template(path = "category-add.html")]
pub struct CategoryAddTemplate;
#[derive(Template)]
#[template(path = "category-edit.html")]
pub struct CategoryEditTemplate {
pub category: entity::category::Model,
}
#[derive(Template)]
#[template(path = "category-articles.html")]
pub struct CategoryArticlesTemplate {
pub params: param::CategoryParams,
pub page_total: u64,
pub category: entity::category::Model,
pub articles: Vec<entity::article::Model>,
}
#[derive(Template)]
#[template(path = "article.html")]
pub struct ArticlesTemplate {
pub page_total: u64,
pub list: Vec<(entity::article::Model, Option<entity::category::Model>)>,
pub params: param::ArticleParams,
}
#[derive(Template)]
#[template(path = "article-add.html")]
pub struct ArticleAddTemplate {
pub categies: Vec<entity::category::Model>,
}

View File

@ -0,0 +1,39 @@
{% extends "base.html" %} {% block title %}添加文章{%endblock%} {% block bc %}
<li class="breadcrumb-item"><a href="/article">分类管理</a></li>
{% endblock %} {% block content %}
<form action="/article/add" method="post">
<form>
<div class="mb-3">
<label for="title" class="form-label">文章标题</label>
<input
type="text"
name="title"
class="form-control"
id="title"
placeholder="输入文章标题"
required
/>
</div>
<div class="mb-3">
<label for="category_id" class="form-label">所属分类</label>
<select class="form-control" id="category_id" name="category_id" required>
<option value="">--请选择--</option>
{% for category in categies%}
<option value="{{category.id}}">{{category.name}}</option>
{%endfor%}
</select>
</div>
<div class="mb-3">
<label for="content" class="form-label">文章内容</label>
<textarea
id="content"
name="content"
class="form-control"
placeholder="请输入文章内容"
required
></textarea>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
</form>
{%endblock%}

46
templates/article.html Normal file
View File

@ -0,0 +1,46 @@
{% extends "base.html" %} {% block title %}文章列表{%endblock%} {% block bc %}
<li class="breadcrumb-item"><a href="/article">文章管理</a></li>
{% endblock %} {% block content %} {%if params.msg.is_some() %}
<div class="alert alert-info" role="alert">
{{ params.msg.clone().unwrap() }}
</div>
{% endif %}
<table class="table">
<thead>
<tr>
<th>#</th>
<th>标题</th>
<th>分类</th>
<th>时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for item in list %} {% let (article, category) = item %}
<tr>
<td>{{ article.id }}</td>
<td>{{ article.title }}</td>
<td>
{% match category%} {% when Some with(category)%} {{ category.name }} {%
when None %} 没有分类 {% endmatch %}
</td>
<td>{{ article.dateline }}</td>
<td>作业</td>
</tr>
{% endfor %}
</tbody>
</table>
<nav>
<ul class="pagination pagination">
{% for page in 0..page_total %} {% if page == params.page() %}
<li class="page-item active">
<span class="page-link">{{ page + 1}}</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{page}}">{{ page + 1}}</a>
</li>
{% endif %} {% endfor %}
</ul>
</nav>
{%endblock%}

160
templates/base.html Normal file
View File

@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css"
/>
<title>axum使用SeaORM</title>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-success">
<div class="container-fluid">
<a class="navbar-brand" href="https://axum.rs" target="_blank"
><i class="bi bi-arrow-through-heart-fill"></i> axum.rs</a
>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/" id="navbar-home">首页</a>
</li>
<li class="nav-item dropdown">
<a
class="nav-link dropdown-toggle"
href="#"
id="navbar-category"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
id="navbar-category"
>
分类管理
</a>
<ul class="dropdown-menu" aria-labelledby="navbar-category">
<li>
<a
class="dropdown-item"
href="/category"
id="navbar-category-list"
>分类列表</a
>
</li>
<li>
<a
class="dropdown-item"
href="/category/add"
id="navbar-category-add"
>添加分类</a
>
</li>
</ul>
</li>
<li class="nav-item dropdown">
<a
class="nav-link dropdown-toggle"
href="#"
id="navbar-article"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
id="navbar-article"
>
文章管理
</a>
<ul class="dropdown-menu" aria-labelledby="navbar-article">
<li>
<a
class="dropdown-item"
href="/article"
id="navbar-article-list"
>文章列表</a
>
</li>
<li>
<a
class="dropdown-item"
href="/article/add"
id="navbar-article-add"
>添加文章</a
>
</li>
</ul>
</li>
<li class="nav-item">
<a
class="nav-link"
href="https://github.com/AxumCourse/axum-with-seaorm"
target="_blank"
><i class="bi bi-github"></i
></a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container py-3">
<div class="row">
<div class="col">
<h2>{% block title %}标题{%endblock%}</h2>
</div>
<div class="col-2 text-end">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
{% block bc %} {%endblock%}
<li class="breadcrumb-item active" aria-current="page">
{%block title%}{%endblock%}
</li>
</ol>
</nav>
</div>
</div>
<main>{% block content %} 在此输入内容 {%endblock %}</main>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
<script>
$(function () {
activeNavItem();
function activeNavItem() {
const path = location.pathname;
$("#navbarNav .nav-link").removeClass("active");
$("#navbarNav .dropdown-item").removeClass("active");
if (path === "/") {
$("#navbar-home").addClass("active");
return;
}
if (path.indexOf("/category") >= 0) {
$("#navbar-category").addClass("active");
if (path === "/category") {
$("#navbar-category-list").addClass("active");
return;
}
if (path.indexOf("/add") >= 0) {
$("#navbar-category-add").addClass("active");
return;
}
return;
}
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,20 @@
{% extends "base.html" %} {% block title %}添加分类{%endblock%} {% block bc %}
<li class="breadcrumb-item"><a href="/category">分类管理</a></li>
{% endblock %} {% block content %}
<form action="/category/add" method="post">
<form>
<div class="mb-3">
<label for="name" class="form-label">分类名称</label>
<input
type="text"
name="name"
class="form-control"
id="name"
placeholder="输入分类名称"
required
/>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
</form>
{%endblock%}

View File

@ -0,0 +1,42 @@
{% extends "base.html" %} {% block title %}
分类文章{%endblock%} {% block bc %}
<li class="breadcrumb-item"><a href="/category">分类管理</a></li>
{%endblock%}
{%block content%}
<h2>{{category.name}}的所有文章</h2>
<table class="table">
<thead>
<tr>
<th>文章ID</th>
<th>文章标题</th>
<th>时间</th>
</tr>
</thead>
<tbody>
{%for article in articles%}
<tr>
<td>{{ article.id }}</td>
<td>{{article.title}}</td>
<td>{{article.dateline}}</td>
</tr>
{%endfor%}
</tbody>
</table>
{% if page_total > 1 %}
<nav>
<ul class="pagination pagination">
{% for page in 0..page_total %}
{% if page == params.page() %}
<li class="page-item active">
<span class="page-link">{{ page + 1}}</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{page}}">{{ page + 1}}</a>
</li>
{% endif %}
{% endfor %}
</ul>
</nav>
{% endif %}
{%endblock%}

View File

@ -0,0 +1,19 @@
{% extends "base.html" %} {% block title %}修改分类{%endblock%} {% block bc %}
<li class="breadcrumb-item"><a href="/category">分类管理</a></li>
{% endblock %} {% block content %}
<form action="/category/edit/{{category.id}}" method="post">
<div class="mb-3">
<label for="name" class="form-label">分类名称</label>
<input
type="text"
name="name"
class="form-control"
id="name"
placeholder="输入分类名称"
value="{{category.name}}"
required
/>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
{%endblock%}

91
templates/category.html Normal file
View File

@ -0,0 +1,91 @@
{% extends "base.html" %} {% block title %}分类列表{%endblock%} {% block bc %}
<li class="breadcrumb-item"><a href="/category">分类管理</a></li>
{% endblock %} {% block content %}
{%if params.msg.is_some() %}
<div class="alert alert-info" role="alert">
{{ params.msg.clone().unwrap() }}
</div>
{% endif %}
<form class="row" method="get" action="/category">
<div class="col-auto">
<label class="visually-hidden" for="keyword">关键字</label>
<div class="input-group">
<div class="input-group-text">关键字</div>
<input type="text" class="form-control" id="keyword" name="keyword" placeholder="输入关键字" value="{{ params.keyword() }}">
</div>
</div>
<div class="col-auto">
<label class="visually-hidden" for="is_del">是否删除</label>
<select class="form-select" id="is_del" name="is_del">
<option value="-1"{% if params.is_del() == -1%} selected{%endif%}>全部</option>
<option value="0"{% if params.is_del() == 0%} selected{%endif%}>未删除</option>
<option value="1"{% if params.is_del() == 1%} selected{%endif%}>已删除</option>
</select>
</div>
<div class="col-auto">
<label class="visually-hidden" for="sort">排序</label>
<select class="form-select" id="sort" name="sort">
<option value=""{% if params.sort().is_empty() %} selected{%endif%}>默认排序</option>
<option value="asc"{% if params.sort() == "asc" %} selected{%endif%}>升序</option>
<option value="desc"{% if params.sort() == "desc" %} selected{%endif%}>降序</option>
</select>
</div>
<div class="col-auto">
<label class="visually-hidden" for="page_size">每页条数</label>
<select class="form-select" id="page_size" name="page_size">
<option value="0"{% if params.page_size() == 0 %} selected{%endif%}>默认条数</option>
<option value="3"{%if params.page_size() == 3 %} selected{%endif%}>每页3条</option>
<option value="5"{%if params.page_size() == 5 %} selected{%endif%}>每页5条</option>
<option value="10"{%if params.page_size() == 10 %} selected{%endif%}>每页10条</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary"><i class="bi bi-search"></i> 搜索</button>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>#</th>
<th>名称</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for category in categories %}
<tr>
<td>{{ category.id }}</td>
<td>{{ category.name }}</td>
<td>
<a href="/category/articles/{{ category.id }}" class="btn btn-success btn-sm">文章列表</a>
<a href="/category/edit/{{ category.id }}" class="btn btn-primary btn-sm">修改</a>
<div class="btn-group">
<button type="button" class="btn btn-danger btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
删除
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/category/del/{{category.id}}" onclick="return confirm('确定删除【{{category.name}}】?')">逻辑删除</a></li>
<li><a class="dropdown-item" href="/category/del/{{category.id}}/true" onclick="return confirm('确定删除【{{category.name}}】?')">物理删除</a></li>
</ul>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<nav>
<ul class="pagination pagination">
{% for page in 0..page_total %}
{% if page == params.page() %}
<li class="page-item active">
<span class="page-link">{{ page + 1}}</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{page}}&keyword={{params.keyword()}}&is_del={{params.is_del()}}&sort={{params.sort()}}&page_size={{params.page_size()}}">{{ page + 1}}</a>
</li>
{% endif %}
{% endfor %}
</ul>
</nav>
{%endblock%}

2
templates/index.html Normal file
View File

@ -0,0 +1,2 @@
{% extends "base.html" %} {% block title %}首页{%endblock%} {% block content %}
首页内容 {%endblock%}