diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 79ed61d..50bef69 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -7,6 +7,7 @@ import Payment from './pages/Payment';
import AIServices from './pages/AIServices';
import ServiceDetail from './pages/ServiceDetail';
import ARExperience from './pages/ARExperience';
+import MyOrders from './pages/MyOrders';
import 'antd/dist/reset.css';
import './App.css';
@@ -19,6 +20,7 @@ function App() {
} />
} />
} />
+ } />
} />
} />
diff --git a/frontend/src/api.js b/frontend/src/api.js
index 0ca8e3f..48225bf 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -2,7 +2,7 @@ import axios from 'axios';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000/api',
- timeout: 5000,
+ timeout: 8000, // 增加超时时间到 10秒
headers: {
'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 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;
diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx
index 59c82ae..ad6242d 100644
--- a/frontend/src/components/Layout.jsx
+++ b/frontend/src/components/Layout.jsx
@@ -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 { RobotOutlined, MenuOutlined, AppstoreOutlined, EyeOutlined } from '@ant-design/icons';
-import { useNavigate, useLocation } from 'react-router-dom';
+import { RobotOutlined, MenuOutlined, AppstoreOutlined, EyeOutlined, SearchOutlined } from '@ant-design/icons';
+import { useNavigate, useLocation, useSearchParams } from 'react-router-dom';
import ParticleBackground from './ParticleBackground';
import { motion, AnimatePresence } from 'framer-motion';
@@ -10,8 +10,18 @@ const { Header, Content, Footer } = AntLayout;
const Layout = ({ children }) => {
const navigate = useNavigate();
const location = useLocation();
+ const [searchParams] = useSearchParams();
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 = [
{
key: '/',
@@ -28,6 +38,11 @@ const Layout = ({ children }) => {
icon: ,
label: 'AR 体验',
},
+ {
+ key: '/my-orders',
+ icon: ,
+ label: '我的订单',
+ },
{
key: 'more',
label: '...',
diff --git a/frontend/src/pages/MyOrders.jsx b/frontend/src/pages/MyOrders.jsx
new file mode 100644
index 0000000..fe21286
--- /dev/null
+++ b/frontend/src/pages/MyOrders.jsx
@@ -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 } color="success">已支付;
+ case 'pending': return } color="warning">待支付;
+ case 'shipped': return } color="processing">已发货;
+ case 'cancelled': return } color="default">已取消;
+ default: return {status};
+ }
+ };
+
+ return (
+
+
+
+
+
我的订单查询
+ Secure Order Verification System
+
+
+ {step < 2 ? (
+
+
+
+ }
+ placeholder="请输入下单时的手机号"
+ style={{
+ background: 'rgba(255,255,255,0.05)',
+ border: '1px solid rgba(255,255,255,0.1)',
+ color: '#fff',
+ height: 50,
+ borderRadius: 8
+ }}
+ />
+
+ )}
+
+ {step === 1 && (
+ <>
+
+ 已发送验证码至 {phone}
+
+
+
+ }
+ 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'
+ }}
+ />
+
+ >
+ )}
+
+
+ }
+ 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 ? '获取验证码' : '查询订单'}
+
+
+
+
+
+ ) : (
+
+
+
+
+
+ (
+
+ showDetail(order)}
+ title={订单号: {order.id} {getStatusTag(order.status)}}
+ 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' }}
+ >
+
+
+ {order.total_price} 元
+ {new Date(order.created_at).toLocaleString()}
+
+
+
+
+ {order.config_image ? (
+
+ ) : (
+
+
+
+ )}
+
+
{order.config_name || `商品 ID: ${order.config}`}
+
数量: x{order.quantity}
+
+
+
+
+ {(order.courier_name || order.tracking_number) && (
+
+
+
+
+ 物流信息
+
+
+
+ 快递公司:
+ {order.courier_name || '未知'}
+
+
+
快递单号:
+ {order.tracking_number ? (
+
e.stopPropagation()}>
+
+ {order.tracking_number}
+
+
+ ) : (
+
暂无单号
+ )}
+
+
+
+ )}
+
+
+
+ )}
+ locale={{ emptyText: 暂无订单信息
}}
+ />
+
+ )}
+
+
订单详情}
+ open={modalVisible}
+ onCancel={() => setModalVisible(false)}
+ footer={[
+
+ ]}
+ width={600}
+ centered
+ >
+ {currentOrder && (
+
+
+ {currentOrder.id}
+
+ {currentOrder.config_name}
+ {new Date(currentOrder.created_at).toLocaleString()}
+ {new Date(currentOrder.updated_at).toLocaleString()}
+ {getStatusTag(currentOrder.status)}
+
+ ¥{currentOrder.total_price}
+
+
+
+
+ {currentOrder.customer_name}
+ {currentOrder.phone_number}
+ {currentOrder.shipping_address}
+
+
+
+ {currentOrder.salesperson_name && (
+
+
+ {currentOrder.salesperson_name}
+ {currentOrder.salesperson_code && {currentOrder.salesperson_code}}
+
+
+ )}
+
+ {(currentOrder.status === 'shipped' || currentOrder.courier_name) && (
+ <>
+ {currentOrder.courier_name || '未知'}
+
+ {currentOrder.tracking_number ? (
+
+ {currentOrder.tracking_number}
+
+ ) : '暂无单号'}
+
+ >
+ )}
+
+ )}
+
+
+
+ );
+};
+
+export default MyOrders;
diff --git a/frontend/src/pages/ProductDetail.jsx b/frontend/src/pages/ProductDetail.jsx
index 85ff18d..da032c7 100644
--- a/frontend/src/pages/ProductDetail.jsx
+++ b/frontend/src/pages/ProductDetail.jsx
@@ -1,6 +1,6 @@
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 } 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 { getConfigs, createOrder, nativePay } from '../api';
import ModelViewer from '../components/ModelViewer';
@@ -16,7 +16,12 @@ const ProductDetail = () => {
const [submitting, setSubmitting] = useState(false);
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(() => {
fetchProduct();
@@ -146,12 +151,28 @@ const ProductDetail = () => {
{product.has_microphone && 阵列麦克风}
-
+
+
-
} onClick={() => setIsModalOpen(true)} style={{ height: 50, padding: '0 40px', fontSize: 18 }}>
- 立即购买
+ {product.stock < 5 && product.stock > 0 && (
+
+ )}
+
+ {product.stock === 0 && (
+
+ )}
+
+
}
+ onClick={() => setIsModalOpen(true)}
+ disabled={product.stock === 0}
+ style={{ height: 50, padding: '0 40px', fontSize: 18 }}
+ >
+ {product.stock === 0 ? '暂时缺货' : '立即购买'}
diff --git a/frontend/src/pages/ServiceDetail.jsx b/frontend/src/pages/ServiceDetail.jsx
index fcbc1da..f3a8e19 100644
--- a/frontend/src/pages/ServiceDetail.jsx
+++ b/frontend/src/pages/ServiceDetail.jsx
@@ -16,8 +16,13 @@ const ServiceDetail = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [submitting, setSubmitting] = useState(false);
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(() => {
const fetchDetail = async () => {