diff --git a/backend/src/api/auth/token.rs b/backend/src/api/auth/token.rs index 60ca90d..6a08419 100644 --- a/backend/src/api/auth/token.rs +++ b/backend/src/api/auth/token.rs @@ -6,15 +6,17 @@ use chrono::Duration; use rocket::{http::Status, post, response::status, serde::json::Json, State}; use serde::{Deserialize, Serialize}; use std::sync::Arc; +use crate::api::Role; + #[derive(Deserialize, Serialize)] -pub struct TokenSystemData { +pub struct TokenData { username: String, password: String, } #[post("/system", format = "application/json", data = "")] pub async fn token_system( state: &State>, - data: Json, + data: Json, ) -> AppResult { let sql = state.sql_get().await.into_app_result()?; let mut builder = builder::QueryBuilder::new( @@ -38,17 +40,6 @@ pub async fn token_system( ) .into_app_result()?, ), - builder::WhereClause::Condition( - builder::Condition::new( - "email".to_string(), - builder::Operator::Eq, - Some(builder::SafeValue::Text( - "author@lsy22.com".into(), - builder::ValidationLevel::Relaxed, - )), - ) - .into_app_result()?, - ), builder::WhereClause::Condition( builder::Condition::new( "role".to_string(), @@ -62,12 +53,15 @@ pub async fn token_system( ), ])); + println!("db: {:?}", sql.get_type()); + let values = sql .get_db() .execute_query(&builder) .await .into_app_result()?; + let password = values .first() .and_then(|row| row.get("password_hash")) @@ -80,6 +74,7 @@ pub async fn token_system( Ok(security::jwt::generate_jwt( security::jwt::CustomClaims { name: "system".into(), + role: Role::Administrator.to_string(), }, Duration::minutes(1), ) diff --git a/backend/src/api/fields.rs b/backend/src/api/fields.rs new file mode 100644 index 0000000..d5a60f5 --- /dev/null +++ b/backend/src/api/fields.rs @@ -0,0 +1,71 @@ +use crate::{ + common::error::{AppResult, CustomResult}, + storage::sql::{self, builder}, +}; +use builder::{SafeValue, SqlOperation, ValidationLevel}; +use std::fmt::{Display, Formatter}; +pub enum TargetType { + Post, + Page, + Theme, + System, +} + +impl Display for TargetType { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + match self { + TargetType::Post => write!(f, "post"), + TargetType::Page => write!(f, "page"), + TargetType::Theme => write!(f, "theme"), + TargetType::System => write!(f, "system"), + } + } +} + +pub enum FieldType { + Data, + Meta, +} + +impl Display for FieldType { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + match self { + FieldType::Data => write!(f, "data"), + FieldType::Meta => write!(f, "meta"), + } + } +} + +pub async fn insert_fields( + sql: &sql::Database, + target_type: TargetType, + target_id: i64, + field_type: FieldType, + field_key: String, + field_value: String, +) -> CustomResult<()> { + let mut builder = builder::QueryBuilder::new( + SqlOperation::Insert, + sql.table_name("fields"), + sql.get_type(), + )?; + builder.set_value( + "target_type".to_string(), + SafeValue::Text(target_type.to_string(), ValidationLevel::Strict), + )?; + builder.set_value("target_id".to_string(), SafeValue::Integer(target_id))?; + builder.set_value( + "field_type".to_string(), + SafeValue::Text(field_type.to_string(), ValidationLevel::Raw), + )?; + builder.set_value( + "field_key".to_string(), + SafeValue::Text(field_key, ValidationLevel::Raw), + )?; + builder.set_value( + "field_value".to_string(), + SafeValue::Text(field_value, ValidationLevel::Raw), + )?; + sql.get_db().execute_query(&builder).await?; + Ok(()) +} diff --git a/backend/src/api/mod.rs b/backend/src/api/mod.rs index 72419a0..ffe60da 100644 --- a/backend/src/api/mod.rs +++ b/backend/src/api/mod.rs @@ -1,11 +1,15 @@ pub mod auth; -pub mod settings; +pub mod fields; +pub mod page; +pub mod post; pub mod setup; pub mod users; -use crate::security::jwt; -use rocket::http::Status; + use rocket::request::{FromRequest, Outcome, Request}; use rocket::routes; +use crate::api::users::Role; +use rocket::http::Status; +use crate::security::jwt; pub struct Token(String); @@ -26,6 +30,7 @@ impl<'r> FromRequest<'r> for Token { } } + pub struct SystemToken(String); #[rocket::async_trait] @@ -43,10 +48,11 @@ impl<'r> FromRequest<'r> for SystemToken { } } + pub fn jwt_routes() -> Vec { routes![auth::token::token_system] } -pub fn configure_routes() -> Vec { - routes![settings::system_config_get] +pub fn fields_routes() -> Vec { + routes![] } diff --git a/backend/src/api/page.rs b/backend/src/api/page.rs new file mode 100644 index 0000000..4cb189d --- /dev/null +++ b/backend/src/api/page.rs @@ -0,0 +1,5 @@ +pub enum PageState { + Publicity, + Hidden, + Privacy, +} diff --git a/backend/src/api/post.rs b/backend/src/api/post.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/src/api/post.rs @@ -0,0 +1 @@ + diff --git a/backend/src/api/settings.rs b/backend/src/api/settings.rs deleted file mode 100644 index 70e1354..0000000 --- a/backend/src/api/settings.rs +++ /dev/null @@ -1,116 +0,0 @@ -use super::SystemToken; -use crate::common::error::{AppResult, AppResultInto, CustomResult}; -use crate::storage::{sql, sql::builder}; -use crate::AppState; -use rocket::{ - get, - http::Status, - serde::json::{Json, Value}, - State, -}; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use std::sync::Arc; - -#[derive(Deserialize, Serialize)] -pub struct SystemConfigure { - pub author_name: String, - pub current_theme: String, - pub site_keyword: String, - pub site_description: String, - pub admin_path: String, -} - -impl Default for SystemConfigure { - fn default() -> Self { - Self { - author_name: "lsy".to_string(), - current_theme: "echoes".to_string(), - site_keyword: "echoes".to_string(), - site_description: "echoes是一个高效、可扩展的博客平台".to_string(), - admin_path: "admin".to_string(), - } - } -} - -pub async fn get_setting( - sql: &sql::Database, - comfig_type: String, - name: String, -) -> CustomResult> { - let name_condition = builder::Condition::new( - "name".to_string(), - builder::Operator::Eq, - Some(builder::SafeValue::Text( - format!("{}_{}", comfig_type, name), - builder::ValidationLevel::Strict, - )), - )?; - - let where_clause = builder::WhereClause::Condition(name_condition); - - let mut sql_builder = builder::QueryBuilder::new( - builder::SqlOperation::Select, - sql.table_name("settings"), - sql.get_type(), - )?; - - sql_builder - .add_condition(where_clause) - .add_field("data".to_string())?; - println!("{:?}", sql_builder.build()); - - let result = sql.get_db().execute_query(&sql_builder).await?; - Ok(Json(json!(result))) -} - -pub async fn insert_setting( - sql: &sql::Database, - comfig_type: String, - name: String, - data: Json, -) -> CustomResult<()> { - let mut builder = builder::QueryBuilder::new( - builder::SqlOperation::Insert, - sql.table_name("settings"), - sql.get_type(), - )?; - builder.set_value( - "name".to_string(), - builder::SafeValue::Text( - format!("{}_{}", comfig_type, name).to_string(), - builder::ValidationLevel::Strict, - ), - )?; - builder.set_value( - "data".to_string(), - builder::SafeValue::Text(data.to_string(), builder::ValidationLevel::Relaxed), - )?; - sql.get_db().execute_query(&builder).await?; - Ok(()) -} - -#[get("/system")] -pub async fn system_config_get( - state: &State>, - _token: SystemToken, -) -> AppResult> { - let sql = state.sql_get().await.into_app_result()?; - let settings = get_setting(&sql, "system".to_string(), sql.table_name("settings")) - .await - .into_app_result()?; - Ok(settings) -} - -#[get("/theme/")] -pub async fn theme_config_get( - state: &State>, - _token: SystemToken, - name: String, -) -> AppResult> { - let sql = state.sql_get().await.into_app_result()?; - let settings = get_setting(&sql, "theme".to_string(), name) - .await - .into_app_result()?; - Ok(settings) -} diff --git a/backend/src/api/setup.rs b/backend/src/api/setup.rs index 29912f5..07fd7db 100644 --- a/backend/src/api/setup.rs +++ b/backend/src/api/setup.rs @@ -1,4 +1,6 @@ -use super::{settings, users}; +use super::fields::{FieldType, TargetType}; +use super::users::Role; +use super::{fields, users}; use crate::common::config; use crate::common::error::{AppResult, AppResultInto}; use crate::common::helpers; @@ -45,7 +47,7 @@ pub struct StepAccountData { } #[derive(Deserialize, Serialize, Debug)] -pub struct InstallReplyData { +pub struct StepAccountResponse { token: String, username: String, password: String, @@ -55,7 +57,7 @@ pub struct InstallReplyData { pub async fn setup_account( data: Json, state: &State>, -) -> AppResult>> { +) -> AppResult> { let mut config = config::Config::read().unwrap_or_default(); if config.init.administrator { return Err(status::Custom( @@ -73,10 +75,6 @@ pub async fn setup_account( state.sql_get().await.into_app_result()? }; - let system_credentials = ( - helpers::generate_random_string(20), - helpers::generate_random_string(20), - ); users::insert_user( &sql, @@ -84,32 +82,49 @@ pub async fn setup_account( username: data.username.clone(), email: data.email, password: data.password, - role: "administrator".to_string(), + role: Role::Administrator, }, ) .await .into_app_result()?; + let system_credentials = ( + helpers::generate_random_string(20), + helpers::generate_random_string(20), + ); + + let system_account = users::RegisterData { + username: system_credentials.0.clone(), + email: "author@lsy22.com".to_string(), + password: system_credentials.1.clone(), + role: Role::Administrator, + }; + users::insert_user( &sql, - users::RegisterData { - username: system_credentials.0.clone(), - email: "author@lsy22.com".to_string(), - password: system_credentials.1.clone(), - role: "administrator".to_string(), - }, + system_account, ) .await .into_app_result()?; - settings::insert_setting( + fields::insert_fields( &sql, - "system".to_string(), - "settings".to_string(), - Json(json!(settings::SystemConfigure { - author_name: data.username.clone(), - ..settings::SystemConfigure::default() - })), + TargetType::System, + 0, + FieldType::Meta, + "keywords".to_string(), + "echoes,blog,个人博客".to_string(), + ) + .await + .into_app_result()?; + + fields::insert_fields( + &sql, + TargetType::System, + 0, + FieldType::Data, + "current_theme".to_string(), + "echoes".to_string(), ) .await .into_app_result()?; @@ -117,6 +132,7 @@ pub async fn setup_account( let token = security::jwt::generate_jwt( security::jwt::CustomClaims { name: data.username, + role: Role::Administrator.to_string(), }, Duration::days(7), ) @@ -125,12 +141,9 @@ pub async fn setup_account( config::Config::write(config).into_app_result()?; state.trigger_restart().await.into_app_result()?; - Ok(status::Custom( - Status::Ok, - Json(InstallReplyData { - token, - username: system_credentials.0, - password: system_credentials.1, - }), - )) -} + Ok(Json(StepAccountResponse { + token, + username: system_credentials.0, + password: system_credentials.1, + })) +} \ No newline at end of file diff --git a/backend/src/api/users.rs b/backend/src/api/users.rs index 4727f1c..7c587aa 100644 --- a/backend/src/api/users.rs +++ b/backend/src/api/users.rs @@ -4,6 +4,7 @@ use crate::storage::{sql, sql::builder}; use regex::Regex; use rocket::{get, http::Status, post, response::status, serde::json::Json, State}; use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; #[derive(Deserialize, Serialize)] pub struct LoginData { @@ -11,24 +12,30 @@ pub struct LoginData { pub password: String, } +#[derive(Debug)] +pub enum Role { + Administrator, + Visitor, +} + +impl Display for Role { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + match self { + Role::Administrator => write!(f, "administrator"), + Role::Visitor => write!(f, "visitor"), + } + } +} + #[derive(Debug)] pub struct RegisterData { pub username: String, pub email: String, pub password: String, - pub role: String, + pub role: Role, } pub async fn insert_user(sql: &sql::Database, data: RegisterData) -> CustomResult<()> { - let role = match data.role.as_str() { - "administrator" | "contributor" => data.role, - _ => { - return Err( - "Invalid role. Must be either 'administrator' or 'contributor'".into_custom_error(), - ) - } - }; - let password_hash = bcrypt::generate_hash(&data.password)?; let re = Regex::new(r"([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)")?; @@ -57,7 +64,7 @@ pub async fn insert_user(sql: &sql::Database, data: RegisterData) -> CustomResul )? .set_value( "role".to_string(), - builder::SafeValue::Text(role, builder::ValidationLevel::Strict), + builder::SafeValue::Text(data.role.to_string(), builder::ValidationLevel::Strict), )?; sql.get_db().execute_query(&builder).await?; diff --git a/backend/src/main.rs b/backend/src/main.rs index 516a6a6..2e73b3c 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -106,9 +106,7 @@ async fn main() -> CustomResult<()> { rocket_builder = rocket_builder.mount("/", rocket::routes![api::setup::setup_account]); } else { state.sql_link(&config.sql_config).await?; - rocket_builder = rocket_builder - .mount("/auth/token", api::jwt_routes()) - .mount("/config", api::configure_routes()); + rocket_builder = rocket_builder.mount("/auth/token", api::jwt_routes()); } let rocket = rocket_builder.ignite().await?; diff --git a/backend/src/security/jwt.rs b/backend/src/security/jwt.rs index eb04fb9..213a1c7 100644 --- a/backend/src/security/jwt.rs +++ b/backend/src/security/jwt.rs @@ -9,6 +9,7 @@ use std::{env, fs, path::PathBuf}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct CustomClaims { pub name: String, + pub role: String, } pub enum SecretKey { diff --git a/backend/src/storage/sql/mysql.rs b/backend/src/storage/sql/mysql.rs index 805af5c..7e10fc9 100644 --- a/backend/src/storage/sql/mysql.rs +++ b/backend/src/storage/sql/mysql.rs @@ -6,8 +6,7 @@ use crate::common::error::{CustomErrorInto, CustomResult}; use crate::config; use async_trait::async_trait; use serde_json::Value; -use sqlx::mysql::MySqlPool; -use sqlx::{Column, Executor, Row, TypeInfo}; +use sqlx::{mysql::MySqlPool, Column, Executor, Row, TypeInfo}; use std::collections::HashMap; #[derive(Clone)] @@ -54,7 +53,7 @@ impl DatabaseTrait for Mysql { builder: &builder::QueryBuilder, ) -> CustomResult>> { let (query, values) = builder.build()?; - + println!("查询语句: {}", query); let mut sqlx_query = sqlx::query(&query); for value in values { @@ -69,6 +68,7 @@ impl DatabaseTrait for Mysql { } let rows = sqlx_query.fetch_all(&self.pool).await?; + println!("查询结果: {:?}", rows); Ok(rows .into_iter() @@ -119,7 +119,7 @@ impl DatabaseTrait for Mysql { let new_pool = Self::connect(&db_config, true).await?.pool; new_pool.execute(grammar.as_str()).await?; - new_pool.close(); + new_pool.close().await; Ok(()) } async fn close(&self) -> CustomResult<()> { diff --git a/backend/src/storage/sql/schema.rs b/backend/src/storage/sql/schema.rs index c5832bf..e9bfd2d 100644 --- a/backend/src/storage/sql/schema.rs +++ b/backend/src/storage/sql/schema.rs @@ -1,7 +1,7 @@ -use super::builder::{Condition, Identifier, Operator, SafeValue, ValidationLevel, WhereClause}; +use super::builder::{Identifier, Operator, SafeValue, ValidationLevel, WhereClause}; use super::DatabaseType; use crate::common::error::{CustomErrorInto, CustomResult}; -use std::fmt::{format, Display}; +use std::fmt::Display; #[derive(Debug, Clone, PartialEq)] pub enum FieldType { @@ -59,7 +59,6 @@ pub struct Field { pub name: Identifier, pub field_type: FieldType, pub constraints: FieldConstraint, - pub validation_level: ValidationLevel, } #[derive(Debug, Clone)] @@ -144,13 +143,11 @@ impl Field { name: &str, field_type: FieldType, constraints: FieldConstraint, - validation_level: ValidationLevel, ) -> CustomResult { Ok(Self { name: Identifier::new(name.to_string())?, field_type, constraints, - validation_level, }) } @@ -390,62 +387,6 @@ impl SchemaBuilder { pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomResult { let db_prefix = db_prefix.to_string()?; let mut schema = SchemaBuilder::new(); - let role_check = WhereClause::Or(vec![ - WhereClause::Condition(Condition::new( - "role".to_string(), - Operator::Eq, - Some(SafeValue::Text( - "'contributor'".to_string(), - ValidationLevel::Raw, - )), - )?), - WhereClause::Condition(Condition::new( - "role".to_string(), - Operator::Eq, - Some(SafeValue::Text( - "'administrator'".to_string(), - ValidationLevel::Raw, - )), - )?), - ]); - let content_state_check = WhereClause::Or(vec![ - WhereClause::Condition(Condition::new( - "status".to_string(), - Operator::Eq, - Some(SafeValue::Text( - "'published'".to_string(), - ValidationLevel::Raw, - )), - )?), - WhereClause::Condition(Condition::new( - "status".to_string(), - Operator::Eq, - Some(SafeValue::Text( - "'private'".to_string(), - ValidationLevel::Raw, - )), - )?), - WhereClause::Condition(Condition::new( - "status".to_string(), - Operator::Eq, - Some(SafeValue::Text( - "'hidden'".to_string(), - ValidationLevel::Raw, - )), - )?), - ]); - let target_type_check = WhereClause::Or(vec![ - WhereClause::Condition(Condition::new( - "target_type".to_string(), - Operator::Eq, - Some(SafeValue::Text("'post'".to_string(), ValidationLevel::Raw)), - )?), - WhereClause::Condition(Condition::new( - "target_type".to_string(), - Operator::Eq, - Some(SafeValue::Text("'page'".to_string(), ValidationLevel::Raw)), - )?), - ]); // 用户表 let mut users_table = Table::new(&format!("{}users", db_prefix))?; @@ -454,31 +395,26 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes "username", FieldType::VarChar(100), FieldConstraint::new().primary(), - ValidationLevel::Strict, )?) .add_field(Field::new( "avatar_url", FieldType::VarChar(255), FieldConstraint::new(), - ValidationLevel::Strict, )?) .add_field(Field::new( "email", FieldType::VarChar(255), FieldConstraint::new().unique().not_null(), - ValidationLevel::Strict, )?) .add_field(Field::new( "password_hash", FieldType::VarChar(255), FieldConstraint::new().not_null(), - ValidationLevel::Strict, )?) .add_field(Field::new( "role", FieldType::VarChar(20), - FieldConstraint::new().not_null().check(role_check.clone()), - ValidationLevel::Strict, + FieldConstraint::new().not_null(), )?) .add_field(Field::new( "created_at", @@ -487,7 +423,6 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes "CURRENT_TIMESTAMP".to_string(), ValidationLevel::Strict, )), - ValidationLevel::Strict, )?) .add_field(Field::new( "updated_at", @@ -496,16 +431,6 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes "CURRENT_TIMESTAMP".to_string(), ValidationLevel::Strict, )), - ValidationLevel::Strict, - )?) - .add_field(Field::new( - "last_login_at", - FieldType::Timestamp, - FieldConstraint::new().not_null().default(SafeValue::Text( - "CURRENT_TIMESTAMP".to_string(), - ValidationLevel::Strict, - )), - ValidationLevel::Strict, )?); schema.add_table(users_table)?; @@ -518,45 +443,49 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes "id", FieldType::Integer(true), FieldConstraint::new().primary(), - ValidationLevel::Strict, )?) .add_field(Field::new( "title", FieldType::VarChar(255), FieldConstraint::new().not_null(), - ValidationLevel::Strict, )?) .add_field(Field::new( "content", FieldType::Text, FieldConstraint::new().not_null(), - ValidationLevel::Strict, + )?) + .add_field(Field::new( + "is_editor", + FieldType::Boolean, + FieldConstraint::new() + .not_null() + .default(SafeValue::Bool(false)), + )?) + .add_field(Field::new( + "draft_content", + FieldType::Text, + FieldConstraint::new(), )?) .add_field(Field::new( "template", FieldType::VarChar(50), FieldConstraint::new(), - ValidationLevel::Strict, )?) .add_field(Field::new( "status", FieldType::VarChar(20), - FieldConstraint::new() - .not_null() - .check(content_state_check.clone()), - ValidationLevel::Strict, + FieldConstraint::new().not_null(), )?); schema.add_table(pages_table)?; - // posts 表 + // 文章表 let mut posts_table = Table::new(&format!("{}posts", db_prefix))?; posts_table .add_field(Field::new( "id", FieldType::Integer(true), FieldConstraint::new().primary(), - ValidationLevel::Strict, )?) .add_field(Field::new( "author_name", @@ -566,33 +495,26 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes .foreign_key(format!("{}users", db_prefix), "username".to_string()) .on_delete(ForeignKeyAction::Cascade) .on_update(ForeignKeyAction::Cascade), - ValidationLevel::Strict, )?) .add_field(Field::new( "cover_image", FieldType::VarChar(255), FieldConstraint::new(), - ValidationLevel::Strict, )?) .add_field(Field::new( "title", FieldType::VarChar(255), FieldConstraint::new(), - ValidationLevel::Strict, )?) .add_field(Field::new( "content", FieldType::Text, FieldConstraint::new().not_null(), - ValidationLevel::Strict, )?) .add_field(Field::new( "status", FieldType::VarChar(20), - FieldConstraint::new() - .not_null() - .check(content_state_check.clone()), - ValidationLevel::Strict, + FieldConstraint::new().not_null(), )?) .add_field(Field::new( "is_editor", @@ -600,13 +522,11 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes FieldConstraint::new() .not_null() .default(SafeValue::Bool(false)), - ValidationLevel::Strict, )?) .add_field(Field::new( "draft_content", FieldType::Text, FieldConstraint::new(), - ValidationLevel::Strict, )?) .add_field(Field::new( "created_at", @@ -615,7 +535,6 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes "CURRENT_TIMESTAMP".to_string(), ValidationLevel::Strict, )), - ValidationLevel::Strict, )?) .add_field(Field::new( "updated_at", @@ -624,13 +543,6 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes "CURRENT_TIMESTAMP".to_string(), ValidationLevel::Strict, )), - ValidationLevel::Strict, - )?) - .add_field(Field::new( - "published_at", - FieldType::Timestamp, - FieldConstraint::new(), - ValidationLevel::Strict, )?); schema.add_table(posts_table)?; @@ -642,7 +554,6 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes "id", FieldType::Integer(true), FieldConstraint::new().primary(), - ValidationLevel::Strict, )?) .add_field(Field::new( "author_id", @@ -652,43 +563,36 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes .foreign_key(format!("{}users", db_prefix), "username".to_string()) .on_delete(ForeignKeyAction::Cascade) .on_update(ForeignKeyAction::Cascade), - ValidationLevel::Strict, )?) .add_field(Field::new( "name", FieldType::VarChar(255), FieldConstraint::new().not_null(), - ValidationLevel::Strict, )?) .add_field(Field::new( "size_bytes", FieldType::BigInt, FieldConstraint::new().not_null(), - ValidationLevel::Strict, )?) .add_field(Field::new( "storage_path", FieldType::VarChar(255), FieldConstraint::new().not_null().unique(), - ValidationLevel::Strict, )?) .add_field(Field::new( "mime_type", FieldType::VarChar(50), FieldConstraint::new().not_null(), - ValidationLevel::Strict, )?) .add_field(Field::new( "category", FieldType::VarChar(50), FieldConstraint::new(), - ValidationLevel::Strict, )?) .add_field(Field::new( "description", FieldType::VarChar(255), FieldConstraint::new(), - ValidationLevel::Strict, )?) .add_field(Field::new( "created_at", @@ -697,172 +601,69 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes "CURRENT_TIMESTAMP".to_string(), ValidationLevel::Strict, )), - ValidationLevel::Strict, )?); schema.add_table(resources_table)?; - // 配置表 - let mut settings_table = Table::new(&format!("{}settings", db_prefix))?; - settings_table - .add_field(Field::new( - "name", - FieldType::VarChar(50), - FieldConstraint::new().primary(), - ValidationLevel::Strict, - )?) - .add_field(Field::new( - "data", - FieldType::Text, - FieldConstraint::new(), - ValidationLevel::Strict, - )?); - - schema.add_table(settings_table)?; - - // 元数据表 - let mut metadata_table = Table::new(&format!("{}metadata", db_prefix))?; - metadata_table - .add_field(Field::new( - "id", - FieldType::Integer(true), - FieldConstraint::new().primary(), - ValidationLevel::Strict, - )?) - .add_field(Field::new( - "target_type", - FieldType::VarChar(20), - FieldConstraint::new() - .not_null() - .check(target_type_check.clone()), - ValidationLevel::Strict, - )?) - .add_field(Field::new( - "target_id", - FieldType::Integer(false), - FieldConstraint::new().not_null(), - ValidationLevel::Strict, - )?) - .add_field(Field::new( - "meta_key", - FieldType::VarChar(50), - FieldConstraint::new().not_null(), - ValidationLevel::Strict, - )?) - .add_field(Field::new( - "meta_value", - FieldType::Text, - FieldConstraint::new(), - ValidationLevel::Strict, - )?); - - metadata_table.add_index(Index::new( - "fk_metadata_posts", - vec!["target_id".to_string()], - false, - )?); - - metadata_table.add_index(Index::new( - "fk_metadata_pages", - vec!["target_id".to_string()], - false, - )?); - - metadata_table.add_index(Index::new( - "idx_metadata_target", - vec!["target_type".to_string(), "target_id".to_string()], - false, - )?); - - schema.add_table(metadata_table)?; - // 自定义字段表 - let mut custom_fields_table = Table::new(&format!("{}custom_fields", db_prefix))?; - custom_fields_table + let mut fields_table = Table::new(&format!("{}fields", db_prefix))?; + fields_table .add_field(Field::new( "id", FieldType::Integer(true), FieldConstraint::new().primary(), - ValidationLevel::Strict, )?) .add_field(Field::new( "target_type", FieldType::VarChar(20), - FieldConstraint::new() - .not_null() - .check(target_type_check.clone()), - ValidationLevel::Strict, + FieldConstraint::new().not_null(), )?) .add_field(Field::new( "target_id", FieldType::Integer(false), FieldConstraint::new().not_null(), - ValidationLevel::Strict, + )?) + .add_field(Field::new( + "field_type", + FieldType::VarChar(50), + FieldConstraint::new().not_null(), )?) .add_field(Field::new( "field_key", FieldType::VarChar(50), FieldConstraint::new().not_null(), - ValidationLevel::Strict, )?) .add_field(Field::new( "field_value", FieldType::Text, FieldConstraint::new(), - ValidationLevel::Strict, - )?) - .add_field(Field::new( - "field_type", - FieldType::VarChar(20), - FieldConstraint::new().not_null(), - ValidationLevel::Strict, )?); - custom_fields_table.add_index(Index::new( - "idx_custom_fields_target", + fields_table.add_index(Index::new( + "idx_fields_target", vec!["target_type".to_string(), "target_id".to_string()], false, )?); - schema.add_table(custom_fields_table)?; + schema.add_table(fields_table)?; - // 在 generate_schema 函数中,删除原有的 tags_tables 和 categories_table - // 替换为新的 taxonomies 表 + // 分类—标签 表 let mut taxonomies_table = Table::new(&format!("{}taxonomies", db_prefix))?; taxonomies_table .add_field(Field::new( "name", FieldType::VarChar(50), FieldConstraint::new().primary(), - ValidationLevel::Strict, )?) .add_field(Field::new( "slug", FieldType::VarChar(50), FieldConstraint::new().not_null().unique(), - ValidationLevel::Strict, )?) .add_field(Field::new( "type", FieldType::VarChar(20), - FieldConstraint::new() - .not_null() - .check(WhereClause::Or(vec![ - WhereClause::Condition(Condition::new( - "type".to_string(), - Operator::Eq, - Some(SafeValue::Text("'tag'".to_string(), ValidationLevel::Raw)), - )?), - WhereClause::Condition(Condition::new( - "type".to_string(), - Operator::Eq, - Some(SafeValue::Text( - "'category'".to_string(), - ValidationLevel::Raw, - )), - )?), - ])), - ValidationLevel::Strict, + FieldConstraint::new().not_null(), )?) .add_field(Field::new( "parent_name", @@ -871,12 +672,11 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes .foreign_key(format!("{}taxonomies", db_prefix), "name".to_string()) .on_delete(ForeignKeyAction::SetNull) .on_update(ForeignKeyAction::Cascade), - ValidationLevel::Strict, )?); schema.add_table(taxonomies_table)?; - // 替换为新的 post_taxonomies 表 + // 分类—标签_文章 关系表 let mut post_taxonomies_table = Table::new(&format!("{}post_taxonomies", db_prefix))?; post_taxonomies_table .add_field(Field::new( @@ -887,22 +687,20 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes .foreign_key(format!("{}posts", db_prefix), "id".to_string()) .on_delete(ForeignKeyAction::Cascade) .on_update(ForeignKeyAction::Cascade), - ValidationLevel::Strict, )?) .add_field(Field::new( - "taxonomy_id", + "taxonomy_name", FieldType::VarChar(50), FieldConstraint::new() .not_null() .foreign_key(format!("{}taxonomies", db_prefix), "name".to_string()) .on_delete(ForeignKeyAction::Cascade) .on_update(ForeignKeyAction::Cascade), - ValidationLevel::Strict, )?); post_taxonomies_table.add_index(Index::new( "pk_post_taxonomies", - vec!["post_id".to_string(), "taxonomy_id".to_string()], + vec!["post_id".to_string(), "taxonomy_name".to_string()], true, )?); diff --git a/backend/src/storage/sql/sqllite.rs b/backend/src/storage/sql/sqllite.rs index e864233..8c306d2 100644 --- a/backend/src/storage/sql/sqllite.rs +++ b/backend/src/storage/sql/sqllite.rs @@ -17,7 +17,7 @@ pub struct Sqlite { #[async_trait] impl DatabaseTrait for Sqlite { - async fn connect(db_config: &config::SqlConfig, db: bool) -> CustomResult { + async fn connect(db_config: &config::SqlConfig, _db: bool) -> CustomResult { let db_file = env::current_dir()? .join("assets") .join("sqllite") @@ -111,7 +111,7 @@ impl DatabaseTrait for Sqlite { let pool = Self::connect(&db_config, false).await?.pool; pool.execute(grammar.as_str()).await?; - pool.close(); + pool.close().await; Ok(()) } diff --git a/frontend/app/dashboard/plugins.tsx b/frontend/app/dashboard/plugins.tsx deleted file mode 100644 index 0cfeacb..0000000 --- a/frontend/app/dashboard/plugins.tsx +++ /dev/null @@ -1,289 +0,0 @@ -import { Template } from "interface/template"; -import { - Container, - Heading, - Text, - Box, - Flex, - Card, - Button, - TextField, - DropdownMenu, - ScrollArea, - Dialog, - Tabs, - Switch, - IconButton, -} from "@radix-ui/themes"; -import { - PlusIcon, - MagnifyingGlassIcon, - DownloadIcon, - GearIcon, - CodeIcon, - Cross2Icon, - CheckIcon, - UpdateIcon, - TrashIcon, - ExclamationTriangleIcon, -} from "@radix-ui/react-icons"; -import { useState } from "react"; -import type { PluginConfig } from "interface/plugin"; - -// 模拟插件数据 -const mockPlugins: (PluginConfig & { - id: number; - preview?: string; - installed?: boolean; -})[] = [ - { - id: 1, - name: "comment-system", - displayName: "评论系统", - version: "1.0.0", - description: "支持多种评论系统集成,包括Disqus、Gitalk等", - author: "Admin", - enabled: true, - icon: "https://api.iconify.design/material-symbols:comment.svg", - preview: - "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=500&auto=format", - managePath: "/dashboard/plugins/comment-system", - installed: true, - configuration: { - system: { - title: "评论系统配置", - description: "配置评论系统参数", - data: { - provider: "gitalk", - clientId: "", - clientSecret: "", - }, - }, - }, - routes: new Set(), - }, - { - id: 2, - name: "image-optimization", - displayName: "图片优化", - version: "1.0.0", - description: "自动优化上传的图片,支持压缩、裁剪、水印等功能", - author: "ThirdParty", - enabled: false, - icon: "https://api.iconify.design/material-symbols:image.svg", - preview: - "https://images.unsplash.com/photo-1618005198919-d3d4b5a92ead?w=500&auto=format", - installed: true, - configuration: { - system: { - title: "图片优化配置", - description: "配置图片优化参数", - data: { - quality: 80, - maxWidth: 1920, - watermark: false, - }, - }, - }, - routes: new Set(), - }, -]; - -// 模拟市场插件数据 -interface MarketPlugin { - id: number; - name: string; - displayName: string; - version: string; - description: string; - author: string; - preview?: string; - downloads: number; - rating: number; -} - -const marketPlugins: MarketPlugin[] = [ - { - id: 4, - name: "image-optimization", - displayName: "图片优化", - version: "1.0.0", - description: "自动优化上传的图片,支持压缩、裁剪、水印等功能", - author: "ThirdParty", - preview: - "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=500&auto=format", - downloads: 1200, - rating: 4.5, - }, - { - id: 5, - name: "markdown-plus", - displayName: "Markdown增强", - version: "2.0.0", - description: "增强的Markdown编辑器,支持更多扩展语法和实时预览", - author: "ThirdParty", - preview: - "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=500&auto=format", - downloads: 3500, - rating: 4.8, - }, -]; - -export default new Template({}, ({ http, args }) => { - const [searchTerm, setSearchTerm] = useState(""); - const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); - const [isSettingsOpen, setIsSettingsOpen] = useState(false); - const [selectedPlugin, setSelectedPlugin] = useState< - (typeof mockPlugins)[0] | null - >(null); - - // 处理插件启用/禁用 - const handleTogglePlugin = (pluginId: number) => { - // 这里添加启用/禁用插件的逻辑 - console.log("Toggle plugin:", pluginId); - }; - - return ( - - {/* 页面标题和操作栏 */} - - - - 插件管理 - - - 共 {mockPlugins.length} 个插件 - - - - - - {/* 搜索栏 */} - - ) => - setSearchTerm(e.target.value) - } - > - - - - - - - {/* 插件列表 */} - - {mockPlugins.map((plugin) => ( - - {/* 插件预览图 */} - {plugin.preview && ( - - {plugin.displayName} - - )} - - {/* 插件信息 */} - - - {plugin.displayName} - handleTogglePlugin(plugin.id)} - /> - - - - 版本 {plugin.version} · 作者 {plugin.author} - - - - {plugin.description} - - - {/* 操作按钮 */} - - {plugin.managePath && plugin.enabled && ( - - )} - - - - - ))} - - - {/* 安装插件对话框 */} - - - 安装插件 - - 上传插件包进行安装 - - - - - { - console.log(e.target.files); - }} - /> - - - - - - - - - - - - - - - - ); -}); diff --git a/frontend/app/env.ts b/frontend/app/env.ts index 01d7e98..3b64e3e 100644 --- a/frontend/app/env.ts +++ b/frontend/app/env.ts @@ -5,7 +5,6 @@ export interface EnvConfig { VITE_API_BASE_URL: string; VITE_API_USERNAME: string; VITE_API_PASSWORD: string; - VITE_PATTERN: string; } export const DEFAULT_CONFIG: EnvConfig = { @@ -15,8 +14,7 @@ export const DEFAULT_CONFIG: EnvConfig = { VITE_API_BASE_URL: "http://127.0.0.1:22000", VITE_API_USERNAME: "", VITE_API_PASSWORD: "", - VITE_PATTERN: "true", -} as const; +}; // 扩展 ImportMeta 接口 declare global { diff --git a/frontend/app/root.tsx b/frontend/app/root.tsx index 31f8aec..3643049 100644 --- a/frontend/app/root.tsx +++ b/frontend/app/root.tsx @@ -6,8 +6,7 @@ import { ScrollRestoration, } from "@remix-run/react"; import { NotificationProvider } from "hooks/Notification"; -import { Theme } from "@radix-ui/themes"; -import { ThemeScript } from "hooks/themeMode"; +import { ThemeScript } from "hooks/ThemeMode"; import "~/index.css"; @@ -17,6 +16,10 @@ export function Layout() { + Echoes @@ -28,11 +31,9 @@ export function Layout() { suppressHydrationWarning={true} data-cz-shortcut-listen="false" > - - + - diff --git a/frontend/app/routes.tsx b/frontend/app/routes.tsx index cce248a..7908b29 100644 --- a/frontend/app/routes.tsx +++ b/frontend/app/routes.tsx @@ -1,27 +1,17 @@ import ErrorPage from "hooks/Error"; -import layout from "themes/echoes/layout"; -import article from "themes/echoes/posts"; -import about from "themes/echoes/about"; import { useLocation } from "react-router-dom"; import post from "themes/echoes/post"; -import { memo, useCallback } from "react"; -import login from "~/dashboard/login"; +import React, { memo, useCallback } from "react"; import adminLayout from "~/dashboard/layout"; import dashboard from "~/dashboard/index"; -import posts from "~/dashboard/posts"; import comments from "~/dashboard/comments"; -import categories from "./dashboard/categories"; -import settings from "./dashboard/settings"; -import files from "./dashboard/files"; -import themes from "./dashboard/themes"; +import categories from "~/dashboard/categories"; +import settings from "~/dashboard/settings"; +import files from "~/dashboard/files"; +import themes from "~/dashboard/themes"; import users from "~/dashboard/users"; -import plugins from "./dashboard/plugins"; +import layout from "~/dashboard/layout"; -const args = { - title: "我的页面", - theme: "dark", - nav: 'indexerroraboutpostlogindashboard', -} as const; // 创建布局渲染器的工厂函数 const createLayoutRenderer = (layoutComponent: any) => { @@ -33,22 +23,53 @@ const createLayoutRenderer = (layoutComponent: any) => { }; }; +// 创建组件的工厂函数 +const createComponentRenderer = (path: string) => { + return React.lazy(async () => { + const module = await import(/* @vite-ignore */ path); + return { + default: (props: any) => { + if (typeof module.default.render === "function") { + return module.default.render(props); + } + }, + }; + }); +}; + // 使用工厂函数创建不同的布局渲染器 const renderLayout = createLayoutRenderer(layout); const renderDashboardLayout = createLayoutRenderer(adminLayout); +const Login = createComponentRenderer("./dashboard/login"); +const posts = createComponentRenderer("themes/echoes/posts"); + const Routes = memo(() => { const location = useLocation(); const [mainPath, subPath] = location.pathname.split("/").filter(Boolean); // 使用 useCallback 缓存渲染函数 const renderContent = useCallback((Component: any) => { - return renderLayout(Component.render(args)); + if (React.isValidElement(Component)) { + return renderLayout(Component); + } + return renderLayout( + Loading...}> + {Component.render ? Component.render(args) : } + , + ); }, []); // 添加管理后台内容渲染函数 const renderDashboardContent = useCallback((Component: any) => { - return renderDashboardLayout(Component.render(args)); + if (React.isValidElement(Component)) { + return renderDashboardLayout(Component); + } + return renderDashboardLayout( + Loading...}> + {Component.render ? Component.render(args) : } + , + ); }, []); // 前台路由 @@ -59,8 +80,10 @@ const Routes = memo(() => { return renderContent(about); case "post": return renderContent(post); + case "posts": + return renderContent(posts); case "login": - return login.render(args); + return ; case "dashboard": // 管理后台路由 if (!subPath) { @@ -83,8 +106,6 @@ const Routes = memo(() => { return renderDashboardContent(themes); case "users": return renderDashboardContent(users); - case "plugins": - return renderDashboardContent(plugins); default: return renderDashboardContent(
404 未找到页面
); } diff --git a/frontend/core/capability.ts b/frontend/core/capability.ts deleted file mode 100644 index 3ecb228..0000000 --- a/frontend/core/capability.ts +++ /dev/null @@ -1,85 +0,0 @@ -export interface CapabilityProps { - name: string; - description?: string; - execute: (...args: any[]) => Promise; -} - -export class CapabilityService { - private capabilities: Map< - string, - Set<{ - source: string; - capability: CapabilityProps; - }> - > = new Map(); - - private static instance: CapabilityService; - - private constructor() {} - - public static getInstance(): CapabilityService { - if (!this.instance) { - this.instance = new CapabilityService(); - } - return this.instance; - } - - private register( - capabilityName: string, - source: string, - capability: CapabilityProps, - ) { - const handlers = this.capabilities.get(capabilityName) || new Set(); - handlers.add({ source, capability }); - } - - private executeCapabilityMethod( - capabilityName: string, - ...args: any[] - ): Set { - const results = new Set(); - const handlers = this.capabilities.get(capabilityName); - - if (handlers) { - handlers.forEach(({ capability }) => { - const methodFunction = capability["execute"]; - if (methodFunction) { - methodFunction(...args) - .then((data) => results.add(data as T)) - .catch((error) => - console.error(`Error executing method ${capabilityName}:`, error), - ); - } - }); - } - return results; - } - - private removeCapability(source: string) { - this.capabilities.forEach((capability_s, capabilityName) => { - const newHandlers = new Set( - Array.from(capability_s).filter( - (capability) => capability.source !== source, - ), - ); - this.capabilities.set(capabilityName, newHandlers); - }); - } - - private removeCapabilitys(capability: string) { - this.capabilities.delete(capability); - } - - public validateCapability(capability: CapabilityProps): boolean { - if (!capability.name || !capability.execute) { - return false; - } - - const namePattern = /^[a-z][a-zA-Z0-9_]*$/; - if (!namePattern.test(capability.name)) { - return false; - } - - return true; - } -} diff --git a/frontend/core/http.ts b/frontend/core/http.ts index a909eda..88f0aa3 100644 --- a/frontend/core/http.ts +++ b/frontend/core/http.ts @@ -2,11 +2,12 @@ import { DEFAULT_CONFIG } from "~/env"; export interface ErrorResponse { title: string; message: string; + detail?: string; } export class HttpClient { private static instance: HttpClient; - private timeout: number; + private readonly timeout: number; private constructor(timeout = 10000) { this.timeout = timeout; @@ -34,41 +35,83 @@ export class HttpClient { return { ...options, headers }; } - private async handleResponse(response: Response): Promise { + private async handleResponse(response: Response): Promise { if (!response.ok) { const contentType = response.headers.get("content-type"); - let message; + let errorDetail = { + status: response.status, + statusText: response.statusText, + message: "", + raw: "", + }; try { if (contentType?.includes("application/json")) { const error = await response.json(); - message = error.message || ""; + errorDetail.message = error.message || ""; + errorDetail.raw = JSON.stringify(error, null, 2); } else { const textError = await response.text(); - message = textError || ""; + errorDetail.message = textError; + errorDetail.raw = textError; } } catch (e) { - console.error("解析响应错误:", e); + console.error("[Response Parse Error]:", e); + errorDetail.message = "响应解析失败"; + errorDetail.raw = e instanceof Error ? e.message : String(e); } switch (response.status) { + case 400: + errorDetail.message = errorDetail.message || "请求参数错误"; + break; + case 401: + errorDetail.message = "未授权访问"; + break; + case 403: + errorDetail.message = "访问被禁止"; + break; case 404: - message = "请求的资源不存在"; + errorDetail.message = "请求的资源不存在"; + break; + case 500: + errorDetail.message = "服务器内部错误"; + break; + case 502: + errorDetail.message = "网关错误"; + break; + case 503: + errorDetail.message = "服务暂时不可用"; + break; + case 504: + errorDetail.message = "网关超时"; break; } const errorResponse: ErrorResponse = { - title: `${response.status} ${response.statusText}`, - message: message, + title: `${errorDetail.status} ${errorDetail.statusText}`, + message: errorDetail.message, + detail: `请求URL: ${response.url}\n状态码: ${errorDetail.status}\n原始错误: ${errorDetail.raw}`, }; + console.error("[HTTP Error]:", errorResponse); throw errorResponse; } - const contentType = response.headers.get("content-type"); - return contentType?.includes("application/json") - ? response.json() - : response.text(); + try { + const contentType = response.headers.get("content-type"); + if (contentType?.includes("application/json")) { + return await response.json(); + } + return (await response.text()) as T; + } catch (e) { + console.error("[Response Parse Error]:", e); + throw { + title: "响应解析错误", + message: "服务器返回的数据格式不正确", + detail: e instanceof Error ? e.message : String(e), + }; + } } private async request( @@ -94,22 +137,23 @@ export class HttpClient { return await this.handleResponse(response); } catch (error: any) { if (error.name === "AbortError") { - const errorResponse: ErrorResponse = { + throw { title: "请求超时", message: "服务器响应时间过长,请稍后重试", + detail: `请求URL: ${url}${endpoint}\n超时时间: ${this.timeout}ms`, }; - throw errorResponse; } + if ((error as ErrorResponse).title && (error as ErrorResponse).message) { throw error; } - console.log(error); - const errorResponse: ErrorResponse = { - title: "未知错误", + console.error("[Request Error]:", error); + throw { + title: "请求失败", message: error.message || "发生未知错误", + detail: `请求URL: ${url}${endpoint}\n错误详情: ${error.stack || error}`, }; - throw errorResponse; } finally { clearTimeout(timeoutId); } diff --git a/frontend/core/template.ts b/frontend/core/template.ts deleted file mode 100644 index 2b1f77b..0000000 --- a/frontend/core/template.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ReactNode } from "react"; // Import React -import { LoaderFunction } from "react-router-dom"; -import { Template } from "interface/template"; - -export class TemplateManager { - private static instance: TemplateManager; - private templates = new Map(); - - private constructor() {} - - public static getInstance(): TemplateManager { - if (!TemplateManager.instance) { - TemplateManager.instance = new TemplateManager(); - } - return TemplateManager.instance; - } - - // 读取主题和模板中的模板 -} diff --git a/frontend/core/theme.ts b/frontend/core/theme.ts new file mode 100644 index 0000000..444822b --- /dev/null +++ b/frontend/core/theme.ts @@ -0,0 +1,53 @@ +import React from "react"; +import { Configuration } from "interface/serializableType"; +import {ThemeConfig} from "interface/theme"; +import {HttpClient} from "core/http" +// 创建布局渲染器的工厂函数 + +const createLayoutRenderer = (layoutComponent: any, args: Configuration) => { + return (children: React.ReactNode) => { + return layoutComponent.render({ + children, + args, + }); + }; +}; + +// 创建组件的工厂函数 +const createComponentRenderer = (path: string) => { + return React.lazy(async () => { + const module = await import(/* @vite-ignore */ path); + return { + default: (props: any) => { + if (typeof module.default.render === "function") { + return module.default.render(props); + } + }, + }; + }); +}; + +export class TemplateManager { + private static instance: TemplateManager; + private routes = new Map(); + private layout: React.FC | undefined; + private error: React.FC | undefined; + private loading: React.FC | undefined; + private field : ThemeConfig; + + private constructor() { + const http=HttpClient.getInstance(); + http.systemToken() + } + + public static getInstance(): TemplateManager { + if (!TemplateManager.instance) { + TemplateManager.instance = new TemplateManager(); + } + return TemplateManager.instance; + } + + + + // 读取主题和模板中的模板 +} diff --git a/frontend/interface/layout.ts b/frontend/interface/layout.ts index de6062d..c3bbb13 100644 --- a/frontend/interface/layout.ts +++ b/frontend/interface/layout.ts @@ -1,17 +1,14 @@ import { HttpClient } from "core/http"; -import { CapabilityService } from "core/capability"; import { Serializable } from "interface/serializableType"; -import { createElement, memo } from "react"; +import React, { createElement, memo } from "react"; export class Layout { - private http: HttpClient; - private capability: CapabilityService; + private readonly http: HttpClient; private readonly MemoizedElement: React.MemoExoticComponent< (props: { children: React.ReactNode; args?: Serializable; - onTouchStart?: (e: TouchEvent) => void; - onTouchEnd?: (e: TouchEvent) => void; + http: HttpClient; }) => React.ReactNode >; @@ -19,29 +16,20 @@ export class Layout { public element: (props: { children: React.ReactNode; args?: Serializable; - onTouchStart?: (e: TouchEvent) => void; - onTouchEnd?: (e: TouchEvent) => void; + http: HttpClient; }) => React.ReactNode, services?: { http?: HttpClient; - capability?: CapabilityService; }, ) { this.http = services?.http || HttpClient.getInstance(); - this.capability = services?.capability || CapabilityService.getInstance(); this.MemoizedElement = memo(element); } - render(props: { - children: React.ReactNode; - args?: Serializable; - onTouchStart?: (e: TouchEvent) => void; - onTouchEnd?: (e: TouchEvent) => void; - }) { + render(props: { children: React.ReactNode; args?: Serializable }) { return createElement(this.MemoizedElement, { ...props, - onTouchStart: props.onTouchStart, - onTouchEnd: props.onTouchEnd, + http: this.http, }); } } diff --git a/frontend/interface/plugin.ts b/frontend/interface/plugin.ts deleted file mode 100644 index 035e62e..0000000 --- a/frontend/interface/plugin.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Configuration, PathDescription } from "interface/serializableType"; - -export interface PluginConfig { - name: string; - version: string; - displayName: string; - description?: string; - author?: string; - enabled: boolean; - icon?: string; - managePath?: string; - configuration?: Configuration; - routes: Set; -} diff --git a/frontend/interface/template.ts b/frontend/interface/template.ts index c58ddef..1bfb841 100644 --- a/frontend/interface/template.ts +++ b/frontend/interface/template.ts @@ -1,45 +1,26 @@ import { HttpClient } from "core/http"; -import { CapabilityService } from "core/capability"; import { Serializable } from "interface/serializableType"; -import { Layout } from "./layout"; +import React from "react"; export class Template { - private http: HttpClient; - private capability: CapabilityService; + private readonly http: HttpClient; constructor( - public config: { - layout?: Layout; - styles?: string[]; - scripts?: string[]; - description?: string; - }, public element: (props: { http: HttpClient; args: Serializable; }) => React.ReactNode, services?: { http?: HttpClient; - capability?: CapabilityService; }, ) { this.http = services?.http || HttpClient.getInstance(); - this.capability = services?.capability || CapabilityService.getInstance(); } render(args: Serializable) { - const content = this.element({ + return this.element({ http: this.http, args, }); - - if (this.config.layout) { - return this.config.layout.render({ - children: content, - args, - }); - } - - return content; } } diff --git a/frontend/interface/theme.ts b/frontend/interface/theme.ts index f41ae33..7036ad0 100644 --- a/frontend/interface/theme.ts +++ b/frontend/interface/theme.ts @@ -1,16 +1,17 @@ -import { Configuration, PathDescription } from "interface/serializableType"; +import {Configuration, PathDescription} from "interface/serializableType"; export interface ThemeConfig { - name: string; - displayName: string; - icon?: string; - version: string; - description?: string; - author?: string; - templates: Map; - layout?: string; - configuration: Configuration; - error?: string; - manage?: string; - routes: Map; + name: string; + displayName: string; + icon?: string; + version: string; + description?: string; + author?: string; + templates: Map; + layout?: string; + configuration: Configuration; + loading?: string; + error?: string; + manage?: string; + routes: Map; } diff --git a/frontend/package.json b/frontend/package.json index 255f540..59b76dc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,7 +31,6 @@ "isbot": "^4.1.0", "markdown-it": "^14.1.0", "markdown-it-toc-done-right": "^4.2.0", - "r": "^0.0.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-markdown": "^9.0.1", diff --git a/frontend/themes/echoes/about.tsx b/frontend/themes/echoes/about.tsx index 05b8ce3..43e9db3 100644 --- a/frontend/themes/echoes/about.tsx +++ b/frontend/themes/echoes/about.tsx @@ -41,7 +41,7 @@ const skills = [ { name: "Python", level: 70 }, ]; -export default new Template({}, ({ http, args }) => { +export default new Template(({}) => { const containerRef = useRef(null); const [isVisible, setIsVisible] = useState(false); diff --git a/frontend/themes/echoes/layout.tsx b/frontend/themes/echoes/layout.tsx index d4eef97..50fcdb1 100644 --- a/frontend/themes/echoes/layout.tsx +++ b/frontend/themes/echoes/layout.tsx @@ -210,11 +210,11 @@ export default new Layout(({ children, args }) => { >