diff --git a/backend/src/api/settings.rs b/backend/src/api/settings.rs index 0b2081f..ba81eb7 100644 --- a/backend/src/api/settings.rs +++ b/backend/src/api/settings.rs @@ -17,7 +17,7 @@ use std::sync::Arc; pub struct SystemConfigure { pub author_name: String, pub current_theme: String, - pub site_keyword: Vec, + pub site_keyword: String, pub site_description: String, pub admin_path: String, } @@ -27,7 +27,7 @@ impl Default for SystemConfigure { Self { author_name: "lsy".to_string(), current_theme: "default".to_string(), - site_keyword: vec!["echoes".to_string()], + site_keyword: "echoes".to_string(), site_description: "echoes是一个高效、可扩展的博客平台".to_string(), admin_path: "admin".to_string(), } @@ -91,8 +91,21 @@ pub async fn system_config_get( _token: SystemToken, ) -> AppResult> { let sql = state.sql_get().await.into_app_result()?; - let configure = get_setting(&sql, "system".to_string(), "settings".to_string()) + let settings = get_setting(&sql, "system".to_string(), "settings".to_string()) .await .into_app_result()?; - Ok(configure) + 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) +} \ No newline at end of file diff --git a/frontend/app/env.d.ts b/frontend/app/env.d.ts index 71ad528..4ecb1e6 100644 --- a/frontend/app/env.d.ts +++ b/frontend/app/env.d.ts @@ -8,14 +8,10 @@ interface ImportMetaEnv { readonly VITE_SERVER_API: string; // 用于访问API的基础URL - readonly VITE_THEME_PATH: string; // 存储主题文件的目录路径 - readonly VITE_CONTENT_PATH: string; //mark文章存储的位置 - readonly VITE_CONTENT_STATIC_PATH: string; //导出文章静态存储的位置 - readonly VITE_PLUGINS_PATH: string; // 存储插件文件的目录路径 - readonly VITE_ASSETS_PATH: string; // 存储静态资源的目录路径 + readonly VITE_SYSTEM_PORT: number; // 系统端口 VITE_SYSTEM_USERNAME: string; // 前端账号名称 VITE_SYSTEM_PASSWORD: string; // 前端账号密码 - VITE_SYSTEM_STATUS: boolean; // 系统是否进行安装 + VITE_INIT_STATUS: boolean; // 系统是否进行安装 } interface ImportMeta { diff --git a/frontend/app/init.tsx b/frontend/app/init.tsx new file mode 100644 index 0000000..d3a8ed4 --- /dev/null +++ b/frontend/app/init.tsx @@ -0,0 +1,3 @@ +export default function page(){ + return <>安装中 +} \ No newline at end of file diff --git a/frontend/app/root.tsx b/frontend/app/root.tsx index 61c8b98..6006375 100644 --- a/frontend/app/root.tsx +++ b/frontend/app/root.tsx @@ -2,25 +2,13 @@ import { Links, Meta, Outlet, - Scripts, ScrollRestoration, } from "@remix-run/react"; -import type { LinksFunction } from "@remix-run/node"; -import "./tailwind.css"; +import { BaseProvider } from "hooks/servicesProvider"; +import { LinksFunction } from "@remix-run/react/dist/routeModules"; -export const links: LinksFunction = () => [ - { rel: "preconnect", href: "https://fonts.googleapis.com" }, - { - rel: "preconnect", - href: "https://fonts.gstatic.com", - crossOrigin: "anonymous", - }, - { - rel: "stylesheet", - href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap", - }, -]; +import "~/tailwind.css"; export function Layout({ children }: { children: React.ReactNode }) { return ( @@ -28,18 +16,23 @@ export function Layout({ children }: { children: React.ReactNode }) { + {children} - ); } - export default function App() { - return ; + return ( + + + + + + ); } diff --git a/frontend/app/routes.tsx b/frontend/app/routes.tsx new file mode 100644 index 0000000..db56338 --- /dev/null +++ b/frontend/app/routes.tsx @@ -0,0 +1,15 @@ +import { useState } from "react"; + +import ReactDOMServer from 'react-dom/server'; +import { useLocation } from 'react-router-dom'; + +const MyComponent = () => { + return
Hello, World!
; +}; + + +export default function Routes() { + const htmlString = ReactDOMServer.renderToString(); + + return (
安装重构
) + } \ No newline at end of file diff --git a/frontend/app/routes/_index.tsx b/frontend/app/routes/_index.tsx deleted file mode 100644 index 13a5c00..0000000 --- a/frontend/app/routes/_index.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import type { MetaFunction } from "@remix-run/node"; - -export const meta: MetaFunction = () => { - return [ - { title: "New Remix App" }, - { name: "description", content: "Welcome to Remix!" }, - ]; -}; - -export default function Index() { - return ( -
-
-
-

- Welcome to Remix -

-
- Remix - Remix -
-
- -
-
- ); -} - -const resources = [ - { - href: "https://remix.run/start/quickstart", - text: "Quick Start (5 min)", - icon: ( - - - - ), - }, - { - href: "https://remix.run/start/tutorial", - text: "Tutorial (30 min)", - icon: ( - - - - ), - }, - { - href: "https://remix.run/docs", - text: "Remix Docs", - icon: ( - - - - ), - }, - { - href: "https://rmx.as/discord", - text: "Join Discord", - icon: ( - - - - ), - }, -]; diff --git a/frontend/common/serializableType.ts b/frontend/common/serializableType.ts new file mode 100644 index 0000000..fd18142 --- /dev/null +++ b/frontend/common/serializableType.ts @@ -0,0 +1,21 @@ +export type Serializable = + | null + | number + | string + | boolean + | { [key: string]: Serializable } + | Array; +export interface Configuration { + [key: string]: { + title: string; + description?: string; + data: Serializable; + }; +} + +export interface PathDescription { + path: string; + name: string; + description?: string; +} + diff --git a/frontend/contracts/capabilityContract.ts b/frontend/contracts/capabilityContract.ts deleted file mode 100644 index b6c7930..0000000 --- a/frontend/contracts/capabilityContract.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface CapabilityProps { - name: string; - description?: string; - execute: (...args: any[]) => Promise; -} diff --git a/frontend/contracts/generalContract.ts b/frontend/contracts/generalContract.ts deleted file mode 100644 index 08b7f46..0000000 --- a/frontend/contracts/generalContract.ts +++ /dev/null @@ -1,14 +0,0 @@ -export type SerializeType = - | null - | number - | string - | boolean - | { [key: string]: SerializeType } - | Array; -export interface Configuration { - [key: string]: { - title: string; - description?: string; - data: SerializeType; - }; -} diff --git a/frontend/contracts/pluginContract.ts b/frontend/contracts/pluginContract.ts deleted file mode 100644 index 1b8de80..0000000 --- a/frontend/contracts/pluginContract.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Configuration } from "contracts/generalContract"; - -export interface PluginConfig { - name: string; - version: string; - displayName: string; - description?: string; - author?: string; - enabled: boolean; - icon?: string; - managePath?: string; - configuration?: Configuration; - routs: Set<{ - description?: string; - path: string; - }>; -} diff --git a/frontend/contracts/templateContract.ts b/frontend/contracts/templateContract.ts deleted file mode 100644 index 675faba..0000000 --- a/frontend/contracts/templateContract.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface TemplateContract { - name: string; - description?: string; - config: { - layout?: string; - styles?: string[]; - scripts?: string[]; - }; - loader: () => Promise; - element: () => React.ReactNode; -} diff --git a/frontend/contracts/themeContract.ts b/frontend/contracts/themeContract.ts deleted file mode 100644 index 8378147..0000000 --- a/frontend/contracts/themeContract.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Configuration } from "contracts/generalContract"; -export interface ThemeConfig { - name: string; - displayName: string; - icon?: string; - version: string; - description?: string; - author?: string; - templates: Map; - globalSettings?: { - layout?: string; - css?: string; - }; - configuration: Configuration; - routes: { - index: string; - post: string; - tag: string; - category: string; - error: string; - loding: string; - page: Map; - }; -} - -export interface ThemeTemplate { - path: string; - name: string; - description?: string; -} diff --git a/frontend/services/apiService.ts b/frontend/core/api.ts similarity index 100% rename from frontend/services/apiService.ts rename to frontend/core/api.ts diff --git a/frontend/services/capabilityService.ts b/frontend/core/capability.ts similarity index 94% rename from frontend/services/capabilityService.ts rename to frontend/core/capability.ts index 841e2aa..b562107 100644 --- a/frontend/services/capabilityService.ts +++ b/frontend/core/capability.ts @@ -1,4 +1,9 @@ -import { CapabilityProps } from "contracts/capabilityContract"; +export interface CapabilityProps { + name: string; + description?: string; + execute: (...args: any[]) => Promise; +} + export class CapabilityService { private capabilities: Map< diff --git a/frontend/services/pluginService.ts b/frontend/core/plugin.ts similarity index 75% rename from frontend/services/pluginService.ts rename to frontend/core/plugin.ts index 7465e4c..3462a00 100644 --- a/frontend/services/pluginService.ts +++ b/frontend/core/plugin.ts @@ -1,10 +1,24 @@ -import { PluginConfiguration } from "types/pluginRequirement"; -import { Contracts } from "contracts/capabilityContract"; + +export interface PluginConfig { + name: string; + version: string; + displayName: string; + description?: string; + author?: string; + enabled: boolean; + icon?: string; + managePath?: string; + configuration?: Configuration; + routes: Set<{ + description?: string; + path: string; + }>; +} + + export class PluginManager { - private plugins: Map = new Map(); - private configurations: Map = new Map(); - private extensions: Map = new Map(); + private configurations: Map = new Map(); async loadPlugins() { const pluginDirs = await this.scanPluginDirectory(); @@ -37,7 +51,7 @@ export class PluginManager { async getPluginConfig( pluginName: string, - ): Promise { + ): Promise { const dbConfig = await this.fetchConfigFromDB(pluginName); if (dbConfig) { return dbConfig; diff --git a/frontend/core/route.ts b/frontend/core/route.ts new file mode 100644 index 0000000..87c952f --- /dev/null +++ b/frontend/core/route.ts @@ -0,0 +1,38 @@ +import { ReactNode } from "react"; // Import React +import { LoaderFunction } from "react-router-dom"; + + +interface RouteElement { + element: ReactNode, + loader?: LoaderFunction, + children?: RouteElement[], +} + +export class RouteManager { + private static instance: RouteManager; + private routes = new Map(); + private routesCache = new Map(); + + private constructor() { } + + public static getInstance(): RouteManager { + if (!RouteManager.instance) { + RouteManager.instance = new RouteManager(); + } + return RouteManager.instance; + } + + private createRouteElement( + path: string, + element: RouteElement + ) { + this.routes.set(path, element); + } + + private getRoutes(path: string): RouteElement | undefined { + return this.routes.get(path); + } + private getRoutesCache(path: string): string | undefined { + return this.routesCache.get(path); + } +} diff --git a/frontend/core/theme.ts b/frontend/core/theme.ts new file mode 100644 index 0000000..d6c50d4 --- /dev/null +++ b/frontend/core/theme.ts @@ -0,0 +1,98 @@ +import { Configuration, PathDescription } from "common/serializableType"; +import { ApiService } from "./api"; + +export interface ThemeConfig { + name: string; + displayName: string; + icon?: string; + version: string; + description?: string; + author?: string; + templates: Map; + globalSettings?: { + layout?: string; + css?: string; + }; + configuration: Configuration; + routes: { + index: string; + post: string; + tag: string; + category: string; + error: string; + loading: string; + page: Map; + }; +} + + + +export interface Template { + name: string; + description?: string; + config: { + layout?: string; + styles?: string[]; + scripts?: string[]; + }; + loader: () => Promise; + element: () => React.ReactNode; +} + + + +export class ThemeService { + private static instance: ThemeService; + private currentTheme?: ThemeConfig; + private api: ApiService; + + private constructor(api: ApiService) { + this.api = api; + } + + public static getInstance(api?: ApiService): ThemeService { + if (!ThemeService.instance && api) { + ThemeService.instance = new ThemeService(api); + } + return ThemeService.instance; + } + + public async getCurrentTheme(): Promise { + try { + const themeConfig = await this.api.request( + "/theme/current", + { method: "GET" }, + ); + this.currentTheme = themeConfig; + } catch (error) { + console.error("Failed to initialize theme:", error); + throw error; + } + } + + + public getThemeConfig(): ThemeConfig | undefined { + return this.currentTheme; + } + + + 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), + }, + ); + + await this.loadTheme(updatedConfig); + } catch (error) { + console.error("Failed to update theme configuration:", error); + throw error; + } + } +} diff --git a/frontend/hooks/servicesProvider.tsx b/frontend/hooks/servicesProvider.tsx index 4898b6f..62fc45e 100644 --- a/frontend/hooks/servicesProvider.tsx +++ b/frontend/hooks/servicesProvider.tsx @@ -1,6 +1,6 @@ -import { CapabilityService } from "services/capabilityService"; -import { ThemeService } from "services/themeService"; -import { ApiService } from "services/apiService"; +import { CapabilityService } from "core/capability"; +import { ApiService } from "core/api"; +import { RouteManager } from "core/route"; import { createServiceContext } from "hooks/createServiceContext"; import { ReactNode } from "react"; @@ -9,18 +9,18 @@ export const { CapabilityProvider, useCapability } = createServiceContext( () => CapabilityService.getInstance(), ); -export const { ThemeProvider, useTheme } = createServiceContext("Theme", () => - ThemeService.getInstance(), +export const { RouteProvider, useRoute } = createServiceContext("Route", () => + RouteManager.getInstance(), ); export const { ApiProvider, useApi } = createServiceContext("Api", () => - ThemeService.getInstance(), + ApiService.getInstance(), ); -export const ServiceProvider = ({ children }: { children: ReactNode }) => ( +export const BaseProvider = ({ children }: { children: ReactNode }) => ( - {children} + {children} ); diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..602e823 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/services/routeManager.ts b/frontend/services/routeManager.ts deleted file mode 100644 index 13de00f..0000000 --- a/frontend/services/routeManager.ts +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react"; // Import React -import { LoaderFunction, RouteObject } from "react-router-dom"; - -export class RouteManager { - private static instance: RouteManager; - private routes: RouteObject[] = []; - - private constructor() {} - - public static getInstance(): RouteManager { - if (!RouteManager.instance) { - RouteManager.instance = new RouteManager(); - } - return RouteManager.instance; - } - - private createRouteElement( - path: string, - element: React.ReactNode, - loader?: LoaderFunction, - children?: RouteObject[], - ) { - this.routes.push({ - path, - element, - loader, - children, - }); - } - - private getRoutes(): RouteObject[] { - return this.routes; - } -} diff --git a/frontend/services/themeService.ts b/frontend/services/themeService.ts deleted file mode 100644 index d3cdf41..0000000 --- a/frontend/services/themeService.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { ThemeConfig, ThemeTemplate } from "contracts/themeContract"; -import { ApiService } from "./apiService"; - -export class ThemeService { - private static instance: ThemeService; - private currentTheme?: ThemeConfig; - private api: ApiService; - - private constructor(api: ApiService) { - this.api = api; - } - - public static getInstance(api?: ApiService): ThemeService { - if (!ThemeService.instance && api) { - ThemeService.instance = new ThemeService(api); - } - return ThemeService.instance; - } - - public async initialize(): Promise { - try { - 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), - }, - ); - - await this.loadTheme(updatedConfig); - } catch (error) { - console.error("Failed to update theme configuration:", error); - throw error; - } - } -} diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index 5f06ad4..4c5c2ef 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -1,7 +1,7 @@ import type { Config } from "tailwindcss"; export default { - content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"], + content: ["./app/**/*.{js,jsx,ts,tsx}"], theme: { extend: { fontFamily: { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index e4e8cef..c529d23 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,24 +1,43 @@ import { vitePlugin as remix } from "@remix-run/dev"; -import { defineConfig } from "vite"; +import { defineConfig, loadEnv } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; +import Routes from "~/routes" -declare module "@remix-run/node" { - interface Future { - v3_singleFetch: true; - } -} - -export default defineConfig({ - plugins: [ - remix({ - future: { - v3_fetcherPersist: true, - v3_relativeSplatPath: true, - v3_throwAbortReason: true, - v3_singleFetch: true, - v3_lazyRouteDiscovery: true, - }, - }), - tsconfigPaths(), - ], -}); +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ''); + return { + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + v3_singleFetch: true, + v3_lazyRouteDiscovery: true, + }, + routes: (defineRoutes) => { + return defineRoutes((route) => { + if (!env.VITE_INIT_STATUS) { + 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" }); + } + }); + } + }), + tsconfigPaths(), + ], + define: { + "import.meta.env.VITE_SYSTEM_STATUS": JSON.stringify(false), + "import.meta.env.VITE_SERVER_API": JSON.stringify("localhost:22000"), + "import.meta.env.VITE_SYSTEM_PORT": JSON.stringify(22100), + }, + server: { + port: Number(env.VITE_SYSTEM_PORT ?? 22100), + strictPort: true, + }, + }; +}); \ No newline at end of file diff --git a/src/hooks/useService.ts b/src/hooks/useService.ts new file mode 100644 index 0000000..76a0603 --- /dev/null +++ b/src/hooks/useService.ts @@ -0,0 +1,19 @@ +export function createServiceHook(name: string, getInstance: () => T) { + const Context = createContext(null); + + const Provider: FC = ({ children }) => ( + + {children} + + ); + + const useService = () => { + const service = useContext(Context); + if (!service) { + throw new Error(`use${name} must be used within ${name}Provider`); + } + return service; + }; + + return [Provider, useService] as const; +} \ No newline at end of file diff --git a/src/types/config.ts b/src/types/config.ts new file mode 100644 index 0000000..a21d158 --- /dev/null +++ b/src/types/config.ts @@ -0,0 +1,13 @@ +export type Json = + | null + | number + | string + | boolean + | { [key: string]: Json } + | Json[]; + +export type Config = Record; \ No newline at end of file diff --git a/src/types/plugin.ts b/src/types/plugin.ts new file mode 100644 index 0000000..c36fc09 --- /dev/null +++ b/src/types/plugin.ts @@ -0,0 +1,17 @@ +export interface PluginDefinition { + meta: { + name: string; + version: string; + displayName: string; + description?: string; + author?: string; + icon?: string; + }; + config?: Config; + routes: { + path: string; + description?: string; + }[]; + enabled: boolean; + managePath?: string; +} \ No newline at end of file