前端:更换框架; 后端:将数据库接口抽象分为关系型数据库和非关系型数据库;更新config文件的操作;更换config数据库字段,添加是否开启非关系型数据库
This commit is contained in:
parent
bd619e7519
commit
828090e365
16
backend/config.toml
Normal file
16
backend/config.toml
Normal file
@ -0,0 +1,16 @@
|
||||
# config/config.toml
|
||||
# 配置文件
|
||||
|
||||
# 信息
|
||||
[info]
|
||||
install = false # 是否为第一次安装
|
||||
non_relational = true # 是否使用非关系型数据库
|
||||
|
||||
# 数据库
|
||||
[db_config]
|
||||
db_type = "postgresql" # 数据库类型
|
||||
address = "localhost" # 地址
|
||||
prot = 5432 # 端口
|
||||
user = "postgres" # 用户名
|
||||
password = "postgres" # 密码
|
||||
db_name = "echoes" # 数据库
|
@ -1,23 +1,25 @@
|
||||
// config/mod.rs
|
||||
/*
|
||||
配置文件结构和操作
|
||||
*/
|
||||
use std::fs;
|
||||
配置文件结构和操作
|
||||
*/
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::{env, fs};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
pub info: Info,
|
||||
pub database: Database,
|
||||
pub db_config: DbConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Info {
|
||||
pub install: bool,
|
||||
pub non_relational: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Database {
|
||||
pub struct DbConfig {
|
||||
pub db_type: String,
|
||||
pub address: String,
|
||||
pub prot: u32,
|
||||
@ -28,7 +30,9 @@ pub struct Database {
|
||||
|
||||
impl Config {
|
||||
/// 读取配置文件
|
||||
pub fn read(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
pub fn read() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let path = env::current_dir()?
|
||||
.join("config.toml");
|
||||
Ok(toml::from_str(&fs::read_to_string(path)?)?)
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
# config/config.toml
|
||||
# 配置文件
|
||||
|
||||
[info]
|
||||
install = false
|
||||
|
||||
[database]
|
||||
db_type = "postgresql"
|
||||
address = "localhost"
|
||||
prot = 5432
|
||||
user = "postgres"
|
||||
password = "postgres"
|
||||
db_name = "echoes"
|
1
backend/src/database/mod.rs
Normal file
1
backend/src/database/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod relational;
|
@ -4,7 +4,7 @@
|
||||
*/
|
||||
mod postgresql;
|
||||
use std::collections::HashMap;
|
||||
use super::config;
|
||||
use crate::config;
|
||||
use async_trait::async_trait;
|
||||
use std::error::Error;
|
||||
use std::sync::Arc;
|
||||
@ -13,7 +13,7 @@ use std::sync::Arc;
|
||||
#[async_trait]
|
||||
pub trait DatabaseTrait: Send + Sync {
|
||||
// 连接数据库
|
||||
async fn connect(database: config::Database) -> Result<Self, Box<dyn Error>> where Self: Sized;
|
||||
async fn connect(database: config::DbConfig) -> Result<Self, Box<dyn Error>> where Self: Sized;
|
||||
// 执行查询
|
||||
async fn query<'a>(&'a self, query: String) -> Result<Vec<HashMap<String, String>>, Box<dyn Error + 'a>>;
|
||||
}
|
||||
@ -31,7 +31,7 @@ impl Database {
|
||||
}
|
||||
|
||||
// 初始化数据库
|
||||
pub async fn init(database: config::Database) -> Result<Self, Box<dyn Error>> {
|
||||
pub async fn init(database: config::DbConfig) -> Result<Self, Box<dyn Error>> {
|
||||
let db = match database.db_type.as_str() {
|
||||
"postgresql" => postgresql::Postgresql::connect(database).await?,
|
||||
_ => return Err("unknown database type".into()),
|
@ -15,10 +15,10 @@ pub struct Postgresql {
|
||||
|
||||
#[async_trait]
|
||||
impl DatabaseTrait for Postgresql {
|
||||
async fn connect(database: config::Database) -> Result<Self, Box<dyn Error>> {
|
||||
async fn connect(db_config: config::DbConfig) -> Result<Self, Box<dyn Error>> {
|
||||
let connection_str = format!(
|
||||
"postgres://{}:{}@{}:{}/{}",
|
||||
database.user, database.password, database.address, database.prot, database.db_name
|
||||
db_config.user, db_config.password, db_config.address, db_config.prot, db_config.db_name
|
||||
);
|
||||
|
||||
// 连接到数据库池
|
@ -1,4 +1,4 @@
|
||||
// /d:/data/echoes/backend/src/main.rs
|
||||
// main.rs
|
||||
|
||||
/**
|
||||
* 主程序入口,提供数据库连接和相关API接口。
|
||||
@ -9,38 +9,29 @@
|
||||
*/
|
||||
|
||||
mod config; // 配置模块
|
||||
mod sql; // SQL模块
|
||||
use crate::sql::Database; // 引入数据库结构
|
||||
mod database; // 数据库模块
|
||||
use database::relational; // 引入关系型数据库
|
||||
use once_cell::sync::Lazy; // 用于延迟初始化
|
||||
use rocket::{get, http::Status, launch, response::status, routes, serde::json::Json}; // 引入Rocket框架相关功能
|
||||
use std::sync::Arc; // 引入Arc用于线程安全的引用计数
|
||||
use tokio::sync::Mutex; // 引入Mutex用于异步锁
|
||||
|
||||
// 全局数据库连接变量
|
||||
static DB: Lazy<Arc<Mutex<Option<Database>>>> = Lazy::new(|| Arc::new(Mutex::new(None)));
|
||||
static DB: Lazy<Arc<Mutex<Option<relational::Database>>>> = Lazy::new(|| Arc::new(Mutex::new(None)));
|
||||
|
||||
/**
|
||||
* 初始化数据库连接
|
||||
*
|
||||
* # 参数
|
||||
* - `database`: 数据库配置
|
||||
*
|
||||
* # 返回
|
||||
* - `Result<(), Box<dyn std::error::Error>>`: 初始化结果
|
||||
*/
|
||||
async fn init_db(database: config::Database) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let database = Database::init(database).await?; // 初始化数据库
|
||||
async fn init_db(database: config::DbConfig) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let database = relational::Database::init(database).await?; // 初始化数据库
|
||||
*DB.lock().await = Some(database); // 保存数据库实例
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库的引用
|
||||
*
|
||||
* # 返回
|
||||
* - `Result<Database, Box<dyn std::error::Error>>`: 数据库实例或错误
|
||||
*/
|
||||
async fn get_db() -> Result<Database, Box<dyn std::error::Error>> {
|
||||
async fn get_db() -> Result<relational::Database, Box<dyn std::error::Error>> {
|
||||
DB.lock()
|
||||
.await
|
||||
.clone()
|
||||
@ -49,10 +40,7 @@ async fn get_db() -> Result<Database, Box<dyn std::error::Error>> {
|
||||
|
||||
/**
|
||||
* 测试数据库接口
|
||||
*
|
||||
* # 返回
|
||||
* - `Result<Json<Vec<std::collections::HashMap<String, String>>>, status::Custom<String>>`: 查询结果或错误
|
||||
*/
|
||||
*/
|
||||
#[get("/sql")]
|
||||
async fn ssql() -> Result<Json<Vec<std::collections::HashMap<String, String>>>, status::Custom<String>> {
|
||||
let db = get_db().await.map_err(|e| {
|
||||
@ -73,9 +61,6 @@ async fn ssql() -> Result<Json<Vec<std::collections::HashMap<String, String>>>,
|
||||
|
||||
/**
|
||||
* 数据库安装接口
|
||||
*
|
||||
* # 返回
|
||||
* - `status::Custom<String>`: 连接成功或失败的信息
|
||||
*/
|
||||
#[get("/install")]
|
||||
async fn install() -> status::Custom<String> {
|
||||
@ -92,14 +77,11 @@ async fn install() -> status::Custom<String> {
|
||||
|
||||
/**
|
||||
* 启动Rocket应用
|
||||
*
|
||||
* # 返回
|
||||
* - `rocket::Rocket`: Rocket实例
|
||||
*/
|
||||
#[launch]
|
||||
async fn rocket() -> _ {
|
||||
let config = config::Config::read("./src/config/config.toml").expect("Failed to read config"); // 读取配置
|
||||
init_db(config.database)
|
||||
let config = config::Config::read().expect("Failed to read config"); // 读取配置
|
||||
init_db(config.db_config)
|
||||
.await
|
||||
.expect("Failed to connect to database"); // 初始化数据库连接
|
||||
rocket::build().mount("/api", routes![install, ssql]) // 挂载API路由
|
||||
|
@ -1,3 +0,0 @@
|
||||
VITE_APP_API='http://localhost:8000/api'
|
||||
VITE_APP_THEMES='@/themes'
|
||||
VITE_APP_PLUGINS='@/plugins'
|
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);
|
||||
});
|
||||
}
|
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;
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,30 +1,43 @@
|
||||
{
|
||||
"name": "echoes",
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"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"
|
||||
},
|
||||
"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",
|
||||
"isbot": "^4.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.13.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"eslint": "^9.13.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.14",
|
||||
"globals": "^15.11.0",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.11.0",
|
||||
"vite": "^5.4.10"
|
||||
"@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: {},
|
||||
},
|
||||
};
|
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
frontend/public/logo-dark.png
Normal file
BIN
frontend/public/logo-dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 78 KiB |
BIN
frontend/public/logo-light.png
Normal file
BIN
frontend/public/logo-light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.8 KiB |
11
frontend/src/env.d.ts
vendored
11
frontend/src/env.d.ts
vendored
@ -1,11 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_APP_API: string
|
||||
readonly VITE_APP_THEMES: string
|
||||
readonly VITE_APP_PLUGINS: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import react from 'react';
|
||||
const page:react.FC=()=>{
|
||||
return (
|
||||
<div>
|
||||
安装
|
||||
</div>)
|
||||
}
|
||||
export default page;
|
@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
@ -1,27 +0,0 @@
|
||||
//main.tsx
|
||||
import * as React from "react";
|
||||
import "./main.css"
|
||||
import DynamicPage from "./page/page.tsx"
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import {createContext} from "react";
|
||||
|
||||
export const serverAddressContext=createContext("localhost:8080")
|
||||
// 动态路由
|
||||
const RouterListener: React.FC = () => {
|
||||
const a=import.meta.env.VITE_APP_THEMES;
|
||||
console.log(a)
|
||||
const pathname = location.pathname.split("/");
|
||||
console.log(pathname)
|
||||
return (
|
||||
<serverAddressContext.Provider value={"localhost:8080"}>
|
||||
<DynamicPage pageName="home"/>
|
||||
</serverAddressContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<RouterListener/>
|
||||
</React.StrictMode>
|
||||
);
|
@ -1,38 +0,0 @@
|
||||
// page/page.tsx
|
||||
import React from "react";
|
||||
const THEMEPATH= "@/themes"
|
||||
import {serverAddressContext} from "../main.tsx";
|
||||
|
||||
// 动态获取当前主题
|
||||
const getCurrentTheme = async (): Promise<string> => {
|
||||
return new Promise<string>((resolve) => {
|
||||
resolve("default");
|
||||
})
|
||||
}
|
||||
|
||||
// 获取页面
|
||||
const loadPage = (theme: string, pageName: string) => {
|
||||
return import(/* @vite-ignore */`${THEMEPATH}/${theme}/${pageName}/page`).catch(() => import((/* @vite-ignore */`${THEMEPATH}/default/page/page`)))
|
||||
}
|
||||
|
||||
// 动态加载页面
|
||||
const DynamicPage: React.FC<{ pageName: string }> = ({pageName}) => {
|
||||
const serverAddress = React.useContext(serverAddressContext)
|
||||
console.log(serverAddress)
|
||||
const [Page, setPage] = React.useState<React.ComponentType | null>(null);
|
||||
const [theme, setTheme] = React.useState<string>("");
|
||||
React.useEffect(() => {
|
||||
getCurrentTheme().then((theme) => {
|
||||
setTheme(theme);
|
||||
loadPage(theme, pageName).then((module) => {
|
||||
setPage(() => module.default);
|
||||
});
|
||||
})
|
||||
})
|
||||
if(!Page){
|
||||
return <div>loading...</div>;
|
||||
}
|
||||
|
||||
return <Page/>;
|
||||
}
|
||||
export default DynamicPage;
|
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,7 +0,0 @@
|
||||
import React from "react";
|
||||
const Page: React.FC = () => {
|
||||
return <div>404</div>;
|
||||
};
|
||||
|
||||
|
||||
export default Page;
|
@ -1,5 +0,0 @@
|
||||
import React from "react";
|
||||
const Page: React.FC = () => {
|
||||
return <div>主页</div>;
|
||||
};
|
||||
export default Page;
|
@ -1,29 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["src","utils","themes","public"]
|
||||
}
|
@ -1,7 +1,33 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/.server/**/*.ts",
|
||||
"**/.server/**/*.tsx",
|
||||
"**/.client/**/*.ts",
|
||||
"**/.client/**/*.tsx"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"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": {
|
||||
"theme/*":["./theme/*"],
|
||||
"types/*":["./types/*"],
|
||||
},
|
||||
|
||||
// Vite takes care of building everything, not tsc.
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "Bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
* 此接口定义了应用程序的上下文配置,包括API基础URL、主题、插件和资源目录的路径。
|
||||
*/
|
||||
export interface AppContext {
|
||||
blogType:boolean; //用于判断是动态博客还是静态博客
|
||||
apiBaseUrl: string; // 用于访问API的基础URL
|
||||
themesPath: string; // 存储主题文件的目录路径
|
||||
pluginsPath: string; // 存储插件文件的目录路径
|
||||
|
@ -1,4 +1,4 @@
|
||||
// /d:/data/echoes/frontend/types/theme.ts
|
||||
// types/theme.ts
|
||||
/**
|
||||
* 主题配置和模板接口定义文件
|
||||
* 该文件包含主题配置接口和主题模板接口的定义,用于主题管理和渲染。
|
||||
@ -16,21 +16,17 @@ export interface ThemeConfig {
|
||||
entry: string; // 主题的入口组件路径
|
||||
templates: Map<string, ThemeTemplate>; // 主题模板的映射表
|
||||
/** 主题全局配置 */
|
||||
globalSettings?: {
|
||||
globalSettings: {
|
||||
layout?: string; // 主题的布局配置
|
||||
css?: string; // 主题的CSS配置
|
||||
};
|
||||
/** 主题配置文件 */
|
||||
settingsSchema?: {
|
||||
type: string; // 配置文件的类型
|
||||
/** 属性定义 */
|
||||
properties: Record<string, {
|
||||
type: string; // 属性的数据类型
|
||||
title: string; // 属性的标题
|
||||
description?: string; // 属性的描述信息
|
||||
data?: any; // 属性的默认数据
|
||||
}>;
|
||||
};
|
||||
settingsSchema: Record<string, {
|
||||
type: string; // 属性的数据类型
|
||||
title: string; // 属性的标题
|
||||
description?: string; // 属性的描述信息
|
||||
data?: any; // 属性的默认数据
|
||||
}>;
|
||||
/** 依赖 */
|
||||
dependencies?: {
|
||||
plugins?: string[]; // 主题所依赖的插件列表
|
||||
|
@ -1,12 +1,24 @@
|
||||
import {defineConfig} from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': __dirname,
|
||||
}
|
||||
}
|
||||
})
|
||||
plugins: [
|
||||
remix({
|
||||
future: {
|
||||
v3_fetcherPersist: true,
|
||||
v3_relativeSplatPath: true,
|
||||
v3_throwAbortReason: true,
|
||||
v3_singleFetch: true,
|
||||
v3_lazyRouteDiscovery: true,
|
||||
},
|
||||
}),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
});
|
||||
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"react-helmet": "^6.1.0"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user