后端:修复数据库异常连接导致后续无法正常重启;
前端:消息提示增加标题,添加Radix UI,重构HTTP错误处理逻辑。
This commit is contained in:
parent
72db2c9de5
commit
a43a9bac36
@ -33,7 +33,7 @@ pub async fn setup_sql(
|
||||
.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())
|
||||
}
|
||||
|
||||
@ -123,7 +123,7 @@ pub async fn setup_account(
|
||||
.into_app_result()?;
|
||||
config.init.administrator = true;
|
||||
config::Config::write(config).into_app_result()?;
|
||||
state.restart_server().await.into_app_result()?;
|
||||
state.trigger_restart().await.into_app_result()?;
|
||||
|
||||
Ok(status::Custom(
|
||||
Status::Ok,
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::common::error::{CustomErrorInto, CustomResult};
|
||||
use crate::security::bcrypt;
|
||||
use crate::storage::{sql, sql::builder};
|
||||
use regex::Regex;
|
||||
use rocket::{get, http::Status, post, response::status, serde::json::Json, State};
|
||||
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 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(
|
||||
builder::SqlOperation::Insert,
|
||||
sql.table_name("users"),
|
||||
|
@ -13,7 +13,6 @@ pub struct AppState {
|
||||
db: Arc<Mutex<Option<sql::Database>>>,
|
||||
shutdown: Arc<Mutex<Option<Shutdown>>>,
|
||||
restart_progress: Arc<Mutex<bool>>,
|
||||
restart_attempts: Arc<Mutex<u32>>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@ -22,7 +21,6 @@ impl AppState {
|
||||
db: Arc::new(Mutex::new(None)),
|
||||
shutdown: Arc::new(Mutex::new(None)),
|
||||
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<()> {
|
||||
*self.db.lock().await = Some(sql::Database::link(config).await?);
|
||||
Ok(())
|
||||
match sql::Database::link(config).await {
|
||||
Ok(db) => {
|
||||
*self.db.lock().await = Some(db);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_shutdown(&self, shutdown: Shutdown) {
|
||||
@ -54,27 +59,6 @@ impl AppState {
|
||||
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]
|
||||
@ -115,31 +99,15 @@ async fn main() -> CustomResult<()> {
|
||||
rocket.launch().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() {
|
||||
println!("正在尝试重启服务器...");
|
||||
|
||||
let mut command = std::process::Command::new(current_exe);
|
||||
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());
|
||||
}
|
||||
match std::process::Command::new(current_exe).spawn() {
|
||||
Ok(_) => println!("成功启动新进程"),
|
||||
Err(e) => eprintln!("启动新进程失败: {}", e),
|
||||
};
|
||||
} else {
|
||||
eprintln!("获取当前可执行文件路径失败");
|
||||
return Err("重启失败: 无法获取可执行文件路径".into_custom_error());
|
||||
}
|
||||
}
|
||||
|
||||
println!("服务器正常退出");
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ impl Default for TextValidator {
|
||||
impl TextValidator {
|
||||
pub fn validate(&self, text: &str, level: ValidationLevel) -> CustomResult<()> {
|
||||
if level == ValidationLevel::Raw {
|
||||
return self.validate_sql_patterns(text);
|
||||
return Ok(());
|
||||
}
|
||||
let max_length = self
|
||||
.level_max_lengths
|
||||
|
@ -1,3 +1,44 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@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;
|
||||
}
|
||||
});
|
||||
message.error(`请填写以下必填项:${fieldNames.join('、')}`);
|
||||
message.error(`请填写以下必填项:${fieldNames.join('、')}`, '验证失败');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -180,11 +180,11 @@ const DatabaseConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
|
||||
Object.assign( newEnv)
|
||||
|
||||
message.success('数据库配置成功!');
|
||||
message.success('数据库配置已保存', '配置成功');
|
||||
setTimeout(() => onNext(), 1000);
|
||||
} catch (error: any) {
|
||||
console.error( error);
|
||||
message.error(error.message );
|
||||
message.error(error.message , error.title || '配置失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -369,11 +369,11 @@ const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
body: JSON.stringify(newEnv),
|
||||
});
|
||||
|
||||
message.success('管理员账号创建成功!');
|
||||
message.success('管理员账号已创建,即将进入下一步', '创建成功');
|
||||
onNext();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
message.error(error.message);
|
||||
message.error(error.message, error.title || '创建失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
@ -7,8 +7,7 @@ import {
|
||||
} from "@remix-run/react";
|
||||
|
||||
import { BaseProvider } from "hooks/servicesProvider";
|
||||
import { MessageProvider } from "hooks/message";
|
||||
import { MessageContainer } from "hooks/message";
|
||||
|
||||
|
||||
import "~/index.css";
|
||||
|
||||
@ -24,42 +23,40 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
</head>
|
||||
<body suppressHydrationWarning={true}>
|
||||
<BaseProvider>
|
||||
<MessageProvider>
|
||||
<MessageContainer />
|
||||
<Outlet />
|
||||
</MessageProvider>
|
||||
</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 />
|
||||
<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>
|
||||
);
|
||||
|
@ -1,14 +1,18 @@
|
||||
import { useState } from "react";
|
||||
import { ErrorResponse } from "core/http";
|
||||
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useHttp } from "hooks/servicesProvider";
|
||||
|
||||
const MyComponent = () => {
|
||||
return <div>Hello, World!</div>;
|
||||
};
|
||||
|
||||
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 {
|
||||
private static instance: HttpClient;
|
||||
private timeout: number;
|
||||
@ -20,7 +25,7 @@ export class HttpClient {
|
||||
headers.set("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
const token = localStorage.getItem("auth_token");
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
@ -31,20 +36,26 @@ export class HttpClient {
|
||||
private async handleResponse(response: Response): Promise<any> {
|
||||
if (!response.ok) {
|
||||
const contentType = response.headers.get("content-type");
|
||||
let message = `${response.statusText} (${response.status})`;
|
||||
let message = `${response.statusText}`;
|
||||
|
||||
try {
|
||||
if (contentType?.includes("application/json")) {
|
||||
const error = await response.json();
|
||||
message = error.message || message;
|
||||
} else {
|
||||
message = this.getErrorMessage(response.status);
|
||||
const textError = await response.text();
|
||||
message = (response.status != 404 && textError) || message;
|
||||
}
|
||||
} catch (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");
|
||||
@ -56,6 +67,7 @@ export class HttpClient {
|
||||
private getErrorMessage(status: number): string {
|
||||
const messages: Record<number, string> = {
|
||||
0: "网络连接失败",
|
||||
400: "请求错误",
|
||||
401: "未授权访问",
|
||||
403: "禁止访问",
|
||||
404: "资源不存在",
|
||||
@ -65,7 +77,7 @@ export class HttpClient {
|
||||
503: "服务不可用",
|
||||
504: "网关超时",
|
||||
};
|
||||
return messages[status] || `请求失败 (${status})`;
|
||||
return messages[status] || `请求失败`;
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
@ -91,7 +103,21 @@ export class HttpClient {
|
||||
|
||||
return await this.handleResponse(response);
|
||||
} 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 {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
@ -139,4 +165,14 @@ export class HttpClient {
|
||||
): Promise<T> {
|
||||
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 { ApiService } from "core/api";
|
||||
import { Template } from "core/template";
|
||||
import { Configuration, PathDescription } from "commons/serializableType";
|
||||
import { HttpClient } from "core/http";
|
||||
|
||||
export interface ThemeConfig {
|
||||
name: string;
|
||||
@ -29,13 +28,13 @@ export interface ThemeConfig {
|
||||
export class ThemeService {
|
||||
private static instance: ThemeService;
|
||||
private currentTheme?: ThemeConfig;
|
||||
private api: ApiService;
|
||||
private http: HttpClient;
|
||||
|
||||
private constructor(api: ApiService) {
|
||||
this.api = api;
|
||||
private constructor(api: HttpClient) {
|
||||
this.http = api;
|
||||
}
|
||||
|
||||
public static getInstance(api?: ApiService): ThemeService {
|
||||
public static getInstance(api?: HttpClient): ThemeService {
|
||||
if (!ThemeService.instance && api) {
|
||||
ThemeService.instance = new ThemeService(api);
|
||||
}
|
||||
@ -44,7 +43,7 @@ export class ThemeService {
|
||||
|
||||
public async getCurrentTheme(): Promise<void> {
|
||||
try {
|
||||
const themeConfig = await this.api.request<ThemeConfig>("/theme", {
|
||||
const themeConfig = await this.http.api<ThemeConfig>("/theme", {
|
||||
method: "GET",
|
||||
});
|
||||
this.currentTheme = themeConfig;
|
||||
@ -63,7 +62,7 @@ export class ThemeService {
|
||||
name: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const updatedConfig = await this.api.request<ThemeConfig>(`/theme/`, {
|
||||
const updatedConfig = await this.http.api<ThemeConfig>(`/theme/`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
@ -4,6 +4,7 @@ interface Message {
|
||||
id: number;
|
||||
type: "success" | "error" | "info" | "warning";
|
||||
content: string;
|
||||
title?: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
@ -17,6 +18,7 @@ interface MessageContextType {
|
||||
addMessage: (
|
||||
type: Message["type"],
|
||||
content: string,
|
||||
title?: string,
|
||||
duration?: number,
|
||||
) => void;
|
||||
}
|
||||
@ -38,12 +40,13 @@ export const MessageProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
const addMessage = (
|
||||
type: Message["type"],
|
||||
content: string,
|
||||
title?: string,
|
||||
duration = 3000,
|
||||
) => {
|
||||
const id = Date.now();
|
||||
|
||||
setMessages((prevMessages) => {
|
||||
const newMessages = [...prevMessages, { id, type, content }];
|
||||
const newMessages = [...prevMessages, { id, type, content, title }];
|
||||
return newMessages;
|
||||
});
|
||||
|
||||
@ -100,17 +103,26 @@ export const MessageProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
lineHeight: "1.5",
|
||||
marginRight: "12px",
|
||||
flex: 1,
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{msg.content}
|
||||
</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
{msg.title && (
|
||||
<div style={{
|
||||
fontSize: "14px",
|
||||
fontWeight: "bold",
|
||||
marginBottom: "4px",
|
||||
}}>
|
||||
{msg.title}
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
lineHeight: "1.5",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{msg.content}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeMessage(msg.id)}
|
||||
style={{
|
||||
@ -180,7 +192,7 @@ export const MessageProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
|
||||
// 修改全局消息实例的实现
|
||||
let globalAddMessage:
|
||||
| ((type: Message["type"], content: string, duration?: number) => void)
|
||||
| ((type: Message["type"], content: string, title?: string, duration?: number) => void)
|
||||
| null = null;
|
||||
|
||||
export const MessageContainer: React.FC = () => {
|
||||
@ -198,32 +210,32 @@ export const MessageContainer: React.FC = () => {
|
||||
|
||||
// 修改消息方法的实现
|
||||
export const message = {
|
||||
success: (content: string) => {
|
||||
success: (content: string, title?: string) => {
|
||||
if (!globalAddMessage) {
|
||||
console.warn("Message system not initialized");
|
||||
return;
|
||||
}
|
||||
globalAddMessage("success", content);
|
||||
globalAddMessage("success", content, title);
|
||||
},
|
||||
error: (content: string) => {
|
||||
error: (content: string, title?: string) => {
|
||||
if (!globalAddMessage) {
|
||||
console.warn("Message system not initialized");
|
||||
return;
|
||||
}
|
||||
globalAddMessage("error", content);
|
||||
globalAddMessage("error", content, title);
|
||||
},
|
||||
warning: (content: string) => {
|
||||
warning: (content: string, title?: string) => {
|
||||
if (!globalAddMessage) {
|
||||
console.warn("Message system not initialized");
|
||||
return;
|
||||
}
|
||||
globalAddMessage("warning", content);
|
||||
globalAddMessage("warning", content, title);
|
||||
},
|
||||
info: (content: string) => {
|
||||
info: (content: string, title?: string) => {
|
||||
if (!globalAddMessage) {
|
||||
console.warn("Message system not initialized");
|
||||
return;
|
||||
}
|
||||
globalAddMessage("info", content);
|
||||
globalAddMessage("info", content, title);
|
||||
},
|
||||
};
|
||||
|
@ -3,6 +3,7 @@ import { HttpClient } from "core/http";
|
||||
import { RouteManager } from "core/route";
|
||||
import { createServiceContext } from "hooks/createServiceContext";
|
||||
import { ReactNode } from "react";
|
||||
import { StyleProvider } from "hooks/stylesProvider";
|
||||
|
||||
export const { CapabilityProvider, useCapability } = createServiceContext(
|
||||
"Capability",
|
||||
@ -20,7 +21,9 @@ export const { HttpProvider, useHttp } = createServiceContext("Http", () =>
|
||||
export const BaseProvider = ({ children }: { children: ReactNode }) => (
|
||||
<HttpProvider>
|
||||
<CapabilityProvider>
|
||||
<RouteProvider>{children}</RouteProvider>
|
||||
<StyleProvider>
|
||||
<RouteProvider>{children}</RouteProvider>
|
||||
</StyleProvider>
|
||||
</CapabilityProvider>
|
||||
</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"
|
||||
},
|
||||
"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/react": "^2.14.0",
|
||||
"@remix-run/serve": "^2.14.0",
|
||||
|
@ -66,7 +66,6 @@ app.post("/env", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.listen(port + 1, address, () => {
|
||||
console.log(`内部服务器运行在 http://${address}:${port + 1}`);
|
||||
});
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { spawn } from "child_process";
|
||||
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));
|
||||
|
||||
|
@ -5,6 +5,7 @@ export default {
|
||||
"./app/**/*.{js,jsx,ts,tsx}",
|
||||
"./common/**/*.{js,jsx,ts,tsx}",
|
||||
"./core/**/*.{js,jsx,ts,tsx}",
|
||||
"./hooks/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
@ -14,23 +15,28 @@ export default {
|
||||
colors: {
|
||||
custom: {
|
||||
bg: {
|
||||
light: "#F5F5FB",
|
||||
dark: "#0F172A",
|
||||
light: "#FAFAFA", // 更柔和的背景色
|
||||
dark: "#111827", // 更深邃的暗色背景
|
||||
},
|
||||
box: {
|
||||
light: "#FFFFFF",
|
||||
dark: "#1E293B",
|
||||
},
|
||||
p: {
|
||||
light: "#4b5563",
|
||||
dark: "#94A3B8",
|
||||
light: "#374151", // 更清晰的文本颜色
|
||||
dark: "#D1D5DB", // 更亮的暗色文本,提高可读性
|
||||
},
|
||||
title: {
|
||||
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",
|
||||
|
@ -9,7 +9,7 @@
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"types": ["@remix-run/node", "vite/client"],
|
||||
"types": ["@remix-run/node", "vite/client", "@radix-ui/react-toast"],
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
|
@ -30,7 +30,6 @@ const createDefineConfig = (config: EnvConfig) => {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default defineConfig(async ({ mode }) => {
|
||||
// 确保每次都读取最新的环境变量
|
||||
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