更新系统配置,修改默认主题为"echoes";前端:新增默认错误,以模板和能力为核心,移除主题和插件服务,上下文服务统一方便实现互相调用的服务,
This commit is contained in:
parent
a43a9bac36
commit
610b3b5422
@ -25,7 +25,7 @@ impl Default for SystemConfigure {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
author_name: "lsy".to_string(),
|
author_name: "lsy".to_string(),
|
||||||
current_theme: "default".to_string(),
|
current_theme: "echoes".to_string(),
|
||||||
site_keyword: "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(),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { createContext, useState, useEffect } from "react";
|
import React, { createContext, useState, useEffect } from "react";
|
||||||
import {useHttp} from 'hooks/servicesProvider'
|
import { message } from "hooks/message";
|
||||||
import { message} from "hooks/message";
|
|
||||||
import {DEFAULT_CONFIG} from "app/env"
|
import {DEFAULT_CONFIG} from "app/env"
|
||||||
|
import { useHub } from "core/hub";
|
||||||
|
|
||||||
|
|
||||||
interface SetupContextType {
|
interface SetupContextType {
|
||||||
@ -94,7 +94,7 @@ const Introduction: React.FC<StepProps> = ({ onNext }) => (
|
|||||||
const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||||
const [dbType, setDbType] = useState("postgresql");
|
const [dbType, setDbType] = useState("postgresql");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const api = useHttp();
|
const http = useHub().http;
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const getRequiredFields = () => {
|
const getRequiredFields = () => {
|
||||||
@ -131,7 +131,7 @@ const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
|||||||
default: return field;
|
default: return field;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
message.error(`请填写以下必填项:${fieldNames.join('、')}`, '验证失败');
|
message.error(`请填写以下必填项:${fieldNames.join('、')}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -154,37 +154,33 @@ const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
|||||||
db_name: (document.querySelector('[name="db_name"]') as HTMLInputElement)?.value?.trim()??"",
|
db_name: (document.querySelector('[name="db_name"]') as HTMLInputElement)?.value?.trim()??"",
|
||||||
};
|
};
|
||||||
|
|
||||||
await api.post('/sql', formData);
|
await http.post('/sql', formData);
|
||||||
|
|
||||||
let oldEnv = import.meta.env?? DEFAULT_CONFIG
|
|
||||||
|
|
||||||
|
|
||||||
|
let oldEnv = import.meta.env ?? DEFAULT_CONFIG;
|
||||||
const viteEnv = Object.entries(oldEnv).reduce((acc, [key, value]) => {
|
const viteEnv = Object.entries(oldEnv).reduce((acc, [key, value]) => {
|
||||||
if (key.startsWith('VITE_')) {
|
if (key.startsWith('VITE_')) {
|
||||||
acc[key] = value;
|
acc[key] = value;
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, any>);
|
}, {} as Record<string, any>);
|
||||||
|
|
||||||
|
|
||||||
const newEnv = {
|
const newEnv = {
|
||||||
...viteEnv,
|
...viteEnv,
|
||||||
VITE_INIT_STATUS: '2'
|
VITE_INIT_STATUS: '2'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await http.dev("/env", {
|
||||||
await api.dev("/env", {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(newEnv),
|
body: JSON.stringify(newEnv),
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.assign( newEnv)
|
Object.assign(import.meta.env, newEnv);
|
||||||
|
|
||||||
message.success('数据库配置已保存', '配置成功');
|
message.success('数据库配置成功!');
|
||||||
setTimeout(() => onNext(), 1000);
|
setTimeout(() => onNext(), 1000);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error( error);
|
console.error( error);
|
||||||
message.error(error.message , error.title || '配置失败');
|
message.error(error.message );
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -331,7 +327,7 @@ interface InstallReplyData {
|
|||||||
|
|
||||||
const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
|
const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const api = useHttp();
|
const http = useHub().http;
|
||||||
|
|
||||||
const handleNext = async () => {
|
const handleNext = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -342,7 +338,7 @@ const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
|
|||||||
email: (document.querySelector('[name="admin_email"]') as HTMLInputElement)?.value,
|
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;
|
const data = response;
|
||||||
|
|
||||||
localStorage.setItem('token', data.token);
|
localStorage.setItem('token', data.token);
|
||||||
@ -358,22 +354,22 @@ const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
|
|||||||
const newEnv = {
|
const newEnv = {
|
||||||
...viteEnv,
|
...viteEnv,
|
||||||
VITE_INIT_STATUS: '3',
|
VITE_INIT_STATUS: '3',
|
||||||
VITE_API_USERNAME:data.username,
|
VITE_API_USERNAME: data.username,
|
||||||
VITE_API_PASSWORD:data.password
|
VITE_API_PASSWORD: data.password
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await http.dev("/env", {
|
||||||
|
|
||||||
await api.dev("/env", {
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(newEnv),
|
body: JSON.stringify(newEnv),
|
||||||
});
|
});
|
||||||
|
|
||||||
message.success('管理员账号已创建,即将进入下一步', '创建成功');
|
Object.assign(import.meta.env, newEnv);
|
||||||
|
|
||||||
|
message.success('管理员账号创建成功!');
|
||||||
onNext();
|
onNext();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
message.error(error.message, error.title || '创建失败');
|
message.error(error.message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -392,7 +388,7 @@ const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SetupComplete: React.FC = () => {
|
const SetupComplete: React.FC = () => {
|
||||||
const api = useHttp();
|
const http = useHub().http;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -458,9 +454,9 @@ const ThemeToggle: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function SetupPage() {
|
export default function SetupPage() {
|
||||||
let step = Number(import.meta.env.VITE_INIT_STATUS)+1;
|
const [currentStep, setCurrentStep] = useState(() => {
|
||||||
|
return Number(import.meta.env.VITE_INIT_STATUS ?? 0) + 1;
|
||||||
const [currentStep, setCurrentStep] = useState(step);
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen w-full bg-custom-bg-light dark:bg-custom-bg-dark">
|
<div className="relative min-h-screen w-full bg-custom-bg-light dark:bg-custom-bg-dark">
|
||||||
|
@ -6,8 +6,8 @@ import {
|
|||||||
ScrollRestoration,
|
ScrollRestoration,
|
||||||
} from "@remix-run/react";
|
} from "@remix-run/react";
|
||||||
|
|
||||||
import { BaseProvider } from "hooks/servicesProvider";
|
import { HubProvider } from "core/hub";
|
||||||
|
import { MessageProvider, MessageContainer } from "hooks/message";
|
||||||
|
|
||||||
import "~/index.css";
|
import "~/index.css";
|
||||||
|
|
||||||
@ -16,47 +16,56 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta
|
||||||
<meta name="generator" content="echoes" />
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="generator"
|
||||||
|
content="echoes"
|
||||||
|
/>
|
||||||
<Meta />
|
<Meta />
|
||||||
<Links />
|
<Links />
|
||||||
</head>
|
</head>
|
||||||
<body suppressHydrationWarning={true}>
|
<body suppressHydrationWarning={true}>
|
||||||
<BaseProvider>
|
<HubProvider>
|
||||||
|
<MessageProvider>
|
||||||
|
<MessageContainer />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</BaseProvider>
|
</MessageProvider>
|
||||||
|
</HubProvider>
|
||||||
|
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `
|
__html: `
|
||||||
(function() {
|
(function() {
|
||||||
function getInitialColorMode() {
|
// 立即应用系统主题
|
||||||
const persistedColorPreference = window.localStorage.getItem('theme');
|
function applyTheme(isDark) {
|
||||||
const hasPersistedPreference = typeof persistedColorPreference === 'string';
|
document.documentElement.classList.toggle('dark', isDark);
|
||||||
if (hasPersistedPreference) {
|
}
|
||||||
return persistedColorPreference;
|
|
||||||
}
|
|
||||||
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';
|
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
document.documentElement.classList.toggle('dark', newColorMode === 'dark');
|
applyTheme(darkModeMediaQuery.matches);
|
||||||
localStorage.setItem('theme', newColorMode);
|
|
||||||
});
|
// 添加主题变化监听
|
||||||
})()
|
try {
|
||||||
`,
|
// 现代浏览器的方式
|
||||||
}}
|
darkModeMediaQuery.addEventListener('change', (e) => {
|
||||||
/>
|
applyTheme(e.matches);
|
||||||
<Scripts />
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// 兼容旧版浏览器
|
||||||
|
darkModeMediaQuery.addListener((e) => {
|
||||||
|
applyTheme(e.matches);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Scripts />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
import { useState } from "react";
|
import ErrorPage from 'hooks/error';
|
||||||
import { ErrorResponse } from "core/http";
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import ReactDOMServer from "react-dom/server";
|
|
||||||
import { useHttp } from "hooks/servicesProvider";
|
|
||||||
|
|
||||||
const MyComponent = () => {
|
const MyComponent = () => {
|
||||||
return <div>Hello, World!</div>;
|
return <div>Hello, World!</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Routes() {
|
export default function Routes() {
|
||||||
let http=useHttp();
|
|
||||||
|
|
||||||
|
|
||||||
return (<div>
|
return (
|
||||||
|
<div>
|
||||||
</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;
|
duration?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MessageOptions {
|
|
||||||
content: string;
|
|
||||||
duration?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MessageContextType {
|
interface MessageContextType {
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
addMessage: (
|
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"
|
"typecheck": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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/node": "^2.14.0",
|
||||||
"@remix-run/react": "^2.14.0",
|
"@remix-run/react": "^2.14.0",
|
||||||
"@remix-run/serve": "^2.14.0",
|
"@remix-run/serve": "^2.14.0",
|
||||||
|
@ -29,7 +29,7 @@ export default {
|
|||||||
title: {
|
title: {
|
||||||
light: "#111827",
|
light: "#111827",
|
||||||
dark: "#F8FAFC", // 更亮的标题颜色
|
dark: "#F8FAFC", // 更亮的标题颜色
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
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 = {
|
const themeConfig: ThemeConfig = {
|
||||||
name: "default",
|
name: "echoes",
|
||||||
displayName: "默认主题",
|
displayName: "默认主题",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
description: "一个简约风格的博客主题",
|
description: "一个简约风格的博客主题",
|
||||||
author: "lsy",
|
author: "lsy",
|
||||||
entry: "default",
|
configuration: {
|
||||||
|
|
||||||
|
},
|
||||||
|
globalSettings:{
|
||||||
|
layout:"layout.tsx"
|
||||||
|
},
|
||||||
templates: new Map([
|
templates: new Map([
|
||||||
[
|
[
|
||||||
"page",
|
"page",
|
||||||
@ -19,9 +24,15 @@ export const themeConfig: ThemeConfig = {
|
|||||||
]),
|
]),
|
||||||
|
|
||||||
routes: {
|
routes: {
|
||||||
|
article:"",
|
||||||
post: "",
|
post: "",
|
||||||
tag: "",
|
tag: "",
|
||||||
category: "",
|
category: "",
|
||||||
page: "",
|
error: "",
|
||||||
|
page: new Map<string, string>([
|
||||||
|
|
||||||
|
]),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default themeConfig;
|
Loading…
Reference in New Issue
Block a user