前端:实现extension接口,后端:修复数据库字段错误

This commit is contained in:
lsy 2024-11-14 01:44:26 +08:00
parent 86ad0fdb29
commit 2b44435c2a
14 changed files with 352 additions and 90 deletions

View File

@ -7,11 +7,11 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto;
--- 用户权限枚举
CREATE TYPE privilege_level AS ENUM ('visitor', 'contributor', 'administrators');
--- 用户信息表
CREATE TABLE person
CREATE TABLE persons
(
person_name VARCHAR(100) PRIMARY KEY, --- 用户名
person_email VARCHAR(255) UNIQUE NOT NULL, --- 用户邮箱
person_picture VARCHAR(255), --- 用户头像
person_icon VARCHAR(255), --- 用户头像
person_password VARCHAR(255) NOT NULL, --- 用户密码
person_created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, --- 用户创建时间
person_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, --- 用户更新时间
@ -28,7 +28,7 @@ CREATE TABLE pages
page_id SERIAL PRIMARY KEY, --- 独立页面唯一id主键
page_meta_keywords VARCHAR(255) NOT NULL, ---mata关键字
page_meta_description VARCHAR(255) NOT NULL, ---mata描述
post_title VARCHAR(255) NOT NULL, --- 文章标题
page_title VARCHAR(255) NOT NULL, --- 文章标题
page_content TEXT NOT NULL, --- 独立页面内容
page_mould VARCHAR(50), --- 独立页面模板名称
page_fields JSON, --- 自定义字段
@ -47,8 +47,8 @@ CREATE TABLE posts
post_content TEXT NOT NULL, --- 文章内容
post_status publication_status DEFAULT 'draft', --- 文章状态
post_editor BOOLEAN DEFAULT FALSE, --- 文章是否编辑未保存
posts_unsaved_content TEXT, --- 未保存的文章
posts_path VARCHAR(255), --- 文章路径
post_unsaved_content TEXT, --- 未保存的文章
post_path VARCHAR(255), --- 文章路径
post_created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, --- 文章创建时间
post_updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, --- 文章更新时间
post_published_at TIMESTAMP, --- 文章发布时间
@ -58,7 +58,7 @@ CREATE TABLE posts
CREATE TABLE tags
(
tag_name VARCHAR(50) PRIMARY KEY CHECK (LOWER(tag_name) = tag_name), --- 标签名称主键
tag_picture VARCHAR(255) --- 标签图标
tag_icon VARCHAR(255) --- 标签图标
);
--- 文章与标签的关系表
CREATE TABLE post_tags

View File

@ -7,10 +7,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TYPE: boolean; //用于判断是动态博客还是静态博客
readonly VITE_APP_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; // 存储静态资源的目录路径
}

View File

@ -1,40 +1,25 @@
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import type { LinksFunction } from "@remix-run/node";
import { Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";
import type { LoaderFunction } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { ThemeProvider } from "hooks/themeContext";
import { createContext, useContext, ReactNode } from 'react';
import "./tailwind.css";
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",
},
];
export function Layout({ children }: { children: React.ReactNode }) {
console.log(import.meta.env.VITE_THEME_NAME);
return (
<html lang="en">
<head>
<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 />
<Links />
</head>
<body>
<ThemeProvider>
{children}
</ThemeProvider>
<ScrollRestoration />
<Scripts />
</body>
@ -43,5 +28,9 @@ export function Layout({ children }: { children: React.ReactNode }) {
}
export default function App() {
return <Outlet />;
return (
<Layout>
<Outlet />
</Layout>
);
}

View File

@ -0,0 +1,23 @@
// service/theme/themeContext.tsx
import { createContext, useContext, ReactNode } from 'react';
import { ThemeService } from 'service/themeService';
const ThemeContext = createContext<ThemeService | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const themeService = ThemeService.getInstance();
return (
<ThemeContext.Provider value={themeService}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme(): ThemeService {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

View File

@ -0,0 +1,84 @@
// File path: service/extensionService.ts
/**
* ExtensionManage
*
*/
import { ExtensionType } from "types/extensionType";
import React from "react";
class ExtensionManage {
/** 存储扩展的映射,键为扩展名称,值为插件名称和扩展的集合 */
private extensions: Map<string, Set<{ pluginName: string; extension: ExtensionType }>> = new Map();
/** ExtensionManage 的唯一实例 */
private static instance: ExtensionManage;
/** 私有构造函数,防止外部实例化 */
private constructor() {}
/**
* ExtensionManage
* @returns {ExtensionManage} ExtensionManage
*/
public static getInstance(): ExtensionManage {
if (!this.instance) {
this.instance = new ExtensionManage();
}
return this.instance;
}
/** 注册扩展 */
private register(extensionName: string, pluginName: string, extension: ExtensionType) {
const handlers = this.extensions.get(extensionName) || new Set();
handlers.add({ pluginName, extension });
this.extensions.set(extensionName, handlers);
}
/** 执行扩展方法 */
private executeExtensionMethod<T>(extensionName: string, method: keyof ExtensionType, ...args: any[]): Set<T> {
const result = new Set<T>();
const handlers = this.extensions.get(extensionName);
if (handlers) {
handlers.forEach(({ extension }) => {
const methodFunction = extension[method];
if (methodFunction) {
try {
const value = methodFunction(...args);
if (value && (typeof value === 'string' || React.isValidElement(value))) {
result.add(value as T);
}
} catch (error) {
console.error(`Error executing hook ${extensionName}:`, error);
}
}
});
}
return result;
}
/** 触发扩展的动作 */
private triggerAction(extensionName: string, ...args: any[]): void {
this.executeExtensionMethod<void>(extensionName, 'action', ...args);
}
/** 触发扩展的组件 */
private triggerComponent(extensionName: string, ...args: any[]): Set<React.FC> {
return this.executeExtensionMethod<React.FC>(extensionName, 'component', ...args);
}
/** 触发扩展的文本 */
private triggerText(extensionName: string, ...args: any[]): Set<string> {
return this.executeExtensionMethod<string>(extensionName, 'text', ...args);
}
/** 移除指定插件的扩展 */
private removePluginExtensions(pluginName: string) {
this.extensions.forEach((handlers, extensionName) => {
const newHandlers = new Set(
Array.from(handlers).filter(handler => handler.pluginName !== pluginName)
);
this.extensions.set(extensionName, newHandlers);
});
}
}

View File

@ -0,0 +1,24 @@
import { PluginConfig ,PluginType ,PluginConfiguration} from "types/pluginType";
export class PluginService {
private static pluginInstance: PluginService | null = null; // 单例实例
private pluginComponents: Map<PluginType, Set<{
name:string,
configuration?:PluginConfiguration,
managePath?: string,
}>> = new Map(); // 插件组件缓存
private constructor (){};
public static getInstance(): PluginService {
if (!this.pluginInstance) {
this.pluginInstance = new PluginService();
}
return this.pluginInstance;
}
}

View File

@ -0,0 +1,52 @@
// service/theme/themeService.ts
import type { ThemeConfig } from 'types/themeType';
export class ThemeService {
private static themeInstance: ThemeService; // 单例实例
private themeConfig: ThemeConfig | null = null; // 当前主题配置
private themeComponents: Map<string, React.ComponentType> = new Map(); // 主题组件缓存
private constructor() { } // 私有构造函数,防止外部实例化
// 获取单例实例
public static getInstance(): ThemeService {
if (!ThemeService.themeInstance) {
ThemeService.themeInstance = new ThemeService();
}
return ThemeService.themeInstance;
}
// 加载主题
async loadTheme(themeConfig: ThemeConfig): Promise<void> {
this.themeConfig = themeConfig; // 设置当前主题
await this.loadThemeComponents(themeConfig); // 加载主题组件
}
// 加载主题组件
private async loadThemeComponents(config:ThemeConfig): Promise<void> {
// 清除现有组件缓存
this.themeComponents.clear();
// 动态导入主题入口组件
const entryComponent = await import(config.entry);
this.themeComponents.set('entry', entryComponent.default); // 缓存入口路径
// 加载所有模板组件
for (const [key, template] of config.templates.entries()) {
const component = await import(template.path);
this.themeComponents.set(key, component.default); // 缓存模板组件
}
}
// 获取指定模板名称的组件
getComponent(templateName: string): React.ComponentType | null {
return this.themeComponents.get(templateName) || null; // 返回组件或null
}
// 获取当前主题配置
getCurrentTheme(): ThemeConfig | null {
return this.currentTheme; // 返回当前主题配置
}
}

View File

@ -0,0 +1,25 @@
import { ThemeConfig } from "types/themeType";
export const themeConfig: ThemeConfig = {
name: 'default',
displayName: '默认主题',
version: '1.0.0',
description: '一个简约风格的博客主题',
author: 'lsy',
entry: './index.tsx',
templates: new Map([
['page', {
path: './templates/page',
name: '文章列表模板',
description: '博客首页展示模板'
}],
]),
settingsSchema: undefined,
routes: {
post: "",
tag: "",
category: "",
page: ""
}
}

View File

@ -25,6 +25,8 @@
"paths": {
"theme/*":["./theme/*"],
"types/*":["./types/*"],
"service/*":["./service/*"],
"hooks/*":["./hooks/*"],
},
// Vite takes care of building everything, not tsc.

View File

@ -0,0 +1,20 @@
/**
* File path: types/extensionType.ts
*
* ExtensionType
*
*
* - action: 可选的操作函数 void
* - component: 可选的组件函数 React
* - text: 可选的文本生成函数
*/
export interface ExtensionType {
/** 可选的操作函数,接受任意参数并返回 void */
action?: (...args: any[]) => void;
/** 可选的组件函数,接受任意参数并返回一个 React 组件 */
component?: (...args: any[]) => React.FC;
/** 可选的文本生成函数,接受任意参数并返回一个字符串 */
text?: (...args: any[]) => string;
}

View File

@ -1,41 +0,0 @@
// File path: types/plugin.ts
/**
*
*
*
*
*/
export interface PluginConfig {
name: string; // 插件名称
version: string; // 插件版本
displayName: string; // 插件显示名称
description?: string; // 插件描述(可选)
author?: string; // 插件作者(可选)
enabled: boolean; // 插件是否启用
icon?: string; // 插件图标URL可选
managePath?: string; // 插件管理页面路径(可选)
entry: string; // 插件入口组件路径
// 主题配置
settingsSchema?: {
type: string; // 配置类型
properties: Record<string, {
type: string; // 属性类型
title: string; // 属性标题
description?: string; // 属性描述(可选)
data?: any; // 额外数据(可选)
}>;
};
// 依赖
dependencies?: {
plugins?: string[]; // 依赖的插件列表(可选)
themes?: string[]; // 依赖的主题列表(可选)
};
// 插件生命周期钩子
hooks?: {
onInstall?: string; // 安装时调用的钩子(可选)
onUninstall?: string; // 卸载时调用的钩子(可选)
onEnable?: string; // 启用时调用的钩子(可选)
onDisable?: string; // 禁用时调用的钩子(可选)
};
}

View File

@ -0,0 +1,48 @@
// File path: types/pluginType.ts
/**
*
*
*
*
*/
export interface PluginConfig {
name: string; // 插件名称
version: string; // 插件版本
displayName: string; // 插件显示名称
description?: string; // 插件描述(可选)
author?: string; // 插件作者(可选)
enabled: boolean; // 插件是否启用
icon?: string; // 插件图标URL可选
managePath?: string; // 插件管理页面路径(可选)
configuration?: PluginConfiguration; // 插件配置
dependencies?: {
plugins?: string[]; // 依赖的插件列表(可选)
themes?: string[]; // 依赖的主题列表(可选)
};
hooks?: {
onInstall?: (context: any) => {}; // 安装时调用的钩子(可选)
onUninstall?: (context: any) => {}; // 卸载时调用的钩子(可选)
onEnable?: (context: any) => {}; // 启用时调用的钩子(可选)
onDisable?: (context: any) => {}; // 禁用时调用的钩子(可选)
};
routs: Set<{
description?: string; // 路由描述(可选)
path: string; // 路由路径
}>;
}
/**
*
*
*
*/
export interface PluginConfiguration {
type: string; // 配置类型
properties: Record<string, {
type: string; // 属性类型
title: string; // 属性标题
description?: string; // 属性描述(可选)
data: any; // 额外数据(可选)
}>;
}

View File

@ -0,0 +1,34 @@
// File path: types/templateType.ts
/**
*
*
*
*/
import React from "react";
import { ExtensionType } from "types/extensionType";
export interface TemplateConfig {
/**
*
*
*
*/
dependencies: Record<string, {
name: string; // 依赖字段的名称
description?: string; // 依赖字段的描述信息
required?: boolean; // 依赖字段是否必填
}>;
extensions?: Record<string, {
description?: string;
extension: ExtensionType;
}>;
/**
*
*
* React
*/
page(params: Map<string, string>): React.FC;
}

View File

@ -1,4 +1,4 @@
// types/theme.ts
// File path: types/themeType.ts
/**
*
*
@ -6,17 +6,18 @@
/**
*
*
*/
export interface ThemeConfig {
name: string; // 主题的唯一标识符
name: string; // 主题的名称
displayName: string; // 主题的显示名称
version: string; // 主题的版本号
description?: string; // 主题的描述信息
author?: string; // 主题的作者信息
entry: string; // 主题的入口组件路径
entry?: string; // 主题的入口路径
templates: Map<string, ThemeTemplate>; // 主题模板的映射表
/** 主题全局配置 */
globalSettings: {
globalSettings?: {
layout?: string; // 主题的布局配置
css?: string; // 主题的CSS配置
};
@ -34,22 +35,23 @@ export interface ThemeConfig {
};
/** 钩子 */
hooks?: {
beforeRender?: string; // 渲染前执行的钩子
afterRender?: string; // 渲染后执行的钩子
onActivate?: string; // 主题激活时执行的钩子
onDeactivate?: string; // 主题停用时执行的钩子
onActivate?: () => {}; // 主题激活时执行的钩子
onDeactivate?: () => {}; // 主题停用时执行的钩子
};
/** 路由 */
routes:{
post:string; // 文章使用的模板
tag:string; // 标签使用的模板
category:string; // 分类使用的模板
page:string; // 独立页面模板路径
routes: {
index: string; // 首页使用的模板
post: string; // 文章使用的模板
tag: string; // 标签使用的模板
category: string; // 分类使用的模板
error: string; // 错误页面用的模板
page: Map<string, string>; // 独立页面模板
}
}
/**
*
*
*/
export interface ThemeTemplate {
path: string; // 模板文件的路径