389 lines
20 KiB
JavaScript
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;
|