后端:修复数据库异常连接导致后续无法正常重启;
前端:消息提示增加标题,添加Radix UI,重构HTTP错误处理逻辑。
This commit is contained in:
parent
72db2c9de5
commit
a43a9bac36
@ -33,7 +33,7 @@ pub async fn setup_sql(
|
|||||||
.into_app_result()?;
|
.into_app_result()?;
|
||||||
|
|
||||||
config::Config::write(config).into_app_result()?;
|
config::Config::write(config).into_app_result()?;
|
||||||
state.restart_server().await.into_app_result()?;
|
state.trigger_restart().await.into_app_result()?;
|
||||||
Ok("Database installation successful".to_string())
|
Ok("Database installation successful".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ pub async fn setup_account(
|
|||||||
.into_app_result()?;
|
.into_app_result()?;
|
||||||
config.init.administrator = true;
|
config.init.administrator = true;
|
||||||
config::Config::write(config).into_app_result()?;
|
config::Config::write(config).into_app_result()?;
|
||||||
state.restart_server().await.into_app_result()?;
|
state.trigger_restart().await.into_app_result()?;
|
||||||
|
|
||||||
Ok(status::Custom(
|
Ok(status::Custom(
|
||||||
Status::Ok,
|
Status::Ok,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::common::error::{CustomErrorInto, CustomResult};
|
use crate::common::error::{CustomErrorInto, CustomResult};
|
||||||
use crate::security::bcrypt;
|
use crate::security::bcrypt;
|
||||||
use crate::storage::{sql, sql::builder};
|
use crate::storage::{sql, sql::builder};
|
||||||
|
use regex::Regex;
|
||||||
use rocket::{get, http::Status, post, response::status, serde::json::Json, State};
|
use rocket::{get, http::Status, post, response::status, serde::json::Json, State};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@ -30,6 +31,12 @@ pub async fn insert_user(sql: &sql::Database, data: RegisterData) -> CustomResul
|
|||||||
|
|
||||||
let password_hash = bcrypt::generate_hash(&data.password)?;
|
let password_hash = bcrypt::generate_hash(&data.password)?;
|
||||||
|
|
||||||
|
let re = Regex::new(r"([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)")?;
|
||||||
|
|
||||||
|
if false == re.is_match(&data.email) {
|
||||||
|
return Err("邮箱格式不正确".into_custom_error());
|
||||||
|
}
|
||||||
|
|
||||||
let mut builder = builder::QueryBuilder::new(
|
let mut builder = builder::QueryBuilder::new(
|
||||||
builder::SqlOperation::Insert,
|
builder::SqlOperation::Insert,
|
||||||
sql.table_name("users"),
|
sql.table_name("users"),
|
||||||
|
@ -13,7 +13,6 @@ pub struct AppState {
|
|||||||
db: Arc<Mutex<Option<sql::Database>>>,
|
db: Arc<Mutex<Option<sql::Database>>>,
|
||||||
shutdown: Arc<Mutex<Option<Shutdown>>>,
|
shutdown: Arc<Mutex<Option<Shutdown>>>,
|
||||||
restart_progress: Arc<Mutex<bool>>,
|
restart_progress: Arc<Mutex<bool>>,
|
||||||
restart_attempts: Arc<Mutex<u32>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
@ -22,7 +21,6 @@ impl AppState {
|
|||||||
db: Arc::new(Mutex::new(None)),
|
db: Arc::new(Mutex::new(None)),
|
||||||
shutdown: Arc::new(Mutex::new(None)),
|
shutdown: Arc::new(Mutex::new(None)),
|
||||||
restart_progress: Arc::new(Mutex::new(false)),
|
restart_progress: Arc::new(Mutex::new(false)),
|
||||||
restart_attempts: Arc::new(Mutex::new(0)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,8 +33,15 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sql_link(&self, config: &config::SqlConfig) -> CustomResult<()> {
|
pub async fn sql_link(&self, config: &config::SqlConfig) -> CustomResult<()> {
|
||||||
*self.db.lock().await = Some(sql::Database::link(config).await?);
|
match sql::Database::link(config).await {
|
||||||
Ok(())
|
Ok(db) => {
|
||||||
|
*self.db.lock().await = Some(db);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_shutdown(&self, shutdown: Shutdown) {
|
pub async fn set_shutdown(&self, shutdown: Shutdown) {
|
||||||
@ -54,27 +59,6 @@ impl AppState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn restart_server(&self) -> CustomResult<()> {
|
|
||||||
const MAX_RESTART_ATTEMPTS: u32 = 3;
|
|
||||||
const RESTART_DELAY_MS: u64 = 1000;
|
|
||||||
|
|
||||||
let mut attempts = self.restart_attempts.lock().await;
|
|
||||||
if *attempts >= MAX_RESTART_ATTEMPTS {
|
|
||||||
return Err("达到最大重启尝试次数".into_custom_error());
|
|
||||||
}
|
|
||||||
*attempts += 1;
|
|
||||||
|
|
||||||
*self.restart_progress.lock().await = true;
|
|
||||||
|
|
||||||
self.shutdown
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.take()
|
|
||||||
.ok_or_else(|| "未能获取rocket的shutdown".into_custom_error())?
|
|
||||||
.notify();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rocket::main]
|
#[rocket::main]
|
||||||
@ -115,31 +99,15 @@ async fn main() -> CustomResult<()> {
|
|||||||
rocket.launch().await?;
|
rocket.launch().await?;
|
||||||
|
|
||||||
if *state.restart_progress.lock().await {
|
if *state.restart_progress.lock().await {
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
|
|
||||||
|
|
||||||
if let Ok(current_exe) = std::env::current_exe() {
|
if let Ok(current_exe) = std::env::current_exe() {
|
||||||
println!("正在尝试重启服务器...");
|
match std::process::Command::new(current_exe).spawn() {
|
||||||
|
Ok(_) => println!("成功启动新进程"),
|
||||||
let mut command = std::process::Command::new(current_exe);
|
Err(e) => eprintln!("启动新进程失败: {}", e),
|
||||||
command.env("RUST_BACKTRACE", "1");
|
|
||||||
|
|
||||||
match command.spawn() {
|
|
||||||
Ok(child) => {
|
|
||||||
println!("成功启动新进程 (PID: {})", child.id());
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("启动新进程失败: {}", e);
|
|
||||||
*state.restart_progress.lock().await = false;
|
|
||||||
return Err(format!("重启失败: {}", e).into_custom_error());
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
eprintln!("获取当前可执行文件路径失败");
|
eprintln!("获取当前可执行文件路径失败");
|
||||||
return Err("重启失败: 无法获取可执行文件路径".into_custom_error());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("服务器正常退出");
|
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ impl Default for TextValidator {
|
|||||||
impl TextValidator {
|
impl TextValidator {
|
||||||
pub fn validate(&self, text: &str, level: ValidationLevel) -> CustomResult<()> {
|
pub fn validate(&self, text: &str, level: ValidationLevel) -> CustomResult<()> {
|
||||||
if level == ValidationLevel::Raw {
|
if level == ValidationLevel::Raw {
|
||||||
return self.validate_sql_patterns(text);
|
return Ok(());
|
||||||
}
|
}
|
||||||
let max_length = self
|
let max_length = self
|
||||||
.level_max_lengths
|
.level_max_lengths
|
||||||
|
@ -1,3 +1,44 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.animate-slideIn {
|
||||||
|
animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-hide {
|
||||||
|
animation: hide 100ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-swipeOut {
|
||||||
|
animation: swipeOut 100ms ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hide {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(calc(100% + 1rem));
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes swipeOut {
|
||||||
|
from {
|
||||||
|
transform: translateX(var(--radix-toast-swipe-end-x));
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(calc(100% + 1rem));
|
||||||
|
}
|
||||||
|
}
|
@ -131,7 +131,7 @@ const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
|||||||
default: return field;
|
default: return field;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
message.error(`请填写以下必填项:${fieldNames.join('、')}`);
|
message.error(`请填写以下必填项:${fieldNames.join('、')}`, '验证失败');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -180,11 +180,11 @@ const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
|||||||
|
|
||||||
Object.assign( newEnv)
|
Object.assign( newEnv)
|
||||||
|
|
||||||
message.success('数据库配置成功!');
|
message.success('数据库配置已保存', '配置成功');
|
||||||
setTimeout(() => onNext(), 1000);
|
setTimeout(() => onNext(), 1000);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error( error);
|
console.error( error);
|
||||||
message.error(error.message );
|
message.error(error.message , error.title || '配置失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -369,11 +369,11 @@ const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
|
|||||||
body: JSON.stringify(newEnv),
|
body: JSON.stringify(newEnv),
|
||||||
});
|
});
|
||||||
|
|
||||||
message.success('管理员账号创建成功!');
|
message.success('管理员账号已创建,即将进入下一步', '创建成功');
|
||||||
onNext();
|
onNext();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
message.error(error.message);
|
message.error(error.message, error.title || '创建失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,7 @@ import {
|
|||||||
} from "@remix-run/react";
|
} from "@remix-run/react";
|
||||||
|
|
||||||
import { BaseProvider } from "hooks/servicesProvider";
|
import { BaseProvider } from "hooks/servicesProvider";
|
||||||
import { MessageProvider } from "hooks/message";
|
|
||||||
import { MessageContainer } from "hooks/message";
|
|
||||||
|
|
||||||
import "~/index.css";
|
import "~/index.css";
|
||||||
|
|
||||||
@ -24,42 +23,40 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|||||||
</head>
|
</head>
|
||||||
<body suppressHydrationWarning={true}>
|
<body suppressHydrationWarning={true}>
|
||||||
<BaseProvider>
|
<BaseProvider>
|
||||||
<MessageProvider>
|
|
||||||
<MessageContainer />
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</MessageProvider>
|
|
||||||
</BaseProvider>
|
</BaseProvider>
|
||||||
<ScrollRestoration />
|
|
||||||
<script
|
<ScrollRestoration />
|
||||||
dangerouslySetInnerHTML={{
|
<script
|
||||||
__html: `
|
dangerouslySetInnerHTML={{
|
||||||
(function() {
|
__html: `
|
||||||
function getInitialColorMode() {
|
(function() {
|
||||||
const persistedColorPreference = window.localStorage.getItem('theme');
|
function getInitialColorMode() {
|
||||||
const hasPersistedPreference = typeof persistedColorPreference === 'string';
|
const persistedColorPreference = window.localStorage.getItem('theme');
|
||||||
if (hasPersistedPreference) {
|
const hasPersistedPreference = typeof persistedColorPreference === 'string';
|
||||||
return persistedColorPreference;
|
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 mql = window.matchMedia('(prefers-color-scheme: dark)');
|
const colorMode = getInitialColorMode();
|
||||||
const hasMediaQueryPreference = typeof mql.matches === 'boolean';
|
document.documentElement.classList.toggle('dark', colorMode === 'dark');
|
||||||
if (hasMediaQueryPreference) {
|
|
||||||
return mql.matches ? 'dark' : 'light';
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||||
}
|
const newColorMode = e.matches ? 'dark' : 'light';
|
||||||
return 'light';
|
document.documentElement.classList.toggle('dark', newColorMode === 'dark');
|
||||||
}
|
localStorage.setItem('theme', newColorMode);
|
||||||
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');
|
<Scripts />
|
||||||
localStorage.setItem('theme', newColorMode);
|
|
||||||
});
|
|
||||||
})()
|
|
||||||
`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Scripts />
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { ErrorResponse } from "core/http";
|
||||||
|
|
||||||
import ReactDOMServer from "react-dom/server";
|
import ReactDOMServer from "react-dom/server";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useHttp } from "hooks/servicesProvider";
|
||||||
|
|
||||||
const MyComponent = () => {
|
const MyComponent = () => {
|
||||||
return <div>Hello, World!</div>;
|
return <div>Hello, World!</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Routes() {
|
export default function Routes() {
|
||||||
const htmlString = ReactDOMServer.renderToString(<MyComponent />);
|
let http=useHttp();
|
||||||
|
|
||||||
return <div>安装重构</div>;
|
|
||||||
|
return (<div>
|
||||||
|
|
||||||
|
</div>);
|
||||||
}
|
}
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
export interface AppConfig {
|
|
||||||
port: string;
|
|
||||||
host: string;
|
|
||||||
initStatus: string;
|
|
||||||
apiUrl: string;
|
|
||||||
credentials: {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_CONFIG: AppConfig = {
|
|
||||||
port: "22100",
|
|
||||||
host: "localhost",
|
|
||||||
initStatus: "0",
|
|
||||||
apiUrl: "http://127.0.0.1:22000",
|
|
||||||
credentials: {
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface ImportMetaEnv extends Record<string, string> {
|
|
||||||
VITE_PORT: string;
|
|
||||||
VITE_HOST: string;
|
|
||||||
VITE_INIT_STATUS: string;
|
|
||||||
VITE_API_URL: string;
|
|
||||||
VITE_USERNAME: string;
|
|
||||||
VITE_PASSWORD: string;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
import { readFile, writeFile } from "fs/promises";
|
|
||||||
import { resolve } from "path";
|
|
||||||
|
|
||||||
const ENV_PATH = resolve(process.cwd(), ".env");
|
|
||||||
|
|
||||||
export async function loadEnv(): Promise<Record<string, string>> {
|
|
||||||
try {
|
|
||||||
const content = await readFile(ENV_PATH, "utf-8");
|
|
||||||
return content.split("\n").reduce(
|
|
||||||
(acc, line) => {
|
|
||||||
const [key, value] = line.split("=").map((s) => s.trim());
|
|
||||||
if (key && value) {
|
|
||||||
acc[key] = value.replace(/["']/g, "");
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{} as Record<string, string>,
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveEnv(env: Record<string, string>): Promise<void> {
|
|
||||||
const content = Object.entries(env)
|
|
||||||
.map(([key, value]) => `${key}="${value}"`)
|
|
||||||
.join("\n");
|
|
||||||
await writeFile(ENV_PATH, content, "utf-8");
|
|
||||||
}
|
|
@ -1,3 +1,8 @@
|
|||||||
|
export interface ErrorResponse {
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class HttpClient {
|
export class HttpClient {
|
||||||
private static instance: HttpClient;
|
private static instance: HttpClient;
|
||||||
private timeout: number;
|
private timeout: number;
|
||||||
@ -20,7 +25,7 @@ export class HttpClient {
|
|||||||
headers.set("Content-Type", "application/json");
|
headers.set("Content-Type", "application/json");
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = localStorage.getItem("auth_token");
|
const token = localStorage.getItem("token");
|
||||||
if (token) {
|
if (token) {
|
||||||
headers.set("Authorization", `Bearer ${token}`);
|
headers.set("Authorization", `Bearer ${token}`);
|
||||||
}
|
}
|
||||||
@ -31,20 +36,26 @@ export class HttpClient {
|
|||||||
private async handleResponse(response: Response): Promise<any> {
|
private async handleResponse(response: Response): Promise<any> {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const contentType = response.headers.get("content-type");
|
const contentType = response.headers.get("content-type");
|
||||||
let message = `${response.statusText} (${response.status})`;
|
let message = `${response.statusText}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (contentType?.includes("application/json")) {
|
if (contentType?.includes("application/json")) {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
message = error.message || message;
|
message = error.message || message;
|
||||||
} else {
|
} else {
|
||||||
message = this.getErrorMessage(response.status);
|
const textError = await response.text();
|
||||||
|
message = (response.status != 404 && textError) || message;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("解析响应错误:", e);
|
console.error("解析响应错误:", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(message);
|
const errorResponse: ErrorResponse = {
|
||||||
|
title: this.getErrorMessage(response.status),
|
||||||
|
message: message
|
||||||
|
};
|
||||||
|
|
||||||
|
throw errorResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentType = response.headers.get("content-type");
|
const contentType = response.headers.get("content-type");
|
||||||
@ -56,6 +67,7 @@ export class HttpClient {
|
|||||||
private getErrorMessage(status: number): string {
|
private getErrorMessage(status: number): string {
|
||||||
const messages: Record<number, string> = {
|
const messages: Record<number, string> = {
|
||||||
0: "网络连接失败",
|
0: "网络连接失败",
|
||||||
|
400: "请求错误",
|
||||||
401: "未授权访问",
|
401: "未授权访问",
|
||||||
403: "禁止访问",
|
403: "禁止访问",
|
||||||
404: "资源不存在",
|
404: "资源不存在",
|
||||||
@ -65,7 +77,7 @@ export class HttpClient {
|
|||||||
503: "服务不可用",
|
503: "服务不可用",
|
||||||
504: "网关超时",
|
504: "网关超时",
|
||||||
};
|
};
|
||||||
return messages[status] || `请求失败 (${status})`;
|
return messages[status] || `请求失败`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async request<T>(
|
private async request<T>(
|
||||||
@ -91,7 +103,21 @@ export class HttpClient {
|
|||||||
|
|
||||||
return await this.handleResponse(response);
|
return await this.handleResponse(response);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw error.name === "AbortError" ? new Error("请求超时") : error;
|
if (error.name === "AbortError") {
|
||||||
|
const errorResponse: ErrorResponse = {
|
||||||
|
title: "请求超时",
|
||||||
|
message: "服务器响应时间过长,请稍后重试"
|
||||||
|
};
|
||||||
|
throw errorResponse;
|
||||||
|
}
|
||||||
|
if ((error as ErrorResponse).title && (error as ErrorResponse).message) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
const errorResponse: ErrorResponse = {
|
||||||
|
title: "未知错误",
|
||||||
|
message: error.message || "发生未知错误"
|
||||||
|
};
|
||||||
|
throw errorResponse;
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
@ -139,4 +165,14 @@ export class HttpClient {
|
|||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
return this.api<T>(endpoint, { ...options, method: "DELETE" });
|
return this.api<T>(endpoint, { ...options, method: "DELETE" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async systemToken<T>(): Promise<T> {
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
"username": import.meta.env.VITE_API_USERNAME,
|
||||||
|
"password": import.meta.env.VITE_API_PASSWORD
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.api<T>("/auth/token/system", { method: "POST",body: JSON.stringify(formData), });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Configuration, PathDescription } from "common/serializableType";
|
import { Configuration, PathDescription } from "commons/serializableType";
|
||||||
import { ApiService } from "core/api";
|
import { HttpClient } from "core/http";
|
||||||
import { Template } from "core/template";
|
|
||||||
|
|
||||||
export interface ThemeConfig {
|
export interface ThemeConfig {
|
||||||
name: string;
|
name: string;
|
||||||
@ -29,13 +28,13 @@ export interface ThemeConfig {
|
|||||||
export class ThemeService {
|
export class ThemeService {
|
||||||
private static instance: ThemeService;
|
private static instance: ThemeService;
|
||||||
private currentTheme?: ThemeConfig;
|
private currentTheme?: ThemeConfig;
|
||||||
private api: ApiService;
|
private http: HttpClient;
|
||||||
|
|
||||||
private constructor(api: ApiService) {
|
private constructor(api: HttpClient) {
|
||||||
this.api = api;
|
this.http = api;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getInstance(api?: ApiService): ThemeService {
|
public static getInstance(api?: HttpClient): ThemeService {
|
||||||
if (!ThemeService.instance && api) {
|
if (!ThemeService.instance && api) {
|
||||||
ThemeService.instance = new ThemeService(api);
|
ThemeService.instance = new ThemeService(api);
|
||||||
}
|
}
|
||||||
@ -44,7 +43,7 @@ export class ThemeService {
|
|||||||
|
|
||||||
public async getCurrentTheme(): Promise<void> {
|
public async getCurrentTheme(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const themeConfig = await this.api.request<ThemeConfig>("/theme", {
|
const themeConfig = await this.http.api<ThemeConfig>("/theme", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
});
|
});
|
||||||
this.currentTheme = themeConfig;
|
this.currentTheme = themeConfig;
|
||||||
@ -63,7 +62,7 @@ export class ThemeService {
|
|||||||
name: string,
|
name: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const updatedConfig = await this.api.request<ThemeConfig>(`/theme/`, {
|
const updatedConfig = await this.http.api<ThemeConfig>(`/theme/`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
@ -4,6 +4,7 @@ interface Message {
|
|||||||
id: number;
|
id: number;
|
||||||
type: "success" | "error" | "info" | "warning";
|
type: "success" | "error" | "info" | "warning";
|
||||||
content: string;
|
content: string;
|
||||||
|
title?: string;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,6 +18,7 @@ interface MessageContextType {
|
|||||||
addMessage: (
|
addMessage: (
|
||||||
type: Message["type"],
|
type: Message["type"],
|
||||||
content: string,
|
content: string,
|
||||||
|
title?: string,
|
||||||
duration?: number,
|
duration?: number,
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
@ -38,12 +40,13 @@ export const MessageProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
const addMessage = (
|
const addMessage = (
|
||||||
type: Message["type"],
|
type: Message["type"],
|
||||||
content: string,
|
content: string,
|
||||||
|
title?: string,
|
||||||
duration = 3000,
|
duration = 3000,
|
||||||
) => {
|
) => {
|
||||||
const id = Date.now();
|
const id = Date.now();
|
||||||
|
|
||||||
setMessages((prevMessages) => {
|
setMessages((prevMessages) => {
|
||||||
const newMessages = [...prevMessages, { id, type, content }];
|
const newMessages = [...prevMessages, { id, type, content, title }];
|
||||||
return newMessages;
|
return newMessages;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -100,17 +103,26 @@ export const MessageProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<div style={{ flex: 1 }}>
|
||||||
style={{
|
{msg.title && (
|
||||||
fontSize: "14px",
|
<div style={{
|
||||||
lineHeight: "1.5",
|
fontSize: "14px",
|
||||||
marginRight: "12px",
|
fontWeight: "bold",
|
||||||
flex: 1,
|
marginBottom: "4px",
|
||||||
wordBreak: "break-word",
|
}}>
|
||||||
}}
|
{msg.title}
|
||||||
>
|
</div>
|
||||||
{msg.content}
|
)}
|
||||||
</span>
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: "14px",
|
||||||
|
lineHeight: "1.5",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{msg.content}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => removeMessage(msg.id)}
|
onClick={() => removeMessage(msg.id)}
|
||||||
style={{
|
style={{
|
||||||
@ -180,7 +192,7 @@ export const MessageProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
|
|
||||||
// 修改全局消息实例的实现
|
// 修改全局消息实例的实现
|
||||||
let globalAddMessage:
|
let globalAddMessage:
|
||||||
| ((type: Message["type"], content: string, duration?: number) => void)
|
| ((type: Message["type"], content: string, title?: string, duration?: number) => void)
|
||||||
| null = null;
|
| null = null;
|
||||||
|
|
||||||
export const MessageContainer: React.FC = () => {
|
export const MessageContainer: React.FC = () => {
|
||||||
@ -198,32 +210,32 @@ export const MessageContainer: React.FC = () => {
|
|||||||
|
|
||||||
// 修改消息方法的实现
|
// 修改消息方法的实现
|
||||||
export const message = {
|
export const message = {
|
||||||
success: (content: string) => {
|
success: (content: string, title?: string) => {
|
||||||
if (!globalAddMessage) {
|
if (!globalAddMessage) {
|
||||||
console.warn("Message system not initialized");
|
console.warn("Message system not initialized");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
globalAddMessage("success", content);
|
globalAddMessage("success", content, title);
|
||||||
},
|
},
|
||||||
error: (content: string) => {
|
error: (content: string, title?: string) => {
|
||||||
if (!globalAddMessage) {
|
if (!globalAddMessage) {
|
||||||
console.warn("Message system not initialized");
|
console.warn("Message system not initialized");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
globalAddMessage("error", content);
|
globalAddMessage("error", content, title);
|
||||||
},
|
},
|
||||||
warning: (content: string) => {
|
warning: (content: string, title?: string) => {
|
||||||
if (!globalAddMessage) {
|
if (!globalAddMessage) {
|
||||||
console.warn("Message system not initialized");
|
console.warn("Message system not initialized");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
globalAddMessage("warning", content);
|
globalAddMessage("warning", content, title);
|
||||||
},
|
},
|
||||||
info: (content: string) => {
|
info: (content: string, title?: string) => {
|
||||||
if (!globalAddMessage) {
|
if (!globalAddMessage) {
|
||||||
console.warn("Message system not initialized");
|
console.warn("Message system not initialized");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
globalAddMessage("info", content);
|
globalAddMessage("info", content, title);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -3,6 +3,7 @@ import { HttpClient } from "core/http";
|
|||||||
import { RouteManager } from "core/route";
|
import { RouteManager } from "core/route";
|
||||||
import { createServiceContext } from "hooks/createServiceContext";
|
import { createServiceContext } from "hooks/createServiceContext";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
import { StyleProvider } from "hooks/stylesProvider";
|
||||||
|
|
||||||
export const { CapabilityProvider, useCapability } = createServiceContext(
|
export const { CapabilityProvider, useCapability } = createServiceContext(
|
||||||
"Capability",
|
"Capability",
|
||||||
@ -20,7 +21,9 @@ export const { HttpProvider, useHttp } = createServiceContext("Http", () =>
|
|||||||
export const BaseProvider = ({ children }: { children: ReactNode }) => (
|
export const BaseProvider = ({ children }: { children: ReactNode }) => (
|
||||||
<HttpProvider>
|
<HttpProvider>
|
||||||
<CapabilityProvider>
|
<CapabilityProvider>
|
||||||
<RouteProvider>{children}</RouteProvider>
|
<StyleProvider>
|
||||||
|
<RouteProvider>{children}</RouteProvider>
|
||||||
|
</StyleProvider>
|
||||||
</CapabilityProvider>
|
</CapabilityProvider>
|
||||||
</HttpProvider>
|
</HttpProvider>
|
||||||
);
|
);
|
||||||
|
10
frontend/hooks/stylesProvider.tsx
Normal file
10
frontend/hooks/stylesProvider.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { MessageProvider, MessageContainer } from "hooks/message";
|
||||||
|
|
||||||
|
export const StyleProvider = ({ children }: { children: ReactNode }) => (
|
||||||
|
<MessageProvider>
|
||||||
|
<MessageContainer />
|
||||||
|
{children}
|
||||||
|
</MessageProvider>
|
||||||
|
);
|
@ -12,6 +12,9 @@
|
|||||||
"typecheck": "tsc"
|
"typecheck": "tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@radix-ui/react-dialog": "^1.1.2",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
|
"@radix-ui/react-toast": "^1.2.2",
|
||||||
"@remix-run/node": "^2.14.0",
|
"@remix-run/node": "^2.14.0",
|
||||||
"@remix-run/react": "^2.14.0",
|
"@remix-run/react": "^2.14.0",
|
||||||
"@remix-run/serve": "^2.14.0",
|
"@remix-run/serve": "^2.14.0",
|
||||||
|
@ -66,7 +66,6 @@ app.post("/env", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
app.listen(port + 1, address, () => {
|
app.listen(port + 1, address, () => {
|
||||||
console.log(`内部服务器运行在 http://${address}:${port + 1}`);
|
console.log(`内部服务器运行在 http://${address}:${port + 1}`);
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { spawn } from "child_process";
|
import { spawn } from "child_process";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from "events";
|
||||||
|
|
||||||
// 设置全局最大监听器数量
|
// 设置全局最大监听器数量
|
||||||
EventEmitter.defaultMaxListeners = 20
|
EventEmitter.defaultMaxListeners = 20;
|
||||||
|
|
||||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ export default {
|
|||||||
"./app/**/*.{js,jsx,ts,tsx}",
|
"./app/**/*.{js,jsx,ts,tsx}",
|
||||||
"./common/**/*.{js,jsx,ts,tsx}",
|
"./common/**/*.{js,jsx,ts,tsx}",
|
||||||
"./core/**/*.{js,jsx,ts,tsx}",
|
"./core/**/*.{js,jsx,ts,tsx}",
|
||||||
|
"./hooks/**/*.{js,jsx,ts,tsx}",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
@ -14,23 +15,28 @@ export default {
|
|||||||
colors: {
|
colors: {
|
||||||
custom: {
|
custom: {
|
||||||
bg: {
|
bg: {
|
||||||
light: "#F5F5FB",
|
light: "#FAFAFA", // 更柔和的背景色
|
||||||
dark: "#0F172A",
|
dark: "#111827", // 更深邃的暗色背景
|
||||||
},
|
},
|
||||||
box: {
|
box: {
|
||||||
light: "#FFFFFF",
|
light: "#FFFFFF",
|
||||||
dark: "#1E293B",
|
dark: "#1E293B",
|
||||||
},
|
},
|
||||||
p: {
|
p: {
|
||||||
light: "#4b5563",
|
light: "#374151", // 更清晰的文本颜色
|
||||||
dark: "#94A3B8",
|
dark: "#D1D5DB", // 更亮的暗色文本,提高可读性
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
light: "#111827",
|
light: "#111827",
|
||||||
dark: "#F1F5F9",
|
dark: "#F8FAFC", // 更亮的标题颜色
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
animation: {
|
||||||
|
hide: 'hide 100ms ease-in',
|
||||||
|
slideIn: 'slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1)',
|
||||||
|
swipeOut: 'swipeOut 100ms ease-out',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
darkMode: "class",
|
darkMode: "class",
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
"types": ["@remix-run/node", "vite/client"],
|
"types": ["@remix-run/node", "vite/client", "@radix-ui/react-toast"],
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
@ -30,7 +30,6 @@ const createDefineConfig = (config: EnvConfig) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default defineConfig(async ({ mode }) => {
|
export default defineConfig(async ({ mode }) => {
|
||||||
// 确保每次都读取最新的环境变量
|
// 确保每次都读取最新的环境变量
|
||||||
const currentConfig = await getLatestEnv();
|
const currentConfig = await getLatestEnv();
|
||||||
|
5
package.json
Normal file
5
package.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-toast": "^1.2.2"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user