328 lines
14 KiB
JavaScript
328 lines
14 KiB
JavaScript
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, Radio, Alert } from 'antd';
|
||
import { ShoppingCartOutlined, SafetyCertificateOutlined, ThunderboltOutlined, EyeOutlined, StarOutlined } from '@ant-design/icons';
|
||
import { getConfigs, createOrder, nativePay } from '../api';
|
||
import ModelViewer from '../components/ModelViewer';
|
||
import LoginModal from '../components/LoginModal';
|
||
import { useAuth } from '../context/AuthContext';
|
||
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 [loginVisible, setLoginVisible] = useState(false);
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [form] = Form.useForm();
|
||
|
||
const { user, login } = useAuth();
|
||
|
||
// 优先从 URL 获取,如果没有则从 localStorage 获取,不再默认绑定 flw666
|
||
const refCode = searchParams.get('ref') || localStorage.getItem('ref_code');
|
||
|
||
useEffect(() => {
|
||
// 自动填充用户信息
|
||
if (user) {
|
||
form.setFieldsValue({
|
||
phone_number: user.phone_number,
|
||
// 如果后端返回了地址信息,这里也可以填充
|
||
// shipping_address: user.shipping_address
|
||
});
|
||
}
|
||
}, [isModalOpen, user]); // 当弹窗打开或用户状态变化时填充
|
||
|
||
useEffect(() => {
|
||
console.log('[ProductDetail] Current ref_code:', refCode);
|
||
}, [refCode]);
|
||
|
||
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 isPickup = values.delivery_method === 'pickup';
|
||
const orderData = {
|
||
goodid: product.id,
|
||
quantity: values.quantity,
|
||
customer_name: values.customer_name,
|
||
phone_number: values.phone_number,
|
||
// 如果是自提,手动设置地址,否则使用表单中的地址
|
||
shipping_address: isPickup ? '线下自提' : values.shipping_address,
|
||
ref_code: refCode
|
||
};
|
||
|
||
console.log('提交订单数据:', orderData); // 调试日志
|
||
const response = await nativePay(orderData);
|
||
message.success('订单已创建,请完成支付');
|
||
navigate(`/payment/${response.data.order_id}`, {
|
||
state: {
|
||
codeUrl: response.data.code_url,
|
||
order_id: response.data.order_id,
|
||
orderInfo: {
|
||
...orderData,
|
||
id: response.data.order_id,
|
||
config_name: product.name,
|
||
total_price: product.price * values.quantity
|
||
}
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error(error);
|
||
message.error('创建订单失败,请检查填写信息');
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
const getModelPaths = (p) => {
|
||
if (!p) return null;
|
||
|
||
// 优先使用后台配置的 3D 模型 URL
|
||
if (p.model_3d_url) {
|
||
return { obj: p.model_3d_url };
|
||
}
|
||
|
||
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} />
|
||
) : product.static_image_url ? (
|
||
<img src={product.static_image_url} alt={product.name} style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} />
|
||
) : (
|
||
<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, display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
|
||
<Tag color="cyan" style={{ background: 'rgba(0,255,255,0.1)', border: '1px solid cyan', padding: '4px 12px', fontSize: '14px', margin: 0 }}>{product.chip_type}</Tag>
|
||
{product.has_camera && <Tag color="blue" style={{ background: 'rgba(0,0,255,0.1)', border: '1px solid blue', padding: '4px 12px', fontSize: '14px', margin: 0 }}>高清摄像头</Tag>}
|
||
{product.has_microphone && <Tag color="purple" style={{ background: 'rgba(114,46,209,0.1)', border: '1px solid #722ed1', padding: '4px 12px', fontSize: '14px', margin: 0 }}>阵列麦克风</Tag>}
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 20, marginBottom: 20 }}>
|
||
<Statistic title="售价" value={product.price} prefix="¥" valueStyle={{ color: '#00b96b', fontSize: 36 }} titleStyle={{ color: '#888' }} />
|
||
<Statistic title="库存" value={product.stock} suffix="件" valueStyle={{ color: product.stock < 5 ? '#ff4d4f' : '#fff', fontSize: 20 }} titleStyle={{ color: '#888' }} />
|
||
</div>
|
||
|
||
{product.stock < 5 && product.stock > 0 && (
|
||
<Alert message={`库存紧张,仅剩 ${product.stock} 件!`} type="warning" showIcon style={{ marginBottom: 20, background: 'rgba(250, 173, 20, 0.1)', border: '1px solid #faad14', color: '#faad14' }} />
|
||
)}
|
||
|
||
{product.stock === 0 && (
|
||
<Alert message="该商品暂时缺货" type="error" showIcon style={{ marginBottom: 20 }} />
|
||
)}
|
||
|
||
<Button
|
||
type="primary"
|
||
size="large"
|
||
icon={<ShoppingCartOutlined />}
|
||
onClick={() => {
|
||
if (!user) {
|
||
setLoginVisible(true);
|
||
} else {
|
||
setIsModalOpen(true);
|
||
}
|
||
}}
|
||
disabled={product.stock === 0}
|
||
style={{ height: 50, padding: '0 40px', fontSize: 18 }}
|
||
>
|
||
{product.stock === 0 ? '暂时缺货' : '立即购买'}
|
||
</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 auto',
|
||
maxWidth: '900px',
|
||
width: '100%',
|
||
overflow: 'hidden',
|
||
borderRadius: 12,
|
||
boxShadow: '0 10px 40px rgba(0,0,0,0.5)'
|
||
}}>
|
||
<img src={product.display_detail_image} alt="产品详情" style={{ width: '100%', display: 'block', height: 'auto' }} />
|
||
</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>
|
||
|
||
{/* Login Modal */}
|
||
<LoginModal
|
||
visible={loginVisible}
|
||
onClose={() => setLoginVisible(false)}
|
||
onLoginSuccess={(userData) => {
|
||
login(userData);
|
||
setLoginVisible(false);
|
||
setIsModalOpen(true);
|
||
}}
|
||
/>
|
||
|
||
{/* Order Modal */}
|
||
<Modal
|
||
title="填写收货信息"
|
||
open={isModalOpen}
|
||
onCancel={() => setIsModalOpen(false)}
|
||
footer={null}
|
||
destroyOnHidden
|
||
>
|
||
<Form
|
||
form={form}
|
||
layout="vertical"
|
||
onFinish={handleBuy}
|
||
initialValues={{ quantity: 1, delivery_method: 'shipping' }}
|
||
>
|
||
<Form.Item label="配送方式" name="delivery_method">
|
||
<Radio.Group buttonStyle="solid">
|
||
<Radio.Button value="shipping">快递配送</Radio.Button>
|
||
<Radio.Button value="pickup">线下自提</Radio.Button>
|
||
</Radio.Group>
|
||
</Form.Item>
|
||
<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
|
||
noStyle
|
||
shouldUpdate={(prevValues, currentValues) => prevValues.delivery_method !== currentValues.delivery_method}
|
||
>
|
||
{({ getFieldValue }) =>
|
||
getFieldValue('delivery_method') === 'shipping' ? (
|
||
<Form.Item label="收货地址" name="shipping_address" rules={[{ required: true, message: '请输入地址' }]}>
|
||
<Input.TextArea rows={3} placeholder="北京市..." />
|
||
</Form.Item>
|
||
) : (
|
||
<div style={{ marginBottom: 24, padding: '12px', background: '#f5f5f5', borderRadius: '4px', border: '1px solid #d9d9d9' }}>
|
||
<p style={{ margin: 0, color: '#666' }}>自提地址:昆明市云纺国际商厦B座1406</p>
|
||
<p style={{ margin: 0, fontSize: '12px', color: '#999' }}>请在工作日 9:00 - 18:00 期间前往提货</p>
|
||
</div>
|
||
)
|
||
}
|
||
</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;
|