最新
This commit is contained in:
856
src/App.css
856
src/App.css
File diff suppressed because it is too large
Load Diff
56
src/App.js
56
src/App.js
@@ -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
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
BIN
src/asset/logo-bai1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 280 KiB |
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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', 'Wi‑Fi 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>
|
||||
|
||||
{/* 进度指示器 */}
|
||||
|
||||
Reference in New Issue
Block a user