前端:创建api,主题,路由服务,重新定义主题插件的约束,错误,加载组件

后端:去除文章和模板的自义定路径,创建获取系统令牌api
This commit is contained in:
lsy 2024-11-18 01:09:28 +08:00
parent f5754f982f
commit 5ca72e42cf
13 changed files with 372 additions and 102 deletions

View File

@ -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, --- 文章发布时间

View File

@ -79,6 +79,20 @@ async fn install() -> status::Custom<String> {
})
}
#[get("/system")]
async fn token_system() -> Result<status::Custom<String>, status::Custom<String>> {
// 创建 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)
// }

View File

@ -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<Props, State> {
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 <div dangerouslySetInnerHTML={{ __html: errorTemplate }} />;
} catch (e) {
// 如果无法获取主题模板,显示默认错误页面
return (
<div className="error-page">
<h1>Something went wrong</h1>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
}
return this.props.children;
}
}

View File

@ -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<LoadingBoundaryProps> = ({
children,
fallback = <div>Loading...</div>
}) => {
return (
<Suspense fallback={fallback}>
{children}
</Suspense>
);
};

View File

@ -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<CapabilityProps<void>>;
routs: Set<{
description?: string; // 路由描述(可选)
path: string; // 路由路径

View File

@ -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<CapabilityProps<void>>;
};
// 渲染函数
render: (props: any) => React.ReactNode;
}

View File

@ -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<string, ThemeTemplate>; // 主题模板的映射表
/** 主题全局配置 */
globalSettings?: {

View File

@ -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 }) => (
<ExtensionProvider>
<ThemeProvider>{children}</ThemeProvider>
</ExtensionProvider>
<ApiProvider>
<CapabilityProvider>
<ThemeProvider>
{children}
</ThemeProvider>
</CapabilityProvider>
</ApiProvider>
);

View File

@ -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],
);
}

View File

@ -40,26 +40,30 @@ export class ApiService {
* @throws Error
*/
private async getSystemToken(): Promise<string> {
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<T>(
endpoint: string,
options: RequestInit = {},
requiresAuth = true
auth ?: string
): Promise<T> {
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}`, {

View File

@ -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<void> {
const themeConfig = this.themeService.getThemeConfig();
if (!themeConfig) {
throw new Error('Theme configuration not loaded');
}
this.routes = [
{
path: '/',
element: this.createRouteElement(themeConfig.routes.index),
errorElement: <ErrorBoundary />,
},
{
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 <div dangerouslySetInnerHTML={{ __html: template }} />;
};
}
/**
*
*/
public getRoutes(): RouteObject[] {
return this.routes;
}
/**
*
*/
public addRoute(path: string, templateName: string): void {
this.routes.push({
path,
element: this.createRouteElement(templateName),
});
}
}

View File

@ -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<string, React.ComponentType> = new Map(); // 主题组件缓存
private static instance: ThemeService;
private currentTheme?: ThemeConfig;
private templates: Map<string, string> = 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<void> {
try {
// 从API获取当前主题配置
const themeConfig = await this.api.request<ThemeConfig>(
'/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<void> {
try {
this.currentTheme = config;
await this.loadTemplates();
} catch (error) {
console.error('Failed to load theme:', error);
throw error;
}
}
/**
*
*/
private async loadTemplates(): Promise<void> {
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<ThemeConfig>): Promise<void> {
try {
const updatedConfig = await this.api.request<ThemeConfig>(
'/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<void> {
this.themeConfig = themeConfig; // 设置当前主题
await this.loadThemeComponents(themeConfig); // 加载主题组件
}
// 加载主题组件
private async loadThemeComponents(config:ThemeConfig): Promise<void> {
// 清除现有组件缓存
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; // 返回当前主题配置
}
}
}
}

View File

@ -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: "",