From 639a0fd50486ec863fb33fcbb5efcb0cdbd187f5 Mon Sep 17 00:00:00 2001 From: lsy Date: Tue, 10 Dec 2024 10:28:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0cxm,ccr=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?=E5=85=B6=E4=BB=96=E6=9C=AA=E6=9B=B4=E6=96=B0=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rust/blog_os/.cargo/config.toml | 9 + rust/blog_os/Cargo.toml | 12 + rust/blog_os/src/main.rs | 15 + rust/blog_os/x86_64-blog_os.json | 15 + rust/svg/src/App.css | 2 +- rust/svg/src/App.tsx | 17 +- rust/svg/src/lib.rs | 3 +- rust/svg/src/vite-env.d.ts | 1 + rust/temp/Cargo.toml | 6 + rust/temp/src/core/mod.rs | 84 --- rust/temp/src/main.rs | 14 - rust/temp/src/request/mod.rs | 102 ---- rust/temp/src/respond/mod.rs | 60 --- rust/temp/src/route/mod.rs | 10 - rust/wasm/life_game/src/index.js | 6 +- web/chess/README.md | 50 ++ web/chess/eslint.config.js | 28 + web/chess/index.html | 13 + web/chess/src/App.tsx | 1 + web/chess/tsconfig.app.json | 25 + web/chess/tsconfig.json | 7 + web/chess/tsconfig.node.json | 23 + web/chess/vite.config.ts | 7 + web/cxm/README.md | 50 ++ web/cxm/eslint.config.js | 28 + web/cxm/index.html | 13 + web/cxm/package.json | 35 ++ web/cxm/src/App.css | 54 ++ web/cxm/src/App.d.ts | 3 + web/cxm/src/App.js | 7 + web/cxm/src/App.tsx | 12 + web/cxm/src/assets/z.woff2 | Bin 0 -> 55516 bytes web/cxm/src/components/ImageModal.css | 40 ++ web/cxm/src/components/ImageModal.d.ts | 7 + web/cxm/src/components/ImageModal.js | 15 + web/cxm/src/components/ImageModal.tsx | 28 + web/cxm/src/components/Parallax.css | 530 ++++++++++++++++++ web/cxm/src/components/Parallax.d.ts | 2 + web/cxm/src/components/Parallax.js | 237 +++++++++ web/cxm/src/components/Parallax.tsx | 394 ++++++++++++++ web/cxm/src/index.css | 98 ++++ web/cxm/src/main.d.ts | 1 + web/cxm/src/main.js | 6 + web/cxm/src/main.tsx | 10 + web/cxm/src/vite-env.d.ts | 1 + web/cxm/tsconfig.app.json | 13 + web/cxm/tsconfig.app.tsbuildinfo | 1 + web/cxm/tsconfig.json | 7 + web/cxm/tsconfig.node.json | 8 + web/cxm/tsconfig.node.tsbuildinfo | 1 + web/cxm/vite.config.d.ts | 2 + web/cxm/vite.config.js | 6 + web/cxm/vite.config.ts | 7 + web/czr/.eslintrc.cjs | 84 +++ web/czr/app/components/Carousel.tsx | 36 ++ web/czr/app/components/Navigation.tsx | 116 ++++ web/czr/app/entry.client.tsx | 18 + web/czr/app/entry.server.tsx | 89 ++++ web/czr/app/env.ts | 27 + web/czr/app/hooks/ParticleImage.tsx | 3 + web/czr/app/index.css | 196 +++++++ web/czr/app/init.tsx | 508 ++++++++++++++++++ web/czr/app/root.tsx | 31 ++ web/czr/app/routes.tsx | 46 ++ web/czr/app/routes/_index.tsx | 146 +++++ web/czr/app/routes/about.tsx | 67 +++ web/czr/app/routes/innovations.tsx | 157 ++++++ web/czr/app/routes/solutions.tsx | 105 ++++ web/czr/app/styles/navigation.css | 5 + web/czr/hooks/ParticleImage.tsx | 711 +++++++++++++++++++++++++ web/czr/hooks/error.tsx | 42 ++ web/czr/hooks/loading.tsx | 103 ++++ web/czr/hooks/notification.tsx | 182 +++++++ web/czr/hooks/themeMode.tsx | 133 +++++ web/czr/index.html | 12 + web/czr/package.json | 81 +++ web/czr/postcss.config.js | 6 + web/czr/server/entry.server.js | 42 ++ web/czr/server/env.ts | 32 ++ web/czr/server/express.ts | 71 +++ web/czr/server/production.js | 23 + web/czr/server/static.js | 31 ++ web/czr/src/entry.client.tsx | 7 + web/czr/start.bat | 79 +++ web/czr/styles/echoes.css | 88 +++ web/czr/tailwind.config.ts | 36 ++ web/czr/tsconfig.json | 32 ++ web/czr/vite.config.ts | 39 ++ 88 files changed, 5226 insertions(+), 284 deletions(-) create mode 100644 rust/blog_os/.cargo/config.toml create mode 100644 rust/blog_os/Cargo.toml create mode 100644 rust/blog_os/src/main.rs create mode 100644 rust/blog_os/x86_64-blog_os.json create mode 100644 rust/svg/src/vite-env.d.ts create mode 100644 rust/temp/Cargo.toml delete mode 100644 rust/temp/src/core/mod.rs delete mode 100644 rust/temp/src/request/mod.rs delete mode 100644 rust/temp/src/respond/mod.rs delete mode 100644 rust/temp/src/route/mod.rs create mode 100644 web/chess/README.md create mode 100644 web/chess/eslint.config.js create mode 100644 web/chess/index.html create mode 100644 web/chess/tsconfig.app.json create mode 100644 web/chess/tsconfig.json create mode 100644 web/chess/tsconfig.node.json create mode 100644 web/chess/vite.config.ts create mode 100644 web/cxm/README.md create mode 100644 web/cxm/eslint.config.js create mode 100644 web/cxm/index.html create mode 100644 web/cxm/package.json create mode 100644 web/cxm/src/App.css create mode 100644 web/cxm/src/App.d.ts create mode 100644 web/cxm/src/App.js create mode 100644 web/cxm/src/App.tsx create mode 100644 web/cxm/src/assets/z.woff2 create mode 100644 web/cxm/src/components/ImageModal.css create mode 100644 web/cxm/src/components/ImageModal.d.ts create mode 100644 web/cxm/src/components/ImageModal.js create mode 100644 web/cxm/src/components/ImageModal.tsx create mode 100644 web/cxm/src/components/Parallax.css create mode 100644 web/cxm/src/components/Parallax.d.ts create mode 100644 web/cxm/src/components/Parallax.js create mode 100644 web/cxm/src/components/Parallax.tsx create mode 100644 web/cxm/src/index.css create mode 100644 web/cxm/src/main.d.ts create mode 100644 web/cxm/src/main.js create mode 100644 web/cxm/src/main.tsx create mode 100644 web/cxm/src/vite-env.d.ts create mode 100644 web/cxm/tsconfig.app.json create mode 100644 web/cxm/tsconfig.app.tsbuildinfo create mode 100644 web/cxm/tsconfig.json create mode 100644 web/cxm/tsconfig.node.json create mode 100644 web/cxm/tsconfig.node.tsbuildinfo create mode 100644 web/cxm/vite.config.d.ts create mode 100644 web/cxm/vite.config.js create mode 100644 web/cxm/vite.config.ts create mode 100644 web/czr/.eslintrc.cjs create mode 100644 web/czr/app/components/Carousel.tsx create mode 100644 web/czr/app/components/Navigation.tsx create mode 100644 web/czr/app/entry.client.tsx create mode 100644 web/czr/app/entry.server.tsx create mode 100644 web/czr/app/env.ts create mode 100644 web/czr/app/hooks/ParticleImage.tsx create mode 100644 web/czr/app/index.css create mode 100644 web/czr/app/init.tsx create mode 100644 web/czr/app/root.tsx create mode 100644 web/czr/app/routes.tsx create mode 100644 web/czr/app/routes/_index.tsx create mode 100644 web/czr/app/routes/about.tsx create mode 100644 web/czr/app/routes/innovations.tsx create mode 100644 web/czr/app/routes/solutions.tsx create mode 100644 web/czr/app/styles/navigation.css create mode 100644 web/czr/hooks/ParticleImage.tsx create mode 100644 web/czr/hooks/error.tsx create mode 100644 web/czr/hooks/loading.tsx create mode 100644 web/czr/hooks/notification.tsx create mode 100644 web/czr/hooks/themeMode.tsx create mode 100644 web/czr/index.html create mode 100644 web/czr/package.json create mode 100644 web/czr/postcss.config.js create mode 100644 web/czr/server/entry.server.js create mode 100644 web/czr/server/env.ts create mode 100644 web/czr/server/express.ts create mode 100644 web/czr/server/production.js create mode 100644 web/czr/server/static.js create mode 100644 web/czr/src/entry.client.tsx create mode 100644 web/czr/start.bat create mode 100644 web/czr/styles/echoes.css create mode 100644 web/czr/tailwind.config.ts create mode 100644 web/czr/tsconfig.json create mode 100644 web/czr/vite.config.ts diff --git a/rust/blog_os/.cargo/config.toml b/rust/blog_os/.cargo/config.toml new file mode 100644 index 0000000..bd02bb2 --- /dev/null +++ b/rust/blog_os/.cargo/config.toml @@ -0,0 +1,9 @@ +[unstable] +build-std-features = ["compiler-builtins-mem"] +build-std = ["core", "compiler_builtins"] + +[build] +target = "x86_64-blog_os.json" + +[target.'cfg(target_os = "none")'] +runner = "bootimage runner" \ No newline at end of file diff --git a/rust/blog_os/Cargo.toml b/rust/blog_os/Cargo.toml new file mode 100644 index 0000000..6ae87d4 --- /dev/null +++ b/rust/blog_os/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "blog_os" +version = "0.1.0" +edition = "2018" + +# 使用 `cargo build` 编译时需要的配置 +[profile.dev] +panic = "abort" # 禁用panic时栈展开 + +# 使用 `cargo build --release` 编译时需要的配置 +[profile.release] +panic = "abort" # 禁用 panic 时栈展开 diff --git a/rust/blog_os/src/main.rs b/rust/blog_os/src/main.rs new file mode 100644 index 0000000..9b760fb --- /dev/null +++ b/rust/blog_os/src/main.rs @@ -0,0 +1,15 @@ +// src/main.rs +#![no_std] // 不链接Rust标准库 +#![no_main] // 禁用所有Rust层级的入口点 +use core::panic::PanicInfo; +/// 这个函数将在panic时被调用 +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} +#[no_mangle] // 不重整函数名 +pub extern "C" fn _start() -> ! { + // 因为编译器会寻找一个名为`_start`的函数,所以这个函数就是入口点 + // 默认命名为`_start` + loop {} +} \ No newline at end of file diff --git a/rust/blog_os/x86_64-blog_os.json b/rust/blog_os/x86_64-blog_os.json new file mode 100644 index 0000000..97da923 --- /dev/null +++ b/rust/blog_os/x86_64-blog_os.json @@ -0,0 +1,15 @@ +{ + "llvm-target": "x86_64-unknown-none", + "data-layout": "e-m:e-i64:64-f80:128-n8:16:32:64-S128", + "arch": "x86_64", + "target-endian": "little", + "target-pointer-width": "64", + "target-c-int-width": "32", + "os": "none", + "executables": true, + "linker-flavor": "ld.lld", + "linker": "rust-lld", + "panic-strategy": "abort", + "disable-redzone": true, + "features": "-mmx,-sse,+soft-float" +} \ No newline at end of file diff --git a/rust/svg/src/App.css b/rust/svg/src/App.css index bd593b2..c63d8db 100644 --- a/rust/svg/src/App.css +++ b/rust/svg/src/App.css @@ -210,8 +210,8 @@ fill: transparent; stroke: currentColor; stroke-width: 2; + stroke-dasharray: var(--path-length); stroke-dashoffset: var(--path-length); - stroke-dasharray: var(--path-length) var(--path-length); animation: logo-anim 15s cubic-bezier(0.4, 0, 0.2, 1) infinite; transform-origin: center; stroke-linecap: round; diff --git a/rust/svg/src/App.tsx b/rust/svg/src/App.tsx index 605292a..06921e8 100644 --- a/rust/svg/src/App.tsx +++ b/rust/svg/src/App.tsx @@ -56,7 +56,7 @@ const App: React.FC = () => { }; const handleDownloadCss = () => { - const animationCss = ` + const animationCss = `/* 注意:每个路径都需要在 SVG 中设置 --path-length 变量 */ .animated-text { max-width: 100%; height: auto; @@ -66,8 +66,9 @@ const App: React.FC = () => { fill: transparent; stroke: currentColor; stroke-width: 2; + /* 使用每个路径自己的长度 */ + stroke-dasharray: var(--path-length); stroke-dashoffset: var(--path-length); - stroke-dasharray: var(--path-length) var(--path-length); animation: logo-anim 15s cubic-bezier(0.4, 0, 0.2, 1) infinite; transform-origin: center; stroke-linecap: round; @@ -88,14 +89,12 @@ const App: React.FC = () => { stroke-dasharray: var(--path-length) var(--path-length); } - /* 慢速绘画过程 */ 50% { stroke-dashoffset: 0; stroke-dasharray: var(--path-length) var(--path-length); fill: transparent; } - /* 慢慢填充效果 */ 60% { stroke-dashoffset: 0; stroke-dasharray: var(--path-length) var(--path-length); @@ -103,7 +102,6 @@ const App: React.FC = () => { opacity: 1; } - /* 保持填充状态 */ 75% { stroke-dashoffset: 0; stroke-dasharray: var(--path-length) var(--path-length); @@ -111,7 +109,6 @@ const App: React.FC = () => { opacity: 1; } - /* 变回线条 */ 85% { stroke-dashoffset: 0; stroke-dasharray: var(--path-length) var(--path-length); @@ -119,7 +116,6 @@ const App: React.FC = () => { opacity: 1; } - /* 线条消失 */ 95% { stroke-dashoffset: var(--path-length); stroke-dasharray: var(--path-length) var(--path-length); @@ -133,6 +129,13 @@ const App: React.FC = () => { fill: transparent; opacity: 0; } +} + +/* 确保在暗色模式下的颜色正确 */ +@media (prefers-color-scheme: dark) { + .animated-text path { + stroke: currentColor; + } }`; const blob = new Blob([animationCss], { type: 'text/css' }); diff --git a/rust/svg/src/lib.rs b/rust/svg/src/lib.rs index b21a52d..9c51fba 100644 --- a/rust/svg/src/lib.rs +++ b/rust/svg/src/lib.rs @@ -210,7 +210,8 @@ impl FontHandler { paths.push(Path::new() .set("d", builder.path_data) - .set("fill", "currentColor")); + .set("fill", "currentColor") + .set("style", format!("--path-length: {};", builder.path_length))); } } diff --git a/rust/svg/src/vite-env.d.ts b/rust/svg/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/rust/svg/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/rust/temp/Cargo.toml b/rust/temp/Cargo.toml new file mode 100644 index 0000000..5a1b222 --- /dev/null +++ b/rust/temp/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "temp" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/rust/temp/src/core/mod.rs b/rust/temp/src/core/mod.rs deleted file mode 100644 index 19b6bd8..0000000 --- a/rust/temp/src/core/mod.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::sync::{Arc, RwLock}; -use tokio::io::{AsyncReadExt}; -use tokio::{net, runtime}; -use crate::request::{Method,Request}; -use crate::route::{Route}; -use crate::respond::{Respond}; - -pub struct Server { - listener_addr: &'static str, - listener_port: i32, - status: bool, - routes:RwLock>, -} -impl Server { - pub fn new(listener_port:i32) -> Arc { - if listener_port < 1 || listener_port > 65535 { - panic!("listener port must be between 1 and 65535"); - } - Arc::new( - Self { - listener_addr: "127.0.0.1", - listener_port, - status: false, - routes: RwLock::new(Vec::new()), - } - ) - } - pub fn start(self: Arc) { - let address=format!("{}:{}", self.listener_addr, self.listener_port); - let rt = runtime::Runtime::new().unwrap(); - println!("Listening on {}", address); - let listener =rt.block_on(async { net::TcpListener::bind(address).await.expect("Failed to bind listener") }); - rt.block_on(self.receive(listener)); - } -} - -impl Server { - async fn receive(self:Arc,listener:net::TcpListener) { - loop { - let (mut socket, _) = listener.accept().await.expect("Failed to accept connection"); - let server = self.clone(); - tokio::spawn(async move { - let mut buf = [0; 1024]; - if let Err(e) = socket.read(&mut buf).await { - println!("Failed to read from socket: {}", e); - return; - } - let content = match String::from_utf8(buf.to_vec()) { - Ok(s) => s, - Err(e1) => { - println!("Failed to convert buffer to string: {}", e1); - return; - } - }; - let request = match Request::build(content) { - Some(req) => req, - None => { - println!("Request parsing failed"); - return; - } - }; - let respond=Respond::build(socket); - for route in server.routes.read().expect("Unable to read route").iter() { - if request.method() == &route.method { - (route.function)(request, respond); - return; - }else { - println!("Request parsing failed"); - } - - } - }); - } - } -} - -impl Server { - pub fn route(self:&Arc, path: &'static str, method: &'static str, function: impl Fn(Request, Arc) + Send + Sync + 'static) { - let test_method=method.to_uppercase().as_str().into(); - if test_method == Method::Uninitialized {panic!("该方法未被定义")} - let route = Route { path, method:test_method, function: Box::new(function) }; - self.routes.write().expect("Route write failed").push(route); - } -} diff --git a/rust/temp/src/main.rs b/rust/temp/src/main.rs index 50f2d2f..e69de29 100644 --- a/rust/temp/src/main.rs +++ b/rust/temp/src/main.rs @@ -1,14 +0,0 @@ -use core::Server; -mod core; -mod request; -mod respond; -mod route; - -fn main() { - let server = Server::new( 8000); - server.route("/","get",|request, respond| { - respond.send(); - println!("{}",request.method()) - }); - server.start(); -} \ No newline at end of file diff --git a/rust/temp/src/request/mod.rs b/rust/temp/src/request/mod.rs deleted file mode 100644 index 5c4791a..0000000 --- a/rust/temp/src/request/mod.rs +++ /dev/null @@ -1,102 +0,0 @@ -use std::collections::HashMap; -use std::fmt::Display; -pub enum Method { - Get, - Post, - Uninitialized -} - -impl From<&str> for Method { - fn from(s: &str) -> Method { - match s{ - "GET" => Method::Get, - "POST" => Method::Post, - _ => Method::Uninitialized - } - } -} - -impl Display for Method { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let method = match self { - Method::Get => {"GET"} - Method::Post => {"POST"} - Method::Uninitialized => {"Uninitialized"} - }; - write!(f, "{}", method) - } -} - -impl PartialEq for Method { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Method::Get, Method::Get) => true, - (Method::Post, Method::Post) => true, - (Method::Uninitialized, Method::Uninitialized) => true, - _ => false, - } - } -} - -pub enum Version { - V1_1, - Uninitialized -} - -impl From<&str> for Version { - fn from(s: &str) -> Version { - match s { - "HTTP/1.1" => Version::V1_1, - _ => Version::Uninitialized - } - } -} - -pub enum Resource{ - Path(String), -} - - -pub struct Request{ - method: Method, - version: Version, - resource: Resource, - headers: HashMap, -} - -impl Request{ - pub fn build(content:String) -> Option{ - let mut content =content.lines(); - - let request_line =content.next().unwrap_or(""); - if request_line.is_empty(){return None} - let request_line:Vec<_> = request_line.split_whitespace().collect(); - if request_line.len()<3 {return None} - let method=request_line[0]; - let resource=request_line[1]; - let version=request_line[2]; - if method.is_empty()||resource.is_empty()||version.is_empty() {return None} - - let mut headers =HashMap::::new(); - for i in content { - if i.len()==0 { break;} - let parts:Vec<&str> = i.split(": ").collect(); - if parts.len() == 2 { - headers.insert(parts[0].to_string(), parts[1].to_string()); - } - }; - - Some(Request{ - method: method.into(), - version: version.into(), - resource: Resource::Path(resource.to_string()), - headers, - }) - } -} - -impl Request { - pub fn method(&self) -> &Method { - &self.method - } -} diff --git a/rust/temp/src/respond/mod.rs b/rust/temp/src/respond/mod.rs deleted file mode 100644 index 55ec2b4..0000000 --- a/rust/temp/src/respond/mod.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::Mutex; -use tokio::io::AsyncWriteExt; -use tokio::net::TcpStream; -use tokio::task; - -pub struct Respond<'a>{ - version:&'a str, - status_code:&'a str, - status_message:&'a str, - headers:HashMap<&'a str, &'a str>, - body:&'a str, - socket:Arc> -} - - -impl Respond<'_> { - pub fn build(socket: TcpStream) -> Arc> { - Arc::new(Respond { - version: "HTTP/1.1", - status_code: "200", - status_message: "OK", - headers: HashMap::new(), - body: "Hello, world!", - socket: Arc::new(Mutex::new(socket)) - }) - } - fn format_message(self:Arc) -> String { - let mut massage =String::new(); - let status_line= format!("{} {} {}\r\n", self.status_message,self.version,self.status_message); - massage.push_str(&status_line); - let mut header =String::new(); - for (key, value) in self.headers.iter() { - header += &format!("{}: {}\r\n", key, value); - } - massage.push_str("\n\n"); - massage.push_str(self.body); - massage - } - - pub fn send(self: Arc) { - let socket = self.socket.clone(); - let message = self.format_message(); - - task::spawn(async move { - let mut socket = socket.lock().await; - if let Ok(_) = socket.write_all(message.as_ref()).await { - if let Err(e) = socket.flush().await { - println!("Failed to flush: {}", e); - } - } else { - println!("Failed to send content"); - } - }); - } -} - - - diff --git a/rust/temp/src/route/mod.rs b/rust/temp/src/route/mod.rs deleted file mode 100644 index 3fbe9db..0000000 --- a/rust/temp/src/route/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -use std::sync::Arc; -use crate::request::{Method, Request}; -use crate::respond::{Respond}; - -pub(crate) struct Route { - pub path: &'static str, - pub method: Method, - pub function: Box) + Send + Sync>, -} - diff --git a/rust/wasm/life_game/src/index.js b/rust/wasm/life_game/src/index.js index cc2027e..ae255e9 100644 --- a/rust/wasm/life_game/src/index.js +++ b/rust/wasm/life_game/src/index.js @@ -32,16 +32,12 @@ const renderLoop = () => { const drawGrid = () => { ctx.beginPath(); ctx.strokeStyle = GRID_COLOR; - - // 画垂直线 for (let i = 0; i <= width; i++) { ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0); ctx.lineTo(i * (CELL_SIZE + 1) + 1, (CELL_SIZE + 1) * height + 1); } - - // 画水平线 for (let j = 0; j <= height; j++) { - ctx.moveTo(0, j * (CELL_SIZE + 1) + 1); + ctx.lineTo(0, j * (CELL_SIZE + 1) + 1); ctx.lineTo((CELL_SIZE + 1) * width + 1, j * (CELL_SIZE + 1) + 1); } diff --git a/web/chess/README.md b/web/chess/README.md new file mode 100644 index 0000000..74872fd --- /dev/null +++ b/web/chess/README.md @@ -0,0 +1,50 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default tseslint.config({ + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) +``` + +- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` +- Optionally add `...tseslint.configs.stylisticTypeChecked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: + +```js +// eslint.config.js +import react from 'eslint-plugin-react' + +export default tseslint.config({ + // Set the react version + settings: { react: { version: '18.3' } }, + plugins: { + // Add the react plugin + react, + }, + rules: { + // other rules... + // Enable its recommended rules + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + }, +}) +``` diff --git a/web/chess/eslint.config.js b/web/chess/eslint.config.js new file mode 100644 index 0000000..092408a --- /dev/null +++ b/web/chess/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/web/chess/index.html b/web/chess/index.html new file mode 100644 index 0000000..e4b78ea --- /dev/null +++ b/web/chess/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/web/chess/src/App.tsx b/web/chess/src/App.tsx index 7559f20..26dac85 100644 --- a/web/chess/src/App.tsx +++ b/web/chess/src/App.tsx @@ -189,6 +189,7 @@ const Game: React.FC = () => { useEffect(()=>{ drawChess(chessGame); + console.log(chessGame?.render()) },[]) function chessBoardClick(e: React.MouseEvent) { const client=e.currentTarget.getBoundingClientRect(); diff --git a/web/chess/tsconfig.app.json b/web/chess/tsconfig.app.json new file mode 100644 index 0000000..5a2def4 --- /dev/null +++ b/web/chess/tsconfig.app.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/web/chess/tsconfig.json b/web/chess/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/web/chess/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/web/chess/tsconfig.node.json b/web/chess/tsconfig.node.json new file mode 100644 index 0000000..9dad701 --- /dev/null +++ b/web/chess/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/web/chess/vite.config.ts b/web/chess/vite.config.ts new file mode 100644 index 0000000..0169386 --- /dev/null +++ b/web/chess/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()] +}); \ No newline at end of file diff --git a/web/cxm/README.md b/web/cxm/README.md new file mode 100644 index 0000000..74872fd --- /dev/null +++ b/web/cxm/README.md @@ -0,0 +1,50 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default tseslint.config({ + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) +``` + +- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` +- Optionally add `...tseslint.configs.stylisticTypeChecked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: + +```js +// eslint.config.js +import react from 'eslint-plugin-react' + +export default tseslint.config({ + // Set the react version + settings: { react: { version: '18.3' } }, + plugins: { + // Add the react plugin + react, + }, + rules: { + // other rules... + // Enable its recommended rules + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + }, +}) +``` diff --git a/web/cxm/eslint.config.js b/web/cxm/eslint.config.js new file mode 100644 index 0000000..092408a --- /dev/null +++ b/web/cxm/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/web/cxm/index.html b/web/cxm/index.html new file mode 100644 index 0000000..e4b78ea --- /dev/null +++ b/web/cxm/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/web/cxm/package.json b/web/cxm/package.json new file mode 100644 index 0000000..13b871d --- /dev/null +++ b/web/cxm/package.json @@ -0,0 +1,35 @@ +{ + "name": "cxm", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@studio-freight/lenis": "^1.0.33", + "gsap": "^3.12.5", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@babel/core": "^7.26.0", + "@babel/parser": "^7.26.3", + "@babel/types": "^7.26.3", + "@eslint/js": "^9.15.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "csstype": "^3.1.3", + "eslint": "^9.15.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.14", + "globals": "^15.12.0", + "typescript": "~5.6.2", + "typescript-eslint": "^8.15.0", + "vite": "^6.0.1" + } +} diff --git a/web/cxm/src/App.css b/web/cxm/src/App.css new file mode 100644 index 0000000..8214e66 --- /dev/null +++ b/web/cxm/src/App.css @@ -0,0 +1,54 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + margin: 0; + padding: 0; + height: 100%; + overflow-x: hidden; +} + +.App { + width: 100%; + height: 100%; + overflow: hidden; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/web/cxm/src/App.d.ts b/web/cxm/src/App.d.ts new file mode 100644 index 0000000..29b5ce3 --- /dev/null +++ b/web/cxm/src/App.d.ts @@ -0,0 +1,3 @@ +import './App.css'; +declare function App(): import("react/jsx-runtime").JSX.Element; +export default App; diff --git a/web/cxm/src/App.js b/web/cxm/src/App.js new file mode 100644 index 0000000..f85044b --- /dev/null +++ b/web/cxm/src/App.js @@ -0,0 +1,7 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { Parallax } from './components/Parallax'; +import './App.css'; +function App() { + return (_jsx("div", { className: "App", children: _jsx(Parallax, {}) })); +} +export default App; diff --git a/web/cxm/src/App.tsx b/web/cxm/src/App.tsx new file mode 100644 index 0000000..b5922a4 --- /dev/null +++ b/web/cxm/src/App.tsx @@ -0,0 +1,12 @@ +import { Parallax } from './components/Parallax'; +import './App.css'; + +function App() { + return ( +
+ +
+ ); +} + +export default App; diff --git a/web/cxm/src/assets/z.woff2 b/web/cxm/src/assets/z.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a04f457bb763616bfbe27c29e90de67f8cddb6d0 GIT binary patch literal 55516 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 F1^{@;VhR8N literal 0 HcmV?d00001 diff --git a/web/cxm/src/components/ImageModal.css b/web/cxm/src/components/ImageModal.css new file mode 100644 index 0000000..6bf7f9c --- /dev/null +++ b/web/cxm/src/components/ImageModal.css @@ -0,0 +1,40 @@ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.9); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + position: relative; + max-width: 90vw; + max-height: 90vh; + display: flex; + justify-content: center; + align-items: center; +} + +.modal-close { + position: absolute; + top: -2rem; + right: -2rem; + background: none; + border: none; + color: white; + font-size: 2rem; + cursor: pointer; + z-index: 1001; +} + +.modal-image { + max-width: 100%; + max-height: 90vh; + object-fit: contain; + border-radius: 8px; +} \ No newline at end of file diff --git a/web/cxm/src/components/ImageModal.d.ts b/web/cxm/src/components/ImageModal.d.ts new file mode 100644 index 0000000..2297cf8 --- /dev/null +++ b/web/cxm/src/components/ImageModal.d.ts @@ -0,0 +1,7 @@ +import './ImageModal.css'; +interface ImageModalProps { + image: string; + onClose: () => void; +} +export declare const ImageModal: ({ image, onClose }: ImageModalProps) => import("react/jsx-runtime").JSX.Element; +export {}; diff --git a/web/cxm/src/components/ImageModal.js b/web/cxm/src/components/ImageModal.js new file mode 100644 index 0000000..522b787 --- /dev/null +++ b/web/cxm/src/components/ImageModal.js @@ -0,0 +1,15 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useEffect } from 'react'; +import './ImageModal.css'; +export const ImageModal = ({ image, onClose }) => { + useEffect(() => { + const handleEsc = (e) => { + if (e.key === 'Escape') { + onClose(); + } + }; + window.addEventListener('keydown', handleEsc); + return () => window.removeEventListener('keydown', handleEsc); + }, [onClose]); + return (_jsx("div", { className: "modal-overlay", onClick: onClose, children: _jsxs("div", { className: "modal-content", onClick: e => e.stopPropagation(), children: [_jsx("button", { className: "modal-close", onClick: onClose, children: "\u00D7" }), _jsx("img", { src: image, alt: "", className: "modal-image" })] }) })); +}; diff --git a/web/cxm/src/components/ImageModal.tsx b/web/cxm/src/components/ImageModal.tsx new file mode 100644 index 0000000..39efecb --- /dev/null +++ b/web/cxm/src/components/ImageModal.tsx @@ -0,0 +1,28 @@ +import { useEffect } from 'react'; +import './ImageModal.css'; + +interface ImageModalProps { + image: string; + onClose: () => void; +} + +export const ImageModal = ({ image, onClose }: ImageModalProps) => { + useEffect(() => { + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }; + window.addEventListener('keydown', handleEsc); + return () => window.removeEventListener('keydown', handleEsc); + }, [onClose]); + + return ( +
+
e.stopPropagation()}> + + +
+
+ ); +}; \ No newline at end of file diff --git a/web/cxm/src/components/Parallax.css b/web/cxm/src/components/Parallax.css new file mode 100644 index 0000000..dfd1d4c --- /dev/null +++ b/web/cxm/src/components/Parallax.css @@ -0,0 +1,530 @@ +@font-face { + font-family: 'PP Neue Corp Wide'; + src: url('../assets/z.woff2') format('woff2'); + font-weight: 800; + font-style: normal; + font-display: swap; +} + +:root { + --color-neutral-900: #0a0a0a; + --color-light: #ffffff; + --color-black: #0a0a0a; + --container-padding: 2rem; + --section-padding: 4rem; + --color-dark-rgb: 10, 10, 10; + --bg-gradient-start: #1a1a2e; + --bg-gradient-end: #0a0a0a; +} + +body { + margin: 0; + padding: 0; + min-height: 100vh; + overflow-x: hidden; + background: linear-gradient(to bottom, var(--bg-gradient-start), var(--bg-gradient-end)); + color: var(--color-light); +} + +html { + overflow-x: hidden; + scroll-behavior: smooth; +} + +#root { + width: 100%; + height: 100%; +} + +.parallax { + width: 100%; + position: relative; + min-height: 100vh; + overflow: visible; +} + +.parallax__header { + z-index: 2; + padding: var(--section-padding) var(--container-padding); + justify-content: center; + align-items: center; + height: 100vh; + position: relative; + overflow: visible; +} + +.parallax__visuals { + object-fit: cover; + width: 100%; + max-width: none; + height: 120%; + position: absolute; + top: 0; + left: 0; +} + +.parallax__black-line-overflow { + z-index: 20; + background-color: var(--color-black); + width: 100%; + height: 2px; + position: absolute; + bottom: -1px; + left: 0; +} + +.parallax__layers { + object-fit: cover; + width: 100%; + max-width: none; + height: 100%; + position: absolute; + top: 0; + left: 0; + overflow: hidden; +} + +.parallax__layer-img { + pointer-events: none; + object-fit: cover; + width: 100%; + max-width: none; + height: 117.5%; + position: absolute; + top: -17.5%; + left: 0; +} + +.parallax__layer-title { + justify-content: center; + align-items: center; + width: 100%; + height: 100svh; + display: flex; + position: absolute; + top: 0; + left: 0; +} + +.parallax__title { + pointer-events: auto; + text-align: center; + text-transform: none; + margin-top: 0; + margin-bottom: .1em; + margin-right: .075em; + font-family: 'PP Neue Corp Wide', sans-serif; + font-size: 11vw; + font-weight: 800; + line-height: 1; + position: relative; +} + +.parallax__fade { + z-index: 30; + background: linear-gradient(to top, + rgba(10, 10, 10, 1) 0%, + rgba(10, 10, 10, 0.95) 15%, + rgba(10, 10, 10, 0.85) 30%, + rgba(10, 10, 10, 0.75) 45%, + rgba(10, 10, 10, 0.6) 60%, + rgba(10, 10, 10, 0.4) 75%, + rgba(10, 10, 10, 0.2) 85%, + rgba(10, 10, 10, 0.1) 92%, + transparent 100% + ); + width: 100%; + height: 30%; + position: absolute; + bottom: 0; + left: 0; +} + +.parallax__content { + padding: var(--section-padding) var(--container-padding); + justify-content: center; + align-items: center; + min-height: 100vh; + display: flex; + position: relative; + z-index: 2; + background: linear-gradient(to bottom, + rgba(26, 42, 46, 0.95) 0%, + rgba(10, 10, 10, 0.98) 85%, + var(--color-black) 100% + ); +} + +.osmo-icon-svg { + width: 8em; + position: relative; + color: var(--color-light); +} + +.travel-grid { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + gap: 2rem; +} + +.travel-item { + padding: 2rem; + background: linear-gradient( + 145deg, + rgba(255, 255, 255, 0.07) 0%, + rgba(255, 255, 255, 0.02) 100% + ); + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.07); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + transition: all 0.3s ease; + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.15); + cursor: pointer; + position: relative; +} + +.travel-item:hover { + transform: translateY(-5px); + background: linear-gradient( + 145deg, + rgba(255, 255, 255, 0.1) 0%, + rgba(255, 255, 255, 0.04) 100% + ); + border-color: rgba(255, 255, 255, 0.12); + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.25); +} + +.travel-item::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 12px; + background: linear-gradient( + 45deg, + transparent 0%, + rgba(255, 255, 255, 0.03) 50%, + transparent 100% + ); + pointer-events: none; +} + +.travel-item h3 { + font-size: 1.5rem; + margin-bottom: 1rem; + color: var(--color-light); + font-family: 'PP Neue Corp Wide', sans-serif; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.travel-item p { + color: var(--color-light); + line-height: 1.8; + opacity: 0.9; + font-size: 0.95rem; + letter-spacing: 0.02em; +} + +.travel-item::after { + content: '点击查看图片'; + position: absolute; + bottom: 1rem; + right: 1rem; + font-size: 0.8rem; + opacity: 0; + transition: opacity 0.3s ease; +} + +.travel-item:hover::after { + opacity: 0.7; +} + +@media (max-width: 768px) { + .travel-intro h2 { + font-size: 2rem; + } + + .travel-grid { + grid-template-columns: 1fr; + } +} + +.container { + position: relative; + width: 100%; + height: 100vh; + overflow: hidden; + background: linear-gradient(to bottom, + rgba(10, 10, 10, 0.98) 0%, + rgba(26, 42, 46, 0.95) 100% + ); +} + +.slider { + position: absolute; + top: 5vh; + width: 100vw; + height: 100vh; + overflow: hidden; + perspective: 300px; + perspective-origin: 50% 50%; +} + +.card { + position: absolute; + top: 35%; + left: 50%; + width: 50%; + height: 400px; + border-radius: 10px; + overflow: hidden; + transform: translate3d(-50%, -50%, 0); + background-color: #000; +} + +.card img { + position: absolute; + width: 100%; + height: 100%; + object-fit: cover; + opacity: 0.75; + top: 0; + left: 0; +} + +.copy { + display: none; +} + +h1 { + display: none; +} + +h1 span { + display: none; +} + +@media(max-width: 768px) { + .card { + width: 90%; + height: 300px; + } +} + +.slider-section { + position: relative; + width: 100%; + height: 100vh; + overflow: hidden; + background: linear-gradient(to bottom, + var(--color-black) 0%, + rgba(26, 42, 46, 0.95) 15%, + rgba(10, 10, 10, 0.98) 100% + ); + margin-top: -2px; +} + +.slider-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-image: + linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px); + background-size: 50px 50px; + opacity: 0.5; + animation: gridMove 30s linear infinite; + pointer-events: none; +} + +.circle-decoration { + position: absolute; + width: 300px; + height: 300px; + border: 1px solid #ffffff; + border-radius: 50%; + pointer-events: none; +} + +.circle-left { + left: -150px; + top: 20%; +} + +.circle-right { + right: -150px; + bottom: 20%; +} + +.number-decoration { + position: absolute; + font-size: 120px; + font-weight: 800; + color: #ffffff; + font-family: 'PP Neue Corp Wide', sans-serif; +} + +.number-top { + top: 5%; + right: 10%; +} + +.number-bottom { + bottom: 5%; + left: 10%; +} + +.side-text { + position: absolute; + top: 50%; + transform: translateY(-50%); + font-size: 1.2rem; + color: rgba(255, 255, 255, 0.85) !important; + writing-mode: vertical-rl; + text-orientation: mixed; + letter-spacing: 0.3em; + font-weight: 500; + transition: all 0.3s ease; + text-shadow: none; + z-index: 10; + mix-blend-mode: normal; +} + +.side-text:hover { + text-shadow: none; + transform: translateY(-50%) scale(1.05); +} + +.side-text-left { + left: 3rem; +} + +.side-text-right { + right: 2rem; + writing-mode: vertical-lr; +} + +.side-text::before { + content: ''; + position: absolute; + width: 1px; + height: 150px; + background: linear-gradient( + to bottom, + transparent, + rgba(255, 255, 255, 0.5), + transparent + ); + left: 50%; + transform: translateX(-50%); + top: -150px; +} + +.side-text::after { + content: ''; + position: absolute; + width: 1px; + height: 150px; + background: linear-gradient( + to bottom, + transparent, + rgba(255, 255, 255, 0.5), + transparent + ); + left: 50%; + transform: translateX(-50%); + bottom: -150px; +} + +.year-display { + position: absolute; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + font-size: 1.1rem; + color: rgba(255, 255, 255, 0.85) !important; + letter-spacing: 0.5em; + font-weight: 500; + text-shadow: none; + z-index: 10; + mix-blend-mode: normal; +} + +.year-display:hover { + text-shadow: none; +} + +@media (max-width: 768px) { + .side-text { + display: none; + } +} + +@keyframes gridMove { + 0% { + transform: translateY(0); + } + 100% { + transform: translateY(50px); + } +} + +.card:hover { + transform: translate(-50%, -50%); +} + +/* 添加旅行卡片的悬停效果 */ +.travel-item { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.travel-item:hover { + transform: translateY(-10px); + background: rgba(255, 255, 255, 0.1); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.travel-item h3 { + transition: color 0.3s ease; +} + +.travel-item:hover h3 { + color: #fff; +} + +.travel-item p { + transition: opacity 0.3s ease; +} + +.travel-item:hover p { + opacity: 0.9; +} + +.parallax__content::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 150px; + background: linear-gradient( + to bottom, + transparent 0%, + var(--color-black) 100% + ); + pointer-events: none; +} + \ No newline at end of file diff --git a/web/cxm/src/components/Parallax.d.ts b/web/cxm/src/components/Parallax.d.ts new file mode 100644 index 0000000..6393aa6 --- /dev/null +++ b/web/cxm/src/components/Parallax.d.ts @@ -0,0 +1,2 @@ +import './Parallax.css'; +export declare const Parallax: () => import("react/jsx-runtime").JSX.Element; diff --git a/web/cxm/src/components/Parallax.js b/web/cxm/src/components/Parallax.js new file mode 100644 index 0000000..86714be --- /dev/null +++ b/web/cxm/src/components/Parallax.js @@ -0,0 +1,237 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { useEffect, useRef, useState } from 'react'; +import gsap from 'gsap'; +import { CustomEase } from 'gsap/CustomEase'; +import ScrollTrigger from 'gsap/ScrollTrigger'; +import Lenis from '@studio-freight/lenis'; +import './Parallax.css'; +import layer1 from '../assets/b1.png'; +import layer2 from '../assets/b2.webp'; +import layer3 from '../assets/b3.webp'; +import { ImageModal } from './ImageModal'; +// 导入旅行图片 +import changbaishan from '../assets/a/changbaishan.jpg'; +import chongqing from '../assets/a/chongqing.jpg'; +import haerbing from '../assets/a/haerbing.jpg'; +import qindao from '../assets/a/qindao.jpg'; +import qinghuangdao from '../assets/a/qinghuangdao.jpg'; +import shichuan from '../assets/a/shichuan.jpg'; +import tianjin from '../assets/a/tianjin.jpg'; +import xizhang from '../assets/a/xizhang.jpg'; +import taian from '../assets/a/taian.jpg'; +// 导入滑动卡片图片 +import a1 from '../assets/b/a1.jpg'; +import a2 from '../assets/b/a2.jpg'; +import a3 from '../assets/b/a3.jpg'; +import a4 from '../assets/b/a4.jpg'; +import a5 from '../assets/b/a5.jpg'; +import a6 from '../assets/b/a6.jpg'; +import a7 from '../assets/b/a7.jpg'; +import a8 from '../assets/b/a8.jpg'; +import a9 from '../assets/b/a9.jpg'; +import a10 from '../assets/b/a10.jpg'; +import a11 from '../assets/b/a11.jpg'; +import a12 from '../assets/b/a12.jpg'; +import a13 from '../assets/b/a13.jpg'; +import a14 from '../assets/b/a14.jpg'; +import a15 from '../assets/b/a15.jpg'; +import a16 from '../assets/b/a16.jpg'; +import a17 from '../assets/b/a17.jpg'; +import a18 from '../assets/b/a18.jpg'; +import a19 from '../assets/b/a19.jpg'; +import a20 from '../assets/b/a20.jpg'; +import a21 from '../assets/b/a21.jpg'; +gsap.registerPlugin(CustomEase, ScrollTrigger); +// 添加 CustomEase +CustomEase.create("cubic", "0.83, 0, 0.17, 1"); +// 在组件外部创建图片数组 +const allImages = [ + a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, + a11, a12, a13, a14, a15, a16, a17, a18, + a19, a20, a21 +]; +// 在组件外部添加一个用于跟踪最近使用的图片的数组 +let recentlyUsedImages = []; +// 修改节流函数,使用闭包来保存状态 +function throttle(func, limit) { + let inThrottle; + let lastResult; + return function (...args) { + if (!inThrottle) { + lastResult = func.apply(this, args); + inThrottle = true; + setTimeout(() => { + inThrottle = false; + }, limit); + } + return lastResult; + }; +} +// 在组件内部,但在 useEffect 外部定义 +function initializeCards() { + const cards = Array.from(document.querySelectorAll(".card")); + gsap.set(cards, { + y: i => -15 + (15 * i) + "%", + z: i => 15 * i, + transformOrigin: "50% 50%" + }); +} +export const Parallax = () => { + const layersRef = useRef(null); + const [selectedImage, setSelectedImage] = useState(null); + const [displayedCards, setDisplayedCards] = useState(() => { + const shuffled = [...allImages].sort(() => 0.5 - Math.random()); + const initial = shuffled.slice(0, 5); + return initial; + }); + useEffect(() => { + const lenis = new Lenis({ + duration: 1.2, + orientation: 'vertical', + gestureOrientation: 'vertical', + smoothWheel: true, + }); + let rafId; + let isAnimating = false; + let autoPlayInterval; + function raf(time) { + lenis.raf(time); + rafId = requestAnimationFrame(raf); + } + rafId = requestAnimationFrame(raf); + const scrollHandler = () => { + ScrollTrigger.update(); + }; + lenis.on('scroll', scrollHandler); + if (layersRef.current) { + const tl = gsap.timeline({ + scrollTrigger: { + trigger: layersRef.current, + start: "top top", + end: "bottom top", + scrub: 1, + invalidateOnRefresh: true, + } + }); + const layers = [ + { layer: "1", yPercent: 50 }, + { layer: "2", yPercent: 35 }, + { layer: "3", yPercent: 25 }, + { layer: "4", yPercent: 15 } + ]; + layers.forEach((layerObj) => { + const element = layersRef.current.querySelector(`[data-parallax-layer="${layerObj.layer}"]`); + if (element) { + gsap.set(element, { + y: 0, + force3D: true + }); + tl.to(element, { + yPercent: layerObj.yPercent, + ease: "none", + force3D: true, + }, 0); + } + }); + } + // 修改 handleClick 函数 + const handleClick = () => { + if (isAnimating) + return; + isAnimating = true; + const slider = document.querySelector(".slider"); + if (!slider) + return; + const cards = Array.from(slider.querySelectorAll(".card")); + const lastCard = cards[cards.length - 1]; + if (lastCard) { + const currentDisplayed = new Set(displayedCards); + const usedImages = new Set(recentlyUsedImages); + const availableImages = allImages.filter(img => !currentDisplayed.has(img) && !usedImages.has(img)); + let newImage; + if (availableImages.length < 5) { + recentlyUsedImages = []; + const newAvailableImages = allImages.filter(img => !currentDisplayed.has(img)); + const randomIndex = Math.floor(Math.random() * newAvailableImages.length); + newImage = newAvailableImages[randomIndex]; + } + else { + const randomIndex = Math.floor(Math.random() * availableImages.length); + newImage = availableImages[randomIndex]; + } + recentlyUsedImages.push(newImage); + // 更新状态,新图片添加到开头 + const newState = [newImage, ...displayedCards.slice(0, -1)]; + setDisplayedCards(newState); + // 先创建新的卡片元素 + const newCard = lastCard.cloneNode(true); + const newImg = newCard.querySelector('img'); + if (newImg) { + newImg.src = newImage; + } + // 执行动画 + gsap.to(lastCard, { + y: "+=150%", + duration: 0.75, + ease: "cubic", + onComplete: () => { + // 用新卡片替换旧卡片 + slider.replaceChild(newCard, lastCard); + slider.prepend(newCard); + gsap.set(cards, { + clearProps: "all" + }); + initializeCards(); + isAnimating = false; + } + }); + } + }; + // 动播放函数 + const autoPlay = () => { + if (!isAnimating) { + handleClick(); + } + }; + // 节流后的点击处理函数 + const throttledHandleClick = throttle(handleClick, 1000); + // 开始自动播放 + autoPlayInterval = setInterval(autoPlay, 2000); + // 鼠标进入时暂停自动播放 + const handleMouseEnter = () => { + clearInterval(autoPlayInterval); + }; + // 鼠标离开时恢复自动播放 + const handleMouseLeave = () => { + autoPlayInterval = setInterval(autoPlay, 2000); + }; + // 初始化卡片位置 + initializeCards(); + const slider = document.querySelector(".slider"); + if (slider) { + slider.addEventListener("click", throttledHandleClick); + slider.addEventListener("mouseenter", handleMouseEnter); + slider.addEventListener("mouseleave", handleMouseLeave); + } + return () => { + if (rafId) { + cancelAnimationFrame(rafId); + } + const slider = document.querySelector(".slider"); + if (slider) { + slider.removeEventListener("click", throttledHandleClick); + slider.removeEventListener("mouseenter", handleMouseEnter); + slider.removeEventListener("mouseleave", handleMouseLeave); + } + clearInterval(autoPlayInterval); + lenis.destroy(); + ScrollTrigger.getAll().forEach(trigger => trigger.kill()); + }; + }, []); + // 3. 添加始化函数 + useEffect(() => { + // 只保留卡片初始化 + initializeCards(); + }, []); + return (_jsxs("div", { className: "parallax", children: [_jsx("section", { className: "parallax__header", children: _jsxs("div", { className: "parallax__visuals", children: [_jsx("div", { className: "parallax__black-line-overflow" }), _jsxs("div", { ref: layersRef, "data-parallax-layers": true, className: "parallax__layers", children: [_jsx("img", { src: layer3, loading: "eager", width: "800", "data-parallax-layer": "1", alt: "", className: "parallax__layer-img" }), _jsx("img", { src: layer2, loading: "eager", width: "800", "data-parallax-layer": "2", alt: "", className: "parallax__layer-img" }), _jsx("div", { "data-parallax-layer": "3", className: "parallax__layer-title", children: _jsx("h2", { className: "parallax__title", children: "\u65C5\u884C\u6545\u4E8B" }) }), _jsx("img", { src: layer1, loading: "eager", width: "800", "data-parallax-layer": "4", alt: "", className: "parallax__layer-img" })] }), _jsx("div", { className: "parallax__fade" })] }) }), _jsxs("section", { className: "parallax__content", children: [_jsxs("div", { className: "travel-grid", children: [_jsxs("div", { className: "travel-item", onClick: () => setSelectedImage(xizhang), children: [_jsx("h3", { children: "\u897F\u85CF" }), _jsx("p", { children: "\u5728\u6D77\u62D44000\u7C73\u7684\u9AD8\u539F\u4E0A\uFF0C\u611F\u53D7\u7740\u6700\u63A5\u8FD1\u5929\u7A7A\u7684\u4FE1\u4EF0\u3002\u5E03\u8FBE\u62C9\u5BAB\u7684\u5E84\u4E25\u8083\u7A46\uFF0C\u5927\u662D\u5BFA\u7684\u8654\u8BDA\u9999\u706B\uFF0C\u4EE5\u53CA\u7EB3\u6728\u9519\u5723\u6E56\u7684\u78A7\u84DD\u6E56\u6C34\uFF0C\u90FD\u8BA9\u4EBA\u5185\u5FC3\u65E0\u6BD4\u5E73\u9759\u3002\u9AD8\u539F\u7684\u661F\u7A7A\u7480\u74A8\u5F97\u8BA9\u4EBA\u5C4F\u606F\u3002" })] }), _jsxs("div", { className: "travel-item", onClick: () => setSelectedImage(shichuan), children: [_jsx("h3", { children: "\u56DB\u5DDD" }), _jsx("p", { children: "\u5728\u6210\u90FD\u7684\u5BBD\u7A84\u5DF7\u5B50\uFF0C\u611F\u53D7\u60A0\u95F2\u7684\u56DB\u5DDD\u6587\u5316\u3002\u7A3B\u57CE\u4E01\u7684\u96EA\u5C71\u3001\u8349\u4E0E\u6E56\u6CCA\u6784\u6210\u4E86\u4EBA\u95F4\u6700\u540E\u7684\u9999\u683C\u91CC\u62C9\u3002\u4E50\u5C71\u5927\u4F5B\u5DCD\u5CE8\u5E84\u4E25\uFF0C\u90FD\u6C5F\u5830\u5343\u5E74\u667A\u6167\uFF0C\u8BA9\u4EBA\u4E0D\u7981\u611F\u53F9\u53E4\u4EBA\u7684\u5320\u5FC3\u3002" })] }), _jsxs("div", { className: "travel-item", onClick: () => setSelectedImage(chongqing), children: [_jsx("h3", { children: "\u91CD\u5E86" }), _jsx("p", { children: "\u591C\u5E55\u964D\u4E34\uFF0C\u8FD9\u5EA7\u4E0D\u591C\u57CE\u5F00\u59CB\u7EFD\u653E\u5149\u3002\u6D2A\u5D16\u6D1E\u5C42\u5C42\u53E0\u53E0\u7684\u706F\u5149\u5012\u6620\u5728\u6C5F\u9762\uFF0C\u4E24\u6C5F\u4EA4\u6C47\u5904\u7684\u7480\u74A8\u591C\u666F\u4EE4\u4EBA\u6C89\u9189\u3002\u5B50\u575D\u8F7B\u8F68\u7A7F\u697C\u800C\u8FC7\uFF0C\u89E3\u653E\u7891\u7684\u7E41\u534E\u591C\u8272\uFF0C\u6784\u6210\u4E86\u8FD9\u5EA7\u7ACB\u4F53\u57CE\u5E02\u6700\u8FF7\u4EBA\u7684\u753B\u5377\u3002" })] }), _jsxs("div", { className: "travel-item", onClick: () => setSelectedImage(haerbing), children: [_jsx("h3", { children: "\u54C8\u5C14\u6EE8" }), _jsx("p", { children: "\u51B0\u96EA\u4E16\u754C\u7684\u6676\u83B9\u5254\u900F\uFF0C\u4E2D\u592E\u5927\u8857\u7684\u6B27\u5F0F\u5EFA\u7B51\uFF0C\u677E\u82B1\u6C5F\u7684\u51B0\u96EA\u5947\u7F18\u3002\u5728\u96F6\u4E0B20\u5EA6\u7684\u5BD2\u51AC\u611F\u53D7\u7740\u8FD9\u5EA7\u57CE\u5E02\u72EC\u7279\u7684\u4FC4\u7F57\u65AF\u98CE\u60C5\u548C\u51B0\u96EA\u827A\u672F\u3002" })] }), _jsxs("div", { className: "travel-item", onClick: () => setSelectedImage(changbaishan), children: [_jsx("h3", { children: "\u957F\u767D\u5C71" }), _jsx("p", { children: "\u5929\u6C60\u7684\u6DF1\u9083\u795E\u79D8\uFF0C\u7011\u5E03\u7684\u6C14\u52BF\u78C5\u7934\uFF0C\u6E29\u6CC9\u7684\u6E29\u6696\u6CBB\u6108\u3002\u5728\u8FD9\u5EA7\u706B\u5C71\u4E0E\u51B0\u96EA\u7684\u5929\u5802\uFF0C\u611F\u53D7\u5927\u81EA\u7136\u7684\u9B3C\u65A7\u795E\u5DE5\u4F53\u9A8C\u4E1C\u5317\u539F\u59CB\u68EE\u6797\u7684\u795E\u79D8\u3002" })] }), _jsxs("div", { className: "travel-item", onClick: () => setSelectedImage(qindao), children: [_jsx("h3", { children: "\u9752\u5C9B" }), _jsx("p", { children: "\u7684\u6D77\u8F7B\u629A\uFF0C\u516B\u5173\u7684\u5EFA\uFF0C\u9152\u535A\u7269\u9986\u91D1\u8272\u56DE\u3002\u6F2B\u6B65\u5728\u6D77\u8FB9\uFF0C\u54C1\u5C1D\u7740\u65B0\u9C9C\u7684\u6D77\u9C9C\uFF0C\u6B23\u8D4F\u7740\u8FD9\u5EA7\u5145\u6EE1\u5FB7\u56FD\u98CE\u60C5\u7684\u6D77\u6EE8\u57CE\u5E02\u3002" })] }), _jsxs("div", { className: "travel-item", onClick: () => setSelectedImage(qinghuangdao), children: [_jsx("h3", { children: "\u79E6\u7687\u5C9B" }), _jsx("p", { children: "\u5728\u4E07\u91CC\u957F\u7684\u8D77\u70B9\u5317\u6234\u6CB3\uFF0C\u542C\u7740\u6E24\u6D77\u6E7E\u7684\u6D6A\u6D9B\u3002\u6F2B\u6B65\u5728\u767D\u8272\u7684\u6C99\u6EE9\u4E0A\uFF0C\u65E5\u51FA\u4E1C\u65B9\uFF0C\u971E\u6EE1\u3002\u8FD9\u91CC\u6709\u6700\u7F8E\u7684\u6D77\u5CB8\u7EBF\uFF0C\u4E5F\u6709\u6700\u52A8\u4EBA\u7684\u5386\u53F2\u6545\u4E8B\u3002" })] }), _jsxs("div", { className: "travel-item", onClick: () => setSelectedImage(tianjin), children: [_jsx("h3", { children: "\u5929\u6D25" }), _jsx("p", { children: "\u4E94\u5927\u6D0B\u697C\u7FA4\u8BC9\u7740\u767E\u5E74\u5386\u53F2\uFF0C\u610F\u5F0F\u98CE\u60C5\u533A\u7684\u5730\u57DF\u60C5\u8C03\u3002\u53E4\u6587\u5316\u8857\u7684\u6D25\u5473\u5C0F\u5403\uFF0C\u6D77\u6CB3\u4E24\u5CB8\u7684\u74A8\u591C\u666F\u3002\u5728\u8FD9\u5EA7\u4E2D\u897F\u5408\u74A7\u7684\u57CE\u5E02\u91CC\uFF0C\u611F\u53D7\u7740\u72EC\u7279\u7684\u6D77\u6D3E\u6587\u5316\u3002" })] }), _jsxs("div", { className: "travel-item", onClick: () => setSelectedImage(taian), children: [_jsx("h3", { children: "\u6CF0\u5B89" }), _jsx("p", { children: "\u6CF0\u5C71\u5DCD\u5CE8\u96C4\u4F1F\uFF0C\u4E91\u6D77\u65E5\u51FA\u58EE\u89C2\u65E0\u6BD4\u3002\u4E0A\u5357\u5929\u95E8\uFF0C\u4FEF\u77B0\u4F17\u6E3A\u5C0F\u3002\u5CB1\u5E99\u7684\u53E4\u8001\u5EFA\uFF0C\u8BC9\u7740\u5343\u5386\u53F2\u3002\u8FD9\u91CC\u4E0D\u4EC5\u662F\u4E94\u5CB3\u4E4B\u9996\uFF0C\u66F4\u662F\u4E2D\u534E\u6587\u5316\u7684\u7CBE\u795E\u8C61\u5F81\u3002" })] })] }), selectedImage && (_jsx(ImageModal, { image: selectedImage, onClose: () => setSelectedImage(null) }))] }), _jsxs("section", { className: "slider-section", children: [_jsx("div", { className: "circle-decoration circle-left" }), _jsx("div", { className: "circle-decoration circle-right" }), _jsx("div", { className: "number-decoration number-top", children: "01" }), _jsx("div", { className: "number-decoration number-bottom", children: "21" }), _jsx("div", { className: "side-text side-text-left", children: "TRAVEL MEMORIES \u00B7 \u65C5\u884C\u8BB0\u5FC6" }), _jsx("div", { className: "side-text side-text-right", children: "PHOTO COLLECTION \u00B7 \u5F71\u50CF\u96C6" }), _jsx("div", { className: "year-display", children: "2022 - 2024" }), _jsx("div", { className: "container", children: _jsx("div", { className: "slider", children: displayedCards.map((img, index) => (_jsx("div", { className: "card", children: _jsx("img", { src: img, alt: "" }) }, index))) }) })] })] })); +}; diff --git a/web/cxm/src/components/Parallax.tsx b/web/cxm/src/components/Parallax.tsx new file mode 100644 index 0000000..9130b14 --- /dev/null +++ b/web/cxm/src/components/Parallax.tsx @@ -0,0 +1,394 @@ +import { useEffect, useRef, useState } from 'react'; +import gsap from 'gsap'; +import { CustomEase } from 'gsap/CustomEase'; +import ScrollTrigger from 'gsap/ScrollTrigger'; +import Lenis from '@studio-freight/lenis'; +import './Parallax.css'; +import layer1 from '../assets/b1.png'; +import layer2 from '../assets/b2.webp'; +import layer3 from '../assets/b3.webp'; + +import { ImageModal } from './ImageModal'; + +// 导入旅行图片 +import changbaishan from '../assets/a/changbaishan.jpg'; +import chongqing from '../assets/a/chongqing.jpg'; +import haerbing from '../assets/a/haerbing.jpg'; +import qindao from '../assets/a/qindao.jpg'; +import qinghuangdao from '../assets/a/qinghuangdao.jpg'; +import shichuan from '../assets/a/shichuan.jpg'; +import tianjin from '../assets/a/tianjin.jpg'; +import xizhang from '../assets/a/xizhang.jpg'; +import taian from '../assets/a/taian.jpg'; + +// 导入滑动卡片图片 +import a1 from '../assets/b/a1.jpg'; +import a2 from '../assets/b/a2.jpg'; +import a3 from '../assets/b/a3.jpg'; +import a4 from '../assets/b/a4.jpg'; +import a5 from '../assets/b/a5.jpg'; +import a6 from '../assets/b/a6.jpg'; +import a7 from '../assets/b/a7.jpg'; +import a8 from '../assets/b/a8.jpg'; +import a9 from '../assets/b/a9.jpg'; +import a10 from '../assets/b/a10.jpg'; +import a11 from '../assets/b/a11.jpg'; +import a12 from '../assets/b/a12.jpg'; +import a13 from '../assets/b/a13.jpg'; +import a14 from '../assets/b/a14.jpg'; +import a15 from '../assets/b/a15.jpg'; +import a16 from '../assets/b/a16.jpg'; +import a17 from '../assets/b/a17.jpg'; +import a18 from '../assets/b/a18.jpg'; +import a19 from '../assets/b/a19.jpg'; +import a20 from '../assets/b/a20.jpg'; +import a21 from '../assets/b/a21.jpg'; + +gsap.registerPlugin(CustomEase, ScrollTrigger); + +// 添加 CustomEase +CustomEase.create("cubic", "0.83, 0, 0.17, 1"); + +// 在组件外部创建图片数组 +const allImages = [ + a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, + a11, a12, a13, a14, a15, a16, a17, a18, + a19, a20, a21 +]; + +// 在组件外部添加一个用于跟踪最近使用的图片的数组 +let recentlyUsedImages: typeof allImages[number][] = []; + +// 修改节流函数,使用闭包来保存状态 +function throttle(func: Function, limit: number) { + let inThrottle: boolean; + let lastResult: any; + return function(this: any, ...args: any[]) { + if (!inThrottle) { + lastResult = func.apply(this, args); + inThrottle = true; + setTimeout(() => { + inThrottle = false; + }, limit); + } + return lastResult; + } +} + +// 在组件内部,但在 useEffect 外部定义 +function initializeCards() { + const cards = Array.from(document.querySelectorAll(".card")); + gsap.set(cards, { + y: i => -15 + (15 * i) + "%", + z: i => 15 * i, + transformOrigin: "50% 50%" + }); +} + +export const Parallax = () => { + const layersRef = useRef(null); + const [selectedImage, setSelectedImage] = useState(null); + const [displayedCards, setDisplayedCards] = useState(() => { + const shuffled = [...allImages].sort(() => 0.5 - Math.random()); + const initial = shuffled.slice(0, 5); + return initial; + }); + + + + useEffect(() => { + const lenis = new Lenis({ + duration: 1.2, + orientation: 'vertical', + gestureOrientation: 'vertical', + smoothWheel: true, + }); + + let rafId: number; + let isAnimating = false; + let autoPlayInterval: ReturnType; + + function raf(time: number) { + lenis.raf(time); + rafId = requestAnimationFrame(raf); + } + rafId = requestAnimationFrame(raf); + + const scrollHandler = () => { + ScrollTrigger.update(); + }; + + lenis.on('scroll', scrollHandler); + + if (layersRef.current) { + const tl = gsap.timeline({ + scrollTrigger: { + trigger: layersRef.current, + start: "top top", + end: "bottom top", + scrub: 1, + invalidateOnRefresh: true, + } + }); + + const layers = [ + { layer: "1", yPercent: 50 }, + { layer: "2", yPercent: 35 }, + { layer: "3", yPercent: 25 }, + { layer: "4", yPercent: 15 } + ]; + + layers.forEach((layerObj) => { + const element = layersRef.current!.querySelector(`[data-parallax-layer="${layerObj.layer}"]`); + if (element) { + gsap.set(element, { + y: 0, + force3D: true + }); + + tl.to(element, { + yPercent: layerObj.yPercent, + ease: "none", + force3D: true, + }, 0); + } + }); + } + + // 修改 handleClick 函数 + const handleClick = () => { + if (isAnimating) return; + isAnimating = true; + + const slider = document.querySelector(".slider"); + if (!slider) return; + + const cards = Array.from(slider.querySelectorAll(".card")); + const lastCard = cards[cards.length - 1]; + + if (lastCard) { + const currentDisplayed = new Set(displayedCards); + const usedImages = new Set(recentlyUsedImages); + const availableImages = allImages.filter(img => + !currentDisplayed.has(img) && !usedImages.has(img) + ); + + let newImage: string; + + if (availableImages.length < 5) { + recentlyUsedImages = []; + const newAvailableImages = allImages.filter(img => !currentDisplayed.has(img)); + const randomIndex = Math.floor(Math.random() * newAvailableImages.length); + newImage = newAvailableImages[randomIndex]; + } else { + const randomIndex = Math.floor(Math.random() * availableImages.length); + newImage = availableImages[randomIndex]; + } + + recentlyUsedImages.push(newImage); + + // 更新状态,新图片添加到开头 + const newState = [newImage, ...displayedCards.slice(0, -1)]; + setDisplayedCards(newState); + + // 先创建新的卡片元素 + const newCard = lastCard.cloneNode(true) as HTMLElement; + const newImg = newCard.querySelector('img'); + if (newImg) { + newImg.src = newImage; + } + + // 执行动画 + gsap.to(lastCard, { + y: "+=150%", + duration: 0.75, + ease: "cubic", + onComplete: () => { + // 用新卡片替换旧卡片 + slider.replaceChild(newCard, lastCard); + slider.prepend(newCard); + + gsap.set(cards, { + clearProps: "all" + }); + initializeCards(); + isAnimating = false; + } + }); + } + }; + + // 动播放函数 + const autoPlay = () => { + if (!isAnimating) { + handleClick(); + } + }; + + // 节流后的点击处理函数 + const throttledHandleClick = throttle(handleClick, 1000); + + // 开始自动播放 + autoPlayInterval = setInterval(autoPlay, 2000); + + // 鼠标进入时暂停自动播放 + const handleMouseEnter = () => { + clearInterval(autoPlayInterval); + }; + + // 鼠标离开时恢复自动播放 + const handleMouseLeave = () => { + autoPlayInterval = setInterval(autoPlay, 2000); + }; + + // 初始化卡片位置 + initializeCards(); + + const slider = document.querySelector(".slider"); + if (slider) { + slider.addEventListener("click", throttledHandleClick); + slider.addEventListener("mouseenter", handleMouseEnter); + slider.addEventListener("mouseleave", handleMouseLeave); + } + + return () => { + if (rafId) { + cancelAnimationFrame(rafId); + } + const slider = document.querySelector(".slider"); + if (slider) { + slider.removeEventListener("click", throttledHandleClick); + slider.removeEventListener("mouseenter", handleMouseEnter); + slider.removeEventListener("mouseleave", handleMouseLeave); + } + clearInterval(autoPlayInterval); + lenis.destroy(); + ScrollTrigger.getAll().forEach(trigger => trigger.kill()); + }; + }, []); + + // 3. 添加始化函数 + useEffect(() => { + // 只保留卡片初始化 + initializeCards(); + }, []); + + return ( +
+
+
+
+
+ + +
+

旅行故事

+
+ +
+
+
+
+
+
+
setSelectedImage(xizhang)}> +

西藏

+

在海拔4000米的高原上,感受着最接近天空的信仰。布达拉宫的庄严肃穆,大昭寺的虔诚香火,以及纳木错圣湖的碧蓝湖水,都让人内心无比平静。高原的星空璀璨得让人屏息。

+
+ +
setSelectedImage(shichuan)}> +

四川

+

在成都的宽窄巷子,感受悠闲的四川文化。稻城丁的雪山、草与湖泊构成了人间最后的香格里拉。乐山大佛巍峨庄严,都江堰千年智慧,让人不禁感叹古人的匠心。

+
+ +
setSelectedImage(chongqing)}> +

重庆

+

夜幕降临,这座不夜城开始绽放光。洪崖洞层层叠叠的灯光倒映在江面,两江交汇处的璀璨夜景令人沉醉。子坝轻轨穿楼而过,解放碑的繁华夜色,构成了这座立体城市最迷人的画卷。

+
+ +
setSelectedImage(haerbing)}> +

哈尔滨

+

冰雪世界的晶莹剔透,中央大街的欧式建筑,松花江的冰雪奇缘。在零下20度的寒冬感受着这座城市独特的俄罗斯风情和冰雪艺术。

+
+ +
setSelectedImage(changbaishan)}> +

长白山

+

天池的深邃神秘,瀑布的气势磅礴,温泉的温暖治愈。在这座火山与冰雪的天堂,感受大自然的鬼斧神工体验东北原始森林的神秘。

+
+ +
setSelectedImage(qindao)}> +

青岛

+

的海轻抚,八关的建,酒博物馆金色回。漫步在海边,品尝着新鲜的海鲜,欣赏着这座充满德国风情的海滨城市。

+
+ +
setSelectedImage(qinghuangdao)}> +

秦皇岛

+

在万里长的起点北戴河,听着渤海湾的浪涛。漫步在白色的沙滩上,日出东方,霞满。这里有最美的海岸线,也有最动人的历史故事。

+
+ +
setSelectedImage(tianjin)}> +

天津

+

五大洋楼群诉着百年历史,意式风情区的地域情调。古文化街的津味小吃,海河两岸的璨夜景。在这座中西合璧的城市里,感受着独特的海派文化。

+
+ +
setSelectedImage(taian)}> +

泰安

+

泰山巍峨雄伟,云海日出壮观无比。上南天门,俯瞰众渺小。岱庙的古老建,诉着千历史。这里不仅是五岳之首,更是中华文化的精神象征。

+
+
+ {selectedImage && ( + setSelectedImage(null)} + /> + )} +
+ +
+
+
+
01
+
21
+
+ TRAVEL MEMORIES · 旅行记忆 +
+
+ PHOTO COLLECTION · 影像集 +
+
+ 2022 - 2024 +
+
+
+ {displayedCards.map((img, index) => ( +
+ +
+ ))} +
+
+
+
+ ); +}; \ No newline at end of file diff --git a/web/cxm/src/index.css b/web/cxm/src/index.css new file mode 100644 index 0000000..8214735 --- /dev/null +++ b/web/cxm/src/index.css @@ -0,0 +1,98 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + overflow: hidden; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} + +#root { + width: 100%; + height: 100%; + overflow: hidden; +} + +html { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + overflow: hidden; +} + +html.lenis { + height: auto; +} + +.lenis.lenis-smooth { + scroll-behavior: auto; +} + +.lenis.lenis-smooth [data-lenis-prevent] { + overscroll-behavior: contain; +} + +.lenis.lenis-stopped { + overflow: hidden; +} diff --git a/web/cxm/src/main.d.ts b/web/cxm/src/main.d.ts new file mode 100644 index 0000000..6a9a4b1 --- /dev/null +++ b/web/cxm/src/main.d.ts @@ -0,0 +1 @@ +import './index.css'; diff --git a/web/cxm/src/main.js b/web/cxm/src/main.js new file mode 100644 index 0000000..2121b5e --- /dev/null +++ b/web/cxm/src/main.js @@ -0,0 +1,6 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import './index.css'; +import App from './App.tsx'; +createRoot(document.getElementById('root')).render(_jsx(StrictMode, { children: _jsx(App, {}) })); diff --git a/web/cxm/src/main.tsx b/web/cxm/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/web/cxm/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/web/cxm/src/vite-env.d.ts b/web/cxm/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/web/cxm/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/cxm/tsconfig.app.json b/web/cxm/tsconfig.app.json new file mode 100644 index 0000000..f3c56fe --- /dev/null +++ b/web/cxm/tsconfig.app.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "paths": { + "@babel/*": ["./node_modules/@babel/*"] + } + }, + "include": ["src"] +} diff --git a/web/cxm/tsconfig.app.tsbuildinfo b/web/cxm/tsconfig.app.tsbuildinfo new file mode 100644 index 0000000..f02ce6d --- /dev/null +++ b/web/cxm/tsconfig.app.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/imagemodal.tsx","./src/components/parallax.tsx"],"errors":true,"version":"5.6.3"} \ No newline at end of file diff --git a/web/cxm/tsconfig.json b/web/cxm/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/web/cxm/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/web/cxm/tsconfig.node.json b/web/cxm/tsconfig.node.json new file mode 100644 index 0000000..5fe31d5 --- /dev/null +++ b/web/cxm/tsconfig.node.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "ESNext", + "target": "ES2015", + "moduleResolution": "bundler" + }, + "include": ["vite.config.ts"] +} diff --git a/web/cxm/tsconfig.node.tsbuildinfo b/web/cxm/tsconfig.node.tsbuildinfo new file mode 100644 index 0000000..2e20a11 --- /dev/null +++ b/web/cxm/tsconfig.node.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./vite.config.ts"],"errors":true,"version":"5.6.3"} \ No newline at end of file diff --git a/web/cxm/vite.config.d.ts b/web/cxm/vite.config.d.ts new file mode 100644 index 0000000..340562a --- /dev/null +++ b/web/cxm/vite.config.d.ts @@ -0,0 +1,2 @@ +declare const _default: import("vite").UserConfig; +export default _default; diff --git a/web/cxm/vite.config.js b/web/cxm/vite.config.js new file mode 100644 index 0000000..bf919fb --- /dev/null +++ b/web/cxm/vite.config.js @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}); diff --git a/web/cxm/vite.config.ts b/web/cxm/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/web/cxm/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/web/czr/.eslintrc.cjs b/web/czr/.eslintrc.cjs new file mode 100644 index 0000000..4f6f59e --- /dev/null +++ b/web/czr/.eslintrc.cjs @@ -0,0 +1,84 @@ +/** + * This is intended to be a basic starting point for linting in your app. + * It relies on recommended configs out of the box for simplicity, but you can + * and should modify this configuration to best suit your team's needs. + */ + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, + env: { + browser: true, + commonjs: true, + es6: true, + }, + ignorePatterns: ["!**/.server", "!**/.client"], + + // Base config + extends: ["eslint:recommended"], + + overrides: [ + // React + { + files: ["**/*.{js,jsx,ts,tsx}"], + plugins: ["react", "jsx-a11y"], + extends: [ + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/recommended", + ], + settings: { + react: { + version: "detect", + }, + formComponents: ["Form"], + linkComponents: [ + { name: "Link", linkAttribute: "to" }, + { name: "NavLink", linkAttribute: "to" }, + ], + "import/resolver": { + typescript: {}, + }, + }, + }, + + // Typescript + { + files: ["**/*.{ts,tsx}"], + plugins: ["@typescript-eslint", "import"], + parser: "@typescript-eslint/parser", + settings: { + "import/internal-regex": "^~/", + "import/resolver": { + node: { + extensions: [".ts", ".tsx"], + }, + typescript: { + alwaysTryTypes: true, + }, + }, + }, + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", + ], + }, + + // Node + { + files: [".eslintrc.cjs"], + env: { + node: true, + }, + }, + ], +}; diff --git a/web/czr/app/components/Carousel.tsx b/web/czr/app/components/Carousel.tsx new file mode 100644 index 0000000..e31e75c --- /dev/null +++ b/web/czr/app/components/Carousel.tsx @@ -0,0 +1,36 @@ +import { useState, useEffect, useCallback } from 'react'; + +interface CarouselProps { + items: { + content: React.ReactNode; + }[]; + interval?: number; +} + +export function Carousel({ + items, + interval = 5000 +}: CarouselProps) { + const [currentIndex, setCurrentIndex] = useState(0); + + const goToNext = useCallback(() => { + setCurrentIndex((current) => (current + 1) % items.length); + }, [items.length]); + + useEffect(() => { + if (interval > 0) { + const timer = setInterval(goToNext, interval); + return () => clearInterval(timer); + } + }, [interval, goToNext]); + + return ( +
+
+
+ {items[currentIndex].content} +
+
+
+ ); +} \ No newline at end of file diff --git a/web/czr/app/components/Navigation.tsx b/web/czr/app/components/Navigation.tsx new file mode 100644 index 0000000..5cc8180 --- /dev/null +++ b/web/czr/app/components/Navigation.tsx @@ -0,0 +1,116 @@ +import { Link, useLocation } from '@remix-run/react'; +import { useState } from 'react'; + +export default function Navigation() { + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const location = useLocation(); + + const isActive = (path: string) => location.pathname === path; + + const navItems = [ + { path: '/', label: '首页' }, + { path: '/solutions', label: '解决方案' }, + { path: '/innovations', label: '创新' }, + { path: '/about', label: '关于我们' }, + ]; + + return ( + + ); +} \ No newline at end of file diff --git a/web/czr/app/entry.client.tsx b/web/czr/app/entry.client.tsx new file mode 100644 index 0000000..94d5dc0 --- /dev/null +++ b/web/czr/app/entry.client.tsx @@ -0,0 +1,18 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/file-conventions/entry.client + */ + +import { RemixBrowser } from "@remix-run/react"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; + +startTransition(() => { + hydrateRoot( + document, + + + + ); +}); diff --git a/web/czr/app/entry.server.tsx b/web/czr/app/entry.server.tsx new file mode 100644 index 0000000..b390646 --- /dev/null +++ b/web/czr/app/entry.server.tsx @@ -0,0 +1,89 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/file-conventions/entry.server + */ + +import { PassThrough } from "node:stream"; + +import type { AppLoadContext, EntryContext } from "@remix-run/node"; +import { createReadableStreamFromReadable } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import { isbot } from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5_000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, + loadContext: AppLoadContext +) { + return isbot(request.headers.get("user-agent") || "") + ? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext) + : handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + responseHeaders.set("Content-Type", "text/html"); + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + } + ); + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + responseHeaders.set("Content-Type", "text/html"); + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }) + ); + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + } + ); + setTimeout(abort, ABORT_DELAY); + }); +} diff --git a/web/czr/app/env.ts b/web/czr/app/env.ts new file mode 100644 index 0000000..01d7e98 --- /dev/null +++ b/web/czr/app/env.ts @@ -0,0 +1,27 @@ +export interface EnvConfig { + VITE_PORT: string; + VITE_ADDRESS: string; + VITE_INIT_STATUS: string; + VITE_API_BASE_URL: string; + VITE_API_USERNAME: string; + VITE_API_PASSWORD: string; + VITE_PATTERN: string; +} + +export const DEFAULT_CONFIG: EnvConfig = { + VITE_PORT: "22100", + VITE_ADDRESS: "localhost", + VITE_INIT_STATUS: "0", + VITE_API_BASE_URL: "http://127.0.0.1:22000", + VITE_API_USERNAME: "", + VITE_API_PASSWORD: "", + VITE_PATTERN: "true", +} as const; + +// 扩展 ImportMeta 接口 +declare global { + interface ImportMetaEnv extends EnvConfig {} + interface ImportMeta { + readonly env: ImportMetaEnv; + } +} diff --git a/web/czr/app/hooks/ParticleImage.tsx b/web/czr/app/hooks/ParticleImage.tsx new file mode 100644 index 0000000..1cb617f --- /dev/null +++ b/web/czr/app/hooks/ParticleImage.tsx @@ -0,0 +1,3 @@ +import React from 'react'; +import { useEffect, useRef, useState } from 'react'; +// ... 其他导入保持不变 diff --git a/web/czr/app/index.css b/web/czr/app/index.css new file mode 100644 index 0000000..f08cad0 --- /dev/null +++ b/web/czr/app/index.css @@ -0,0 +1,196 @@ +@import "@radix-ui/themes/styles.css"; +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --transition-duration: 150ms; + --transition-easing: cubic-bezier(0.4, 0, 0.2, 1); + --hljs-theme: 'github'; +} + +:root[class~="dark"] { + --hljs-theme: 'github-dark'; +} + +/* 确保 Radix UI 主题类包裹整个应用 */ +.radix-themes { + transition: + background-color var(--transition-duration) var(--transition-easing), + color var(--transition-duration) var(--transition-easing); + min-height: 100%; +} + +/* 基础布局样式 */ +html, +body { + height: 100%; +} + +/* 添加暗色模式支持 */ +.radix-themes-dark { + @apply dark; +} + +/* 隐藏不活跃的主题样式 */ +[data-theme="light"] .hljs-dark { + display: none; +} + +[data-theme="dark"] .hljs-light { + display: none; +} + +/* 导航栏基础样式 */ +.nav-container { + @apply fixed top-0 left-0 right-0 z-50; + @apply bg-white/80 dark:bg-gray-900/80; + @apply backdrop-blur-sm; + @apply border-b border-gray-200 dark:border-gray-700; +} + +.nav-content { + @apply container mx-auto px-4; + @apply flex items-center justify-between; + @apply h-16; +} + +.nav-logo { + @apply text-xl font-bold; + @apply text-gray-800 dark:text-white; + @apply hover:text-gray-600 dark:hover:text-gray-300; + @apply transition-colors duration-200; +} + +.nav-links { + @apply hidden md:flex items-center space-x-8; +} + +.nav-link { + @apply text-gray-600 dark:text-gray-300; + @apply hover:text-gray-900 dark:hover:text-white; + @apply transition-colors duration-200; + @apply font-medium; +} + +/* 移动端菜单按钮 */ +.mobile-menu-button { + @apply md:hidden; + @apply p-2 rounded-md; + @apply text-gray-600 dark:text-gray-300; + @apply hover:bg-gray-100 dark:hover:bg-gray-800; + @apply transition-colors duration-200; +} + +/* 移动端导航菜单 */ +.mobile-nav { + @apply md:hidden; + @apply fixed top-16 left-0 right-0; + @apply bg-white dark:bg-gray-900; + @apply border-b border-gray-200 dark:border-gray-700; + @apply shadow-lg; +} + +.mobile-nav-links { + @apply flex flex-col space-y-4; + @apply p-4; +} + +.mobile-nav-link { + @apply text-gray-600 dark:text-gray-300; + @apply hover:text-gray-900 dark:hover:text-white; + @apply transition-colors duration-200; + @apply font-medium; + @apply block; + @apply py-2; +} + +/* 激活状态的导航链接 */ +.nav-link.active, +.mobile-nav-link.active { + @apply text-blue-600 dark:text-blue-400; +} + + + +/* 注意:每个路径都需要在 SVG 中设置 --path-length 变量 */ +.animated-text { + max-width: 100%; + height: auto; +} + +.animated-text path { + fill: transparent; + stroke: currentColor; + stroke-width: 2; + /* 使用每个路径自己的长度 */ + stroke-dasharray: var(--path-length); + stroke-dashoffset: var(--path-length); + animation: logo-anim 15s cubic-bezier(0.4, 0, 0.2, 1) infinite; + transform-origin: center; + stroke-linecap: round; + stroke-linejoin: round; +} + +@keyframes logo-anim { + 0% { + stroke-dashoffset: var(--path-length); + stroke-dasharray: var(--path-length) var(--path-length); + opacity: 0; + fill: transparent; + } + + 5% { + opacity: 1; + stroke-dashoffset: var(--path-length); + stroke-dasharray: var(--path-length) var(--path-length); + } + + 50% { + stroke-dashoffset: 0; + stroke-dasharray: var(--path-length) var(--path-length); + fill: transparent; + } + + 60% { + stroke-dashoffset: 0; + stroke-dasharray: var(--path-length) var(--path-length); + fill: currentColor; + opacity: 1; + } + + 75% { + stroke-dashoffset: 0; + stroke-dasharray: var(--path-length) var(--path-length); + fill: currentColor; + opacity: 1; + } + + 85% { + stroke-dashoffset: 0; + stroke-dasharray: var(--path-length) var(--path-length); + fill: transparent; + opacity: 1; + } + + 95% { + stroke-dashoffset: var(--path-length); + stroke-dasharray: var(--path-length) var(--path-length); + fill: transparent; + opacity: 1; + } + + 100% { + stroke-dashoffset: var(--path-length); + stroke-dasharray: var(--path-length) var(--path-length); + fill: transparent; + opacity: 0; + } +} + +/* 确保在暗色模式下的颜色正确 */ +@media (prefers-color-scheme: dark) { + .animated-text path { + stroke: currentColor; + } +} \ No newline at end of file diff --git a/web/czr/app/init.tsx b/web/czr/app/init.tsx new file mode 100644 index 0000000..ba43738 --- /dev/null +++ b/web/czr/app/init.tsx @@ -0,0 +1,508 @@ +import React, { createContext, useState } from "react"; +import { DEFAULT_CONFIG } from "app/env"; +import { HttpClient } from "core/http"; +import { ThemeModeToggle } from "hooks/themeMode"; +import { + Theme, + Button, + Select, + Flex, + Container, + Heading, + Text, + Box, + TextField, +} from "@radix-ui/themes"; +import { toast } from "hooks/notification"; +import { Echoes } from "hooks/echoes"; + +interface SetupContextType { + currentStep: number; + setCurrentStep: (step: number) => void; +} + +const SetupContext = createContext({ + currentStep: 1, + setCurrentStep: () => {}, +}); + +// 步骤组件的通用属性接口 +interface StepProps { + onNext: () => void; +} + +const StepContainer: React.FC<{ title: string; children: React.ReactNode }> = ({ + title, + children, +}) => ( + + + {title} + + + {children} + + +); + +// 通用的导航按钮组件 +const NavigationButtons: React.FC< + StepProps & { loading?: boolean; disabled?: boolean } +> = ({ onNext, loading = false, disabled = false }) => ( + + + +); + +// 修改输入框组件 +const InputField: React.FC<{ + label: string; + name: string; + defaultValue?: string | number; + hint?: string; + required?: boolean; +}> = ({ label, name, defaultValue, hint, required = true }) => ( + + + {label} {required && *} + + + + + {hint && ( + + {hint} + + )} + +); + +const Introduction: React.FC = ({ onNext }) => ( + + + 欢迎使用 Echoes + + + +); + +const DatabaseConfig: React.FC = ({ onNext }) => { + const [dbType, setDbType] = useState("postgresql"); + const [loading, setLoading] = useState(false); + const http = HttpClient.getInstance(); + + const validateForm = () => { + const getRequiredFields = () => { + switch (dbType) { + case "sqllite": + return ["db_prefix", "db_name"]; + case "postgresql": + case "mysql": + return [ + "db_host", + "db_prefix", + "db_port", + "db_user", + "db_password", + "db_name", + ]; + default: + return []; + } + }; + + const requiredFields = getRequiredFields(); + const emptyFields: string[] = []; + + requiredFields.forEach((field) => { + const input = document.querySelector( + `[name="${field}"]`, + ) as HTMLInputElement; + if (input && (!input.value || input.value.trim() === "")) { + emptyFields.push(field); + } + }); + + if (emptyFields.length > 0) { + const fieldNames = emptyFields.map((field) => { + switch (field) { + case "db_host": + return "数据库地址"; + case "db_prefix": + return "数据库前缀"; + case "db_port": + return "端口"; + case "db_user": + return "用户名"; + case "db_password": + return "密码"; + case "db_name": + return "数据库名"; + default: + return field; + } + }); + toast.error(`请填写以下必填项:${fieldNames.join("、")}`); + return false; + } + return true; + }; + + const handleNext = async () => { + const validation = validateForm(); + if (validation !== true) { + return; + } + + setLoading(true); + try { + const formData = { + db_type: dbType, + host: + ( + document.querySelector('[name="db_host"]') as HTMLInputElement + )?.value?.trim() ?? "", + db_prefix: + ( + document.querySelector('[name="db_prefix"]') as HTMLInputElement + )?.value?.trim() ?? "", + port: Number( + ( + document.querySelector('[name="db_port"]') as HTMLInputElement + )?.value?.trim() ?? 0, + ), + user: + ( + document.querySelector('[name="db_user"]') as HTMLInputElement + )?.value?.trim() ?? "", + password: + ( + document.querySelector('[name="db_password"]') as HTMLInputElement + )?.value?.trim() ?? "", + db_name: + ( + document.querySelector('[name="db_name"]') as HTMLInputElement + )?.value?.trim() ?? "", + }; + + await http.post("/sql", formData); + + let oldEnv = import.meta.env ?? DEFAULT_CONFIG; + const viteEnv = Object.entries(oldEnv).reduce( + (acc, [key, value]) => { + if (key.startsWith("VITE_")) { + acc[key] = value; + } + return acc; + }, + {} as Record, + ); + + const newEnv = { + ...viteEnv, + VITE_INIT_STATUS: "2", + }; + + await http.dev("/env", { + method: "POST", + body: JSON.stringify(newEnv), + }); + + Object.assign(import.meta.env, newEnv); + + toast.success("数据库配置成功!"); + + setTimeout(() => onNext(), 1000); + } catch (error: any) { + console.error(error); + toast.error(error.message, error.title); + } finally { + setLoading(false); + } + }; + + return ( + +
+ + + 数据库类型 + + + + + + PostgreSQL + MySQL + SQLite + + + + + + {dbType === "postgresql" && ( + <> + + + + + + + + )} + {dbType === "mysql" && ( + <> + + + + + + + + )} + {dbType === "sqllite" && ( + <> + + + + )} + +
+
+ ); +}; + +interface InstallReplyData { + token: string; + username: string; + password: string; +} + +const AdminConfig: React.FC = ({ onNext }) => { + const [loading, setLoading] = useState(false); + const http = HttpClient.getInstance(); + + const handleNext = async () => { + setLoading(true); + try { + const formData = { + username: ( + document.querySelector('[name="admin_username"]') as HTMLInputElement + )?.value, + password: ( + document.querySelector('[name="admin_password"]') as HTMLInputElement + )?.value, + email: ( + document.querySelector('[name="admin_email"]') as HTMLInputElement + )?.value, + }; + + const response = (await http.post( + "/administrator", + formData, + )) as InstallReplyData; + const data = response; + + localStorage.setItem("token", data.token); + + let oldEnv = import.meta.env ?? DEFAULT_CONFIG; + const viteEnv = Object.entries(oldEnv).reduce( + (acc, [key, value]) => { + if (key.startsWith("VITE_")) { + acc[key] = value; + } + return acc; + }, + {} as Record, + ); + + const newEnv = { + ...viteEnv, + VITE_INIT_STATUS: "3", + VITE_API_USERNAME: data.username, + VITE_API_PASSWORD: data.password, + }; + + await http.dev("/env", { + method: "POST", + body: JSON.stringify(newEnv), + }); + + Object.assign(import.meta.env, newEnv); + + toast.success("管理员账号创建成功!"); + onNext(); + } catch (error: any) { + console.error(error); + toast.error(error.message, error.title); + } finally { + setLoading(false); + } + }; + + return ( + +
+ + + + +
+
+ ); +}; + +const SetupComplete: React.FC = () => ( + + + + 恭喜!安装已完成 + + 系统正在重启中,请稍候... + + + + + + + +); + +export default function SetupPage() { + const [currentStep, setCurrentStep] = useState(() => { + return Number(import.meta.env.VITE_INIT_STATUS ?? 0) + 1; + }); + + return ( + + + + + + + + + + + + + + + + {currentStep === 1 && ( + setCurrentStep(currentStep + 1)} /> + )} + {currentStep === 2 && ( + setCurrentStep(currentStep + 1)} + /> + )} + {currentStep === 3 && ( + setCurrentStep(currentStep + 1)} /> + )} + {currentStep === 4 && } + + + + + + ); +} diff --git a/web/czr/app/root.tsx b/web/czr/app/root.tsx new file mode 100644 index 0000000..eec2753 --- /dev/null +++ b/web/czr/app/root.tsx @@ -0,0 +1,31 @@ +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; + +import "./index.css" +import Navigation from '~/components/Navigation'; + +export default function App() { + return ( + + + + + + + + + +
+ +
+ + + + + ); +} diff --git a/web/czr/app/routes.tsx b/web/czr/app/routes.tsx new file mode 100644 index 0000000..37543b7 --- /dev/null +++ b/web/czr/app/routes.tsx @@ -0,0 +1,46 @@ +import ErrorPage from "hooks/error"; +import layout from "themes/echoes/layout"; +import article from "themes/echoes/article"; +import about from "themes/echoes/about"; +import { useLocation } from "react-router-dom"; +import post from "themes/echoes/post"; + +export default function Routes() { + const location = useLocation(); + let path = location.pathname; + + const args = { + title: "我的页面", + theme: "dark", + nav: 'indexerroraboutpost', + }; + + console.log(path); + path = path.split("/")[1]; + + if (path === "error") { + return layout.render({ + children: ErrorPage.render(args), + args, + }); + } + + if (path === "about") { + return layout.render({ + children: about.render(args), + args, + }); + } + + if (path === "post") { + return layout.render({ + children: post.render(args), + args, + }); + } + + return layout.render({ + children: article.render(args), + args, + }); +} diff --git a/web/czr/app/routes/_index.tsx b/web/czr/app/routes/_index.tsx new file mode 100644 index 0000000..4991238 --- /dev/null +++ b/web/czr/app/routes/_index.tsx @@ -0,0 +1,146 @@ +import type { MetaFunction } from "@remix-run/node"; + +export const meta: MetaFunction = () => { + return [ + { title: "新纪元科技 - 引领创新未来" }, + { name: "description", content: "专注于环保科技创新,为可持续发展提供解决方案" }, + ]; +}; + +export default function Index() { + return ( +
+ + {/* Hero区域 */} +
+
+
+

+ 创新科技,守护地球 +

+

+ 致力于开发环保科技解决方案,用创新力量推动可持续发展 +

+ +
+
+
+ + {/* 核心技术 */} +
+
+

核心创新技术

+
+
+
+ + + +
+

智能环保系统

+

采用AI技术优化资源利用,提高能源使用效率

+
+ +
+
+ + + +
+

新能源转换

+

创新能源转换技术,实现清洁能源的高效利用

+
+ +
+
+ + + +
+

生态监测

+

全方位环境监测系统,保护生态平衡

+
+
+
+
+ + {/* 解决方案 */} +
+
+

创新解决方案

+
+
+

智慧城市环保系统

+

整合城市环境数据,提供智能化环保解决方案

+
    +
  • • 空气质量实时监测
  • +
  • • 智能垃圾分类系统
  • +
  • • 城市能源管理优化
  • +
+
+ +
+

工业节能方案

+

为工业企业提供全方位的节能减排解决方案

+
    +
  • • 能源使用效率优化
  • +
  • • 废物循环利用系统
  • +
  • • 清洁生产技术改造
  • +
+
+
+
+
+ + {/* 页脚 */} +
+
+
+
+

关于我们

+

新纪元科技致力于环保科技创新,为地球可持续发展贡献力量

+
+
+

联系方式

+

电话:400-888-8888

+

邮箱:contact@xingjiyuan.com

+
+
+

解决方案

+ +
+
+

公司地址

+

中国上海市浦东新区科技创新大道888号

+
+
+
+

© 2024 新纪元科技 版权所有

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/web/czr/app/routes/about.tsx b/web/czr/app/routes/about.tsx new file mode 100644 index 0000000..c6d3b9b --- /dev/null +++ b/web/czr/app/routes/about.tsx @@ -0,0 +1,67 @@ +import type { MetaFunction } from "@remix-run/node"; +export const meta: MetaFunction = () => { + return [ + { title: "关于我们 - 新纪元科技" }, + { name: "description", content: "了解新纪元科技的使命与愿景" }, + ]; +}; + +export default function About() { + return ( +
+ + {/* 页面标题 */} +
+
+

+ 关于我们 +

+

+ 致力于用科技创新推动环保事业发展 +

+
+
+ + {/* 公司介绍 */} +
+
+
+
+

+ 公司简介 +

+

+ 新纪元科技成立于2020年,是一家专注于环保科技创新的高新技术企业。我们致力于通过技术创新解决环境问题,推动可持续发展。 +

+

+ 公司拥有一支专业的研发团队,在环保技术领域具有深厚的积累和创新能力。 +

+
+
+

+ 愿景使命 +

+
+
+

愿景

+

成为全球领先的环保科技创新企业

+
+
+

使命

+

用科技创新守护地球家园

+
+
+
+
+
+
+ + {/* 页脚 */} +
+ {/* 同首页页脚内容 */} +
+
+ ); +} \ No newline at end of file diff --git a/web/czr/app/routes/innovations.tsx b/web/czr/app/routes/innovations.tsx new file mode 100644 index 0000000..9e63e47 --- /dev/null +++ b/web/czr/app/routes/innovations.tsx @@ -0,0 +1,157 @@ +import type { MetaFunction } from "@remix-run/node"; +import { ImageLoader } from "hooks/ParticleImage"; +import { useLoaderData } from "@remix-run/react"; +import { Carousel } from "~/components/Carousel"; + +export const meta: MetaFunction = () => { + return [ + { title: "创新技术 - 新纪元科技" }, + { name: "description", content: "新纪元科技的创新技术" }, + ]; +}; + +export const loader = () => { + return { + isClient: true, + innovations: [ + { + title: "智能环境监测", + image: "/a1.jpg", + }, + { + title: "清洁能源技术", + image: "/a2.jpg", + }, + { + title: "废物处理创新", + image: "/a3.jpg", + } + ] + }; +}; + +export default function Innovations() { + const { isClient, innovations } = useLoaderData(); + + return ( +
+ {/* 头部区域:标题 + 轮播图 */} +
+
+ {/* 标题部分 */} +
+

+ 创新技术 +

+

+ 引领环保科技发展,推动行业技术革新 +

+
+ + {/* 轮播图部分 */} + {isClient ? ( +
+
+
+
+ ({ + content: ( +
+ +
+
+

{innovation.title}

+

探索环保科技的无限可能

+
+
+ ), + }))} + interval={5000} + /> +
+
+
+ ) : ( +
+
+
+ )} +
+
+ + {/* 技术详情部分 */} +
+
+

+ + 核心技术详解 + +

+
+
+
+ + + +
+

+ AI环境优化 + +

+

+ 运用人工智能技术,实现环境数据的智能分析和决策优化,提供精准的环境治理方案。 +

+
+ +
+
+ + + +
+

+ 清洁能源转换 + +

+

+ 创新的能源转换技术,提高清洁能源利用效率,推动色能源革命。 +

+
+ +
+
+ + + +
+

+ 生态修复系统 + +

+

+ 综合性生态环境修复解决方案,助力自然生态系统恢复与保护 +

+
+
+
+
+ + {/* 页脚 */} +
+ {/* 同首页页脚内容 */} +
+
+ ); +} \ No newline at end of file diff --git a/web/czr/app/routes/solutions.tsx b/web/czr/app/routes/solutions.tsx new file mode 100644 index 0000000..5ff58a4 --- /dev/null +++ b/web/czr/app/routes/solutions.tsx @@ -0,0 +1,105 @@ +import type { MetaFunction } from "@remix-run/node"; +import { ImageLoader } from "hooks/ParticleImage"; +import { useLoaderData } from "@remix-run/react"; + +export const meta: MetaFunction = () => { + return [ + { title: "解决方案 - 新纪元科技" }, + { name: "description", content: "新纪元科技提供的环保科技解决方案" }, + ]; +}; + +export const loader = () => { + return { isClient: true }; +}; + +export default function Solutions() { + const { isClient } = useLoaderData(); + + return ( +
+ {/* 页面标题和轮播图 */} +
+
+
+ {isClient ? ( +
+ {/* 轮播图代码从这里开始 */} +
+ ) : ( +
+ )} +

+ 解决方案 +

+

+ 为不同行业提供定制化的环保科技解决方案,助力企业实现可持续发展 +

+
+
+
+ + {/* 解决方案详情 */} +
+
+
+
+

+ 智慧城市解决方案 + +

+
+

通过智能技术优化城市环境管理

+
    +
  • + • 智能环境监测系统 +
  • +
  • + • 城市垃圾分类管理 +
  • +
  • + • 智慧能源管理平台 +
  • +
  • + • 城市空气质量优化 +
  • +
+
+
+ +
+

+ 工业节能方案 + +

+
+

帮助工业企业实现节能减排

+
    +
  • + • 工业能源审计 +
  • +
  • + • 节能改造方案 +
  • +
  • + • 废物循环利用 +
  • +
  • + • 清洁生产技术 +
  • +
+
+
+
+
+
+ + {/* 页脚 - 可以提取为共享组件 */} +
+ {/* 同首页页脚内容 */} +
+
+ ); +} \ No newline at end of file diff --git a/web/czr/app/styles/navigation.css b/web/czr/app/styles/navigation.css new file mode 100644 index 0000000..3d6ff15 --- /dev/null +++ b/web/czr/app/styles/navigation.css @@ -0,0 +1,5 @@ +.nav-logo svg { + width: 120px; + height: auto; + color: #1a1a1a; +} \ No newline at end of file diff --git a/web/czr/hooks/ParticleImage.tsx b/web/czr/hooks/ParticleImage.tsx new file mode 100644 index 0000000..80a46db --- /dev/null +++ b/web/czr/hooks/ParticleImage.tsx @@ -0,0 +1,711 @@ +import { useEffect, useRef, useState, useCallback } from 'react'; +import * as THREE from 'three'; +import { gsap } from 'gsap'; +import throttle from 'lodash/throttle'; + +interface HSL { + h: number; + s: number; + l: number; +} + +interface Particle { + x: number; + y: number; + z: number; + originalX: number; + originalY: number; + originalColor: THREE.Color; + delay: number; +} + +const createErrorParticles = (width: number, height: number) => { + const particles: Particle[] = []; + const positionArray: number[] = []; + const colorArray: number[] = []; + + const errorColor = new THREE.Color(0.8, 0, 0); + const size = Math.min(width, height); + const scaleFactor = size * 0.3; + const particlesPerLine = 50; + + // X 形状的两条线 + const lines = [ + // 左上到右下的线 + { start: [-1, 1], end: [1, -1] }, + // 右上到左下的线 + { start: [1, 1], end: [-1, -1] } + ]; + + lines.forEach(line => { + for (let i = 0; i < particlesPerLine; i++) { + const t = i / (particlesPerLine - 1); + const x = line.start[0] + (line.end[0] - line.start[0]) * t; + const y = line.start[1] + (line.end[1] - line.start[1]) * t; + + // 添加一些随机偏移 + const randomOffset = 0.1; + const randomX = x + (Math.random() - 0.5) * randomOffset; + const randomY = y + (Math.random() - 0.5) * randomOffset; + + const scaledX = randomX * scaleFactor; + const scaledY = randomY * scaleFactor; + + particles.push({ + x: scaledX, + y: scaledY, + z: 0, + originalX: scaledX, + originalY: scaledY, + originalColor: errorColor, + delay: 0 + }); + + // 修改初始位置生成方式 + const angle = Math.random() * Math.PI * 2; + const distance = size * 2; + positionArray.push( + Math.cos(angle) * distance, + Math.sin(angle) * distance, + 0 + ); + + // 初始颜色设置为最终颜色的一半亮度 + colorArray.push(errorColor.r * 0.5, errorColor.g * 0.5, errorColor.b * 0.5); + } + }); + + const particleSize = Math.max(1.2, (size / 200) * 1.2); + + return { particles, positionArray, colorArray, particleSize }; +}; + +// 修改 createSmileParticles 函数 +const createSmileParticles = (width: number, height: number) => { + const particles: Particle[] = []; + const positionArray: number[] = []; + const colorArray: number[] = []; + + const size = Math.min(width, height); + const scale = size / 200; + const radius = size * 0.35; + const particleSize = Math.max(1.2, scale * 1.2); + const particleColor = new THREE.Color(0.8, 0.6, 0); + + // 预先计算所有需要的粒子位置 + const allPoints: { x: number; y: number }[] = []; + + // 计算脸部轮廓的点 + const outlinePoints = Math.floor(60 * scale); + for (let i = 0; i < outlinePoints; i++) { + const angle = (i / outlinePoints) * Math.PI * 2; + allPoints.push({ + x: Math.cos(angle) * radius, + y: Math.sin(angle) * radius + }); + } + + // 修改眼睛的生成方式 + const eyeOffset = radius * 0.3; + const eyeY = radius * 0.15; + const eyeSize = radius * 0.1; // 稍微减小眼睛尺寸 + const eyePoints = Math.floor(20 * scale); + + [-1, 1].forEach(side => { + // 使用同心圆的方式生成眼睛 + const eyeCenterX = side * eyeOffset; + const rings = 3; // 同心圆的数量 + + for (let ring = 0; ring < rings; ring++) { + const ringRadius = eyeSize * (1 - ring / rings); // 从外到内递减半径 + const pointsInRing = Math.floor(eyePoints / rings); + + for (let i = 0; i < pointsInRing; i++) { + const angle = (i / pointsInRing) * Math.PI * 2; + allPoints.push({ + x: eyeCenterX + Math.cos(angle) * ringRadius, + y: eyeY + Math.sin(angle) * ringRadius + }); + } + } + + // 添加中心点 + allPoints.push({ + x: eyeCenterX, + y: eyeY + }); + }); + + // 计算嘴巴的点 + const smileWidth = radius * 0.6; + const smileY = -radius * 0.35; + const smilePoints = Math.floor(25 * scale); + + for (let i = 0; i < smilePoints; i++) { + const t = i / (smilePoints - 1); + const x = (t * 2 - 1) * smileWidth; + const y = smileY + Math.pow(x / smileWidth, 2) * radius * 0.2; + allPoints.push({ x, y }); + } + + // 为所有点创建粒子 + allPoints.forEach(point => { + particles.push({ + x: point.x, + y: point.y, + z: 0, + originalX: point.x, + originalY: point.y, + originalColor: particleColor, + delay: 0 + }); + + // 生成初始位置(从外围圆形区域开始) + const initAngle = Math.random() * Math.PI * 2; + const distance = size * 2; + positionArray.push( + Math.cos(initAngle) * distance, + Math.sin(initAngle) * distance, + 0 + ); + + // 初始颜色设置为最终颜色的一半亮度 + colorArray.push( + particleColor.r * 0.5, + particleColor.g * 0.5, + particleColor.b * 0.5 + ); + }); + + return { particles, positionArray, colorArray, particleSize }; +}; + +// 在文件开头添加新的 helper 函数 +const easeOutCubic = (t: number) => { + return 1 - Math.pow(1 - t, 3); +}; + +const customEase = (t: number) => { + return t < 0.5 + ? 4 * t * t * t + : 1 - Math.pow(-2 * t + 2, 3) / 2; +}; + +// 在文件开头添加新的 LoaderStatus 接口 +interface LoaderStatus { + isLoading: boolean; + hasError: boolean; + timeoutError: boolean; + animationPhase: 'assembling' | 'image' | 'dissolving' | 'transitioning'; +} + +// 修改 ParticleImage 组件的 props 接口 +interface ParticleImageProps { + src?: string; + status?: LoaderStatus; + onLoad?: () => void; + onAnimationComplete?: () => void; + transitionType?: 'in' | 'out' | 'none'; + previousParticles?: Particle[]; + onParticlesCreated?: (particles: Particle[]) => void; + onAnimationPhaseChange?: (phase: 'assembling' | 'image' | 'dissolving' | 'transitioning') => void; +} + +// 修改 BG_CONFIG +const BG_CONFIG = { + colors: { + from: 'rgb(10,37,77)', + via: 'rgb(8,27,57)', + to: 'rgb(2,8,23)' + }, + className: 'bg-gradient-to-br from-[rgb(248,250,252)] via-[rgb(241,245,249)] to-[rgb(236,241,247)] dark:from-[rgb(10,37,77)] dark:via-[rgb(8,27,57)] dark:to-[rgb(2,8,23)]', + size: { + container: '' + } +}; + +// 修改图像采样函数 +const createParticlesFromImage = (imageData: ImageData, width: number, height: number) => { + const particles: Particle[] = []; + const positionArray: number[] = []; + const colorArray: number[] = []; + + // 根据容器尺寸计算缩放因子 + const aspectRatio = width / height; + const scaleFactor = width / 2; // 使用容器宽度的一半作为基准 + + // 固定粒子数量以保持一致的视觉效果 + const particlesPerSide = Math.floor(Math.min(150, Math.max(80, Math.min(width, height) / 4))); + const stepX = width / particlesPerSide; + const stepY = height / particlesPerSide; + + for (let y = 0; y < height; y += stepY) { + for (let x = 0; x < width; x += stepX) { + const pixelX = Math.floor(x); + const pixelY = Math.floor(y); + const i = (pixelY * width + pixelX) * 4; + + const r = imageData.data[i] / 255; + const g = imageData.data[i + 1] / 255; + const b = imageData.data[i + 2] / 255; + + // 计算亮度并设置最小值 + const brightness = Math.max(0.1, (r + g + b) / 3); + + // 将坐标映射到容器范围 + const px = ((x / width) * 2 - 1) * scaleFactor; + const py = ((1 - y / height) * 2 - 1) * (scaleFactor / aspectRatio); + + // 创建粒子 + const finalColor = new THREE.Color( + Math.max(0.1, r), + Math.max(0.1, g), + Math.max(0.1, b) + ); + + particles.push({ + x: px, + y: py, + z: 0, + originalX: px, + originalY: py, + originalColor: finalColor, + delay: Math.random() * 0.3 + }); + + // 设置初始位置 + const angle = Math.random() * Math.PI * 2; + const distance = Math.max(width, height); + positionArray.push( + Math.cos(angle) * distance, + Math.sin(angle) * distance, + 0 + ); + + // 设置初始颜色 + colorArray.push( + finalColor.r * 0.3, + finalColor.g * 0.3, + finalColor.b * 0.3 + ); + } + } + + // 调整粒子大小 + const particleSize = Math.max(2, Math.min(width, height) / 150); + + return { particles, positionArray, colorArray, particleSize }; +}; + +// 优化动画效果 +const animateParticles = ( + particles: Particle[], + geometry: THREE.BufferGeometry, + onComplete?: () => void +) => { + const positionAttribute = geometry.attributes.position; + const colorAttribute = geometry.attributes.color; + + particles.forEach((particle, i) => { + const i3 = i * 3; + + // 位置动画 + gsap.to(positionAttribute.array, { + duration: 1 + Math.random() * 0.5, + delay: particle.delay, + [i3]: particle.originalX, + [i3 + 1]: particle.originalY, + [i3 + 2]: 0, + ease: "power2.out", + onUpdate: () => void (positionAttribute.needsUpdate = true) + }); + + // 颜色动画 + gsap.to(colorAttribute.array, { + duration: 0.8, + delay: particle.delay, + [i3]: particle.originalColor.r, + [i3 + 1]: particle.originalColor.g, + [i3 + 2]: particle.originalColor.b, + ease: "power2.inOut", + onUpdate: () => { + colorAttribute.needsUpdate = true; + return; + }, + onComplete: i === particles.length - 1 ? onComplete : undefined + }); + }); +}; + +export const ParticleImage = ({ + src, + status, + onLoad, + onAnimationComplete, + transitionType = 'in', + previousParticles, + onParticlesCreated, + onAnimationPhaseChange +}: ParticleImageProps) => { + const containerRef = useRef(null); + const sceneRef = useRef(); + const cameraRef = useRef(); + const rendererRef = useRef(); + const animationFrameRef = useRef(); + const geometryRef = useRef(); + const materialRef = useRef(); + const pointsRef = useRef(); + + // 清理函数 + const cleanup = useCallback(() => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + + // 清理 Three.js 资源 + if (geometryRef.current) { + geometryRef.current.dispose(); + } + + if (materialRef.current) { + materialRef.current.dispose(); + } + + if (pointsRef.current) { + if (pointsRef.current.geometry) { + pointsRef.current.geometry.dispose(); + } + if (pointsRef.current.material instanceof THREE.Material) { + pointsRef.current.material.dispose(); + } + sceneRef.current?.remove(pointsRef.current); + } + + if (rendererRef.current) { + rendererRef.current.dispose(); + if (containerRef.current?.contains(rendererRef.current.domElement)) { + containerRef.current.removeChild(rendererRef.current.domElement); + } + } + + // 清理 GSAP 动画 + gsap.killTweensOf('*'); + }, []); + + useEffect(() => { + if (!containerRef.current || !src) return; + + const width = containerRef.current.offsetWidth; + const height = containerRef.current.offsetHeight; + + // 建错误动画函数 + const showErrorAnimation = () => { + if (!sceneRef.current) return; + + const { particles, positionArray, colorArray, particleSize } = createErrorParticles(width, height); + // ... 其余错误动画代码 ... + }; + + const timeoutId = setTimeout(() => showErrorAnimation(), 5000); + + // 初始化场景 + const scene = new THREE.Scene(); + sceneRef.current = scene; + + // 调整相机视角 + const camera = new THREE.OrthographicCamera( + width / -2, + width / 2, + height / 2, + height / -2, + 1, + 1000 + ); + camera.position.z = 500; + cameraRef.current = camera; + + const renderer = new THREE.WebGLRenderer({ + alpha: true, + antialias: true + }); + renderer.setSize(width, height); + rendererRef.current = renderer; + containerRef.current.appendChild(renderer.domElement); + + // 加载图片 + const img = new Image(); + img.crossOrigin = 'anonymous'; + + img.onload = () => { + clearTimeout(timeoutId); + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (ctx) { + // 计算目标尺寸和裁剪区域 + const targetAspect = width / height; + const imgAspect = img.width / img.height; + + let sourceWidth = img.width; + let sourceHeight = img.height; + let sourceX = 0; + let sourceY = 0; + + // 裁剪源图片,确保比例匹配目标容器 + if (imgAspect > targetAspect) { + sourceWidth = img.height * targetAspect; + sourceX = (img.width - sourceWidth) / 2; + } else { + sourceHeight = img.width / targetAspect; + sourceY = (img.height - sourceHeight) / 2; + } + + canvas.width = width; + canvas.height = height; + + // 清除画布 + ctx.clearRect(0, 0, width, height); + + // 绘制图像 + ctx.drawImage( + img, + sourceX, sourceY, sourceWidth, sourceHeight, + 0, 0, width, height + ); + + const imageData = ctx.getImageData(0, 0, width, height); + const { particles, positionArray, colorArray, particleSize } = createParticlesFromImage(imageData, width, height); + + // 通知父组件新的粒子已创建 + onParticlesCreated?.(particles); + + // 创建粒子系统 + const geometry = new THREE.BufferGeometry(); + geometryRef.current = geometry; + + const material = new THREE.PointsMaterial({ + size: particleSize, + vertexColors: true, + transparent: true, + opacity: 1, + sizeAttenuation: true, + blending: THREE.AdditiveBlending, + depthWrite: false, + depthTest: false + }); + materialRef.current = material; + + geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionArray, 3)); + geometry.setAttribute('color', new THREE.Float32BufferAttribute(colorArray, 3)); + + const points = new THREE.Points(geometry, material); + pointsRef.current = points; + scene.add(points); + + // 动画分 + const timeline = gsap.timeline(); + + const positionAttribute = geometry.attributes.position; + const colorAttribute = geometry.attributes.color; + + // 立即执动画 + if (transitionType === 'in') { + // 设置初始位置和颜色 + particles.forEach((particle, i) => { + const i3 = i * 3; + // 设置随机初始位置 + positionAttribute.array[i3] = (Math.random() - 0.5) * width * 2; + positionAttribute.array[i3 + 1] = (Math.random() - 0.5) * height * 2; + positionAttribute.array[i3 + 2] = Math.random() * 100; + + // 设置初始颜色 + colorAttribute.array[i3] = particle.originalColor.r * 0.2; + colorAttribute.array[i3 + 1] = particle.originalColor.g * 0.2; + colorAttribute.array[i3 + 2] = particle.originalColor.b * 0.2; + }); + + positionAttribute.needsUpdate = true; + colorAttribute.needsUpdate = true; + + // 创建动画 + particles.forEach((particle, i) => { + const i3 = i * 3; + + // 位置动画 + gsap.to(positionAttribute.array, { + duration: 2, + delay: particle.delay, + [i3]: particle.originalX, + [i3 + 1]: particle.originalY, + [i3 + 2]: 0, + ease: "power2.inOut", + onUpdate: () => { positionAttribute.needsUpdate = true; } + }); + + // 颜色动画 + gsap.to(colorAttribute.array, { + duration: 1.8, + delay: particle.delay + 0.2, + [i3]: particle.originalColor.r, + [i3 + 1]: particle.originalColor.g, + [i3 + 2]: particle.originalColor.b, + ease: "power2.inOut", + onUpdate: () => { colorAttribute.needsUpdate = true; } + }); + }); + } else if (transitionType === 'out') { + particles.forEach((particle, i) => { + const i3 = i * 3; + gsap.to(colorAttribute.array, { + duration: 1, + [i3]: particle.originalColor.r * 0.2, + [i3 + 1]: particle.originalColor.g * 0.2, + [i3 + 2]: particle.originalColor.b * 0.2, + ease: "power2.in", + onUpdate: () => { + colorAttribute.needsUpdate = true; + } + }); + }); + } + + // 画循环 + const animate = () => { + animationFrameRef.current = requestAnimationFrame(animate); + renderer.render(scene, camera); + }; + animate(); + + onLoad?.(); + } + }; + + img.src = src; + + return () => { + clearTimeout(timeoutId); + cleanup(); + }; + }, [src, cleanup, onLoad, onAnimationComplete, transitionType, previousParticles, onParticlesCreated]); + + return
; +}; + +// 修改 ImageLoader 组件的 props 接口 +interface ImageLoaderProps { + src?: string; + alt: string; + className?: string; + containerClassName?: string; // 新增容器类名属性 +} + +// 修改 ImageLoader 组件 +export const ImageLoader = ({ src, alt, className = '', containerClassName = '' }: ImageLoaderProps) => { + const [status, setStatus] = useState({ + isLoading: true, + hasError: false, + timeoutError: false, + animationPhase: 'assembling' + }); + const [showImage, setShowImage] = useState(false); + const timeoutRef = useRef(); + const loadingRef = useRef(false); + const imageRef = useRef(null); + const [currentParticles, setCurrentParticles] = useState([]); + + // 动画循环 + const startAnimationCycle = useCallback(async () => { + while (true) { + // 1. 粒子组合成图像 + setStatus(prev => ({ ...prev, animationPhase: 'assembling' })); + setShowImage(false); + await new Promise(resolve => setTimeout(resolve, 2500)); + + // 2. 显示实际图片 + setShowImage(true); + await new Promise(resolve => setTimeout(resolve, 3000)); + + // 3. 隐藏图片,显示粒子 + setShowImage(false); + setStatus(prev => ({ ...prev, animationPhase: 'dissolving' })); + await new Promise(resolve => setTimeout(resolve, 3000)); + + // 4. 粒子过渡状态 + setStatus(prev => ({ ...prev, animationPhase: 'transitioning' })); + await new Promise(resolve => setTimeout(resolve, 1500)); + } + }, []); + + // 处理图片预加载 + const preloadImage = useCallback(() => { + if (!src || loadingRef.current) return; + + loadingRef.current = true; + const img = new Image(); + img.crossOrigin = 'anonymous'; + + img.onload = () => { + imageRef.current = img; + setStatus(prev => ({ + ...prev, + isLoading: false, + hasError: false, + timeoutError: false + })); + startAnimationCycle(); + }; + + img.onerror = () => { + setStatus(prev => ({ + ...prev, + isLoading: false, + hasError: true, + timeoutError: false + })); + }; + + img.src = src; + }, [src, startAnimationCycle]); + + useEffect(() => { + preloadImage(); + return () => { + loadingRef.current = false; + }; + }, [preloadImage]); + + return ( +
+
+ { + setStatus(prev => ({ ...prev, animationPhase: phase })); + }} + /> +
+ {!status.hasError && !status.timeoutError && imageRef.current && ( +
+ {alt} +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/web/czr/hooks/error.tsx b/web/czr/hooks/error.tsx new file mode 100644 index 0000000..fad69d9 --- /dev/null +++ b/web/czr/hooks/error.tsx @@ -0,0 +1,42 @@ +import React, { useState, useEffect } from "react"; +import { Template } from "interface/template"; + +export default new Template({}, ({ args }) => { + const [text, setText] = useState(""); + const fullText = "404 - 页面不见了 :("; + const typingSpeed = 100; + + useEffect(() => { + let currentIndex = 0; + const typingEffect = setInterval(() => { + if (currentIndex < fullText.length) { + setText(fullText.slice(0, currentIndex + 1)); + currentIndex++; + } else { + clearInterval(typingEffect); + } + }, typingSpeed); + + return () => clearInterval(typingEffect); + }, []); + + return ( +
+
+

+ {text} + | +

+

+ 抱歉,您访问的页面已经离家出走了 +

+ +
+
+ ); +}); diff --git a/web/czr/hooks/loading.tsx b/web/czr/hooks/loading.tsx new file mode 100644 index 0000000..848f2b0 --- /dev/null +++ b/web/czr/hooks/loading.tsx @@ -0,0 +1,103 @@ +import React, { createContext, useState, useContext } from "react"; + +interface LoadingContextType { + isLoading: boolean; + showLoading: () => void; + hideLoading: () => void; +} + +const LoadingContext = createContext({ + isLoading: false, + showLoading: () => {}, + hideLoading: () => {}, +}); + +export const LoadingProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [isLoading, setIsLoading] = useState(false); + + const showLoading = () => setIsLoading(true); + const hideLoading = () => setIsLoading(false); + + return ( + + {children} + {isLoading && ( +
+
+
+ 加载中... +
+
+ )} + + + ); +}; + +// 全局loading实例 +let globalShowLoading: (() => void) | null = null; +let globalHideLoading: (() => void) | null = null; + +export const LoadingContainer: React.FC = () => { + const { showLoading, hideLoading } = useContext(LoadingContext); + + React.useEffect(() => { + globalShowLoading = showLoading; + globalHideLoading = hideLoading; + return () => { + globalShowLoading = null; + globalHideLoading = null; + }; + }, [showLoading, hideLoading]); + + return null; +}; + +// 导出loading方法 +export const loading = { + show: () => { + if (!globalShowLoading) { + console.warn("Loading system not initialized"); + return; + } + globalShowLoading(); + }, + hide: () => { + if (!globalHideLoading) { + console.warn("Loading system not initialized"); + return; + } + globalHideLoading(); + }, +}; diff --git a/web/czr/hooks/notification.tsx b/web/czr/hooks/notification.tsx new file mode 100644 index 0000000..d5e39b1 --- /dev/null +++ b/web/czr/hooks/notification.tsx @@ -0,0 +1,182 @@ +// @ts-nocheck +import React, { createContext, useState, useContext } from "react"; +import { Button, Flex, Card, Text, Box } from "@radix-ui/themes"; +import { + CheckCircledIcon, + CrossCircledIcon, + InfoCircledIcon, +} from "@radix-ui/react-icons"; + +// 定义通知类型枚举 +export enum NotificationType { + SUCCESS = "success", + ERROR = "error", + INFO = "info", +} + +// 通知类型定义 +type Notification = { + id: string; + type: NotificationType; + title: string; + message?: string; +}; + +// 通知配置类型定义 +type NotificationConfig = { + icon: React.ReactNode; + bgColor: string; +}; + +// 通知配置映射 +const notificationConfigs: Record = { + [NotificationType.SUCCESS]: { + icon: , + bgColor: "bg-[rgba(0,168,91,0.85)]", + }, + [NotificationType.ERROR]: { + icon: , + bgColor: "bg-[rgba(225,45,57,0.85)]", + }, + [NotificationType.INFO]: { + icon: , + bgColor: "bg-[rgba(38,131,255,0.85)]", + }, +}; + +// 修改通知上下文类型定义 +type NotificationContextType = { + show: (type: NotificationType, title: string, message?: string) => void; + success: (title: string, message?: string) => void; + error: (title: string, message?: string) => void; + info: (title: string, message?: string) => void; +}; + +const NotificationContext = createContext({ + show: () => {}, + success: () => {}, + error: () => {}, + info: () => {}, +}); + +// 简化全局 toast 对象定义 +export const toast: NotificationContextType = { + show: () => {}, + success: () => {}, + error: () => {}, + info: () => {}, +}; + +export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [notifications, setNotifications] = useState([]); + + // 统一参数顺序:title 在前,message 在后 + const show = (type: NotificationType, title: string, message?: string) => { + const id = Math.random().toString(36).substring(2, 9); + const newNotification = { id, type, title, message }; + + setNotifications((prev) => [...prev, newNotification]); + + setTimeout(() => { + setNotifications((prev) => + prev.filter((notification) => notification.id !== id), + ); + }, 3000); + }; + + // 简化快捷方法定义 + const contextValue = { + show, + success: (title: string, message?: string) => + show(NotificationType.SUCCESS, title, message), + error: (title: string, message?: string) => + show(NotificationType.ERROR, title, message), + info: (title: string, message?: string) => + show(NotificationType.INFO, title, message), + }; + + // 初始化全局方法 + React.useEffect(() => { + Object.assign(toast, contextValue); + }, []); + + const closeNotification = (id: string) => { + setNotifications((prev) => + prev.filter((notification) => notification.id !== id), + ); + }; + + return ( + + {notifications.length > 0 && ( + + {notifications.map((notification) => ( + + + + + + + {notificationConfigs[notification.type].icon} + + {notification.title && ( + + {notification.title} + + )} + + + {notification.message} + + +
+
+
+ + + ))} + + )} + {children} + + ); +}; + +// 导出hook +export const useNotification = () => { + const context = useContext(NotificationContext); + if (!context) { + throw new Error( + "useNotification must be used within a NotificationProvider", + ); + } + return context; +}; diff --git a/web/czr/hooks/themeMode.tsx b/web/czr/hooks/themeMode.tsx new file mode 100644 index 0000000..d6f24b5 --- /dev/null +++ b/web/czr/hooks/themeMode.tsx @@ -0,0 +1,133 @@ +import React, { useState, useEffect } from "react"; +import { MoonIcon, SunIcon } from "@radix-ui/react-icons"; +import { Button } from "@radix-ui/themes"; + +const THEME_KEY = "theme-preference"; + +// 添加这个脚本来预先设置主题,避免闪烁 +const themeScript = ` + (function() { + function getInitialTheme() { + const savedTheme = localStorage.getItem("${THEME_KEY}"); + if (savedTheme) return savedTheme; + + const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + const theme = isDark ? "dark" : "light"; + localStorage.setItem("${THEME_KEY}", theme); + return theme; + } + document.documentElement.className = getInitialTheme(); + })() +`; + +export const ThemeScript = () => { + return + + \ No newline at end of file diff --git a/web/czr/package.json b/web/czr/package.json new file mode 100644 index 0000000..d071521 --- /dev/null +++ b/web/czr/package.json @@ -0,0 +1,81 @@ +{ + "name": "frontend", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "remix vite:dev", + "build": "vite build", + "preview": "vite preview", + "serve:static": "node server/static.js", + "typecheck": "tsc", + "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .", + "format": "prettier --write .", + "clean": "rm -rf dist", + "start": "node server/static.js", + "generate:html": "node server/entry.server.js" + }, + "dependencies": { + "@heroicons/react": "^2.2.0", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/themes": "^3.1.6", + "@remix-run/node": "^2.14.0", + "@remix-run/react": "^2.14.0", + "@remix-run/serve": "^2.14.0", + "@tailwindcss/typography": "^0.5.15", + "@types/axios": "^0.14.4", + "@types/react-syntax-highlighter": "^15.5.13", + "@types/three": "^0.170.0", + "axios": "^1.7.7", + "bootstrap-icons": "^1.11.3", + "cors": "^2.8.5", + "express": "^4.21.1", + "gsap": "^3.12.5", + "html-react-parser": "^5.1.19", + "isbot": "^4.1.0", + "r": "^0.0.5", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-markdown": "^9.0.1", + "react-syntax-highlighter": "^15.6.1", + "rehype-raw": "^7.0.0", + "three": "^0.171.0" + }, + "devDependencies": { + "@remix-run/dev": "^2.14.0", + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/lodash": "^4.17.13", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "autoprefixer": "^10.4.19", + "concurrently": "^9.1.0", + "cross-env": "^7.0.3", + "eslint": "^8.57.1", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "postcss": "^8.4.38", + "prettier": "^3.3.3", + "tailwindcss": "^3.4.4", + "tsx": "^4.19.2", + "typescript": "^5.1.6", + "vite": "^5.4.11", + "vite-tsconfig-paths": "^4.2.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "markdownlint-config": { + "$schema": null + }, + "remix": { + "future": { + "v3_lazyRouteDiscovery": true + } + } +} diff --git a/web/czr/postcss.config.js b/web/czr/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/web/czr/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/web/czr/server/entry.server.js b/web/czr/server/entry.server.js new file mode 100644 index 0000000..fd8609e --- /dev/null +++ b/web/czr/server/entry.server.js @@ -0,0 +1,42 @@ +import { renderToString } from 'react-dom/server'; +import { RemixServer } from '@remix-run/react'; +import { createReadStream, createWriteStream } from 'fs'; +import { mkdir } from 'fs/promises'; +import { dirname, resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const projectRoot = resolve(__dirname, '..'); + +async function generateHTML() { + try { + const distDir = resolve(projectRoot, 'dist'); + await mkdir(distDir, { recursive: true }); + + const html = ` + + + + + + Your App + + +
+ + +`; + + const indexPath = resolve(distDir, 'index.html'); + const writer = createWriteStream(indexPath); + writer.write(html); + writer.end(); + + console.log('HTML file generated successfully at:', indexPath); + } catch (error) { + console.error('Error generating HTML:', error); + process.exit(1); + } +} + +generateHTML(); \ No newline at end of file diff --git a/web/czr/server/env.ts b/web/czr/server/env.ts new file mode 100644 index 0000000..c4c71ef --- /dev/null +++ b/web/czr/server/env.ts @@ -0,0 +1,32 @@ +import fs from "fs/promises"; +import path from "path"; + +export async function readEnvFile() { + const envPath = path.resolve(process.cwd(), ".env"); + try { + const content = await fs.readFile(envPath, "utf-8"); + return content.split("\n").reduce( + (acc, line) => { + const [key, value] = line.split("=").map((s) => s.trim()); + if (key && value) { + acc[key] = value.replace(/["']/g, ""); + } + return acc; + }, + {} as Record, + ); + } catch { + return {}; + } +} + +export async function writeEnvFile(env: Record) { + const envPath = path.resolve(process.cwd(), ".env"); + const content = Object.entries(env) + .map( + ([key, value]) => + `${key}=${typeof value === "string" ? `"${value}"` : value}`, + ) + .join("\n"); + await fs.writeFile(envPath, content, "utf-8"); +} diff --git a/web/czr/server/express.ts b/web/czr/server/express.ts new file mode 100644 index 0000000..385a3ca --- /dev/null +++ b/web/czr/server/express.ts @@ -0,0 +1,71 @@ +import express from "express"; +import cors from "cors"; +import { DEFAULT_CONFIG } from "../app/env"; +import { readEnvFile, writeEnvFile } from "./env"; + +const app = express(); +const address = process.env.VITE_ADDRESS ?? DEFAULT_CONFIG.VITE_ADDRESS; +const port = Number(process.env.VITE_PORT ?? DEFAULT_CONFIG.VITE_PORT); + +const ALLOWED_ORIGIN = `http://${address}:${port}`; +// 配置 CORS,只允许来自 Vite 服务器的请求 +app.use( + cors({ + origin: (origin, callback) => { + if (!origin || origin === ALLOWED_ORIGIN) { + callback(null, true); + } else { + callback(new Error("不允许的来源")); + } + }, + credentials: true, + }), +); + +// 添加 IP 和端口检查中间件 +const checkAccessMiddleware = ( + req: express.Request, + res: express.Response, + next: express.NextFunction, +) => { + const clientIp = req.ip === "::1" ? "localhost" : req.ip; + const clientPort = Number(req.get("origin")?.split(":").pop() ?? 0); + + const isLocalIp = clientIp === "localhost" || clientIp === "127.0.0.1"; + const isAllowedPort = clientPort === port; + + if (isLocalIp && isAllowedPort) { + next(); + } else { + res.status(403).json({ + error: "禁止访问", + detail: `仅允许 ${address}:${port} 访问`, + }); + } +}; + +app.use(checkAccessMiddleware); +app.use(express.json()); + +app.get("/env", async (req, res) => { + try { + const envData = await readEnvFile(); + res.json(envData); + } catch (error) { + res.status(500).json({ error: "读取环境变量失败" }); + } +}); + +app.post("/env", async (req, res) => { + try { + const newEnv = req.body; + await writeEnvFile(newEnv); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ error: "更新环境变量失败" }); + } +}); + +app.listen(port + 1, address, () => { + console.log(`内部服务器运行在 http://${address}:${port + 1}`); +}); diff --git a/web/czr/server/production.js b/web/czr/server/production.js new file mode 100644 index 0000000..f1959c1 --- /dev/null +++ b/web/czr/server/production.js @@ -0,0 +1,23 @@ +import express from 'express'; +import { createRequestHandler } from "@remix-run/express"; +import * as build from "../build/server/index.js"; + +const app = express(); + +// 静态文件服务 +app.use(express.static("public")); +app.use(express.static("build/client")); + +// Remix 请求处理 +app.all( + "*", + createRequestHandler({ + build, + mode: "production", + }) +); + +const port = process.env.PORT || 3000; +app.listen(port, () => { + console.log(`Express server listening on port ${port}`); +}); \ No newline at end of file diff --git a/web/czr/server/static.js b/web/czr/server/static.js new file mode 100644 index 0000000..3a8cc70 --- /dev/null +++ b/web/czr/server/static.js @@ -0,0 +1,31 @@ +import express from 'express'; +import { dirname, resolve } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const projectRoot = resolve(__dirname, '..'); + +const app = express(); +const port = process.env.PORT || 3000; + +// 设置静态文件目录 +app.use(express.static(resolve(projectRoot, 'dist'))); + +// 所有路由都返回 index.html +app.get('*', (req, res) => { + res.sendFile(resolve(projectRoot, 'dist', 'index.html')); +}); + +// 确保dist目录存在 +import { mkdir } from 'fs/promises'; +try { + await mkdir(resolve(projectRoot, 'dist'), { recursive: true }); +} catch (error) { + console.error('Error creating dist directory:', error); +} + +app.listen(port, () => { + console.log(`Server running at http://localhost:${port}`); + console.log('Static files directory:', resolve(projectRoot, 'dist')); + console.log('Index file path:', resolve(projectRoot, 'dist', 'index.html')); +}); \ No newline at end of file diff --git a/web/czr/src/entry.client.tsx b/web/czr/src/entry.client.tsx new file mode 100644 index 0000000..c1d76b4 --- /dev/null +++ b/web/czr/src/entry.client.tsx @@ -0,0 +1,7 @@ +import { RemixBrowser } from "@remix-run/react"; +import { startTransition } from "react"; +import { hydrateRoot } from "react-dom/client"; + +startTransition(() => { + hydrateRoot(document, ); +}); \ No newline at end of file diff --git a/web/czr/start.bat b/web/czr/start.bat new file mode 100644 index 0000000..cbbc232 --- /dev/null +++ b/web/czr/start.bat @@ -0,0 +1,79 @@ +@echo off +title CZR启动程序 +echo 正在启动程序... + +:: 创建日志文件 +echo %date% %time% > startup_log.txt +echo =============== 启动日志 =============== >> startup_log.txt + +:: 获取批处理文件所在目录 +cd /d "%~dp0" +echo 当前目录: %CD% >> startup_log.txt + +:: 检查czr目录是否存在 +if not exist "czr" ( + echo 错误:找不到czr文件夹! >> startup_log.txt + echo 错误:找不到czr文件夹! + echo 当前目录是:%CD% + echo 请确保start.bat文件与czr文件夹在同一目录 + type startup_log.txt + pause + exit /b 1 +) + +:: 切换到czr目录 +cd czr +echo 已进入czr目录: %CD% >> startup_log.txt + +:: 检查package.json是否存在 +if not exist "package.json" ( + echo 错误:在czr目录中找不到package.json文件! >> startup_log.txt + echo 错误:在czr目录中找不到package.json文件! + echo 当前目录是:%CD% + type startup_log.txt + pause + exit /b 1 +) + +:: 检查是否安装了Node.js +where npm >nul 2>nul +if %ERRORLEVEL% neq 0 ( + echo 错误:未安装Node.js或npm! >> startup_log.txt + echo 错误:未安装Node.js或npm! + echo 请先安装Node.js: https://nodejs.org/ + type startup_log.txt + pause + exit /b 1 +) + +:: 检查node_modules是否存在 +if not exist "node_modules" ( + echo node_modules文件夹不存在,正在安装依赖... >> startup_log.txt + echo node_modules文件夹不存在,正在安装依赖... + call npm install + if %ERRORLEVEL% neq 0 ( + echo npm install 失败! >> startup_log.txt + echo npm install 失败! + type startup_log.txt + pause + exit /b 1 + ) +) + +:: 执行npm run start +echo 正在执行 npm run start... >> startup_log.txt +echo 正在执行 npm run start... +call npm run start + +:: 如果npm命令执行失败 +if %ERRORLEVEL% neq 0 ( + echo 执行npm run start时出错! >> startup_log.txt + echo 执行npm run start时出错! + echo 错误代码:%ERRORLEVEL% + type startup_log.txt + pause + exit /b 1 +) + +type startup_log.txt +pause \ No newline at end of file diff --git a/web/czr/styles/echoes.css b/web/czr/styles/echoes.css new file mode 100644 index 0000000..b2f9ddf --- /dev/null +++ b/web/czr/styles/echoes.css @@ -0,0 +1,88 @@ +.animated-text { + max-width: 100%; + height: auto; + transform: translateZ(0); + -webkit-transform: translateZ(0); + backface-visibility: hidden; + -webkit-backface-visibility: hidden; + will-change: transform; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + .animated-text path { + fill: transparent; + stroke: currentColor; + stroke-width: 2; + stroke-dasharray: var(--path-length); + stroke-dashoffset: var(--path-length); + animation: logo-anim 10s cubic-bezier(0.4, 0, 0.2, 1) infinite; + transform-origin: center; + stroke-linecap: round; + stroke-linejoin: round; + animation-play-state: running !important; + will-change: transform; + } + + @keyframes logo-anim { + 0% { + stroke-dashoffset: var(--path-length); + stroke-dasharray: var(--path-length) var(--path-length); + fill: transparent; + opacity: 0; + } + + 5% { + opacity: 1; + stroke-dashoffset: var(--path-length); + stroke-dasharray: var(--path-length) var(--path-length); + } + + 50% { + stroke-dashoffset: 0; + stroke-dasharray: var(--path-length) var(--path-length); + fill: transparent; + } + + 60%, 75% { + stroke-dashoffset: 0; + stroke-dasharray: var(--path-length) var(--path-length); + fill: currentColor; + opacity: 1; + } + + 85% { + stroke-dashoffset: 0; + stroke-dasharray: var(--path-length) var(--path-length); + fill: transparent; + opacity: 1; + } + + 95% { + stroke-dashoffset: var(--path-length); + stroke-dasharray: var(--path-length) var(--path-length); + fill: transparent; + opacity: 1; + } + + 100% { + stroke-dashoffset: var(--path-length); + stroke-dasharray: var(--path-length) var(--path-length); + fill: transparent; + opacity: 0; + } + } + + @media (prefers-color-scheme: dark) { + .animated-text path { + stroke: currentColor; + } + } + + @media (hover: none) and (pointer: coarse) { + .animated-text { + touch-action: manipulation; + cursor: pointer; + } + } + \ No newline at end of file diff --git a/web/czr/tailwind.config.ts b/web/czr/tailwind.config.ts new file mode 100644 index 0000000..4af7f84 --- /dev/null +++ b/web/czr/tailwind.config.ts @@ -0,0 +1,36 @@ +import type { Config } from "tailwindcss"; +import typography from '@tailwindcss/typography'; + +export default { + content: [ + "./app/**/*.{js,jsx,ts,tsx}", + "./common/**/*.{js,jsx,ts,tsx}", + "./core/**/*.{js,jsx,ts,tsx}", + "./hooks/**/*.{js,jsx,ts,tsx}", + "./themes/**/*.{js,jsx,ts,tsx}", + ], + darkMode: 'class', + important: true, + theme: { + extend: { + fontFamily: { + sans: ["Inter", "system-ui", "sans-serif"], + }, + keyframes: { + progress: { + from: { transform: "scaleX(1)" }, + to: { transform: "scaleX(0)" }, + }, + }, + animation: { + progress: "progress 3s linear", + }, + zIndex: { + "-10": "-10", + }, + }, + }, + plugins: [ + typography, + ], +} satisfies Config; diff --git a/web/czr/tsconfig.json b/web/czr/tsconfig.json new file mode 100644 index 0000000..952cc89 --- /dev/null +++ b/web/czr/tsconfig.json @@ -0,0 +1,32 @@ +{ + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.tsx" + ], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["@remix-run/node", "vite/client"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["app/*"] + }, + + // Vite takes care of building everything, not tsc. + "noEmit": true + } +} diff --git a/web/czr/vite.config.ts b/web/czr/vite.config.ts new file mode 100644 index 0000000..c5a1ea1 --- /dev/null +++ b/web/czr/vite.config.ts @@ -0,0 +1,39 @@ +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + v3_singleFetch: true, + } + }), + tsconfigPaths(), + ], + build: { + outDir: 'dist', + assetsDir: 'assets', + chunkSizeWarningLimit: 1000, + rollupOptions: { + output: { + manualChunks: { + vendor: [ + 'react', + 'react-dom', + 'three', + 'gsap' + ], + ui: ['@radix-ui/themes', '@radix-ui/react-icons'], + }, + assetFileNames: 'assets/[name]-[hash][extname]', + chunkFileNames: 'assets/[name]-[hash].js', + entryFileNames: 'assets/[name]-[hash].js' + } + } + }, + base: '', +})