优化SQL查询构建,新增NOT条件支持,修复文本转义逻辑,更新主题切换逻辑,改进加载动画,更新依赖项,重构主题管理组件,修复多个小问题。

This commit is contained in:
lsy 2024-12-12 20:18:08 +08:00
parent 507595a269
commit 195f8de576
9 changed files with 353 additions and 298 deletions

View File

@ -199,7 +199,7 @@ impl SafeValue {
SafeValue::Float(f) => Ok(f.to_string()), SafeValue::Float(f) => Ok(f.to_string()),
SafeValue::Text(s, level) => { SafeValue::Text(s, level) => {
TextValidator::default().validate(s, *level)?; TextValidator::default().validate(s, *level)?;
Ok(format!("{}", s.replace('\'', "''"))) Ok(format!("{}", s))
} }
SafeValue::DateTime(dt) => Ok(format!("{}", dt.to_rfc3339())), SafeValue::DateTime(dt) => Ok(format!("{}", dt.to_rfc3339())),
} }
@ -254,8 +254,7 @@ pub enum Operator {
Lte, Lte,
Like, Like,
In, In,
IsNull, IsNull
IsNotNull,
} }
impl Operator { impl Operator {
@ -269,8 +268,7 @@ impl Operator {
Operator::Lte => "<=", Operator::Lte => "<=",
Operator::Like => "LIKE", Operator::Like => "LIKE",
Operator::In => "IN", Operator::In => "IN",
Operator::IsNull => "IS NULL", Operator::IsNull => "IS NULL"
Operator::IsNotNull => "IS NOT NULL",
} }
} }
} }
@ -297,6 +295,7 @@ pub enum WhereClause {
And(Vec<WhereClause>), And(Vec<WhereClause>),
Or(Vec<WhereClause>), Or(Vec<WhereClause>),
Condition(Condition), Condition(Condition),
Not(Condition)
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -463,6 +462,10 @@ impl QueryBuilder {
WhereClause::Condition(condition) => { WhereClause::Condition(condition) => {
self.build_condition(condition, &mut params, param_index)? 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)) Ok((sql, params))

View File

@ -77,7 +77,7 @@ impl Database {
db: Arc::new(db), db: Arc::new(db),
prefix: Arc::new(database.db_prefix.clone()), prefix: Arc::new(database.db_prefix.clone()),
db_type: Arc::new(match database.db_type.to_lowercase().as_str() { db_type: Arc::new(match database.db_type.to_lowercase().as_str() {
// "postgresql" => DatabaseType::PostgreSQL, "postgresql" => DatabaseType::PostgreSQL,
"mysql" => DatabaseType::MySQL, "mysql" => DatabaseType::MySQL,
"sqllite" => DatabaseType::SQLite, "sqllite" => DatabaseType::SQLite,
_ => return Err("unknown database type".into_custom_error()), _ => return Err("unknown database type".into_custom_error()),

View File

@ -1,7 +1,7 @@
use super::builder::{Condition, Identifier, Operator, SafeValue, ValidationLevel, WhereClause}; use super::builder::{Condition, Identifier, Operator, SafeValue, ValidationLevel, WhereClause};
use super::DatabaseType; use super::DatabaseType;
use crate::common::error::{CustomErrorInto, CustomResult}; use crate::common::error::{CustomErrorInto, CustomResult};
use std::fmt::Display; use std::fmt::{format, Display};
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum FieldType { pub enum FieldType {
@ -200,6 +200,7 @@ impl Field {
Err("Invalid IN clause value".into_custom_error()) Err("Invalid IN clause value".into_custom_error())
} }
} }
Operator::IsNull => Ok(format!("{} IS NULL", field_name)),
Operator::Eq Operator::Eq
| Operator::Ne | Operator::Ne
| Operator::Gt | Operator::Gt
@ -220,9 +221,26 @@ impl Field {
_ => Err("Unsupported operator for CHECK constraint".into_custom_error()), _ => Err("Unsupported operator for CHECK constraint".into_custom_error()),
} }
} }
_ => { WhereClause::And(conditions) => {
Err("Only simple conditions are supported for CHECK constraints" let conditions: CustomResult<Vec<String>> = conditions
.into_custom_error()) .iter()
.map(|c| Self::build_check_constraint(c))
.collect();
Ok(format!("({})", conditions?.join(" AND ")))
}
WhereClause::Or(conditions) => {
let conditions: CustomResult<Vec<String>> = 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<String> { pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomResult<String> {
let db_prefix = db_prefix.to_string()?; let db_prefix = db_prefix.to_string()?;
let mut schema = SchemaBuilder::new(); let mut schema = SchemaBuilder::new();
let user_level = "('contributor', 'administrator')"; let role_check = WhereClause::Or(vec![
let content_state = "('draft', 'published', 'private', 'hidden')"; 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))?; 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( .add_field(Field::new(
"role", "role",
FieldType::VarChar(20), FieldType::VarChar(20),
FieldConstraint::new() FieldConstraint::new().not_null().check(role_check.clone()),
.not_null()
.check(WhereClause::Condition(Condition::new(
"role".to_string(),
Operator::In,
Some(SafeValue::Text(
user_level.to_string(),
ValidationLevel::Relaxed,
)),
)?)),
ValidationLevel::Strict, ValidationLevel::Strict,
)?) )?)
.add_field(Field::new( .add_field(Field::new(
@ -438,9 +501,7 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
.add_field(Field::new( .add_field(Field::new(
"last_login_at", "last_login_at",
FieldType::Timestamp, FieldType::Timestamp,
FieldConstraint::new() FieldConstraint::new().not_null().default(SafeValue::Text(
.not_null()
.default(SafeValue::Text(
"CURRENT_TIMESTAMP".to_string(), "CURRENT_TIMESTAMP".to_string(),
ValidationLevel::Strict, ValidationLevel::Strict,
)), )),
@ -480,16 +541,7 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
.add_field(Field::new( .add_field(Field::new(
"status", "status",
FieldType::VarChar(20), FieldType::VarChar(20),
FieldConstraint::new() FieldConstraint::new().not_null().check(content_state_check.clone()),
.not_null()
.check(WhereClause::Condition(Condition::new(
"status".to_string(),
Operator::In,
Some(SafeValue::Text(
content_state.to_string(),
ValidationLevel::Standard,
)),
)?)),
ValidationLevel::Strict, ValidationLevel::Strict,
)?); )?);
@ -535,16 +587,7 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
.add_field(Field::new( .add_field(Field::new(
"status", "status",
FieldType::VarChar(20), FieldType::VarChar(20),
FieldConstraint::new() FieldConstraint::new().not_null().check(content_state_check.clone()),
.not_null()
.check(WhereClause::Condition(Condition::new(
"status".to_string(),
Operator::In,
Some(SafeValue::Text(
content_state.to_string(),
ValidationLevel::Standard,
)),
)?)),
ValidationLevel::Strict, ValidationLevel::Strict,
)?) )?)
.add_field(Field::new( .add_field(Field::new(
@ -678,47 +721,47 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
metadata_table metadata_table
.add_field(Field::new( .add_field(Field::new(
"id", "id",
FieldType::Integer(true), FieldType::Integer(true),
FieldConstraint::new().primary(), FieldConstraint::new().primary(),
ValidationLevel::Strict, ValidationLevel::Strict,
)?).add_field(Field::new( )?)
.add_field(Field::new(
"target_type", "target_type",
FieldType::VarChar(20), FieldType::VarChar(20),
FieldConstraint::new() FieldConstraint::new().not_null().check(target_type_check.clone()),
.not_null()
.check(WhereClause::Condition(Condition::new(
"target_type".to_string(),
Operator::In,
Some(SafeValue::Text(
"('post', 'page')".to_string(),
ValidationLevel::Standard,
)),
)?)),
ValidationLevel::Strict, ValidationLevel::Strict,
)?).add_field(Field::new( )?)
.add_field(Field::new(
"target_id", "target_id",
FieldType::Integer(false), FieldType::Integer(false),
FieldConstraint::new() FieldConstraint::new().not_null(),
.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,
)?)),
ValidationLevel::Strict, ValidationLevel::Strict,
)?).add_field(Field::new( )?)
.add_field(Field::new(
"meta_key", "meta_key",
FieldType::VarChar(50), FieldType::VarChar(50),
FieldConstraint::new().not_null(), FieldConstraint::new().not_null(),
ValidationLevel::Strict, ValidationLevel::Strict,
)?).add_field(Field::new( )?)
.add_field(Field::new(
"meta_value", "meta_value",
FieldType::Text, FieldType::Text,
FieldConstraint::new(), FieldConstraint::new(),
ValidationLevel::Strict, 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( metadata_table.add_index(Index::new(
"idx_metadata_target", "idx_metadata_target",
vec!["target_type".to_string(), "target_id".to_string()], 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), FieldType::Integer(true),
FieldConstraint::new().primary(), FieldConstraint::new().primary(),
ValidationLevel::Strict, ValidationLevel::Strict,
)?).add_field(Field::new( )?)
.add_field(Field::new(
"target_type", "target_type",
FieldType::VarChar(20), FieldType::VarChar(20),
FieldConstraint::new() FieldConstraint::new().not_null().check(target_type_check.clone()),
.not_null()
.check(WhereClause::Condition(Condition::new(
"target_type".to_string(),
Operator::In,
Some(SafeValue::Text(
"('post', 'page')".to_string(),
ValidationLevel::Standard,
)),
)?)),
ValidationLevel::Strict, ValidationLevel::Strict,
)?).add_field(Field::new( )?)
.add_field(Field::new(
"target_id", "target_id",
FieldType::Integer(false), FieldType::Integer(false),
FieldConstraint::new().not_null(), FieldConstraint::new().not_null(),
ValidationLevel::Strict, ValidationLevel::Strict,
)?).add_field(Field::new( )?)
.add_field(Field::new(
"field_key", "field_key",
FieldType::VarChar(50), FieldType::VarChar(50),
FieldConstraint::new().not_null(), FieldConstraint::new().not_null(),
ValidationLevel::Strict, ValidationLevel::Strict,
)?).add_field(Field::new( )?)
.add_field(Field::new(
"field_value", "field_value",
FieldType::Text, FieldType::Text,
FieldConstraint::new(), FieldConstraint::new(),
ValidationLevel::Strict, ValidationLevel::Strict,
)?).add_field(Field::new( )?)
.add_field(Field::new(
"field_type", "field_type",
FieldType::VarChar(20), FieldType::VarChar(20),
FieldConstraint::new().not_null(), FieldConstraint::new().not_null(),
@ -788,37 +827,45 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
FieldType::VarChar(50), FieldType::VarChar(50),
FieldConstraint::new().primary(), FieldConstraint::new().primary(),
ValidationLevel::Strict, ValidationLevel::Strict,
)?).add_field(Field::new( )?)
.add_field(Field::new(
"slug", "slug",
FieldType::VarChar(50), FieldType::VarChar(50),
FieldConstraint::new().not_null().unique(), FieldConstraint::new().not_null().unique(),
ValidationLevel::Strict, ValidationLevel::Strict,
)?).add_field(Field::new( )?)
.add_field(Field::new(
"type", "type",
FieldType::VarChar(20), FieldType::VarChar(20),
FieldConstraint::new() FieldConstraint::new()
.not_null() .not_null()
.check(WhereClause::Condition(Condition::new( .check(WhereClause::Or(vec![
"type".to_string(), WhereClause::Condition(Condition::new(
Operator::In, "type".to_string(),
Some(SafeValue::Text( Operator::Eq,
"('tag', 'category')".to_string(), Some(SafeValue::Text(
ValidationLevel::Standard, "'tag'".to_string(),
)), ValidationLevel::Raw,
)?)), )),
)?),
WhereClause::Condition(Condition::new(
"type".to_string(),
Operator::Eq,
Some(SafeValue::Text(
"'category'".to_string(),
ValidationLevel::Raw,
)),
)?),
])),
ValidationLevel::Strict, ValidationLevel::Strict,
)?).add_field(Field::new( )?)
"parent_id", .add_field(Field::new(
"parent_name",
FieldType::VarChar(50), FieldType::VarChar(50),
FieldConstraint::new() FieldConstraint::new()
.foreign_key(format!("{}taxonomies", db_prefix), "name".to_string()) .foreign_key(format!("{}taxonomies", db_prefix), "name".to_string())
.on_delete(ForeignKeyAction::SetNull) .on_delete(ForeignKeyAction::SetNull)
.on_update(ForeignKeyAction::Cascade) .on_update(ForeignKeyAction::Cascade),
.check(WhereClause::Condition(Condition::new(
"(type = 'category' OR parent_id IS NULL)".to_string(),
Operator::Raw,
None,
)?)),
ValidationLevel::Strict, ValidationLevel::Strict,
)?); )?);
@ -836,7 +883,8 @@ pub fn generate_schema(db_type: DatabaseType, db_prefix: SafeValue) -> CustomRes
.on_delete(ForeignKeyAction::Cascade) .on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade), .on_update(ForeignKeyAction::Cascade),
ValidationLevel::Strict, ValidationLevel::Strict,
)?).add_field(Field::new( )?)
.add_field(Field::new(
"taxonomy_id", "taxonomy_id",
FieldType::VarChar(50), FieldType::VarChar(50),
FieldConstraint::new() FieldConstraint::new()

View File

@ -456,30 +456,75 @@ const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
); );
}; };
const SetupComplete: React.FC = () => ( const SetupComplete: React.FC = () => {
<StepContainer title="安装完成"> useEffect(() => {
<Flex direction="column" align="center" gap="4"> // 添加延迟后刷新页面
<Text size="5" weight="medium"> const timer = setTimeout(() => {
window.location.reload();
</Text> }, 3000);
<Text size="3">...</Text>
<Box mt="4"> return () => clearTimeout(timer);
<Flex justify="center"> }, []);
<Box className="animate-spin rounded-full h-8 w-8 border-b-2 border-current"></Box>
</Flex> return (
</Box> <StepContainer title="安装完成">
</Flex> <Flex direction="column" align="center" gap="4">
</StepContainer> <Text size="5" weight="medium">
);
</Text>
<Text size="3">...</Text>
<Box mt="4">
<Flex justify="center">
<Box className="animate-spin rounded-full h-8 w-8 border-b-2 border-current"></Box>
</Flex>
</Box>
</Flex>
</StepContainer>
);
};
export default function SetupPage() { export default function SetupPage() {
const [currentStep, setCurrentStep] = useState(1); const [currentStep, setCurrentStep] = useState(1);
const [isClient, setIsClient] = useState(false);
useEffect(() => { useEffect(() => {
// 在客户端组件挂载后更新状态 // 标记客户端渲染完成
const initStatus = Number(import.meta.env.VITE_INIT_STATUS ?? 0) + 1; setIsClient(true);
setCurrentStep(initStatus);
// 获取初始化状态
const initStatus = Number(import.meta.env.VITE_INIT_STATUS ?? 0);
// 如果已完成初始化,直接刷新页面
if (initStatus >= 3) {
window.location.reload();
return;
}
// 否则设置当前步骤
setCurrentStep(initStatus + 1);
}, []); }, []);
// 在服务端渲染时或客户端首次渲染时,返回加载状态
if (!isClient) {
return (
<Theme
grayColor="gray"
accentColor="gray"
radius="medium"
panelBackground="solid"
appearance="inherit"
>
<Box className="min-h-screen w-full">
<Flex justify="center" pt="2">
<Box className="w-20 h-20">
<Echoes />
</Box>
</Flex>
</Box>
</Theme>
);
}
return ( return (
<Theme <Theme
grayColor="gray" grayColor="gray"
@ -490,7 +535,9 @@ export default function SetupPage() {
> >
<Box className="min-h-screen w-full"> <Box className="min-h-screen w-full">
<Box position="fixed" top="2" right="4"> <Box position="fixed" top="2" right="4">
<ThemeModeToggle /> <Box className="w-10 h-10 flex items-center justify-center [&_button]:w-10 [&_button]:h-10 [&_svg]:w-6 [&_svg]:h-6 [&_button]:text-[--gray-12] [&_button:hover]:text-[--accent-9]">
<ThemeModeToggle />
</Box>
</Box> </Box>
<Flex justify="center" pt="2"> <Flex justify="center" pt="2">

View File

@ -7,7 +7,7 @@ import {
} from "@remix-run/react"; } from "@remix-run/react";
import { NotificationProvider } from "hooks/Notification"; import { NotificationProvider } from "hooks/Notification";
import { Theme } from "@radix-ui/themes"; import { Theme } from "@radix-ui/themes";
import { ThemeScript } from "hooks/ThemeMode"; import { ThemeScript } from "hooks/themeMode";
import "~/index.css"; import "~/index.css";
@ -70,3 +70,4 @@ export function Layout() {
export default function App() { export default function App() {
return <Layout />; return <Layout />;
} }

View File

@ -757,7 +757,7 @@ export const ParticleImage = ({
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (ctx) { if (ctx) {
// 增加一个小的边距以确保<EFBFBD><EFBFBD> // 增加一个小的边距以确保
const padding = 2; // 添加2像素的距 const padding = 2; // 添加2像素的距
canvas.width = width + padding * 2; canvas.width = width + padding * 2;
canvas.height = height + padding * 2; canvas.height = height + padding * 2;
@ -961,7 +961,7 @@ export const ParticleImage = ({
containerRef.current.removeChild(renderer.domElement); containerRef.current.removeChild(renderer.domElement);
renderer.dispose(); renderer.dispose();
} }
// 清有 GSAP <EFBFBD><EFBFBD><EFBFBD> // 清有 GSAP
gsap.killTweensOf('*'); gsap.killTweensOf('*');
// 移除 resize 监听 // 移除 resize 监听
@ -1043,11 +1043,6 @@ const loadingQueue = {
add(url: string, instanceId: string, isLongConnection = false) { add(url: string, instanceId: string, isLongConnection = false) {
const key = `${instanceId}:${url}`; const key = `${instanceId}:${url}`;
if (!this.items.has(key)) { 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, { this.items.set(key, {
isProcessing: false, isProcessing: false,
@ -1056,7 +1051,7 @@ const loadingQueue = {
lastActiveTime: Date.now() lastActiveTime: Date.now()
}); });
// 连接再直接进入备选队列,而是等待加载完成后再加入 // 连接<EFBFBD><EFBFBD>再直接进入备选队列,而是等待加载完成后再加入
this.processQueue(); this.processQueue();
return true; return true;
} }
@ -1064,32 +1059,15 @@ const loadingQueue = {
}, },
processQueue() { processQueue() {
console.log('[Queue] Processing queue:', {
availableSlots: this.availableSlots,
currentProcessing: this.currentProcessing,
pendingQueueSize: this.pendingQueue.size,
totalItems: this.items.size
});
if (this.availableSlots > 0) { if (this.availableSlots > 0) {
let nextKey: string | undefined; let nextKey: string | undefined;
// 优先从备选队列中获取已加载完成的长连接 // 修改这部分逻辑,不再区分长连接和普通请求
if (this.pendingQueue.size > 0) { const nextItem = Array.from(this.items.entries())
nextKey = Array.from(this.pendingQueue)[0]; .find(([_, item]) => !item.isProcessing);
this.pendingQueue.delete(nextKey);
console.log('[Queue] Processing from pending queue:', nextKey); if (nextItem) {
} else { nextKey = nextItem[0];
// 如果没有待处理的长连接,处理普通请求
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);
}
} }
if (nextKey) { if (nextKey) {
@ -1101,17 +1079,12 @@ const loadingQueue = {
startProcessing(key: string) { startProcessing(key: string) {
const item = this.items.get(key); const item = this.items.get(key);
if (item && !item.isProcessing) { if (item && !item.isProcessing) {
console.log('[Queue] Start processing:', key, {
isLongConnection: item.isLongConnection,
isError: key.includes('error')
});
item.isProcessing = true; item.isProcessing = true;
// 只有普通请求且不是错误状态时才增加处理数量 // 只有普通请求且不是错误状态时才增加处理数量
if (!item.isLongConnection && !key.includes('error')) { if (!item.isLongConnection && !key.includes('error')) {
this.currentProcessing++; this.currentProcessing++;
console.log('[Queue] Increased processing count:', this.currentProcessing);
} }
} }
}, },
@ -1122,7 +1095,6 @@ const loadingQueue = {
const item = this.items.get(key); const item = this.items.get(key);
if (item?.isLongConnection) { if (item?.isLongConnection) {
this.pendingQueue.add(key); this.pendingQueue.add(key);
console.log('[Queue] Added to pending queue:', key);
} }
}, },
@ -1130,27 +1102,16 @@ const loadingQueue = {
const key = `${instanceId}:${url}`; const key = `${instanceId}:${url}`;
const item = this.items.get(key); 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')) { if (item?.isProcessing && !item.isLongConnection && !key.includes('error')) {
this.currentProcessing--; this.currentProcessing--;
console.log('[Queue] Decreased processing count:', this.currentProcessing);
} }
// 确保从队列中移除 // 确保从队列中移除
this.items.delete(key); this.items.delete(key);
this.pendingQueue.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')) { if (key.includes('error')) {
this.processQueue(); this.processQueue();
@ -1166,10 +1127,9 @@ const loadingQueue = {
const key = `${instanceId}:${url}`; const key = `${instanceId}:${url}`;
const item = this.items.get(key); const item = this.items.get(key);
// 错误状态不占用槽位,只在第一次检查时输出日志 // 错误状态的处理保持不变
if (key.includes('error')) { if (key.includes('error')) {
if (!item?.lastLogTime) { if (!item?.lastLogTime) {
console.log('[Queue] Can process (error):', key, true);
if (item) { if (item) {
item.lastLogTime = Date.now(); item.lastLogTime = Date.now();
} }
@ -1177,22 +1137,13 @@ const loadingQueue = {
return true; 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; const canProcess = item?.isProcessing || false;
// 只在状态发生变化时或者每秒最多输出一次日志
if (item && if (item &&
(item.lastProcessState !== canProcess || (item.lastProcessState !== canProcess ||
!item.lastLogTime || !item.lastLogTime ||
Date.now() - item.lastLogTime > 1000)) { Date.now() - item.lastLogTime > 1000)) {
console.log('[Queue] Can process (normal):', key, canProcess);
item.lastProcessState = canProcess; item.lastProcessState = canProcess;
item.lastLogTime = Date.now(); item.lastLogTime = Date.now();
} }
@ -1233,7 +1184,6 @@ export const ImageLoader = ({
useEffect(() => { useEffect(() => {
if (!src) return; if (!src) return;
console.log('[Queue] Effect triggered for:', src, 'Instance:', instanceId.current);
setShowImage(false); setShowImage(false);
setAnimationComplete(false); setAnimationComplete(false);
setCanShowParticles(false); setCanShowParticles(false);
@ -1246,7 +1196,6 @@ export const ImageLoader = ({
const timeSinceLastAnimation = now - lastAnimationTime; const timeSinceLastAnimation = now - lastAnimationTime;
if (particleLoadQueue.size === 0) { if (particleLoadQueue.size === 0) {
console.log('[Queue] Starting immediate animation for:', src, 'Instance:', instanceId.current);
particleLoadQueue.add(src); particleLoadQueue.add(src);
setCanShowParticles(true); setCanShowParticles(true);
lastAnimationTime = now; lastAnimationTime = now;
@ -1258,12 +1207,10 @@ export const ImageLoader = ({
Math.min(ANIMATION_THRESHOLD, timeSinceLastAnimation) Math.min(ANIMATION_THRESHOLD, timeSinceLastAnimation)
); );
console.log('[Queue] Scheduling delayed animation for:', src, 'Instance:', instanceId.current);
const timer = setTimeout(() => { const timer = setTimeout(() => {
const key = `${instanceId.current}:${src}`; const key = `${instanceId.current}:${src}`;
if (!loadingQueue.items.has(key)) return; if (!loadingQueue.items.has(key)) return;
console.log('[Queue] Starting delayed animation for:', src, 'Instance:', instanceId.current);
particleLoadQueue.add(src); particleLoadQueue.add(src);
setCanShowParticles(true); setCanShowParticles(true);
lastAnimationTime = Date.now(); lastAnimationTime = Date.now();
@ -1282,7 +1229,6 @@ export const ImageLoader = ({
const cleanup = checkQueue(); const cleanup = checkQueue();
return () => { return () => {
console.log('[Queue] Cleanup effect for:', src, 'Instance:', instanceId.current);
cleanup?.(); cleanup?.();
loadingQueue.remove(src, instanceId.current); loadingQueue.remove(src, instanceId.current);
}; };
@ -1393,7 +1339,6 @@ export const ImageLoader = ({
}; };
img.onerror = () => { img.onerror = () => {
console.log('[Image Loader] Error loading image:', src);
if (timeoutRef.current) { if (timeoutRef.current) {
clearTimeout(timeoutRef.current); clearTimeout(timeoutRef.current);
} }
@ -1429,7 +1374,7 @@ export const ImageLoader = ({
}; };
}, [src, preloadImage]); }, [src, preloadImage]);
// 添加一个的状来控制粒子动画 // 添加一个<EFBFBD><EFBFBD><EFBFBD>的状来控制粒子动画
const [canShowParticles, setCanShowParticles] = useState(false); const [canShowParticles, setCanShowParticles] = useState(false);
// 添加加载动画组件 // 添加加载动画组件
@ -1449,25 +1394,20 @@ export const ImageLoader = ({
src={src} src={src}
status={status} status={status}
onLoad={() => { onLoad={() => {
console.log('[ParticleImage] onLoad START:', src);
if (imageRef.current) { if (imageRef.current) {
// 保持为空 // 保持为空
} }
console.log('[ParticleImage] onLoad END:', src);
}} }}
onAnimationComplete={() => { onAnimationComplete={() => {
console.log('[ParticleImage] Animation START:', src);
if (imageRef.current && src) { if (imageRef.current && src) {
setShowImage(true); setShowImage(true);
requestAnimationFrame(() => { requestAnimationFrame(() => {
console.log('[ParticleImage] Setting animation complete:', src);
setAnimationComplete(true); setAnimationComplete(true);
particleLoadQueue.delete(src); particleLoadQueue.delete(src);
loadingQueue.remove(src, instanceId.current); loadingQueue.remove(src, instanceId.current);
setTimeout(() => { setTimeout(() => {
console.log('[ParticleImage] Fading image:', src);
const img = document.querySelector(`img[src="${imageRef.current?.src}"]`) as HTMLImageElement; const img = document.querySelector(`img[src="${imageRef.current?.src}"]`) as HTMLImageElement;
if (img) { if (img) {
img.style.opacity = '1'; img.style.opacity = '1';
@ -1475,7 +1415,6 @@ export const ImageLoader = ({
}, 50); }, 50);
}); });
} }
console.log('[ParticleImage] Animation END:', src);
}} }}
/> />
)} )}

View File

@ -4,89 +4,97 @@ import { Button } from "@radix-ui/themes";
const THEME_KEY = "theme-preference"; const THEME_KEY = "theme-preference";
// 添加这个脚本来预先设置主题,避免闪烁 // 修改主题脚本,确保在服务端和客户端都能正确初始化
const themeScript = ` const themeScript = `
(function() { (function() {
function getInitialTheme() { try {
const savedTheme = localStorage.getItem("${THEME_KEY}"); const savedTheme = localStorage.getItem("${THEME_KEY}");
if (savedTheme) { const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.className = savedTheme; const theme = savedTheme || (prefersDark ? "dark" : "light");
return savedTheme; document.documentElement.dataset.theme = theme;
} document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(theme);
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; } catch (e) {
const theme = isDark ? "dark" : "light"; console.error('[ThemeScript] Error:', e);
document.documentElement.className = theme; document.documentElement.dataset.theme = 'light';
localStorage.setItem("${THEME_KEY}", theme); document.documentElement.classList.remove('light', 'dark');
return theme; document.documentElement.classList.add('light');
}
// 确保在 DOM 内容加载前执行
if (document.documentElement) {
getInitialTheme();
} else {
document.addEventListener('DOMContentLoaded', getInitialTheme);
} }
})() })()
`; `;
// ThemeScript 组件需要尽早执行
export const ThemeScript = () => { export const ThemeScript = () => {
return <script dangerouslySetInnerHTML={{ __html: themeScript }} />; return (
<script
dangerouslySetInnerHTML={{
__html: themeScript,
}}
/>
);
};
// 客户端专用的 hook
const useClientOnly = (callback: () => void, deps: any[] = []) => {
useEffect(() => {
if (typeof window !== 'undefined') {
callback();
}
}, deps);
}; };
export const ThemeModeToggle: React.FC = () => { export const ThemeModeToggle: React.FC = () => {
const [isDark, setIsDark] = useState<boolean | null>(null); const [mounted, setMounted] = useState(false);
const [theme, setTheme] = useState<string>(() => {
// 初始化主题状态 if (typeof document !== 'undefined') {
useEffect(() => { return document.documentElement.dataset.theme || 'light';
if (typeof window !== 'undefined') {
const initTheme = () => {
const savedTheme = localStorage.getItem(THEME_KEY);
const currentTheme = document.documentElement.className;
// 确保 localStorage 和 DOM 的主题状态一致
if (savedTheme && savedTheme !== currentTheme) {
document.documentElement.className = savedTheme;
}
setIsDark(savedTheme === 'dark' || currentTheme === 'dark');
};
initTheme();
// 监听系统主题变化
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemThemeChange = (e: MediaQueryListEvent) => {
if (!localStorage.getItem(THEME_KEY)) {
const newTheme = e.matches ? 'dark' : 'light';
document.documentElement.className = newTheme;
setIsDark(e.matches);
}
};
mediaQuery.addEventListener('change', handleSystemThemeChange);
return () => {
mediaQuery.removeEventListener('change', handleSystemThemeChange);
};
} }
return 'light';
});
useClientOnly(() => {
const currentTheme = document.documentElement.dataset.theme || 'light';
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(currentTheme);
setTheme(currentTheme);
setMounted(true);
}, []); }, []);
useEffect(() => {
if (!mounted) return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemThemeChange = (e: MediaQueryListEvent) => {
if (!localStorage.getItem(THEME_KEY)) {
const newTheme = e.matches ? 'dark' : 'light';
updateTheme(newTheme);
}
};
mediaQuery.addEventListener('change', handleSystemThemeChange);
return () => mediaQuery.removeEventListener('change', handleSystemThemeChange);
}, [mounted]);
const updateTheme = (newTheme: string) => {
document.documentElement.dataset.theme = newTheme;
document.documentElement.classList.remove('light', 'dark');
document.documentElement.classList.add(newTheme);
setTheme(newTheme);
};
const toggleTheme = () => { const toggleTheme = () => {
if (isDark === null) return; const newTheme = theme === 'dark' ? 'light' : 'dark';
const newIsDark = !isDark; updateTheme(newTheme);
setIsDark(newIsDark);
const newTheme = newIsDark ? "dark" : "light";
document.documentElement.className = newTheme;
localStorage.setItem(THEME_KEY, newTheme); localStorage.setItem(THEME_KEY, newTheme);
}; };
if (isDark === null) { if (!mounted) {
return ( return (
<Button <Button
variant="ghost" variant="ghost"
className="w-full h-full p-0 rounded-lg transition-all duration-300 transform" className="w-full h-full p-0 rounded-lg transition-all duration-300 transform"
aria-label="Loading theme" aria-label="Theme toggle"
> >
<MoonIcon className="w-full h-full" /> <MoonIcon className="w-full h-full" />
</Button> </Button>
@ -100,7 +108,7 @@ export const ThemeModeToggle: React.FC = () => {
className="w-full h-full p-0 rounded-lg transition-all duration-300 transform" className="w-full h-full p-0 rounded-lg transition-all duration-300 transform"
aria-label="Toggle theme" aria-label="Toggle theme"
> >
{isDark ? ( {theme === 'dark' ? (
<SunIcon className="w-full h-full" /> <SunIcon className="w-full h-full" />
) : ( ) : (
<MoonIcon className="w-full h-full" /> <MoonIcon className="w-full h-full" />
@ -109,44 +117,34 @@ export const ThemeModeToggle: React.FC = () => {
); );
}; };
// 更新类型定义
declare global {
interface Window {
__THEME__?: "light" | "dark";
}
}
export const useThemeMode = () => { export const useThemeMode = () => {
const [mounted, setMounted] = useState(false);
const [mode, setMode] = useState<"light" | "dark">("light"); const [mode, setMode] = useState<"light" | "dark">("light");
useEffect(() => { useClientOnly(() => {
if (typeof window !== "undefined") { const handleThemeChange = () => {
const saved = localStorage.getItem(THEME_KEY); const currentTheme = document.documentElement.dataset.theme as "light" | "dark";
if (saved) { setMode(currentTheme || "light");
setMode(saved as "light" | "dark"); };
} else {
const isDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
setMode(isDark ? "dark" : "light");
}
// 监听主题变化事件 handleThemeChange();
const handleThemeChange = (e: CustomEvent) => { setMounted(true);
setMode(e.detail.theme);
};
window.addEventListener( const observer = new MutationObserver((mutations) => {
"theme-change", mutations.forEach((mutation) => {
handleThemeChange as EventListener, if (mutation.attributeName === 'data-theme') {
); handleThemeChange();
return () => }
window.removeEventListener( });
"theme-change", });
handleThemeChange as EventListener,
); observer.observe(document.documentElement, {
} attributes: true,
attributeFilter: ['data-theme']
});
return () => observer.disconnect();
}, []); }, []);
return { mode }; return { mode: mounted ? mode : "light" };
}; };

View File

@ -64,6 +64,7 @@
"postcss": "^8.4.38", "postcss": "^8.4.38",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.4",
"terser": "^5.37.0",
"tsx": "^4.19.2", "tsx": "^4.19.2",
"typescript": "^5.1.6", "typescript": "^5.1.6",
"vite": "^5.1.0", "vite": "^5.1.0",

View File

@ -1,5 +1,5 @@
import { vitePlugin as remix } from "@remix-run/dev"; import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig, loadEnv } from "vite"; import { defineConfig, loadEnv, ConfigEnv, UserConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths"; import tsconfigPaths from "vite-tsconfig-paths";
import { resolve } from "path"; import { resolve } from "path";
import { readEnvFile } from "./server/env"; import { readEnvFile } from "./server/env";
@ -30,7 +30,7 @@ const createDefineConfig = (config: EnvConfig) => {
); );
}; };
export default defineConfig(async ({ mode }) => { export default defineConfig(async ({ mode }: ConfigEnv): Promise<UserConfig> => {
// 确保每次都读取最新的环境变量 // 确保每次都读取最新的环境变量
const currentConfig = await getLatestEnv(); const currentConfig = await getLatestEnv();
const env = loadEnv(mode, process.cwd(), "VITE_"); const env = loadEnv(mode, process.cwd(), "VITE_");
@ -64,8 +64,7 @@ export default defineConfig(async ({ mode }) => {
], ],
define: createDefineConfig(currentConfig), define: createDefineConfig(currentConfig),
server: { server: {
host: true, host: currentConfig.VITE_ADDRESS,
address: currentConfig.VITE_ADDRESS,
port: Number(env.VITE_SYSTEM_PORT ?? currentConfig.VITE_PORT), port: Number(env.VITE_SYSTEM_PORT ?? currentConfig.VITE_PORT),
strictPort: true, strictPort: true,
hmr: true, hmr: true,
@ -89,9 +88,28 @@ export default defineConfig(async ({ mode }) => {
envPrefix: "VITE_", envPrefix: "VITE_",
build: { build: {
rollupOptions: { rollupOptions: {
// 移除 manualChunks 配置 output: {
manualChunks(id) {
// 根据模块路径进行代码分割
if (id.includes('node_modules')) {
return 'vendor';
}
}
}
}, },
chunkSizeWarningLimit: 1500 chunkSizeWarningLimit: 1500,
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
cssMinify: true,
cssCodeSplit: true,
sourcemap: mode !== 'production',
assetsInlineLimit: 4096,
reportCompressedSize: false,
}, },
ssr: { ssr: {
noExternal: ['three', '@react-three/fiber', '@react-three/drei', 'gsap'] noExternal: ['three', '@react-three/fiber', '@react-three/drei', 'gsap']