fix: 3D Show
This commit is contained in:
172
frontend/src/pages/AIServices.jsx
Normal file
172
frontend/src/pages/AIServices.jsx
Normal 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;
|
||||
111
frontend/src/pages/ARExperience.jsx
Normal file
111
frontend/src/pages/ARExperience.jsx
Normal 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;
|
||||
78
frontend/src/pages/Home.css
Normal file
78
frontend/src/pages/Home.css
Normal 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
177
frontend/src/pages/Home.jsx
Normal 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;
|
||||
52
frontend/src/pages/Payment.css
Normal file
52
frontend/src/pages/Payment.css
Normal 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);
|
||||
}
|
||||
183
frontend/src/pages/Payment.jsx
Normal file
183
frontend/src/pages/Payment.jsx
Normal 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;
|
||||
33
frontend/src/pages/ProductDetail.css
Normal file
33
frontend/src/pages/ProductDetail.css
Normal 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;
|
||||
}
|
||||
232
frontend/src/pages/ProductDetail.jsx
Normal file
232
frontend/src/pages/ProductDetail.jsx
Normal 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;
|
||||
223
frontend/src/pages/ServiceDetail.jsx
Normal file
223
frontend/src/pages/ServiceDetail.jsx
Normal 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;
|
||||
Reference in New Issue
Block a user