更新系统配置,修改默认主题为"echoes";前端:新增默认错误,以模板和能力为核心,移除主题和插件服务,上下文服务统一方便实现互相调用的服务,
This commit is contained in:
parent
a43a9bac36
commit
610b3b5422
@ -25,7 +25,7 @@ impl Default for SystemConfigure {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
author_name: "lsy".to_string(),
|
||||
current_theme: "default".to_string(),
|
||||
current_theme: "echoes".to_string(),
|
||||
site_keyword: "echoes".to_string(),
|
||||
site_description: "echoes是一个高效、可扩展的博客平台".to_string(),
|
||||
admin_path: "admin".to_string(),
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { createContext, useState, useEffect } from "react";
|
||||
import {useHttp} from 'hooks/servicesProvider'
|
||||
import { message } from "hooks/message";
|
||||
import {DEFAULT_CONFIG} from "app/env"
|
||||
import { useHub } from "core/hub";
|
||||
|
||||
|
||||
interface SetupContextType {
|
||||
@ -94,7 +94,7 @@ const Introduction: React.FC<StepProps> = ({ onNext }) => (
|
||||
const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
const [dbType, setDbType] = useState("postgresql");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const api = useHttp();
|
||||
const http = useHub().http;
|
||||
|
||||
const validateForm = () => {
|
||||
const getRequiredFields = () => {
|
||||
@ -131,7 +131,7 @@ const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
default: return field;
|
||||
}
|
||||
});
|
||||
message.error(`请填写以下必填项:${fieldNames.join('、')}`, '验证失败');
|
||||
message.error(`请填写以下必填项:${fieldNames.join('、')}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -154,11 +154,9 @@ const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
db_name: (document.querySelector('[name="db_name"]') as HTMLInputElement)?.value?.trim()??"",
|
||||
};
|
||||
|
||||
await api.post('/sql', formData);
|
||||
|
||||
let oldEnv = import.meta.env?? DEFAULT_CONFIG
|
||||
|
||||
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;
|
||||
@ -166,25 +164,23 @@ const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
|
||||
|
||||
const newEnv = {
|
||||
...viteEnv,
|
||||
VITE_INIT_STATUS: '2'
|
||||
};
|
||||
|
||||
|
||||
await api.dev("/env", {
|
||||
await http.dev("/env", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(newEnv),
|
||||
});
|
||||
|
||||
Object.assign( newEnv)
|
||||
Object.assign(import.meta.env, newEnv);
|
||||
|
||||
message.success('数据库配置已保存', '配置成功');
|
||||
message.success('数据库配置成功!');
|
||||
setTimeout(() => onNext(), 1000);
|
||||
} catch (error: any) {
|
||||
console.error( error);
|
||||
message.error(error.message , error.title || '配置失败');
|
||||
message.error(error.message );
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -331,7 +327,7 @@ interface InstallReplyData {
|
||||
|
||||
const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const api = useHttp();
|
||||
const http = useHub().http;
|
||||
|
||||
const handleNext = async () => {
|
||||
setLoading(true);
|
||||
@ -342,7 +338,7 @@ const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
email: (document.querySelector('[name="admin_email"]') as HTMLInputElement)?.value,
|
||||
};
|
||||
|
||||
const response = await api.post('/administrator', formData) as InstallReplyData;
|
||||
const response = await http.post('/administrator', formData) as InstallReplyData;
|
||||
const data = response;
|
||||
|
||||
localStorage.setItem('token', data.token);
|
||||
@ -362,18 +358,18 @@ const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
VITE_API_PASSWORD: data.password
|
||||
};
|
||||
|
||||
|
||||
|
||||
await api.dev("/env", {
|
||||
await http.dev("/env", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(newEnv),
|
||||
});
|
||||
|
||||
message.success('管理员账号已创建,即将进入下一步', '创建成功');
|
||||
Object.assign(import.meta.env, newEnv);
|
||||
|
||||
message.success('管理员账号创建成功!');
|
||||
onNext();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
message.error(error.message, error.title || '创建失败');
|
||||
message.error(error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -392,7 +388,7 @@ const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
};
|
||||
|
||||
const SetupComplete: React.FC = () => {
|
||||
const api = useHttp();
|
||||
const http = useHub().http;
|
||||
|
||||
|
||||
|
||||
@ -458,9 +454,9 @@ const ThemeToggle: React.FC = () => {
|
||||
};
|
||||
|
||||
export default function SetupPage() {
|
||||
let step = Number(import.meta.env.VITE_INIT_STATUS)+1;
|
||||
|
||||
const [currentStep, setCurrentStep] = useState(step);
|
||||
const [currentStep, setCurrentStep] = useState(() => {
|
||||
return Number(import.meta.env.VITE_INIT_STATUS ?? 0) + 1;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen w-full bg-custom-bg-light dark:bg-custom-bg-dark">
|
||||
|
@ -6,8 +6,8 @@ import {
|
||||
ScrollRestoration,
|
||||
} from "@remix-run/react";
|
||||
|
||||
import { BaseProvider } from "hooks/servicesProvider";
|
||||
|
||||
import { HubProvider } from "core/hub";
|
||||
import { MessageProvider, MessageContainer } from "hooks/message";
|
||||
|
||||
import "~/index.css";
|
||||
|
||||
@ -16,42 +16,51 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="generator" content="echoes" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1"
|
||||
/>
|
||||
<meta
|
||||
name="generator"
|
||||
content="echoes"
|
||||
/>
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body suppressHydrationWarning={true}>
|
||||
<BaseProvider>
|
||||
<HubProvider>
|
||||
<MessageProvider>
|
||||
<MessageContainer />
|
||||
<Outlet />
|
||||
</BaseProvider>
|
||||
</MessageProvider>
|
||||
</HubProvider>
|
||||
|
||||
<ScrollRestoration />
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
function getInitialColorMode() {
|
||||
const persistedColorPreference = window.localStorage.getItem('theme');
|
||||
const hasPersistedPreference = typeof persistedColorPreference === 'string';
|
||||
if (hasPersistedPreference) {
|
||||
return persistedColorPreference;
|
||||
// 立即应用系统主题
|
||||
function applyTheme(isDark) {
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
}
|
||||
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const hasMediaQueryPreference = typeof mql.matches === 'boolean';
|
||||
if (hasMediaQueryPreference) {
|
||||
return mql.matches ? 'dark' : 'light';
|
||||
}
|
||||
return 'light';
|
||||
}
|
||||
const colorMode = getInitialColorMode();
|
||||
document.documentElement.classList.toggle('dark', colorMode === 'dark');
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
const newColorMode = e.matches ? 'dark' : 'light';
|
||||
document.documentElement.classList.toggle('dark', newColorMode === 'dark');
|
||||
localStorage.setItem('theme', newColorMode);
|
||||
// 获取系统主题并立即应用
|
||||
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
applyTheme(darkModeMediaQuery.matches);
|
||||
|
||||
// 添加主题变化监听
|
||||
try {
|
||||
// 现代浏览器的方式
|
||||
darkModeMediaQuery.addEventListener('change', (e) => {
|
||||
applyTheme(e.matches);
|
||||
});
|
||||
} catch (e) {
|
||||
// 兼容旧版浏览器
|
||||
darkModeMediaQuery.addListener((e) => {
|
||||
applyTheme(e.matches);
|
||||
});
|
||||
}
|
||||
})()
|
||||
`,
|
||||
}}
|
||||
|
@ -1,18 +1,16 @@
|
||||
import { useState } from "react";
|
||||
import { ErrorResponse } from "core/http";
|
||||
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
import { useHttp } from "hooks/servicesProvider";
|
||||
import ErrorPage from 'hooks/error';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const MyComponent = () => {
|
||||
return <div>Hello, World!</div>;
|
||||
};
|
||||
|
||||
export default function Routes() {
|
||||
let http=useHttp();
|
||||
|
||||
|
||||
return (<div>
|
||||
|
||||
</div>);
|
||||
return (
|
||||
<div>
|
||||
<ErrorPage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
34
frontend/core/hub.ts
Normal file
34
frontend/core/hub.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { CapabilityService } from "core/capability";
|
||||
import { HttpClient } from "core/http";
|
||||
import { RouteManager } from "core/route";
|
||||
import { createServiceContext } from "hooks/createServiceContext";
|
||||
|
||||
export default class ServerHub{
|
||||
private static instance: ServerHub;
|
||||
public http:HttpClient;
|
||||
public route:RouteManager;
|
||||
public capability:CapabilityService;
|
||||
|
||||
|
||||
private constructor(http:HttpClient,route:RouteManager,capability:CapabilityService){
|
||||
this.http=http;
|
||||
this.route=route;
|
||||
this.capability=capability;
|
||||
}
|
||||
|
||||
public static getInstance(): ServerHub {
|
||||
if (!this.instance) {
|
||||
this.instance = new ServerHub(
|
||||
HttpClient.getInstance(),
|
||||
RouteManager.getInstance(),
|
||||
CapabilityService.getInstance(),
|
||||
);
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const { HubProvider, useHub } = createServiceContext("Hub", () =>
|
||||
ServerHub.getInstance(),
|
||||
);
|
@ -1,64 +0,0 @@
|
||||
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 configurations: Map<string, PluginConfig> = new Map();
|
||||
|
||||
async loadPlugins() {
|
||||
const pluginDirs = await this.scanPluginDirectory();
|
||||
|
||||
for (const dir of pluginDirs) {
|
||||
try {
|
||||
const config = await import(`@/plugins/${dir}/plugin.config.ts`);
|
||||
const plugin: PluginProps = config.default;
|
||||
|
||||
this.plugins.set(plugin.name, plugin);
|
||||
|
||||
if (plugin.settingsSchema) {
|
||||
this.configurations.set(plugin.name, plugin.settingsSchema);
|
||||
}
|
||||
|
||||
if (plugin.extensions) {
|
||||
Object.entries(plugin.extensions).forEach(([key, value]) => {
|
||||
this.extensions.set(`${plugin.name}.${key}`, value.extension);
|
||||
});
|
||||
}
|
||||
|
||||
if (plugin.hooks?.onInstall) {
|
||||
await plugin.hooks.onInstall({});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load plugin from directory ${dir}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getPluginConfig(pluginName: string): Promise<PluginConfig | undefined> {
|
||||
const dbConfig = await this.fetchConfigFromDB(pluginName);
|
||||
if (dbConfig) {
|
||||
return dbConfig;
|
||||
}
|
||||
return this.configurations.get(pluginName);
|
||||
}
|
||||
|
||||
private async fetchConfigFromDB(pluginName: string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private async scanPluginDirectory(): Promise<string[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
export interface Template {
|
||||
name: string;
|
||||
description?: string;
|
||||
config: {
|
||||
layout?: string;
|
||||
styles?: string[];
|
||||
scripts?: string[];
|
||||
};
|
||||
loader: () => Promise<void>;
|
||||
element: () => React.ReactNode;
|
||||
}
|
@ -1,79 +0,0 @@
|
||||
import { Configuration, PathDescription } from "commons/serializableType";
|
||||
import { HttpClient } from "core/http";
|
||||
|
||||
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 class ThemeService {
|
||||
private static instance: ThemeService;
|
||||
private currentTheme?: ThemeConfig;
|
||||
private http: HttpClient;
|
||||
|
||||
private constructor(api: HttpClient) {
|
||||
this.http = api;
|
||||
}
|
||||
|
||||
public static getInstance(api?: HttpClient): ThemeService {
|
||||
if (!ThemeService.instance && api) {
|
||||
ThemeService.instance = new ThemeService(api);
|
||||
}
|
||||
return ThemeService.instance;
|
||||
}
|
||||
|
||||
public async getCurrentTheme(): Promise<void> {
|
||||
try {
|
||||
const themeConfig = await this.http.api<ThemeConfig>("/theme", {
|
||||
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>,
|
||||
name: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const updatedConfig = await this.http.api<ThemeConfig>(`/theme/`, {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
43
frontend/hooks/error.tsx
Normal file
43
frontend/hooks/error.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
const ErrorPage = () => {
|
||||
const [text, setText] = useState("");
|
||||
const fullText = "404 - 页面不见了 :(";
|
||||
const typingSpeed = 100;
|
||||
|
||||
useEffect(() => {
|
||||
let currentIndex = 0;
|
||||
const typingEffect = setInterval(() => {
|
||||
if (currentIndex < fullText.length) {
|
||||
setText(fullText.slice(0, currentIndex + 1));
|
||||
currentIndex++;
|
||||
} else {
|
||||
clearInterval(typingEffect);
|
||||
}
|
||||
}, typingSpeed);
|
||||
|
||||
return () => clearInterval(typingEffect);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-custom-bg-light dark:bg-custom-bg-dark transition-colors duration-300">
|
||||
<div className="text-center">
|
||||
<h1 className="text-6xl font-bold text-custom-title-light dark:text-custom-title-dark mb-4">
|
||||
{text}
|
||||
<span className="animate-pulse">|</span>
|
||||
</h1>
|
||||
<p className="text-custom-p-light dark:text-custom-p-dark text-xl">
|
||||
抱歉,您访问的页面已经离家出走了
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.href = '/'}
|
||||
className="mt-8 px-6 py-3 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-colors duration-300"
|
||||
>
|
||||
返回首页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorPage;
|
101
frontend/hooks/loading.tsx
Normal file
101
frontend/hooks/loading.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import React, { createContext, useState, useContext } from "react";
|
||||
|
||||
interface LoadingContextType {
|
||||
isLoading: boolean;
|
||||
showLoading: () => void;
|
||||
hideLoading: () => void;
|
||||
}
|
||||
|
||||
const LoadingContext = createContext<LoadingContextType>({
|
||||
isLoading: false,
|
||||
showLoading: () => {},
|
||||
hideLoading: () => {},
|
||||
});
|
||||
|
||||
export const LoadingProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const showLoading = () => setIsLoading(true);
|
||||
const hideLoading = () => setIsLoading(false);
|
||||
|
||||
return (
|
||||
<LoadingContext.Provider value={{ isLoading, showLoading, hideLoading }}>
|
||||
{children}
|
||||
{isLoading && (
|
||||
<div className="fixed inset-0 flex flex-col items-center justify-center bg-black/25 dark:bg-black/40 z-[999999]">
|
||||
<div className="loading-spinner mb-2" />
|
||||
<div className="text-custom-p-light dark:text-custom-p-dark text-sm">加载中...</div>
|
||||
</div>
|
||||
)}
|
||||
<style>{`
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 3px solid rgba(59, 130, 246, 0.2);
|
||||
border-radius: 50%;
|
||||
border-top-color: #3B82F6;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.dark .loading-spinner {
|
||||
border: 3px solid rgba(96, 165, 250, 0.2);
|
||||
border-top-color: #60A5FA;
|
||||
}
|
||||
`}</style>
|
||||
</LoadingContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// 全局loading实例
|
||||
let globalShowLoading: (() => void) | null = null;
|
||||
let globalHideLoading: (() => void) | null = null;
|
||||
|
||||
export const LoadingContainer: React.FC = () => {
|
||||
const { showLoading, hideLoading } = useContext(LoadingContext);
|
||||
|
||||
React.useEffect(() => {
|
||||
globalShowLoading = showLoading;
|
||||
globalHideLoading = hideLoading;
|
||||
return () => {
|
||||
globalShowLoading = null;
|
||||
globalHideLoading = null;
|
||||
};
|
||||
}, [showLoading, hideLoading]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// 导出loading方法
|
||||
export const loading = {
|
||||
show: () => {
|
||||
if (!globalShowLoading) {
|
||||
console.warn("Loading system not initialized");
|
||||
return;
|
||||
}
|
||||
globalShowLoading();
|
||||
},
|
||||
hide: () => {
|
||||
if (!globalHideLoading) {
|
||||
console.warn("Loading system not initialized");
|
||||
return;
|
||||
}
|
||||
globalHideLoading();
|
||||
},
|
||||
};
|
@ -8,11 +8,6 @@ interface Message {
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
interface MessageOptions {
|
||||
content: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
interface MessageContextType {
|
||||
messages: Message[];
|
||||
addMessage: (
|
||||
|
@ -1,29 +0,0 @@
|
||||
import { CapabilityService } from "core/capability";
|
||||
import { HttpClient } from "core/http";
|
||||
import { RouteManager } from "core/route";
|
||||
import { createServiceContext } from "hooks/createServiceContext";
|
||||
import { ReactNode } from "react";
|
||||
import { StyleProvider } from "hooks/stylesProvider";
|
||||
|
||||
export const { CapabilityProvider, useCapability } = createServiceContext(
|
||||
"Capability",
|
||||
() => CapabilityService.getInstance(),
|
||||
);
|
||||
|
||||
export const { RouteProvider, useRoute } = createServiceContext("Route", () =>
|
||||
RouteManager.getInstance(),
|
||||
);
|
||||
|
||||
export const { HttpProvider, useHttp } = createServiceContext("Http", () =>
|
||||
HttpClient.getInstance(),
|
||||
);
|
||||
|
||||
export const BaseProvider = ({ children }: { children: ReactNode }) => (
|
||||
<HttpProvider>
|
||||
<CapabilityProvider>
|
||||
<StyleProvider>
|
||||
<RouteProvider>{children}</RouteProvider>
|
||||
</StyleProvider>
|
||||
</CapabilityProvider>
|
||||
</HttpProvider>
|
||||
);
|
@ -1,10 +0,0 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { MessageProvider, MessageContainer } from "hooks/message";
|
||||
|
||||
export const StyleProvider = ({ children }: { children: ReactNode }) => (
|
||||
<MessageProvider>
|
||||
<MessageContainer />
|
||||
{children}
|
||||
</MessageProvider>
|
||||
);
|
14
frontend/interface/plugin.ts
Normal file
14
frontend/interface/plugin.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Configuration, PathDescription } from "commons/serializableType";
|
||||
|
||||
export interface PluginConfig {
|
||||
name: string;
|
||||
version: string;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
enabled: boolean;
|
||||
icon?: string;
|
||||
managePath?: string;
|
||||
configuration?: Configuration;
|
||||
routes: Set<PathDescription>;
|
||||
}
|
25
frontend/interface/template.ts
Normal file
25
frontend/interface/template.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { HttpClient } from 'core/http';
|
||||
import { CapabilityService } from 'core/capability';
|
||||
|
||||
export class Template {
|
||||
constructor(
|
||||
public name: string,
|
||||
public config: {
|
||||
layout?: string;
|
||||
styles?: string[];
|
||||
scripts?: string[];
|
||||
},
|
||||
public element: (services: {
|
||||
http: HttpClient;
|
||||
capability: CapabilityService;
|
||||
}) => React.ReactNode,
|
||||
public description?: string,
|
||||
) {}
|
||||
|
||||
render(services: {
|
||||
http: HttpClient;
|
||||
capability: CapabilityService;
|
||||
}) {
|
||||
return this.element(services);
|
||||
}
|
||||
}
|
24
frontend/interface/theme.ts
Normal file
24
frontend/interface/theme.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Configuration, PathDescription } from "commons/serializableType";
|
||||
|
||||
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: {
|
||||
article:string;
|
||||
post: string;
|
||||
tag: string;
|
||||
category: string;
|
||||
error: string;
|
||||
page: Map<string, string>;
|
||||
};
|
||||
}
|
@ -12,9 +12,6 @@
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.2",
|
||||
"@remix-run/node": "^2.14.0",
|
||||
"@remix-run/react": "^2.14.0",
|
||||
"@remix-run/serve": "^2.14.0",
|
||||
|
@ -29,7 +29,7 @@ export default {
|
||||
title: {
|
||||
light: "#111827",
|
||||
dark: "#F8FAFC", // 更亮的标题颜色
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
|
15
frontend/themes/echoes/templates/page.tsx
Normal file
15
frontend/themes/echoes/templates/page.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { Template } from 'core/template';
|
||||
|
||||
export default new Template(
|
||||
"page",
|
||||
{
|
||||
layout: "default",
|
||||
},
|
||||
({ http }) => {
|
||||
return (
|
||||
<div>
|
||||
Hello World
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
@ -1,12 +1,17 @@
|
||||
import { ThemeConfig } from "contracts/themeContract";
|
||||
import { ThemeConfig } from "interface/theme";
|
||||
|
||||
export const themeConfig: ThemeConfig = {
|
||||
name: "default",
|
||||
const themeConfig: ThemeConfig = {
|
||||
name: "echoes",
|
||||
displayName: "默认主题",
|
||||
version: "1.0.0",
|
||||
description: "一个简约风格的博客主题",
|
||||
author: "lsy",
|
||||
entry: "default",
|
||||
configuration: {
|
||||
|
||||
},
|
||||
globalSettings:{
|
||||
layout:"layout.tsx"
|
||||
},
|
||||
templates: new Map([
|
||||
[
|
||||
"page",
|
||||
@ -19,9 +24,15 @@ export const themeConfig: ThemeConfig = {
|
||||
]),
|
||||
|
||||
routes: {
|
||||
article:"",
|
||||
post: "",
|
||||
tag: "",
|
||||
category: "",
|
||||
page: "",
|
||||
error: "",
|
||||
page: new Map<string, string>([
|
||||
|
||||
]),
|
||||
},
|
||||
};
|
||||
|
||||
export default themeConfig;
|
Loading…
Reference in New Issue
Block a user