From 43e21e1d49c1caeb32812d9a64e9c9fe91413ec5 Mon Sep 17 00:00:00 2001 From: lsy Date: Wed, 4 Dec 2024 02:35:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=EF=BC=9A=E6=9B=B4=E6=8D=A2?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E5=92=8C=E4=BC=98=E5=8C=96echoes=E5=9B=BE?= =?UTF-8?q?=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/main.rs | 11 +- backend/src/storage/sql/mod.rs | 8 +- backend/src/storage/sql/mysql.rs | 14 +- backend/src/storage/sql/postgresql.rs | 13 +- backend/src/storage/sql/sqllite.rs | 11 +- frontend/app/env.ts | 4 +- frontend/app/index.css | 186 +++++++------- frontend/app/init.tsx | 322 ++++++++++++++----------- frontend/app/page.tsx | 11 + frontend/app/root.tsx | 70 ++---- frontend/app/routes.tsx | 8 +- frontend/core/capability.ts | 2 +- frontend/core/http.ts | 34 +-- frontend/core/template.ts | 5 +- frontend/hooks/echo.tsx | 44 ---- frontend/hooks/echoes.tsx | 43 ++++ frontend/hooks/error.tsx | 4 +- frontend/hooks/loading.tsx | 6 +- frontend/hooks/notification.tsx | 84 ++++--- frontend/hooks/themeMode.tsx | 73 ++++-- frontend/hooks/tide.tsx | 239 ++++++++++++++++++ frontend/interface/layout.ts | 15 +- frontend/interface/template.ts | 10 +- frontend/interface/theme.ts | 2 +- frontend/package.json | 1 + frontend/public/echoes.png | Bin 88110 -> 0 bytes frontend/public/favicon.ico | Bin 10106 -> 171737 bytes frontend/start.ts | 6 +- frontend/tailwind.config.ts | 14 +- frontend/themes/echoes/article.tsx | 13 +- frontend/themes/echoes/layout.tsx | 175 +++++++++----- frontend/themes/echoes/theme.config.ts | 16 +- 32 files changed, 907 insertions(+), 537 deletions(-) create mode 100644 frontend/app/page.tsx delete mode 100644 frontend/hooks/echo.tsx create mode 100644 frontend/hooks/echoes.tsx create mode 100644 frontend/hooks/tide.tsx delete mode 100644 frontend/public/echoes.png diff --git a/backend/src/main.rs b/backend/src/main.rs index 1062afc..47b0edc 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -38,9 +38,7 @@ impl AppState { *self.db.lock().await = Some(db); Ok(()) } - Err(e) => { - Err(e) - } + Err(e) => Err(e), } } @@ -50,9 +48,9 @@ impl AppState { pub async fn trigger_restart(&self) -> CustomResult<()> { *self.restart_progress.lock().await = true; - if let Ok(db) = self.sql_get().await{ + if let Ok(db) = self.sql_get().await { db.get_db().close().await?; - } + } self.shutdown .lock() .await @@ -61,7 +59,6 @@ impl AppState { .notify(); Ok(()) } - } #[rocket::main] @@ -111,6 +108,6 @@ async fn main() -> CustomResult<()> { eprintln!("获取当前可执行文件路径失败"); } } - + std::process::exit(0); } diff --git a/backend/src/storage/sql/mod.rs b/backend/src/storage/sql/mod.rs index 9b44004..24e38ea 100644 --- a/backend/src/storage/sql/mod.rs +++ b/backend/src/storage/sql/mod.rs @@ -28,7 +28,7 @@ impl std::fmt::Display for DatabaseType { #[async_trait] pub trait DatabaseTrait: Send + Sync { - async fn connect(database: &config::SqlConfig,db:bool) -> CustomResult + async fn connect(database: &config::SqlConfig, db: bool) -> CustomResult where Self: Sized; async fn execute_query<'a>( @@ -67,9 +67,9 @@ impl Database { pub async fn link(database: &config::SqlConfig) -> CustomResult { let db: Box = match database.db_type.to_lowercase().as_str() { - "postgresql" => Box::new(postgresql::Postgresql::connect(database,true).await?), - "mysql" => Box::new(mysql::Mysql::connect(database,true).await?), - "sqllite" => Box::new(sqllite::Sqlite::connect(database,true).await?), + "postgresql" => Box::new(postgresql::Postgresql::connect(database, true).await?), + "mysql" => Box::new(mysql::Mysql::connect(database, true).await?), + "sqllite" => Box::new(sqllite::Sqlite::connect(database, true).await?), _ => return Err("unknown database type".into_custom_error()), }; diff --git a/backend/src/storage/sql/mysql.rs b/backend/src/storage/sql/mysql.rs index 419105f..805af5c 100644 --- a/backend/src/storage/sql/mysql.rs +++ b/backend/src/storage/sql/mysql.rs @@ -2,7 +2,7 @@ use super::{ builder::{self, SafeValue}, schema, DatabaseTrait, }; -use crate::common::error::{CustomResult,CustomErrorInto}; +use crate::common::error::{CustomErrorInto, CustomResult}; use crate::config; use async_trait::async_trait; use serde_json::Value; @@ -37,15 +37,16 @@ impl DatabaseTrait for Mysql { let pool = tokio::time::timeout( std::time::Duration::from_secs(5), - MySqlPool::connect(&connection_str) - ).await.map_err(|_| "连接超时".into_custom_error())??; + MySqlPool::connect(&connection_str), + ) + .await + .map_err(|_| "连接超时".into_custom_error())??; - if let Err(e) = pool.acquire().await{ + if let Err(e) = pool.acquire().await { pool.close().await; - return Err(format!("数据库连接测试失败: {}", e).into_custom_error()); + return Err(format!("数据库连接测试失败: {}", e).into_custom_error()); } - Ok(Mysql { pool }) } async fn execute_query<'a>( @@ -102,7 +103,6 @@ impl DatabaseTrait for Mysql { ); let grammar = schema::generate_schema(super::DatabaseType::MySQL, db_prefix)?; - let pool = Self::connect(&db_config, false).await?.pool; pool.execute(format!("CREATE DATABASE `{}`", db_config.db_name).as_str()) diff --git a/backend/src/storage/sql/postgresql.rs b/backend/src/storage/sql/postgresql.rs index 50d5627..22e7d4b 100644 --- a/backend/src/storage/sql/postgresql.rs +++ b/backend/src/storage/sql/postgresql.rs @@ -2,7 +2,7 @@ use super::{ builder::{self, SafeValue}, schema, DatabaseTrait, }; -use crate::common::error::{CustomResult,CustomErrorInto}; +use crate::common::error::{CustomErrorInto, CustomResult}; use crate::config; use async_trait::async_trait; use serde_json::Value; @@ -34,15 +34,16 @@ impl DatabaseTrait for Postgresql { ); } - let pool = tokio::time::timeout( std::time::Duration::from_secs(5), - PgPool::connect(&connection_str) - ).await.map_err(|_| "连接超时".into_custom_error())??; + PgPool::connect(&connection_str), + ) + .await + .map_err(|_| "连接超时".into_custom_error())??; - if let Err(e) = pool.acquire().await{ + if let Err(e) = pool.acquire().await { pool.close().await; - return Err(format!("数据库连接测试失败: {}", e).into_custom_error()); + return Err(format!("数据库连接测试失败: {}", e).into_custom_error()); } Ok(Postgresql { pool }) diff --git a/backend/src/storage/sql/sqllite.rs b/backend/src/storage/sql/sqllite.rs index 49d1778..e864233 100644 --- a/backend/src/storage/sql/sqllite.rs +++ b/backend/src/storage/sql/sqllite.rs @@ -34,15 +34,16 @@ impl DatabaseTrait for Sqlite { let pool = tokio::time::timeout( std::time::Duration::from_secs(5), - SqlitePool::connect(&connection_str) - ).await.map_err(|_| "连接超时".into_custom_error())??; + SqlitePool::connect(&connection_str), + ) + .await + .map_err(|_| "连接超时".into_custom_error())??; - if let Err(e) = pool.acquire().await{ + if let Err(e) = pool.acquire().await { pool.close().await; - return Err(format!("数据库连接测试失败: {}", e).into_custom_error()); + return Err(format!("数据库连接测试失败: {}", e).into_custom_error()); } - Ok(Sqlite { pool }) } diff --git a/frontend/app/env.ts b/frontend/app/env.ts index 32a086d..01d7e98 100644 --- a/frontend/app/env.ts +++ b/frontend/app/env.ts @@ -15,12 +15,12 @@ export const DEFAULT_CONFIG: EnvConfig = { VITE_API_BASE_URL: "http://127.0.0.1:22000", VITE_API_USERNAME: "", VITE_API_PASSWORD: "", - VITE_PATTERN: "true" + VITE_PATTERN: "true", } as const; // 扩展 ImportMeta 接口 declare global { - interface ImportMetaEnv extends EnvConfig { } + interface ImportMetaEnv extends EnvConfig {} interface ImportMeta { readonly env: ImportMetaEnv; } diff --git a/frontend/app/index.css b/frontend/app/index.css index 416aec6..b39a8dd 100644 --- a/frontend/app/index.css +++ b/frontend/app/index.css @@ -6,7 +6,6 @@ :root { --transition-duration: 150ms; --transition-easing: cubic-bezier(0.4, 0, 0.2, 1); - --logo-path-length: 1000; } /* 基础过渡效果 */ @@ -16,102 +15,127 @@ color var(--transition-duration) var(--transition-easing); } -/* 主题过渡效果 */ -.dark body, -body { - transition: background-color var(--transition-duration) var(--transition-easing); -} - /* 基础布局样式 */ html, body { height: 100%; } -/* 响应式调整 */ -@media (max-width: 640px) { - html { - font-size: 14px; - } -} - /* Logo 动画 */ -@keyframes logo-anim { - 0% { - stroke-dashoffset: var(--logo-path-length); - stroke-dasharray: var(--logo-path-length) var(--logo-path-length); - opacity: 0; - fill: transparent; - } - - 5% { - opacity: 1; - stroke-dashoffset: var(--logo-path-length); - stroke-dasharray: var(--logo-path-length) var(--logo-path-length); - } - - /* 慢速绘画过程 */ - 45% { - stroke-dashoffset: 0; - stroke-dasharray: var(--logo-path-length) var(--logo-path-length); - fill: transparent; - } - - /* 慢慢填充效果 */ - 50% { - stroke-dashoffset: 0; - stroke-dasharray: var(--logo-path-length) var(--logo-path-length); - fill: currentColor; - opacity: 1; - } - - /* 保持填充状态 */ - 75% { - stroke-dashoffset: 0; - stroke-dasharray: var(--logo-path-length) var(--logo-path-length); - fill: currentColor; - opacity: 1; - } - - /* 变回线条 */ - 85% { - stroke-dashoffset: 0; - stroke-dasharray: var(--logo-path-length) var(--logo-path-length); - fill: transparent; - opacity: 1; - } - - /* 线条消失 */ - 95% { - stroke-dashoffset: var(--logo-path-length); - stroke-dasharray: var(--logo-path-length) var(--logo-path-length); - fill: transparent; - opacity: 1; - } - - 100% { - stroke-dashoffset: var(--logo-path-length); - stroke-dasharray: var(--logo-path-length) var(--logo-path-length); - fill: transparent; - opacity: 0; - } +.animated-text { + max-width: 100%; + height: auto; } -#logo-anim, -#logo-anim path { +.animated-text path { fill: transparent; stroke: currentColor; stroke-width: 2; - stroke-dashoffset: var(--logo-path-length); - stroke-dasharray: var(--logo-path-length) var(--logo-path-length); + stroke-dasharray: var(--path-length); + stroke-dashoffset: var(--path-length); animation: logo-anim 15s cubic-bezier(0.4, 0, 0.2, 1) infinite; transform-origin: center; stroke-linecap: round; stroke-linejoin: round; } -/* 确保 Logo 在暗色模式下的颜色正确 */ -.dark #logo-anim, -.dark #logo-anim path { - stroke: currentColor; +@keyframes logo-anim { + 0% { + stroke-dashoffset: var(--path-length); + stroke-dasharray: var(--path-length) var(--path-length); + fill: transparent; + opacity: 0; + } + + 5% { + opacity: 1; + stroke-dashoffset: var(--path-length); + stroke-dasharray: var(--path-length) var(--path-length); + } + + 50% { + stroke-dashoffset: 0; + stroke-dasharray: var(--path-length) var(--path-length); + fill: transparent; + } + + 60%, 75% { + stroke-dashoffset: 0; + stroke-dasharray: var(--path-length) var(--path-length); + fill: currentColor; + opacity: 1; + } + + 85% { + stroke-dashoffset: 0; + stroke-dasharray: var(--path-length) var(--path-length); + fill: transparent; + opacity: 1; + } + + 95% { + stroke-dashoffset: var(--path-length); + stroke-dasharray: var(--path-length) var(--path-length); + fill: transparent; + opacity: 1; + } + + 100% { + stroke-dashoffset: var(--path-length); + stroke-dasharray: var(--path-length) var(--path-length); + fill: transparent; + opacity: 0; + } +} + +/* 确保在暗色模式下的颜色正确 */ +@media (prefers-color-scheme: dark) { + .animated-text path { + stroke: currentColor; + } +} + + + +/* 先确保基本动画工作后再添加脉动效果 */ +.root { + fill: none; + stroke: var(--accent-9); + stroke-width: 1px; + stroke-linecap: round; + opacity: 0; + stroke-dasharray: 50; + stroke-dashoffset: 50; + animation: rootGrow 0.8s ease-out forwards var(--delay); +} + +@keyframes rootGrow { + 0% { + opacity: 0; + stroke-dashoffset: 50; + stroke-width: 0.5px; + } + 100% { + opacity: 0.6; + stroke-dashoffset: 0; + stroke-width: 1px; + } +} + +@keyframes growPath { + 0% { + stroke-dashoffset: 100%; + } + 100% { + stroke-dashoffset: 0; + } +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 0.6; + } } diff --git a/frontend/app/init.tsx b/frontend/app/init.tsx index 04957f1..ba43738 100644 --- a/frontend/app/init.tsx +++ b/frontend/app/init.tsx @@ -1,10 +1,20 @@ -import React, { createContext, useState, useContext, useEffect } from "react"; -import {DEFAULT_CONFIG} from "app/env" +import React, { createContext, useState } from "react"; +import { DEFAULT_CONFIG } from "app/env"; import { HttpClient } from "core/http"; import { ThemeModeToggle } from "hooks/themeMode"; -import { Theme, Button, Select, Flex, Container, Heading, Text, Box } from '@radix-ui/themes'; -import { toast } from 'hooks/notification'; -import { Echoes } from "hooks/echo"; +import { + Theme, + Button, + Select, + Flex, + Container, + Heading, + Text, + Box, + TextField, +} from "@radix-ui/themes"; +import { toast } from "hooks/notification"; +import { Echoes } from "hooks/echoes"; interface SetupContextType { currentStep: number; @@ -25,27 +35,28 @@ const StepContainer: React.FC<{ title: string; children: React.ReactNode }> = ({ title, children, }) => ( - - {title} + + + {title} + {children} - + ); // 通用的导航按钮组件 -const NavigationButtons: React.FC = ({ - onNext, - loading = false, - disabled = false -}) => ( - - ); @@ -58,28 +69,31 @@ const InputField: React.FC<{ hint?: string; required?: boolean; }> = ({ label, name, defaultValue, hint, required = true }) => ( - - + + {label} {required && *} - - {hint && {hint}} + > + + + {hint && ( + + {hint} + + )} ); const Introduction: React.FC = ({ onNext }) => ( -
-

- 欢迎使用 Echoes -

- -
+ + 欢迎使用 Echoes + +
); @@ -91,11 +105,18 @@ const DatabaseConfig: React.FC = ({ onNext }) => { const validateForm = () => { const getRequiredFields = () => { switch (dbType) { - case 'sqllite': - return ['db_prefix', 'db_name']; - case 'postgresql': - case 'mysql': - return ['db_host', 'db_prefix', 'db_port', 'db_user', 'db_password', 'db_name']; + case "sqllite": + return ["db_prefix", "db_name"]; + case "postgresql": + case "mysql": + return [ + "db_host", + "db_prefix", + "db_port", + "db_user", + "db_password", + "db_name", + ]; default: return []; } @@ -104,26 +125,35 @@ const DatabaseConfig: React.FC = ({ onNext }) => { const requiredFields = getRequiredFields(); const emptyFields: string[] = []; - requiredFields.forEach(field => { - const input = document.querySelector(`[name="${field}"]`) as HTMLInputElement; - if (input && (!input.value || input.value.trim() === '')) { + requiredFields.forEach((field) => { + const input = document.querySelector( + `[name="${field}"]`, + ) as HTMLInputElement; + if (input && (!input.value || input.value.trim() === "")) { emptyFields.push(field); } }); if (emptyFields.length > 0) { - const fieldNames = emptyFields.map(field => { + const fieldNames = emptyFields.map((field) => { switch (field) { - case 'db_host': return '数据库地址'; - case 'db_prefix': return '数据库前缀'; - case 'db_port': return '端口'; - case 'db_user': return '用户名'; - case 'db_password': return '密码'; - case 'db_name': return '数据库名'; - default: return field; + case "db_host": + return "数据库地址"; + case "db_prefix": + return "数据库前缀"; + case "db_port": + return "端口"; + case "db_user": + return "用户名"; + case "db_password": + return "密码"; + case "db_name": + return "数据库名"; + default: + return field; } }); - toast.error(`请填写以下必填项:${fieldNames.join('、')}`); + toast.error(`请填写以下必填项:${fieldNames.join("、")}`); return false; } return true; @@ -139,27 +169,49 @@ const DatabaseConfig: React.FC = ({ onNext }) => { try { const formData = { db_type: dbType, - host: (document.querySelector('[name="db_host"]') as HTMLInputElement)?.value?.trim()??"", - db_prefix: (document.querySelector('[name="db_prefix"]') as HTMLInputElement)?.value?.trim()??"", - port: Number((document.querySelector('[name="db_port"]') as HTMLInputElement)?.value?.trim()??0), - user: (document.querySelector('[name="db_user"]') as HTMLInputElement)?.value?.trim()??"", - password: (document.querySelector('[name="db_password"]') as HTMLInputElement)?.value?.trim()??"", - db_name: (document.querySelector('[name="db_name"]') as HTMLInputElement)?.value?.trim()??"", + host: + ( + document.querySelector('[name="db_host"]') as HTMLInputElement + )?.value?.trim() ?? "", + db_prefix: + ( + document.querySelector('[name="db_prefix"]') as HTMLInputElement + )?.value?.trim() ?? "", + port: Number( + ( + document.querySelector('[name="db_port"]') as HTMLInputElement + )?.value?.trim() ?? 0, + ), + user: + ( + document.querySelector('[name="db_user"]') as HTMLInputElement + )?.value?.trim() ?? "", + password: + ( + document.querySelector('[name="db_password"]') as HTMLInputElement + )?.value?.trim() ?? "", + db_name: + ( + document.querySelector('[name="db_name"]') as HTMLInputElement + )?.value?.trim() ?? "", }; - await http.post('/sql', formData); + await http.post("/sql", formData); let oldEnv = import.meta.env ?? DEFAULT_CONFIG; - const viteEnv = Object.entries(oldEnv).reduce((acc, [key, value]) => { - if (key.startsWith('VITE_')) { - acc[key] = value; - } - return acc; - }, {} as Record); - + const viteEnv = Object.entries(oldEnv).reduce( + (acc, [key, value]) => { + if (key.startsWith("VITE_")) { + acc[key] = value; + } + return acc; + }, + {} as Record, + ); + const newEnv = { ...viteEnv, - VITE_INIT_STATUS: '2' + VITE_INIT_STATUS: "2", }; await http.dev("/env", { @@ -169,8 +221,8 @@ const DatabaseConfig: React.FC = ({ onNext }) => { Object.assign(import.meta.env, newEnv); - toast.success('数据库配置成功!'); - + toast.success("数据库配置成功!"); + setTimeout(() => onNext(), 1000); } catch (error: any) { console.error(error); @@ -302,8 +354,8 @@ const DatabaseConfig: React.FC = ({ onNext }) => { /> )} - @@ -312,14 +364,12 @@ const DatabaseConfig: React.FC = ({ onNext }) => { ); }; - interface InstallReplyData { - token: string, - username: string, - password: string, + token: string; + username: string; + password: string; } - const AdminConfig: React.FC = ({ onNext }) => { const [loading, setLoading] = useState(false); const http = HttpClient.getInstance(); @@ -328,29 +378,41 @@ const AdminConfig: React.FC = ({ onNext }) => { setLoading(true); try { const formData = { - username: (document.querySelector('[name="admin_username"]') as HTMLInputElement)?.value, - password: (document.querySelector('[name="admin_password"]') as HTMLInputElement)?.value, - email: (document.querySelector('[name="admin_email"]') as HTMLInputElement)?.value, + username: ( + document.querySelector('[name="admin_username"]') as HTMLInputElement + )?.value, + password: ( + document.querySelector('[name="admin_password"]') as HTMLInputElement + )?.value, + email: ( + document.querySelector('[name="admin_email"]') as HTMLInputElement + )?.value, }; - const response = await http.post('/administrator', formData) as InstallReplyData; + const response = (await http.post( + "/administrator", + formData, + )) as InstallReplyData; const data = response; - - localStorage.setItem('token', data.token); - + + localStorage.setItem("token", data.token); + let oldEnv = import.meta.env ?? DEFAULT_CONFIG; - const viteEnv = Object.entries(oldEnv).reduce((acc, [key, value]) => { - if (key.startsWith('VITE_')) { - acc[key] = value; - } - return acc; - }, {} as Record); - + const viteEnv = Object.entries(oldEnv).reduce( + (acc, [key, value]) => { + if (key.startsWith("VITE_")) { + acc[key] = value; + } + return acc; + }, + {} as Record, + ); + const newEnv = { ...viteEnv, - VITE_INIT_STATUS: '3', + VITE_INIT_STATUS: "3", VITE_API_USERNAME: data.username, - VITE_API_PASSWORD: data.password + VITE_API_PASSWORD: data.password, }; await http.dev("/env", { @@ -360,7 +422,7 @@ const AdminConfig: React.FC = ({ onNext }) => { Object.assign(import.meta.env, newEnv); - toast.success('管理员账号创建成功!'); + toast.success("管理员账号创建成功!"); onNext(); } catch (error: any) { console.error(error); @@ -382,62 +444,46 @@ const AdminConfig: React.FC = ({ onNext }) => { ); }; -const SetupComplete: React.FC = () => { - - return ( - -
-

- 恭喜!安装已完成 -

-

- 系统正在重启中,请稍候... -

-
-
-
-
-
- ); -}; +const SetupComplete: React.FC = () => ( + + + + 恭喜!安装已完成 + + 系统正在重启中,请稍候... + + + + + + + +); export default function SetupPage() { const [currentStep, setCurrentStep] = useState(() => { return Number(import.meta.env.VITE_INIT_STATUS ?? 0) + 1; }); - const [appearance, setAppearance] = useState<'light' | 'dark'>('light'); - - useEffect(() => { - // 在客户端运行时检查主题 - const isDark = document.documentElement.classList.contains('dark'); - setAppearance(isDark ? 'dark' : 'light'); - - // 监听主题变化 - const handleThemeChange = (event: CustomEvent<{ theme: 'light' | 'dark' }>) => { - setAppearance(event.detail.theme); - }; - - window.addEventListener('theme-change', handleThemeChange as EventListener); - return () => window.removeEventListener('theme-change', handleThemeChange as EventListener); - }, []); - return ( - -
-
+ + -
-
-
+ + + + -
-
+ + + @@ -445,20 +491,18 @@ export default function SetupPage() { setCurrentStep(currentStep + 1)} /> )} {currentStep === 2 && ( - setCurrentStep(currentStep + 1)} /> )} {currentStep === 3 && ( - setCurrentStep(currentStep + 1)} - /> + setCurrentStep(currentStep + 1)} /> )} {currentStep === 4 && } -
+
); -} \ No newline at end of file +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000..1796bc1 --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,11 @@ +"use client"; +import React from "react"; +import GrowingTree from "../hooks/tide"; + +export default function Page() { + return ( +
+ +
+ ); +} diff --git a/frontend/app/root.tsx b/frontend/app/root.tsx index 9c7b973..f14bcd9 100644 --- a/frontend/app/root.tsx +++ b/frontend/app/root.tsx @@ -6,74 +6,42 @@ import { ScrollRestoration, } from "@remix-run/react"; import { NotificationProvider } from "hooks/notification"; -import { Theme } from '@radix-ui/themes'; +import { Theme } from "@radix-ui/themes"; import { useEffect, useState } from "react"; import "~/index.css"; export function Layout() { - const [theme, setTheme] = useState<'light' | 'dark'>('light'); - - useEffect(() => { - // 初始化主题 - const isDark = document.documentElement.classList.contains('dark'); - setTheme(isDark ? 'dark' : 'light'); - - // 监听主题变化 - const handleThemeChange = (event: CustomEvent<{ theme: 'light' | 'dark' }>) => { - setTheme(event.detail.theme); - }; - - window.addEventListener('theme-change', handleThemeChange as EventListener); - return () => window.removeEventListener('theme-change', handleThemeChange as EventListener); - }, []); - return ( - + - - - - + + + Echoes