diff --git a/backend/src/storage/sql/builder.rs b/backend/src/storage/sql/builder.rs index 54ee929..69f5439 100644 --- a/backend/src/storage/sql/builder.rs +++ b/backend/src/storage/sql/builder.rs @@ -199,7 +199,7 @@ impl SafeValue { SafeValue::Float(f) => Ok(f.to_string()), SafeValue::Text(s, level) => { TextValidator::default().validate(s, *level)?; - Ok(format!("{}", s.replace('\'', "''"))) + Ok(format!("{}", s)) } SafeValue::DateTime(dt) => Ok(format!("{}", dt.to_rfc3339())), } @@ -254,8 +254,7 @@ pub enum Operator { Lte, Like, In, - IsNull, - IsNotNull, + IsNull } impl Operator { @@ -269,8 +268,7 @@ impl Operator { Operator::Lte => "<=", Operator::Like => "LIKE", Operator::In => "IN", - Operator::IsNull => "IS NULL", - Operator::IsNotNull => "IS NOT NULL", + Operator::IsNull => "IS NULL" } } } @@ -297,6 +295,7 @@ pub enum WhereClause { And(Vec), Or(Vec), Condition(Condition), + Not(Condition) } #[derive(Debug, Clone)] @@ -463,6 +462,10 @@ impl QueryBuilder { WhereClause::Condition(condition) => { self.build_condition(condition, &mut params, param_index)? } + WhereClause::Not(condition) => { + let condition_sql = self.build_condition(condition, &mut params, param_index)?; + format!("NOT ({})", condition_sql) + } }; Ok((sql, params)) diff --git a/backend/src/storage/sql/mod.rs b/backend/src/storage/sql/mod.rs index 24e38ea..766c5d8 100644 --- a/backend/src/storage/sql/mod.rs +++ b/backend/src/storage/sql/mod.rs @@ -77,7 +77,7 @@ impl Database { db: Arc::new(db), prefix: Arc::new(database.db_prefix.clone()), db_type: Arc::new(match database.db_type.to_lowercase().as_str() { - // "postgresql" => DatabaseType::PostgreSQL, + "postgresql" => DatabaseType::PostgreSQL, "mysql" => DatabaseType::MySQL, "sqllite" => DatabaseType::SQLite, _ => return Err("unknown database type".into_custom_error()), diff --git a/backend/src/storage/sql/schema.rs b/backend/src/storage/sql/schema.rs index 8d63200..b057867 100644 --- a/backend/src/storage/sql/schema.rs +++ b/backend/src/storage/sql/schema.rs @@ -1,7 +1,7 @@ use super::builder::{Condition, Identifier, Operator, SafeValue, ValidationLevel, WhereClause}; use super::DatabaseType; use crate::common::error::{CustomErrorInto, CustomResult}; -use std::fmt::Display; +use std::fmt::{format, Display}; #[derive(Debug, Clone, PartialEq)] pub enum FieldType { @@ -200,6 +200,7 @@ impl Field { Err("Invalid IN clause value".into_custom_error()) } } + Operator::IsNull => Ok(format!("{} IS NULL", field_name)), Operator::Eq | Operator::Ne | Operator::Gt @@ -220,9 +221,26 @@ impl Field { _ => Err("Unsupported operator for CHECK constraint".into_custom_error()), } } - _ => { - Err("Only simple conditions are supported for CHECK constraints" - .into_custom_error()) + WhereClause::And(conditions) => { + let conditions: CustomResult> = conditions + .iter() + .map(|c| Self::build_check_constraint(c)) + .collect(); + Ok(format!("({})", conditions?.join(" AND "))) + } + WhereClause::Or(conditions) => { + let conditions: CustomResult> = conditions + .iter() + .map(|c| Self::build_check_constraint(c)) + .collect(); + Ok(format!("({})", conditions?.join(" OR "))) + } + WhereClause::Not(condition) => { + let inner_condition = WhereClause::Condition(condition.clone()); + Ok(format!( + "NOT ({})", + Self::build_check_constraint(&inner_condition)? + )) } } } @@ -372,8 +390,62 @@ impl SchemaBuilder { pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomResult { let db_prefix = db_prefix.to_string()?; let mut schema = SchemaBuilder::new(); - let user_level = "('contributor', 'administrator')"; - let content_state = "('draft', 'published', 'private', 'hidden')"; + let role_check = WhereClause::Or(vec![ + WhereClause::Condition(Condition::new( + "role".to_string(), + Operator::Eq, + Some(SafeValue::Text( + "'contributor'".to_string(), + ValidationLevel::Raw, + )), + )?), + WhereClause::Condition(Condition::new( + "role".to_string(), + Operator::Eq, + Some(SafeValue::Text( + "'administrator'".to_string(), + ValidationLevel::Raw, + )), + )?), + ]); + let content_state_check = WhereClause::Or(vec![ + WhereClause::Condition(Condition::new( + "status".to_string(), + Operator::Eq, + Some(SafeValue::Text( + "'published'".to_string(), + ValidationLevel::Raw, + )), + )?), + WhereClause::Condition(Condition::new( + "status".to_string(), + Operator::Eq, + Some(SafeValue::Text( + "'private'".to_string(), + ValidationLevel::Raw, + )), + )?), + WhereClause::Condition(Condition::new( + "status".to_string(), + Operator::Eq, + Some(SafeValue::Text( + "'hidden'".to_string(), + ValidationLevel::Raw, + )), + )?), + ]); + let target_type_check = WhereClause::Or(vec![ + WhereClause::Condition(Condition::new( + "target_type".to_string(), + Operator::Eq, + Some(SafeValue::Text("'post'".to_string(), ValidationLevel::Raw)), + )?), + WhereClause::Condition(Condition::new( + "target_type".to_string(), + Operator::Eq, + Some(SafeValue::Text("'page'".to_string(), ValidationLevel::Raw)), + )?), + ]); // 用户表 let mut users_table = Table::new(&format!("{}users", db_prefix))?; @@ -405,16 +477,7 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes .add_field(Field::new( "role", FieldType::VarChar(20), - FieldConstraint::new() - .not_null() - .check(WhereClause::Condition(Condition::new( - "role".to_string(), - Operator::In, - Some(SafeValue::Text( - user_level.to_string(), - ValidationLevel::Relaxed, - )), - )?)), + FieldConstraint::new().not_null().check(role_check.clone()), ValidationLevel::Strict, )?) .add_field(Field::new( @@ -438,9 +501,7 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes .add_field(Field::new( "last_login_at", FieldType::Timestamp, - FieldConstraint::new() - .not_null() - .default(SafeValue::Text( + FieldConstraint::new().not_null().default(SafeValue::Text( "CURRENT_TIMESTAMP".to_string(), ValidationLevel::Strict, )), @@ -480,16 +541,7 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes .add_field(Field::new( "status", FieldType::VarChar(20), - FieldConstraint::new() - .not_null() - .check(WhereClause::Condition(Condition::new( - "status".to_string(), - Operator::In, - Some(SafeValue::Text( - content_state.to_string(), - ValidationLevel::Standard, - )), - )?)), + FieldConstraint::new().not_null().check(content_state_check.clone()), ValidationLevel::Strict, )?); @@ -535,16 +587,7 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes .add_field(Field::new( "status", FieldType::VarChar(20), - FieldConstraint::new() - .not_null() - .check(WhereClause::Condition(Condition::new( - "status".to_string(), - Operator::In, - Some(SafeValue::Text( - content_state.to_string(), - ValidationLevel::Standard, - )), - )?)), + FieldConstraint::new().not_null().check(content_state_check.clone()), ValidationLevel::Strict, )?) .add_field(Field::new( @@ -678,47 +721,47 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes metadata_table .add_field(Field::new( "id", - FieldType::Integer(true), + FieldType::Integer(true), FieldConstraint::new().primary(), ValidationLevel::Strict, - )?).add_field(Field::new( + )?) + .add_field(Field::new( "target_type", FieldType::VarChar(20), - FieldConstraint::new() - .not_null() - .check(WhereClause::Condition(Condition::new( - "target_type".to_string(), - Operator::In, - Some(SafeValue::Text( - "('post', 'page')".to_string(), - ValidationLevel::Standard, - )), - )?)), + FieldConstraint::new().not_null().check(target_type_check.clone()), ValidationLevel::Strict, - )?).add_field(Field::new( + )?) + .add_field(Field::new( "target_id", FieldType::Integer(false), - FieldConstraint::new() - .not_null() - .check(WhereClause::Condition(Condition::new( - "(target_type = 'post' AND EXISTS (SELECT 1 FROM posts WHERE id = target_id)) OR \ - (target_type = 'page' AND EXISTS (SELECT 1 FROM pages WHERE id = target_id))".to_string(), - Operator::Raw, - None, - )?)), + FieldConstraint::new().not_null(), ValidationLevel::Strict, - )?).add_field(Field::new( + )?) + .add_field(Field::new( "meta_key", FieldType::VarChar(50), FieldConstraint::new().not_null(), ValidationLevel::Strict, - )?).add_field(Field::new( + )?) + .add_field(Field::new( "meta_value", FieldType::Text, FieldConstraint::new(), ValidationLevel::Strict, )?); + metadata_table.add_index(Index::new( + "fk_metadata_posts", + vec!["target_id".to_string()], + false, + )?); + + metadata_table.add_index(Index::new( + "fk_metadata_pages", + vec!["target_id".to_string()], + false, + )?); + metadata_table.add_index(Index::new( "idx_metadata_target", vec!["target_type".to_string(), "target_id".to_string()], @@ -735,36 +778,32 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes FieldType::Integer(true), FieldConstraint::new().primary(), ValidationLevel::Strict, - )?).add_field(Field::new( + )?) + .add_field(Field::new( "target_type", FieldType::VarChar(20), - FieldConstraint::new() - .not_null() - .check(WhereClause::Condition(Condition::new( - "target_type".to_string(), - Operator::In, - Some(SafeValue::Text( - "('post', 'page')".to_string(), - ValidationLevel::Standard, - )), - )?)), + FieldConstraint::new().not_null().check(target_type_check.clone()), ValidationLevel::Strict, - )?).add_field(Field::new( + )?) + .add_field(Field::new( "target_id", FieldType::Integer(false), FieldConstraint::new().not_null(), ValidationLevel::Strict, - )?).add_field(Field::new( + )?) + .add_field(Field::new( "field_key", FieldType::VarChar(50), FieldConstraint::new().not_null(), ValidationLevel::Strict, - )?).add_field(Field::new( + )?) + .add_field(Field::new( "field_value", FieldType::Text, FieldConstraint::new(), ValidationLevel::Strict, - )?).add_field(Field::new( + )?) + .add_field(Field::new( "field_type", FieldType::VarChar(20), FieldConstraint::new().not_null(), @@ -788,37 +827,45 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes FieldType::VarChar(50), FieldConstraint::new().primary(), ValidationLevel::Strict, - )?).add_field(Field::new( + )?) + .add_field(Field::new( "slug", FieldType::VarChar(50), FieldConstraint::new().not_null().unique(), ValidationLevel::Strict, - )?).add_field(Field::new( + )?) + .add_field(Field::new( "type", FieldType::VarChar(20), FieldConstraint::new() .not_null() - .check(WhereClause::Condition(Condition::new( - "type".to_string(), - Operator::In, - Some(SafeValue::Text( - "('tag', 'category')".to_string(), - ValidationLevel::Standard, - )), - )?)), + .check(WhereClause::Or(vec![ + WhereClause::Condition(Condition::new( + "type".to_string(), + Operator::Eq, + Some(SafeValue::Text( + "'tag'".to_string(), + ValidationLevel::Raw, + )), + )?), + WhereClause::Condition(Condition::new( + "type".to_string(), + Operator::Eq, + Some(SafeValue::Text( + "'category'".to_string(), + ValidationLevel::Raw, + )), + )?), + ])), ValidationLevel::Strict, - )?).add_field(Field::new( - "parent_id", + )?) + .add_field(Field::new( + "parent_name", FieldType::VarChar(50), FieldConstraint::new() .foreign_key(format!("{}taxonomies", db_prefix), "name".to_string()) .on_delete(ForeignKeyAction::SetNull) - .on_update(ForeignKeyAction::Cascade) - .check(WhereClause::Condition(Condition::new( - "(type = 'category' OR parent_id IS NULL)".to_string(), - Operator::Raw, - None, - )?)), + .on_update(ForeignKeyAction::Cascade), ValidationLevel::Strict, )?); @@ -836,7 +883,8 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes .on_delete(ForeignKeyAction::Cascade) .on_update(ForeignKeyAction::Cascade), ValidationLevel::Strict, - )?).add_field(Field::new( + )?) + .add_field(Field::new( "taxonomy_id", FieldType::VarChar(50), FieldConstraint::new() diff --git a/frontend/app/init.tsx b/frontend/app/init.tsx index 6e25c63..24ff01f 100644 --- a/frontend/app/init.tsx +++ b/frontend/app/init.tsx @@ -456,30 +456,75 @@ const AdminConfig: React.FC = ({ onNext }) => { ); }; -const SetupComplete: React.FC = () => ( - - - - 恭喜!安装已完成 - - 系统正在重启中,请稍候... - - - - - - - -); +const SetupComplete: React.FC = () => { + useEffect(() => { + // 添加延迟后刷新页面 + const timer = setTimeout(() => { + window.location.reload(); + }, 3000); + + return () => clearTimeout(timer); + }, []); + + return ( + + + + 恭喜!安装已完成 + + 系统正在重启中,请稍候... + + + + + + + + ); +}; export default function SetupPage() { const [currentStep, setCurrentStep] = useState(1); + const [isClient, setIsClient] = useState(false); + useEffect(() => { - // 在客户端组件挂载后更新状态 - const initStatus = Number(import.meta.env.VITE_INIT_STATUS ?? 0) + 1; - setCurrentStep(initStatus); + // 标记客户端渲染完成 + setIsClient(true); + + // 获取初始化状态 + const initStatus = Number(import.meta.env.VITE_INIT_STATUS ?? 0); + + // 如果已完成初始化,直接刷新页面 + if (initStatus >= 3) { + window.location.reload(); + return; + } + + // 否则设置当前步骤 + setCurrentStep(initStatus + 1); }, []); + // 在服务端渲染时或客户端首次渲染时,返回加载状态 + if (!isClient) { + return ( + + + + + + + + + + ); + } + return ( - + + + diff --git a/frontend/app/root.tsx b/frontend/app/root.tsx index 003ff08..2a2419e 100644 --- a/frontend/app/root.tsx +++ b/frontend/app/root.tsx @@ -7,7 +7,7 @@ import { } from "@remix-run/react"; import { NotificationProvider } from "hooks/Notification"; import { Theme } from "@radix-ui/themes"; -import { ThemeScript } from "hooks/ThemeMode"; +import { ThemeScript } from "hooks/themeMode"; import "~/index.css"; @@ -70,3 +70,4 @@ export function Layout() { export default function App() { return ; } + diff --git a/frontend/hooks/ParticleImage.tsx b/frontend/hooks/ParticleImage.tsx index d151a83..c085518 100644 --- a/frontend/hooks/ParticleImage.tsx +++ b/frontend/hooks/ParticleImage.tsx @@ -757,7 +757,7 @@ export const ParticleImage = ({ const ctx = canvas.getContext('2d'); if (ctx) { - // 增加一个小的边距以确保��盖 + // 增加一个小的边距以确保盖 const padding = 2; // 添加2像素的距 canvas.width = width + padding * 2; canvas.height = height + padding * 2; @@ -961,7 +961,7 @@ export const ParticleImage = ({ containerRef.current.removeChild(renderer.domElement); renderer.dispose(); } - // 清有 GSAP ���画 + // 清有 GSAP 画 gsap.killTweensOf('*'); // 移除 resize 监听 @@ -1043,11 +1043,6 @@ const loadingQueue = { add(url: string, instanceId: string, isLongConnection = false) { const key = `${instanceId}:${url}`; if (!this.items.has(key)) { - console.log('[Queue] Adding:', key, - isLongConnection ? '(long connection)' : '', - 'Current processing:', this.currentProcessing, - 'Max concurrent:', this.maxConcurrent - ); this.items.set(key, { isProcessing: false, @@ -1056,7 +1051,7 @@ const loadingQueue = { lastActiveTime: Date.now() }); - // 连接不再直接进入备选队列,而是等待加载完成后再加入 + // 连接��再直接进入备选队列,而是等待加载完成后再加入 this.processQueue(); return true; } @@ -1064,32 +1059,15 @@ const loadingQueue = { }, processQueue() { - console.log('[Queue] Processing queue:', { - availableSlots: this.availableSlots, - currentProcessing: this.currentProcessing, - pendingQueueSize: this.pendingQueue.size, - totalItems: this.items.size - }); - if (this.availableSlots > 0) { let nextKey: string | undefined; - // 优先从备选队列中获取已加载完成的长连接 - if (this.pendingQueue.size > 0) { - nextKey = Array.from(this.pendingQueue)[0]; - this.pendingQueue.delete(nextKey); - console.log('[Queue] Processing from pending queue:', nextKey); - } else { - // 如果没有待处理的长连接,处理普通请求 - const normalItem = Array.from(this.items.entries()) - .find(([_, item]) => - !item.isProcessing && - !item.isLongConnection - ); - if (normalItem) { - nextKey = normalItem[0]; - console.log('[Queue] Processing normal request:', nextKey); - } + // 修改这部分逻辑,不再区分长连接和普通请求 + const nextItem = Array.from(this.items.entries()) + .find(([_, item]) => !item.isProcessing); + + if (nextItem) { + nextKey = nextItem[0]; } if (nextKey) { @@ -1101,17 +1079,12 @@ const loadingQueue = { startProcessing(key: string) { const item = this.items.get(key); if (item && !item.isProcessing) { - console.log('[Queue] Start processing:', key, { - isLongConnection: item.isLongConnection, - isError: key.includes('error') - }); - + item.isProcessing = true; // 只有普通请求且不是错误状态时才增加处理数量 if (!item.isLongConnection && !key.includes('error')) { this.currentProcessing++; - console.log('[Queue] Increased processing count:', this.currentProcessing); } } }, @@ -1122,7 +1095,6 @@ const loadingQueue = { const item = this.items.get(key); if (item?.isLongConnection) { this.pendingQueue.add(key); - console.log('[Queue] Added to pending queue:', key); } }, @@ -1130,27 +1102,16 @@ const loadingQueue = { const key = `${instanceId}:${url}`; const item = this.items.get(key); - console.log('[Queue] Removing:', key, - 'Is long connection:', item?.isLongConnection, - 'Was processing:', item?.isProcessing, - 'Has error:', key.includes('error') - ); // 只有普通请求且正在处理时才减少处理数量 if (item?.isProcessing && !item.isLongConnection && !key.includes('error')) { this.currentProcessing--; - console.log('[Queue] Decreased processing count:', this.currentProcessing); } // 确保从队列中移除 this.items.delete(key); this.pendingQueue.delete(key); - console.log('[Queue] After remove - Processing:', this.currentProcessing, - 'Pending queue size:', this.pendingQueue.size, - 'Total items:', this.items.size - ); - // 如果是错误状态,立即处理下一个请求 if (key.includes('error')) { this.processQueue(); @@ -1166,10 +1127,9 @@ const loadingQueue = { const key = `${instanceId}:${url}`; const item = this.items.get(key); - // 错误状态不占用槽位,只在第一次检查时输出日志 + // 错误状态的处理保持不变 if (key.includes('error')) { if (!item?.lastLogTime) { - console.log('[Queue] Can process (error):', key, true); if (item) { item.lastLogTime = Date.now(); } @@ -1177,22 +1137,13 @@ const loadingQueue = { return true; } - // 长连接在备选队列中时可以处理 - if (item?.isLongConnection && this.pendingQueue.has(key)) { - if (!item.lastLogTime || Date.now() - item.lastLogTime > 1000) { - console.log('[Queue] Can process (pending long):', key, true); - item.lastLogTime = Date.now(); - } - return true; - } - + // 移除长连接的特殊处理,统一处理所有请求 const canProcess = item?.isProcessing || false; - // 只在状态发生变化时或者每秒最多输出一次日志 + if (item && (item.lastProcessState !== canProcess || !item.lastLogTime || Date.now() - item.lastLogTime > 1000)) { - console.log('[Queue] Can process (normal):', key, canProcess); item.lastProcessState = canProcess; item.lastLogTime = Date.now(); } @@ -1233,7 +1184,6 @@ export const ImageLoader = ({ useEffect(() => { if (!src) return; - console.log('[Queue] Effect triggered for:', src, 'Instance:', instanceId.current); setShowImage(false); setAnimationComplete(false); setCanShowParticles(false); @@ -1246,7 +1196,6 @@ export const ImageLoader = ({ const timeSinceLastAnimation = now - lastAnimationTime; if (particleLoadQueue.size === 0) { - console.log('[Queue] Starting immediate animation for:', src, 'Instance:', instanceId.current); particleLoadQueue.add(src); setCanShowParticles(true); lastAnimationTime = now; @@ -1258,12 +1207,10 @@ export const ImageLoader = ({ Math.min(ANIMATION_THRESHOLD, timeSinceLastAnimation) ); - console.log('[Queue] Scheduling delayed animation for:', src, 'Instance:', instanceId.current); const timer = setTimeout(() => { const key = `${instanceId.current}:${src}`; if (!loadingQueue.items.has(key)) return; - console.log('[Queue] Starting delayed animation for:', src, 'Instance:', instanceId.current); particleLoadQueue.add(src); setCanShowParticles(true); lastAnimationTime = Date.now(); @@ -1282,7 +1229,6 @@ export const ImageLoader = ({ const cleanup = checkQueue(); return () => { - console.log('[Queue] Cleanup effect for:', src, 'Instance:', instanceId.current); cleanup?.(); loadingQueue.remove(src, instanceId.current); }; @@ -1393,7 +1339,6 @@ export const ImageLoader = ({ }; img.onerror = () => { - console.log('[Image Loader] Error loading image:', src); if (timeoutRef.current) { clearTimeout(timeoutRef.current); } @@ -1429,7 +1374,7 @@ export const ImageLoader = ({ }; }, [src, preloadImage]); - // 添加一个新的状来控制粒子动画 + // 添加一个���的状来控制粒子动画 const [canShowParticles, setCanShowParticles] = useState(false); // 添加加载动画组件 @@ -1449,25 +1394,20 @@ export const ImageLoader = ({ src={src} status={status} onLoad={() => { - console.log('[ParticleImage] onLoad START:', src); if (imageRef.current) { // 保持为空 } - console.log('[ParticleImage] onLoad END:', src); }} onAnimationComplete={() => { - console.log('[ParticleImage] Animation START:', src); if (imageRef.current && src) { setShowImage(true); requestAnimationFrame(() => { - console.log('[ParticleImage] Setting animation complete:', src); setAnimationComplete(true); particleLoadQueue.delete(src); loadingQueue.remove(src, instanceId.current); setTimeout(() => { - console.log('[ParticleImage] Fading image:', src); const img = document.querySelector(`img[src="${imageRef.current?.src}"]`) as HTMLImageElement; if (img) { img.style.opacity = '1'; @@ -1475,7 +1415,6 @@ export const ImageLoader = ({ }, 50); }); } - console.log('[ParticleImage] Animation END:', src); }} /> )} diff --git a/frontend/hooks/themeMode.tsx b/frontend/hooks/themeMode.tsx index 9f33191..5de2242 100644 --- a/frontend/hooks/themeMode.tsx +++ b/frontend/hooks/themeMode.tsx @@ -4,89 +4,97 @@ import { Button } from "@radix-ui/themes"; const THEME_KEY = "theme-preference"; -// 添加这个脚本来预先设置主题,避免闪烁 +// 修改主题脚本,确保在服务端和客户端都能正确初始化 const themeScript = ` (function() { - function getInitialTheme() { + try { const savedTheme = localStorage.getItem("${THEME_KEY}"); - if (savedTheme) { - document.documentElement.className = savedTheme; - return savedTheme; - } - - const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; - const theme = isDark ? "dark" : "light"; - document.documentElement.className = theme; - localStorage.setItem("${THEME_KEY}", theme); - return theme; - } - - // 确保在 DOM 内容加载前执行 - if (document.documentElement) { - getInitialTheme(); - } else { - document.addEventListener('DOMContentLoaded', getInitialTheme); + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + const theme = savedTheme || (prefersDark ? "dark" : "light"); + document.documentElement.dataset.theme = theme; + document.documentElement.classList.remove('light', 'dark'); + document.documentElement.classList.add(theme); + } catch (e) { + console.error('[ThemeScript] Error:', e); + document.documentElement.dataset.theme = 'light'; + document.documentElement.classList.remove('light', 'dark'); + document.documentElement.classList.add('light'); } })() `; +// ThemeScript 组件需要尽早执行 export const ThemeScript = () => { - return