更新cxm,ccr以及其他未更新的

This commit is contained in:
lsy 2024-12-10 10:28:04 +08:00
parent 014aec6757
commit 639a0fd504
88 changed files with 5226 additions and 284 deletions

View File

@ -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"

12
rust/blog_os/Cargo.toml Normal file
View File

@ -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 时栈展开

15
rust/blog_os/src/main.rs Normal file
View File

@ -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 {}
}

View File

@ -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"
}

View File

@ -210,8 +210,8 @@
fill: transparent; fill: transparent;
stroke: currentColor; stroke: currentColor;
stroke-width: 2; stroke-width: 2;
stroke-dasharray: var(--path-length);
stroke-dashoffset: 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; animation: logo-anim 15s cubic-bezier(0.4, 0, 0.2, 1) infinite;
transform-origin: center; transform-origin: center;
stroke-linecap: round; stroke-linecap: round;

View File

@ -56,7 +56,7 @@ const App: React.FC = () => {
}; };
const handleDownloadCss = () => { const handleDownloadCss = () => {
const animationCss = ` const animationCss = `/* 注意:每个路径都需要在 SVG 中设置 --path-length 变量 */
.animated-text { .animated-text {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
@ -66,8 +66,9 @@ const App: React.FC = () => {
fill: transparent; fill: transparent;
stroke: currentColor; stroke: currentColor;
stroke-width: 2; stroke-width: 2;
/* 使用每个路径自己的长度 */
stroke-dasharray: var(--path-length);
stroke-dashoffset: 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; animation: logo-anim 15s cubic-bezier(0.4, 0, 0.2, 1) infinite;
transform-origin: center; transform-origin: center;
stroke-linecap: round; stroke-linecap: round;
@ -88,14 +89,12 @@ const App: React.FC = () => {
stroke-dasharray: var(--path-length) var(--path-length); stroke-dasharray: var(--path-length) var(--path-length);
} }
/* 慢速绘画过程 */
50% { 50% {
stroke-dashoffset: 0; stroke-dashoffset: 0;
stroke-dasharray: var(--path-length) var(--path-length); stroke-dasharray: var(--path-length) var(--path-length);
fill: transparent; fill: transparent;
} }
/* 慢慢填充效果 */
60% { 60% {
stroke-dashoffset: 0; stroke-dashoffset: 0;
stroke-dasharray: var(--path-length) var(--path-length); stroke-dasharray: var(--path-length) var(--path-length);
@ -103,7 +102,6 @@ const App: React.FC = () => {
opacity: 1; opacity: 1;
} }
/* 保持填充状态 */
75% { 75% {
stroke-dashoffset: 0; stroke-dashoffset: 0;
stroke-dasharray: var(--path-length) var(--path-length); stroke-dasharray: var(--path-length) var(--path-length);
@ -111,7 +109,6 @@ const App: React.FC = () => {
opacity: 1; opacity: 1;
} }
/* 变回线条 */
85% { 85% {
stroke-dashoffset: 0; stroke-dashoffset: 0;
stroke-dasharray: var(--path-length) var(--path-length); stroke-dasharray: var(--path-length) var(--path-length);
@ -119,7 +116,6 @@ const App: React.FC = () => {
opacity: 1; opacity: 1;
} }
/* 线条消失 */
95% { 95% {
stroke-dashoffset: var(--path-length); stroke-dashoffset: var(--path-length);
stroke-dasharray: var(--path-length) var(--path-length); stroke-dasharray: var(--path-length) var(--path-length);
@ -133,6 +129,13 @@ const App: React.FC = () => {
fill: transparent; fill: transparent;
opacity: 0; opacity: 0;
} }
}
/* 确保在暗色模式下的颜色正确 */
@media (prefers-color-scheme: dark) {
.animated-text path {
stroke: currentColor;
}
}`; }`;
const blob = new Blob([animationCss], { type: 'text/css' }); const blob = new Blob([animationCss], { type: 'text/css' });

View File

@ -210,7 +210,8 @@ impl FontHandler {
paths.push(Path::new() paths.push(Path::new()
.set("d", builder.path_data) .set("d", builder.path_data)
.set("fill", "currentColor")); .set("fill", "currentColor")
.set("style", format!("--path-length: {};", builder.path_length)));
} }
} }

1
rust/svg/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

6
rust/temp/Cargo.toml Normal file
View File

@ -0,0 +1,6 @@
[package]
name = "temp"
version = "0.1.0"
edition = "2021"
[dependencies]

View File

@ -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<Vec<Route>>,
}
impl Server {
pub fn new(listener_port:i32) -> Arc<Self> {
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<Self>) {
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<Self>,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<Self>, path: &'static str, method: &'static str, function: impl Fn(Request, Arc<Respond>) + 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);
}
}

View File

@ -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();
}

View File

@ -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<String, String>,
}
impl Request{
pub fn build(content:String) -> Option<Request>{
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::<String, String>::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
}
}

View File

@ -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<Mutex<TcpStream>>
}
impl Respond<'_> {
pub fn build(socket: TcpStream) -> Arc<Respond<'static>> {
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<Self>) -> 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<Self>) {
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");
}
});
}
}

View File

@ -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<dyn Fn(Request,Arc<Respond>) + Send + Sync>,
}

View File

@ -32,16 +32,12 @@ const renderLoop = () => {
const drawGrid = () => { const drawGrid = () => {
ctx.beginPath(); ctx.beginPath();
ctx.strokeStyle = GRID_COLOR; ctx.strokeStyle = GRID_COLOR;
// 画垂直线
for (let i = 0; i <= width; i++) { for (let i = 0; i <= width; i++) {
ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0); ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0);
ctx.lineTo(i * (CELL_SIZE + 1) + 1, (CELL_SIZE + 1) * height + 1); ctx.lineTo(i * (CELL_SIZE + 1) + 1, (CELL_SIZE + 1) * height + 1);
} }
// 画水平线
for (let j = 0; j <= height; j++) { 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); ctx.lineTo((CELL_SIZE + 1) * width + 1, j * (CELL_SIZE + 1) + 1);
} }

50
web/chess/README.md Normal file
View File

@ -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,
},
})
```

View File

@ -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 },
],
},
},
)

13
web/chess/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -189,6 +189,7 @@ const Game: React.FC = () => {
useEffect(()=>{ useEffect(()=>{
drawChess(chessGame); drawChess(chessGame);
console.log(chessGame?.render())
},[]) },[])
function chessBoardClick(e: React.MouseEvent<HTMLCanvasElement>) { function chessBoardClick(e: React.MouseEvent<HTMLCanvasElement>) {
const client=e.currentTarget.getBoundingClientRect(); const client=e.currentTarget.getBoundingClientRect();

View File

@ -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"]
}

7
web/chess/tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -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"]
}

7
web/chess/vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()]
});

50
web/cxm/README.md Normal file
View File

@ -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,
},
})
```

28
web/cxm/eslint.config.js Normal file
View File

@ -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 },
],
},
},
)

13
web/cxm/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

35
web/cxm/package.json Normal file
View File

@ -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"
}
}

54
web/cxm/src/App.css Normal file
View File

@ -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;
}

3
web/cxm/src/App.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
import './App.css';
declare function App(): import("react/jsx-runtime").JSX.Element;
export default App;

7
web/cxm/src/App.js Normal file
View File

@ -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;

12
web/cxm/src/App.tsx Normal file
View File

@ -0,0 +1,12 @@
import { Parallax } from './components/Parallax';
import './App.css';
function App() {
return (
<div className="App">
<Parallax />
</div>
);
}
export default App;

BIN
web/cxm/src/assets/z.woff2 Normal file

Binary file not shown.

View File

@ -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;
}

View File

@ -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 {};

View File

@ -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" })] }) }));
};

View File

@ -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 (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<button className="modal-close" onClick={onClose}>×</button>
<img src={image} alt="" className="modal-image" />
</div>
</div>
);
};

View File

@ -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;
}

2
web/cxm/src/components/Parallax.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
import './Parallax.css';
export declare const Parallax: () => import("react/jsx-runtime").JSX.Element;

File diff suppressed because one or more lines are too long

View File

@ -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<HTMLDivElement>(null);
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [displayedCards, setDisplayedCards] = useState<string[]>(() => {
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<typeof setInterval>;
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 (
<div className="parallax">
<section className="parallax__header">
<div className="parallax__visuals">
<div className="parallax__black-line-overflow"></div>
<div ref={layersRef} data-parallax-layers className="parallax__layers">
<img
src={layer3}
loading="eager"
width="800"
data-parallax-layer="1"
alt=""
className="parallax__layer-img"
/>
<img
src={layer2}
loading="eager"
width="800"
data-parallax-layer="2"
alt=""
className="parallax__layer-img"
/>
<div data-parallax-layer="3" className="parallax__layer-title">
<h2 className="parallax__title"></h2>
</div>
<img
src={layer1}
loading="eager"
width="800"
data-parallax-layer="4"
alt=""
className="parallax__layer-img"
/>
</div>
<div className="parallax__fade"></div>
</div>
</section>
<section className="parallax__content">
<div className="travel-grid">
<div className="travel-item" onClick={() => setSelectedImage(xizhang)}>
<h3>西</h3>
<p>4000</p>
</div>
<div className="travel-item" onClick={() => setSelectedImage(shichuan)}>
<h3></h3>
<p></p>
</div>
<div className="travel-item" onClick={() => setSelectedImage(chongqing)}>
<h3></h3>
<p>穿</p>
</div>
<div className="travel-item" onClick={() => setSelectedImage(haerbing)}>
<h3></h3>
<p>20</p>
</div>
<div className="travel-item" onClick={() => setSelectedImage(changbaishan)}>
<h3></h3>
<p></p>
</div>
<div className="travel-item" onClick={() => setSelectedImage(qindao)}>
<h3></h3>
<p></p>
</div>
<div className="travel-item" onClick={() => setSelectedImage(qinghuangdao)}>
<h3></h3>
<p>线</p>
</div>
<div className="travel-item" onClick={() => setSelectedImage(tianjin)}>
<h3></h3>
<p>西</p>
</div>
<div className="travel-item" onClick={() => setSelectedImage(taian)}>
<h3></h3>
<p></p>
</div>
</div>
{selectedImage && (
<ImageModal
image={selectedImage}
onClose={() => setSelectedImage(null)}
/>
)}
</section>
<section className="slider-section">
<div className="circle-decoration circle-left"></div>
<div className="circle-decoration circle-right"></div>
<div className="number-decoration number-top">01</div>
<div className="number-decoration number-bottom">21</div>
<div className="side-text side-text-left">
TRAVEL MEMORIES ·
</div>
<div className="side-text side-text-right">
PHOTO COLLECTION ·
</div>
<div className="year-display">
2022 - 2024
</div>
<div className="container">
<div className="slider">
{displayedCards.map((img, index) => (
<div className="card" key={index}>
<img src={img} alt="" />
</div>
))}
</div>
</div>
</section>
</div>
);
};

98
web/cxm/src/index.css Normal file
View File

@ -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;
}

1
web/cxm/src/main.d.ts vendored Normal file
View File

@ -0,0 +1 @@
import './index.css';

6
web/cxm/src/main.js Normal file
View File

@ -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, {}) }));

10
web/cxm/src/main.tsx Normal file
View File

@ -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(
<StrictMode>
<App />
</StrictMode>,
)

1
web/cxm/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

13
web/cxm/tsconfig.app.json Normal file
View File

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"paths": {
"@babel/*": ["./node_modules/@babel/*"]
}
},
"include": ["src"]
}

View File

@ -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"}

7
web/cxm/tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"module": "ESNext",
"target": "ES2015",
"moduleResolution": "bundler"
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1 @@
{"root":["./vite.config.ts"],"errors":true,"version":"5.6.3"}

2
web/cxm/vite.config.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
declare const _default: import("vite").UserConfig;
export default _default;

6
web/cxm/vite.config.js Normal file
View File

@ -0,0 +1,6 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
});

7
web/cxm/vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})

84
web/czr/.eslintrc.cjs Normal file
View File

@ -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,
},
},
],
};

View File

@ -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 (
<div className="relative">
<div className="flex justify-center items-center">
<div>
{items[currentIndex].content}
</div>
</div>
</div>
);
}

View File

@ -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 (
<nav className="nav-container">
<div className="nav-content">
<Link to="/" className="nav-logo">
<svg
className="animated-text"
preserveAspectRatio="xMidYMid meet"
viewBox="47.4 25.200005 455.2 109.8"
xmlns="http://www.w3.org/2000/svg"
style={{
width: '120px', // 设置固定宽度
height: 'auto', // 高度自动调整保持比例
color: '#1a1a1a' // 设置logo颜色
}}
>
<path
d="M 119.200005 86.90001 L 120.9 87.70001 L 120.700005 98.600006 Q 120.3 109.600006 120.3 114.00001 Q 120.3 118.00001 119.6 120.850006 Q 118.9 123.700005 117.9 123.700005 Q 117 123.700005 117 124.350006 Q 117 125.00001 115.450005 124.850006 Q 113.9 124.700005 114.25 123.65001 Q 114.6 122.600006 114.3 105.50001 Q 114.200005 94.00001 114 90.90001 Q 113.8 87.8 112.8 86.600006 Q 111.600006 84.8 112 84.40001 Q 112.4 84.00001 114.8 85.00001 L 119.200005 86.90001 ZM 109.95 47.250008 Q 110.600006 47.100006 112.7 47.300003 Q 117 47.90001 118 48.300003 Q 119 48.700005 119 49.800003 Q 119 51.200005 111.5 58.800007 Q 106.7 63.800007 105.350006 65.55 Q 104 67.3 104 68.70001 Q 104 70.90001 104.55 71.25001 Q 105.100006 71.600006 105.55 75.40001 Q 106 79.20001 106.350006 79.20001 Q 106.7 79.20001 114.25 72.50001 Q 121.8 65.8 123.3 64.90001 Q 124.6 64.00001 127.9 63.900005 Q 131.20001 63.800007 132 64.600006 Q 132.8 65.40001 132.3 66.45001 Q 131.8 67.50001 130.70001 67.50001 Q 128.9 67.50001 124.05 71.20001 Q 119.200005 74.90001 114.3 79.90001 Q 107.9 86.40001 106.65 88.65001 Q 105.4 90.90001 105.4 95.40001 Q 105.4 100.3 104.2 101.40001 Q 103.100006 102.50001 102.2 101.950005 Q 101.3 101.40001 99.7 98.40001 Q 97.8 94.700005 96.2 93.40001 L 94.7 92.3 L 97 89.90001 Q 99.3 87.600006 100 84.8 Q 100.3 83.3 100.45 81.100006 Q 100.600006 78.90001 100.45 77.3 Q 100.3 75.70001 100 75.70001 Q 99.4 75.70001 95.4 83.8 L 91.3 91.700005 L 91.3 100.8 L 91.3 109.90001 L 89.100006 110.200005 Q 87.100006 110.700005 84.100006 108.90001 Q 78.3 105.600006 73.8 108.90001 Q 71.9 110.30001 70.25 110.50001 Q 68.6 110.700005 68.1 109.50001 Q 67.6 108.50001 67.6 106.15001 Q 67.6 103.8 69.15 103.200005 Q 70.7 102.600006 71.2 102.90001 Q 71.7 103.100006 77.05 97.3 Q 82.4 91.50001 83.3 89.700005 Q 84.4 87.40001 82.600006 87.70001 Q 81.2 87.8 78.3 89.40001 Q 73.8 91.90001 72.25 91.3 Q 70.7 90.700005 70.2 85.90001 Q 69.8 83.50001 70.4 81.95001 Q 71 80.40001 74.9 73.90001 Q 76.7 70.70001 78.4 67.00001 Q 80.1 63.300007 79.8 63.000008 Q 79.5 62.70001 76.45 64.90001 Q 73.4 67.100006 71.2 69.20001 Q 68.3 71.90001 66.55 75.20001 Q 64.8 78.50001 61.85 79.90001 Q 58.9 81.3 58.15 80.850006 Q 57.4 80.40001 57.4 76.90001 L 57.4 73.3 L 63.4 69.00001 Q 79.5 57.400005 82.4 54.90001 Q 84.2 53.200005 85.2 52.90001 Q 86.2 52.600006 89.5 52.90001 Q 92.8 53.300003 94.25 54.050003 Q 95.7 54.800003 95.7 56.400005 Q 95.7 57.20001 92.7 59.400005 Q 89.7 61.600006 88.5 63.900005 L 87.3 66.100006 L 89.8 68.600006 Q 92.4 71.00001 94.4 70.8 Q 95.9 70.70001 96.850006 69.600006 Q 97.8 68.50001 100.2 63.900005 Q 103.7 57.20001 105.850006 54.100006 Q 108 51.000008 108 50.300003 Q 108 49.600006 108.8 48.300003 Q 109.3 47.40001 109.95 47.250008 ZM 106.350006 58.600006 Q 106.4 58.300007 106.350006 58.100006 Q 106.3 57.900005 106.2 57.900005 Q 105.5 57.900005 104.7 59.70001 Q 104.2 60.900005 104.4 61.000008 Q 104.9 61.20001 105.8 59.70001 Q 105.9 59.500008 106 59.300007 Q 106.3 58.900005 106.350006 58.600006 ZM 84.8 71.600006 Q 84.2 71.600006 83.350006 74.350006 Q 82.5 77.100006 82.9 77.600006 Q 83.9 78.40001 84.9 76.40001 Q 85.5 75.100006 85.5 74.00001 Q 85.5 71.600006 84.8 71.600006 ZM 80.5 35.90001 Q 81.6 36.40001 84.2 37.500008 Q 87.3 39.000008 89.5 42.200005 Q 90.9 44.300003 91.100006 45.15001 Q 91.3 46.000008 90.9 47.500008 Q 90.4 48.800003 89.7 49.40001 Q 89 50.000008 87.4 50.40001 Q 84.7 51.200005 84.4 50.850006 Q 84.100006 50.500008 80.6 51.800003 Q 79.7 52.100006 78.95 52.350006 Q 78.2 52.600006 77.75 52.700005 Q 77.3 52.800003 77.3 52.700005 Q 77.3 52.600006 79.7 49.700005 Q 85.2 43.100006 80.3 38.500008 Q 78.7 37.000008 78.7 36.000008 Q 78.7 35.200005 80.5 35.90001 Z"
fill="currentColor"
style={{ "--path-length": "1019.60175" } as React.CSSProperties}
/>
<path
d="M 201.5 57.600006 Q 202.7 58.20001 205.5 59.000008 Q 208 59.600006 209.20001 61.150005 Q 210.4 62.70001 209.3 63.900005 Q 208.70001 64.90001 207.6 68.25001 Q 206.5 71.600006 205.1 73.40001 Q 204.20001 74.600006 204.20001 74.90001 Q 204.20001 75.20001 205.3 75.8 Q 206.6 76.50001 206.75 77.75001 Q 206.9 79.00001 205.8 79.70001 L 199.2 83.40001 Q 193.8 86.600006 192.1 87.100006 Q 190.9 87.3 190.35 88.350006 Q 189.8 89.40001 188.9 92.600006 Q 186.5 100.90001 188.1 101.90001 Q 188.6 102.200005 193.9 102.8 Q 199.2 103.40001 206.8 102.200005 Q 214.4 101.00001 216.5 99.40001 Q 217.3 98.700005 217.3 96.00001 Q 217.3 93.3 218.1 91.8 Q 218.70001 90.90001 218.9 90.8 Q 219.1 90.700005 219.6 91.40001 Q 220.20001 92.50001 223.3 94.00001 Q 224.5 94.50001 225.3 95.100006 Q 226.1 95.700005 225.9 96.00001 Q 225.6 96.700005 226.4 99.00001 Q 226.8 100.50001 226.75 101.25001 Q 226.70001 102.00001 226 103.200005 Q 223.5 107.00001 211.1 109.700005 Q 202.4 111.700005 193 110.600006 Q 183.6 109.50001 180.9 106.30001 Q 179.6 104.50001 179.75 101.05 Q 179.9 97.600006 181.5 94.90001 L 183.2 91.8 L 181.6 90.600006 Q 180.1 89.40001 180.1 86.00001 Q 180.1 83.600006 180.45 83.100006 Q 180.8 82.600006 182.9 81.90001 Q 185.7 81.00001 188.1 78.90001 Q 190.1 77.20001 193.1 73.00001 Q 196.1 68.8 196.6 66.8 Q 196.9 65.70001 196.7 65.55 Q 196.5 65.40001 195 66.40001 Q 192.9 67.50001 191.7 69.3 Q 190.7 70.70001 188.4 72.40001 Q 186.1 74.100006 185.1 74.100006 Q 184.2 74.100006 182.35 72.45001 Q 180.5 70.8 179.4 69.100006 Q 178.5 67.70001 178.55 67.50001 Q 178.6 67.3 180 67.3 Q 181.5 67.3 185.4 65.90001 Q 189.3 64.50001 189.3 63.900005 Q 189.3 63.500008 191.65 62.250008 Q 194 61.000008 195.9 59.000008 Q 198.6 56.100006 201.5 57.600006 ZM 170.55 45.550003 Q 170.9 45.300003 172.8 45.500008 Q 176.9 46.200005 177.9 49.800003 Q 178.4 51.200005 173.9 56.600006 Q 169.4 62.000008 169.4 62.650005 Q 169.4 63.300007 165.4 67.50001 Q 158.5 74.90001 158.5 76.600006 Q 158.5 77.3 161.3 77.70001 Q 164.1 78.100006 164.4 78.40001 Q 164.7 78.70001 164 84.8 Q 163.6 89.00001 163.65 90.100006 Q 163.7 91.200005 164.6 91.90001 Q 165.9 92.90001 166.6 94.200005 Q 166.8 94.600006 167.05 94.700005 Q 167.3 94.8 167.8 94.450005 Q 168.3 94.100006 169.05 93.3 Q 169.8 92.50001 171.2 91.00001 Q 175.8 85.70001 175.5 87.100006 Q 175.4 87.600006 173.8 89.90001 Q 170.7 94.40001 167 101.90001 Q 163.3 109.40001 163.3 111.200005 Q 163.3 113.40001 162.05 114.350006 Q 160.8 115.30001 159.3 114.65001 Q 157.8 114.00001 157.5 111.90001 Q 157 110.100006 156.3 107.40001 Q 155.9 105.80001 156 104.80001 Q 156.1 103.8 157.1 101.90001 L 158.7 98.8 L 157.1 94.50001 Q 155.5 90.200005 155.6 87.600006 L 155.6 85.20001 L 153.1 86.3 Q 150.7 87.40001 148.4 89.00001 Q 146.7 90.3 145.4 90.450005 Q 144.1 90.600006 143.8 89.600006 Q 143.6 88.8 143.3 85.100006 Q 143.1 82.50001 143.4 81.75001 Q 143.7 81.00001 145.7 79.70001 Q 147.7 78.40001 150 76.100006 Q 152.3 73.8 157.6 68.00001 Q 161 64.100006 165.7 56.500008 Q 170.4 48.90001 170.2 47.200005 Q 170.2 45.800003 170.55 45.550003 Z"
fill="currentColor"
style={{ "--path-length": "659.9861" } as React.CSSProperties}
/>
<path
d="M 291.9 67.40001 Q 291.6 68.600006 290.3 69.3 Q 289.1 70.00001 285.05 75.00001 Q 281 80.00001 279.1 81.70001 Q 277.4 83.3 277.35 84.100006 Q 277.3 84.90001 278.5 86.40001 Q 280 88.100006 279.8 90.15001 Q 279.6 92.200005 277.3 98.3 Q 272.7 109.90001 276 110.600006 Q 278.3 111.100006 285.95 111.05001 Q 293.6 111.00001 295.7 110.40001 Q 298.1 109.80001 299.95 107.700005 Q 301.8 105.600006 302.2 103.00001 Q 302.8 100.00001 303.9 96.65001 Q 305 93.3 305.4 93.3 Q 306 93.3 306 95.100006 Q 306 96.90001 308.4 100.200005 Q 309.9 102.200005 310.4 103.50001 Q 310.9 104.80001 310.9 106.80001 Q 311 112.40001 307.35 114.80001 Q 303.7 117.200005 295.3 117.100006 Q 289.2 117.100006 282.6 117.200005 Q 278.5 117.30001 276.75 117.100006 Q 275 116.90001 273.4 116.00001 Q 271.6 115.200005 271 114.30001 Q 270.4 113.40001 269.9 111.100006 Q 269.2 108.50001 269.65 106.350006 Q 270.1 104.200005 271.2 104.200005 Q 271.8 104.200005 271.8 103.200005 Q 271.8 102.200005 273.2 98.00001 Q 274.6 93.8 275.2 93.00001 Q 275.9 91.90001 276.15 89.55 Q 276.4 87.20001 275.9 86.40001 Q 275.4 85.8 275.15 85.90001 Q 274.9 86.00001 274.2 87.00001 Q 273 88.600006 271.1 89.700005 Q 269.5 90.8 263.3 96.75001 Q 257.1 102.700005 252.5 107.700005 Q 249.9 110.700005 246.85 112.700005 Q 243.8 114.700005 241.85 115.00001 Q 239.9 115.30001 239.2 113.80001 Q 238.4 111.100006 244.8 106.200005 Q 248.8 103.200005 258.45 92.15001 Q 268.1 81.100006 267.4 80.50001 Q 267 80.20001 262.7 82.8 Q 259.8 84.600006 255.65 86.55 Q 251.5 88.50001 250.6 88.50001 Q 249.9 88.50001 248.85 86.25001 Q 247.8 84.00001 247.8 82.50001 Q 247.8 81.100006 248.2 80.600006 Q 248.6 80.100006 250.5 79.600006 Q 253.1 78.90001 255.5 76.50001 Q 259.7 72.50001 258.1 75.50001 Q 257.3 77.00001 258.2 76.8 Q 259 76.600006 261.6 75.100006 L 271.4 69.70001 Q 278.3 65.90001 281 63.150005 Q 283.7 60.400005 283.95 60.400005 Q 284.2 60.400005 286.25 62.150005 Q 288.3 63.900005 289.15 63.900005 Q 290 63.900005 291.2 65.100006 Q 292.2 66.20001 291.9 67.40001 ZM 284.45 43.300003 Q 286.6 42.90001 288.3 44.40001 Q 289.3 45.40001 289.7 45.000008 Q 290 44.700005 290.4 45.600006 Q 290.5 45.800003 290.6 46.000008 Q 290.8 47.000008 290.5 47.65001 Q 290.2 48.300003 288.8 49.40001 Q 286.1 51.600006 282.5 53.950005 Q 278.9 56.300007 278.35 56.300007 Q 277.8 56.300007 274.8 58.800007 Q 271.8 61.300007 269.2 63.900005 Q 267.2 66.100006 266.3 65.8 Q 266.1 65.70001 266.7 64.95001 Q 267.3 64.20001 268.45 62.900005 Q 269.6 61.600006 271.1 60.100006 Q 276.5 54.700005 276.5 54.000008 Q 276.6 52.600006 271.5 55.40001 Q 270.7 55.800003 269.8 56.400005 Q 263.1 60.400005 259.8 56.20001 Q 258.2 53.90001 258.2 53.40001 Q 258.2 52.90001 260 52.90001 Q 262 52.90001 265.15 51.700005 Q 268.3 50.500008 275.9 46.90001 Q 282.3 43.700005 284.45 43.300003 Z"
fill="currentColor"
style={{ "--path-length": "663.6744" } as React.CSSProperties}
/>
<path
d="M 358.65 38.750008 Q 358.9 39.300003 358.9 41.000008 Q 358.9 43.000008 357.75 45.950005 Q 356.6 48.90001 355.6 49.300003 Q 355 49.500008 351.85 53.250008 Q 348.7 57.000008 348.7 57.400005 Q 348.7 57.900005 351.4 59.100006 Q 355 60.70001 355.7 62.300007 Q 356.4 63.900005 355.9 69.90001 L 355.6 76.90001 L 359.2 72.90001 Q 363 68.70001 364.8 66.20001 Q 366.1 64.3 366.15 63.750008 Q 366.2 63.20001 365.4 62.000008 Q 364.4 60.500008 364.45 59.95001 Q 364.5 59.400005 365.8 59.400005 Q 367.2 59.400005 370.6 57.70001 Q 375.7 55.000008 380.3 57.000008 Q 381.2 57.400005 381.4 56.20001 Q 381.5 55.100006 381.5 51.600006 Q 381.5 45.700005 380.7 44.250008 Q 379.9 42.800003 381.2 41.90001 Q 384 39.800003 385.8 41.90001 Q 387.6 44.000008 387.6 49.000008 Q 387.6 53.100006 389.8 57.100006 Q 391.7 60.70001 394.9 64.15001 Q 398.1 67.600006 400.7 68.90001 Q 402.9 70.100006 403.15 71.70001 Q 403.4 73.3 401.4 73.90001 Q 395.8 75.600006 390.6 78.90001 L 386.5 81.600006 L 386 92.3 Q 385.3 107.600006 383.9 114.55001 Q 382.5 121.50001 379.7 122.200005 Q 378 122.600006 377.7 121.950005 Q 377.4 121.30001 378.1 118.30001 Q 378.8 114.700005 379.2 100.8 Q 379.2 100.50001 379.2 100.00001 Q 379.7 88.100006 378.9 87.20001 Q 378.2 86.40001 375.3 88.50001 Q 375.2 88.600006 375 88.700005 L 374.9 88.8 L 374.6 89.00001 Q 371.8 91.00001 369.65 92.00001 Q 367.5 93.00001 364.1 95.40001 Q 361.8 97.00001 361.1 97.25001 Q 360.4 97.50001 359.8 96.700005 Q 358.9 95.50001 358.9 92.50001 Q 358.9 90.700005 359.25 90.00001 Q 359.6 89.3 360.5 89.00001 Q 362 88.40001 368.15 81.350006 Q 374.3 74.3 373.6 74.55 Q 372.9 74.8 368.95 76.70001 Q 365 78.600006 364.35 78.25001 Q 363.7 77.90001 363.7 75.45001 Q 363.7 73.00001 366.8 69.100006 Q 370.3 64.600006 369.7 64.3 Q 369.5 64.20001 369.2 64.20001 Q 368.5 64.20001 366.1 67.8 L 361 75.20001 Q 356.5 81.100006 355.7 84.00001 Q 354.9 86.90001 355.1 95.90001 Q 355.4 106.30001 355.05 107.15001 Q 354.7 108.00001 352.1 108.00001 Q 349.5 108.00001 345.9 105.200005 Q 342.3 102.40001 338.8 100.850006 Q 335.3 99.3 333.05 96.200005 Q 330.8 93.100006 330.8 92.700005 Q 331.1 91.600006 334.6 93.3 Q 337.7 94.90001 337.7 93.600006 Q 337.7 92.90001 339.05 91.05 Q 340.4 89.200005 342.15 84.75001 Q 343.9 80.3 344.6 79.20001 Q 346 76.600006 344.5 76.600006 Q 343.1 76.8 338.2 78.90001 Q 333.4 81.20001 330.25 81.350006 Q 327.1 81.50001 326.9 79.40001 Q 326.8 78.600006 327.05 78.25001 Q 327.3 77.90001 328.2 77.90001 Q 329.6 77.90001 334.75 74.90001 Q 339.9 71.90001 343.9 70.70001 L 348 69.40001 L 348 65.100006 Q 348 60.20001 346.5 60.20001 Q 345.7 60.100006 344.6 61.100006 Q 342.9 62.600006 342 62.20001 Q 341.9 62.000008 341.9 61.800007 Q 341.9 61.500008 344.6 58.050007 Q 347.3 54.600006 347.3 53.90001 Q 347.3 53.200005 350.3 48.65001 Q 353.3 44.100006 354.75 41.15001 Q 356.2 38.200005 357.6 38.200005 Q 358.4 38.200005 358.65 38.750008 ZM 388.6 59.70001 Q 387.8 59.600006 386.9 66.40001 Q 385.8 76.50001 387.1 76.50001 Q 388.1 76.50001 390.95 75.100006 Q 393.8 73.70001 394.5 72.90001 Q 395.1 72.00001 394.9 71.350006 Q 394.7 70.70001 393.4 68.8 Q 391.3 66.00001 390.4 63.20001 Q 389.2 59.70001 388.6 59.70001 ZM 378.1 61.500008 Q 377.6 61.70001 377.2 62.20001 Q 375.3 63.800007 373.15 66.70001 Q 371 69.600006 371.3 70.100006 Q 371.7 70.40001 376.3 68.8 Q 379.9 67.70001 380.4 66.95001 Q 380.9 66.20001 381.1 61.70001 Q 381.2 59.100006 378.1 61.500008 ZM 379.55 75.20001 Q 378.9 75.20001 375.45 78.90001 Q 372 82.600006 372 83.3 Q 372 84.100006 376 82.100006 Q 378.9 80.600006 379.55 79.95001 Q 380.2 79.3 380.2 77.600006 Q 380.2 75.20001 379.55 75.20001 ZM 346.8 84.00001 Q 346.2 83.40001 345.8 84.90001 Q 345.8 85.00001 345.8 85.3 Q 345.3 87.600006 345.8 87.3 Q 346 87.20001 346.6 86.20001 Q 347.4 84.70001 346.8 84.00001 Z"
fill="currentColor"
style={{ "--path-length": "1026.061" } as React.CSSProperties}
/>
<path
d="M 471.4 45.40001 Q 471.6 45.700005 471.8 45.800003 Q 473.7 47.40001 474.1 48.40001 Q 474.5 49.40001 474.5 52.500008 L 474.5 56.800007 L 478.5 57.20001 Q 482.6 57.500008 483.5 58.900005 Q 484.2 59.900005 484.05 60.400005 Q 483.9 60.900005 482.8 61.800007 Q 481.1 63.000008 477.6 63.500008 Q 474.5 64.00001 473.35 65.350006 Q 472.2 66.70001 471.8 70.50001 Q 471.3 74.50001 471 75.8 Q 471 76.100006 471 76.350006 Q 471 76.600006 471.5 76.8 Q 472 77.00001 472.8 77.00001 Q 473.6 77.00001 475.3 77.00001 Q 479 77.00001 480.15 77.40001 Q 481.3 77.8 481.8 79.100006 Q 482.1 79.90001 480.25 84.75001 Q 478.4 89.600006 476.2 94.00001 L 475.1 96.3 L 477.5 98.200005 Q 479.9 100.3 485.2 103.700005 Q 489.7 106.700005 491.15 108.50001 Q 492.6 110.30001 492.6 112.80001 Q 492.6 117.50001 487.5 117.200005 Q 485.7 117.100006 485.25 116.80001 Q 484.8 116.50001 484.9 115.40001 Q 485.1 113.80001 484.1 112.30001 Q 483.4 111.100006 478.05 107.15001 Q 472.7 103.200005 471.1 102.600006 Q 470.3 102.3 468.2 103.90001 Q 466.5 105.40001 462.4 106.950005 Q 458.3 108.50001 456.2 108.50001 Q 451.3 108.50001 447.45 106.450005 Q 443.6 104.40001 442.1 101.00001 L 440.9 98.600006 L 440.4 102.700005 Q 439.8 106.700005 439.8 110.200005 Q 439.9 113.80001 439.2 114.65001 Q 438.5 115.50001 436.3 114.600006 Q 435 114.100006 434 113.00001 Q 433 111.90001 431.7 109.40001 Q 429.2 104.80001 427.65 103.100006 Q 426.1 101.40001 424.6 101.850006 Q 423.1 102.3 420.75 101.65001 Q 418.4 101.00001 417.8 100.100006 Q 416.9 98.600006 417.8 97.40001 Q 418.7 96.200005 422.3 93.90001 Q 427.3 90.8 431.2 87.600006 L 435.1 84.50001 L 435.1 78.20001 Q 435.1 71.90001 435.7 69.3 Q 436.3 66.70001 435.8 66.40001 Q 435.3 66.100006 430.4 69.50001 Q 422.2 75.00001 419 71.70001 Q 418.5 71.20001 418.35 70.850006 Q 418.2 70.50001 418.65 70.100006 Q 419.1 69.70001 420 69.25001 Q 420.9 68.8 422.8 68.00001 Q 426.6 66.50001 431.9 63.400005 Q 436.5 60.70001 437.35 59.050007 Q 438.2 57.400005 438.2 51.100006 Q 438.2 45.40001 438.95 45.40001 Q 439.7 45.40001 442.55 48.550003 Q 445.4 51.700005 445.4 54.100006 Q 445.4 55.40001 445.75 56.050007 Q 446.1 56.70001 447.1 57.20001 Q 448.9 57.900005 451.15 57.900005 Q 453.4 57.900005 454.2 58.70001 Q 455.9 60.400005 453.1 61.100006 Q 452.1 61.300007 450.3 61.500008 Q 445.5 62.000008 444.45 62.800007 Q 443.4 63.600006 442.55 69.95001 Q 441.7 76.3 442.2 76.75001 Q 442.7 77.20001 444 75.90001 Q 445.9 74.3 445.8 75.350006 Q 445.7 76.40001 443.8 79.40001 Q 442.1 82.00001 441.7 83.40001 Q 441.3 84.8 441.3 88.200005 Q 441.3 90.100006 441.3 90.950005 Q 441.3 91.8 441.45 92.450005 Q 441.6 93.100006 441.85 93.00001 Q 442.1 92.90001 442.4 92.90001 Q 443.1 92.8 443.55 93.40001 Q 444 94.00001 444.6 95.8 Q 445.9 100.200005 449.7 101.3 Q 453.5 102.40001 457.8 99.600006 Q 459.3 98.700005 459.95 98.3 Q 460.6 97.90001 461.05 97.3 Q 461.5 96.700005 461.45 96.450005 Q 461.4 96.200005 460.7 95.55 Q 460 94.90001 459.25 94.450005 Q 458.5 94.00001 456.7 92.90001 Q 447.4 87.8 447.4 87.100006 Q 447.4 86.600006 448.35 86.55 Q 449.3 86.50001 450.75 86.8 Q 452.2 87.100006 453.7 87.600006 Q 455.7 88.50001 456.25 88.50001 Q 456.8 88.50001 457.5 87.50001 Q 458.4 86.3 458.05 84.90001 Q 457.7 83.50001 459.3 81.8 Q 460.5 80.50001 462.4 75.40001 Q 464.3 70.3 463.7 69.70001 Q 463.4 69.40001 460.3 70.50001 Q 457.6 71.40001 456.15 70.90001 Q 454.7 70.40001 453.1 68.20001 Q 452.4 67.20001 452.5 66.90001 Q 452.6 66.600006 453.4 66.3 Q 455.4 65.600006 459.9 63.550007 Q 464.4 61.500008 465 61.20001 Q 466.1 60.20001 467 46.500008 Q 467.2 44.500008 467.2 43.90001 Q 467.5 42.200005 471.4 45.40001 ZM 469.2 84.90001 Q 468.2 85.20001 467.5 86.00001 Q 466.5 87.20001 465.5 88.8 Q 464.5 90.40001 464.5 91.00001 Q 464.5 91.3 466.1 91.3 Q 467.7 91.3 469.55 88.600006 Q 471.4 85.90001 471.4 85.20001 Q 471.4 83.90001 469.2 84.90001 ZM 433.1 94.850006 Q 432.5 94.8 431 97.00001 Q 429.8 98.600006 429.9 99.8 Q 430 101.00001 431.3 101.00001 Q 432.2 101.00001 432.55 100.3 Q 432.9 99.600006 433.3 97.00001 Q 433.7 94.90001 433.1 94.850006 Z"
fill="currentColor"
style={{ "--path-length": "768.58575" } as React.CSSProperties}
/>
</svg>
</Link>
{/* 桌面端导航链接 */}
<div className="nav-links">
{navItems.map((item) => (
<Link
key={item.path}
to={item.path}
className={`nav-link ${isActive(item.path) ? 'active' : ''}`}
>
{item.label}
</Link>
))}
</div>
{/* 移动端菜单按钮 */}
<button
className="mobile-menu-button"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
aria-label="Toggle menu"
>
<svg
className="w-6 h-6"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
{isMobileMenuOpen ? (
<path d="M6 18L18 6M6 6l12 12" />
) : (
<path d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</div>
{/* 移动端导航菜单 */}
{isMobileMenuOpen && (
<div className="mobile-nav">
<div className="mobile-nav-links">
{navItems.map((item) => (
<Link
key={item.path}
to={item.path}
className={`mobile-nav-link ${isActive(item.path) ? 'active' : ''}`}
onClick={() => setIsMobileMenuOpen(false)}
>
{item.label}
</Link>
))}
</div>
</div>
)}
</nav>
);
}

View File

@ -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,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});

View File

@ -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(
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />,
{
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(
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />,
{
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);
});
}

27
web/czr/app/env.ts Normal file
View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
import React from 'react';
import { useEffect, useRef, useState } from 'react';
// ... 其他导入保持不变

196
web/czr/app/index.css Normal file
View File

@ -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;
}
}

508
web/czr/app/init.tsx Normal file
View File

@ -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<SetupContextType>({
currentStep: 1,
setCurrentStep: () => {},
});
// 步骤组件的通用属性接口
interface StepProps {
onNext: () => void;
}
const StepContainer: React.FC<{ title: string; children: React.ReactNode }> = ({
title,
children,
}) => (
<Box style={{ width: "90%", maxWidth: "600px", margin: "0 auto" }}>
<Heading size="5" mb="4" weight="bold">
{title}
</Heading>
<Flex direction="column" gap="4">
{children}
</Flex>
</Box>
);
// 通用的导航按钮组件
const NavigationButtons: React.FC<
StepProps & { loading?: boolean; disabled?: boolean }
> = ({ onNext, loading = false, disabled = false }) => (
<Flex justify="end" mt="4">
<Button
size="3"
disabled={loading || disabled}
onClick={onNext}
style={{ width: "100%" }}
>
{loading ? "处理中..." : "下一步"}
</Button>
</Flex>
);
// 修改输入框组件
const InputField: React.FC<{
label: string;
name: string;
defaultValue?: string | number;
hint?: string;
required?: boolean;
}> = ({ label, name, defaultValue, hint, required = true }) => (
<Box mb="4">
<Text as="label" size="2" weight="medium" className="block mb-2">
{label} {required && <Text color="red">*</Text>}
</Text>
<TextField.Root
name={name}
defaultValue={defaultValue?.toString()}
required={required}
>
<TextField.Slot></TextField.Slot>
</TextField.Root>
{hint && (
<Text color="gray" size="1" mt="1">
{hint}
</Text>
)}
</Box>
);
const Introduction: React.FC<StepProps> = ({ onNext }) => (
<StepContainer title="安装说明">
<Text size="3" style={{ lineHeight: 1.6 }}>
使 Echoes
</Text>
<NavigationButtons onNext={onNext} />
</StepContainer>
);
const DatabaseConfig: React.FC<StepProps> = ({ 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<string, any>,
);
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 (
<StepContainer title="数据库配置">
<div>
<Box mb="6">
<Text as="label" size="2" weight="medium" mb="2" className="block">
</Text>
<Select.Root value={dbType} onValueChange={setDbType}>
<Select.Trigger />
<Select.Content>
<Select.Group>
<Select.Item value="postgresql">PostgreSQL</Select.Item>
<Select.Item value="mysql">MySQL</Select.Item>
<Select.Item value="sqllite">SQLite</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
</Box>
{dbType === "postgresql" && (
<>
<InputField
label="数据库地址"
name="db_host"
defaultValue="localhost"
hint="通常使 localhost"
required
/>
<InputField
label="数据库前缀"
name="db_prefix"
defaultValue="echoec_"
hint="通常使用 echoec_"
required
/>
<InputField
label="端口"
name="db_port"
defaultValue={5432}
hint="PostgreSQL 默认端口为 5432"
required
/>
<InputField
label="用户名"
name="db_user"
defaultValue="postgres"
required
/>
<InputField
label="密码"
name="db_password"
defaultValue="postgres"
required
/>
<InputField
label="数据库名"
name="db_name"
defaultValue="echoes"
required
/>
</>
)}
{dbType === "mysql" && (
<>
<InputField
label="数据库地址"
name="db_host"
defaultValue="localhost"
hint="通常使用 localhost"
required
/>
<InputField
label="数据库前缀"
name="db_prefix"
defaultValue="echoec_"
hint="通常使用 echoec_"
required
/>
<InputField
label="端口"
name="db_port"
defaultValue={3306}
hint="mysql 默认端口为 3306"
required
/>
<InputField
label="用户名"
name="db_user"
defaultValue="root"
required
/>
<InputField
label="密码"
name="db_password"
defaultValue="mysql"
required
/>
<InputField
label="数据库名"
name="db_name"
defaultValue="echoes"
required
/>
</>
)}
{dbType === "sqllite" && (
<>
<InputField
label="数据库前缀"
name="db_prefix"
defaultValue="echoec_"
hint="通常使用 echoec_"
required
/>
<InputField
label="数据库名"
name="db_name"
defaultValue="echoes.db"
required
/>
</>
)}
<NavigationButtons
onNext={handleNext}
loading={loading}
disabled={loading}
/>
</div>
</StepContainer>
);
};
interface InstallReplyData {
token: string;
username: string;
password: string;
}
const AdminConfig: React.FC<StepProps> = ({ 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<string, any>,
);
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 (
<StepContainer title="创建管理员账号">
<div className="space-y-6">
<InputField label="用户名" name="admin_username" />
<InputField label="密码" name="admin_password" />
<InputField label="邮箱" name="admin_email" />
<NavigationButtons onNext={handleNext} loading={loading} />
</div>
</StepContainer>
);
};
const SetupComplete: React.FC = () => (
<StepContainer title="安装完成">
<Flex direction="column" align="center" gap="4">
<Text size="5" weight="medium">
</Text>
<Text size="3">...</Text>
<Box mt="4">
<Flex justify="center">
<Box className="animate-spin rounded-full h-8 w-8 border-b-2 border-current"></Box>
</Flex>
</Box>
</Flex>
</StepContainer>
);
export default function SetupPage() {
const [currentStep, setCurrentStep] = useState(() => {
return Number(import.meta.env.VITE_INIT_STATUS ?? 0) + 1;
});
return (
<Theme
grayColor="gray"
accentColor="gray"
radius="medium"
panelBackground="solid"
appearance="inherit"
>
<Box className="min-h-screen w-full">
<Box position="fixed" top="2" right="4">
<ThemeModeToggle />
</Box>
<Flex justify="center" pt="2">
<Box className="w-20 h-20">
<Echoes />
</Box>
</Flex>
<Flex direction="column" className="min-h-screen w-full pb-4">
<Container className="w-full">
<SetupContext.Provider value={{ currentStep, setCurrentStep }}>
{currentStep === 1 && (
<Introduction onNext={() => setCurrentStep(currentStep + 1)} />
)}
{currentStep === 2 && (
<DatabaseConfig
onNext={() => setCurrentStep(currentStep + 1)}
/>
)}
{currentStep === 3 && (
<AdminConfig onNext={() => setCurrentStep(currentStep + 1)} />
)}
{currentStep === 4 && <SetupComplete />}
</SetupContext.Provider>
</Container>
</Flex>
</Box>
</Theme>
);
}

31
web/czr/app/root.tsx Normal file
View File

@ -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 (
<html lang="zh">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body suppressHydrationWarning={true}>
<Navigation />
<div className="pt-16">
<Outlet />
</div>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}

46
web/czr/app/routes.tsx Normal file
View File

@ -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: '<a href="/">index</a><a href="/error">error</a><a href="/about">about</a><a href="/post">post</a>',
};
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,
});
}

View File

@ -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 (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-white text-gray-800">
{/* Hero区域 */}
<div className="relative overflow-hidden bg-gradient-to-r from-green-50 to-blue-50">
<div className="max-w-7xl mx-auto px-4 py-24">
<div className="text-center">
<h1 className="text-5xl font-bold mb-6 bg-gradient-to-r from-green-600 to-blue-600 bg-clip-text text-transparent">
</h1>
<p className="text-xl text-gray-600 max-w-2xl mx-auto mb-10">
</p>
<button className="bg-gradient-to-r from-green-600 to-blue-600 text-white px-8 py-3 rounded-full font-semibold
hover:opacity-90 transition-all duration-300 hover:scale-105 hover:shadow-lg">
</button>
</div>
</div>
</div>
{/* 核心技术 */}
<section className="py-20 bg-white" id="innovations">
<div className="max-w-7xl mx-auto px-4">
<h2 className="text-3xl font-bold text-center mb-16"></h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
<div className="text-center group transform transition-all duration-300 hover:-translate-y-2 hover:shadow-xl rounded-xl p-6">
<div className="bg-green-50 w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-6
group-hover:bg-green-100 transition-colors duration-300 group-hover:scale-110">
<svg className="w-10 h-10 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<h3 className="text-xl font-semibold mb-4"></h3>
<p className="text-gray-600">AI技术优化资源利用使</p>
</div>
<div className="text-center group transform transition-all duration-300 hover:-translate-y-2 hover:shadow-xl rounded-xl p-6">
<div className="bg-blue-50 w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-6
group-hover:bg-blue-100 transition-colors duration-300 group-hover:scale-110">
<svg className="w-10 h-10 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
<h3 className="text-xl font-semibold mb-4"></h3>
<p className="text-gray-600"></p>
</div>
<div className="text-center group transform transition-all duration-300 hover:-translate-y-2 hover:shadow-xl rounded-xl p-6">
<div className="bg-cyan-50 w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-6
group-hover:bg-cyan-100 transition-colors duration-300 group-hover:scale-110">
<svg className="w-10 h-10 text-cyan-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<h3 className="text-xl font-semibold mb-4"></h3>
<p className="text-gray-600"></p>
</div>
</div>
</div>
</section>
{/* 解决方案 */}
<section className="py-20 bg-gradient-to-r from-green-50 to-blue-50" id="solutions">
<div className="max-w-7xl mx-auto px-4">
<h2 className="text-3xl font-bold text-center mb-16"></h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="bg-white rounded-lg p-8 shadow-sm hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<h3 className="text-xl font-semibold mb-4"></h3>
<p className="text-gray-600 mb-4"></p>
<ul className="text-gray-600 space-y-2">
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
<div className="bg-white rounded-lg p-8 shadow-sm hover:shadow-xl transition-all duration-300 hover:-translate-y-1">
<h3 className="text-xl font-semibold mb-4"></h3>
<p className="text-gray-600 mb-4"></p>
<ul className="text-gray-600 space-y-2">
<li className="transition-all duration-200 hover:text-green-600 hover:translate-x-2"> 使</li>
<li className="transition-all duration-200 hover:text-green-600 hover:translate-x-2"> </li>
<li className="transition-all duration-200 hover:text-green-600 hover:translate-x-2"> </li>
</ul>
</div>
</div>
</div>
</section>
{/* 页脚 */}
<footer className="bg-gray-900 text-white py-12">
<div className="max-w-7xl mx-auto px-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div>
<h4 className="text-lg font-semibold mb-4"></h4>
<p className="text-gray-400"></p>
</div>
<div>
<h4 className="text-lg font-semibold mb-4"></h4>
<p className="text-gray-400">400-888-8888</p>
<p className="text-gray-400">contact@xingjiyuan.com</p>
</div>
<div>
<h4 className="text-lg font-semibold mb-4"></h4>
<ul className="text-gray-400 space-y-2">
<li>
<a href="#" className="hover:text-white transition-colors duration-200 hover:translate-x-2 inline-block">
</a>
</li>
<li>
<a href="#" className="hover:text-white transition-colors duration-200 hover:translate-x-2 inline-block">
</a>
</li>
<li>
<a href="#" className="hover:text-white transition-colors duration-200 hover:translate-x-2 inline-block">
</a>
</li>
</ul>
</div>
<div>
<h4 className="text-lg font-semibold mb-4"></h4>
<p className="text-gray-400">888</p>
</div>
</div>
<div className="border-t border-gray-800 mt-8 pt-8 text-center text-gray-400">
<p>© 2024 </p>
</div>
</div>
</footer>
</div>
);
}

View File

@ -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 (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-white text-gray-800">
{/* 页面标题 */}
<div className="bg-gradient-to-r from-green-50 to-blue-50 py-20">
<div className="max-w-7xl mx-auto px-4">
<h1 className="text-4xl font-bold text-center bg-gradient-to-r from-green-600 to-blue-600 bg-clip-text text-transparent">
</h1>
<p className="text-center text-gray-600 mt-4 max-w-2xl mx-auto">
</p>
</div>
</div>
{/* 公司介绍 */}
<div className="py-20">
<div className="max-w-7xl mx-auto px-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
<div className="transform transition-all duration-500 hover:scale-105">
<h2 className="text-3xl font-bold mb-6 relative after:content-[''] after:absolute after:bottom-0
after:left-0 after:w-20 after:h-1 after:bg-gradient-to-r after:from-green-500 after:to-blue-500">
</h2>
<p className="text-gray-600 mb-4">
2020
</p>
<p className="text-gray-600">
</p>
</div>
<div className="transform transition-all duration-500 hover:scale-105">
<h2 className="text-3xl font-bold mb-6 relative after:content-[''] after:absolute after:bottom-0
after:left-0 after:w-20 after:h-1 after:bg-gradient-to-r after:from-green-500 after:to-blue-500">
使
</h2>
<div className="space-y-4">
<div>
<h3 className="text-xl font-semibold mb-2"></h3>
<p className="text-gray-600"></p>
</div>
<div>
<h3 className="text-xl font-semibold mb-2">使</h3>
<p className="text-gray-600"></p>
</div>
</div>
</div>
</div>
</div>
</div>
{/* 页脚 */}
<footer className="bg-gray-900 text-white py-12">
{/* 同首页页脚内容 */}
</footer>
</div>
);
}

View File

@ -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<typeof loader>();
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-white text-gray-800">
{/* 头部区域:标题 + 轮播图 */}
<div className="relative bg-gradient-to-b from-green-50 to-blue-50/30 pt-16 pb-32 overflow-hidden">
<div className="max-w-7xl mx-auto px-4">
{/* 标题部分 */}
<div className="text-center mb-16">
<h1 className="text-5xl font-bold bg-gradient-to-r from-green-600 to-blue-600 bg-clip-text text-transparent">
</h1>
<p className="text-gray-600 mt-6 text-lg max-w-2xl mx-auto">
</p>
</div>
{/* 轮播图部分 */}
{isClient ? (
<div className="relative">
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-white/10 pointer-events-none" />
<div className="flex justify-center">
<div className="w-[600px]">
<Carousel
items={innovations.map((innovation) => ({
content: (
<div className="w-[600px] h-[400px] relative rounded-xl overflow-hidden">
<ImageLoader
src={innovation.image}
alt={innovation.title}
className="relative z-[1]"
containerClassName="w-full h-full"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent z-[2]" />
<div className="absolute bottom-0 left-0 right-0 p-8 z-[3]">
<h3 className="text-white text-3xl font-bold mb-2">{innovation.title}</h3>
<p className="text-white/80 text-lg"></p>
</div>
</div>
),
}))}
interval={5000}
/>
</div>
</div>
</div>
) : (
<div className="flex justify-center">
<div className="w-[600px] h-[400px] bg-gray-100 rounded-xl" />
</div>
)}
</div>
</div>
{/* 技术详情部分 */}
<div className="py-24 bg-white">
<div className="max-w-7xl mx-auto px-4">
<h2 className="text-4xl font-bold text-center mb-20">
<span className="bg-gradient-to-r from-green-600 to-blue-600 bg-clip-text text-transparent">
</span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="bg-white rounded-lg p-8 shadow-lg transform transition-all duration-300
hover:-translate-y-2 hover:shadow-xl">
<div className="bg-green-50 w-16 h-16 rounded-full flex items-center justify-center mb-6
transform transition-all duration-300 hover:scale-110 hover:rotate-12">
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<h3 className="text-xl font-bold mb-4 relative">
<span className="relative z-10">AI环境优化</span>
<span className="absolute bottom-0 left-0 w-full h-2 bg-green-100 transform -skew-x-12"></span>
</h3>
<p className="text-gray-600">
</p>
</div>
<div className="bg-white rounded-lg p-8 shadow-lg transform transition-all duration-300
hover:-translate-y-2 hover:shadow-xl">
<div className="bg-blue-50 w-16 h-16 rounded-full flex items-center justify-center mb-6
transform transition-all duration-300 hover:scale-110 hover:rotate-12">
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
</div>
<h3 className="text-xl font-bold mb-4 relative">
<span className="relative z-10"></span>
<span className="absolute bottom-0 left-0 w-full h-2 bg-blue-100 transform -skew-x-12"></span>
</h3>
<p className="text-gray-600">
</p>
</div>
<div className="bg-white rounded-lg p-8 shadow-lg transform transition-all duration-300
hover:-translate-y-2 hover:shadow-xl">
<div className="bg-purple-50 w-16 h-16 rounded-full flex items-center justify-center mb-6
transform transition-all duration-300 hover:scale-110 hover:rotate-12">
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<h3 className="text-xl font-bold mb-4 relative">
<span className="relative z-10"></span>
<span className="absolute bottom-0 left-0 w-full h-2 bg-purple-100 transform -skew-x-12"></span>
</h3>
<p className="text-gray-600">
</p>
</div>
</div>
</div>
</div>
{/* 页脚 */}
<footer className="bg-gray-900 text-white py-12">
{/* 同首页页脚内容 */}
</footer>
</div>
);
}

View File

@ -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<typeof loader>();
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-white text-gray-800">
{/* 页面标题和轮播图 */}
<div className="bg-gradient-to-r from-green-50 to-blue-50 py-20">
<div className="max-w-7xl mx-auto px-4">
<div className="flex flex-col items-center">
{isClient ? (
<div className="w-[60px] md:w-[70px] h-[60px] md:h-[70px]">
{/* 轮播图代码从这里开始 */}
</div>
) : (
<div className="w-[60px] md:w-[70px] h-[60px] md:h-[70px] bg-gray-100 rounded-lg" />
)}
<h1 className="text-4xl font-bold text-center mt-8 bg-gradient-to-r from-green-600 to-blue-600 bg-clip-text text-transparent">
</h1>
<p className="text-center text-gray-600 mt-4 max-w-2xl mx-auto">
</p>
</div>
</div>
</div>
{/* 解决方案详情 */}
<div className="py-20">
<div className="max-w-7xl mx-auto px-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
<div className="bg-white rounded-lg p-8 shadow-lg transform transition-all duration-300
hover:-translate-y-2 hover:shadow-xl border border-transparent hover:border-green-100">
<h2 className="text-2xl font-bold mb-6 relative inline-block">
<span className="relative z-10"></span>
<span className="absolute bottom-0 left-0 w-full h-2 bg-green-100 transform -skew-x-12"></span>
</h2>
<div className="space-y-4">
<p className="text-gray-600"></p>
<ul className="space-y-2 text-gray-600">
<li className="transition-all duration-200 hover:text-green-600 hover:translate-x-2">
</li>
<li className="transition-all duration-200 hover:text-green-600 hover:translate-x-2">
</li>
<li className="transition-all duration-200 hover:text-green-600 hover:translate-x-2">
</li>
<li className="transition-all duration-200 hover:text-green-600 hover:translate-x-2">
</li>
</ul>
</div>
</div>
<div className="bg-white rounded-lg p-8 shadow-lg transform transition-all duration-300
hover:-translate-y-2 hover:shadow-xl border border-transparent hover:border-green-100">
<h2 className="text-2xl font-bold mb-6 relative inline-block">
<span className="relative z-10"></span>
<span className="absolute bottom-0 left-0 w-full h-2 bg-green-100 transform -skew-x-12"></span>
</h2>
<div className="space-y-4">
<p className="text-gray-600"></p>
<ul className="space-y-2 text-gray-600">
<li className="transition-all duration-200 hover:text-green-600 hover:translate-x-2">
</li>
<li className="transition-all duration-200 hover:text-green-600 hover:translate-x-2">
</li>
<li className="transition-all duration-200 hover:text-green-600 hover:translate-x-2">
</li>
<li className="transition-all duration-200 hover:text-green-600 hover:translate-x-2">
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
{/* 页脚 - 可以提取为共享组件 */}
<footer className="bg-gray-900 text-white py-12">
{/* 同首页页脚内容 */}
</footer>
</div>
);
}

View File

@ -0,0 +1,5 @@
.nav-logo svg {
width: 120px;
height: auto;
color: #1a1a1a;
}

View File

@ -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<HTMLDivElement>(null);
const sceneRef = useRef<THREE.Scene>();
const cameraRef = useRef<THREE.OrthographicCamera>();
const rendererRef = useRef<THREE.WebGLRenderer>();
const animationFrameRef = useRef<number>();
const geometryRef = useRef<THREE.BufferGeometry>();
const materialRef = useRef<THREE.PointsMaterial>();
const pointsRef = useRef<THREE.Points>();
// 清理函数
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 <div ref={containerRef} className="w-full h-full" />;
};
// 修改 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<LoaderStatus>({
isLoading: true,
hasError: false,
timeoutError: false,
animationPhase: 'assembling'
});
const [showImage, setShowImage] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout>();
const loadingRef = useRef(false);
const imageRef = useRef<HTMLImageElement | null>(null);
const [currentParticles, setCurrentParticles] = useState<Particle[]>([]);
// 动画循环
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 (
<div className={`relative shrink-0 overflow-hidden ${containerClassName}`}>
<div className={`absolute inset-0 ${BG_CONFIG.className} rounded-lg overflow-hidden`}>
<ParticleImage
src={src}
status={status}
onParticlesCreated={setCurrentParticles}
onAnimationPhaseChange={(phase) => {
setStatus(prev => ({ ...prev, animationPhase: phase }));
}}
/>
</div>
{!status.hasError && !status.timeoutError && imageRef.current && (
<div className="absolute inset-0 rounded-lg overflow-hidden">
<img
src={imageRef.current.src}
alt={alt}
className={`
w-full h-full object-cover
transition-opacity duration-1000
${className}
${showImage ? 'opacity-100' : 'opacity-0'}
`}
style={{
visibility: showImage ? 'visible' : 'hidden',
objectFit: 'cover',
objectPosition: 'center'
}}
/>
</div>
)}
</div>
);
};

42
web/czr/hooks/error.tsx Normal file
View File

@ -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 (
<div className="min-h-screen flex items-center justify-center bg-[--background] transition-colors duration-300">
<div className="text-center">
<h1 className="text-6xl font-bold text-[--foreground] mb-4">
{text}
<span className="animate-pulse">|</span>
</h1>
<p className="text-[--muted-foreground] text-xl">
访
</p>
<button
onClick={() => (window.location.href = "/")}
className="mt-8 px-6 py-3 bg-[--primary] hover:bg-[--primary-foreground] text-[--primary-foreground] hover:text-[--primary] rounded-lg transition-colors duration-300"
>
</button>
</div>
</div>
);
});

103
web/czr/hooks/loading.tsx Normal file
View File

@ -0,0 +1,103 @@
import React, { createContext, useState, useContext } from "react";
interface LoadingContextType {
isLoading: boolean;
showLoading: () => void;
hideLoading: () => void;
}
const LoadingContext = createContext<LoadingContextType>({
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 (
<LoadingContext.Provider value={{ isLoading, showLoading, hideLoading }}>
{children}
{isLoading && (
<div className="fixed inset-0 flex flex-col items-center justify-center bg-black/25 dark:bg-black/40 z-[999999]">
<div className="loading-spinner mb-2" />
<div className="text-custom-p-light dark:text-custom-p-dark text-sm">
...
</div>
</div>
)}
<style>{`
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-spinner {
width: 30px;
height: 30px;
border: 3px solid rgba(59, 130, 246, 0.2);
border-radius: 50%;
border-top-color: #3B82F6;
animation: spin 0.8s linear infinite;
}
.dark .loading-spinner {
border: 3px solid rgba(96, 165, 250, 0.2);
border-top-color: #60A5FA;
}
`}</style>
</LoadingContext.Provider>
);
};
// 全局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();
},
};

View File

@ -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, NotificationConfig> = {
[NotificationType.SUCCESS]: {
icon: <CheckCircledIcon className="w-5 h-5 text-white" />,
bgColor: "bg-[rgba(0,168,91,0.85)]",
},
[NotificationType.ERROR]: {
icon: <CrossCircledIcon className="w-5 h-5 text-white" />,
bgColor: "bg-[rgba(225,45,57,0.85)]",
},
[NotificationType.INFO]: {
icon: <InfoCircledIcon className="w-5 h-5 text-white" />,
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<NotificationContextType>({
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<Notification[]>([]);
// 统一参数顺序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 (
<NotificationContext.Provider value={contextValue}>
{notifications.length > 0 && (
<Box
position="fixed"
top="4"
className="fixed top-4 right-4 z-[1000] flex flex-col gap-2 w-full max-w-[360px] px-4 md:px-0 md:right-6"
>
{notifications.map((notification) => (
<Card
key={notification.id}
className="p-0 overflow-hidden shadow-lg w-full"
>
<Flex
direction="column"
gap="2"
className={`relative min-h-[52px] p-4 ${notificationConfigs[notification.type].bgColor}`}
>
<Button
variant="ghost"
onClick={() => closeNotification(notification.id)}
className="absolute right-2 top-2 p-1 min-w-0 h-auto text-white opacity-70 cursor-pointer bg-transparent border-none text-sm hover:opacity-100 transition-opacity"
>
</Button>
<Flex direction="column" gap="1.5" className="pr-6">
<Flex align="center" gap="2">
<span className="flex items-center justify-center">
{notificationConfigs[notification.type].icon}
</span>
{notification.title && (
<Text
weight="bold"
size="2"
className="text-white leading-tight"
>
{notification.title}
</Text>
)}
</Flex>
<Text size="2" className="text-white/80 leading-normal">
{notification.message}
</Text>
</Flex>
<div className="h-0.5 w-full bg-white/10 mt-1">
<div
className="h-full bg-white/20 animate-[progress_3s_linear]"
style={{
transformOrigin: "left",
}}
/>
</div>
</Flex>
</Card>
))}
</Box>
)}
{children}
</NotificationContext.Provider>
);
};
// 导出hook
export const useNotification = () => {
const context = useContext(NotificationContext);
if (!context) {
throw new Error(
"useNotification must be used within a NotificationProvider",
);
}
return context;
};

133
web/czr/hooks/themeMode.tsx Normal file
View File

@ -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 <script dangerouslySetInnerHTML={{ __html: themeScript }} />;
};
export const ThemeModeToggle: React.FC = () => {
const [isDark, setIsDark] = useState<boolean | null>(null);
useEffect(() => {
if (typeof window !== 'undefined') {
const savedTheme = localStorage.getItem(THEME_KEY);
const initialIsDark = savedTheme === 'dark' || document.documentElement.className === 'dark';
setIsDark(initialIsDark);
}
}, []);
useEffect(() => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
const isDarkTheme = document.documentElement.className === 'dark';
setIsDark(isDarkTheme);
}
});
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
return () => observer.disconnect();
}, []);
const toggleTheme = () => {
if (isDark === null) return;
const newIsDark = !isDark;
setIsDark(newIsDark);
const newTheme = newIsDark ? "dark" : "light";
document.documentElement.className = newTheme;
localStorage.setItem(THEME_KEY, newTheme);
};
if (isDark === null) {
return (
<Button
variant="ghost"
className="w-full h-full p-0 rounded-lg transition-all duration-300 transform"
aria-label="Loading theme"
>
<MoonIcon className="w-full h-full" />
</Button>
);
}
return (
<Button
variant="ghost"
onClick={toggleTheme}
className="w-full h-full p-0 rounded-lg transition-all duration-300 transform"
aria-label="Toggle theme"
>
{isDark ? (
<SunIcon className="w-full h-full" />
) : (
<MoonIcon className="w-full h-full" />
)}
</Button>
);
};
// 更新类型定义
declare global {
interface Window {
__THEME__?: "light" | "dark";
}
}
export const useThemeMode = () => {
const [mode, setMode] = useState<"light" | "dark">("light");
useEffect(() => {
if (typeof window !== "undefined") {
const saved = localStorage.getItem(THEME_KEY);
if (saved) {
setMode(saved as "light" | "dark");
} else {
const isDark = window.matchMedia(
"(prefers-color-scheme: dark)",
).matches;
setMode(isDark ? "dark" : "light");
}
// 监听主题变化事件
const handleThemeChange = (e: CustomEvent) => {
setMode(e.detail.theme);
};
window.addEventListener(
"theme-change",
handleThemeChange as EventListener,
);
return () =>
window.removeEventListener(
"theme-change",
handleThemeChange as EventListener,
);
}
}, []);
return { mode };
};

12
web/czr/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Remix App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/entry.client.tsx"></script>
</body>
</html>

81
web/czr/package.json Normal file
View File

@ -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
}
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -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 = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Your App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/assets/index.js"></script>
</body>
</html>`;
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();

32
web/czr/server/env.ts Normal file
View File

@ -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<string, string>,
);
} catch {
return {};
}
}
export async function writeEnvFile(env: Record<string, string>) {
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");
}

71
web/czr/server/express.ts Normal file
View File

@ -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}`);
});

View File

@ -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}`);
});

31
web/czr/server/static.js Normal file
View File

@ -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'));
});

View File

@ -0,0 +1,7 @@
import { RemixBrowser } from "@remix-run/react";
import { startTransition } from "react";
import { hydrateRoot } from "react-dom/client";
startTransition(() => {
hydrateRoot(document, <RemixBrowser />);
});

79
web/czr/start.bat Normal file
View File

@ -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

88
web/czr/styles/echoes.css Normal file
View File

@ -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;
}
}

View File

@ -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;

32
web/czr/tsconfig.json Normal file
View File

@ -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
}
}

39
web/czr/vite.config.ts Normal file
View File

@ -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: '',
})