This commit is contained in:
jeremygan2021
2025-09-20 14:32:23 +08:00
parent a9ee899cc5
commit 98cca278b0
15 changed files with 2016 additions and 137 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@ const App = () => {
const sections = [
{ id: 'hero', component: HeroSection, title: '发现新视界', subtitle: 'Discover New Horizons' },
{ id: 'product', component: ProductSection, title: 'AI产品', subtitle: 'Innovative Solutions' },
{ id: 'product', component: ProductSection, title: 'AI产品', subtitle: 'AI hardware, AI solution' },
{ id: 'team', component: TeamSection, title: '专业团队', subtitle: 'Expert Team' },
{ id: 'cases', component: CaseSection, title: '成功案例', subtitle: 'Success Stories' },
{ id: 'contact', component: ContactSection, title: '联系我们', subtitle: 'Get In Touch' }
@@ -36,6 +36,8 @@ const App = () => {
useEffect(() => {
let timeout;
let touchStartY = 0;
let touchEndY = 0;
const handleWheel = (e) => {
e.preventDefault();
@@ -74,14 +76,66 @@ const App = () => {
}, 1200); // 增加到1.2秒,与动画时间匹配
};
// 触摸事件处理
const handleTouchStart = (e) => {
touchStartY = e.touches[0].clientY;
};
const handleTouchMove = (e) => {
e.preventDefault();
};
const handleTouchEnd = (e) => {
if (isScrollingRef.current) {
return;
}
touchEndY = e.changedTouches[0].clientY;
const deltaY = touchStartY - touchEndY;
const minSwipeDistance = 50; // 最小滑动距离
if (Math.abs(deltaY) > minSwipeDistance) {
isScrollingRef.current = true;
setIsScrolling(true);
let newSection = currentSectionRef.current;
if (deltaY > 0 && currentSectionRef.current < sections.length - 1) {
// 向上滑动,下一页
newSection = currentSectionRef.current + 1;
} else if (deltaY < 0 && currentSectionRef.current > 0) {
// 向下滑动,上一页
newSection = currentSectionRef.current - 1;
}
if (newSection !== currentSectionRef.current) {
setCurrentSection(newSection);
}
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
isScrollingRef.current = false;
setIsScrolling(false);
}, 1200);
}
};
const container = containerRef.current;
if (container) {
container.addEventListener('wheel', handleWheel, { passive: false });
container.addEventListener('touchstart', handleTouchStart, { passive: true });
container.addEventListener('touchmove', handleTouchMove, { passive: false });
container.addEventListener('touchend', handleTouchEnd, { passive: true });
}
return () => {
if (container) {
container.removeEventListener('wheel', handleWheel);
container.removeEventListener('touchstart', handleTouchStart);
container.removeEventListener('touchmove', handleTouchMove);
container.removeEventListener('touchend', handleTouchEnd);
}
if (timeout) {
clearTimeout(timeout);

BIN
src/asset/logo-bai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

BIN
src/asset/logo-bai1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

View File

@@ -17,7 +17,7 @@ const HeroSection = ({ isActive }) => {
animate={isActive ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.6 }}
>
推动变革
推动线下AI变革
</motion.h1>
<motion.h2
@@ -26,9 +26,9 @@ const HeroSection = ({ isActive }) => {
animate={isActive ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.8 }}
>
<span className="highlight-text">通过</span>
<span className="highlight-text">通过</span> 线
<br />
人工智能技术
普及人工智能技术
</motion.h2>
<motion.div
@@ -39,13 +39,30 @@ const HeroSection = ({ isActive }) => {
>
<div className="quote-line"></div>
<p>
通过利用战略洞察和行业网络
通过软硬件一体的整体解决方案
<br />
Radiant 作为增长催化剂为我们的
量迹AI用普惠的解决方案
<br />
投资组合公司和投资者创造卓越价值
让人类进入AI时代
</p>
</motion.div>
<motion.button
className="learn-more-btn"
initial={{ opacity: 0, y: 30 }}
animate={isActive ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 1.6 }}
whileHover={{
scale: 1.05,
y: -3,
boxShadow: "0 10px 30px rgba(0, 245, 212, 0.3)"
}}
whileTap={{ scale: 0.95 }}
>
<span className="btn-text">了解更多</span>
<div className="btn-glow"></div>
<div className="btn-arrow"></div>
</motion.button>
</motion.div>
{/* 右侧几何形状 */}
@@ -55,45 +72,91 @@ const HeroSection = ({ isActive }) => {
animate={isActive ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 1.5, delay: 0.5 }}
>
{/* 背景视频 */}
<video
className="hero-background-video"
autoPlay
muted
loop
playsInline
controls={false}
preload="auto"
src="/stay.mp4"
onError={(e) => console.error('Video error:', e)}
onLoadStart={() => console.log('Video loading started')}
onCanPlay={() => console.log('Video can play')}
/>
<div className="geometric-container">
{/* 小立方体 */}
<motion.div
className="cube-main"
className="cube-small"
initial={{ rotateY: 0, rotateX: 0 }}
animate={isActive ? {
rotateY: [0, 15, 0],
rotateX: [0, -10, 0]
rotateY: 360,
rotateX: [0, 30, 0]
} : {}}
transition={{
duration: 8,
duration: 15,
repeat: Infinity,
ease: "easeInOut"
ease: "linear"
}}
>
<div className="cube-face front"></div>
<div className="cube-face back"></div>
{/* <div className="cube-face back"></div> */}
<div className="cube-face right"></div>
<div className="cube-face left"></div>
<div className="cube-face top"></div>
<div className="cube-face bottom"></div>
</motion.div>
<motion.div
className="cube-small"
initial={{ rotateY: 0 }}
animate={isActive ? { rotateY: 360 } : {}}
transition={{
duration: 12,
{/* 浮动圆环 */}
<motion.div
className="floating-ring"
initial={{ rotateZ: 0, y: 0 }}
animate={isActive ? {
rotateZ: 360,
y: [0, -20, 0]
} : {}}
transition={{
duration: 20,
repeat: Infinity,
ease: "linear"
}}
>
<div className="cube-face front"></div>
<div className="cube-face back"></div>
<div className="cube-face right"></div>
<div className="cube-face left"></div>
<div className="cube-face top"></div>
<div className="cube-face bottom"></div>
</motion.div>
/>
{/* 浮动粒子 */}
<motion.div
className="floating-particle particle-1"
initial={{ x: 0, y: 0, opacity: 0 }}
animate={isActive ? {
x: [0, 40, -20, 0],
y: [0, -30, 20, 0],
opacity: [0, 0.6, 0.3, 0]
} : {}}
transition={{
duration: 8,
repeat: Infinity,
ease: "easeInOut",
delay: 1
}}
/>
<motion.div
className="floating-particle particle-2"
initial={{ x: 0, y: 0, opacity: 0 }}
animate={isActive ? {
x: [0, -60, 30, 0],
y: [0, 40, -25, 0],
opacity: [0, 0.4, 0.6, 0]
} : {}}
transition={{
duration: 12,
repeat: Infinity,
ease: "easeInOut",
delay: 3
}}
/>
</div>
</motion.div>

View File

@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import logoBai from '../asset/logo-bai.png';
const Navigation = ({ currentSection, sections, onSectionChange }) => {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
@@ -47,12 +48,16 @@ const Navigation = ({ currentSection, sections, onSectionChange }) => {
}}
transition={{ duration: 0.5 }}
>
Q
<img
src={logoBai}
alt="Quant Speed Logo"
className="logo-image"
/>
</motion.div>
<motion.span
className="logo-text"
>
Quant Speed
Quant Speed 量迹AI科技
</motion.span>
</motion.div>

View File

@@ -1,7 +1,202 @@
import React from 'react';
import React, { Suspense, useEffect, useMemo, useRef, useState } from 'react';
import { motion } from 'framer-motion';
import { Canvas, useFrame, useLoader } from '@react-three/fiber';
import { OrbitControls, Bounds, ContactShadows, Html } from '@react-three/drei';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader';
import * as THREE from 'three';
const BoardScene = ({ onProject, onToggleConnections, objPath, mtlPath, modelName, isMobile }) => {
const groupRef = useRef();
const materials = useLoader(MTLLoader, mtlPath);
materials.preload();
const obj = useLoader(OBJLoader, objPath, (loader) => {
loader.setMaterials(materials);
});
const { size } = useMemo(() => {
const box = new THREE.Box3().setFromObject(obj);
const size = new THREE.Vector3();
const center = new THREE.Vector3();
box.getSize(size);
box.getCenter(center);
// 将模型平移到原点,旋转更自然
obj.position.sub(center);
return { size };
}, [obj]);
useFrame((_state, delta) => {
if (groupRef.current) {
groupRef.current.rotation.y += delta * 0.2; // 轻微自转
}
});
const annotations = useMemo(() => {
const hx = size.x * 0.5;
const hz = size.z * 0.5;
const radius = Math.max(hx, hz) * 1.9 + 0.4; // 外扩半径,避免重叠
const fromEsp32 = new THREE.Vector3(0, 0.05 * size.y, 0);
const toEsp32 = fromEsp32.clone().add(new THREE.Vector3(-radius, 0.22 * size.y, radius)); // 左前
const fromAntenna = new THREE.Vector3(hx * 0.6, 0.1 * size.y, -hz * 0.4);
const toAntenna = fromAntenna.clone().add(new THREE.Vector3(radius, 0.18 * size.y, -radius)); // 右后
const fromPower = new THREE.Vector3(-hx * 0.55, -0.05 * size.y, 0.15 * hz);
const toPower = fromPower.clone().add(new THREE.Vector3(-radius, 0.16 * size.y, -radius)); // 左后
const fromGpio = new THREE.Vector3(0.0, -0.1 * size.y, hz * 0.6);
const toGpio = fromGpio.clone().add(new THREE.Vector3(radius, -0.02 * size.y, radius)); // 右前
return [
{
from: fromEsp32,
to: toEsp32,
title: '小量 V3 · ESP32 主控',
lines: ['双核 Xtensa 240MHz', 'WiFi 802.11 b/g/n', 'Bluetooth LE 5.0']
},
{
from: fromAntenna,
to: toAntenna,
title: '2.4GHz 天线区',
lines: ['射频匹配网络', '无线性能优化']
},
{
from: fromPower,
to: toPower,
title: '电源稳压',
lines: ['LDO/BUCK 稳压', '3.3V 供电,纹波低']
},
{
from: fromGpio,
to: toGpio,
title: 'GPIO 接口',
lines: ['多路 ADC/DAC', 'I2C/SPI/UART']
}
];
}, [size]);
// 将 3D 锚点实时投影为屏幕坐标,交给外部 2D Overlay
useFrame((state) => {
if (!onProject || !groupRef.current) return;
const { camera, size: viewportSize } = state;
const results = annotations.map((a) => {
const world = a.from.clone().applyMatrix4(groupRef.current.matrixWorld);
const ndc = world.clone().project(camera);
const x = (ndc.x * 0.5 + 0.5) * viewportSize.width;
const y = (-ndc.y * 0.5 + 0.5) * viewportSize.height;
return { x, y, title: a.title, lines: a.lines };
});
onProject(results);
});
const titlePosition = useMemo(() => {
if (!size) return [0, 0, 0];
return isMobile
? [-size.x * 0.9, size.y * 0.2, 0]
: [0, size.y * 0.7, 0];
}, [size, isMobile]);
return (
<group ref={groupRef}>
<primitive object={obj} />
{/* 模型标题(点击名称显示详情) */}
<Html position={titlePosition} center style={{ pointerEvents: 'auto' }}>
<div
onClick={onToggleConnections}
title="点击名称显示详情"
style={{
background: 'rgba(0,0,0,0.4)',
border: '1px solid rgba(0,245,212,0.35)',
borderRadius: 12,
padding: '6px 10px',
color: 'white',
fontSize: 14,
whiteSpace: 'nowrap',
backdropFilter: 'blur(8px)',
cursor: 'pointer'
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ fontWeight: 700 }}>{modelName}</span>
<span style={{
background: '#00f5d4',
color: '#0a0f1c',
borderRadius: 10,
padding: '2px 6px',
fontSize: 12,
fontWeight: 600
}}>详情</span>
</div>
<div style={{ opacity: 0.9, fontSize: 12, marginTop: 2 }}>点击名称显示详情</div>
</div>
</Html>
<ContactShadows opacity={0.5} scale={10} blur={2.5} far={4} resolution={1024} color="#1a2336" />
</group>
);
};
const ProductSection = ({ isActive }) => {
const visualRef = useRef();
const [canvasSize, setCanvasSize] = useState({ width: 800, height: 800 });
const [screenAnchors, setScreenAnchors] = useState([]);
const [showConnections, setShowConnections] = useState(false);
const [modelKey, setModelKey] = useState('v3'); // v3 | mini | v2
const [isMobile, setIsMobile] = useState(false);
const modelConfigs = {
v3: {
obj: '/3dmodo/xiaoliang1.obj',
mtl: '/3dmodo/xiaoliang1.mtl',
name: '小量 V3'
},
mini: {
obj: '/3dmimi/3D_PCB_V3-mini.obj',
mtl: '/3dmimi/3D_PCB_V3-mini.mtl',
name: '小量 mini'
},
v2: {
obj: '/3dV2/xiaoliangV2.obj',
mtl: '/3dV2/xiaolaingV2.mtl',
name: '小量 V2'
}
};
useEffect(() => {
const node = visualRef.current;
if (!node) return;
const resizeDirect = () => {
if (!node) return;
setCanvasSize({ width: node.clientWidth, height: node.clientHeight });
};
resizeDirect();
const ro = new ResizeObserver((entries) => {
const entry = entries && entries[0];
if (entry) {
const cr = entry.contentRect;
setCanvasSize({ width: Math.round(cr.width), height: Math.round(cr.height) });
} else {
resizeDirect();
}
});
ro.observe(node);
window.addEventListener('resize', resizeDirect);
return () => {
window.removeEventListener('resize', resizeDirect);
ro.disconnect();
};
}, []);
useEffect(() => {
const check = () => setIsMobile(window.innerWidth <= 768);
check();
window.addEventListener('resize', check);
return () => window.removeEventListener('resize', check);
}, []);
const panelWidth = isMobile ? 0 : 260; // 移动端不占右侧面板
return (
<div className="product-section-minimal">
<motion.div
@@ -10,18 +205,18 @@ const ProductSection = ({ isActive }) => {
animate={isActive ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 1.2, delay: 0.3 }}
>
<motion.h1
<motion.div
className="product-main-title"
initial={{ opacity: 0, y: 40 }}
animate={isActive ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 0.6 }}
>
我们的战略
<br />
<span className="highlight-text">针对</span>
<br />
远见项目
</motion.h1>
<img
src="/AItime.svg"
alt="AI时代标题"
className="title-svg"
/>
</motion.div>
<motion.div
className="product-quote"
@@ -31,13 +226,30 @@ const ProductSection = ({ isActive }) => {
>
<div className="quote-line"></div>
<p>
Radiant的战略投资框架旨在
量迹科技的战略在于
<br />
赋能创业者我们专注于与创始人
将低价AI硬件嵌入更多线下场景
<br />
建立持久关系推动有意义的变革
让人工智能技术普惠更多人
</p>
</motion.div>
<motion.button
className="learn-more-btn"
initial={{ opacity: 0, y: 30 }}
animate={isActive ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8, delay: 1.4 }}
whileHover={{
scale: 1.05,
y: -3,
boxShadow: "0 10px 30px rgba(0, 245, 212, 0.3)"
}}
whileTap={{ scale: 0.95 }}
>
<span className="btn-text">了解更多</span>
<div className="btn-glow"></div>
<div className="btn-arrow"></div>
</motion.button>
</motion.div>
<motion.div
@@ -46,32 +258,156 @@ const ProductSection = ({ isActive }) => {
animate={isActive ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 1.5, delay: 0.5 }}
>
<div className="visual-container">
<motion.div
className="visual-element-1"
animate={isActive ? {
rotateY: [0, 360],
scale: [1, 1.1, 1]
} : {}}
transition={{
duration: 20,
repeat: Infinity,
ease: "linear"
<div className="visual-container" ref={visualRef} style={{ position: 'relative' }}>
<Canvas
dpr={[1, 2]}
camera={{ position: [2.8, 1.8, 3.6], fov: 45 }}
shadows
>
<Suspense fallback={null}>
<ambientLight intensity={0.6} />
<directionalLight position={[5, 7, 5]} intensity={1.0} castShadow shadow-mapSize-width={2048} shadow-mapSize-height={2048} />
<Bounds fit clip observe>
<BoardScene
onProject={setScreenAnchors}
onToggleConnections={() => setShowConnections((v) => !v)}
objPath={modelConfigs[modelKey].obj}
mtlPath={modelConfigs[modelKey].mtl}
modelName={modelConfigs[modelKey].name}
isMobile={isMobile}
/>
</Bounds>
</Suspense>
<OrbitControls makeDefault enableDamping enablePan={false} maxPolarAngle={Math.PI * 0.8} minPolarAngle={Math.PI * 0.2} />
</Canvas>
{/* 2D Overlay桌面端右侧标签+连线;移动端:不在容器内渲染) */}
{showConnections && !isMobile && (
<>
<svg
width={canvasSize.width + panelWidth}
height={canvasSize.height}
style={{ position: 'absolute', left: 0, top: 0, overflow: 'visible', pointerEvents: 'none', zIndex: 1 }}
>
{screenAnchors.map((a, idx) => {
const count = Math.max(1, screenAnchors.length);
const padTop = Math.round(canvasSize.height * 0.15);
const padBottom = Math.round(canvasSize.height * 0.15);
const avail = Math.max(0, canvasSize.height - padTop - padBottom);
const step = count > 1 ? avail / (count - 1) : 0;
const labelY = Math.round(padTop + idx * step);
const startX = Math.min(Math.max(a.x, 0), canvasSize.width);
const startY = Math.min(Math.max(a.y, 0), canvasSize.height);
const endX = canvasSize.width + 16;
const endY = labelY + 16;
const ctrlX = (startX + endX) / 2 + 40;
const ctrlY = (startY + endY) / 2 - 20;
const d = `M ${startX},${startY} Q ${ctrlX},${ctrlY} ${endX},${endY}`;
return (
<path key={`p-${idx}`} d={d} stroke="#00f5d4" strokeWidth="1.5" fill="none" />
);
})}
</svg>
{screenAnchors.map((a, idx) => {
const count = Math.max(1, screenAnchors.length);
const padTop = Math.round(canvasSize.height * 0.15);
const padBottom = Math.round(canvasSize.height * 0.15);
const avail = Math.max(0, canvasSize.height - padTop - padBottom);
const step = count > 1 ? avail / (count - 1) : 0;
const labelY = Math.round(padTop + idx * step);
const left = canvasSize.width + 24;
return (
<div
key={`l-${idx}`}
style={{
position: 'absolute',
left,
top: labelY,
width: panelWidth - 40,
background: 'rgba(10,15,28,0.85)',
border: '1px solid rgba(0,245,212,0.35)',
borderRadius: 12,
padding: '10px 12px',
color: 'white',
fontSize: 13,
boxShadow: '0 8px 25px rgba(0, 245, 212, 0.15)',
pointerEvents: 'auto',
zIndex: 2
}}
>
<div style={{ fontWeight: 700, color: '#00f5d4', marginBottom: 4 }}>{a.title}</div>
{a.lines?.map((t, i2) => (
<div key={i2} style={{ opacity: 0.9 }}>{t}</div>
))}
</div>
);
})}
</>
)}
{/* 模型切换滑动按钮 */}
<div
style={{
position: 'absolute',
left: '50%',
bottom: 16,
transform: 'translateX(-50%)',
display: 'flex',
gap: 8,
background: 'rgba(10,15,28,0.6)',
border: '1px solid rgba(255,255,255,0.15)',
borderRadius: 20,
padding: '6px 8px',
backdropFilter: 'blur(8px)',
zIndex: 10,
pointerEvents: 'auto'
}}
/>
<motion.div
className="visual-element-2"
animate={isActive ? {
rotateX: [0, 360],
y: [0, -20, 0]
} : {}}
transition={{
duration: 15,
repeat: Infinity,
ease: "easeInOut"
}}
/>
>
{[
{ key: 'v3', label: 'V3' },
{ key: 'mini', label: 'mini' },
{ key: 'v2', label: 'V2' }
].map((btn) => (
<button
key={btn.key}
onClick={() => setModelKey(btn.key)}
style={{
border: 'none',
outline: 'none',
padding: '6px 10px',
borderRadius: 14,
cursor: 'pointer',
color: btn.key === modelKey ? '#0a0f1c' : '#ffffff',
background: btn.key === modelKey ? '#00f5d4' : 'transparent'
}}
>
{btn.label}
</button>
))}
</div>
</div>
{/* 移动端:画布下方展示标签列表(不画连线) */}
{showConnections && isMobile && (
<div style={{ width: '100%', marginTop: 12, display: 'flex', flexDirection: 'column', gap: 8 }}>
{screenAnchors.map((a, idx) => (
<div
key={`ml-${idx}`}
style={{
background: 'rgba(10,15,28,0.85)',
border: '1px solid rgba(0,245,212,0.35)',
borderRadius: 12,
padding: '10px 12px',
color: 'white',
fontSize: 13,
boxShadow: '0 8px 25px rgba(0, 245, 212, 0.15)'
}}
>
<div style={{ fontWeight: 700, color: '#00f5d4', marginBottom: 4 }}>{a.title}</div>
{a.lines?.map((t, i2) => (
<div key={i2} style={{ opacity: 0.9 }}>{t}</div>
))}
</div>
))}
</div>
)}
</motion.div>
{/* 进度指示器 */}