后端:新增主题配置获取接口。
前端:配置了默认环境变量值,创建自定义路由,将契约和服务合并到核心
This commit is contained in:
parent
dbdfbf5d8a
commit
b689233e91
@ -17,7 +17,7 @@ use std::sync::Arc;
|
|||||||
pub struct SystemConfigure {
|
pub struct SystemConfigure {
|
||||||
pub author_name: String,
|
pub author_name: String,
|
||||||
pub current_theme: String,
|
pub current_theme: String,
|
||||||
pub site_keyword: Vec<String>,
|
pub site_keyword: String,
|
||||||
pub site_description: String,
|
pub site_description: String,
|
||||||
pub admin_path: String,
|
pub admin_path: String,
|
||||||
}
|
}
|
||||||
@ -27,7 +27,7 @@ impl Default for SystemConfigure {
|
|||||||
Self {
|
Self {
|
||||||
author_name: "lsy".to_string(),
|
author_name: "lsy".to_string(),
|
||||||
current_theme: "default".to_string(),
|
current_theme: "default".to_string(),
|
||||||
site_keyword: vec!["echoes".to_string()],
|
site_keyword: "echoes".to_string(),
|
||||||
site_description: "echoes是一个高效、可扩展的博客平台".to_string(),
|
site_description: "echoes是一个高效、可扩展的博客平台".to_string(),
|
||||||
admin_path: "admin".to_string(),
|
admin_path: "admin".to_string(),
|
||||||
}
|
}
|
||||||
@ -91,8 +91,21 @@ pub async fn system_config_get(
|
|||||||
_token: SystemToken,
|
_token: SystemToken,
|
||||||
) -> AppResult<Json<Value>> {
|
) -> AppResult<Json<Value>> {
|
||||||
let sql = state.sql_get().await.into_app_result()?;
|
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
|
.await
|
||||||
.into_app_result()?;
|
.into_app_result()?;
|
||||||
Ok(configure)
|
Ok(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/theme/<name>")]
|
||||||
|
pub async fn theme_config_get(
|
||||||
|
state: &State<Arc<AppState>>,
|
||||||
|
_token: SystemToken,
|
||||||
|
name: String,
|
||||||
|
) -> AppResult<Json<Value>> {
|
||||||
|
let sql = state.sql_get().await.into_app_result()?;
|
||||||
|
let settings = get_setting(&sql, "theme".to_string(), name)
|
||||||
|
.await
|
||||||
|
.into_app_result()?;
|
||||||
|
Ok(settings)
|
||||||
}
|
}
|
8
frontend/app/env.d.ts
vendored
8
frontend/app/env.d.ts
vendored
@ -8,14 +8,10 @@
|
|||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_SERVER_API: string; // 用于访问API的基础URL
|
readonly VITE_SERVER_API: string; // 用于访问API的基础URL
|
||||||
readonly VITE_THEME_PATH: string; // 存储主题文件的目录路径
|
readonly VITE_SYSTEM_PORT: number; // 系统端口
|
||||||
readonly VITE_CONTENT_PATH: string; //mark文章存储的位置
|
|
||||||
readonly VITE_CONTENT_STATIC_PATH: string; //导出文章静态存储的位置
|
|
||||||
readonly VITE_PLUGINS_PATH: string; // 存储插件文件的目录路径
|
|
||||||
readonly VITE_ASSETS_PATH: string; // 存储静态资源的目录路径
|
|
||||||
VITE_SYSTEM_USERNAME: string; // 前端账号名称
|
VITE_SYSTEM_USERNAME: string; // 前端账号名称
|
||||||
VITE_SYSTEM_PASSWORD: string; // 前端账号密码
|
VITE_SYSTEM_PASSWORD: string; // 前端账号密码
|
||||||
VITE_SYSTEM_STATUS: boolean; // 系统是否进行安装
|
VITE_INIT_STATUS: boolean; // 系统是否进行安装
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
|
3
frontend/app/init.tsx
Normal file
3
frontend/app/init.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default function page(){
|
||||||
|
return <>安装中</>
|
||||||
|
}
|
@ -2,25 +2,13 @@ import {
|
|||||||
Links,
|
Links,
|
||||||
Meta,
|
Meta,
|
||||||
Outlet,
|
Outlet,
|
||||||
Scripts,
|
|
||||||
ScrollRestoration,
|
ScrollRestoration,
|
||||||
} from "@remix-run/react";
|
} 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 = () => [
|
import "~/tailwind.css";
|
||||||
{ 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",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Layout({ children }: { children: React.ReactNode }) {
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
@ -28,18 +16,23 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="generator" content="echoes" />
|
||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{children}
|
{children}
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
<Scripts />
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return <Outlet />;
|
return (
|
||||||
|
<BaseProvider>
|
||||||
|
<Layout>
|
||||||
|
<Outlet />
|
||||||
|
</Layout>
|
||||||
|
</BaseProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
15
frontend/app/routes.tsx
Normal file
15
frontend/app/routes.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import ReactDOMServer from 'react-dom/server';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
const MyComponent = () => {
|
||||||
|
return <div>Hello, World!</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default function Routes() {
|
||||||
|
const htmlString = ReactDOMServer.renderToString(<MyComponent />);
|
||||||
|
|
||||||
|
return (<div>安装重构</div>)
|
||||||
|
}
|
@ -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 (
|
|
||||||
<div className="flex h-screen items-center justify-center">
|
|
||||||
<div className="flex flex-col items-center gap-16">
|
|
||||||
<header className="flex flex-col items-center gap-9">
|
|
||||||
<h1 className="leading text-2xl font-bold text-gray-800 dark:text-gray-100">
|
|
||||||
Welcome to <span className="sr-only">Remix</span>
|
|
||||||
</h1>
|
|
||||||
<div className="h-[144px] w-[434px]">
|
|
||||||
<img
|
|
||||||
src="/logo-light.png"
|
|
||||||
alt="Remix"
|
|
||||||
className="block w-full dark:hidden"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
src="/logo-dark.png"
|
|
||||||
alt="Remix"
|
|
||||||
className="hidden w-full dark:block"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<nav className="flex flex-col items-center justify-center gap-4 rounded-3xl border border-gray-200 p-6 dark:border-gray-700">
|
|
||||||
<p className="leading-6 text-gray-700 dark:text-gray-200">
|
|
||||||
What's next?
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
{resources.map(({ href, text, icon }) => (
|
|
||||||
<li key={href}>
|
|
||||||
<a
|
|
||||||
className="group flex items-center gap-3 self-stretch p-3 leading-normal text-blue-700 hover:underline dark:text-blue-500"
|
|
||||||
href={href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
{text}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resources = [
|
|
||||||
{
|
|
||||||
href: "https://remix.run/start/quickstart",
|
|
||||||
text: "Quick Start (5 min)",
|
|
||||||
icon: (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="none"
|
|
||||||
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M8.51851 12.0741L7.92592 18L15.6296 9.7037L11.4815 7.33333L12.0741 2L4.37036 10.2963L8.51851 12.0741Z"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: "https://remix.run/start/tutorial",
|
|
||||||
text: "Tutorial (30 min)",
|
|
||||||
icon: (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="none"
|
|
||||||
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M4.561 12.749L3.15503 14.1549M3.00811 8.99944H1.01978M3.15503 3.84489L4.561 5.2508M8.3107 1.70923L8.3107 3.69749M13.4655 3.84489L12.0595 5.2508M18.1868 17.0974L16.635 18.6491C16.4636 18.8205 16.1858 18.8205 16.0144 18.6491L13.568 16.2028C13.383 16.0178 13.0784 16.0347 12.915 16.239L11.2697 18.2956C11.047 18.5739 10.6029 18.4847 10.505 18.142L7.85215 8.85711C7.75756 8.52603 8.06365 8.21994 8.39472 8.31453L17.6796 10.9673C18.0223 11.0653 18.1115 11.5094 17.8332 11.7321L15.7766 13.3773C15.5723 13.5408 15.5554 13.8454 15.7404 14.0304L18.1868 16.4767C18.3582 16.6481 18.3582 16.926 18.1868 17.0974Z"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: "https://remix.run/docs",
|
|
||||||
text: "Remix Docs",
|
|
||||||
icon: (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="none"
|
|
||||||
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M9.99981 10.0751V9.99992M17.4688 17.4688C15.889 19.0485 11.2645 16.9853 7.13958 12.8604C3.01467 8.73546 0.951405 4.11091 2.53116 2.53116C4.11091 0.951405 8.73546 3.01467 12.8604 7.13958C16.9853 11.2645 19.0485 15.889 17.4688 17.4688ZM2.53132 17.4688C0.951566 15.8891 3.01483 11.2645 7.13974 7.13963C11.2647 3.01471 15.8892 0.951453 17.469 2.53121C19.0487 4.11096 16.9854 8.73551 12.8605 12.8604C8.73562 16.9853 4.11107 19.0486 2.53132 17.4688Z"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: "https://rmx.as/discord",
|
|
||||||
text: "Join Discord",
|
|
||||||
icon: (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 20"
|
|
||||||
fill="none"
|
|
||||||
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M15.0686 1.25995L14.5477 1.17423L14.2913 1.63578C14.1754 1.84439 14.0545 2.08275 13.9422 2.31963C12.6461 2.16488 11.3406 2.16505 10.0445 2.32014C9.92822 2.08178 9.80478 1.84975 9.67412 1.62413L9.41449 1.17584L8.90333 1.25995C7.33547 1.51794 5.80717 1.99419 4.37748 2.66939L4.19 2.75793L4.07461 2.93019C1.23864 7.16437 0.46302 11.3053 0.838165 15.3924L0.868838 15.7266L1.13844 15.9264C2.81818 17.1714 4.68053 18.1233 6.68582 18.719L7.18892 18.8684L7.50166 18.4469C7.96179 17.8268 8.36504 17.1824 8.709 16.4944L8.71099 16.4904C10.8645 17.0471 13.128 17.0485 15.2821 16.4947C15.6261 17.1826 16.0293 17.8269 16.4892 18.4469L16.805 18.8725L17.3116 18.717C19.3056 18.105 21.1876 17.1751 22.8559 15.9238L23.1224 15.724L23.1528 15.3923C23.5873 10.6524 22.3579 6.53306 19.8947 2.90714L19.7759 2.73227L19.5833 2.64518C18.1437 1.99439 16.6386 1.51826 15.0686 1.25995ZM16.6074 10.7755L16.6074 10.7756C16.5934 11.6409 16.0212 12.1444 15.4783 12.1444C14.9297 12.1444 14.3493 11.6173 14.3493 10.7877C14.3493 9.94885 14.9378 9.41192 15.4783 9.41192C16.0471 9.41192 16.6209 9.93851 16.6074 10.7755ZM8.49373 12.1444C7.94513 12.1444 7.36471 11.6173 7.36471 10.7877C7.36471 9.94885 7.95323 9.41192 8.49373 9.41192C9.06038 9.41192 9.63892 9.93712 9.6417 10.7815C9.62517 11.6239 9.05462 12.1444 8.49373 12.1444Z"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
21
frontend/common/serializableType.ts
Normal file
21
frontend/common/serializableType.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export type Serializable =
|
||||||
|
| null
|
||||||
|
| number
|
||||||
|
| string
|
||||||
|
| boolean
|
||||||
|
| { [key: string]: Serializable }
|
||||||
|
| Array<Serializable>;
|
||||||
|
export interface Configuration {
|
||||||
|
[key: string]: {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
data: Serializable;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PathDescription {
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
@ -1,5 +0,0 @@
|
|||||||
export interface CapabilityProps<T> {
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
execute: (...args: any[]) => Promise<T>;
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
export type SerializeType =
|
|
||||||
| null
|
|
||||||
| number
|
|
||||||
| string
|
|
||||||
| boolean
|
|
||||||
| { [key: string]: SerializeType }
|
|
||||||
| Array<SerializeType>;
|
|
||||||
export interface Configuration {
|
|
||||||
[key: string]: {
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
data: SerializeType;
|
|
||||||
};
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}>;
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
export interface TemplateContract {
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
config: {
|
|
||||||
layout?: string;
|
|
||||||
styles?: string[];
|
|
||||||
scripts?: string[];
|
|
||||||
};
|
|
||||||
loader: () => Promise<void>;
|
|
||||||
element: () => React.ReactNode;
|
|
||||||
}
|
|
@ -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<string, ThemeTemplate>;
|
|
||||||
globalSettings?: {
|
|
||||||
layout?: string;
|
|
||||||
css?: string;
|
|
||||||
};
|
|
||||||
configuration: Configuration;
|
|
||||||
routes: {
|
|
||||||
index: string;
|
|
||||||
post: string;
|
|
||||||
tag: string;
|
|
||||||
category: string;
|
|
||||||
error: string;
|
|
||||||
loding: string;
|
|
||||||
page: Map<string, string>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ThemeTemplate {
|
|
||||||
path: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
@ -1,4 +1,9 @@
|
|||||||
import { CapabilityProps } from "contracts/capabilityContract";
|
export interface CapabilityProps<T> {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
execute: (...args: any[]) => Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export class CapabilityService {
|
export class CapabilityService {
|
||||||
private capabilities: Map<
|
private capabilities: Map<
|
@ -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 {
|
export class PluginManager {
|
||||||
private plugins: Map<string, PluginProps> = new Map();
|
private configurations: Map<string, PluginConfig> = new Map();
|
||||||
private configurations: Map<string, PluginConfiguration> = new Map();
|
|
||||||
private extensions: Map<string, ExtensionProps> = new Map();
|
|
||||||
|
|
||||||
async loadPlugins() {
|
async loadPlugins() {
|
||||||
const pluginDirs = await this.scanPluginDirectory();
|
const pluginDirs = await this.scanPluginDirectory();
|
||||||
@ -37,7 +51,7 @@ export class PluginManager {
|
|||||||
|
|
||||||
async getPluginConfig(
|
async getPluginConfig(
|
||||||
pluginName: string,
|
pluginName: string,
|
||||||
): Promise<PluginConfiguration | undefined> {
|
): Promise<PluginConfig | undefined> {
|
||||||
const dbConfig = await this.fetchConfigFromDB(pluginName);
|
const dbConfig = await this.fetchConfigFromDB(pluginName);
|
||||||
if (dbConfig) {
|
if (dbConfig) {
|
||||||
return dbConfig;
|
return dbConfig;
|
38
frontend/core/route.ts
Normal file
38
frontend/core/route.ts
Normal file
@ -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<string, RouteElement>();
|
||||||
|
private routesCache = new Map<string, string>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
98
frontend/core/theme.ts
Normal file
98
frontend/core/theme.ts
Normal file
@ -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<string, PathDescription>;
|
||||||
|
globalSettings?: {
|
||||||
|
layout?: string;
|
||||||
|
css?: string;
|
||||||
|
};
|
||||||
|
configuration: Configuration;
|
||||||
|
routes: {
|
||||||
|
index: string;
|
||||||
|
post: string;
|
||||||
|
tag: string;
|
||||||
|
category: string;
|
||||||
|
error: string;
|
||||||
|
loading: string;
|
||||||
|
page: Map<string, string>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export interface Template {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
config: {
|
||||||
|
layout?: string;
|
||||||
|
styles?: string[];
|
||||||
|
scripts?: string[];
|
||||||
|
};
|
||||||
|
loader: () => Promise<void>;
|
||||||
|
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<void> {
|
||||||
|
try {
|
||||||
|
const themeConfig = await this.api.request<ThemeConfig>(
|
||||||
|
"/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<ThemeConfig>): Promise<void> {
|
||||||
|
try {
|
||||||
|
const updatedConfig = await this.api.request<ThemeConfig>(
|
||||||
|
"/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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { CapabilityService } from "services/capabilityService";
|
import { CapabilityService } from "core/capability";
|
||||||
import { ThemeService } from "services/themeService";
|
import { ApiService } from "core/api";
|
||||||
import { ApiService } from "services/apiService";
|
import { RouteManager } from "core/route";
|
||||||
import { createServiceContext } from "hooks/createServiceContext";
|
import { createServiceContext } from "hooks/createServiceContext";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
@ -9,18 +9,18 @@ export const { CapabilityProvider, useCapability } = createServiceContext(
|
|||||||
() => CapabilityService.getInstance(),
|
() => CapabilityService.getInstance(),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const { ThemeProvider, useTheme } = createServiceContext("Theme", () =>
|
export const { RouteProvider, useRoute } = createServiceContext("Route", () =>
|
||||||
ThemeService.getInstance(),
|
RouteManager.getInstance(),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const { ApiProvider, useApi } = createServiceContext("Api", () =>
|
export const { ApiProvider, useApi } = createServiceContext("Api", () =>
|
||||||
ThemeService.getInstance(),
|
ApiService.getInstance(),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const ServiceProvider = ({ children }: { children: ReactNode }) => (
|
export const BaseProvider = ({ children }: { children: ReactNode }) => (
|
||||||
<ApiProvider>
|
<ApiProvider>
|
||||||
<CapabilityProvider>
|
<CapabilityProvider>
|
||||||
<ThemeProvider>{children}</ThemeProvider>
|
<RouteProvider>{children}</RouteProvider>
|
||||||
</CapabilityProvider>
|
</CapabilityProvider>
|
||||||
</ApiProvider>
|
</ApiProvider>
|
||||||
);
|
);
|
||||||
|
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.9 KiB |
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<void> {
|
|
||||||
try {
|
|
||||||
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),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.loadTheme(updatedConfig);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to update theme configuration:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"],
|
content: ["./app/**/*.{js,jsx,ts,tsx}"],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import { vitePlugin as remix } from "@remix-run/dev";
|
import { vitePlugin as remix } from "@remix-run/dev";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig, loadEnv } from "vite";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
import Routes from "~/routes"
|
||||||
|
|
||||||
declare module "@remix-run/node" {
|
export default defineConfig(({ mode }) => {
|
||||||
interface Future {
|
const env = loadEnv(mode, process.cwd(), '');
|
||||||
v3_singleFetch: true;
|
return {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [
|
plugins: [
|
||||||
remix({
|
remix({
|
||||||
future: {
|
future: {
|
||||||
@ -18,7 +15,29 @@ export default defineConfig({
|
|||||||
v3_singleFetch: true,
|
v3_singleFetch: true,
|
||||||
v3_lazyRouteDiscovery: 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(),
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
});
|
});
|
19
src/hooks/useService.ts
Normal file
19
src/hooks/useService.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export function createServiceHook<T>(name: string, getInstance: () => T) {
|
||||||
|
const Context = createContext<T | null>(null);
|
||||||
|
|
||||||
|
const Provider: FC<PropsWithChildren> = ({ children }) => (
|
||||||
|
<Context.Provider value={getInstance()}>
|
||||||
|
{children}
|
||||||
|
</Context.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
13
src/types/config.ts
Normal file
13
src/types/config.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export type Json =
|
||||||
|
| null
|
||||||
|
| number
|
||||||
|
| string
|
||||||
|
| boolean
|
||||||
|
| { [key: string]: Json }
|
||||||
|
| Json[];
|
||||||
|
|
||||||
|
export type Config = Record<string, {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
value: Json;
|
||||||
|
}>;
|
17
src/types/plugin.ts
Normal file
17
src/types/plugin.ts
Normal file
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user