Files
market_page/frontend/src/pages/MyOrders.jsx
jeremygan2021 fd33201793
All checks were successful
Deploy to Server / deploy (push) Successful in 37s
移动端
2026-02-24 16:09:07 +08:00

389 lines
20 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { Form, Input, Button, Card, List, Tag, Typography, message, Space, Statistic, Divider, Modal, Descriptions, Tabs } from 'antd';
import { MobileOutlined, LockOutlined, SearchOutlined, CarOutlined, InboxOutlined, SafetyCertificateOutlined, CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined, UserOutlined, EnvironmentOutlined, PhoneOutlined, CalendarOutlined } from '@ant-design/icons';
import { getMySignups } from '../api';
import { motion } from 'framer-motion';
import LoginModal from '../components/LoginModal';
import { useAuth } from '../context/AuthContext';
import { useNavigate } from 'react-router-dom';
const { Title, Text, Paragraph } = Typography;
const MyOrders = () => {
const [loading, setLoading] = useState(false);
const [orders, setOrders] = useState([]);
const [activities, setActivities] = useState([]);
const [modalVisible, setModalVisible] = useState(false);
const [currentOrder, setCurrentOrder] = useState(null);
const [signupInfoModalVisible, setSignupInfoModalVisible] = useState(false);
const [currentSignupInfo, setCurrentSignupInfo] = useState(null);
const [loginVisible, setLoginVisible] = useState(false);
const navigate = useNavigate();
const { user, login } = useAuth();
useEffect(() => {
if (user) {
handleQueryData();
}
}, [user]);
const showDetail = (order) => {
setCurrentOrder(order);
setModalVisible(true);
};
const showSignupInfo = (info) => {
setCurrentSignupInfo(info);
setSignupInfoModalVisible(true);
};
const handleQueryData = async () => {
setLoading(true);
try {
const { default: api } = await import('../api');
// Parallel fetch
const [ordersRes, activitiesRes] = await Promise.allSettled([
api.get('/orders/'),
getMySignups()
]);
if (ordersRes.status === 'fulfilled') {
setOrders(ordersRes.value.data);
}
if (activitiesRes.status === 'fulfilled') {
setActivities(activitiesRes.value.data);
}
} 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>;
}
};
const getOrderTitle = (order) => {
if (order.config_name) return order.config_name;
if (order.course_title) return order.course_title;
if (order.activity_title) return order.activity_title;
// Fallback to ID if no name/title
if (order.config) return `硬件 ID: ${order.config}`;
if (order.course) return `课程 ID: ${order.course}`;
if (order.activity) return `活动 ID: ${order.activity}`;
return '未知商品';
};
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>
{!user ? (
<div style={{ textAlign: 'center', padding: 40, background: 'rgba(0,0,0,0.5)', borderRadius: 16 }}>
<Text style={{ color: '#fff', fontSize: 18, display: 'block', marginBottom: 20 }}>请先登录以查看您的订单</Text>
<Button type="primary" size="large" onClick={() => setLoginVisible(true)}>立即登录</Button>
</div>
) : (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
<div style={{ marginBottom: 20, textAlign: 'right', color: '#fff' }}>
当前登录用户: <span style={{ color: '#00b96b', fontWeight: 'bold', marginRight: 10 }}>{user.nickname}</span>
<Button
onClick={handleQueryData}
loading={loading}
icon={<SearchOutlined />}
>
刷新
</Button>
</div>
<Tabs defaultActiveKey="1" items={[
{
key: '1',
label: <span style={{ fontSize: 16 }}>我的订单</span>,
children: (
<List
grid={{ gutter: 24, xs: 1, sm: 1, md: 2, lg: 2, xl: 3, xxl: 3 }}
dataSource={orders}
loading={loading}
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={getOrderTitle(order)}
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 }}>{getOrderTitle(order)}</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> }}
/>
)
},
{
key: '2',
label: <span style={{ fontSize: 16 }}>我的活动</span>,
children: (
<List
grid={{ gutter: 24, xs: 1, sm: 1, md: 2, lg: 2, xl: 3, xxl: 3 }}
dataSource={activities}
loading={loading}
renderItem={item => {
const activity = item.activity_info || item.activity || item;
return (
<List.Item>
<Card
hoverable
onClick={() => navigate(`/activity/${activity.id}`)}
cover={
<div style={{ height: 160, overflow: 'hidden' }}>
<img
alt={activity.title}
src={activity.cover_image || activity.banner_url || 'https://via.placeholder.com/400x200'}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</div>
}
style={{
background: 'rgba(0,0,0,0.6)',
border: '1px solid rgba(255,255,255,0.1)',
marginBottom: 10,
backdropFilter: 'blur(10px)',
overflow: 'hidden'
}}
headStyle={{ borderBottom: '1px solid rgba(255,255,255,0.1)' }}
bodyStyle={{ padding: '16px' }}
>
<div style={{ color: '#ccc' }}>
<Title level={4} style={{ color: '#fff', marginBottom: 10, fontSize: 18 }} ellipsis={{ rows: 1 }}>{activity.title}</Title>
<div style={{ marginBottom: 12 }}>
<Space>
<CalendarOutlined style={{ color: '#00b96b' }} />
<Text style={{ color: '#bbb' }}>{new Date(activity.start_time).toLocaleDateString()}</Text>
</Space>
</div>
<div style={{ marginBottom: 12 }}>
<Space>
<EnvironmentOutlined style={{ color: '#00f0ff' }} />
<Text style={{ color: '#bbb' }} ellipsis>{activity.location || '线上活动'}</Text>
</Space>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 16 }}>
<Tag color="blue">{activity.status || '已报名'}</Tag>
<Space>
{item.signup_info && Object.keys(item.signup_info).length > 0 && (
<Button size="small" onClick={(e) => { e.stopPropagation(); showSignupInfo(item.signup_info); }}>
查看报名信息
</Button>
)}
<Button type="primary" size="small" ghost>查看详情</Button>
</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="商品名称">{getOrderTitle(currentOrder)}</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>
<LoginModal
visible={loginVisible}
onClose={() => setLoginVisible(false)}
onLoginSuccess={(userData) => {
login(userData);
if (userData.phone_number) {
handleQueryData();
}
}}
/>
<Modal
title="报名信息详情"
open={signupInfoModalVisible}
onCancel={() => setSignupInfoModalVisible(false)}
footer={[
<Button key="close" onClick={() => setSignupInfoModalVisible(false)}>
关闭
</Button>
]}
>
{currentSignupInfo && (
<Descriptions column={1} bordered>
{Object.entries(currentSignupInfo).map(([key, value]) => (
<Descriptions.Item label={key} key={key}>
{typeof value === 'object' ? JSON.stringify(value) : String(value)}
</Descriptions.Item>
))}
</Descriptions>
)}
</Modal>
</div>
</div>
);
};
export default MyOrders;