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