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,172 @@
import React, { useEffect, useState } from 'react';
import { Row, Col, Typography, Button, Spin } from 'antd';
import { motion } from 'framer-motion';
import { RightOutlined } from '@ant-design/icons';
import { getServices } from '../api';
import { useNavigate } from 'react-router-dom';
const { Title, Paragraph } = Typography;
const AIServices = () => {
const [services, setServices] = useState([]);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
useEffect(() => {
const fetchServices = async () => {
try {
const response = await getServices();
setServices(response.data);
} catch (error) {
console.error("Failed to fetch services:", error);
} finally {
setLoading(false);
}
};
fetchServices();
}, []);
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '100px 0' }}>
<Spin size="large" tip="Loading services..." />
</div>
);
}
return (
<div style={{ padding: '20px 0' }}>
<div style={{ textAlign: 'center', marginBottom: 60 }}>
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.8 }}
>
<Title level={1} style={{ color: '#fff', fontSize: 'clamp(2rem, 4vw, 3rem)' }}>
AI 全栈<span style={{ color: '#00f0ff', textShadow: '0 0 10px rgba(0,240,255,0.5)' }}>解决方案</span>
</Title>
</motion.div>
<Paragraph style={{ color: '#888', maxWidth: 700, margin: '0 auto', fontSize: 16 }}>
从数据处理到模型部署我们为您提供一站式 AI 基础设施服务
</Paragraph>
</div>
<Row gutter={[32, 32]} justify="center">
{services.map((item, index) => (
<Col xs={24} md={8} key={item.id}>
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.2, duration: 0.5 }}
whileHover={{ scale: 1.03 }}
onClick={() => navigate(`/services/${item.id}`)}
style={{ cursor: 'pointer' }}
>
<div
className="glass-panel"
style={{
padding: 30,
height: '100%',
position: 'relative',
overflow: 'hidden',
border: `1px solid ${item.color}33`,
boxShadow: `0 0 20px ${item.color}11`
}}
>
{/* HUD 装饰线 */}
<div style={{ position: 'absolute', top: 0, left: 0, width: 20, height: 2, background: item.color }} />
<div style={{ position: 'absolute', top: 0, left: 0, width: 2, height: 20, background: item.color }} />
<div style={{ position: 'absolute', bottom: 0, right: 0, width: 20, height: 2, background: item.color }} />
<div style={{ position: 'absolute', bottom: 0, right: 0, width: 2, height: 20, background: item.color }} />
<div style={{ marginBottom: 20, display: 'flex', alignItems: 'center' }}>
<div style={{
width: 60, height: 60,
borderRadius: '50%',
background: `${item.color}22`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
marginRight: 15,
overflow: 'hidden'
}}>
{item.display_icon ? (
<img src={item.display_icon} alt={item.title} style={{ width: '60%', height: '60%', objectFit: 'contain' }} />
) : (
<div style={{ width: 30, height: 30, background: item.color, borderRadius: '50%' }} />
)}
</div>
<h3 style={{ margin: 0, fontSize: 22, color: '#fff' }}>{item.title}</h3>
</div>
<p style={{ color: '#ccc', lineHeight: 1.6, minHeight: 60 }}>{item.description}</p>
<div style={{ marginTop: 20 }}>
{item.features_list && item.features_list.map((feat, i) => (
<div key={i} style={{
display: 'flex', alignItems: 'center', marginBottom: 8, color: item.color
}}>
<div style={{ width: 6, height: 6, background: item.color, marginRight: 10, borderRadius: '50%' }} />
{feat}
</div>
))}
</div>
<Button
type="link"
style={{ padding: 0, marginTop: 20, color: '#fff' }}
icon={<RightOutlined />}
onClick={(e) => {
e.stopPropagation();
navigate(`/services/${item.id}`);
}}
>
了解更多
</Button>
</div>
</motion.div>
</Col>
))}
</Row>
{/* 动态流程图模拟 */}
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
style={{ marginTop: 80, padding: 40, background: 'rgba(0,0,0,0.3)', borderRadius: 20, border: '1px dashed #333', textAlign: 'center' }}
>
<Title level={3} style={{ color: '#fff', marginBottom: 40 }}>服务流程</Title>
<Row justify="space-around" align="middle" gutter={[20, 20]}>
{['需求分析', '数据准备', '模型训练', '测试验证', '私有化部署'].map((step, i) => (
<Col key={i} xs={12} md={4}>
<div style={{
width: '100%', aspectRatio: '1',
border: '2px solid #333', borderRadius: '50%',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: '#888', fontSize: 16, fontWeight: 'bold',
position: 'relative'
}}>
{step}
{/* 简单的连接线模拟 */}
{i < 4 && (
<div className="process-arrow" style={{
position: 'absolute', right: -20, top: '50%',
width: 20, height: 2, background: '#333',
display: 'none' // 移动端隐藏
}} />
)}
</div>
</Col>
))}
</Row>
<style>{`
@media (min-width: 768px) {
.process-arrow { display: block !important; }
}
`}</style>
</motion.div>
</div>
);
};
export default AIServices;

View File

@@ -0,0 +1,111 @@
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Button, Typography, Spin, Row, Col, Empty } from 'antd';
import { ScanOutlined } from '@ant-design/icons';
import { getARServices } from '../api';
const { Title, Paragraph } = Typography;
const ARExperience = () => {
const [scanning, setScanning] = useState(true);
const [arServices, setArServices] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchAR = async () => {
try {
const res = await getARServices();
setArServices(res.data);
} catch (error) {
console.error("Failed to fetch AR services:", error);
} finally {
setLoading(false);
}
}
fetchAR();
}, []);
if (loading) return <div style={{ textAlign: 'center', padding: 100 }}><Spin size="large" /></div>;
return (
<div style={{ padding: '40px 0', minHeight: '80vh', position: 'relative' }}>
<div style={{ textAlign: 'center', marginBottom: 60, position: 'relative', zIndex: 2 }}>
<Title level={1} style={{ color: '#fff', letterSpacing: 4 }}>
AR <span style={{ color: '#00f0ff' }}>UNIVERSE</span>
</Title>
<Paragraph style={{ color: '#aaa', fontSize: 18, maxWidth: 600, margin: '0 auto' }}>
探索全息增强现实体验请佩戴您的设备或使用移动端摄像头扫描空间
</Paragraph>
</div>
{arServices.length === 0 ? (
<div style={{ textAlign: 'center', marginTop: 100, zIndex: 2, position: 'relative' }}>
<Empty description={<span style={{ color: '#666' }}>暂无 AR 体验内容</span>} />
</div>
) : (
<Row gutter={[32, 32]} justify="center" style={{ padding: '0 20px', position: 'relative', zIndex: 2 }}>
{arServices.map((item, index) => (
<Col xs={24} md={12} lg={8} key={item.id}>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
whileHover={{ scale: 1.02 }}
>
<div style={{
background: 'rgba(255,255,255,0.05)',
border: '1px solid rgba(0,240,255,0.2)',
borderRadius: 12,
overflow: 'hidden',
height: '100%'
}}>
<div style={{ height: 200, background: '#000', overflow: 'hidden', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{item.display_cover_image ? (
<img src={item.display_cover_image} alt={item.title} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
) : (
<ScanOutlined style={{ fontSize: 40, color: '#333' }} />
)}
</div>
<div style={{ padding: 20 }}>
<h3 style={{ color: '#fff', fontSize: 20 }}>{item.title}</h3>
<p style={{ color: '#888', marginBottom: 20, minHeight: 44 }}>{item.description}</p>
<Button type="primary" block ghost style={{ borderColor: '#00f0ff', color: '#00f0ff' }}>
启动体验
</Button>
</div>
</div>
</motion.div>
</Col>
))}
</Row>
)}
{/* 装饰性背景 */}
<div style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: `
radial-gradient(circle at 50% 50%, rgba(0, 240, 255, 0.05) 0%, transparent 50%)
`,
zIndex: 0,
pointerEvents: 'none'
}} />
<div style={{
position: 'fixed',
bottom: 0,
width: '100%',
height: '300px',
background: `linear-gradient(to top, rgba(0,0,0,0.8), transparent)`,
zIndex: 1,
pointerEvents: 'none'
}} />
</div>
);
};
export default ARExperience;

View File

@@ -0,0 +1,78 @@
.tech-card {
background: rgba(255, 255, 255, 0.05) !important;
border: 1px solid #303030 !important;
transition: all 0.3s ease;
cursor: pointer;
box-shadow: none !important; /* 强制移除默认阴影 */
overflow: hidden; /* 确保子元素不会溢出产生黑边 */
outline: none;
}
.tech-card:hover {
border-color: #00b96b !important;
box-shadow: 0 0 20px rgba(0, 185, 107, 0.4) !important; /* 增强悬停发光 */
transform: translateY(-5px);
}
.tech-card .ant-card-body {
border-top: none !important;
box-shadow: none !important;
}
.tech-card-title {
color: #fff;
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
}
.tech-price {
color: #00b96b;
font-size: 20px;
font-weight: bold;
}
.product-scroll-container {
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
padding: 30px 20px; /* 增加左右内边距,为悬停缩放和投影留出空间 */
margin: 0 -20px; /* 使用负外边距抵消内边距,使滚动条能延伸到版心边缘 */
width: calc(100% + 40px);
}
/* 自定义滚动条 */
.product-scroll-container::-webkit-scrollbar {
height: 6px;
}
.product-scroll-container::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
margin: 0 20px; /* 让滚动条轨道在版心内显示 */
}
.product-scroll-container::-webkit-scrollbar-thumb {
background: rgba(0, 185, 107, 0.2);
border-radius: 3px;
transition: all 0.3s;
}
.product-scroll-container::-webkit-scrollbar-thumb:hover {
background: rgba(0, 185, 107, 0.5);
}
/* 布局对齐 */
.product-scroll-container .ant-row {
margin-left: 0 !important;
margin-right: 0 !important;
padding: 0;
display: flex;
flex-wrap: nowrap;
justify-content: flex-start;
}
.product-scroll-container .ant-col {
flex: 0 0 320px;
padding: 0 12px;
}

177
frontend/src/pages/Home.jsx Normal file
View File

@@ -0,0 +1,177 @@
import React, { useEffect, useState } from 'react';
import { Card, Row, Col, Tag, Button, Spin, Typography } from 'antd';
import { RocketOutlined, RightOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { getConfigs } from '../api';
import './Home.css';
const { Title, Paragraph } = Typography;
const Home = () => {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [typedText, setTypedText] = useState('');
const [isTypingComplete, setIsTypingComplete] = useState(false);
const fullText = "未来已来 AI 核心驱动";
const navigate = useNavigate();
useEffect(() => {
fetchProducts();
let i = 0;
const typingInterval = setInterval(() => {
i++;
setTypedText(fullText.slice(0, i));
if (i >= fullText.length) {
clearInterval(typingInterval);
setIsTypingComplete(true);
}
}, 150);
return () => clearInterval(typingInterval);
}, []);
const fetchProducts = async () => {
try {
const response = await getConfigs();
setProducts(response.data);
} catch (error) {
console.error('Failed to fetch products:', error);
} finally {
setLoading(false);
}
};
const cardVariants = {
hidden: { opacity: 0, y: 50 },
visible: (i) => ({
opacity: 1,
y: 0,
transition: {
delay: i * 0.1,
duration: 0.5,
type: "spring",
stiffness: 100
}
}),
hover: {
scale: 1.05,
rotateX: 5,
rotateY: 5,
transition: { duration: 0.3 }
}
};
if (loading) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
<Spin size="large" tip="加载硬件配置中..." />
</div>
);
}
return (
<div>
<div style={{ textAlign: 'center', marginBottom: 60 }}>
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 1 }}
style={{ marginBottom: 30 }}
>
<motion.img
src="/gXEu5E01.svg"
alt="Quant Speed Logo"
animate={{
filter: [
'invert(1) brightness(2) drop-shadow(0 0 10px rgba(0, 240, 255, 0.3))',
'invert(1) brightness(2) drop-shadow(0 0 20px rgba(0, 240, 255, 0.7))',
'invert(1) brightness(2) drop-shadow(0 0 10px rgba(0, 240, 255, 0.3))'
]
}}
transition={{ duration: 3, repeat: Infinity, ease: "easeInOut" }}
style={{ width: 180, height: 'auto' }}
/>
</motion.div>
<Title level={1} style={{ color: '#fff', fontSize: 'clamp(2rem, 5vw, 4rem)', marginBottom: 20, minHeight: '60px' }}>
<span className="neon-text-green">{typedText}</span>
{!isTypingComplete && <span className="cursor-blink">|</span>}
</Title>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 2, duration: 1 }}
>
<Paragraph style={{ color: '#aaa', fontSize: '18px', maxWidth: 600, margin: '0 auto', lineHeight: '1.6' }}>
量迹 AI 硬件为您提供最强大的边缘计算能力搭载最新一代神经处理单元赋能您的每一个创意
</Paragraph>
</motion.div>
</div>
<div className="product-scroll-container">
<Row gutter={[24, 24]} wrap={false}>
{products.map((product, index) => (
<Col key={product.id} flex="0 0 320px">
<motion.div
custom={index}
initial="hidden"
animate="visible"
whileHover="hover"
variants={cardVariants}
style={{ perspective: 1000 }}
>
<Card
className="tech-card glass-panel"
bordered={false}
cover={
<div style={{
height: 200,
background: 'linear-gradient(135deg, rgba(31,31,31,0.8), rgba(42,42,42,0.8))',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
color: '#444',
borderBottom: '1px solid rgba(255,255,255,0.05)'
}}>
<motion.div
animate={{ y: [0, -10, 0] }}
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }}
>
<RocketOutlined style={{ fontSize: 60, color: '#00b96b' }} />
</motion.div>
</div>
}
onClick={() => navigate(`/product/${product.id}`)}
>
<div className="tech-card-title neon-text-blue">{product.name}</div>
<div style={{ marginBottom: 10, height: 40, overflow: 'hidden', color: '#bbb' }}>
{product.description}
</div>
<div style={{ marginBottom: 15 }}>
<Tag color="cyan" style={{ background: 'rgba(0,255,255,0.1)', border: '1px solid cyan' }}>{product.chip_type}</Tag>
{product.has_camera && <Tag color="blue" style={{ background: 'rgba(0,0,255,0.1)', border: '1px solid blue' }}>Camera</Tag>}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className="tech-price neon-text-green">¥{product.price}</div>
<Button type="primary" shape="circle" icon={<RightOutlined />} style={{ background: '#00b96b', borderColor: '#00b96b' }} />
</div>
</Card>
</motion.div>
</Col>
))}
</Row>
</div>
<style>{`
.cursor-blink {
animation: blink 1s step-end infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
`}</style>
</div>
);
};
export default Home;

View File

@@ -0,0 +1,52 @@
.payment-container {
max-width: 600px;
margin: 50px auto;
padding: 40px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid #303030;
border-radius: 12px;
text-align: center;
}
.payment-title {
color: #fff;
font-size: 28px;
margin-bottom: 30px;
}
.payment-amount {
font-size: 48px;
color: #00b96b;
font-weight: bold;
margin: 20px 0;
}
.payment-info {
text-align: left;
background: rgba(0,0,0,0.3);
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
color: #ccc;
}
.payment-method {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 30px;
}
.payment-method-item {
border: 1px solid #444;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
color: #fff;
}
.payment-method-item.active {
border-color: #00b96b;
background: rgba(0, 185, 107, 0.1);
}

View File

@@ -0,0 +1,183 @@
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Button, message, Result, Spin, QRCode } from 'antd';
import { WechatOutlined, AlipayCircleOutlined, CheckCircleOutlined } from '@ant-design/icons';
import { getOrder, initiatePayment, confirmPayment } from '../api';
import './Payment.css';
const Payment = () => {
const { orderId } = useParams();
const navigate = useNavigate();
const [order, setOrder] = useState(null);
const [loading, setLoading] = useState(true);
const [paying, setPaying] = useState(false);
const [paySuccess, setPaySuccess] = useState(false);
const [paymentMethod, setPaymentMethod] = useState('wechat');
useEffect(() => {
fetchOrder();
}, [orderId]);
const fetchOrder = async () => {
try {
const response = await getOrder(orderId);
setOrder(response.data);
} catch (error) {
console.error('Failed to fetch order:', error);
// Fallback if getOrder API fails (404/405), we might show basic info or error
// Assuming for now it works or we handle it
message.error('无法获取订单信息,请重试');
} finally {
setLoading(false);
}
};
const handlePay = async () => {
if (paymentMethod === 'alipay') {
message.info('暂未开通支付宝支付,请使用微信支付');
return;
}
setPaying(true);
try {
// 1. 获取微信支付参数
const response = await initiatePayment(orderId);
const payData = response.data;
if (typeof WeixinJSBridge === 'undefined') {
message.warning('请在微信内置浏览器中打开以完成支付');
setPaying(false);
return;
}
// 2. 调用微信支付
const onBridgeReady = () => {
window.WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId": payData.appId, // 公众号名称,由商户传入
"timeStamp": payData.timeStamp, // 时间戳自1970年以来的秒数
"nonceStr": payData.nonceStr, // 随机串
"package": payData.package,
"signType": payData.signType, // 微信签名方式:
"paySign": payData.paySign // 微信签名
},
function(res) {
setPaying(false);
if (res.err_msg == "get_brand_wcpay_request:ok") {
message.success('支付成功!');
setPaySuccess(true);
// 这里可以再次调用后端查询接口确认状态,但通常 JSAPI 回调 ok 即可认为成功
// 为了保险,可以去轮询一下后端状态,或者直接展示成功页
} else if (res.err_msg == "get_brand_wcpay_request:cancel") {
message.info('支付已取消');
} else {
message.error('支付失败,请重试');
console.error('WeChat Pay Error:', res);
}
}
);
};
if (typeof window.WeixinJSBridge == "undefined") {
if (document.addEventListener) {
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
} else if (document.attachEvent) {
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
} else {
onBridgeReady();
}
} catch (error) {
console.error(error);
if (error.response && error.response.data && error.response.data.error) {
message.error(error.response.data.error);
} else {
message.error('支付发起失败,请稍后重试');
}
setPaying(false);
}
};
if (loading) return <div style={{ padding: 50, textAlign: 'center' }}><Spin size="large" /></div>;
if (paySuccess) {
return (
<div className="payment-container" style={{ borderColor: '#00b96b' }}>
<Result
status="success"
icon={<CheckCircleOutlined style={{ color: '#00b96b' }} />}
title={<span style={{ color: '#fff' }}>支付成功</span>}
subTitle={<span style={{ color: '#888' }}>订单 {orderId} 已完成支付我们将尽快为您发货</span>}
extra={[
<Button type="primary" key="home" onClick={() => navigate('/')}>
返回首页
</Button>,
]}
/>
</div>
);
}
return (
<div className="payment-container">
<div className="payment-title">收银台</div>
{order ? (
<>
<div className="payment-amount">¥{order.total_price}</div>
<div className="payment-info">
<p><strong>订单编号</strong> {order.id}</p>
<p><strong>商品名称</strong> {order.config_name || 'AI 硬件设备'}</p>
<p><strong>收货人</strong> {order.customer_name}</p>
</div>
</>
) : (
<div className="payment-info">
<p>订单 ID: {orderId}</p>
<p>无法加载详情但您可以尝试支付</p>
</div>
)}
<div style={{ color: '#fff', marginBottom: 15, textAlign: 'left' }}>选择支付方式</div>
<div className="payment-method">
<div
className={`payment-method-item ${paymentMethod === 'wechat' ? 'active' : ''}`}
onClick={() => setPaymentMethod('wechat')}
>
<WechatOutlined style={{ color: '#09BB07', fontSize: 24, verticalAlign: 'middle', marginRight: 8 }} />
微信支付
</div>
<div
className={`payment-method-item ${paymentMethod === 'alipay' ? 'active' : ''}`}
onClick={() => setPaymentMethod('alipay')}
>
<AlipayCircleOutlined style={{ color: '#1677FF', fontSize: 24, verticalAlign: 'middle', marginRight: 8 }} />
支付宝
</div>
</div>
{paying && (
<div style={{ margin: '20px 0', padding: 20, background: '#fff', borderRadius: 8, display: 'inline-block' }}>
<QRCode value={`mock-payment-${orderId}`} bordered={false} />
<p style={{ color: '#000', marginTop: 10 }}>请扫码支付 (模拟)</p>
</div>
)}
{!paying && (
<Button
type="primary"
size="large"
block
onClick={handlePay}
style={{ height: 50, fontSize: 18, background: paymentMethod === 'wechat' ? '#09BB07' : '#1677FF' }}
>
立即支付
</Button>
)}
</div>
);
};
export default Payment;

View File

@@ -0,0 +1,33 @@
.product-detail-container {
color: #fff;
}
.feature-section {
padding: 60px 0;
border-bottom: 1px solid #303030;
text-align: center;
}
.feature-title {
font-size: 32px;
font-weight: bold;
margin-bottom: 20px;
color: #00b96b;
}
.feature-desc {
font-size: 18px;
color: #888;
max-width: 800px;
margin: 0 auto;
}
.spec-tag {
background: rgba(0, 185, 107, 0.1);
border: 1px solid #00b96b;
color: #00b96b;
padding: 5px 15px;
border-radius: 4px;
margin-right: 10px;
display: inline-block;
}

View File

@@ -0,0 +1,232 @@
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
import { Button, Row, Col, Tag, Statistic, Modal, Form, Input, InputNumber, message, Spin, Descriptions } from 'antd';
import { ShoppingCartOutlined, SafetyCertificateOutlined, ThunderboltOutlined, EyeOutlined, StarOutlined } from '@ant-design/icons';
import { getConfigs, createOrder } from '../api';
import ModelViewer from '../components/ModelViewer';
import './ProductDetail.css';
const ProductDetail = () => {
const { id } = useParams();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [form] = Form.useForm();
const refCode = searchParams.get('ref');
useEffect(() => {
fetchProduct();
}, [id]);
const fetchProduct = async () => {
try {
const response = await getConfigs();
const found = response.data.find(p => String(p.id) === id);
if (found) {
setProduct(found);
} else {
message.error('未找到该产品');
navigate('/');
}
} catch (error) {
console.error('Failed to fetch product:', error);
message.error('加载失败');
} finally {
setLoading(false);
}
};
const handleBuy = async (values) => {
setSubmitting(true);
try {
const orderData = {
config: product.id,
quantity: values.quantity,
customer_name: values.customer_name,
phone_number: values.phone_number,
shipping_address: values.shipping_address,
ref_code: refCode
};
const response = await createOrder(orderData);
message.success('订单创建成功');
navigate(`/payment/${response.data.id}`);
} catch (error) {
console.error(error);
message.error('创建订单失败,请检查填写信息');
} finally {
setSubmitting(false);
}
};
const getModelPaths = (p) => {
if (!p) return null;
const text = (p.name + p.description).toLowerCase();
if (text.includes('mini')) {
return { obj: '/3dmimi/3D_PCB_V3-mini.obj', mtl: '/3dmimi/3D_PCB_V3-mini.mtl' };
} else if (text.includes('v2')) {
return { obj: '/3dV2/xiaoliangV2.obj', mtl: '/3dV2/xiaoliangV2.mtl' };
} else if (text.includes('vision') || text.includes('视觉') || text.includes('camera')) {
return { obj: '/3dmodo/xiaoliang1.obj', mtl: '/3dmodo/xiaoliang1.mtl' };
}
return null;
};
const modelPaths = getModelPaths(product);
const renderIcon = (feature) => {
if (feature.display_icon) {
return <img src={feature.display_icon} alt={feature.title} style={{ width: 60, height: 60, objectFit: 'contain', marginBottom: 20 }} />;
}
const iconProps = { style: { fontSize: 60, color: '#00b96b', marginBottom: 20 } };
switch(feature.icon_name) {
case 'SafetyCertificate':
return <SafetyCertificateOutlined {...iconProps} />;
case 'Eye':
return <EyeOutlined {...iconProps} style={{ ...iconProps.style, color: '#1890ff' }} />;
case 'Thunderbolt':
return <ThunderboltOutlined {...iconProps} style={{ ...iconProps.style, color: '#faad14' }} />;
default:
return <StarOutlined {...iconProps} />;
}
};
if (loading) return <div style={{ padding: 50, textAlign: 'center' }}><Spin size="large" /></div>;
if (!product) return null;
return (
<div className="product-detail-container" style={{ paddingBottom: '60px' }}>
{/* Hero Section */}
<Row gutter={40} align="middle" style={{ minHeight: '60vh' }}>
<Col xs={24} md={12}>
<div style={{
height: 400,
background: 'radial-gradient(circle, #2a2a2a 0%, #000 100%)',
borderRadius: 20,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
border: '1px solid #333',
overflow: 'hidden'
}}>
{modelPaths ? (
<ModelViewer objPath={modelPaths.obj} mtlPath={modelPaths.mtl} />
) : (
<ThunderboltOutlined style={{ fontSize: 120, color: '#00b96b' }} />
)}
</div>
</Col>
<Col xs={24} md={12}>
<h1 style={{ fontSize: 48, fontWeight: 'bold', color: '#fff' }}>{product.name}</h1>
<p style={{ fontSize: 20, color: '#888', margin: '20px 0' }}>{product.description}</p>
<div style={{ marginBottom: 30 }}>
<span className="spec-tag">{product.chip_type}</span>
{product.has_camera && <span className="spec-tag">高清摄像头</span>}
{product.has_microphone && <span className="spec-tag">阵列麦克风</span>}
</div>
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 20, marginBottom: 40 }}>
<Statistic title="售价" value={product.price} prefix="¥" valueStyle={{ color: '#00b96b', fontSize: 36 }} titleStyle={{ color: '#888' }} />
</div>
<Button type="primary" size="large" icon={<ShoppingCartOutlined />} onClick={() => setIsModalOpen(true)} style={{ height: 50, padding: '0 40px', fontSize: 18 }}>
立即购买
</Button>
</Col>
</Row>
{/* Feature Section */}
<div style={{ marginTop: 100 }}>
{product.features && product.features.length > 0 ? (
product.features.map((feature, index) => (
<div className="feature-section" key={index}>
{renderIcon(feature)}
<div className="feature-title">{feature.title}</div>
<div className="feature-desc">{feature.description}</div>
</div>
))
) : (
// Fallback content if no features are configured
<>
<div className="feature-section">
<SafetyCertificateOutlined style={{ fontSize: 60, color: '#00b96b', marginBottom: 20 }} />
<div className="feature-title">工业级安全标准</div>
<div className="feature-desc">
采用军工级加密芯片保障您的数据隐私安全无论是边缘计算还是云端同步全程加密传输 AI 应用无后顾之忧
</div>
</div>
<div className="feature-section">
<EyeOutlined style={{ fontSize: 60, color: '#1890ff', marginBottom: 20 }} />
<div className="feature-title">超清视觉感知</div>
<div className="feature-desc">
搭载 4K 高清摄像头与 AI 视觉算法实时捕捉每一个细节支持人脸识别物体检测姿态分析等多种视觉任务
</div>
</div>
<div className="feature-section">
<ThunderboltOutlined style={{ fontSize: 60, color: '#faad14', marginBottom: 20 }} />
<div className="feature-title">极致性能释放</div>
<div className="feature-desc">
{product.chip_type} 强劲核心提供高达 XX TOPS 的算力支持低功耗设计满足 24 小时全天候运行需求
</div>
</div>
</>
)}
{product.display_detail_image ? (
<div style={{ margin: '60px 0', width: '100%', overflow: 'hidden', borderRadius: 12 }}>
<img src={product.display_detail_image} alt="产品详情" style={{ width: '100%', display: 'block' }} />
</div>
) : (
<div style={{ margin: '60px 0', height: 800, background: '#111', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#333', fontSize: 24, border: '1px dashed #333' }}>
产品详情长图展示区域 (请在后台配置)
</div>
)}
</div>
{/* Order Modal */}
<Modal
title="填写收货信息"
open={isModalOpen}
onCancel={() => setIsModalOpen(false)}
footer={null}
destroyOnHidden
>
<Form
form={form}
layout="vertical"
onFinish={handleBuy}
initialValues={{ quantity: 1 }}
>
<Form.Item label="购买数量" name="quantity" rules={[{ required: true }]}>
<InputNumber min={1} max={100} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="收货人姓名" name="customer_name" rules={[{ required: true, message: '请输入姓名' }]}>
<Input placeholder="张三" />
</Form.Item>
<Form.Item label="联系电话" name="phone_number" rules={[{ required: true, message: '请输入电话' }]}>
<Input placeholder="13800000000" />
</Form.Item>
<Form.Item label="收货地址" name="shipping_address" rules={[{ required: true, message: '请输入地址' }]}>
<Input.TextArea rows={3} placeholder="北京市..." />
</Form.Item>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10, marginTop: 20 }}>
<Button onClick={() => setIsModalOpen(false)}>取消</Button>
<Button type="primary" htmlType="submit" loading={submitting}>提交订单</Button>
</div>
</Form>
</Modal>
</div>
);
};
export default ProductDetail;

View File

@@ -0,0 +1,223 @@
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Typography, Button, Spin, Empty, Descriptions, Tag, Row, Col, Modal, Form, Input, message, Statistic } from 'antd';
import { ArrowLeftOutlined, ClockCircleOutlined, GiftOutlined, ShoppingCartOutlined } from '@ant-design/icons';
import { getServiceDetail, createServiceOrder } from '../api';
import { motion } from 'framer-motion';
const { Title, Paragraph } = Typography;
const ServiceDetail = () => {
const { id } = useParams();
const navigate = useNavigate();
const [service, setService] = useState(null);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [form] = Form.useForm();
useEffect(() => {
const fetchDetail = async () => {
try {
const response = await getServiceDetail(id);
setService(response.data);
} catch (error) {
console.error("Failed to fetch service detail:", error);
} finally {
setLoading(false);
}
};
fetchDetail();
}, [id]);
const handlePurchase = async (values) => {
setSubmitting(true);
try {
const orderData = {
service: service.id,
customer_name: values.customer_name,
company_name: values.company_name,
phone_number: values.phone_number,
email: values.email,
requirements: values.requirements
};
await createServiceOrder(orderData);
message.success('需求已提交,我们的销售顾问将尽快与您联系!');
setIsModalOpen(false);
} catch (error) {
console.error(error);
message.error('提交失败,请重试');
} finally {
setSubmitting(false);
}
};
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '100px 0' }}>
<Spin size="large" tip="Loading..." />
</div>
);
}
if (!service) {
return (
<div style={{ textAlign: 'center', padding: '100px 0' }}>
<Empty description="Service not found" />
<Button type="primary" onClick={() => navigate('/services')} style={{ marginTop: 20 }}>
Return to Services
</Button>
</div>
);
}
return (
<div style={{ padding: '20px 0' }}>
<Button
type="text"
icon={<ArrowLeftOutlined />}
style={{ color: '#fff', marginBottom: 20 }}
onClick={() => navigate('/services')}
>
返回服务列表
</Button>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Row gutter={[40, 40]}>
<Col xs={24} md={16}>
<div style={{ textAlign: 'left', marginBottom: 40 }}>
<Title level={1} style={{ color: '#fff' }}>
{service.title}
</Title>
<Paragraph style={{ color: '#888', fontSize: 18 }}>
{service.description}
</Paragraph>
<div style={{ marginTop: 30, background: 'rgba(255,255,255,0.05)', padding: 20, borderRadius: 12 }}>
<Title level={4} style={{ color: '#fff' }}>服务详情</Title>
<Descriptions column={1} labelStyle={{ color: '#888' }} contentStyle={{ color: '#fff' }}>
<Descriptions.Item label={<span style={{ display: 'flex', alignItems: 'center' }}><ClockCircleOutlined style={{ marginRight: 8 }} /> 交付周期</span>}>
{service.delivery_time || '待沟通'}
</Descriptions.Item>
<Descriptions.Item label={<span style={{ display: 'flex', alignItems: 'center' }}><GiftOutlined style={{ marginRight: 8 }} /> 交付内容</span>}>
{service.delivery_content || '根据需求定制'}
</Descriptions.Item>
</Descriptions>
</div>
</div>
{service.display_detail_image ? (
<div style={{
width: '100%',
background: '#111',
borderRadius: 12,
overflow: 'hidden',
boxShadow: `0 0 30px ${service.color}22`,
border: `1px solid ${service.color}33`
}}>
<img
src={service.display_detail_image}
alt={service.title}
style={{ width: '100%', display: 'block' }}
/>
</div>
) : (
<div style={{ textAlign: 'center', padding: 100, background: '#111', borderRadius: 12, color: '#666' }}>
暂无详情图片
</div>
)}
</Col>
<Col xs={24} md={8}>
<div style={{ position: 'sticky', top: 100 }}>
<div style={{
background: '#1f1f1f',
padding: 30,
borderRadius: 16,
border: `1px solid ${service.color}44`,
boxShadow: `0 0 20px ${service.color}11`
}}>
<Title level={3} style={{ color: '#fff', marginTop: 0 }}>服务报价</Title>
<div style={{ display: 'flex', alignItems: 'baseline', marginBottom: 20 }}>
<span style={{ fontSize: 36, color: service.color, fontWeight: 'bold' }}>¥{service.price}</span>
<span style={{ color: '#888', marginLeft: 8 }}>/ {service.unit} </span>
</div>
<div style={{ marginBottom: 20 }}>
{service.features_list && service.features_list.map((feat, i) => (
<Tag color={service.color} key={i} style={{ marginBottom: 8, padding: '4px 10px' }}>
{feat}
</Tag>
))}
</div>
<Button
type="primary"
size="large"
block
icon={<ShoppingCartOutlined />}
style={{
height: 50,
background: service.color,
borderColor: service.color,
color: '#000',
fontWeight: 'bold'
}}
onClick={() => setIsModalOpen(true)}
>
立即咨询 / 购买
</Button>
<p style={{ color: '#666', marginTop: 15, fontSize: 12, textAlign: 'center' }}>
* 具体价格可能因需求复杂度而异提交需求后我们将提供详细报价单
</p>
</div>
</div>
</Col>
</Row>
</motion.div>
{/* Purchase Modal */}
<Modal
title={`咨询/购买 - ${service.title}`}
open={isModalOpen}
onCancel={() => setIsModalOpen(false)}
footer={null}
destroyOnHidden
>
<p style={{ marginBottom: 20, color: '#666' }}>请填写您的联系方式和需求我们的技术顾问将在 24 小时内与您联系</p>
<Form
form={form}
layout="vertical"
onFinish={handlePurchase}
>
<Form.Item label="您的姓名" name="customer_name" rules={[{ required: true, message: '请输入姓名' }]}>
<Input placeholder="例如:张经理" />
</Form.Item>
<Form.Item label="公司/机构名称" name="company_name">
<Input placeholder="例如:某某科技有限公司" />
</Form.Item>
<Form.Item label="联系电话" name="phone_number" rules={[{ required: true, message: '请输入电话' }]}>
<Input placeholder="13800000000" />
</Form.Item>
<Form.Item label="电子邮箱" name="email" rules={[{ type: 'email' }]}>
<Input placeholder="example@company.com" />
</Form.Item>
<Form.Item label="需求描述" name="requirements">
<Input.TextArea rows={4} placeholder="请简单描述您的业务场景或具体需求..." />
</Form.Item>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 10, marginTop: 20 }}>
<Button onClick={() => setIsModalOpen(false)}>取消</Button>
<Button type="primary" htmlType="submit" loading={submitting}>提交需求</Button>
</div>
</Form>
</Modal>
</div>
);
};
export default ServiceDetail;