Init
This commit is contained in:
commit
84458b3d3e
|
@ -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');
|
|
@ -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"
|
|
@ -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"
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -0,0 +1,2 @@
|
|||
pub mod category;
|
||||
pub mod article;
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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, ()))
|
||||
}
|
|
@ -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>;
|
|
@ -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;
|
|
@ -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))
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
use sea_orm::DatabaseConnection;
|
||||
|
||||
pub struct AppState {
|
||||
pub conn: DatabaseConnection,
|
||||
}
|
|
@ -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>,
|
||||
}
|
|
@ -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%}
|
|
@ -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%}
|
|
@ -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>
|
|
@ -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%}
|
|
@ -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%}
|
|
@ -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%}
|
|
@ -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%}
|
|
@ -0,0 +1,2 @@
|
|||
{% extends "base.html" %} {% block title %}首页{%endblock%} {% block content %}
|
||||
首页内容 {%endblock%}
|
Loading…
Reference in New Issue