前端:设计了能力服务和api服务
This commit is contained in:
parent
4b06234df8
commit
f5754f982f
2
README.md
Normal file
2
README.md
Normal file
@ -0,0 +1,2 @@
|
||||
能力是主题和系统向插件暴露的接口,契约则规定了开发者在开发主题或插件时的限制和模板。
|
||||
系统使用 Remix,后端采用 Rust 的 Rocket。
|
@ -5,7 +5,7 @@ CREATE DATABASE echoes;
|
||||
--- 安装自动生成uuid插件
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
--- 用户权限枚举
|
||||
CREATE TYPE privilege_level AS ENUM ('visitor', 'contributor', 'administrators');
|
||||
CREATE TYPE privilege_level AS ENUM ( 'contributor', 'administrators');
|
||||
--- 用户信息表
|
||||
CREATE TABLE persons
|
||||
(
|
||||
@ -18,7 +18,7 @@ CREATE TABLE persons
|
||||
person_avatar VARCHAR(255), --- 用户头像URL
|
||||
person_role VARCHAR(50), --- 用户角色
|
||||
person_last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP, --- 最后登录时间
|
||||
person_level privilege_level NOT NULL --- 用户权限
|
||||
person_level privilege_level NOT NULL DEFULT 'contributor' --- 用户权限
|
||||
);
|
||||
--- 页面状态枚举
|
||||
CREATE TYPE publication_status AS ENUM ('draft', 'published', 'private','hide');
|
||||
|
@ -88,7 +88,7 @@ async fn rocket() -> _ {
|
||||
init_db(config.db_config)
|
||||
.await
|
||||
.expect("Failed to connect to database"); // 初始化数据库连接
|
||||
rocket::build().mount("/api", routes![install, ssql]) // 挂载API路由
|
||||
rocket::build().mount("/", routes![install, ssql]) // 挂载API路由
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,3 +1 @@
|
||||
VITE_SERVER_STATUS = false
|
||||
VITE_SERVER_ADDRESS = "localhost:8000"
|
||||
VITE_SERVER_TOKEN = ""
|
||||
VITE_API_BASE_URL = 1
|
84
frontend/.eslintrc.cjs
Normal file
84
frontend/.eslintrc.cjs
Normal file
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* This is intended to be a basic starting point for linting in your app.
|
||||
* It relies on recommended configs out of the box for simplicity, but you can
|
||||
* and should modify this configuration to best suit your team's needs.
|
||||
*/
|
||||
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
commonjs: true,
|
||||
es6: true,
|
||||
},
|
||||
ignorePatterns: ["!**/.server", "!**/.client"],
|
||||
|
||||
// Base config
|
||||
extends: ["eslint:recommended"],
|
||||
|
||||
overrides: [
|
||||
// React
|
||||
{
|
||||
files: ["**/*.{js,jsx,ts,tsx}"],
|
||||
plugins: ["react", "jsx-a11y"],
|
||||
extends: [
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:jsx-a11y/recommended",
|
||||
],
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
formComponents: ["Form"],
|
||||
linkComponents: [
|
||||
{ name: "Link", linkAttribute: "to" },
|
||||
{ name: "NavLink", linkAttribute: "to" },
|
||||
],
|
||||
"import/resolver": {
|
||||
typescript: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Typescript
|
||||
{
|
||||
files: ["**/*.{ts,tsx}"],
|
||||
plugins: ["@typescript-eslint", "import"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
settings: {
|
||||
"import/internal-regex": "^~/",
|
||||
"import/resolver": {
|
||||
node: {
|
||||
extensions: [".ts", ".tsx"],
|
||||
},
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
extends: [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
],
|
||||
},
|
||||
|
||||
// Node
|
||||
{
|
||||
files: [".eslintrc.cjs"],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
18
frontend/app/entry.client.tsx
Normal file
18
frontend/app/entry.client.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* By default, Remix will handle hydrating your app on the client for you.
|
||||
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
|
||||
* For more information, see https://remix.run/file-conventions/entry.client
|
||||
*/
|
||||
|
||||
import { RemixBrowser } from "@remix-run/react";
|
||||
import { startTransition, StrictMode } from "react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
|
||||
startTransition(() => {
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<RemixBrowser />
|
||||
</StrictMode>
|
||||
);
|
||||
});
|
140
frontend/app/entry.server.tsx
Normal file
140
frontend/app/entry.server.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
/**
|
||||
* By default, Remix will handle generating the HTTP Response for you.
|
||||
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
|
||||
* For more information, see https://remix.run/file-conventions/entry.server
|
||||
*/
|
||||
|
||||
import { PassThrough } from "node:stream";
|
||||
|
||||
import type { AppLoadContext, EntryContext } from "@remix-run/node";
|
||||
import { createReadableStreamFromReadable } from "@remix-run/node";
|
||||
import { RemixServer } from "@remix-run/react";
|
||||
import { isbot } from "isbot";
|
||||
import { renderToPipeableStream } from "react-dom/server";
|
||||
|
||||
const ABORT_DELAY = 5_000;
|
||||
|
||||
export default function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext,
|
||||
// This is ignored so we can keep it in the template for visibility. Feel
|
||||
// free to delete this parameter in your app if you're not using it!
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
loadContext: AppLoadContext
|
||||
) {
|
||||
return isbot(request.headers.get("user-agent") || "")
|
||||
? handleBotRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext
|
||||
)
|
||||
: handleBrowserRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext
|
||||
);
|
||||
}
|
||||
|
||||
function handleBotRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false;
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<RemixServer
|
||||
context={remixContext}
|
||||
url={request.url}
|
||||
abortDelay={ABORT_DELAY}
|
||||
/>,
|
||||
{
|
||||
onAllReady() {
|
||||
shellRendered = true;
|
||||
const body = new PassThrough();
|
||||
const stream = createReadableStreamFromReadable(body);
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
|
||||
resolve(
|
||||
new Response(stream, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
})
|
||||
);
|
||||
|
||||
pipe(body);
|
||||
},
|
||||
onShellError(error: unknown) {
|
||||
reject(error);
|
||||
},
|
||||
onError(error: unknown) {
|
||||
responseStatusCode = 500;
|
||||
// Log streaming rendering errors from inside the shell. Don't log
|
||||
// errors encountered during initial shell rendering since they'll
|
||||
// reject and get logged in handleDocumentRequest.
|
||||
if (shellRendered) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setTimeout(abort, ABORT_DELAY);
|
||||
});
|
||||
}
|
||||
|
||||
function handleBrowserRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let shellRendered = false;
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<RemixServer
|
||||
context={remixContext}
|
||||
url={request.url}
|
||||
abortDelay={ABORT_DELAY}
|
||||
/>,
|
||||
{
|
||||
onShellReady() {
|
||||
shellRendered = true;
|
||||
const body = new PassThrough();
|
||||
const stream = createReadableStreamFromReadable(body);
|
||||
|
||||
responseHeaders.set("Content-Type", "text/html");
|
||||
|
||||
resolve(
|
||||
new Response(stream, {
|
||||
headers: responseHeaders,
|
||||
status: responseStatusCode,
|
||||
})
|
||||
);
|
||||
|
||||
pipe(body);
|
||||
},
|
||||
onShellError(error: unknown) {
|
||||
reject(error);
|
||||
},
|
||||
onError(error: unknown) {
|
||||
responseStatusCode = 500;
|
||||
// Log streaming rendering errors from inside the shell. Don't log
|
||||
// errors encountered during initial shell rendering since they'll
|
||||
// reject and get logged in handleDocumentRequest.
|
||||
if (shellRendered) {
|
||||
console.error(error);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setTimeout(abort, ABORT_DELAY);
|
||||
});
|
||||
}
|
22
frontend/app/env.d.ts
vendored
Normal file
22
frontend/app/env.d.ts
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
// File path: app/end.d.ts
|
||||
|
||||
/**
|
||||
* 配置
|
||||
*/
|
||||
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_SERVER_API: string; // 用于访问API的基础URL
|
||||
readonly VITE_THEME_PATH: string; // 存储主题文件的目录路径
|
||||
readonly VITE_CONTENT_PATH: string; //mark文章存储的位置
|
||||
readonly VITE_CONTENT_STATIC_PATH: string; //导出文章静态存储的位置
|
||||
readonly VITE_PLUGINS_PATH: string; // 存储插件文件的目录路径
|
||||
readonly VITE_ASSETS_PATH: string; // 存储静态资源的目录路径
|
||||
VITE_SYSTEM_USERNAME: string; // 前端账号名称
|
||||
VITE_SYSTEM_PASSWORD: string; // 前端账号密码
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
45
frontend/app/root.tsx
Normal file
45
frontend/app/root.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import {
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from "@remix-run/react";
|
||||
import type { LinksFunction } from "@remix-run/node";
|
||||
|
||||
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 }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
}
|
138
frontend/app/routes/_index.tsx
Normal file
138
frontend/app/routes/_index.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
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>
|
||||
),
|
||||
},
|
||||
];
|
12
frontend/app/tailwind.css
Normal file
12
frontend/app/tailwind.css
Normal file
@ -0,0 +1,12 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply bg-white dark:bg-gray-950;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
@ -2,28 +2,12 @@
|
||||
/**
|
||||
* 能力契约接口
|
||||
*/
|
||||
export interface CapabilityProps {
|
||||
// 能力名称
|
||||
name: string;
|
||||
// 能力描述
|
||||
description?: string;
|
||||
// 能力版本
|
||||
version: string;
|
||||
// 能力参数定义
|
||||
parameters?: {
|
||||
type: 'object';
|
||||
properties: Record<string, {
|
||||
type: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
}>;
|
||||
};
|
||||
// 能力返回值定义
|
||||
returns?: {
|
||||
type: string;
|
||||
description?: string;
|
||||
};
|
||||
// 能力执行函数
|
||||
execute: (...args: any[]) => Promise<any>;
|
||||
}
|
||||
export interface CapabilityProps<T> {
|
||||
// 能力名称
|
||||
name: string;
|
||||
// 能力描述
|
||||
description?: string;
|
||||
// 能力执行函数
|
||||
execute: (...args: any[]) => Promise<T>;
|
||||
}
|
||||
|
@ -20,16 +20,12 @@ export interface PluginConfig {
|
||||
icon?: string; // 插件图标URL(可选)
|
||||
managePath?: string; // 插件管理页面路径(可选)
|
||||
configuration?: PluginConfiguration; // 插件配置
|
||||
/** 能力 */
|
||||
capabilities?: Set<CapabilityProps>;
|
||||
/** 声明需要使用的能力,没有实际作用 */
|
||||
capabilities?: Set<CapabilityProps<void>>;
|
||||
routs: Set<{
|
||||
description?: string; // 路由描述(可选)
|
||||
path: string; // 路由路径
|
||||
}>;
|
||||
// 模块初始化函数
|
||||
initialize?: () => Promise<void>;
|
||||
// 模块销毁函数
|
||||
destroy?: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { CapabilityProps } from "contracts/capabilityContract";
|
||||
|
||||
export interface TemplateContract {
|
||||
// 模板名称
|
||||
name: string;
|
||||
@ -11,25 +13,10 @@ export interface TemplateContract {
|
||||
styles?: string[];
|
||||
// 模板脚本
|
||||
scripts?: string[];
|
||||
// 模板区域定义
|
||||
zones?: Record<string, {
|
||||
name: string;
|
||||
description?: string;
|
||||
allowedComponents?: string[];
|
||||
}>;
|
||||
};
|
||||
// 模板数据契约
|
||||
dataContract?: {
|
||||
// 必需的数据字段
|
||||
required: string[];
|
||||
// 可选的数据字段
|
||||
optional?: string[];
|
||||
// 数据验证规则
|
||||
validation?: Record<string, {
|
||||
type: string;
|
||||
rules: any[];
|
||||
}>;
|
||||
};
|
||||
/** 声明需要使用的能力,没有实际作用 */
|
||||
capabilities?: Set<CapabilityProps<void>>;
|
||||
|
||||
// 渲染函数
|
||||
render: (props: any) => React.ReactNode;
|
||||
}
|
@ -8,7 +8,6 @@
|
||||
* 主题配置接口
|
||||
* 定义主题的基本信息、模板、全局配置、依赖、钩子和路由。
|
||||
*/
|
||||
import { CapabilityProps } from "contracts/capabilityContract";
|
||||
import { SerializeType } from "contracts/generalContract";
|
||||
export interface ThemeConfig {
|
||||
name: string; // 主题的名称
|
||||
@ -16,7 +15,7 @@ export interface ThemeConfig {
|
||||
version: string; // 主题的版本号
|
||||
description?: string; // 主题的描述信息
|
||||
author?: string; // 主题的作者信息
|
||||
entry?: string; // 主题的入口路径
|
||||
entry: string; // 主题的入口路径
|
||||
templates: Map<string, ThemeTemplate>; // 主题模板的映射表
|
||||
/** 主题全局配置 */
|
||||
globalSettings?: {
|
||||
@ -29,14 +28,6 @@ export interface ThemeConfig {
|
||||
description?: string; // 属性的描述信息
|
||||
data: SerializeType; // 属性的默认数据
|
||||
}>;
|
||||
/** 依赖 */
|
||||
dependencies?: {
|
||||
plugins?: string[]; // 主题所依赖的插件列表
|
||||
assets?: string[]; // 主题所依赖的资源列表
|
||||
};
|
||||
/** 能力 */
|
||||
capabilities?: Set<CapabilityProps>;
|
||||
|
||||
/** 路由 */
|
||||
routes: {
|
||||
index: string; // 首页使用的模板
|
||||
|
@ -1,22 +1,25 @@
|
||||
import { ExtensionService } from 'services/extensionService';
|
||||
import { ThemeService } from 'services/themeService';
|
||||
import { createServiceContext } from './createServiceContext';
|
||||
import { ReactNode } from 'react';
|
||||
import { CapabilityService } from "services/capabilityService";
|
||||
import { ThemeService } from "services/themeService";
|
||||
import { createServiceContext } from "./createServiceContext";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export const {
|
||||
ExtensionProvider,
|
||||
useExtension
|
||||
} = createServiceContext('Extension', () => ExtensionService.getInstance());
|
||||
export const { ExtensionProvider, useExtension } = createServiceContext(
|
||||
"Extension",
|
||||
() => CapabilityService.getInstance(),
|
||||
);
|
||||
|
||||
export const {
|
||||
ThemeProvider,
|
||||
useTheme
|
||||
} = createServiceContext("Theme", () => ThemeService.getInstance());
|
||||
export const { ThemeProvider, useTheme } = createServiceContext("Theme", () =>
|
||||
ThemeService.getInstance(),
|
||||
);
|
||||
|
||||
export const ServiceProvider = ({ children }: { children: ReactNode })=>(
|
||||
<ExtensionProvider>
|
||||
<ThemeProvider>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</ExtensionProvider>
|
||||
);
|
||||
// File path:hooks/servicesProvider.tsx
|
||||
/**
|
||||
* ServiceProvider 组件用于提供扩展和主题上下文给其子组件。
|
||||
*
|
||||
* @param children - 要渲染的子组件。
|
||||
*/
|
||||
export const ServiceProvider = ({ children }: { children: ReactNode }) => (
|
||||
<ExtensionProvider>
|
||||
<ThemeProvider>{children}</ThemeProvider>
|
||||
</ExtensionProvider>
|
||||
);
|
||||
|
@ -1,23 +1,45 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"build": "remix vite:build",
|
||||
"dev": "remix vite:dev",
|
||||
"lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
|
||||
"start": "remix-serve ./build/server/index.js",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.28.0"
|
||||
"@remix-run/node": "^2.14.0",
|
||||
"@remix-run/react": "^2.14.0",
|
||||
"@remix-run/serve": "^2.14.0",
|
||||
"@types/axios": "^0.14.4",
|
||||
"axios": "^1.7.7",
|
||||
"isbot": "^4.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"typescript": "^5.6.3"
|
||||
"@remix-run/dev": "^2.14.0",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^5.1.0",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
|
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
118
frontend/services/apiService.ts
Normal file
118
frontend/services/apiService.ts
Normal file
@ -0,0 +1,118 @@
|
||||
// File path: /d:/data/echoes/frontend/services/apiService.ts
|
||||
|
||||
/**
|
||||
* ApiConfig接口用于配置API服务的基本信息。
|
||||
*/
|
||||
interface ApiConfig {
|
||||
baseURL: string; // API的基础URL
|
||||
timeout?: number; // 请求超时时间(可选)
|
||||
}
|
||||
|
||||
export class ApiService {
|
||||
private static instance: ApiService; // ApiService的单例实例
|
||||
private baseURL: string; // API的基础URL
|
||||
private timeout: number; // 请求超时时间
|
||||
|
||||
/**
|
||||
* 构造函数用于初始化ApiService实例。
|
||||
* @param config ApiConfig配置对象
|
||||
*/
|
||||
private constructor(config: ApiConfig) {
|
||||
this.baseURL = config.baseURL;
|
||||
this.timeout = config.timeout || 10000; // 默认超时时间为10000毫秒
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取ApiService的单例实例。
|
||||
* @param config 可选的ApiConfig配置对象
|
||||
* @returns ApiService实例
|
||||
*/
|
||||
public static getInstance(config?: ApiConfig): ApiService {
|
||||
if (!this.instance && config) {
|
||||
this.instance = new ApiService(config);
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统令牌。
|
||||
* @returns Promise<string> 返回系统令牌
|
||||
* @throws Error 如果未找到凭据或请求失败
|
||||
*/
|
||||
private async getSystemToken(): Promise<string> {
|
||||
const credentials = localStorage.getItem('system_credentials');
|
||||
if (!credentials) {
|
||||
throw new Error('System credentials not found');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseURL}/auth/system`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(JSON.parse(credentials)),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get system token');
|
||||
}
|
||||
|
||||
const { token } = await response.json();
|
||||
return token;
|
||||
} catch (error) {
|
||||
console.error('Error getting system token:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起API请求。
|
||||
* @param endpoint 请求的API端点
|
||||
* @param options 请求选项
|
||||
* @param requiresAuth 是否需要身份验证(默认为true)
|
||||
* @returns Promise<T> 返回API响应数据
|
||||
* @throws Error 如果请求超时或发生其他错误
|
||||
*/
|
||||
public async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {},
|
||||
requiresAuth = true
|
||||
): Promise<T> {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||
|
||||
try {
|
||||
const headers = new Headers(options.headers);
|
||||
|
||||
if (requiresAuth) {
|
||||
const token = await this.getSystemToken();
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseURL}${endpoint}`, {
|
||||
...options,
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API Error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data as T;
|
||||
} catch (error: any) {
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error('Request timeout');
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ApiService.getInstance({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
});
|
105
frontend/services/capabilityService.ts
Normal file
105
frontend/services/capabilityService.ts
Normal file
@ -0,0 +1,105 @@
|
||||
// File path: services/capabilityService.ts
|
||||
|
||||
/**
|
||||
* CapabilityService 是一个单例类,用于管理能力的实例。
|
||||
* 提供注册、执行和移除能力的功能。
|
||||
*/
|
||||
import { CapabilityProps } from "contracts/capabilityContract";
|
||||
|
||||
export class CapabilityService {
|
||||
// 存储能力的映射,键为能力名称,值为能力源和能力属性的集合
|
||||
private capabilities: Map<string, Set<{
|
||||
source: string;
|
||||
capability: CapabilityProps<any>}>> = new Map();
|
||||
|
||||
// CapabilityService 的唯一实例
|
||||
private static instance: CapabilityService;
|
||||
|
||||
/**
|
||||
* 私有构造函数,防止外部实例化
|
||||
*/
|
||||
private constructor() { }
|
||||
|
||||
/**
|
||||
* 获取 CapabilityService 的唯一实例。
|
||||
* @returns {CapabilityService} 返回 CapabilityService 的唯一实例。
|
||||
*/
|
||||
public static getInstance(): CapabilityService {
|
||||
if (!this.instance) {
|
||||
this.instance = new CapabilityService();
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册能力
|
||||
* @param capabilityName 能力名称
|
||||
* @param source 能力来源
|
||||
* @param capability 能力属性
|
||||
*/
|
||||
private register(capabilityName: string, source: string, capability: CapabilityProps<any>) {
|
||||
const handlers = this.capabilities.get(capabilityName) || new Set();
|
||||
handlers.add({ source, capability });
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行指定能力的方法
|
||||
* @param capabilityName 能力名称
|
||||
* @param args 方法参数
|
||||
* @returns {Set<T>} 执行结果的集合
|
||||
*/
|
||||
private executeCapabilityMethod<T>(capabilityName: string, ...args: any[]): Set<T> {
|
||||
const results = new Set<T>();
|
||||
const handlers = this.capabilities.get(capabilityName);
|
||||
|
||||
if (handlers) {
|
||||
handlers.forEach(({ capability }) => {
|
||||
const methodFunction = capability['execute'];
|
||||
if (methodFunction) {
|
||||
methodFunction(...args)
|
||||
.then((data) => results.add(data as T))
|
||||
.catch((error) => console.error(`Error executing method ${capabilityName}:`, error));
|
||||
}
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定来源的能力
|
||||
* @param source 能力来源
|
||||
*/
|
||||
private removeCapability(source: string) {
|
||||
this.capabilities.forEach((capability_s, capabilityName) => {
|
||||
const newHandlers = new Set(
|
||||
Array.from(capability_s).filter(capability => capability.source !== source)
|
||||
);
|
||||
this.capabilities.set(capabilityName, newHandlers);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定能力
|
||||
* @param capability 能力名称
|
||||
*/
|
||||
private removeCapabilitys(capability: string) {
|
||||
this.capabilities.delete(capability);
|
||||
}
|
||||
|
||||
public validateCapability(capability: CapabilityProps<any>): boolean {
|
||||
// 验证能力是否符合基本要求
|
||||
if (!capability.name || !capability.execute) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证能力名称格式
|
||||
const namePattern = /^[a-z][a-zA-Z0-9_]*$/;
|
||||
if (!namePattern.test(capability.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
// File path: service/extensionService.ts
|
||||
|
||||
/**
|
||||
* ExtensionManage 是一个单例类,用于管理扩展的实例。
|
||||
* 提供注册、触发和移除插件扩展的功能。
|
||||
*/
|
||||
import { ExtensionProps } from "types/extensionRequirement";
|
||||
import React from "react";
|
||||
import { PluginConfiguration } from "types/pluginRequirement";
|
||||
|
||||
export class ExtensionService {
|
||||
/** 存储扩展的映射,键为扩展名称,值为插件名称和扩展的集合 */
|
||||
private extensions: Map<string, Set<{ pluginName: string; extension: ExtensionProps }>> = new Map();
|
||||
private configuration: Map<string, PluginConfiguration> = new Map();
|
||||
/** ExtensionManage 的唯一实例 */
|
||||
private static instance: ExtensionService;
|
||||
|
||||
/** 私有构造函数,防止外部实例化 */
|
||||
private constructor() { }
|
||||
|
||||
/**
|
||||
* 获取 ExtensionManage 的唯一实例。
|
||||
* @returns {ExtensionManage} 返回 ExtensionManage 的唯一实例。
|
||||
*/
|
||||
public static getInstance(): ExtensionService {
|
||||
if (!this.instance) {
|
||||
this.instance = new ExtensionService();
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/** 注册扩展 */
|
||||
private register(extensionName: string, pluginName: string, extension: ExtensionProps, pluginConfiguration: PluginConfiguration) {
|
||||
const handlers = this.extensions.get(extensionName) || new Set();
|
||||
this.configuration.has(extensionName) || this.configuration.set(pluginName, pluginConfiguration);
|
||||
|
||||
handlers.add({ pluginName, extension });
|
||||
this.extensions.set(extensionName, handlers);
|
||||
}
|
||||
|
||||
/** 执行扩展方法 */
|
||||
private executeExtensionMethod<T>(extensionName: string, method: keyof ExtensionProps, ...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);
|
||||
});
|
||||
this.configuration.delete(pluginName);
|
||||
}
|
||||
|
||||
//获取指定配置文件
|
||||
private getConfiguration(pluginName: string): PluginConfiguration | undefined {
|
||||
return this.configuration.get(pluginName);
|
||||
}
|
||||
}
|
22
frontend/tailwind.config.ts
Normal file
22
frontend/tailwind.config.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
export default {
|
||||
content: ["./app/**/{**,.client,.server}/**/*.{js,jsx,ts,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: [
|
||||
"Inter",
|
||||
"ui-sans-serif",
|
||||
"system-ui",
|
||||
"sans-serif",
|
||||
"Apple Color Emoji",
|
||||
"Segoe UI Emoji",
|
||||
"Segoe UI Symbol",
|
||||
"Noto Color Emoji",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
} satisfies Config;
|
@ -1,19 +1,32 @@
|
||||
{
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/.server/**/*.ts",
|
||||
"**/.server/**/*.tsx",
|
||||
"**/.client/**/*.ts",
|
||||
"**/.client/**/*.tsx"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"target": "es6", // 编译到 ES5
|
||||
"lib": ["dom", "dom.iterable", "esnext"], // 指定要编译的库
|
||||
"allowJs": true, // 允许 JavaScript 文件
|
||||
"skipLibCheck": true, // 跳过库检查
|
||||
"esModuleInterop": true, // 支持 CommonJS 模块
|
||||
"allowSyntheticDefaultImports": true, // 允许导入默认值
|
||||
"strict": true, // 启用严格模式
|
||||
"forceConsistentCasingInFileNames": true, // 强制文件名大小写一致
|
||||
"module": "esnext", // 使用 ES 模块
|
||||
"moduleResolution": "node", // 模块解析策略
|
||||
"resolveJsonModule": true, // 允许导入 JSON 模块
|
||||
"isolatedModules": true, // 每个文件单独编译
|
||||
"noEmit": true // 不输出任何文件,只检查类型
|
||||
},
|
||||
"include": ["src/**/*"], // 包含 src 文件夹中的所有 TypeScript 文件
|
||||
"exclude": ["node_modules"] // 排除 node_modules 文件夹
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"types": ["@remix-run/node", "vite/client"],
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"target": "ES2022",
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
},
|
||||
|
||||
// Vite takes care of building everything, not tsc.
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
|
24
frontend/vite.config.ts
Normal file
24
frontend/vite.config.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { vitePlugin as remix } from "@remix-run/dev";
|
||||
import { defineConfig } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
declare module "@remix-run/node" {
|
||||
interface Future {
|
||||
v3_singleFetch: true;
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
remix({
|
||||
future: {
|
||||
v3_fetcherPersist: true,
|
||||
v3_relativeSplatPath: true,
|
||||
v3_throwAbortReason: true,
|
||||
v3_singleFetch: true,
|
||||
v3_lazyRouteDiscovery: true,
|
||||
},
|
||||
}),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
});
|
Loading…
Reference in New Issue
Block a user