From e54c6b67c0975877fd2746ab0c1af07855a22244 Mon Sep 17 00:00:00 2001 From: lsy Date: Fri, 20 Dec 2024 00:34:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=EF=BC=9A=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E6=A8=A1=E5=BC=8F=E5=8A=A8=E6=80=81=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=20=E5=90=8E=E7=AB=AF=EF=BC=9A=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E5=AD=97=E6=AE=B5restful=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/api/auth/token.rs | 17 +- backend/src/api/fields.rs | 325 +++++++++++++++++- backend/src/api/mod.rs | 4 +- backend/src/api/setup.rs | 34 +- backend/src/main.rs | 9 +- backend/src/storage/sql/schema.rs | 78 +++-- frontend/app/dashboard/index.tsx | 170 --------- frontend/app/env.ts | 2 - frontend/app/init.tsx | 222 ++++++------ frontend/app/routes.tsx | 127 +------ frontend/core/http.ts | 91 +++-- frontend/core/moulde.ts | 216 ++++++++++++ frontend/core/theme.ts | 53 --- .../echoes/utils => hooks}/colorScheme.ts | 0 frontend/hooks/error.tsx | 2 +- frontend/hooks/notification.tsx | 4 +- frontend/interface/api.ts | 79 +++++ frontend/interface/fields.ts | 116 ++----- frontend/interface/template.ts | 15 +- frontend/interface/theme.ts | 8 +- frontend/themes/echoes/layout.tsx | 9 +- frontend/themes/echoes/post.tsx | 23 +- frontend/themes/echoes/posts.tsx | 15 +- frontend/themes/echoes/theme.config.ts | 50 ++- frontend/vite.config.ts | 14 +- 25 files changed, 995 insertions(+), 688 deletions(-) delete mode 100644 frontend/app/dashboard/index.tsx create mode 100644 frontend/core/moulde.ts delete mode 100644 frontend/core/theme.ts rename frontend/{themes/echoes/utils => hooks}/colorScheme.ts (100%) create mode 100644 frontend/interface/api.ts diff --git a/backend/src/api/auth/token.rs b/backend/src/api/auth/token.rs index 6a08419..2f9284f 100644 --- a/backend/src/api/auth/token.rs +++ b/backend/src/api/auth/token.rs @@ -3,7 +3,7 @@ use crate::security; use crate::storage::sql::builder; use crate::AppState; use chrono::Duration; -use rocket::{http::Status, post, response::status, serde::json::Json, State}; +use rocket::{http::Status, post,get, response::status, serde::json::Json, State}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use crate::api::Role; @@ -53,8 +53,6 @@ pub async fn token_system( ), ])); - println!("db: {:?}", sql.get_type()); - let values = sql .get_db() .execute_query(&builder) @@ -80,3 +78,16 @@ pub async fn token_system( ) .into_app_result()?) } + + +#[get("/test")] +pub async fn test_token(state: &State>) -> AppResult { + Ok(security::jwt::generate_jwt( + security::jwt::CustomClaims { + name: "system".into(), + role: Role::Administrator.to_string(), + }, + Duration::days(999), + ) + .into_app_result()?) +} diff --git a/backend/src/api/fields.rs b/backend/src/api/fields.rs index d5a60f5..945a30a 100644 --- a/backend/src/api/fields.rs +++ b/backend/src/api/fields.rs @@ -1,9 +1,17 @@ -use crate::{ - common::error::{AppResult, CustomResult}, - storage::sql::{self, builder}, +use super::SystemToken; +use crate::common::error::{AppResult, AppResultInto, CustomErrorInto, CustomResult}; +use crate::storage::sql::{ + self, + builder::{self, Condition, Operator, SafeValue, SqlOperation, ValidationLevel, WhereClause}, }; -use builder::{SafeValue, SqlOperation, ValidationLevel}; +use crate::AppState; +use rocket::serde::json::Json; +use rocket::{delete, get, post, put, State}; +use serde_json::{from_str, json, to_value, Value}; use std::fmt::{Display, Formatter}; +use std::sync::Arc; + +#[derive(Clone)] pub enum TargetType { Post, Page, @@ -22,6 +30,19 @@ impl Display for TargetType { } } +impl TargetType { + pub fn from_str(s: &str) -> CustomResult { + match s.to_lowercase().as_str() { + "post" => Ok(TargetType::Post), + "page" => Ok(TargetType::Page), + "theme" => Ok(TargetType::Theme), + "system" => Ok(TargetType::System), + _ => Err("无效的目标类型".into_custom_error()), + } + } +} + +#[derive(Clone)] pub enum FieldType { Data, Meta, @@ -36,13 +57,23 @@ impl Display for FieldType { } } +impl FieldType { + pub fn from_str(s: &str) -> CustomResult { + match s.to_lowercase().as_str() { + "data" => Ok(FieldType::Data), + "meta" => Ok(FieldType::Meta), + _ => Err("无效的字段类型".into_custom_error()), + } + } +} + pub async fn insert_fields( sql: &sql::Database, target_type: TargetType, target_id: i64, field_type: FieldType, - field_key: String, - field_value: String, + field_key: &str, + field_value: &str, ) -> CustomResult<()> { let mut builder = builder::QueryBuilder::new( SqlOperation::Insert, @@ -60,12 +91,290 @@ pub async fn insert_fields( )?; builder.set_value( "field_key".to_string(), - SafeValue::Text(field_key, ValidationLevel::Raw), + SafeValue::Text(field_key.to_string(), ValidationLevel::Raw), )?; builder.set_value( "field_value".to_string(), - SafeValue::Text(field_value, ValidationLevel::Raw), + SafeValue::Text(field_value.to_string(), ValidationLevel::Raw), )?; sql.get_db().execute_query(&builder).await?; Ok(()) } + +pub async fn get_field( + sql: &sql::Database, + target_type: TargetType, + target_id: i64, +) -> CustomResult> { + let mut builder = builder::QueryBuilder::new( + SqlOperation::Select, + sql.table_name("fields"), + sql.get_type(), + )?; + builder + .add_field("field_type".to_string())? + .add_field("field_key".to_string())? + .add_field("field_value".to_string())? + .add_condition(WhereClause::And(vec![ + WhereClause::Condition(Condition::new( + "target_id".to_string(), + Operator::Eq, + Some(SafeValue::Integer(target_id)), + )?), + WhereClause::Condition(Condition::new( + "target_type".to_string(), + Operator::Eq, + Some(SafeValue::Text( + target_type.to_string(), + ValidationLevel::Standard, + )), + )?), + ])); + let values = sql.get_db().execute_query(&builder).await?; + + let processed_values = values + .into_iter() + .map(|mut row| { + if let Some(Value::String(field_value)) = row.get("field_value") { + if let Ok(json_value) = from_str::(&field_value) { + row.insert("field_value".to_string(), json_value); + } + } + row + }) + .collect::>(); + + let json_value = to_value(processed_values)?; + Ok(Json(json_value)) +} + +pub async fn delete_fields( + sql: &sql::Database, + target_type: TargetType, + target_id: i64, + field_type: FieldType, + field_key: &str, +) -> CustomResult<()> { + let mut builder = builder::QueryBuilder::new( + SqlOperation::Delete, + sql.table_name("fields"), + sql.get_type(), + )?; + builder.set_value("target_id".to_string(), SafeValue::Integer(target_id))?; + builder.set_value( + "target_type".to_string(), + SafeValue::Text(target_type.to_string(), ValidationLevel::Standard), + )?; + builder.set_value( + "field_type".to_string(), + SafeValue::Text(field_type.to_string(), ValidationLevel::Standard), + )?; + builder.set_value( + "field_key".to_string(), + SafeValue::Text(field_key.to_string(), ValidationLevel::Standard), + )?; + sql.get_db().execute_query(&builder).await?; + Ok(()) +} + +pub async fn delete_all_fields( + sql: &sql::Database, + target_type: TargetType, + target_id: i64, +) -> CustomResult<()> { + let mut builder = builder::QueryBuilder::new( + SqlOperation::Delete, + sql.table_name("fields"), + sql.get_type(), + )?; + builder.set_value("target_id".to_string(), SafeValue::Integer(target_id))?; + builder.set_value( + "target_type".to_string(), + SafeValue::Text(target_type.to_string(), ValidationLevel::Standard), + )?; + sql.get_db().execute_query(&builder).await?; + Ok(()) +} + +pub async fn update_field( + sql: &sql::Database, + target_type: TargetType, + target_id: i64, + field_type: FieldType, + field_key: &str, + field_value: &str, +) -> CustomResult<()> { + let mut builder = builder::QueryBuilder::new( + SqlOperation::Update, + sql.table_name("fields"), + sql.get_type(), + )?; + builder + .set_value( + "field_value".to_string(), + SafeValue::Text(field_value.to_string(), ValidationLevel::Raw), + )? + .add_condition(WhereClause::And(vec![ + WhereClause::Condition(Condition::new( + "target_type".to_string(), + Operator::Eq, + Some(SafeValue::Text( + target_type.to_string(), + ValidationLevel::Standard, + )), + )?), + WhereClause::Condition(Condition::new( + "target_id".to_string(), + Operator::Eq, + Some(SafeValue::Integer(target_id)), + )?), + WhereClause::Condition(Condition::new( + "field_type".to_string(), + Operator::Eq, + Some(SafeValue::Text( + field_type.to_string(), + ValidationLevel::Standard, + )), + )?), + WhereClause::Condition(Condition::new( + "field_key".to_string(), + Operator::Eq, + Some(SafeValue::Text( + field_key.to_string(), + ValidationLevel::Standard, + )), + )?), + ])); + sql.get_db().execute_query(&builder).await?; + Ok(()) +} + +#[get("//")] +pub async fn get_field_handler( + token: SystemToken, + state: &State>, + target_type: &str, + target_id: i64, +) -> AppResult> { + let sql = state.sql_get().await.into_app_result()?; + let target_type = TargetType::from_str(&target_type).into_app_result()?; + let values = get_field(&sql, target_type, target_id) + .await + .into_app_result()?; + Ok(values) +} + +#[post( + "////", + data = "", + format = "application/json" +)] +pub async fn insert_field_handler( + token: SystemToken, + state: &State>, + target_type: &str, + target_id: i64, + field_type: &str, + field_key: &str, + data: Json, +) -> AppResult { + let sql = state.sql_get().await.into_app_result()?; + let target_type = TargetType::from_str(&target_type).into_app_result()?; + let data_str = data.to_string(); + let field_type = FieldType::from_str(&field_type).into_app_result()?; + insert_fields( + &sql, + target_type.clone(), + target_id.clone(), + field_type.clone(), + field_key.clone(), + &data_str, + ) + .await + .into_app_result()?; + Ok(format!( + "操作:插入字段\n目标类型:{}\n目标ID:{}\n字段类型:{}\n字段名称:{}\n字段值:{}", + target_type, target_id, field_type, field_key, data_str + ) + .to_string()) +} + +#[delete("////")] +pub async fn delete_field_handler( + token: SystemToken, + state: &State>, + target_type: &str, + target_id: i64, + field_type: &str, + field_key: &str, +) -> AppResult { + let sql = state.sql_get().await.into_app_result()?; + let target_type = TargetType::from_str(&target_type).into_app_result()?; + let field_type = FieldType::from_str(&field_type).into_app_result()?; + delete_fields( + &sql, + target_type.clone(), + target_id.clone(), + field_type.clone(), + field_key.clone(), + ) + .await + .into_app_result()?; + Ok(format!( + "操作:删除单个字段\n目标类型:{}\n目标ID:{}\n字段类型:{}\n字段名称:{}\n", + target_type, target_id, field_type, field_key + ) + .to_string()) +} +#[delete("//")] +pub async fn delete_all_fields_handler( + token: SystemToken, + state: &State>, + target_type: &str, + target_id: i64, +) -> AppResult { + let sql = state.sql_get().await.into_app_result()?; + let target_type = TargetType::from_str(&target_type).into_app_result()?; + delete_all_fields(&sql, target_type.clone(), target_id.clone()) + .await + .into_app_result()?; + Ok(format!( + "操作:删除所有字段\n目标类型:{}\n目标ID:{}\n", + target_type, target_id + ) + .to_string()) +} +#[put( + "////", + data = "", + format = "application/json" +)] +pub async fn update_field_handler( + token: SystemToken, + state: &State>, + target_type: &str, + target_id: i64, + field_type: &str, + field_key: &str, + data: Json, +) -> AppResult { + let sql = state.sql_get().await.into_app_result()?; + let target_type = TargetType::from_str(&target_type).into_app_result()?; + let data_str = data.to_string(); + let field_type = FieldType::from_str(&field_type).into_app_result()?; + update_field( + &sql, + target_type.clone(), + target_id.clone(), + field_type.clone(), + field_key.clone(), + &data_str, + ) + .await + .into_app_result()?; + Ok(format!( + "操作:更新字段\n目标类型:{}\n目标ID:{}\n字段类型:{}\n字段名称:{}\n字段值:{}", + target_type, target_id, field_type, field_key, data_str + ) + .to_string()) +} diff --git a/backend/src/api/mod.rs b/backend/src/api/mod.rs index ffe60da..f948437 100644 --- a/backend/src/api/mod.rs +++ b/backend/src/api/mod.rs @@ -50,9 +50,9 @@ impl<'r> FromRequest<'r> for SystemToken { pub fn jwt_routes() -> Vec { - routes![auth::token::token_system] + routes![auth::token::token_system,auth::token::test_token] } pub fn fields_routes() -> Vec { - routes![] + routes![fields::get_field_handler,fields::insert_field_handler,fields::delete_field_handler,fields::delete_all_fields_handler,fields::update_field_handler] } diff --git a/backend/src/api/setup.rs b/backend/src/api/setup.rs index 07fd7db..c4c65e5 100644 --- a/backend/src/api/setup.rs +++ b/backend/src/api/setup.rs @@ -8,10 +8,34 @@ use crate::security; use crate::storage::sql; use crate::AppState; use chrono::Duration; -use rocket::{http::Status, post, response::status, serde::json::Json, State}; +use rocket::{http::Status,get, post, response::status, serde::json::Json, State}; use serde::{Deserialize, Serialize}; use serde_json::json; use std::sync::Arc; +use std::sync::OnceLock; + + +static STEP: OnceLock = OnceLock::new(); + + +#[get("/step")] +pub async fn get_step() -> String { + STEP.get_or_init(|| { + let config = config::Config::read().unwrap_or_else(|e| { + eprintln!("配置读取失败: {}", e); + config::Config::default() + }); + + if !config.init.sql { + 1 + } else if !config.init.administrator { + 2 + } else { + 3 + } + }).to_string() +} + #[post("/sql", format = "application/json", data = "")] pub async fn setup_sql( @@ -112,8 +136,8 @@ pub async fn setup_account( TargetType::System, 0, FieldType::Meta, - "keywords".to_string(), - "echoes,blog,个人博客".to_string(), + "keywords", + "echoes,blog,个人博客", ) .await .into_app_result()?; @@ -123,8 +147,8 @@ pub async fn setup_account( TargetType::System, 0, FieldType::Data, - "current_theme".to_string(), - "echoes".to_string(), + "current_theme", + "echoes", ) .await .into_app_result()?; diff --git a/backend/src/main.rs b/backend/src/main.rs index 2e73b3c..db37b38 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -6,7 +6,7 @@ mod storage; use crate::common::config; use common::error::{CustomErrorInto, CustomResult}; use rocket::http::Method; -use rocket::Shutdown; +use rocket::{Shutdown, routes}; use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors, CorsOptions}; use std::sync::Arc; use storage::sql; @@ -100,13 +100,16 @@ async fn main() -> CustomResult<()> { .manage(state.clone()) .attach(cors()); + rocket_builder = rocket_builder.mount("/", routes![api::setup::get_step]); + if !config.init.sql { - rocket_builder = rocket_builder.mount("/", rocket::routes![api::setup::setup_sql]); + rocket_builder = rocket_builder.mount("/", routes![api::setup::setup_sql]); } else if !config.init.administrator { - rocket_builder = rocket_builder.mount("/", rocket::routes![api::setup::setup_account]); + rocket_builder = rocket_builder.mount("/", routes![api::setup::setup_account]); } else { state.sql_link(&config.sql_config).await?; rocket_builder = rocket_builder.mount("/auth/token", api::jwt_routes()); + rocket_builder = rocket_builder.mount("/field", api::fields_routes()); } let rocket = rocket_builder.ignite().await?; diff --git a/backend/src/storage/sql/schema.rs b/backend/src/storage/sql/schema.rs index e9bfd2d..c5f455a 100644 --- a/backend/src/storage/sql/schema.rs +++ b/backend/src/storage/sql/schema.rs @@ -66,6 +66,7 @@ pub struct Table { pub name: Identifier, pub fields: Vec, pub indexes: Vec, + pub primary_keys: Vec, } #[derive(Debug, Clone)] @@ -251,16 +252,20 @@ impl Field { if self.constraints.is_unique { sql.push_str(" UNIQUE"); } - if self.constraints.is_primary { - match (db_type, &self.field_type) { - (DatabaseType::SQLite, FieldType::Integer(true)) => { + if self.constraints.is_primary && db_type == DatabaseType::SQLite { + match &self.field_type { + FieldType::Integer(true) => { sql.push_str(" PRIMARY KEY AUTOINCREMENT"); } + _ => {} + } + } else if self.constraints.is_primary { + match (db_type, &self.field_type) { (DatabaseType::MySQL, FieldType::Integer(true)) => { - sql.push_str(" PRIMARY KEY"); + sql.push_str(" PRIMARY KEY AUTO_INCREMENT"); } (DatabaseType::PostgreSQL, FieldType::Integer(true)) => { - sql.push_str(" PRIMARY KEY"); + sql.push_str(" PRIMARY KEY GENERATED ALWAYS AS IDENTITY"); } _ => sql.push_str(" PRIMARY KEY"), } @@ -294,10 +299,14 @@ impl Table { name: Identifier::new(name.to_string())?, fields: Vec::new(), indexes: Vec::new(), + primary_keys: Vec::new(), }) } pub fn add_field(&mut self, field: Field) -> &mut Self { + if field.constraints.is_primary { + self.primary_keys.push(field.name.as_str().to_string()); + } self.fields.push(field); self } @@ -308,15 +317,49 @@ impl Table { } pub fn to_sql(&self, db_type: DatabaseType) -> CustomResult { - let fields_sql: CustomResult> = + let mut fields_sql: CustomResult> = self.fields.iter().map(|f| f.to_sql(db_type)).collect(); let fields_sql = fields_sql?; - let mut sql = format!( - "CREATE TABLE {} (\n {}\n);", - self.name.as_str(), - fields_sql.join(",\n ") - ); + let mut sql = String::new(); + + match db_type { + DatabaseType::SQLite => { + if self.primary_keys.len() > 1 { + sql = format!( + "CREATE TABLE {} (\n {},\n CONSTRAINT pk_{} PRIMARY KEY ({}))", + self.name.as_str(), + fields_sql.join(",\n "), + self.name.as_str(), + self.primary_keys.join(", ") + ); + } else { + sql = format!( + "CREATE TABLE {} (\n {}\n)", + self.name.as_str(), + fields_sql.join(",\n ") + ); + } + } + _ => { + if self.primary_keys.len() > 1 { + sql = format!( + "CREATE TABLE {} (\n {},\n PRIMARY KEY ({}))", + self.name.as_str(), + fields_sql.join(",\n "), + self.primary_keys.join(", ") + ); + } else { + sql = format!( + "CREATE TABLE {} (\n {}\n)", + self.name.as_str(), + fields_sql.join(",\n ") + ); + } + } + } + + sql.push(';'); // 添加索引 for index in &self.indexes { @@ -608,30 +651,25 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes // 自定义字段表 let mut fields_table = Table::new(&format!("{}fields", db_prefix))?; fields_table - .add_field(Field::new( - "id", - FieldType::Integer(true), - FieldConstraint::new().primary(), - )?) .add_field(Field::new( "target_type", FieldType::VarChar(20), - FieldConstraint::new().not_null(), + FieldConstraint::new().not_null().primary(), )?) .add_field(Field::new( "target_id", FieldType::Integer(false), - FieldConstraint::new().not_null(), + FieldConstraint::new().not_null().primary(), )?) .add_field(Field::new( "field_type", FieldType::VarChar(50), - FieldConstraint::new().not_null(), + FieldConstraint::new().not_null().primary(), )?) .add_field(Field::new( "field_key", FieldType::VarChar(50), - FieldConstraint::new().not_null(), + FieldConstraint::new().not_null().primary(), )?) .add_field(Field::new( "field_value", diff --git a/frontend/app/dashboard/index.tsx b/frontend/app/dashboard/index.tsx deleted file mode 100644 index 2a25a63..0000000 --- a/frontend/app/dashboard/index.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import { Template } from "interface/template"; -import { Container, Heading, Text, Box, Flex, Card } from "@radix-ui/themes"; -import { - BarChartIcon, - ReaderIcon, - ChatBubbleIcon, - PersonIcon, - EyeOpenIcon, - HeartIcon, - RocketIcon, - LayersIcon, -} from "@radix-ui/react-icons"; -import { useMemo } from "react"; - -// 模拟统计数据 -const stats = [ - { - label: "文章总数", - value: "128", - icon: , - trend: "+12%", - color: "var(--accent-9)", - }, - { - label: "总访问量", - value: "25,438", - icon: , - trend: "+8.2%", - color: "var(--green-9)", - }, - { - label: "评论数", - value: "1,024", - icon: , - trend: "+5.4%", - color: "var(--blue-9)", - }, - { - label: "用户互动", - value: "3,842", - icon: , - trend: "+15.3%", - color: "var(--pink-9)", - }, -]; - -// 模拟最近文章数据 -const recentPosts = [ - { - title: "构建现代化的前端开发工作流", - views: 1234, - comments: 23, - likes: 89, - status: "published", - }, - { - title: "React 18 新特性详解", - views: 892, - comments: 15, - likes: 67, - status: "published", - }, - { - title: "TypeScript 高级特性指南", - views: 756, - comments: 12, - likes: 45, - status: "draft", - }, - { - title: "前端性能优化实践", - views: 645, - comments: 8, - likes: 34, - status: "published", - }, -]; - -export default new Template({}, ({ http, args }) => { - return ( - - {/* 页面标题 */} - - 仪表盘 - - - {/* 统计卡片 */} - - {stats.map((stat, index) => ( - - - - - {stat.label} - - - {stat.value} - - - {stat.trend} - - - - {stat.icon} - - - - ))} - - - {/* 最近文章列表 */} - - - 最近文章 - - - {recentPosts.map((post, index) => ( - - - - - {post.title} - - - - - - {post.views} - - - - - - {post.comments} - - - - - - {post.likes} - - - - - - {post.status === "published" ? "已发布" : "草稿"} - - - - ))} - - - - ); -}); diff --git a/frontend/app/env.ts b/frontend/app/env.ts index 3b64e3e..8d96a34 100644 --- a/frontend/app/env.ts +++ b/frontend/app/env.ts @@ -1,7 +1,6 @@ export interface EnvConfig { VITE_PORT: string; VITE_ADDRESS: string; - VITE_INIT_STATUS: string; VITE_API_BASE_URL: string; VITE_API_USERNAME: string; VITE_API_PASSWORD: string; @@ -10,7 +9,6 @@ export interface EnvConfig { export const DEFAULT_CONFIG: EnvConfig = { VITE_PORT: "22100", VITE_ADDRESS: "localhost", - VITE_INIT_STATUS: "0", VITE_API_BASE_URL: "http://127.0.0.1:22000", VITE_API_USERNAME: "", VITE_API_PASSWORD: "", diff --git a/frontend/app/init.tsx b/frontend/app/init.tsx index eb9ab1f..441d9d3 100644 --- a/frontend/app/init.tsx +++ b/frontend/app/init.tsx @@ -15,6 +15,7 @@ import { } from "@radix-ui/themes"; import { toast } from "hooks/Notification"; import { Echoes } from "hooks/Echoes"; +import { ModuleManager } from "core/moulde"; interface SetupContextType { currentStep: number; @@ -97,6 +98,45 @@ const Introduction: React.FC = ({ onNext }) => ( ); +// 新增工具函数 +const updateEnvConfig = async (newValues: Record) => { + const http = HttpClient.getInstance(); + // 获取所有 VITE_ 开头的环境变量 + let oldEnv = import.meta.env ?? DEFAULT_CONFIG; + const viteEnv = Object.entries(oldEnv).reduce( + (acc, [key, value]) => { + if (key.startsWith("VITE_")) { + acc[key] = value; + } + return acc; + }, + {} as Record, + ); + + const newEnv = { + ...viteEnv, + ...newValues, + }; + + await http.dev("/env", { + method: "POST", + body: JSON.stringify(newEnv), + }); + + Object.assign(import.meta.env, newEnv); +}; + +// 新增表单数据收集函数 +const getFormData = (fields: string[]) => { + return fields.reduce((acc, field) => { + const input = document.querySelector(`[name="${field}"]`) as HTMLInputElement; + if (input) { + acc[field] = input.value.trim(); + } + return acc; + }, {} as Record); +}; + const DatabaseConfig: React.FC = ({ onNext }) => { const [dbType, setDbType] = useState("postgresql"); const [loading, setLoading] = useState(false); @@ -167,66 +207,32 @@ const DatabaseConfig: React.FC = ({ onNext }) => { setLoading(true); try { + const formFields = getFormData([ + "db_host", + "db_prefix", + "db_port", + "db_user", + "db_password", + "db_name", + ]); + const formData = { db_type: dbType, - host: - ( - document.querySelector('[name="db_host"]') as HTMLInputElement - )?.value?.trim() ?? "", - db_prefix: - ( - document.querySelector('[name="db_prefix"]') as HTMLInputElement - )?.value?.trim() ?? "", - port: Number( - ( - document.querySelector('[name="db_port"]') as HTMLInputElement - )?.value?.trim() ?? 0, - ), - user: - ( - document.querySelector('[name="db_user"]') as HTMLInputElement - )?.value?.trim() ?? "", - password: - ( - document.querySelector('[name="db_password"]') as HTMLInputElement - )?.value?.trim() ?? "", - db_name: - ( - document.querySelector('[name="db_name"]') as HTMLInputElement - )?.value?.trim() ?? "", + host: formFields?.db_host ?? "localhost", + db_prefix: formFields?.db_prefix ?? "echoec_", + port:Number(formFields?.db_port?? 0), + user: formFields?.db_user ?? "", + password: formFields?.db_password ?? "", + db_name: formFields?.db_name ?? "", }; await http.post("/sql", formData); - let oldEnv = import.meta.env ?? DEFAULT_CONFIG; - const viteEnv = Object.entries(oldEnv).reduce( - (acc, [key, value]) => { - if (key.startsWith("VITE_")) { - acc[key] = value; - } - return acc; - }, - {} as Record, - ); - - const newEnv = { - ...viteEnv, - VITE_INIT_STATUS: "2", - }; - - await http.dev("/env", { - method: "POST", - body: JSON.stringify(newEnv), - }); - - Object.assign(import.meta.env, newEnv); - toast.success("数据库配置成功!"); - setTimeout(() => onNext(), 1000); } catch (error: any) { console.error(error); - toast.error(error.message, error.title); + toast.error(error.title, error.message); } finally { setLoading(false); } @@ -389,56 +395,40 @@ const AdminConfig: React.FC = ({ onNext }) => { const handleNext = async () => { setLoading(true); try { - const formData = { - username: ( - document.querySelector('[name="admin_username"]') as HTMLInputElement - )?.value, - password: ( - document.querySelector('[name="admin_password"]') as HTMLInputElement - )?.value, - email: ( - document.querySelector('[name="admin_email"]') as HTMLInputElement - )?.value, + const formData = getFormData([ + 'admin_username', + 'admin_password', + 'admin_email', + ]); + + // 添加非空验证 + if (!formData.admin_username || !formData.admin_password || !formData.admin_email) { + toast.error('请填写所有必填字段'); + return; + } + + // 调整数据格式以匹配后端期望的格式 + const requestData = { + username: formData.admin_username, + password: formData.admin_password, + email: formData.admin_email, }; - const response = (await http.post( - "/administrator", - formData, - )) as InstallReplyData; - const data = response; + const response = (await http.post("/administrator", requestData)) as InstallReplyData; + const { token, username, password } = response; - localStorage.setItem("token", data.token); + localStorage.setItem("token", token); - let oldEnv = import.meta.env ?? DEFAULT_CONFIG; - const viteEnv = Object.entries(oldEnv).reduce( - (acc, [key, value]) => { - if (key.startsWith("VITE_")) { - acc[key] = value; - } - return acc; - }, - {} as Record, - ); - - const newEnv = { - ...viteEnv, - VITE_INIT_STATUS: "3", - VITE_API_USERNAME: data.username, - VITE_API_PASSWORD: data.password, - }; - - await http.dev("/env", { - method: "POST", - body: JSON.stringify(newEnv), + await updateEnvConfig({ + VITE_API_USERNAME: username, + VITE_API_PASSWORD: password, }); - Object.assign(import.meta.env, newEnv); - toast.success("管理员账号创建成功!"); onNext(); } catch (error: any) { console.error(error); - toast.error(error.message, error.title); + toast.error(error.title, error.message); } finally { setLoading(false); } @@ -484,28 +474,35 @@ const SetupComplete: React.FC = () => { }; export default function SetupPage() { + const [moduleManager, setModuleManager] = useState(null); const [currentStep, setCurrentStep] = useState(1); - const [isClient, setIsClient] = useState(false); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { - // 标记客户端渲染完成 - setIsClient(true); + const initManager = async () => { + try { + const manager = await ModuleManager.getInstance(); + setModuleManager(manager); + // 确保初始步骤至少从1开始 + setCurrentStep(Math.max(manager.getStep() + 1, 1)); + } catch (error) { + console.error('Init manager error:', error); + } finally { + setIsLoading(false); + } + }; - // 获取初始化状态 - const initStatus = Number(import.meta.env.VITE_INIT_STATUS ?? 0); - - // 如果已完成初始化,直接刷新页面 - if (initStatus >= 3) { - window.location.reload(); - return; - } - - // 否则设置当前步骤 - setCurrentStep(initStatus + 1); + initManager(); }, []); - // 在服务端渲染时或客户端首次渲染时,返回加载状态 - if (!isClient) { + const handleStepChange = async (step: number) => { + if (moduleManager) { + await moduleManager.setStep(step - 1); + setCurrentStep(step); + } + }; + + if (isLoading) { return ( - + {currentStep === 1 && ( - setCurrentStep(currentStep + 1)} /> + handleStepChange(2)} /> )} {currentStep === 2 && ( - setCurrentStep(currentStep + 1)} - /> + handleStepChange(3)} /> )} {currentStep === 3 && ( - setCurrentStep(currentStep + 1)} /> + handleStepChange(4)} /> )} {currentStep === 4 && } diff --git a/frontend/app/routes.tsx b/frontend/app/routes.tsx index 7908b29..760653c 100644 --- a/frontend/app/routes.tsx +++ b/frontend/app/routes.tsx @@ -1,117 +1,28 @@ -import ErrorPage from "hooks/Error"; -import { useLocation } from "react-router-dom"; -import post from "themes/echoes/post"; -import React, { memo, useCallback } from "react"; -import adminLayout from "~/dashboard/layout"; -import dashboard from "~/dashboard/index"; -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 users from "~/dashboard/users"; -import layout from "~/dashboard/layout"; - - -// 创建布局渲染器的工厂函数 -const createLayoutRenderer = (layoutComponent: any) => { - 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); - } - }, - }; - }); -}; - -// 使用工厂函数创建不同的布局渲染器 -const renderLayout = createLayoutRenderer(layout); -const renderDashboardLayout = createLayoutRenderer(adminLayout); - -const Login = createComponentRenderer("./dashboard/login"); -const posts = createComponentRenderer("themes/echoes/posts"); +import React, { memo, useState, useEffect } from "react"; +import { ModuleManager } from "core/moulde"; +import SetupPage from "app/init"; const Routes = memo(() => { - const location = useLocation(); - const [mainPath, subPath] = location.pathname.split("/").filter(Boolean); + const [manager, setManager] = useState(null); - // 使用 useCallback 缓存渲染函数 - const renderContent = useCallback((Component: any) => { - if (React.isValidElement(Component)) { - return renderLayout(Component); - } - return renderLayout( - Loading...}> - {Component.render ? Component.render(args) : } - , - ); + useEffect(() => { + ModuleManager.getInstance().then(instance => { + setManager(instance); + }); }, []); - // 添加管理后台内容渲染函数 - const renderDashboardContent = useCallback((Component: any) => { - if (React.isValidElement(Component)) { - return renderDashboardLayout(Component); - } - return renderDashboardLayout( - Loading...}> - {Component.render ? Component.render(args) : } - , - ); - }, []); - - // 前台路由 - switch (mainPath) { - case "error": - return renderContent(ErrorPage); - case "about": - return renderContent(about); - case "post": - return renderContent(post); - case "posts": - return renderContent(posts); - case "login": - return ; - case "dashboard": - // 管理后台路由 - if (!subPath) { - return renderDashboardContent(dashboard); - } - - // 根据子路径返回对应的管理页面 - switch (subPath) { - case "posts": - return renderDashboardContent(posts); - case "comments": - return renderDashboardContent(comments); - case "categories": - return renderDashboardContent(categories); - case "files": - return renderDashboardContent(files); - case "settings": - return renderDashboardContent(settings); - case "themes": - return renderDashboardContent(themes); - case "users": - return renderDashboardContent(users); - default: - return renderDashboardContent(
404 未找到页面
); - } - default: - return renderContent(article); + if (!manager?.isInitialized()) { + return null; } + + const step = manager.getStep(); + + if (step < 3) { + return ; + } + + const currentPath = window.location.pathname; + return manager.getPage(currentPath); }); export default Routes; diff --git a/frontend/core/http.ts b/frontend/core/http.ts index 88f0aa3..087ccb5 100644 --- a/frontend/core/http.ts +++ b/frontend/core/http.ts @@ -2,7 +2,12 @@ import { DEFAULT_CONFIG } from "~/env"; export interface ErrorResponse { title: string; message: string; - detail?: string; + detail: { + url: string; + status: number; + statusText: string; + raw: string; + }; } export class HttpClient { @@ -27,9 +32,11 @@ export class HttpClient { headers.set("Content-Type", "application/json"); } - const token = localStorage.getItem("token"); - if (token) { - headers.set("Authorization", `Bearer ${token}`); + if (typeof window !== 'undefined' && !headers.has("Authorization")) { + const token = localStorage.getItem("token"); + if (token) { + headers.set("Authorization", `Bearer ${token}`); + } } return { ...options, headers }; @@ -52,7 +59,19 @@ export class HttpClient { errorDetail.raw = JSON.stringify(error, null, 2); } else { const textError = await response.text(); - errorDetail.message = textError; + if (textError.includes("")) { + const titleMatch = textError.match(/(.*?)<\/title>/); + const bodyMatch = textError.match(/<p>(.*?)<\/p>/); + + errorDetail.message = bodyMatch?.[1] || ""; + if (titleMatch?.[1]) { + const titleText = titleMatch[1]; + const statusCodeRemoved = titleText.replace(/^\d+:?\s*/, ''); + errorDetail.statusText = statusCodeRemoved.trim(); + } + } else { + errorDetail.message = textError; + } errorDetail.raw = textError; } } catch (e) { @@ -61,37 +80,47 @@ export class HttpClient { 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: - errorDetail.message = "请求的资源不存在"; - break; - case 500: - errorDetail.message = "服务器内部错误"; - break; - case 502: - errorDetail.message = "网关错误"; - break; - case 503: - errorDetail.message = "服务暂时不可用"; - break; - case 504: - errorDetail.message = "网关超时"; - break; + if (!errorDetail.message) { + switch (response.status) { + case 400: + errorDetail.message = "请求参数错误"; + break; + case 401: + errorDetail.message = "未授权访问"; + break; + case 403: + errorDetail.message = "访问被禁止"; + break; + case 404: + errorDetail.message = "请求的资源不存在"; + break; + case 422: + 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: `${errorDetail.status} ${errorDetail.statusText}`, message: errorDetail.message, - detail: `请求URL: ${response.url}\n状态码: ${errorDetail.status}\n原始错误: ${errorDetail.raw}`, + detail: { + url: response.url, + status: errorDetail.status, + statusText: errorDetail.statusText, + raw: errorDetail.raw + } }; console.error("[HTTP Error]:", errorResponse); diff --git a/frontend/core/moulde.ts b/frontend/core/moulde.ts new file mode 100644 index 0000000..bcd56ca --- /dev/null +++ b/frontend/core/moulde.ts @@ -0,0 +1,216 @@ +import React from "react"; +import { Configuration } from "interface/serializableType"; +import { ThemeConfig } from "interface/theme"; +import { HttpClient } from "core/http" +import { hashString } from "hooks/colorScheme" +import ErrorPage from "hooks/Error"; + +import { Field, FieldType, FindField, deserializeFields } from "interface/fields"; + +import { Template } from "interface/template"; + + + +// 创建布局渲染器的工厂函数 +export const createLayoutRenderer = (layoutComponent: any, args: Configuration) => { + const LayoutComponent = React.memo((props: { children: React.ReactNode }) => { + console.log('LayoutComponent props:', props); + console.log('layoutComponent:', layoutComponent); + console.log('args:', args); + + if (typeof layoutComponent.render === 'function') { + const rendered = layoutComponent.render({ + children: props.children, + args: args || {}, + }); + console.log('Rendered result:', rendered); + return rendered; + } + return React.createElement(layoutComponent, { + children: props.children, + args: args || {}, + }); + }); + LayoutComponent.displayName = 'LayoutRenderer'; + return (children: React.ReactNode) => React.createElement(LayoutComponent, { children }); +}; + +// 创建组件的工厂函数 +export const createComponentRenderer = (path: string) => { + return React.lazy(async () => { + try { + const normalizedPath = path.startsWith('../') ? path : `../${path}`; + const module = await import(/* @vite-ignore */ normalizedPath); + if (module.default instanceof Template) { + const Component = React.memo((props: any) => { + const renderProps = { + ...props, + args: props.args || {}, + http: props.http || HttpClient.getInstance() + }; + return module.default.render(renderProps); + }); + Component.displayName = `LazyComponent(${path})`; + return { default: Component }; + } + throw new Error(`模块 ${path} 不是一个有效的模板组件`); + } catch (error) { + console.error(`加载组件失败: ${path}`, error); + throw error; + } + }); +}; + +export class ModuleManager { + private static instance: ModuleManager; + private layout: ((children: React.ReactNode) => any) | undefined; + private error: React.FC | undefined; + private loading: React.FC | undefined; + private field: Array<Field> | undefined; + private theme: ThemeConfig | undefined; + private step!: number; + private initialized: boolean = false; + + private constructor() { + this.step = 0; + } + + public getStep(): number { + return this.step; + } + + public isInitialized(): boolean { + return this.initialized; + } + + public async setStep(step: number): Promise<void> { + this.step = step; + + // 如果是最后一步,重新初始化以加载完整配置 + if (step >= 3) { + await this.init(); + } + } + + private async init() { + try { + const http = HttpClient.getInstance(); + const step = await http.get("/step"); + this.step = Number(step) || 0; + + if (this.step >= 3) { + const token = await http.systemToken<string>(); + const headers = { + Authorization: `Bearer ${token}` + }; + + const field = await http.get("/field/system/0", { headers }) as Array<Field> | undefined; + + if (!field) { + throw new Error("获取系统自定义字段失败"); + } + + this.field = deserializeFields(field); + + if (!this.field[0].field_type) { + console.error('field_type is undefined in field:', this.field); + this.step = 0; + this.initialized = true; + return; + } + + const themeName = FindField(this.field, "current_theme", FieldType.data)?.field_value as string; + const themeId = hashString(themeName) + let rawThemeFields = await http.get(`/field/theme/${themeId}`, { headers }) as Array<Field>; + let themeFields = deserializeFields(rawThemeFields); + let themeConfig = FindField(themeFields, "config", FieldType.data)?.field_value as ThemeConfig; + if (!themeConfig) { + const themeModule = await import(/* @vite-ignore */ `../themes/${themeName}/theme.config.ts`); + themeConfig = themeModule.default; + await http.post(`/field/theme/${themeId}/data/config`, JSON.stringify(themeConfig), { headers }); + } + + this.theme = themeConfig; + + try { + if (this.theme.error) { + const normalizedPath = `../themes/${this.theme}/${this.theme.error}`; + const ErrorModule = createComponentRenderer(normalizedPath); + this.error = () => React.createElement(React.Suspense, { fallback: null }, React.createElement(ErrorModule)); + } + } finally { + if (!this.theme.error) { + this.error = () => ErrorPage.render({}); + } + } + try{ + if (this.theme.layout) { + const normalizedPath = `../themes/${themeName}/${this.theme.layout}`; + const layoutModule = await import(/* @vite-ignore */ normalizedPath); + this.layout = createLayoutRenderer(layoutModule.default, this.theme.configuration); + } + } catch (error) { + console.error('Failed to load layout:', error); + this.layout = undefined; + } + } + + this.initialized = true; + } catch (error) { + console.error('Init error:', error); + this.step = 0; // 出错时重置步骤 + this.initialized = true; + } + } + + public static async getInstance(): Promise<ModuleManager> { + if (!ModuleManager.instance) { + ModuleManager.instance = new ModuleManager(); + await ModuleManager.instance.init(); + } + return ModuleManager.instance; + } + + public getPage(path: string): React.ReactNode { + if (!this.theme?.routes) { + console.error('Theme or routes not initialized'); + return this.error ? React.createElement(this.error) : null; + } + const temple_path = this.theme.routes[path]; + console.log('temple_path:', temple_path); + console.log('theme configuration:', this.theme.configuration); + + try { + if (temple_path) { + const TemplateComponent = createComponentRenderer(/* @vite-ignore */ `../themes/${this.theme.name}/${temple_path}`); + console.log('TemplateComponent:', TemplateComponent); + + const Component = React.createElement( + React.Suspense, + { fallback: null }, + React.createElement(TemplateComponent, { + args: this.theme?.configuration || {}, + http: HttpClient.getInstance() + }) + ); + console.log('Created Component:', Component); + + if (this.layout) { + const layoutResult = this.layout(Component); + console.log('Layout result:', layoutResult); + return layoutResult; + } + return Component; + } + } catch (error) { + console.error('Failed to render page:', error); + } + + const error = this.error ? React.createElement(this.error) : null; + if (this.layout) { + return this.layout(error); + } + return error; + } + +} diff --git a/frontend/core/theme.ts b/frontend/core/theme.ts deleted file mode 100644 index 444822b..0000000 --- a/frontend/core/theme.ts +++ /dev/null @@ -1,53 +0,0 @@ -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<string, string>(); - 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/themes/echoes/utils/colorScheme.ts b/frontend/hooks/colorScheme.ts similarity index 100% rename from frontend/themes/echoes/utils/colorScheme.ts rename to frontend/hooks/colorScheme.ts diff --git a/frontend/hooks/error.tsx b/frontend/hooks/error.tsx index fad69d9..c7e7a9a 100644 --- a/frontend/hooks/error.tsx +++ b/frontend/hooks/error.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import { Template } from "interface/template"; -export default new Template({}, ({ args }) => { +export default new Template( ({ }) => { const [text, setText] = useState(""); const fullText = "404 - 页面不见了 :("; const typingSpeed = 100; diff --git a/frontend/hooks/notification.tsx b/frontend/hooks/notification.tsx index d5e39b1..86907a1 100644 --- a/frontend/hooks/notification.tsx +++ b/frontend/hooks/notification.tsx @@ -134,7 +134,7 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ ✕ </Button> <Flex direction="column" gap="1.5" className="pr-6"> - <Flex align="center" gap="2"> + <Flex className="items-center space-x-3"> <span className="flex items-center justify-center"> {notificationConfigs[notification.type].icon} </span> @@ -142,7 +142,7 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ <Text weight="bold" size="2" - className="text-white leading-tight" + className="text-white leading-tight truncate max-w-[250px]" > {notification.title} </Text> diff --git a/frontend/interface/api.ts b/frontend/interface/api.ts new file mode 100644 index 0000000..d73fece --- /dev/null +++ b/frontend/interface/api.ts @@ -0,0 +1,79 @@ +// 定义数据库表的字段接口 + +export interface User { + username: string; + avatarUrl?: string; + email: string; + passwordHash: string; + role: string; + createdAt: Date; + updatedAt: Date; +} + +export interface Page { + id: number; + title: string; + content: string; + isEditor: boolean; + draftContent?: string; + template?: string; + status: string; +} + +export interface Post { + id: number; + authorName: string; + coverImage?: string; + title?: string; + content: string; + status: string; + isEditor: boolean; + draftContent?: string; + createdAt: Date; + updatedAt: Date; +} + +export interface Resource { + id: number; + authorId: string; + name: string; + sizeBytes: number; + storagePath: string; + mimeType: string; + category?: string; + description?: string; + createdAt: Date; +} + +export interface CustomField { + targetType: "post" | "page"; + targetId: number; + fieldType: string; + fieldKey: string; + fieldValue?: string; +} + +export interface Taxonomy { + name: string; + slug: string; + type: "tag" | "category"; + parentName?: string; +} + +export interface PostTaxonomy { + postId: number; + taxonomyName: string; +} + +// 用于前端展示的扩展接口 +export interface PostDisplay extends Post { + taxonomies?: { + categories: Taxonomy[]; + tags: Taxonomy[]; + }; + customFields?: CustomField[]; +} + +export interface PageDisplay extends Page { + customFields?: CustomField[]; +} diff --git a/frontend/interface/fields.ts b/frontend/interface/fields.ts index 581ee97..4dd29c5 100644 --- a/frontend/interface/fields.ts +++ b/frontend/interface/fields.ts @@ -1,95 +1,39 @@ -// 定义数据库表的字段接口 - -export interface User { - username: string; - avatarUrl?: string; - email: string; - passwordHash: string; - role: string; - createdAt: Date; - updatedAt: Date; - lastLoginAt: Date; +export enum FieldType { + meta = "meta", + data = "data", } -export interface Page { - id: number; - title: string; - content: string; - template?: string; - status: string; +export interface Field { + field_key: string; + field_type: FieldType; + field_value: any; } -export interface Post { - id: number; - authorName: string; - coverImage?: string; - title?: string; - content: string; - status: string; - isEditor: boolean; - draftContent?: string; - createdAt: Date; - updatedAt: Date; - publishedAt?: Date; +export function FindField(fields: Array<Field>, field_key: string, field_type: FieldType) { + return fields.find(field => field.field_key === field_key && field.field_type === field_type); } -export interface Resource { - id: number; - authorId: string; - name: string; - sizeBytes: number; - storagePath: string; - mimeType: string; - category?: string; - description?: string; - createdAt: Date; +export function deserializeFields(rawFields: any[]): Field[] { + return rawFields.map(field => { + let parsedValue = field.field_value; + + // 如果是字符串,尝试解析 + if (typeof field.field_value === 'string') { + try { + // 先尝试解析为 JSON + parsedValue = JSON.parse(field.field_value); + } catch { + // 如果解析失败,保持原始字符串 + parsedValue = field.field_value; + } + } + + return { + field_key: field.field_key, + field_type: field.field_type as FieldType, + field_value: parsedValue + }; + }); } -export interface Setting { - name: string; - data?: string; -} -export interface Metadata { - id: number; - targetType: "post" | "page"; - targetId: number; - metaKey: string; - metaValue?: string; -} - -export interface CustomField { - id: number; - targetType: "post" | "page"; - targetId: number; - fieldKey: string; - fieldValue?: string; - fieldType: string; -} - -export interface Taxonomy { - name: string; - slug: string; - type: "tag" | "category"; - parentId?: string; -} - -export interface PostTaxonomy { - postId: number; - taxonomyId: string; -} - -// 用于前端展示的扩展接口 -export interface PostDisplay extends Post { - taxonomies?: { - categories: Taxonomy[]; - tags: Taxonomy[]; - }; - metadata?: Metadata[]; - customFields?: CustomField[]; -} - -export interface PageDisplay extends Page { - metadata?: Metadata[]; - customFields?: CustomField[]; -} diff --git a/frontend/interface/template.ts b/frontend/interface/template.ts index 1bfb841..6f260d9 100644 --- a/frontend/interface/template.ts +++ b/frontend/interface/template.ts @@ -1,24 +1,25 @@ import { HttpClient } from "core/http"; import { Serializable } from "interface/serializableType"; -import React from "react"; +import React, { memo } from "react"; export class Template { private readonly http: HttpClient; + private readonly MemoizedElement: React.MemoExoticComponent< + (props: { http: HttpClient; args: Serializable }) => React.ReactNode + >; constructor( - public element: (props: { - http: HttpClient; - args: Serializable; - }) => React.ReactNode, + element: (props: { http: HttpClient; args: Serializable }) => React.ReactNode, services?: { http?: HttpClient; - }, + } ) { this.http = services?.http || HttpClient.getInstance(); + this.MemoizedElement = memo(element); } render(args: Serializable) { - return this.element({ + return React.createElement(this.MemoizedElement, { http: this.http, args, }); diff --git a/frontend/interface/theme.ts b/frontend/interface/theme.ts index 7036ad0..6530465 100644 --- a/frontend/interface/theme.ts +++ b/frontend/interface/theme.ts @@ -7,11 +7,15 @@ export interface ThemeConfig { version: string; description?: string; author?: string; - templates: Map<string, PathDescription>; + templates: { + [key: string]: PathDescription; + }; layout?: string; configuration: Configuration; loading?: string; error?: string; manage?: string; - routes: Map<string, string>; + routes: { + [path: string]: string; + }; } diff --git a/frontend/themes/echoes/layout.tsx b/frontend/themes/echoes/layout.tsx index 50fcdb1..ba6de8a 100644 --- a/frontend/themes/echoes/layout.tsx +++ b/frontend/themes/echoes/layout.tsx @@ -73,8 +73,13 @@ export default new Layout(({ children, args }) => { }, [handleScroll]); const navString = - typeof args === "object" && args && "nav" in args - ? (args.nav as string) + typeof args === "object" && + args && + "nav" in args && + typeof args.nav === "object" && + args.nav && + "content" in args.nav + ? String(args.nav.content) : ""; // 添加回到顶部的处理函数 diff --git a/frontend/themes/echoes/post.tsx b/frontend/themes/echoes/post.tsx index afa2637..12d5963 100644 --- a/frontend/themes/echoes/post.tsx +++ b/frontend/themes/echoes/post.tsx @@ -20,8 +20,8 @@ import { ScrollArea, } from "@radix-ui/themes"; import { CalendarIcon, CodeIcon } from "@radix-ui/react-icons"; -import type { PostDisplay } from "interface/fields"; -import { getColorScheme } from "themes/echoes/utils/colorScheme"; +import type { PostDisplay } from "interface/api"; +import { getColorScheme } from "hooks/colorScheme"; import MarkdownIt from "markdown-it"; import remarkGfm from "remark-gfm"; import { toast } from "hooks/Notification"; @@ -377,7 +377,6 @@ function greet(user: User): string { > 💡 **提示**:部分高级排版功能可能需要特定的 Markdown 编辑器或渲染支持,请确认是否支持这些功能。 `, authorName: "Markdown 专家", - publishedAt: new Date("2024-03-15"), coverImage: "https://images.unsplash.com/photo-1499951360447-b19be8fe80f5?w=1200&h=600", status: "published", @@ -398,22 +397,6 @@ function greet(user: User): string { { name: "写作", slug: "writing", type: "tag" }, ], }, - metadata: [ - { - id: 1, - targetType: "post", - targetId: 1, - metaKey: "description", - metaValue: "从基础语法到高级排版,全面了解 Markdown 的各种用法和技巧。", - }, - { - id: 2, - targetType: "post", - targetId: 1, - metaKey: "keywords", - metaValue: "Markdown,基础语法,高级排版,布局设计", - }, - ], }; // 添加复制能的接口 @@ -1191,7 +1174,7 @@ export default new Template(({}) => { <Flex align="center" gap="2"> <CalendarIcon className="w-3.5 h-3.5" /> <Text size="2"> - {mockPost.publishedAt?.toLocaleDateString("zh-CN", { + {mockPost.createdAt?.toLocaleDateString("zh-CN", { year: "numeric", month: "long", day: "numeric", diff --git a/frontend/themes/echoes/posts.tsx b/frontend/themes/echoes/posts.tsx index 46343e6..d6d2d19 100644 --- a/frontend/themes/echoes/posts.tsx +++ b/frontend/themes/echoes/posts.tsx @@ -13,11 +13,11 @@ import { ChevronLeftIcon, ChevronRightIcon, } from "@radix-ui/react-icons"; -import { PostDisplay } from "interface/fields"; +import { PostDisplay } from "interface/api"; import { useMemo } from "react"; import { ImageLoader } from "hooks/ParticleImage"; -import { getColorScheme, hashString } from "themes/echoes/utils/colorScheme"; +import { getColorScheme, hashString } from "hooks/colorScheme"; // 修改模拟文章列表数据 const mockArticles: PostDisplay[] = [ @@ -26,7 +26,6 @@ const mockArticles: PostDisplay[] = [ title: "构建现代化的前端开发工作流", content: "在现代前端开发中,一个高效的工作流程对于提高开发效率至关重要...", authorName: "张三", - publishedAt: new Date("2024-03-15"), coverImage: "https://www.helloimg.com/i/2024/12/11/6759312352499.png", status: "published", isEditor: false, @@ -46,7 +45,6 @@ const mockArticles: PostDisplay[] = [ content: "React 18 带来了许多令人兴奋的新特性,包括并发渲染、自动批处理更新...", authorName: "李四", - publishedAt: new Date("2024-03-14"), coverImage: "", status: "published", isEditor: false, @@ -66,7 +64,6 @@ const mockArticles: PostDisplay[] = [ content: "在这篇文章中,我们将探讨一些提高 JavaScript 性能的技巧和最佳实践...", authorName: "王五", - publishedAt: new Date("2024-03-13"), coverImage: "ssssxx", status: "published", isEditor: false, @@ -91,7 +88,6 @@ const mockArticles: PostDisplay[] = [ title: "移动端适配最佳实践", content: "移动端开发中的各种适配问题及解决方案...", authorName: "田六", - publishedAt: new Date("2024-03-13"), coverImage: "https://images.unsplash.com/photo-1537432376769-00f5c2f4c8d2?w=500&auto=format", status: "published", @@ -114,7 +110,6 @@ const mockArticles: PostDisplay[] = [ content: "本文将深入探讨现代全栈开发的各个方面,包括前端框架选择、后端架构设计、数据库优化、微服务部署以及云原生实践...", authorName: "赵七", - publishedAt: new Date("2024-03-12"), coverImage: "https://images.unsplash.com/photo-1537432376769-00f5c2f4c8d2?w=500&auto=format", status: "published", @@ -147,7 +142,6 @@ const mockArticles: PostDisplay[] = [ content: "探索 TypeScript 的高级类型系统、装饰器、类型编程等特性,以及在大型项目中的最佳实践...", authorName: "孙八", - publishedAt: new Date("2024-03-11"), coverImage: "https://images.unsplash.com/photo-1516116216624-53e697fedbea?w=500&auto=format", status: "published", @@ -173,7 +167,6 @@ const mockArticles: PostDisplay[] = [ content: "全面解析 Web 性能优化策略,包括资源加载优化、渲染性能优化、网络优化等多个...", authorName: "周九", - publishedAt: new Date("2024-03-10"), coverImage: "https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=500&auto=format", status: "published", @@ -203,7 +196,6 @@ const mockArticles: PostDisplay[] = [ content: "详细介绍微前端的架构设计、实现方案、应用集成以及实际项目中的经验总结...", authorName: "吴十", - publishedAt: new Date("2024-03-09"), coverImage: "https://images.unsplash.com/photo-1517180102446-f3ece451e9d8?w=500&auto=format", status: "published", @@ -229,7 +221,6 @@ const mockArticles: PostDisplay[] = [ content: "探索如何将人工智能技术融入前端开发流程,包括智能代码补全、自动化测试、UI 生成、性能优化建议等实践应用...", authorName: "陈十一", - publishedAt: new Date("2024-03-08"), coverImage: "https://images.unsplash.com/photo-1677442136019-21780ecad995?w=500&auto=format", status: "published", @@ -357,7 +348,7 @@ export default new Template(({}) => { > <CalendarIcon className="w-4 h-4" /> <Text size="2"> - {article.publishedAt?.toLocaleDateString("zh-CN", { + {article.createdAt.toLocaleDateString("zh-CN", { year: "numeric", month: "long", day: "numeric", diff --git a/frontend/themes/echoes/theme.config.ts b/frontend/themes/echoes/theme.config.ts index 4ec0cb7..7d50480 100644 --- a/frontend/themes/echoes/theme.config.ts +++ b/frontend/themes/echoes/theme.config.ts @@ -13,34 +13,28 @@ const themeConfig: ThemeConfig = { }, }, layout: "layout.tsx", - templates: new Map([ - [ - "posts", - { - path: "posts", - name: "文章列表模板", - description: "博客首页展示模板", - }, - ], - [ - "post", - { - path: "post", - name: "文章详情模板", - description: "文章详情展示模板", - }, - ], - [ - "about", - { - path: "about", - name: "关于页面模板", - description: "关于页面展示模板", - }, - ], - ]), - - routes: new Map<string, string>([]), + templates: { + posts: { + path: "posts", + name: "文章列表模板", + description: "博客首页展示模板", + }, + post: { + path: "post", + name: "文章详情模板", + description: "文章详情展示模板", + }, + about: { + path: "about", + name: "关于页面模板", + description: "关于页面展示模板", + }, + }, + routes: { + "/": "posts.tsx", + "/about": "about.tsx", + "/post": "post.tsx" + } }; export default themeConfig; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 8cf89d7..e5b71bd 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -46,18 +46,10 @@ export default defineConfig( v3_singleFetch: true, v3_lazyRouteDiscovery: true, }, - routes: async (defineRoutes) => { - // 每次路由配置时重新读取环境变量 - const latestConfig = await getLatestEnv(); - + routes: (defineRoutes) => { return defineRoutes((route) => { - if (Number(latestConfig.VITE_INIT_STATUS) < 3) { - route("/", "init.tsx", { id: "index-route" }); - route("*", "init.tsx", { id: "catch-all-route" }); - } else { - route("/", "routes.tsx", { id: "index-route" }); - route("*", "routes.tsx", { id: "catch-all-route" }); - } + route("/", "routes.tsx",{id:"index-route"}); + route("*", "routes.tsx",{id:"catch-all-route"}); }); }, }),