fix: 3D Show

This commit is contained in:
xiaoma
2026-02-02 19:10:34 +08:00
commit b8024da3dc
61 changed files with 4123 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
import React, { useState } from 'react';
import { Layout as AntLayout, Menu, ConfigProvider, theme, Drawer, Button } from 'antd';
import { RobotOutlined, MenuOutlined, AppstoreOutlined, EyeOutlined } from '@ant-design/icons';
import { useNavigate, useLocation } from 'react-router-dom';
import ParticleBackground from './ParticleBackground';
import { motion, AnimatePresence } from 'framer-motion';
const { Header, Content, Footer } = AntLayout;
const Layout = ({ children }) => {
const navigate = useNavigate();
const location = useLocation();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const items = [
{
key: '/',
icon: <RobotOutlined />,
label: 'AI 硬件',
},
{
key: '/services',
icon: <AppstoreOutlined />,
label: 'AI 服务',
},
{
key: '/ar',
icon: <EyeOutlined />,
label: 'AR 体验',
},
{
key: 'more',
label: '...',
},
];
const handleMenuClick = (key) => {
if (key === 'more') return;
navigate(key);
setMobileMenuOpen(false);
};
return (
<ConfigProvider
theme={{
algorithm: theme.darkAlgorithm,
token: {
colorPrimary: '#00b96b',
colorBgContainer: 'transparent',
colorBgLayout: 'transparent',
fontFamily: "'Orbitron', sans-serif",
},
}}
>
<ParticleBackground />
<AntLayout style={{ minHeight: '100vh', background: 'transparent' }}>
<Header
style={{
position: 'fixed',
top: 0,
left: 0,
zIndex: 1000,
width: '100%',
padding: 0,
background: 'rgba(0, 0, 0, 0.7)',
backdropFilter: 'blur(20px)',
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
display: 'flex',
height: '72px',
lineHeight: '72px',
boxShadow: '0 4px 30px rgba(0, 0, 0, 0.5)'
}}
>
<div style={{
width: '100%',
padding: '0 40px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
height: '100%'
}}>
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
style={{
color: '#fff',
fontSize: '20px',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
cursor: 'pointer'
}}
onClick={() => navigate('/')}
>
<img src="/liangji_logo.svg" alt="Quant Speed Logo" style={{ height: '40px', filter: 'invert(1) brightness(2)' }} />
</motion.div>
{/* Desktop Menu */}
<div className="desktop-menu" style={{ display: 'none', flex: 1 }}>
<Menu
theme="dark"
mode="horizontal"
selectedKeys={[location.pathname]}
items={items}
onClick={(e) => handleMenuClick(e.key)}
style={{
background: 'transparent',
borderBottom: 'none',
display: 'flex',
justifyContent: 'flex-end',
minWidth: '400px'
}}
/>
</div>
<style>{`
@media (min-width: 768px) {
.desktop-menu { display: block !important; }
.mobile-menu-btn { display: none !important; }
}
`}</style>
{/* Mobile Menu Button */}
<Button
className="mobile-menu-btn"
type="text"
icon={<MenuOutlined style={{ color: '#fff', fontSize: 20 }} />}
onClick={() => setMobileMenuOpen(true)}
/>
</div>
</Header>
{/* Mobile Drawer Menu */}
<Drawer
title={<span style={{ color: '#00b96b' }}>导航菜单</span>}
placement="right"
onClose={() => setMobileMenuOpen(false)}
open={mobileMenuOpen}
styles={{ body: { padding: 0, background: '#111' }, header: { background: '#111', borderBottom: '1px solid #333' }, wrapper: { width: 250 } }}
>
<Menu
theme="dark"
mode="vertical"
selectedKeys={[location.pathname]}
items={items}
onClick={(e) => handleMenuClick(e.key)}
style={{ background: 'transparent', borderRight: 'none' }}
/>
</Drawer>
<Content style={{ marginTop: 72, padding: '40px 20px', overflowX: 'hidden' }}>
<div style={{
maxWidth: '1200px',
margin: '0 auto',
width: '100%',
minHeight: 'calc(100vh - 128px)'
}}>
<AnimatePresence mode="wait">
<motion.div
key={location.pathname}
initial={{ opacity: 0, y: 20, filter: 'blur(10px)' }}
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
exit={{ opacity: 0, y: -20, filter: 'blur(10px)' }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
</AnimatePresence>
</div>
</Content>
<Footer style={{ textAlign: 'center', background: 'rgba(0,0,0,0.5)', color: '#666', backdropFilter: 'blur(5px)' }}>
Quant Speed AI Hardware ©{new Date().getFullYear()} Created by Quant Speed Tech
</Footer>
</AntLayout>
</ConfigProvider>
);
};
export default Layout;

View File

@@ -0,0 +1,70 @@
import React, { Suspense } from 'react';
import { Canvas, useLoader } from '@react-three/fiber';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader';
import { OrbitControls, Stage, useProgress, Environment, ContactShadows } from '@react-three/drei';
import { Spin } from 'antd';
const Model = ({ objPath, mtlPath, scale = 1 }) => {
const materials = useLoader(MTLLoader, mtlPath);
const obj = useLoader(OBJLoader, objPath, (loader) => {
materials.preload();
loader.setMaterials(materials);
});
const clone = obj.clone();
return <primitive object={clone} scale={scale} />;
};
const LoadingOverlay = () => {
const { progress, active } = useProgress();
if (!active) return null;
return (
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'rgba(0,0,0,0.5)',
zIndex: 10,
pointerEvents: 'none'
}}>
<div style={{ textAlign: 'center' }}>
<Spin size="large" />
<div style={{ color: '#00b96b', marginTop: 10, fontWeight: 'bold' }}>
{progress.toFixed(0)}% Loading
</div>
</div>
</div>
);
};
const ModelViewer = ({ objPath, mtlPath, scale = 1, autoRotate = true }) => {
return (
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
<LoadingOverlay />
<Canvas shadows dpr={[1, 2]} camera={{ fov: 45, position: [0, 0, 5] }} style={{ height: '100%', width: '100%' }}>
<ambientLight intensity={0.7} />
<pointLight position={[10, 10, 10]} intensity={1} />
<spotLight position={[-10, 10, 10]} angle={0.15} penumbra={1} intensity={1} />
<Suspense fallback={null}>
<Stage environment="city" intensity={0.6} adjustCamera={true}>
<Model objPath={objPath} mtlPath={mtlPath} scale={scale} />
</Stage>
<Environment preset="city" />
<ContactShadows position={[0, -0.8, 0]} opacity={0.4} scale={10} blur={2} far={0.8} />
</Suspense>
<OrbitControls autoRotate={autoRotate} makeDefault />
</Canvas>
</div>
);
};
export default ModelViewer;

View File

@@ -0,0 +1,92 @@
import React, { useEffect, useRef } from 'react';
const ParticleBackground = () => {
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
let animationFrameId;
const resizeCanvas = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
};
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
const particles = [];
const particleCount = 100;
class Particle {
constructor() {
this.x = Math.random() * canvas.width;
this.y = Math.random() * canvas.height;
this.vx = (Math.random() - 0.5) * 0.5;
this.vy = (Math.random() - 0.5) * 0.5;
this.size = Math.random() * 2;
this.color = Math.random() > 0.5 ? 'rgba(0, 185, 107, ' : 'rgba(0, 240, 255, '; // Green or Blue
}
update() {
this.x += this.vx;
this.y += this.vy;
if (this.x < 0 || this.x > canvas.width) this.vx *= -1;
if (this.y < 0 || this.y > canvas.height) this.vy *= -1;
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = this.color + Math.random() * 0.5 + ')';
ctx.fill();
}
}
for (let i = 0; i < particleCount; i++) {
particles.push(new Particle());
}
const animate = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw connecting lines
ctx.lineWidth = 0.5;
for (let i = 0; i < particleCount; i++) {
for (let j = i; j < particleCount; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
ctx.beginPath();
ctx.strokeStyle = `rgba(100, 255, 218, ${1 - distance / 100})`;
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.stroke();
}
}
}
particles.forEach(p => {
p.update();
p.draw();
});
animationFrameId = requestAnimationFrame(animate);
};
animate();
return () => {
window.removeEventListener('resize', resizeCanvas);
cancelAnimationFrame(animationFrameId);
};
}, []);
return <canvas ref={canvasRef} id="particle-canvas" />;
};
export default ParticleBackground;