This commit is contained in:
lsy 2024-11-27 19:52:49 +08:00
parent b689233e91
commit bc42edd38e
20 changed files with 312 additions and 140 deletions

View File

@ -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

View File

@ -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,
}
}
}

View File

@ -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
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -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>
);
}

View File

@ -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>
<Layout>
<Outlet />
</Layout>
);
}

View File

@ -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>;
return <div>Hello, World!</div>;
};
export default function Routes() {
const htmlString = ReactDOMServer.renderToString(<MyComponent />);
const htmlString = ReactDOMServer.renderToString(<MyComponent />);
return (<div></div>)
}
return <div></div>;
}

View File

@ -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;
}
}

View File

@ -18,4 +18,3 @@ export interface PathDescription {
name: string;
description?: string;
}

View File

@ -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 {

View File

@ -4,7 +4,6 @@ export interface CapabilityProps<T> {
execute: (...args: any[]) => Promise<T>;
}
export class CapabilityService {
private capabilities: Map<
string,

View File

@ -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;

View File

@ -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 {
@ -13,7 +12,7 @@ export class RouteManager {
private routes = new Map<string, RouteElement>();
private routesCache = new Map<string, string>();
private constructor() { }
private constructor() {}
public static getInstance(): RouteManager {
if (!RouteManager.instance) {
@ -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);
}

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -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;

View File

@ -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"),
};
});

View File

@ -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;
}

View File

@ -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;
}>;

View File

@ -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;
}