From 5ca72e42cfc510ccb19ace69899cdb190bc7012f Mon Sep 17 00:00:00 2001 From: lsy Date: Mon, 18 Nov 2024 01:09:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=EF=BC=9A=E5=88=9B=E5=BB=BAap?= =?UTF-8?q?i=EF=BC=8C=E4=B8=BB=E9=A2=98=EF=BC=8C=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=EF=BC=8C=E9=87=8D=E6=96=B0=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E6=8F=92=E4=BB=B6=E7=9A=84=E7=BA=A6=E6=9D=9F?= =?UTF-8?q?=EF=BC=8C=E9=94=99=E8=AF=AF=EF=BC=8C=E5=8A=A0=E8=BD=BD=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=20=E5=90=8E=E7=AB=AF=EF=BC=9A=E5=8E=BB=E9=99=A4?= =?UTF-8?q?=E6=96=87=E7=AB=A0=E5=92=8C=E6=A8=A1=E6=9D=BF=E7=9A=84=E8=87=AA?= =?UTF-8?q?=E4=B9=89=E5=AE=9A=E8=B7=AF=E5=BE=84=EF=BC=8C=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E7=B3=BB=E7=BB=9F=E4=BB=A4=E7=89=8Capi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../database/relational/postgresql/init.sql | 2 - backend/src/main.rs | 39 ++-- frontend/components/ErrorBoundary.tsx | 50 +++++ frontend/components/LoadingBoundary.tsx | 18 ++ frontend/contracts/pluginContract.ts | 3 - frontend/contracts/templateContract.ts | 7 +- frontend/contracts/themeContract.ts | 2 +- frontend/hooks/servicesProvider.tsx | 27 ++- frontend/hooks/useAsyncError.tsx | 15 ++ frontend/services/apiService.ts | 25 ++- frontend/services/routeManager.ts | 92 +++++++++ frontend/services/themeService.ts | 191 +++++++++++++----- frontend/themes/default/theme.config.ts | 3 +- 13 files changed, 372 insertions(+), 102 deletions(-) create mode 100644 frontend/components/ErrorBoundary.tsx create mode 100644 frontend/components/LoadingBoundary.tsx create mode 100644 frontend/hooks/useAsyncError.tsx create mode 100644 frontend/services/routeManager.ts diff --git a/backend/src/database/relational/postgresql/init.sql b/backend/src/database/relational/postgresql/init.sql index a526382..d3fd96b 100644 --- a/backend/src/database/relational/postgresql/init.sql +++ b/backend/src/database/relational/postgresql/init.sql @@ -32,7 +32,6 @@ CREATE TABLE pages page_content TEXT NOT NULL, --- 独立页面内容 page_mould VARCHAR(50), --- 独立页面模板名称 page_fields JSON, --- 自定义字段 - page_path VARCHAR(255), --- 文章路径 page_status publication_status DEFAULT 'draft' --- 文章状态 ); --- 文章表 @@ -48,7 +47,6 @@ CREATE TABLE posts post_status publication_status DEFAULT 'draft', --- 文章状态 post_editor BOOLEAN DEFAULT FALSE, --- 文章是否编辑未保存 post_unsaved_content TEXT, --- 未保存的文章 - post_path VARCHAR(255), --- 文章路径 post_created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, --- 文章创建时间 post_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, --- 文章更新时间 post_published_at TIMESTAMP, --- 文章发布时间 diff --git a/backend/src/main.rs b/backend/src/main.rs index b995672..e20bb92 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -79,6 +79,20 @@ async fn install() -> status::Custom { }) } +#[get("/system")] +async fn token_system() -> Result, status::Custom> { + // 创建 Claims + let claims = secret::CustomClaims { + user_id: String::from("system"), + device_ua: String::from("system"), + }; + // 生成JWT + let token = secret::generate_jwt(claims,Duration::seconds(1)) + .map_err(|e| status::Custom(Status::InternalServerError, format!("JWT generation failed: {}", e)))?; + + Ok(status::Custom(Status::Ok, token)) +} + /** * 启动Rocket应用 */ @@ -88,26 +102,7 @@ async fn rocket() -> _ { init_db(config.db_config) .await .expect("Failed to connect to database"); // 初始化数据库连接 - rocket::build().mount("/", routes![install, ssql]) // 挂载API路由 + rocket::build() + .mount("/", routes![install, ssql]) // 挂载API路由 + .mount("/auth/token", routes![token_system]) } - - -// fn main(){ -// // secret::generate_key().expect("msg"); -// // 创建claims -// let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; - -// // 创建 Claims -// let claims = secret::CustomClaims { -// user_id: String::from("lsy"), -// device_ua: String::from("lsy"), -// }; -// let t=String::from("eyJhbGciOiJFZERTQSJ9.eyJleHAiOjE3MzE3NTczMDMsIm5iZiI6MTczMTc1NzI4MywiaWF0IjoxNzMxNzU3MjgzLCJ1c2VyX2lkIjoibHN5IiwiZGV2aWNlX3VhIjoibHN5In0.C8t5XZFSKnnDVmc6WkY-gzGNSAP7lNAjP9yBjhdvIRO7r_QjDnfcm0INIqCt5cyvnRlE2rFJIx_axOfLx2QJAw"); -// // 生成JWT -// let token = secret::generate_jwt(claims,Duration::seconds(20)).expect("msg"); -// println!("{}", token); - -// // 验证JWT -// let a=secret::validate_jwt(&t).expect("msg"); -// println!("\n\n{}",a.user_id) -// } \ No newline at end of file diff --git a/frontend/components/ErrorBoundary.tsx b/frontend/components/ErrorBoundary.tsx new file mode 100644 index 0000000..645a385 --- /dev/null +++ b/frontend/components/ErrorBoundary.tsx @@ -0,0 +1,50 @@ +// File path: components/ErrorBoundary.tsx +import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { ThemeService } from '../services/themeService'; + +interface Props { + children?: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +export default class ErrorBoundary extends Component { + public state: State = { + hasError: false + }; + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Uncaught error:', error, errorInfo); + } + + public render() { + if (this.state.hasError) { + const themeService = ThemeService.getInstance(); + try { + // 尝试使用主题的错误模板 + const errorTemplate = themeService.getTemplate('error'); + return
; + } catch (e) { + // 如果无法获取主题模板,显示默认错误页面 + return ( +
+

Something went wrong

+

{this.state.error?.message}

+ +
+ ); + } + } + + return this.props.children; + } +} diff --git a/frontend/components/LoadingBoundary.tsx b/frontend/components/LoadingBoundary.tsx new file mode 100644 index 0000000..c590ccc --- /dev/null +++ b/frontend/components/LoadingBoundary.tsx @@ -0,0 +1,18 @@ +// File path: components/LoadingBoundary.tsx +import React, { Suspense } from 'react'; + +interface LoadingBoundaryProps { + children: React.ReactNode; + fallback?: React.ReactNode; +} + +export const LoadingBoundary: React.FC = ({ + children, + fallback =
Loading...
+}) => { + return ( + + {children} + + ); +}; diff --git a/frontend/contracts/pluginContract.ts b/frontend/contracts/pluginContract.ts index f55474d..d88a326 100644 --- a/frontend/contracts/pluginContract.ts +++ b/frontend/contracts/pluginContract.ts @@ -7,7 +7,6 @@ * 还包括插件的生命周期钩子和依赖项的配置。 */ import { SerializeType } from "contracts/generalContract"; -import { CapabilityProps } from "contracts/capabilityContract"; export interface PluginConfig { @@ -20,8 +19,6 @@ export interface PluginConfig { icon?: string; // 插件图标URL(可选) managePath?: string; // 插件管理页面路径(可选) configuration?: PluginConfiguration; // 插件配置 - /** 声明需要使用的能力,没有实际作用 */ - capabilities?: Set>; routs: Set<{ description?: string; // 路由描述(可选) path: string; // 路由路径 diff --git a/frontend/contracts/templateContract.ts b/frontend/contracts/templateContract.ts index e8cd1d5..69e1adf 100644 --- a/frontend/contracts/templateContract.ts +++ b/frontend/contracts/templateContract.ts @@ -1,5 +1,3 @@ -import { CapabilityProps } from "contracts/capabilityContract"; - export interface TemplateContract { // 模板名称 name: string; @@ -13,10 +11,7 @@ export interface TemplateContract { styles?: string[]; // 模板脚本 scripts?: string[]; - }; - /** 声明需要使用的能力,没有实际作用 */ - capabilities?: Set>; - + }; // 渲染函数 render: (props: any) => React.ReactNode; } \ No newline at end of file diff --git a/frontend/contracts/themeContract.ts b/frontend/contracts/themeContract.ts index d9839f8..08ddcb3 100644 --- a/frontend/contracts/themeContract.ts +++ b/frontend/contracts/themeContract.ts @@ -12,10 +12,10 @@ import { SerializeType } from "contracts/generalContract"; export interface ThemeConfig { name: string; // 主题的名称 displayName: string; // 主题的显示名称 + icon?: string; // 主题图标URL(可选) version: string; // 主题的版本号 description?: string; // 主题的描述信息 author?: string; // 主题的作者信息 - entry: string; // 主题的入口路径 templates: Map; // 主题模板的映射表 /** 主题全局配置 */ globalSettings?: { diff --git a/frontend/hooks/servicesProvider.tsx b/frontend/hooks/servicesProvider.tsx index 9da328f..5df67b2 100644 --- a/frontend/hooks/servicesProvider.tsx +++ b/frontend/hooks/servicesProvider.tsx @@ -1,17 +1,22 @@ import { CapabilityService } from "services/capabilityService"; import { ThemeService } from "services/themeService"; -import { createServiceContext } from "./createServiceContext"; +import { ApiService } from "services/apiService"; +import { createServiceContext } from "hooks/createServiceContext"; import { ReactNode } from "react"; -export const { ExtensionProvider, useExtension } = createServiceContext( - "Extension", - () => CapabilityService.getInstance(), +export const { CapabilityProvider, useCapability } = createServiceContext( + "Capability", () => CapabilityService.getInstance(), ); -export const { ThemeProvider, useTheme } = createServiceContext("Theme", () => - ThemeService.getInstance(), +export const { ThemeProvider, useTheme } = createServiceContext( + "Theme", () => ThemeService.getInstance(), ); +export const { ApiProvider, useApi } = createServiceContext( + "Api", () => ThemeService.getInstance(), +); + + // File path:hooks/servicesProvider.tsx /** * ServiceProvider 组件用于提供扩展和主题上下文给其子组件。 @@ -19,7 +24,11 @@ export const { ThemeProvider, useTheme } = createServiceContext("Theme", () => * @param children - 要渲染的子组件。 */ export const ServiceProvider = ({ children }: { children: ReactNode }) => ( - - {children} - + + + + {children} + + + ); diff --git a/frontend/hooks/useAsyncError.tsx b/frontend/hooks/useAsyncError.tsx new file mode 100644 index 0000000..7f16f37 --- /dev/null +++ b/frontend/hooks/useAsyncError.tsx @@ -0,0 +1,15 @@ +// File path: hooks/useAsyncError.tsx +import { useState, useCallback } from 'react'; + +export function useAsyncError() { + const [, setError] = useState(); + + return useCallback( + (e: Error) => { + setError(() => { + throw e; + }); + }, + [setError], + ); +} \ No newline at end of file diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts index 0695a48..9b4ef4d 100644 --- a/frontend/services/apiService.ts +++ b/frontend/services/apiService.ts @@ -40,26 +40,30 @@ export class ApiService { * @throws Error 如果未找到凭据或请求失败 */ private async getSystemToken(): Promise { - const credentials = localStorage.getItem('system_credentials'); - if (!credentials) { - throw new Error('System credentials not found'); + const username = import.meta.env.VITE_SYSTEM_USERNAME; + const password = import.meta.env.VITE_SYSTEM_PASSWORD; + if (!username || !password ) { + throw new Error('Failed to obtain the username or password of the front-end system'); } try { - const response = await fetch(`${this.baseURL}/auth/system`, { + const response = await fetch(`${this.baseURL}/auth/token/system`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(JSON.parse(credentials)), + body: JSON.stringify({ + username, + password, + }), }); if (!response.ok) { throw new Error('Failed to get system token'); } - const { token } = await response.json(); - return token; + const data = await response.text(); + return data; // Assuming the token is in the 'token' field of the response } catch (error) { console.error('Error getting system token:', error); throw error; @@ -77,7 +81,7 @@ export class ApiService { public async request( endpoint: string, options: RequestInit = {}, - requiresAuth = true + auth ?: string ): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); @@ -85,9 +89,8 @@ export class ApiService { try { const headers = new Headers(options.headers); - if (requiresAuth) { - const token = await this.getSystemToken(); - headers.set('Authorization', `Bearer ${token}`); + if (auth) { + headers.set('Authorization', `Bearer ${auth}`); } const response = await fetch(`${this.baseURL}${endpoint}`, { diff --git a/frontend/services/routeManager.ts b/frontend/services/routeManager.ts new file mode 100644 index 0000000..f205a51 --- /dev/null +++ b/frontend/services/routeManager.ts @@ -0,0 +1,92 @@ +// File path: services/routeManager.ts +import { useEffect } from 'react'; +import { useRoutes, RouteObject } from 'react-router-dom'; +import { ThemeService } from './themeService'; +import ErrorBoundary from '../components/ErrorBoundary'; + +export class RouteManager { + private static instance: RouteManager; + private themeService: ThemeService; + private routes: RouteObject[] = []; + + private constructor(themeService: ThemeService) { + this.themeService = themeService; + } + + public static getInstance(themeService?: ThemeService): RouteManager { + if (!RouteManager.instance && themeService) { + RouteManager.instance = new RouteManager(themeService); + } + return RouteManager.instance; + } + + /** + * 初始化路由 + */ + public async initialize(): Promise { + const themeConfig = this.themeService.getThemeConfig(); + if (!themeConfig) { + throw new Error('Theme configuration not loaded'); + } + + this.routes = [ + { + path: '/', + element: this.createRouteElement(themeConfig.routes.index), + errorElement: , + }, + { + path: '/post/:id', + element: this.createRouteElement(themeConfig.routes.post), + }, + { + path: '/tag/:tag', + element: this.createRouteElement(themeConfig.routes.tag), + }, + { + path: '/category/:category', + element: this.createRouteElement(themeConfig.routes.category), + }, + { + path: '*', + element: this.createRouteElement(themeConfig.routes.error), + }, + ]; + + // 添加自定义页面路由 + themeConfig.routes.page.forEach((template, path) => { + this.routes.push({ + path, + element: this.createRouteElement(template), + }); + }); + } + + /** + * 创建路由元素 + */ + private createRouteElement(templateName: string) { + return (props: any) => { + const template = this.themeService.getTemplate(templateName); + // 这里可以添加模板渲染逻辑 + return
; + }; + } + + /** + * 获取所有路由 + */ + public getRoutes(): RouteObject[] { + return this.routes; + } + + /** + * 添加新路由 + */ + public addRoute(path: string, templateName: string): void { + this.routes.push({ + path, + element: this.createRouteElement(templateName), + }); + } +} \ No newline at end of file diff --git a/frontend/services/themeService.ts b/frontend/services/themeService.ts index 4f4f1d5..5997ddd 100644 --- a/frontend/services/themeService.ts +++ b/frontend/services/themeService.ts @@ -1,52 +1,151 @@ -// service/theme/themeService.ts -import { ThemeConfig } from "contracts/themeContract" +// File path: services/themeService.ts +import { ThemeConfig, ThemeTemplate } from "contracts/themeContract"; +import { ApiService } from "./apiService"; export class ThemeService { - private static themeInstance: ThemeService; // 单例实例 - private themeConfig: ThemeConfig | null = null; // 当前主题配置 - private themeComponents: Map = new Map(); // 主题组件缓存 + private static instance: ThemeService; + private currentTheme?: ThemeConfig; + private templates: Map = new Map(); + private api: ApiService; - private constructor() { } // 私有构造函数,防止外部实例化 + private constructor(api: ApiService) { + this.api = api; + } - // 获取单例实例 - public static getInstance(): ThemeService { - if (!ThemeService.themeInstance) { - ThemeService.themeInstance = new ThemeService(); + public static getInstance(api?: ApiService): ThemeService { + if (!ThemeService.instance && api) { + ThemeService.instance = new ThemeService(api); + } + return ThemeService.instance; + } + + /** + * 初始化主题服务 + */ + public async initialize(): Promise { + try { + // 从API获取当前主题配置 + const themeConfig = await this.api.request( + '/theme/current', + { method: 'GET' } + ); + + // 加载主题配置 + await this.loadTheme(themeConfig); + } catch (error) { + console.error('Failed to initialize theme:', error); + throw error; + } + } + + /** + * 加载主题配置 + */ + private async loadTheme(config: ThemeConfig): Promise { + try { + this.currentTheme = config; + await this.loadTemplates(); + } catch (error) { + console.error('Failed to load theme:', error); + throw error; + } + } + + /** + * 加载主题模板 + */ + private async loadTemplates(): Promise { + if (!this.currentTheme) { + throw new Error('No theme configuration loaded'); + } + + const loadTemplate = async (template: ThemeTemplate) => { + try { + const response = await fetch(template.path); + const templateContent = await response.text(); + this.templates.set(template.name, templateContent); + } catch (error) { + console.error(`Failed to load template ${template.name}:`, error); + throw error; + } + }; + + // 并行加载所有模板 + const loadPromises = Array.from(this.currentTheme.templates.values()) + .map(template => loadTemplate(template)); + + await Promise.all(loadPromises); + } + + /** + * 获取主题配置 + */ + public getThemeConfig(): ThemeConfig | undefined { + return this.currentTheme; + } + + /** + * 获取模板内容 + */ + public getTemplate(templateName: string): string { + const template = this.templates.get(templateName); + if (!template) { + throw new Error(`Template ${templateName} not found`); + } + return template; + } + + /** + * 根据路由获取对应的模板 + */ + public getTemplateByRoute(route: string): string { + if (!this.currentTheme) { + throw new Error('No theme configuration loaded'); + } + + let templateName: string | undefined; + + // 检查是否是预定义路由 + if (route === '/') { + templateName = this.currentTheme.routes.index; + } else if (route.startsWith('/post/')) { + templateName = this.currentTheme.routes.post; + } else if (route.startsWith('/tag/')) { + templateName = this.currentTheme.routes.tag; + } else if (route.startsWith('/category/')) { + templateName = this.currentTheme.routes.category; + } else { + // 检查自定义页面路由 + templateName = this.currentTheme.routes.page.get(route); + } + + if (!templateName) { + templateName = this.currentTheme.routes.error; + } + + return this.getTemplate(templateName); + } + + /** + * 更新主题配置 + */ + public async updateThemeConfig(config: Partial): Promise { + try { + const updatedConfig = await this.api.request( + '/theme/config', + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(config), } - return ThemeService.themeInstance; + ); + + await this.loadTheme(updatedConfig); + } catch (error) { + console.error('Failed to update theme configuration:', error); + throw error; } - - // 加载主题 - async loadTheme(themeConfig: ThemeConfig): Promise { - this.themeConfig = themeConfig; // 设置当前主题 - await this.loadThemeComponents(themeConfig); // 加载主题组件 - } - - // 加载主题组件 - private async loadThemeComponents(config:ThemeConfig): Promise { - // 清除现有组件缓存 - this.themeComponents.clear(); - - // 动态导入主题入口组件 - const entryComponent = await import(config.entry); - this.themeComponents.set('entry', entryComponent.default); // 缓存入口路径 - - // 加载所有模板组件 - for (const [key, template] of config.templates.entries()) { - const component = await import(template.path); - this.themeComponents.set(key, component.default); // 缓存模板组件 - } - } - - // 获取指定模板名称的组件 - getComponent(templateName: string): React.ComponentType | null { - return this.themeComponents.get(templateName) || null; // 返回组件或null - } - - // 获取当前主题配置 - getCurrentTheme(): ThemeConfig | null { - return this.currentTheme; // 返回当前主题配置 - } -} - - + } +} \ No newline at end of file diff --git a/frontend/themes/default/theme.config.ts b/frontend/themes/default/theme.config.ts index 0486865..d0be2ba 100644 --- a/frontend/themes/default/theme.config.ts +++ b/frontend/themes/default/theme.config.ts @@ -1,4 +1,4 @@ -import { ThemeConfig } from "types/themeTypeRequirement"; +import { ThemeConfig } from "contracts/themeContract"; export const themeConfig: ThemeConfig = { name: 'default', @@ -15,7 +15,6 @@ export const themeConfig: ThemeConfig = { }], ]), - settingsSchema: undefined, routes: { post: "", tag: "",