后端:修复数据库异常连接导致后续无法正常重启;

前端:消息提示增加标题,添加Radix UI,重构HTTP错误处理逻辑。
This commit is contained in:
lsy 2024-11-30 14:03:32 +08:00
parent 72db2c9de5
commit a43a9bac36
23 changed files with 227 additions and 199 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
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) => { <ScrollRestoration />
const newColorMode = e.matches ? 'dark' : 'light'; <script
document.documentElement.classList.toggle('dark', newColorMode === 'dark'); dangerouslySetInnerHTML={{
localStorage.setItem('theme', newColorMode); __html: `
}); (function() {
})() function getInitialColorMode() {
`, const persistedColorPreference = window.localStorage.getItem('theme');
}} const hasPersistedPreference = typeof persistedColorPreference === 'string';
/> if (hasPersistedPreference) {
<Scripts /> 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> </body>
</html> </html>
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -0,0 +1,5 @@
{
"dependencies": {
"@radix-ui/react-toast": "^1.2.2"
}
}