This commit is contained in:
jeremygan2021
2026-02-11 00:26:27 +08:00
parent 5232ab9960
commit 61afc52ac2
6 changed files with 398 additions and 10 deletions

View File

@@ -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>

View File

@@ -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;

View File

@@ -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: '...',

View 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;

View File

@@ -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>

View File

@@ -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 () => {