finish
This commit is contained in:
@@ -7,6 +7,7 @@ import Payment from './pages/Payment';
|
|||||||
import AIServices from './pages/AIServices';
|
import AIServices from './pages/AIServices';
|
||||||
import ServiceDetail from './pages/ServiceDetail';
|
import ServiceDetail from './pages/ServiceDetail';
|
||||||
import ARExperience from './pages/ARExperience';
|
import ARExperience from './pages/ARExperience';
|
||||||
|
import MyOrders from './pages/MyOrders';
|
||||||
import 'antd/dist/reset.css';
|
import 'antd/dist/reset.css';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ function App() {
|
|||||||
<Route path="/services" element={<AIServices />} />
|
<Route path="/services" element={<AIServices />} />
|
||||||
<Route path="/services/:id" element={<ServiceDetail />} />
|
<Route path="/services/:id" element={<ServiceDetail />} />
|
||||||
<Route path="/ar" element={<ARExperience />} />
|
<Route path="/ar" element={<ARExperience />} />
|
||||||
|
<Route path="/my-orders" element={<MyOrders />} />
|
||||||
<Route path="/product/:id" element={<ProductDetail />} />
|
<Route path="/product/:id" element={<ProductDetail />} />
|
||||||
<Route path="/payment/:orderId" element={<Payment />} />
|
<Route path="/payment/:orderId" element={<Payment />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import axios from 'axios';
|
|||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000/api',
|
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000/api',
|
||||||
timeout: 5000,
|
timeout: 8000, // 增加超时时间到 10秒
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
}
|
}
|
||||||
@@ -21,4 +21,7 @@ export const getServiceDetail = (id) => api.get(`/services/${id}/`);
|
|||||||
export const createServiceOrder = (data) => api.post('/service-orders/', data);
|
export const createServiceOrder = (data) => api.post('/service-orders/', data);
|
||||||
export const getARServices = () => api.get('/ar/');
|
export const getARServices = () => api.get('/ar/');
|
||||||
|
|
||||||
|
export const sendSms = (data) => api.post('/auth/send-sms/', data);
|
||||||
|
export const queryMyOrders = (data) => api.post('/orders/my_orders/', data);
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Layout as AntLayout, Menu, ConfigProvider, theme, Drawer, Button } from 'antd';
|
import { Layout as AntLayout, Menu, ConfigProvider, theme, Drawer, Button } from 'antd';
|
||||||
import { RobotOutlined, MenuOutlined, AppstoreOutlined, EyeOutlined } from '@ant-design/icons';
|
import { RobotOutlined, MenuOutlined, AppstoreOutlined, EyeOutlined, SearchOutlined } from '@ant-design/icons';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
|
||||||
import ParticleBackground from './ParticleBackground';
|
import ParticleBackground from './ParticleBackground';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
|
||||||
@@ -10,8 +10,18 @@ const { Header, Content, Footer } = AntLayout;
|
|||||||
const Layout = ({ children }) => {
|
const Layout = ({ children }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
|
// 全局监听并持久化 ref 参数
|
||||||
|
useEffect(() => {
|
||||||
|
const ref = searchParams.get('ref');
|
||||||
|
if (ref) {
|
||||||
|
console.log('[Layout] Capturing sales ref code:', ref);
|
||||||
|
localStorage.setItem('ref_code', ref);
|
||||||
|
}
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
key: '/',
|
key: '/',
|
||||||
@@ -28,6 +38,11 @@ const Layout = ({ children }) => {
|
|||||||
icon: <EyeOutlined />,
|
icon: <EyeOutlined />,
|
||||||
label: 'AR 体验',
|
label: 'AR 体验',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: '/my-orders',
|
||||||
|
icon: <SearchOutlined />,
|
||||||
|
label: '我的订单',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'more',
|
key: 'more',
|
||||||
label: '...',
|
label: '...',
|
||||||
|
|||||||
342
frontend/src/pages/MyOrders.jsx
Normal file
342
frontend/src/pages/MyOrders.jsx
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Form, Input, Button, Card, List, Tag, Typography, message, Space, Statistic, Divider, Modal, Descriptions } from 'antd';
|
||||||
|
import { MobileOutlined, LockOutlined, SearchOutlined, CarOutlined, InboxOutlined, SafetyCertificateOutlined, CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined, UserOutlined, EnvironmentOutlined, PhoneOutlined } from '@ant-design/icons';
|
||||||
|
import { sendSms, queryMyOrders } from '../api';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
const { Title, Text, Paragraph } = Typography;
|
||||||
|
|
||||||
|
const MyOrders = () => {
|
||||||
|
const [step, setStep] = useState(0); // 0: Input Phone, 1: Verify Code, 2: Show Orders
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [phone, setPhone] = useState('');
|
||||||
|
const [orders, setOrders] = useState([]);
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
const [currentOrder, setCurrentOrder] = useState(null);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const showDetail = (order) => {
|
||||||
|
setCurrentOrder(order);
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendSms = async (values) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const { phone_number } = values;
|
||||||
|
await sendSms({ phone_number });
|
||||||
|
message.success('验证码已发送');
|
||||||
|
setPhone(phone_number);
|
||||||
|
setStep(1);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
message.error('发送验证码失败,请重试');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQueryOrders = async (values) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const { code } = values;
|
||||||
|
const response = await queryMyOrders({ phone_number: phone, code });
|
||||||
|
setOrders(response.data);
|
||||||
|
setStep(2);
|
||||||
|
if (response.data.length === 0) {
|
||||||
|
message.info('未查询到相关订单');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
message.error('验证失败或查询出错');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusTag = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'paid': return <Tag icon={<CheckCircleOutlined />} color="success">已支付</Tag>;
|
||||||
|
case 'pending': return <Tag icon={<ClockCircleOutlined />} color="warning">待支付</Tag>;
|
||||||
|
case 'shipped': return <Tag icon={<CarOutlined />} color="processing">已发货</Tag>;
|
||||||
|
case 'cancelled': return <Tag icon={<CloseCircleOutlined />} color="default">已取消</Tag>;
|
||||||
|
default: return <Tag>{status}</Tag>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
minHeight: '80vh',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: '20px'
|
||||||
|
}}>
|
||||||
|
<div style={{ width: '100%', maxWidth: 1200 }}>
|
||||||
|
<div style={{ textAlign: 'center', marginBottom: 40 }}>
|
||||||
|
<SafetyCertificateOutlined style={{ fontSize: 48, color: '#00b96b', marginBottom: 20 }} />
|
||||||
|
<Title level={2} style={{ color: '#fff', margin: 0, fontFamily: "'Orbitron', sans-serif" }}>我的订单查询</Title>
|
||||||
|
<Text style={{ color: '#666' }}>Secure Order Verification System</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{step < 2 ? (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
background: 'rgba(0,0,0,0.6)',
|
||||||
|
border: '1px solid rgba(0, 185, 107, 0.3)',
|
||||||
|
backdropFilter: 'blur(20px)',
|
||||||
|
borderRadius: '16px',
|
||||||
|
boxShadow: '0 8px 32px 0 rgba(0, 185, 107, 0.1)',
|
||||||
|
maxWidth: 600,
|
||||||
|
margin: '0 auto'
|
||||||
|
}}
|
||||||
|
bodyStyle={{ padding: '40px' }}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={step === 0 ? handleSendSms : handleQueryOrders}
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{step === 0 && (
|
||||||
|
<Form.Item
|
||||||
|
name="phone_number"
|
||||||
|
rules={[{ required: true, message: '请输入手机号' }, { pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<MobileOutlined style={{ color: '#00b96b', fontSize: 20 }} />}
|
||||||
|
placeholder="请输入下单时的手机号"
|
||||||
|
style={{
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
color: '#fff',
|
||||||
|
height: 50,
|
||||||
|
borderRadius: 8
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 1 && (
|
||||||
|
<>
|
||||||
|
<div style={{ textAlign: 'center', marginBottom: 30, color: '#aaa' }}>
|
||||||
|
已发送验证码至 <span style={{ color: '#00b96b', fontWeight: 'bold' }}>{phone}</span>
|
||||||
|
<Button type="link" onClick={() => setStep(0)} style={{ color: '#1890ff', marginLeft: 10 }}>修改</Button>
|
||||||
|
</div>
|
||||||
|
<Form.Item
|
||||||
|
name="code"
|
||||||
|
rules={[{ required: true, message: '请输入验证码' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<LockOutlined style={{ color: '#00b96b', fontSize: 20 }} />}
|
||||||
|
placeholder="请输入6位验证码"
|
||||||
|
maxLength={6}
|
||||||
|
style={{
|
||||||
|
background: 'rgba(255,255,255,0.05)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
color: '#fff',
|
||||||
|
height: 50,
|
||||||
|
borderRadius: 8,
|
||||||
|
textAlign: 'center',
|
||||||
|
letterSpacing: '8px',
|
||||||
|
fontSize: '20px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item style={{ marginBottom: 0, marginTop: 20 }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
block
|
||||||
|
loading={loading}
|
||||||
|
icon={<SearchOutlined />}
|
||||||
|
style={{
|
||||||
|
height: 50,
|
||||||
|
fontSize: 18,
|
||||||
|
background: 'linear-gradient(90deg, #00b96b 0%, #009456 100%)',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 8,
|
||||||
|
boxShadow: '0 4px 15px rgba(0, 185, 107, 0.3)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{step === 0 ? '获取验证码' : '查询订单'}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
|
||||||
|
<div style={{ marginBottom: 20, textAlign: 'right' }}>
|
||||||
|
<Button
|
||||||
|
onClick={() => { setStep(0); setOrders([]); form.resetFields(); }}
|
||||||
|
ghost
|
||||||
|
style={{ borderColor: '#666', color: '#888' }}
|
||||||
|
>
|
||||||
|
查询其他号码
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<List
|
||||||
|
grid={{ gutter: 24, xs: 1, sm: 1, md: 2, lg: 2, xl: 3, xxl: 3 }}
|
||||||
|
dataSource={orders}
|
||||||
|
renderItem={order => (
|
||||||
|
<List.Item>
|
||||||
|
<Card
|
||||||
|
hoverable
|
||||||
|
onClick={() => showDetail(order)}
|
||||||
|
title={<Space><span style={{ color: '#fff' }}>订单号: {order.id}</span> {getStatusTag(order.status)}</Space>}
|
||||||
|
style={{
|
||||||
|
background: 'rgba(0,0,0,0.6)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
marginBottom: 10,
|
||||||
|
backdropFilter: 'blur(10px)'
|
||||||
|
}}
|
||||||
|
headStyle={{ borderBottom: '1px solid rgba(255,255,255,0.1)' }}
|
||||||
|
bodyStyle={{ padding: '20px' }}
|
||||||
|
>
|
||||||
|
<div style={{ color: '#ccc' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10 }}>
|
||||||
|
<Text strong style={{ color: '#00b96b', fontSize: 16 }}>{order.total_price} 元</Text>
|
||||||
|
<Text style={{ color: '#888' }}>{new Date(order.created_at).toLocaleString()}</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ background: 'rgba(255,255,255,0.05)', padding: 15, borderRadius: 8, marginBottom: 15 }}>
|
||||||
|
<Space align="center" size="middle">
|
||||||
|
{order.config_image ? (
|
||||||
|
<img
|
||||||
|
src={order.config_image}
|
||||||
|
alt={order.config_name}
|
||||||
|
style={{ width: 60, height: 60, objectFit: 'cover', borderRadius: 8, border: '1px solid rgba(255,255,255,0.1)' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
background: 'rgba(24,144,255,0.1)',
|
||||||
|
borderRadius: 8,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
border: '1px solid rgba(24,144,255,0.2)'
|
||||||
|
}}>
|
||||||
|
<InboxOutlined style={{ fontSize: 24, color: '#1890ff' }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<div style={{ color: '#fff', fontSize: 16, fontWeight: '500', marginBottom: 4 }}>{order.config_name || `商品 ID: ${order.config}`}</div>
|
||||||
|
<div style={{ color: '#888' }}>数量: <span style={{ color: '#00b96b' }}>x{order.quantity}</span></div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(order.courier_name || order.tracking_number) && (
|
||||||
|
<div style={{ background: 'rgba(24,144,255,0.1)', padding: 15, borderRadius: 8, border: '1px solid rgba(24,144,255,0.3)' }}>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<Space>
|
||||||
|
<CarOutlined style={{ color: '#1890ff', fontSize: 18 }} />
|
||||||
|
<Text style={{ color: '#fff', fontSize: 16 }}>物流信息</Text>
|
||||||
|
</Space>
|
||||||
|
<Divider style={{ margin: '8px 0', borderColor: 'rgba(255,255,255,0.1)' }} />
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<span style={{ color: '#aaa' }}>快递公司:</span>
|
||||||
|
<span style={{ color: '#fff' }}>{order.courier_name || '未知'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<span style={{ color: '#aaa' }}>快递单号:</span>
|
||||||
|
{order.tracking_number ? (
|
||||||
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
|
<Paragraph
|
||||||
|
copyable={{ text: order.tracking_number, tooltips: ['复制', '已复制'] }}
|
||||||
|
style={{ color: '#fff', fontFamily: 'monospace', fontSize: 16, margin: 0 }}
|
||||||
|
>
|
||||||
|
{order.tracking_number}
|
||||||
|
</Paragraph>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span style={{ color: '#fff', fontFamily: 'monospace', fontSize: 16 }}>暂无单号</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
locale={{ emptyText: <div style={{ color: '#888', padding: 40, textAlign: 'center' }}>暂无订单信息</div> }}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={<Title level={4} style={{ margin: 0 }}>订单详情</Title>}
|
||||||
|
open={modalVisible}
|
||||||
|
onCancel={() => setModalVisible(false)}
|
||||||
|
footer={[
|
||||||
|
<Button key="close" onClick={() => setModalVisible(false)}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
width={600}
|
||||||
|
centered
|
||||||
|
>
|
||||||
|
{currentOrder && (
|
||||||
|
<Descriptions column={1} bordered size="middle" labelStyle={{ width: '140px', fontWeight: 'bold' }}>
|
||||||
|
<Descriptions.Item label="订单号">
|
||||||
|
<Paragraph copyable={{ text: currentOrder.id }} style={{ marginBottom: 0 }}>{currentOrder.id}</Paragraph>
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="商品名称">{currentOrder.config_name}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="下单时间">{new Date(currentOrder.created_at).toLocaleString()}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="状态更新时间">{new Date(currentOrder.updated_at).toLocaleString()}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="当前状态">{getStatusTag(currentOrder.status)}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="订单总价">
|
||||||
|
<Text strong style={{ color: '#00b96b' }}>¥{currentOrder.total_price}</Text>
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
<Descriptions.Item label="收件人信息">
|
||||||
|
<Space direction="vertical" size={0}>
|
||||||
|
<Space><UserOutlined /> {currentOrder.customer_name}</Space>
|
||||||
|
<Space><PhoneOutlined /> {currentOrder.phone_number}</Space>
|
||||||
|
<Space align="start"><EnvironmentOutlined /> {currentOrder.shipping_address}</Space>
|
||||||
|
</Space>
|
||||||
|
</Descriptions.Item>
|
||||||
|
|
||||||
|
{currentOrder.salesperson_name && (
|
||||||
|
<Descriptions.Item label="订单推荐员">
|
||||||
|
<Space>
|
||||||
|
{currentOrder.salesperson_name}
|
||||||
|
{currentOrder.salesperson_code && <Tag color="blue">{currentOrder.salesperson_code}</Tag>}
|
||||||
|
</Space>
|
||||||
|
</Descriptions.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(currentOrder.status === 'shipped' || currentOrder.courier_name) && (
|
||||||
|
<>
|
||||||
|
<Descriptions.Item label="快递公司">{currentOrder.courier_name || '未知'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="快递单号">
|
||||||
|
{currentOrder.tracking_number ? (
|
||||||
|
<Paragraph copyable={{ text: currentOrder.tracking_number }} style={{ marginBottom: 0 }}>
|
||||||
|
{currentOrder.tracking_number}
|
||||||
|
</Paragraph>
|
||||||
|
) : '暂无单号'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Descriptions>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MyOrders;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { Button, Row, Col, Tag, Statistic, Modal, Form, Input, InputNumber, message, Spin, Descriptions, Radio } from 'antd';
|
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 { ShoppingCartOutlined, SafetyCertificateOutlined, ThunderboltOutlined, EyeOutlined, StarOutlined } from '@ant-design/icons';
|
||||||
import { getConfigs, createOrder, nativePay } from '../api';
|
import { getConfigs, createOrder, nativePay } from '../api';
|
||||||
import ModelViewer from '../components/ModelViewer';
|
import ModelViewer from '../components/ModelViewer';
|
||||||
@@ -16,7 +16,12 @@ const ProductDetail = () => {
|
|||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const refCode = searchParams.get('ref') || 'flw666';
|
// 优先从 URL 获取,如果没有则从 localStorage 获取,不再默认绑定 flw666
|
||||||
|
const refCode = searchParams.get('ref') || localStorage.getItem('ref_code');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('[ProductDetail] Current ref_code:', refCode);
|
||||||
|
}, [refCode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchProduct();
|
fetchProduct();
|
||||||
@@ -146,12 +151,28 @@ const ProductDetail = () => {
|
|||||||
{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>}
|
{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>
|
||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 20, marginBottom: 40 }}>
|
<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.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>
|
</div>
|
||||||
|
|
||||||
<Button type="primary" size="large" icon={<ShoppingCartOutlined />} onClick={() => setIsModalOpen(true)} style={{ height: 50, padding: '0 40px', fontSize: 18 }}>
|
{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={() => setIsModalOpen(true)}
|
||||||
|
disabled={product.stock === 0}
|
||||||
|
style={{ height: 50, padding: '0 40px', fontSize: 18 }}
|
||||||
|
>
|
||||||
|
{product.stock === 0 ? '暂时缺货' : '立即购买'}
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -16,8 +16,13 @@ const ServiceDetail = () => {
|
|||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
// 优先从 URL 获取,如果没有则从 localStorage 获取
|
||||||
|
const refCode = searchParams.get('ref') || localStorage.getItem('ref_code');
|
||||||
|
|
||||||
const refCode = searchParams.get('ref') || 'flw666';
|
useEffect(() => {
|
||||||
|
console.log('[ServiceDetail] Current ref_code:', refCode);
|
||||||
|
}, [refCode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchDetail = async () => {
|
const fetchDetail = async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user