优化SQL查询构建,新增NOT条件支持,修复文本转义逻辑,更新主题切换逻辑,改进加载动画,更新依赖项,重构主题管理组件,修复多个小问题。
This commit is contained in:
parent
507595a269
commit
195f8de576
@ -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<WhereClause>),
|
||||
Or(Vec<WhereClause>),
|
||||
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))
|
||||
|
@ -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()),
|
||||
|
@ -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<Vec<String>> = conditions
|
||||
.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> {
|
||||
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(
|
||||
@ -681,44 +724,44 @@ 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()
|
||||
.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()
|
||||
|
@ -456,30 +456,75 @@ const AdminConfig: React.FC<StepProps> = ({ onNext }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const SetupComplete: React.FC = () => (
|
||||
<StepContainer title="安装完成">
|
||||
<Flex direction="column" align="center" gap="4">
|
||||
<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>
|
||||
);
|
||||
const SetupComplete: React.FC = () => {
|
||||
useEffect(() => {
|
||||
// 添加延迟后刷新页面
|
||||
const timer = setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 3000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StepContainer title="安装完成">
|
||||
<Flex direction="column" align="center" gap="4">
|
||||
<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() {
|
||||
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 (
|
||||
<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 (
|
||||
<Theme
|
||||
grayColor="gray"
|
||||
@ -490,7 +535,9 @@ export default function SetupPage() {
|
||||
>
|
||||
<Box className="min-h-screen w-full">
|
||||
<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>
|
||||
|
||||
<Flex justify="center" pt="2">
|
||||
|
@ -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 <Layout />;
|
||||
}
|
||||
|
||||
|
@ -757,7 +757,7 @@ export const ParticleImage = ({
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (ctx) {
|
||||
// 增加一个小的边距以确保<EFBFBD><EFBFBD>盖
|
||||
// 增加一个小的边距以确保盖
|
||||
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 <EFBFBD><EFBFBD><EFBFBD>画
|
||||
// 清有 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()
|
||||
});
|
||||
|
||||
// 连接不再直接进入备选队列,而是等待加载完成后再加入
|
||||
// 连接<EFBFBD><EFBFBD>再直接进入备选队列,而是等待加载完成后再加入
|
||||
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]);
|
||||
|
||||
// 添加一个新的状来控制粒子动画
|
||||
// 添加一个<EFBFBD><EFBFBD><EFBFBD>的状来控制粒子动画
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -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 <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 = () => {
|
||||
const [isDark, setIsDark] = useState<boolean | null>(null);
|
||||
|
||||
// 初始化主题状态
|
||||
useEffect(() => {
|
||||
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);
|
||||
};
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [theme, setTheme] = useState<string>(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
return document.documentElement.dataset.theme || 'light';
|
||||
}
|
||||
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 = () => {
|
||||
if (isDark === null) return;
|
||||
const newIsDark = !isDark;
|
||||
setIsDark(newIsDark);
|
||||
const newTheme = newIsDark ? "dark" : "light";
|
||||
document.documentElement.className = newTheme;
|
||||
const newTheme = theme === 'dark' ? 'light' : 'dark';
|
||||
updateTheme(newTheme);
|
||||
localStorage.setItem(THEME_KEY, newTheme);
|
||||
};
|
||||
|
||||
if (isDark === null) {
|
||||
if (!mounted) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
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" />
|
||||
</Button>
|
||||
@ -100,7 +108,7 @@ export const ThemeModeToggle: React.FC = () => {
|
||||
className="w-full h-full p-0 rounded-lg transition-all duration-300 transform"
|
||||
aria-label="Toggle theme"
|
||||
>
|
||||
{isDark ? (
|
||||
{theme === 'dark' ? (
|
||||
<SunIcon 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 = () => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [mode, setMode] = useState<"light" | "dark">("light");
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const saved = localStorage.getItem(THEME_KEY);
|
||||
if (saved) {
|
||||
setMode(saved as "light" | "dark");
|
||||
} else {
|
||||
const isDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
).matches;
|
||||
setMode(isDark ? "dark" : "light");
|
||||
}
|
||||
useClientOnly(() => {
|
||||
const handleThemeChange = () => {
|
||||
const currentTheme = document.documentElement.dataset.theme as "light" | "dark";
|
||||
setMode(currentTheme || "light");
|
||||
};
|
||||
|
||||
// 监听主题变化事件
|
||||
const handleThemeChange = (e: CustomEvent) => {
|
||||
setMode(e.detail.theme);
|
||||
};
|
||||
handleThemeChange();
|
||||
setMounted(true);
|
||||
|
||||
window.addEventListener(
|
||||
"theme-change",
|
||||
handleThemeChange as EventListener,
|
||||
);
|
||||
return () =>
|
||||
window.removeEventListener(
|
||||
"theme-change",
|
||||
handleThemeChange as EventListener,
|
||||
);
|
||||
}
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.attributeName === 'data-theme') {
|
||||
handleThemeChange();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme']
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return { mode };
|
||||
return { mode: mounted ? mode : "light" };
|
||||
};
|
||||
|
@ -64,6 +64,7 @@
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.3.3",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"terser": "^5.37.0",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^5.1.0",
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 { resolve } from "path";
|
||||
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 env = loadEnv(mode, process.cwd(), "VITE_");
|
||||
@ -64,8 +64,7 @@ export default defineConfig(async ({ mode }) => {
|
||||
],
|
||||
define: createDefineConfig(currentConfig),
|
||||
server: {
|
||||
host: true,
|
||||
address: currentConfig.VITE_ADDRESS,
|
||||
host: currentConfig.VITE_ADDRESS,
|
||||
port: Number(env.VITE_SYSTEM_PORT ?? currentConfig.VITE_PORT),
|
||||
strictPort: true,
|
||||
hmr: true,
|
||||
@ -89,9 +88,28 @@ export default defineConfig(async ({ mode }) => {
|
||||
envPrefix: "VITE_",
|
||||
build: {
|
||||
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: {
|
||||
noExternal: ['three', '@react-three/fiber', '@react-three/drei', 'gsap']
|
||||
|
Loading…
Reference in New Issue
Block a user