定义
This commit is contained in:
parent
b689233e91
commit
bc42edd38e
@ -25,13 +25,13 @@ pub struct InstallReplyData {
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[post("/install", format = "application/json", data = "<data>")]
|
||||
pub async fn install(
|
||||
#[post("/sql", format = "application/json", data = "<data>")]
|
||||
pub async fn steup_sql(
|
||||
data: Json<InstallData>,
|
||||
state: &State<Arc<AppState>>,
|
||||
) -> AppResult<status::Custom<Json<InstallReplyData>>> {
|
||||
let mut config = config::Config::read().unwrap_or_default();
|
||||
if config.info.install {
|
||||
if config.init.sql {
|
||||
return Err(status::Custom(
|
||||
Status::BadRequest,
|
||||
"Database already initialized".to_string(),
|
||||
@ -39,7 +39,7 @@ pub async fn install(
|
||||
}
|
||||
let data = data.into_inner();
|
||||
let sql = {
|
||||
config.info.install = true;
|
||||
config.init.sql = true;
|
||||
config.sql_config = data.sql_config.clone();
|
||||
sql::Database::initial_setup(data.sql_config.clone())
|
||||
.await
|
||||
|
@ -7,7 +7,7 @@ use std::{env, fs};
|
||||
pub struct Config {
|
||||
pub address: String,
|
||||
pub port: u32,
|
||||
pub info: Info,
|
||||
pub init: Init,
|
||||
pub sql_config: SqlConfig,
|
||||
}
|
||||
|
||||
@ -16,23 +16,25 @@ impl Default for Config {
|
||||
Self {
|
||||
address: "0.0.0.0".to_string(),
|
||||
port: 22000,
|
||||
info: Info::default(),
|
||||
init: Init::default(),
|
||||
sql_config: SqlConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
pub struct Info {
|
||||
pub install: bool,
|
||||
pub non_relational: bool,
|
||||
pub struct Init {
|
||||
pub sql: bool,
|
||||
pub no_sql: bool,
|
||||
pub administrator: bool,
|
||||
}
|
||||
|
||||
impl Default for Info {
|
||||
impl Default for Init {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
install: false,
|
||||
non_relational: false,
|
||||
sql: false,
|
||||
no_sql: false,
|
||||
administrator: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
7
frontend/app/env.d.ts
vendored
7
frontend/app/env.d.ts
vendored
@ -8,9 +8,10 @@
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_SERVER_API: string; // 用于访问API的基础URL
|
||||
readonly VITE_SYSTEM_PORT: number; // 系统端口
|
||||
VITE_SYSTEM_USERNAME: string; // 前端账号名称
|
||||
VITE_SYSTEM_PASSWORD: string; // 前端账号密码
|
||||
readonly VITE_ADDRESS: string; // 前端地址
|
||||
readonly VITE_PORT: number; // 前端系统端口
|
||||
VITE_USERNAME: string; // 前端账号名称
|
||||
VITE_PASSWORD: string; // 前端账号密码
|
||||
VITE_INIT_STATUS: boolean; // 系统是否进行安装
|
||||
}
|
||||
|
||||
|
3
frontend/app/index.css
Normal file
3
frontend/app/index.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
@ -1,3 +1,198 @@
|
||||
export default function page(){
|
||||
return <>安装中</>
|
||||
import React, { useContext, createContext, useState } from "react";
|
||||
|
||||
interface SetupContextType {
|
||||
currentStep: number;
|
||||
setCurrentStep: (step: number) => void;
|
||||
}
|
||||
|
||||
const SetupContext = createContext<SetupContextType>({
|
||||
currentStep: 1,
|
||||
setCurrentStep: () => {},
|
||||
});
|
||||
|
||||
// 步骤组件的通用属性接口
|
||||
interface StepProps {
|
||||
onNext: () => void;
|
||||
onPrev?: () => void;
|
||||
}
|
||||
|
||||
// 通用的步骤容器组件
|
||||
const StepContainer: React.FC<{ title: string; children: React.ReactNode }> = ({
|
||||
title,
|
||||
children,
|
||||
}) => (
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<h2 className="text-2xl font-semibold text-custom-title-light dark:text-custom-title-dark mb-5">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="bg-custom-box-light dark:bg-custom-box-dark rounded-lg shadow-lg p-8">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 通用的导航按钮组件
|
||||
const NavigationButtons: React.FC<StepProps> = ({ onNext, onPrev }) => (
|
||||
<div className="flex gap-4 mt-6">
|
||||
{onPrev && (
|
||||
<button
|
||||
onClick={onPrev}
|
||||
className="px-6 py-2 rounded-lg bg-gray-500 hover:bg-gray-600 text-white transition-colors"
|
||||
>
|
||||
上一步
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onNext}
|
||||
className="px-6 py-2 rounded-lg bg-blue-500 hover:bg-blue-600 text-white transition-colors"
|
||||
>
|
||||
下一步
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 输入框组件
|
||||
const InputField: React.FC<{
|
||||
label: string;
|
||||
name: string;
|
||||
defaultValue?: string | number;
|
||||
hint?: string;
|
||||
}> = ({ label, name, defaultValue, hint }) => (
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl text-custom-title-light dark:text-custom-title-dark mb-2">
|
||||
{label}
|
||||
</h3>
|
||||
<input
|
||||
name={name}
|
||||
defaultValue={defaultValue}
|
||||
className="w-full p-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700"
|
||||
/>
|
||||
{hint && (
|
||||
<p className="text-xs text-custom-p-light dark:text-custom-p-dark mt-1">
|
||||
{hint}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Introduction: React.FC<StepProps> = ({ onNext }) => (
|
||||
<StepContainer title="安装说明">
|
||||
<div className="space-y-6">
|
||||
<p className="text-xl text-custom-p-light dark:text-custom-p-dark">
|
||||
欢迎使用 Echoes
|
||||
</p>
|
||||
<NavigationButtons onNext={onNext} />
|
||||
</div>
|
||||
</StepContainer>
|
||||
);
|
||||
|
||||
const DatabaseConfig: React.FC<StepProps> = ({ onNext, onPrev }) => {
|
||||
const [dbType, setDbType] = useState("postgresql");
|
||||
|
||||
return (
|
||||
<StepContainer title="数据库配置">
|
||||
<div className="space-y-6">
|
||||
<div className="mb-6">
|
||||
<h3 className="text-xl text-custom-title-light dark:text-custom-title-dark mb-2">
|
||||
数据库类型
|
||||
</h3>
|
||||
<select
|
||||
value={dbType}
|
||||
onChange={(e) => setDbType(e.target.value)}
|
||||
className="w-full p-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700"
|
||||
>
|
||||
<option value="postgresql">PostgreSQL</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{dbType === "postgresql" && (
|
||||
<>
|
||||
<InputField
|
||||
label="数据库地址"
|
||||
name="db_host"
|
||||
defaultValue="localhost"
|
||||
hint="通常使用 localhost"
|
||||
/>
|
||||
<InputField
|
||||
label="端口"
|
||||
name="db_port"
|
||||
defaultValue={5432}
|
||||
hint="PostgreSQL 默认端口为 5432"
|
||||
/>
|
||||
<InputField
|
||||
label="用户名"
|
||||
name="db_user"
|
||||
defaultValue="postgres"
|
||||
/>
|
||||
<InputField
|
||||
label="密码"
|
||||
name="db_password"
|
||||
defaultValue="postgres"
|
||||
/>
|
||||
<InputField
|
||||
label="数据库名"
|
||||
name="db_name"
|
||||
defaultValue="echoes"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<NavigationButtons onNext={onNext} onPrev={onPrev} />
|
||||
</div>
|
||||
</StepContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const AdminConfig: React.FC<StepProps> = ({ onNext, onPrev }) => (
|
||||
<StepContainer title="创建管理员账号">
|
||||
<div className="space-y-6">
|
||||
<InputField label="用户名" name="admin_username" />
|
||||
<InputField label="密码" name="admin_password" />
|
||||
<InputField label="邮箱" name="admin_email" />
|
||||
<NavigationButtons onNext={onNext} onPrev={onPrev} />
|
||||
</div>
|
||||
</StepContainer>
|
||||
);
|
||||
|
||||
const SetupComplete: React.FC = () => (
|
||||
<StepContainer title="安装完成">
|
||||
<div className="text-center">
|
||||
<p className="text-xl text-custom-p-light dark:text-custom-p-dark">
|
||||
恭喜!安装已完成,系统即将重启...
|
||||
</p>
|
||||
</div>
|
||||
</StepContainer>
|
||||
);
|
||||
|
||||
export default function SetupPage() {
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full bg-custom-bg-light dark:bg-custom-bg-dark">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold text-custom-title-light dark:text-custom-title-dark mb-4">
|
||||
Echoes
|
||||
</h1>
|
||||
</div>
|
||||
<SetupContext.Provider value={{ currentStep, setCurrentStep }}>
|
||||
{currentStep === 1 && (
|
||||
<Introduction onNext={() => setCurrentStep(currentStep + 1)} />
|
||||
)}
|
||||
{currentStep === 2 && (
|
||||
<DatabaseConfig
|
||||
onNext={() => setCurrentStep(currentStep + 1)}
|
||||
onPrev={() => setCurrentStep(currentStep - 1)}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 3 && (
|
||||
<AdminConfig
|
||||
onNext={() => setCurrentStep(currentStep + 1)}
|
||||
onPrev={() => setCurrentStep(currentStep - 1)}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 4 && <SetupComplete />}
|
||||
</SetupContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -2,13 +2,13 @@ import {
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from "@remix-run/react";
|
||||
|
||||
import { BaseProvider } from "hooks/servicesProvider";
|
||||
import { LinksFunction } from "@remix-run/react/dist/routeModules";
|
||||
|
||||
import "~/tailwind.css";
|
||||
import "~/index.css";
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@ -20,19 +20,49 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<body suppressHydrationWarning={true}>
|
||||
<BaseProvider>
|
||||
<Outlet />
|
||||
</BaseProvider>
|
||||
<ScrollRestoration />
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
function getInitialColorMode() {
|
||||
const persistedColorPreference = window.localStorage.getItem('theme');
|
||||
const hasPersistedPreference = typeof persistedColorPreference === 'string';
|
||||
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';
|
||||
document.documentElement.classList.toggle('dark', newColorMode === 'dark');
|
||||
localStorage.setItem('theme', newColorMode);
|
||||
});
|
||||
})()
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
export default function App() {
|
||||
return (
|
||||
<BaseProvider>
|
||||
<Layout>
|
||||
<Outlet />
|
||||
</Layout>
|
||||
</BaseProvider>
|
||||
);
|
||||
}
|
||||
|
@ -1,15 +1,14 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
const MyComponent = () => {
|
||||
return <div>Hello, World!</div>;
|
||||
};
|
||||
|
||||
|
||||
export default function Routes() {
|
||||
const htmlString = ReactDOMServer.renderToString(<MyComponent />);
|
||||
|
||||
return (<div>安装重构</div>)
|
||||
return <div>安装重构</div>;
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body {
|
||||
@apply bg-white dark:bg-gray-950;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
@ -18,4 +18,3 @@ export interface PathDescription {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
|
@ -55,9 +55,7 @@ export class ApiService {
|
||||
|
||||
private async getToken(username: string, password: string): Promise<string> {
|
||||
if (username.split(" ").length === 0 || password.split(" ").length === 0) {
|
||||
throw new Error(
|
||||
"Username or password cannot be empty",
|
||||
);
|
||||
throw new Error("Username or password cannot be empty");
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -4,7 +4,6 @@ export interface CapabilityProps<T> {
|
||||
execute: (...args: any[]) => Promise<T>;
|
||||
}
|
||||
|
||||
|
||||
export class CapabilityService {
|
||||
private capabilities: Map<
|
||||
string,
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
export interface PluginConfig {
|
||||
name: string;
|
||||
version: string;
|
||||
@ -15,8 +14,6 @@ export interface PluginConfig {
|
||||
}>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class PluginManager {
|
||||
private configurations: Map<string, PluginConfig> = new Map();
|
||||
|
||||
@ -49,9 +46,7 @@ export class PluginManager {
|
||||
}
|
||||
}
|
||||
|
||||
async getPluginConfig(
|
||||
pluginName: string,
|
||||
): Promise<PluginConfig | undefined> {
|
||||
async getPluginConfig(pluginName: string): Promise<PluginConfig | undefined> {
|
||||
const dbConfig = await this.fetchConfigFromDB(pluginName);
|
||||
if (dbConfig) {
|
||||
return dbConfig;
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { ReactNode } from "react"; // Import React
|
||||
import { LoaderFunction } from "react-router-dom";
|
||||
|
||||
|
||||
interface RouteElement {
|
||||
element: ReactNode,
|
||||
loader?: LoaderFunction,
|
||||
children?: RouteElement[],
|
||||
element: ReactNode;
|
||||
loader?: LoaderFunction;
|
||||
children?: RouteElement[];
|
||||
}
|
||||
|
||||
export class RouteManager {
|
||||
@ -22,10 +21,7 @@ export class RouteManager {
|
||||
return RouteManager.instance;
|
||||
}
|
||||
|
||||
private createRouteElement(
|
||||
path: string,
|
||||
element: RouteElement
|
||||
) {
|
||||
private createRouteElement(path: string, element: RouteElement) {
|
||||
this.routes.set(path, element);
|
||||
}
|
||||
|
||||
|
@ -25,8 +25,6 @@ export interface ThemeConfig {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface Template {
|
||||
name: string;
|
||||
description?: string;
|
||||
@ -39,8 +37,6 @@ export interface Template {
|
||||
element: () => React.ReactNode;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class ThemeService {
|
||||
private static instance: ThemeService;
|
||||
private currentTheme?: ThemeConfig;
|
||||
@ -70,12 +66,10 @@ export class ThemeService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public getThemeConfig(): ThemeConfig | undefined {
|
||||
return this.currentTheme;
|
||||
}
|
||||
|
||||
|
||||
public async updateThemeConfig(config: Partial<ThemeConfig>): Promise<void> {
|
||||
try {
|
||||
const updatedConfig = await this.api.request<ThemeConfig>(
|
||||
|
BIN
frontend/public/echoes.png
Normal file
BIN
frontend/public/echoes.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 86 KiB |
@ -1,22 +1,37 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
export default {
|
||||
content: ["./app/**/*.{js,jsx,ts,tsx}"],
|
||||
content: [
|
||||
"./app/**/*.{js,jsx,ts,tsx}",
|
||||
"./common/**/*.{js,jsx,ts,tsx}",
|
||||
"./core/**/*.{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",
|
||||
],
|
||||
sans: ["Inter", "system-ui", "sans-serif"],
|
||||
},
|
||||
colors: {
|
||||
custom: {
|
||||
bg: {
|
||||
light: "#F5F5FB",
|
||||
dark: "#0F172A"
|
||||
},
|
||||
box: {
|
||||
light: "#FFFFFF",
|
||||
dark: "#1E293B"
|
||||
},
|
||||
p: {
|
||||
light: "#4b5563",
|
||||
dark: "#94A3B8"
|
||||
},
|
||||
title: {
|
||||
light: "#111827",
|
||||
dark: "#F1F5F9"
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
},
|
||||
},
|
||||
darkMode: "class",
|
||||
} satisfies Config;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { vitePlugin as remix } from "@remix-run/dev";
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import Routes from "~/routes"
|
||||
import { resolve } from "path";
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '');
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
return {
|
||||
plugins: [
|
||||
remix({
|
||||
@ -20,24 +20,31 @@ export default defineConfig(({ mode }) => {
|
||||
if (!env.VITE_INIT_STATUS) {
|
||||
route("/", "init.tsx", { id: "index-route" });
|
||||
route("*", "init.tsx", { id: "catch-all-route" });
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
route("/", "routes.tsx", { id: "index-route" });
|
||||
route("*", "routes.tsx", { id: "catch-all-route" });
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
define: {
|
||||
"import.meta.env.VITE_SYSTEM_STATUS": JSON.stringify(false),
|
||||
"import.meta.env.VITE_INIT_STATUS": JSON.stringify(false),
|
||||
"import.meta.env.VITE_SERVER_API": JSON.stringify("localhost:22000"),
|
||||
"import.meta.env.VITE_SYSTEM_PORT": JSON.stringify(22100),
|
||||
"import.meta.env.VITE_PORT": JSON.stringify(22100),
|
||||
"import.meta.env.VITE_ADDRESS": JSON.stringify("localhost"),
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
address: "localhost",
|
||||
port: Number(env.VITE_SYSTEM_PORT ?? 22100),
|
||||
strictPort: true,
|
||||
hmr: true, // 确保启用热更新
|
||||
watch: {
|
||||
usePolling: true, // 添加这个配置可以解决某些系统下热更新不工作的问题
|
||||
},
|
||||
},
|
||||
publicDir: resolve(__dirname, "public"),
|
||||
};
|
||||
});
|
@ -1,19 +0,0 @@
|
||||
export function createServiceHook<T>(name: string, getInstance: () => T) {
|
||||
const Context = createContext<T | null>(null);
|
||||
|
||||
const Provider: FC<PropsWithChildren> = ({ children }) => (
|
||||
<Context.Provider value={getInstance()}>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
);
|
||||
|
||||
const useService = () => {
|
||||
const service = useContext(Context);
|
||||
if (!service) {
|
||||
throw new Error(`use${name} must be used within ${name}Provider`);
|
||||
}
|
||||
return service;
|
||||
};
|
||||
|
||||
return [Provider, useService] as const;
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
export type Json =
|
||||
| null
|
||||
| number
|
||||
| string
|
||||
| boolean
|
||||
| { [key: string]: Json }
|
||||
| Json[];
|
||||
|
||||
export type Config = Record<string, {
|
||||
title: string;
|
||||
description?: string;
|
||||
value: Json;
|
||||
}>;
|
@ -1,17 +0,0 @@
|
||||
export interface PluginDefinition {
|
||||
meta: {
|
||||
name: string;
|
||||
version: string;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
icon?: string;
|
||||
};
|
||||
config?: Config;
|
||||
routes: {
|
||||
path: string;
|
||||
description?: string;
|
||||
}[];
|
||||
enabled: boolean;
|
||||
managePath?: string;
|
||||
}
|
Loading…
Reference in New Issue
Block a user