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 ? ( + + +
+ {step === 0 && ( + + } + 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' + }} + /> + + + )} + + + + +
+
+
+ ) : ( + +
+ +
+ + ( + + 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} + ) : ( +
+ +
+ )} +
+
{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 && 阵列麦克风} -
+
+
- 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 () => {