From ba17778a8f45640b4b523dc2ad13ff166ff6297d Mon Sep 17 00:00:00 2001
From: lsy
Date: Wed, 18 Dec 2024 21:54:37 +0800
Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=EF=BC=9A=E5=90=88?=
=?UTF-8?q?=E5=B9=B6=E8=87=AA=E5=AE=9A=E4=B9=89=E5=AD=97=E6=AE=B5=E5=92=8C?=
=?UTF-8?q?=E8=AE=BE=E8=AE=A1=E5=AD=97=E6=AE=B5=EF=BC=8C=E6=89=80=E6=9C=89?=
=?UTF-8?q?=E9=99=90=E5=88=B6=E7=A7=BB=E5=88=B0=E5=BA=94=E7=94=A8=E5=B1=82?=
=?UTF-8?q?=E9=99=90=E5=88=B6=20=E5=90=8E=E7=AB=AF=EF=BC=9A=E5=88=A0?=
=?UTF-8?q?=E9=99=A4=E6=95=B0=E6=8D=AE=E5=BA=93=E6=9E=84=E5=BB=BA=E9=9C=80?=
=?UTF-8?q?=E8=A6=81=E7=AD=89=E7=BA=A7=EF=BC=8Cjwt=E6=B7=BB=E5=8A=A0rote?=
=?UTF-8?q?=E4=BF=A1=E6=81=AF=20=E5=89=8D=E7=AB=AF=EF=BC=9A=E4=BF=AE?=
=?UTF-8?q?=E5=A4=8Dpost=EF=BC=8C=E5=88=A0=E9=99=A4=E6=8F=92=E4=BB=B6?=
=?UTF-8?q?=E6=9C=BA=E5=88=B6=EF=BC=8C=E4=BC=98=E5=8C=96http?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
backend/src/api/auth/token.rs | 21 +-
backend/src/api/fields.rs | 71 ++++++
backend/src/api/mod.rs | 16 +-
backend/src/api/page.rs | 5 +
backend/src/api/post.rs | 1 +
backend/src/api/settings.rs | 116 ----------
backend/src/api/setup.rs | 73 ++++---
backend/src/api/users.rs | 29 ++-
backend/src/main.rs | 4 +-
backend/src/security/jwt.rs | 1 +
backend/src/storage/sql/mysql.rs | 8 +-
backend/src/storage/sql/schema.rs | 270 +++--------------------
backend/src/storage/sql/sqllite.rs | 4 +-
frontend/app/dashboard/plugins.tsx | 289 -------------------------
frontend/app/env.ts | 4 +-
frontend/app/root.tsx | 11 +-
frontend/app/routes.tsx | 63 ++++--
frontend/core/capability.ts | 85 --------
frontend/core/http.ts | 82 +++++--
frontend/core/template.ts | 19 --
frontend/core/theme.ts | 53 +++++
frontend/interface/layout.ts | 24 +-
frontend/interface/plugin.ts | 14 --
frontend/interface/template.ts | 25 +--
frontend/interface/theme.ts | 27 +--
frontend/package.json | 1 -
frontend/themes/echoes/about.tsx | 2 +-
frontend/themes/echoes/layout.tsx | 4 +-
frontend/themes/echoes/post.tsx | 63 ++----
frontend/themes/echoes/posts.tsx | 2 +-
frontend/themes/echoes/theme.config.ts | 22 +-
frontend/tsconfig.json | 5 +-
32 files changed, 429 insertions(+), 985 deletions(-)
create mode 100644 backend/src/api/fields.rs
create mode 100644 backend/src/api/page.rs
create mode 100644 backend/src/api/post.rs
delete mode 100644 backend/src/api/settings.rs
delete mode 100644 frontend/app/dashboard/plugins.tsx
delete mode 100644 frontend/core/capability.ts
delete mode 100644 frontend/core/template.ts
create mode 100644 frontend/core/theme.ts
delete mode 100644 frontend/interface/plugin.ts
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}
- 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 }) => {
>
),
ul: ({ children, ...props }: ComponentPropsWithoutRef<"ul">) => (
{
),
// 修改 summary 组件
- summary: ({
- node,
- ...props
- }: ComponentPropsWithoutRef<"summary"> & { node?: any }) => (
+ summary: (props: ComponentPropsWithoutRef<"summary">) => (
),
@@ -1148,7 +1117,7 @@ export default new Template({}, ({ http, args }) => {
>
);
- // 在组件顶部添加 useMemo 包静态内容
+ // 在组顶部添加 useMemo 包静态内容
const PostContent = useMemo(() => {
// 在渲染内容前重置 headingIds
if (headingIdsArrays[mockPost.id]) {
@@ -1180,7 +1149,7 @@ export default new Template({}, ({ http, args }) => {
);
- }, [mockPost.content, components, mockPost.id, headingIdsArrays]); // 添加必要的依赖
+ }, [components, headingIdsArrays]);
return (
{
+export default new Template(({}) => {
const articleData = useMemo(() => mockArticles, []);
const totalPages = 25; // 假设有25页
const currentPage = 1; // 当前页码
diff --git a/frontend/themes/echoes/theme.config.ts b/frontend/themes/echoes/theme.config.ts
index f64a577..4ec0cb7 100644
--- a/frontend/themes/echoes/theme.config.ts
+++ b/frontend/themes/echoes/theme.config.ts
@@ -9,19 +9,35 @@ const themeConfig: ThemeConfig = {
configuration: {
nav: {
title: "导航配置",
- data: '你好 不好',
+ data: 'indexerroraboutpostlogindashboard',
},
},
layout: "layout.tsx",
templates: new Map([
[
- "page",
+ "posts",
{
- path: "./templates/page",
+ path: "posts",
name: "文章列表模板",
description: "博客首页展示模板",
},
],
+ [
+ "post",
+ {
+ path: "post",
+ name: "文章详情模板",
+ description: "文章详情展示模板",
+ },
+ ],
+ [
+ "about",
+ {
+ path: "about",
+ name: "关于页面模板",
+ description: "关于页面展示模板",
+ },
+ ],
]),
routes: new Map([]),
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index 9d87dd3..f22cac7 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -5,7 +5,8 @@
"**/.server/**/*.ts",
"**/.server/**/*.tsx",
"**/.client/**/*.ts",
- "**/.client/**/*.tsx"
+ "**/.client/**/*.tsx",
+ "**/interface/**/*.ts"
],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
@@ -23,7 +24,7 @@
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
- "~/*": ["./app/*"]
+ "~/*": ["./app/*"],
},
// Vite takes care of building everything, not tsc.